Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
4d040b7057 build(deps): bump actions/setup-python from 4 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-11 06:05:08 +00:00
114 changed files with 7233 additions and 12628 deletions

View File

@@ -33,7 +33,7 @@ jobs:
- name: Run simulator image
run: docker run --name simulator --network=host hwi/ledger_emulator &
- name: Install Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install python dependencies

View File

@@ -12,7 +12,7 @@ jobs:
rust:
- version: stable
clippy: true
- version: 1.63.0 # MSRV
- version: 1.57.0 # MSRV
features:
- --no-default-features
- --all-features
@@ -28,13 +28,28 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- name: Pin dependencies for MSRV
if: matrix.rust.version == '1.63.0'
if: matrix.rust.version == '1.57.0'
run: |
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
cargo update -p time --precise "0.3.20"
cargo update -p home --precise "0.5.5"
cargo update -p proptest --precise "1.2.0"
cargo update -p url --precise "2.5.0"
cargo update -p log --precise "0.4.18"
cargo update -p tempfile --precise "3.6.0"
cargo update -p reqwest --precise "0.11.18"
cargo update -p hyper-rustls --precise 0.24.0
cargo update -p rustls:0.21.9 --precise "0.21.1"
cargo update -p rustls:0.20.9 --precise "0.20.8"
cargo update -p tokio --precise "1.29.1"
cargo update -p tokio-util --precise "0.7.8"
cargo update -p flate2 --precise "1.0.26"
cargo update -p h2 --precise "0.3.20"
cargo update -p rustls-webpki:0.100.3 --precise "0.100.1"
cargo update -p rustls-webpki:0.101.7 --precise "0.101.1"
cargo update -p zip --precise "0.6.2"
cargo update -p time --precise "0.3.13"
cargo update -p byteorder --precise "1.4.3"
cargo update -p webpki --precise "0.22.2"
cargo update -p os_str_bytes --precise 6.5.1
cargo update -p sct --precise 0.7.0
cargo update -p cc --precise "1.0.81"
cargo update -p jobserver --precise "0.1.26"
- name: Build
run: cargo build ${{ matrix.features }}
- name: Test
@@ -58,15 +73,15 @@ jobs:
- name: Check bdk_chain
working-directory: ./crates/chain
# TODO "--target thumbv6m-none-eabi" should work but currently does not
run: cargo check --no-default-features --features miniscript/no-std,hashbrown
- name: Check bdk wallet
working-directory: ./crates/wallet
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,hashbrown
- name: Check bdk
working-directory: ./crates/bdk
# TODO "--target thumbv6m-none-eabi" should work but currently does not
run: cargo check --no-default-features --features miniscript/no-std,bdk_chain/hashbrown
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
- name: Check esplora
working-directory: ./crates/esplora
# TODO "--target thumbv6m-none-eabi" should work but currently does not
run: cargo check --no-default-features --features miniscript/no-std,bdk_chain/hashbrown
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
check-wasm:
name: Check WASM
@@ -90,12 +105,12 @@ jobs:
target: "wasm32-unknown-unknown"
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- name: Check bdk wallet
working-directory: ./crates/wallet
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
- name: Check bdk
working-directory: ./crates/bdk
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
- name: Check esplora
working-directory: ./crates/esplora
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,async
fmt:
name: Rust fmt
@@ -119,7 +134,9 @@ jobs:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.78.0
# we pin clippy instead of using "stable" so that our CI doesn't break
# at each new cargo release
toolchain: "1.67.0"
components: clippy
override: true
- name: Rust Cache

View File

@@ -10,7 +10,7 @@ jobs:
- name: Checkout sources
uses: actions/checkout@v2
- name: Set default toolchain
run: rustup default nightly-2024-05-12
run: rustup default nightly-2022-12-14
- name: Set profile
run: rustup set profile minimal
- name: Update toolchain

1
.gitignore vendored
View File

@@ -7,4 +7,3 @@ Cargo.lock
# Example persisted files.
*.db
*.sqlite*

View File

@@ -1,15 +1,12 @@
[workspace]
resolver = "2"
members = [
"crates/wallet",
"crates/bdk",
"crates/chain",
"crates/file_store",
"crates/sqlite",
"crates/electrum",
"crates/esplora",
"crates/bitcoind_rpc",
"crates/hwi",
"crates/testenv",
"example-crates/example_cli",
"example-crates/example_electrum",
"example-crates/example_esplora",
@@ -17,7 +14,6 @@ members = [
"example-crates/wallet_electrum",
"example-crates/wallet_esplora_blocking",
"example-crates/wallet_esplora_async",
"example-crates/wallet_rpc",
"nursery/tmp_plan",
"nursery/coin_select"
]

View File

@@ -10,19 +10,19 @@
</p>
<p>
<a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.svg"/></a>
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
<a href="https://docs.rs/bdk_wallet"><img alt="Wallet API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
<a href="https://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html"><img alt="Rustc Version 1.57.0+" src="https://img.shields.io/badge/rustc-1.57.0%2B-lightgrey.svg"/></a>
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
</p>
<h4>
<a href="https://bitcoindevkit.org">Project Homepage</a>
<span> | </span>
<a href="https://docs.rs/bdk_wallet">Documentation</a>
<a href="https://docs.rs/bdk">Documentation</a>
</h4>
</div>
@@ -39,18 +39,15 @@ It is built upon the excellent [`rust-bitcoin`] and [`rust-miniscript`] crates.
The project is split up into several crates in the `/crates` directory:
- [`wallet`](./crates/wallet): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
- [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
- [`chain`](./crates/chain): Tools for storing and indexing chain data
- [`persist`](./crates/persist): Types that define data persistence of a BDK wallet
- [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file.
- [`esplora`](./crates/esplora): Extends the [`esplora-client`] crate with methods to fetch chain data from an esplora HTTP server in the form that [`bdk_chain`] and `Wallet` can consume.
- [`electrum`](./crates/electrum): Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume.
Fully working examples of how to use these components are in `/example-crates`:
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk_wallet `Wallet`.
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk_wallet` library.
- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk_wallet` library.
- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk_wallet` library.
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk `Wallet`.
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk` library.
- [`wallet_esplora_blocking`](./example-crates/wallet_esplora_blocking): Uses the `Wallet` to sync and spend using the Esplora blocking interface.
- [`wallet_esplora_async`](./example-crates/wallet_esplora_async): Uses the `Wallet` to sync and spend using the Esplora asynchronous interface.
- [`wallet_electrum`](./example-crates/wallet_electrum): Uses the `Wallet` to sync and spend using Electrum.
@@ -63,16 +60,51 @@ Fully working examples of how to use these components are in `/example-crates`:
[`bdk_chain`]: https://docs.rs/bdk-chain/
## Minimum Supported Rust Version (MSRV)
This library should compile with any combination of features with Rust 1.63.0.
This library should compile with any combination of features with Rust 1.57.0.
To build with the MSRV you will need to pin dependencies as follows:
```shell
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
cargo update -p time --precise "0.3.20"
cargo update -p home --precise "0.5.5"
cargo update -p proptest --precise "1.2.0"
cargo update -p url --precise "2.5.0"
# log 0.4.19 has MSRV 1.60.0+
cargo update -p log --precise "0.4.18"
# tempfile 3.7.0 has MSRV 1.63.0+
cargo update -p tempfile --precise "3.6.0"
# reqwest 0.11.19 has MSRV 1.63.0+
cargo update -p reqwest --precise "0.11.18"
# hyper-rustls 0.24.1 has MSRV 1.60.0+
cargo update -p hyper-rustls --precise 0.24.0
# rustls 0.21.7 has MSRV 1.60.0+
cargo update -p rustls:0.21.9 --precise "0.21.1"
# rustls 0.20.9 has MSRV 1.60.0+
cargo update -p rustls:0.20.9 --precise "0.20.8"
# tokio 1.33 has MSRV 1.63.0+
cargo update -p tokio --precise "1.29.1"
# tokio-util 0.7.9 doesn't build with MSRV 1.57.0
cargo update -p tokio-util --precise "0.7.8"
# flate2 1.0.27 has MSRV 1.63.0+
cargo update -p flate2 --precise "1.0.26"
# h2 0.3.21 has MSRV 1.63.0+
cargo update -p h2 --precise "0.3.20"
# rustls-webpki 0.100.3 has MSRV 1.60.0+
cargo update -p rustls-webpki:0.100.3 --precise "0.100.1"
# rustls-webpki 0.101.2 has MSRV 1.60.0+
cargo update -p rustls-webpki:0.101.7 --precise "0.101.1"
# zip 0.6.6 has MSRV 1.59.0+
cargo update -p zip --precise "0.6.2"
# time 0.3.14 has MSRV 1.59.0+
cargo update -p time --precise "0.3.13"
# byteorder 1.5.0 has MSRV 1.60.0+
cargo update -p byteorder --precise "1.4.3"
# webpki 0.22.4 requires `ring:0.17.2` which has MSRV 1.61.0+
cargo update -p webpki --precise "0.22.2"
# os_str_bytes 6.6.0 has MSRV 1.61.0+
cargo update -p os_str_bytes --precise 6.5.1
# sct 0.7.1 has MSRV 1.61.0+
cargo update -p sct --precise 0.7.0
# cc 1.0.82 has MSRV 1.61.0+
cargo update -p cc --precise "1.0.81"
# jobserver 0.1.27 has MSRV 1.66.0+
cargo update -p jobserver --precise "0.1.26"
```
## License

View File

@@ -1 +1 @@
msrv="1.63.0"
msrv="1.57.0"

View File

@@ -1,7 +1,7 @@
[package]
name = "bdk_wallet"
name = "bdk"
homepage = "https://bitcoindevkit.org"
version = "1.0.0-alpha.13"
version = "1.0.0-alpha.2"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk"
description = "A modern, lightweight, descriptor-based wallet library"
@@ -10,18 +10,19 @@ readme = "README.md"
license = "MIT OR Apache-2.0"
authors = ["Bitcoin Dev Kit Developers"]
edition = "2021"
rust-version = "1.63"
rust-version = "1.57"
[dependencies]
rand = "^0.8"
miniscript = { version = "12.0.0", features = ["serde"], default-features = false }
bitcoin = { version = "0.32.0", features = ["serde", "base64", "rand-std"], default-features = false }
miniscript = { version = "10.0.0", features = ["serde"], default-features = false }
bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false }
bdk_chain = { path = "../chain", version = "0.6.0", features = ["miniscript", "serde"], default-features = false }
# Optional dependencies
bip39 = { version = "2.0", optional = true }
hwi = { version = "0.7.0", optional = true, features = [ "miniscript"] }
bip39 = { version = "1.0.1", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = "0.2"
@@ -33,6 +34,8 @@ std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"]
hardware-signer = ["hwi"]
test-hardware-signer = ["hardware-signer"]
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
@@ -43,7 +46,6 @@ dev-getrandom-wasm = ["getrandom/js"]
lazy_static = "1.4"
assert_matches = "1.5.0"
tempfile = "3"
bdk_sqlite = { path = "../sqlite" }
bdk_file_store = { path = "../file_store" }
anyhow = "1"

View File

@@ -8,25 +8,25 @@
</p>
<p>
<a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.svg"/></a>
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
<a href="https://docs.rs/bdk_wallet"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
<a href="https://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html"><img alt="Rustc Version 1.57.0+" src="https://img.shields.io/badge/rustc-1.57.0%2B-lightgrey.svg"/></a>
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
</p>
<h4>
<a href="https://bitcoindevkit.org">Project Homepage</a>
<span> | </span>
<a href="https://docs.rs/bdk_wallet">Documentation</a>
<a href="https://docs.rs/bdk">Documentation</a>
</h4>
</div>
# BDK Wallet
## `bdk`
The `bdk_wallet` crate provides the [`Wallet`] type which is a simple, high-level
The `bdk` crate provides the [`Wallet`](`crate::Wallet`) type which is a simple, high-level
interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
for many simple applications as well as a good demonstration of how to use the other mechanisms to
construct a wallet. It has two keychains (external and internal) which are defined by
@@ -34,78 +34,64 @@ construct a wallet. It has two keychains (external and internal) which are defin
chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
can create and sign transactions.
For details about the API of `Wallet` see the [module-level documentation][`Wallet`].
For more information, see the [`Wallet`'s documentation](https://docs.rs/bdk/latest/bdk/wallet/struct.Wallet.html).
## Blockchain data
### Blockchain data
In order to get blockchain data for `Wallet` to consume, you should configure a client from
an available chain source. Typically you make a request to the chain source and get a response
that the `Wallet` can use to update its view of the chain.
In order to get blockchain data for `Wallet` to consume, you have to put it into particular form.
Right now this is [`KeychainScan`] which is defined in [`bdk_chain`].
This can be created manually or from blockchain-scanning crates.
**Blockchain Data Sources**
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures.
**Examples**
* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async)
* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking)
* [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora)
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc)
## Persistence
### Persistence
To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::CombinedChangeSet`].
To persist the `Wallet` on disk, `Wallet` needs to be constructed with a
[`Persist`](https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainPersist.html) implementation.
**Implementations**
* [`bdk_file_store`]: Stores wallet changes in a simple flat file.
* [`bdk_sqlite`]: Stores wallet changes in a SQLite relational database file.
* [`bdk_file_store`]: a simple flat-file implementation of `Persist`.
**Example**
<!-- compile_fail because outpoint and txout are fake variables -->
```rust,no_run
use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
```rust
use bdk::{bitcoin::Network, wallet::{AddressIndex, Wallet}};
fn main() {
// Open or create a new file store for wallet data.
let mut db =
bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db")
.expect("create store");
// a type that implements `Persist`
let db = ();
// Create a wallet with initial wallet data read from the file store.
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
let changeset = db.aggregate_changesets().expect("changeset loaded");
let mut wallet =
Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet)
.expect("create or load wallet");
let descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet).expect("should create");
// Get a new address to receive bitcoin.
let receive_address = wallet.reveal_next_address(KeychainKind::External);
// Persist staged wallet data changes to the file store.
let staged_changeset = wallet.take_staged();
if let Some(changeset) = staged_changeset {
db.append_changeset(&changeset)
.expect("must commit changes to database");
}
println!("Your new receive address is: {}", receive_address.address);
// get a new address (this increments revealed derivation index)
println!("revealed address: {}", wallet.get_address(AddressIndex::New));
println!("staged changes: {:?}", wallet.staged());
// persist changes
wallet.commit().expect("must save");
}
```
<!-- ### Sync the balance of a descriptor -->
<!-- ```rust,no_run -->
<!-- use bdk_wallet::Wallet; -->
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
<!-- use bdk_wallet::SyncOptions; -->
<!-- use bdk_wallet::electrum_client::Client; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- use bdk::Wallet; -->
<!-- use bdk::blockchain::ElectrumBlockchain; -->
<!-- use bdk::SyncOptions; -->
<!-- use bdk::electrum_client::Client; -->
<!-- use bdk::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
<!-- let wallet = Wallet::new( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
@@ -115,7 +101,7 @@ fn main() {
<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
<!-- println!("Descriptor balance: {} SAT", wallet.balance()?); -->
<!-- println!("Descriptor balance: {} SAT", wallet.get_balance()?); -->
<!-- Ok(()) -->
<!-- } -->
@@ -123,12 +109,12 @@ fn main() {
<!-- ### Generate a few addresses -->
<!-- ```rust -->
<!-- use bdk_wallet::Wallet; -->
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- use bdk::Wallet; -->
<!-- use bdk::wallet::AddressIndex::New; -->
<!-- use bdk::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
<!-- let wallet = Wallet::new( -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- let wallet = Wallet::new_no_persist( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
<!-- Network::Testnet, -->
@@ -145,19 +131,19 @@ fn main() {
<!-- ### Create a transaction -->
<!-- ```rust,no_run -->
<!-- use bdk_wallet::{FeeRate, Wallet, SyncOptions}; -->
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
<!-- use bdk::{FeeRate, Wallet, SyncOptions}; -->
<!-- use bdk::blockchain::ElectrumBlockchain; -->
<!-- use bdk_wallet::electrum_client::Client; -->
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
<!-- use bdk::electrum_client::Client; -->
<!-- use bdk::wallet::AddressIndex::New; -->
<!-- use bitcoin::base64; -->
<!-- use bdk_wallet::bitcoin::consensus::serialize; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- use bdk::bitcoin::consensus::serialize; -->
<!-- use bdk::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
<!-- let wallet = Wallet::new( -->
<!-- let wallet = Wallet::new_no_persist( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
<!-- Network::Testnet, -->
@@ -186,14 +172,14 @@ fn main() {
<!-- ### Sign a transaction -->
<!-- ```rust,no_run -->
<!-- use bdk_wallet::{Wallet, SignOptions}; -->
<!-- use bdk::{Wallet, SignOptions}; -->
<!-- use bitcoin::base64; -->
<!-- use bdk_wallet::bitcoin::consensus::deserialize; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- use bdk::bitcoin::consensus::deserialize; -->
<!-- use bdk::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
<!-- let wallet = Wallet::new( -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- let wallet = Wallet::new_no_persist( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", -->
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), -->
<!-- Network::Testnet, -->
@@ -216,7 +202,7 @@ fn main() {
cargo test
```
# License
## License
Licensed under either of
@@ -225,18 +211,16 @@ Licensed under either of
at your option.
# Contribution
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the Apache-2.0
license, shall be dual licensed as above, without any additional terms or
conditions.
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
[`bdk_sqlite`]: https://docs.rs/bdk_sqlite/latest
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
[`KeychainScan`]: https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainScan.html
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html

View File

@@ -0,0 +1,66 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
extern crate bdk;
extern crate bitcoin;
extern crate miniscript;
extern crate serde_json;
use std::error::Error;
use std::str::FromStr;
use bitcoin::Network;
use miniscript::policy::Concrete;
use miniscript::Descriptor;
use bdk::wallet::AddressIndex::New;
use bdk::{KeychainKind, Wallet};
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
/// can be derived from the policy.
///
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
fn main() -> Result<(), Box<dyn Error>> {
// We start with a generic miniscript policy string
let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))";
println!("Compiling policy: \n{}", policy_str);
// Parse the string as a [`Concrete`] type miniscript policy.
let policy = Concrete::<String>::from_str(policy_str)?;
// Create a `wsh` type descriptor from the policy.
// `policy.compile()` returns the resulting miniscript from the policy.
let descriptor = Descriptor::new_wsh(policy.compile()?)?;
println!("Compiled into following Descriptor: \n{}", descriptor);
// Create a new wallet from this descriptor
let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?;
println!(
"First derived address from the descriptor: \n{}",
wallet.get_address(New)
);
// BDK also has it's own `Policy` structure to represent the spending condition in a more
// human readable json format.
let spending_policy = wallet.policies(KeychainKind::External)?;
println!(
"The BDK spending policy: \n{}",
serde_json::to_string_pretty(&spending_policy)?
);
Ok(())
}

View File

@@ -7,14 +7,14 @@
// licenses.
use anyhow::anyhow;
use bdk_wallet::bitcoin::bip32::DerivationPath;
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::descriptor;
use bdk_wallet::descriptor::IntoWalletDescriptor;
use bdk_wallet::keys::bip39::{Language, Mnemonic, WordCount};
use bdk_wallet::keys::{GeneratableKey, GeneratedKey};
use bdk_wallet::miniscript::Tap;
use bdk::bitcoin::bip32::DerivationPath;
use bdk::bitcoin::secp256k1::Secp256k1;
use bdk::bitcoin::Network;
use bdk::descriptor;
use bdk::descriptor::IntoWalletDescriptor;
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
use bdk::keys::{GeneratableKey, GeneratedKey};
use bdk::miniscript::Tap;
use std::str::FromStr;
/// This example demonstrates how to generate a mnemonic phrase
@@ -33,8 +33,8 @@ fn main() -> Result<(), anyhow::Error> {
let mnemonic_with_passphrase = (mnemonic, None);
// define external and internal derivation key path
let external_path = DerivationPath::from_str("m/86h/1h/0h/0").unwrap();
let internal_path = DerivationPath::from_str("m/86h/1h/0h/1").unwrap();
let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap();
let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap();
// generate external and internal descriptor from mnemonic
let (external_descriptor, ext_keymap) =

View File

@@ -9,14 +9,14 @@
// You may not use this file except in accordance with one or both of these
// licenses.
extern crate bdk_wallet;
extern crate bdk;
use std::error::Error;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
use bdk_wallet::wallet::signer::SignersContainer;
use bdk::bitcoin::Network;
use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
use bdk::wallet::signer::SignersContainer;
/// This example describes the use of the BDK's [`bdk_wallet::descriptor::policy`] module.
/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module.
///
/// Policy is higher abstraction representation of the wallet descriptor spending condition.
/// This is useful to express complex miniscript spending conditions into more human readable form.
@@ -34,11 +34,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
// Use the descriptor string to derive the full descriptor and a keymap.
// The wallet descriptor can be used to create a new bdk_wallet::wallet.
// The wallet descriptor can be used to create a new bdk::wallet.
// While the `keymap` can be used to create a `SignerContainer`.
//
// The `SignerContainer` can sign for `PSBT`s.
// a bdk_wallet::wallet internally uses these to handle transaction signing.
// a bdk::wallet internally uses these to handle transaction signing.
// But they can be used as independent tools also.
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;

View File

@@ -42,16 +42,22 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
c
}
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> {
/// Computes the checksum bytes of a descriptor.
/// `exclude_hash = true` ignores all data after the first '#' (inclusive).
pub(crate) fn calc_checksum_bytes_internal(
mut desc: &str,
exclude_hash: bool,
) -> Result<[u8; 8], DescriptorError> {
let mut c = 1;
let mut cls = 0;
let mut clscount = 0;
let mut original_checksum = None;
if let Some(split) = desc.split_once('#') {
desc = split.0;
original_checksum = Some(split.1);
if exclude_hash {
if let Some(split) = desc.split_once('#') {
desc = split.0;
original_checksum = Some(split.1);
}
}
for ch in desc.as_bytes() {
@@ -89,10 +95,39 @@ pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> {
Ok(checksum)
}
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
calc_checksum_bytes_internal(desc, true)
}
/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
calc_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
calc_checksum_bytes_internal(desc, true)
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
}
// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum
// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes
/// Compute the checksum bytes of a descriptor
#[deprecated(
since = "0.24.0",
note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765."
)]
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
calc_checksum_bytes_internal(desc, false)
}
/// Compute the checksum of a descriptor
#[deprecated(
since = "0.24.0",
note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765."
)]
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
calc_checksum_bytes_internal(desc, false)
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
}
#[cfg(test)]

View File

@@ -274,13 +274,14 @@ macro_rules! impl_sortedmulti {
#[macro_export]
macro_rules! parse_tap_tree {
( @merge $tree_a:expr, $tree_b:expr) => {{
use $crate::alloc::sync::Arc;
use $crate::miniscript::descriptor::TapTree;
$tree_a
.and_then(|tree_a| Ok((tree_a, $tree_b?)))
.and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
a_keymap.extend(b_keymap.into_iter());
Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
Ok((TapTree::Tree(Arc::new(a_tree), Arc::new(b_tree)), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
})
}};
@@ -423,7 +424,7 @@ macro_rules! apply_modifier {
///
/// ```
/// # use std::str::FromStr;
/// let (my_descriptor, my_keys_map, networks) = bdk_wallet::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
/// let (my_descriptor, my_keys_map, networks) = bdk::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
@@ -444,7 +445,7 @@ macro_rules! apply_modifier {
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
/// let my_timelock = 50;
///
/// let (descriptor_a, key_map_a, networks) = bdk_wallet::descriptor! {
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
/// wsh (
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock))
/// )
@@ -452,12 +453,11 @@ macro_rules! apply_modifier {
///
/// #[rustfmt::skip]
/// let b_items = vec![
/// bdk_wallet::fragment!(pk(my_key_1))?,
/// bdk_wallet::fragment!(s:pk(my_key_2))?,
/// bdk_wallet::fragment!(s:n:d:v:older(my_timelock))?,
/// bdk::fragment!(pk(my_key_1))?,
/// bdk::fragment!(s:pk(my_key_2))?,
/// bdk::fragment!(s:n:d:v:older(my_timelock))?,
/// ];
/// let (descriptor_b, mut key_map_b, networks) =
/// bdk_wallet::descriptor!(wsh(thresh_vec(2, b_items)))?;
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
///
/// assert_eq!(descriptor_a, descriptor_b);
/// assert_eq!(key_map_a.len(), key_map_b.len());
@@ -476,7 +476,7 @@ macro_rules! apply_modifier {
/// let my_key_2 =
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
///
/// let (descriptor, key_map, networks) = bdk_wallet::descriptor! {
/// let (descriptor, key_map, networks) = bdk::descriptor! {
/// wsh (
/// multi(2, my_key_1, my_key_2)
/// )
@@ -492,7 +492,7 @@ macro_rules! apply_modifier {
/// let my_key =
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
///
/// let (descriptor, key_map, networks) = bdk_wallet::descriptor!(wpkh(my_key))?;
/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
@@ -703,10 +703,10 @@ macro_rules! fragment {
$crate::keys::make_pkh($key, &secp)
});
( after ( $value:expr ) ) => ({
$crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value).expect("valid `AbsLockTime`"))
$crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value))
});
( older ( $value:expr ) ) => ({
$crate::impl_leaf_opcode_value!(Older, $crate::miniscript::RelLockTime::from_consensus($value).expect("valid `RelLockTime`")) // TODO!!
$crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
});
( sha256 ( $hash:expr ) ) => ({
$crate::impl_leaf_opcode_value!(Sha256, $hash)
@@ -757,8 +757,7 @@ macro_rules! fragment {
(keys_acc, net_acc)
});
let thresh = $crate::miniscript::Threshold::new($thresh, items).expect("valid threshold and pks collection");
$crate::impl_leaf_opcode_value!(Thresh, thresh)
$crate::impl_leaf_opcode_value_two!(Thresh, $thresh, items)
.map(|(minisc, _, _)| (minisc, key_maps, valid_networks))
});
( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({
@@ -770,12 +769,7 @@ macro_rules! fragment {
( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
let fun = |k, pks| {
let thresh = $crate::miniscript::Threshold::new(k, pks).expect("valid threshold and pks collection");
$crate::miniscript::Terminal::Multi(thresh)
};
$crate::keys::make_multi($thresh, fun, $keys, &secp)
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp)
});
( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
$crate::group_multi_keys!( $( $key ),* )
@@ -784,12 +778,7 @@ macro_rules! fragment {
( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
let fun = |k, pks| {
let thresh = $crate::miniscript::Threshold::new(k, pks).expect("valid threshold and pks collection");
$crate::miniscript::Terminal::MultiA(thresh)
};
$crate::keys::make_multi($thresh, fun, $keys, &secp)
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp)
});
( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({
$crate::group_multi_keys!( $( $key ),* )
@@ -817,7 +806,7 @@ mod test {
use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
use bitcoin::bip32;
use bitcoin::Network::{Bitcoin, Regtest, Signet, Testnet};
use bitcoin::network::constants::Network::{Bitcoin, Regtest, Signet, Testnet};
use bitcoin::PrivateKey;
// test the descriptor!() macro
@@ -947,7 +936,7 @@ mod test {
#[test]
fn test_bip32_legacy_descriptors() {
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let path = bip32::DerivationPath::from_str("m/0").unwrap();
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
@@ -992,7 +981,7 @@ mod test {
#[test]
fn test_bip32_segwitv0_descriptors() {
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let path = bip32::DerivationPath::from_str("m/0").unwrap();
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
@@ -1049,10 +1038,10 @@ mod test {
#[test]
fn test_dsl_sortedmulti() {
let key_1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let key_1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let path_1 = bip32::DerivationPath::from_str("m/0").unwrap();
let key_2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
let key_2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
let path_2 = bip32::DerivationPath::from_str("m/1").unwrap();
let desc_key1 = (key_1, path_1);
@@ -1108,7 +1097,7 @@ mod test {
// - verify the valid_networks returned is correctly computed based on the keys present in the descriptor
#[test]
fn test_valid_networks() {
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let path = bip32::DerivationPath::from_str("m/0").unwrap();
let desc_key = (xprv, path).into_descriptor_key().unwrap();
@@ -1118,7 +1107,7 @@ mod test {
[Testnet, Regtest, Signet].iter().cloned().collect()
);
let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
let desc_key = (xprv, path).into_descriptor_key().unwrap();
@@ -1131,15 +1120,15 @@ mod test {
fn test_key_maps_merged() {
let secp = Secp256k1::new();
let xprv1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let xprv1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap();
let xprv2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
let xprv2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap();
let xprv3 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
let xprv3 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap();
@@ -1163,7 +1152,7 @@ mod test {
#[test]
fn test_script_context_validation() {
// this compiles
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let path = bip32::DerivationPath::from_str("m/0").unwrap();
let desc_key: DescriptorKey<Legacy> = (xprv, path).into_descriptor_key().unwrap();

View File

@@ -23,6 +23,7 @@ pub enum Error {
HardenedDerivationXpub,
/// The descriptor contains multipath keys
MultiPath,
/// Error thrown while working with [`keys`](crate::keys)
Key(crate::keys::KeyError),
/// Error while extracting and manipulating policies
@@ -36,13 +37,11 @@ pub enum Error {
/// Error during base58 decoding
Base58(bitcoin::base58::Error),
/// Key-related error
Pk(bitcoin::key::ParsePublicKeyError),
Pk(bitcoin::key::Error),
/// Miniscript error
Miniscript(miniscript::Error),
/// Hex decoding error
Hex(bitcoin::hex::HexToBytesError),
/// The provided wallet descriptors are identical
ExternalAndInternalAreTheSame,
Hex(bitcoin::hashes::hex::Error),
}
impl From<crate::keys::KeyError> for Error {
@@ -80,9 +79,6 @@ impl fmt::Display for Error {
Self::Pk(err) => write!(f, "Key-related error: {}", err),
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
Self::Hex(err) => write!(f, "Hex decoding error: {}", err),
Self::ExternalAndInternalAreTheSame => {
write!(f, "External and internal descriptors are the same")
}
}
}
}
@@ -102,8 +98,8 @@ impl From<bitcoin::base58::Error> for Error {
}
}
impl From<bitcoin::key::ParsePublicKeyError> for Error {
fn from(err: bitcoin::key::ParsePublicKeyError) -> Self {
impl From<bitcoin::key::Error> for Error {
fn from(err: bitcoin::key::Error) -> Self {
Error::Pk(err)
}
}
@@ -114,8 +110,8 @@ impl From<miniscript::Error> for Error {
}
}
impl From<bitcoin::hex::HexToBytesError> for Error {
fn from(err: bitcoin::hex::HexToBytesError) -> Self {
impl From<bitcoin::hashes::hex::Error> for Error {
fn from(err: bitcoin::hashes::hex::Error) -> Self {
Error::Hex(err)
}
}

View File

@@ -18,7 +18,7 @@ use crate::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub};
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey};
use bitcoin::{psbt, taproot};
use bitcoin::{Network, TxOut};
@@ -229,7 +229,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
let pk = match pk {
DescriptorPublicKey::XPub(ref xpub) => {
let mut xpub = xpub.clone();
xpub.xkey.network = self.network.into();
xpub.xkey.network = self.network;
DescriptorPublicKey::XPub(xpub)
}
@@ -264,11 +264,11 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
.map(|(mut k, mut v)| {
match (&mut k, &mut v) {
(DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
xpub.xkey.network = network.into();
xprv.xkey.network = network.into();
xpub.xkey.network = network;
xprv.xkey.network = network;
}
(_, DescriptorSecretKey::Single(key)) => {
key.key.network = network.into();
key.key.network = network;
}
_ => {}
}
@@ -377,7 +377,7 @@ where
pub(crate) trait DescriptorMeta {
fn is_witness(&self) -> bool;
fn is_taproot(&self) -> bool;
fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>>;
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>>;
fn derive_from_hd_keypaths(
&self,
hd_keypaths: &HdKeyPaths,
@@ -418,7 +418,7 @@ impl DescriptorMeta for ExtendedDescriptor {
self.desc_type() == DescriptorType::Tr
}
fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>> {
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>> {
let mut answer = Vec::new();
self.for_each_key(|pk| {
@@ -438,20 +438,21 @@ impl DescriptorMeta for ExtendedDescriptor {
secp: &SecpCtx,
) -> Option<DerivedDescriptor> {
// Ensure that deriving `xpub` with `path` yields `expected`
let verify_key =
|xpub: &DescriptorXKey<Xpub>, path: &DerivationPath, expected: &SinglePubKey| {
let derived = xpub
.xkey
.derive_pub(secp, path)
.expect("The path should never contain hardened derivation steps")
.public_key;
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
path: &DerivationPath,
expected: &SinglePubKey| {
let derived = xpub
.xkey
.derive_pub(secp, path)
.expect("The path should never contain hardened derivation steps")
.public_key;
match expected {
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
_ => false,
}
};
match expected {
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
_ => false,
}
};
let mut path_found = None;
@@ -604,10 +605,10 @@ mod test {
use core::str::FromStr;
use assert_matches::assert_matches;
use bitcoin::hex::FromHex;
use bitcoin::hashes::hex::FromHex;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::{bip32, Psbt};
use bitcoin::{NetworkKind, ScriptBuf};
use bitcoin::ScriptBuf;
use bitcoin::{bip32, psbt::Psbt};
use super::*;
use crate::psbt::PsbtUtils;
@@ -726,7 +727,7 @@ mod test {
let secp = Secp256k1::new();
let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
let path = bip32::DerivationPath::from_str("m/0").unwrap();
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
@@ -743,9 +744,9 @@ mod test {
.unwrap();
let mut xprv_testnet = xprv;
xprv_testnet.network = NetworkKind::Test;
xprv_testnet.network = Network::Testnet;
let xpub_testnet = bip32::Xpub::from_priv(&secp, &xprv_testnet);
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
xkey: xpub_testnet,
origin: None,
@@ -835,7 +836,7 @@ mod test {
fn test_descriptor_from_str_from_output_of_macro() {
let secp = Secp256k1::new();
let tpub = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
let tpub = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
let key = (tpub, path).into_descriptor_key().unwrap();
@@ -894,7 +895,7 @@ mod test {
.update_with_descriptor_unchecked(&descriptor)
.unwrap();
assert_eq!(psbt_input.redeem_script, Some(script.to_p2wsh()));
assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh()));
assert_eq!(psbt_input.witness_script, Some(script));
}
}

View File

@@ -20,10 +20,10 @@
//!
//! ```
//! # use std::sync::Arc;
//! # use bdk_wallet::descriptor::*;
//! # use bdk_wallet::wallet::signer::*;
//! # use bdk_wallet::bitcoin::secp256k1::Secp256k1;
//! use bdk_wallet::descriptor::policy::BuildSatisfaction;
//! # use bdk::descriptor::*;
//! # use bdk::wallet::signer::*;
//! # use bdk::bitcoin::secp256k1::Secp256k1;
//! use bdk::descriptor::policy::BuildSatisfaction;
//! let secp = Secp256k1::new();
//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
//!
@@ -40,7 +40,6 @@ use crate::collections::{BTreeMap, HashSet, VecDeque};
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::max;
use miniscript::miniscript::limits::{MAX_PUBKEYS_IN_CHECKSIGADD, MAX_PUBKEYS_PER_MULTISIG};
use core::fmt;
@@ -49,12 +48,12 @@ use serde::{Serialize, Serializer};
use bitcoin::bip32::Fingerprint;
use bitcoin::hashes::{hash160, ripemd160, sha256};
use bitcoin::{absolute, key::XOnlyPublicKey, relative, PublicKey, Sequence};
use bitcoin::{absolute, key::XOnlyPublicKey, PublicKey, Sequence};
use miniscript::descriptor::{
DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
};
use miniscript::{hash256, Threshold};
use miniscript::hash256;
use miniscript::{
Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
};
@@ -138,7 +137,7 @@ pub enum SatisfiableItem {
/// Relative timelock locktime
RelativeTimelock {
/// The timelock value
value: relative::LockTime,
value: Sequence,
},
/// Multi-signature public keys with threshold count
Multisig {
@@ -587,25 +586,30 @@ impl Policy {
Ok(Some(policy))
}
fn make_multi<Ctx: ScriptContext + 'static, const MAX: usize>(
threshold: &Threshold<DescriptorPublicKey, MAX>,
fn make_multisig<Ctx: ScriptContext + 'static>(
keys: &[DescriptorPublicKey],
signers: &SignersContainer,
build_sat: BuildSatisfaction,
threshold: usize,
sorted: bool,
secp: &SecpCtx,
) -> Result<Option<Policy>, PolicyError> {
let parsed_keys = threshold.iter().map(|k| PkOrF::from_key(k, secp)).collect();
if threshold == 0 {
return Ok(None);
}
let parsed_keys = keys.iter().map(|k| PkOrF::from_key(k, secp)).collect();
let mut contribution = Satisfaction::Partial {
n: threshold.n(),
m: threshold.k(),
n: keys.len(),
m: threshold,
items: vec![],
conditions: Default::default(),
sorted: Some(sorted),
};
let mut satisfaction = contribution.clone();
for (index, key) in threshold.iter().enumerate() {
for (index, key) in keys.iter().enumerate() {
if signers.find(signer_id(key, secp)).is_some() {
contribution.add(
&Satisfaction::Complete {
@@ -614,6 +618,7 @@ impl Policy {
index,
)?;
}
if let Some(psbt) = build_sat.psbt() {
if Ctx::find_signature(psbt, key, secp) {
satisfaction.add(
@@ -630,11 +635,12 @@ impl Policy {
let mut policy: Policy = SatisfiableItem::Multisig {
keys: parsed_keys,
threshold: threshold.k(),
threshold,
}
.into();
policy.contribution = contribution;
policy.satisfaction = satisfaction;
Ok(Some(policy))
}
@@ -719,7 +725,7 @@ impl Policy {
timelock: Some(*value),
}),
SatisfiableItem::RelativeTimelock { value } => Ok(Condition {
csv: Some((*value).into()),
csv: Some(*value),
timelock: None,
}),
_ => Ok(Condition::default()),
@@ -946,14 +952,11 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
Some(policy)
}
Terminal::Older(value) => {
let mut policy: Policy = SatisfiableItem::RelativeTimelock {
value: (*value).into(),
}
.into();
let mut policy: Policy = SatisfiableItem::RelativeTimelock { value: *value }.into();
policy.contribution = Satisfaction::Complete {
condition: Condition {
timelock: None,
csv: Some((*value).into()),
csv: Some(*value),
},
};
if let BuildSatisfaction::PsbtTimelocks {
@@ -963,11 +966,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
} = build_sat
{
let older = Older::new(Some(current_height), Some(input_max_height), false);
let older_sat =
Satisfier::<bitcoin::PublicKey>::check_older(&older, (*value).into());
let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
Satisfier::<bitcoin::PublicKey>::check_older(&sat, (*value).into())
});
let older_sat = Satisfier::<bitcoin::PublicKey>::check_older(&older, *value);
let inputs_sat = psbt_inputs_sat(psbt)
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_older(&sat, *value));
if older_sat && inputs_sat {
policy.satisfaction = policy.contribution.clone();
}
@@ -985,12 +986,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
Terminal::Hash160(hash) => {
Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
}
Terminal::Multi(threshold) => Policy::make_multi::<Ctx, MAX_PUBKEYS_PER_MULTISIG>(
threshold, signers, build_sat, false, secp,
)?,
Terminal::MultiA(threshold) => Policy::make_multi::<Ctx, MAX_PUBKEYS_IN_CHECKSIGADD>(
threshold, signers, build_sat, false, secp,
)?,
Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => {
Policy::make_multisig::<Ctx>(pks, signers, build_sat, *k, false, secp)?
}
// Identities
Terminal::Alt(inner)
| Terminal::Swap(inner)
@@ -1018,9 +1016,8 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
a.extract_policy(signers, build_sat, secp)?,
b.extract_policy(signers, build_sat, secp)?,
)?,
Terminal::Thresh(threshold) => {
let mut k = threshold.k();
let nodes = threshold.data();
Terminal::Thresh(k, nodes) => {
let mut threshold = *k;
let mapped: Vec<_> = nodes
.iter()
.map(|n| n.extract_policy(signers, build_sat, secp))
@@ -1030,13 +1027,13 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
.collect();
if mapped.len() < nodes.len() {
k = match k.checked_sub(nodes.len() - mapped.len()) {
threshold = match threshold.checked_sub(nodes.len() - mapped.len()) {
None => return Ok(None),
Some(x) => x,
};
}
Policy::make_thresh(mapped, k)?
Policy::make_thresh(mapped, threshold)?
}
// Unsupported
@@ -1090,10 +1087,13 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Result<Option<Policy>, Error> {
let threshold = Threshold::new(keys.k(), keys.pks().to_vec())
.expect("valid threshold and pks collection");
Ok(Policy::make_multi::<Ctx, MAX_PUBKEYS_PER_MULTISIG>(
&threshold, signers, build_sat, true, secp,
Ok(Policy::make_multisig::<Ctx>(
keys.pks.as_ref(),
signers,
build_sat,
keys.k,
true,
secp,
)?)
}
@@ -1137,7 +1137,7 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
let key_spend_sig =
miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
if tr.tap_tree().is_none() {
if tr.taptree().is_none() {
Ok(Some(key_spend_sig))
} else {
let mut items = vec![key_spend_sig];
@@ -1184,8 +1184,8 @@ mod test {
secp: &SecpCtx,
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
let path = bip32::DerivationPath::from_str(path).unwrap();
let tprv = bip32::Xpriv::from_str(tprv).unwrap();
let tpub = bip32::Xpub::from_priv(secp, &tprv);
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
let tpub = bip32::ExtendedPubKey::from_priv(secp, &tprv);
let fingerprint = tprv.fingerprint(secp);
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
let pubkey = (tpub, path).into_descriptor_key().unwrap();

View File

@@ -36,17 +36,17 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
/// ## Example
///
/// ```
/// use bdk_wallet::descriptor::error::Error as DescriptorError;
/// use bdk_wallet::keys::{IntoDescriptorKey, KeyError};
/// use bdk_wallet::miniscript::Legacy;
/// use bdk_wallet::template::{DescriptorTemplate, DescriptorTemplateOut};
/// use bdk::descriptor::error::Error as DescriptorError;
/// use bdk::keys::{IntoDescriptorKey, KeyError};
/// use bdk::miniscript::Legacy;
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
/// use bitcoin::Network;
///
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
///
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
/// Ok(bdk_wallet::descriptor!(pkh(self.0))?)
/// Ok(bdk::descriptor!(pkh(self.0))?)
/// }
/// }
/// ```
@@ -72,21 +72,17 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
/// ## Example
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Pkh;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::Wallet;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::P2Pkh;
///
/// let key_external =
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?;
/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?;
///
/// assert_eq!(
/// wallet
/// .next_unused_address(KeychainKind::External)
/// .to_string(),
/// wallet.get_address(New).to_string(),
/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
@@ -104,25 +100,17 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
/// ## Example
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh_P2Sh;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::Wallet;
/// use bdk::template::P2Wpkh_P2Sh;
/// use bdk::wallet::AddressIndex;
///
/// let key_external =
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(
/// P2Wpkh_P2Sh(key_external),
/// P2Wpkh_P2Sh(key_internal),
/// Network::Testnet,
/// )?;
/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?;
///
/// assert_eq!(
/// wallet
/// .next_unused_address(KeychainKind::External)
/// .to_string(),
/// wallet.get_address(AddressIndex::New).to_string(),
/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
@@ -141,21 +129,17 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
/// ## Example
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet};
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet};
/// use bdk::template::P2Wpkh;
/// use bdk::wallet::AddressIndex::New;
///
/// let key_external =
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?;
/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?;
///
/// assert_eq!(
/// wallet
/// .next_unused_address(KeychainKind::External)
/// .to_string(),
/// wallet.get_address(New).to_string(),
/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
@@ -173,21 +157,17 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// ## Example
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2TR;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::Wallet;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::P2TR;
///
/// let key_external =
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?;
/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?;
///
/// assert_eq!(
/// wallet
/// .next_unused_address(KeychainKind::External)
/// .to_string(),
/// wallet.get_address(New).to_string(),
/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46"
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
@@ -210,19 +190,20 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip44;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip44;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
/// Bip44(key.clone(), KeychainKind::External),
/// Bip44(key, KeychainKind::Internal),
/// Some(Bip44(key, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
/// assert_eq!(wallet.get_address(New).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
@@ -246,20 +227,21 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip44Public;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip44Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip44Public(key, fingerprint, KeychainKind::Internal),
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
/// assert_eq!(wallet.get_address(New).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
@@ -283,19 +265,20 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip49;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip49;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
/// Bip49(key.clone(), KeychainKind::External),
/// Bip49(key, KeychainKind::Internal),
/// Some(Bip49(key, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
/// assert_eq!(wallet.get_address(New).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
@@ -319,20 +302,21 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip49Public;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip49Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip49Public(key, fingerprint, KeychainKind::Internal),
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
/// assert_eq!(wallet.get_address(New).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
@@ -356,19 +340,20 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip84;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip84;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
/// Bip84(key.clone(), KeychainKind::External),
/// Bip84(key, KeychainKind::Internal),
/// Some(Bip84(key, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
@@ -392,20 +377,21 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip84Public;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip84Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip84Public(key, fingerprint, KeychainKind::Internal),
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
@@ -429,19 +415,20 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip86;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip86;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
/// Bip86(key.clone(), KeychainKind::External),
/// Bip86(key, KeychainKind::Internal),
/// Some(Bip86(key, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
/// assert_eq!(wallet.get_address(New).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip86<K: DerivableKey<Tap>>(pub K, pub KeychainKind);
@@ -465,20 +452,21 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip86Public;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip86Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip86Public(key, fingerprint, KeychainKind::Internal),
/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
/// assert_eq!(wallet.get_address(New).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip86Public<K: DerivableKey<Tap>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
@@ -579,29 +567,29 @@ mod test {
fn test_bip44_template_cointype() {
use bitcoin::bip32::ChildNumber::{self, Hardened};
let xprvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
assert!(xprvkey.network.is_mainnet());
let xprvkey = bitcoin::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
assert_eq!(Network::Bitcoin, xprvkey.network);
let xdesc = Bip44(xprvkey, KeychainKind::Internal)
.build(Network::Bitcoin)
.unwrap();
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
let purpose = path.first().unwrap();
let purpose = path.get(0).unwrap();
assert_matches!(purpose, Hardened { index: 44 });
let coin_type = path.get(1).unwrap();
assert_matches!(coin_type, Hardened { index: 0 });
}
let tprvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
assert!(!tprvkey.network.is_mainnet());
let tprvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
assert_eq!(Network::Testnet, tprvkey.network);
let tdesc = Bip44(tprvkey, KeychainKind::Internal)
.build(Network::Testnet)
.unwrap();
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
let purpose = path.first().unwrap();
let purpose = path.get(0).unwrap();
assert_matches!(purpose, Hardened { index: 44 });
let coin_type = path.get(1).unwrap();
assert_matches!(coin_type, Hardened { index: 1 });
@@ -752,7 +740,7 @@ mod test {
// BIP44 `pkh(key/44'/0'/0'/{0,1}/*)`
#[test]
fn test_bip44_template() {
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
check(
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
false,
@@ -782,7 +770,7 @@ mod test {
// BIP44 public `pkh(key/{0,1}/*)`
#[test]
fn test_bip44_public_template() {
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
check(
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
@@ -813,7 +801,7 @@ mod test {
// BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
#[test]
fn test_bip49_template() {
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
check(
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
true,
@@ -843,7 +831,7 @@ mod test {
// BIP49 public `sh(wpkh(key/{0,1}/*))`
#[test]
fn test_bip49_public_template() {
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
check(
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
@@ -874,7 +862,7 @@ mod test {
// BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)`
#[test]
fn test_bip84_template() {
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
check(
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
true,
@@ -904,7 +892,7 @@ mod test {
// BIP84 public `wpkh(key/{0,1}/*)`
#[test]
fn test_bip84_public_template() {
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
check(
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
@@ -936,7 +924,7 @@ mod test {
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
#[test]
fn test_bip86_template() {
let prvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
check(
Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin),
false,
@@ -967,7 +955,7 @@ mod test {
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
#[test]
fn test_bip86_public_template() {
let pubkey = bitcoin::bip32::Xpub::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap();
check(
Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),

View File

@@ -57,7 +57,7 @@ pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
Ok(bip32::Xpriv::new_master(Network::Bitcoin, &self[..])?.into())
Ok(bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self[..])?.into())
}
fn into_descriptor_key(

View File

@@ -97,7 +97,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
}
}
// This method is used internally by `bdk_wallet::fragment!` and `bdk_wallet::descriptor!`. It has to be
// This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be
// public because it is effectively called by external crates once the macros are expanded,
// but since it is not meant to be part of the public api we hide it from the docs.
#[doc(hidden)]
@@ -110,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
Ok((public, KeyMap::default(), valid_networks))
}
DescriptorKey::Secret(secret, valid_networks, _) => {
let mut key_map = KeyMap::new();
let mut key_map = KeyMap::with_capacity(1);
let public = secret
.to_public(secp)
@@ -206,9 +206,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// Key type valid in any context:
///
/// ```
/// use bdk_wallet::bitcoin::PublicKey;
/// use bdk::bitcoin::PublicKey;
///
/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
///
/// pub struct MyKeyType {
/// pubkey: PublicKey,
@@ -224,9 +224,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// Key type that is only valid on mainnet:
///
/// ```
/// use bdk_wallet::bitcoin::PublicKey;
/// use bdk::bitcoin::PublicKey;
///
/// use bdk_wallet::keys::{
/// use bdk::keys::{
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
/// ScriptContext, SinglePub, SinglePubKey,
/// };
@@ -251,11 +251,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// Key type that internally encodes in which context it's valid. The context is checked at runtime:
///
/// ```
/// use bdk_wallet::bitcoin::PublicKey;
/// use bdk::bitcoin::PublicKey;
///
/// use bdk_wallet::keys::{
/// DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext,
/// };
/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
///
/// pub struct MyKeyType {
/// is_legacy: bool,
@@ -281,17 +279,17 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// makes the compiler (correctly) fail.
///
/// ```compile_fail
/// use bdk_wallet::bitcoin::PublicKey;
/// use bdk::bitcoin::PublicKey;
/// use core::str::FromStr;
///
/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
///
/// pub struct MySegwitOnlyKeyType {
/// pubkey: PublicKey,
/// }
///
/// impl IntoDescriptorKey<bdk_wallet::miniscript::Segwitv0> for MySegwitOnlyKeyType {
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk_wallet::miniscript::Segwitv0>, KeyError> {
/// impl IntoDescriptorKey<bdk::miniscript::Segwitv0> for MySegwitOnlyKeyType {
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk::miniscript::Segwitv0>, KeyError> {
/// self.pubkey.into_descriptor_key()
/// }
/// }
@@ -299,8 +297,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// let key = MySegwitOnlyKeyType {
/// pubkey: PublicKey::from_str("...")?,
/// };
/// let (descriptor, _, _) = bdk_wallet::descriptor!(pkh(key))?;
/// // ^^^^^ changing this to `wpkh` would make it compile
/// let (descriptor, _, _) = bdk::descriptor!(pkh(key))?;
/// // ^^^^^ changing this to `wpkh` would make it compile
///
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
@@ -311,15 +309,15 @@ pub trait IntoDescriptorKey<Ctx: ScriptContext>: Sized {
/// Enum for extended keys that can be either `xprv` or `xpub`
///
/// An instance of [`ExtendedKey`] can be constructed from an [`Xpriv`](bip32::Xpriv)
/// or an [`Xpub`](bip32::Xpub) by using the `From` trait.
/// An instance of [`ExtendedKey`] can be constructed from an [`ExtendedPrivKey`](bip32::ExtendedPrivKey)
/// or an [`ExtendedPubKey`](bip32::ExtendedPubKey) by using the `From` trait.
///
/// Defaults to the [`Legacy`](miniscript::Legacy) context.
pub enum ExtendedKey<Ctx: ScriptContext = miniscript::Legacy> {
/// A private extended key, aka an `xprv`
Private((bip32::Xpriv, PhantomData<Ctx>)),
Private((bip32::ExtendedPrivKey, PhantomData<Ctx>)),
/// A public extended key, aka an `xpub`
Public((bip32::Xpub, PhantomData<Ctx>)),
Public((bip32::ExtendedPubKey, PhantomData<Ctx>)),
}
impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
@@ -331,43 +329,43 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
}
}
/// Transform the [`ExtendedKey`] into an [`Xpriv`](bip32::Xpriv) for the
/// Transform the [`ExtendedKey`] into an [`ExtendedPrivKey`](bip32::ExtendedPrivKey) for the
/// given [`Network`], if the key contains the private data
pub fn into_xprv(self, network: Network) -> Option<bip32::Xpriv> {
pub fn into_xprv(self, network: Network) -> Option<bip32::ExtendedPrivKey> {
match self {
ExtendedKey::Private((mut xprv, _)) => {
xprv.network = network.into();
xprv.network = network;
Some(xprv)
}
ExtendedKey::Public(_) => None,
}
}
/// Transform the [`ExtendedKey`] into an [`Xpub`](bip32::Xpub) for the
/// Transform the [`ExtendedKey`] into an [`ExtendedPubKey`](bip32::ExtendedPubKey) for the
/// given [`Network`]
pub fn into_xpub<C: Signing>(
self,
network: bitcoin::Network,
secp: &Secp256k1<C>,
) -> bip32::Xpub {
) -> bip32::ExtendedPubKey {
let mut xpub = match self {
ExtendedKey::Private((xprv, _)) => bip32::Xpub::from_priv(secp, &xprv),
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_priv(secp, &xprv),
ExtendedKey::Public((xpub, _)) => xpub,
};
xpub.network = network.into();
xpub.network = network;
xpub
}
}
impl<Ctx: ScriptContext> From<bip32::Xpub> for ExtendedKey<Ctx> {
fn from(xpub: bip32::Xpub) -> Self {
impl<Ctx: ScriptContext> From<bip32::ExtendedPubKey> for ExtendedKey<Ctx> {
fn from(xpub: bip32::ExtendedPubKey) -> Self {
ExtendedKey::Public((xpub, PhantomData))
}
}
impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
fn from(xprv: bip32::Xpriv) -> Self {
impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
fn from(xprv: bip32::ExtendedPrivKey) -> Self {
ExtendedKey::Private((xprv, PhantomData))
}
}
@@ -385,13 +383,13 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
///
/// ## Examples
///
/// Key types that can be directly converted into an [`Xpriv`] or
/// an [`Xpub`] can implement only the required `into_extended_key()` method.
/// Key types that can be directly converted into an [`ExtendedPrivKey`] or
/// an [`ExtendedPubKey`] can implement only the required `into_extended_key()` method.
///
/// ```
/// use bdk_wallet::bitcoin;
/// use bdk_wallet::bitcoin::bip32;
/// use bdk_wallet::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
/// use bdk::bitcoin;
/// use bdk::bitcoin::bip32;
/// use bdk::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
///
/// struct MyCustomKeyType {
/// key_data: bitcoin::PrivateKey,
@@ -401,8 +399,8 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
///
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
/// let xprv = bip32::Xpriv {
/// network: self.network.into(),
/// let xprv = bip32::ExtendedPrivKey {
/// network: self.network,
/// depth: 0,
/// parent_fingerprint: bip32::Fingerprint::default(),
/// private_key: self.key_data.inner,
@@ -417,12 +415,12 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
///
/// Types that don't internally encode the [`Network`] in which they are valid need some extra
/// steps to override the set of valid networks, otherwise only the network specified in the
/// [`Xpriv`] or [`Xpub`] will be considered valid.
/// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid.
///
/// ```
/// use bdk_wallet::bitcoin;
/// use bdk_wallet::bitcoin::bip32;
/// use bdk_wallet::keys::{
/// use bdk::bitcoin;
/// use bdk::bitcoin::bip32;
/// use bdk::keys::{
/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext,
/// };
///
@@ -433,8 +431,8 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
///
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
/// let xprv = bip32::Xpriv {
/// network: bitcoin::Network::Bitcoin.into(), // pick an arbitrary network here
/// let xprv = bip32::ExtendedPrivKey {
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
/// depth: 0,
/// parent_fingerprint: bip32::Fingerprint::default(),
/// private_key: self.key_data.inner,
@@ -461,8 +459,8 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
/// ```
///
/// [`DerivationPath`]: (bip32::DerivationPath)
/// [`Xpriv`]: (bip32::Xpriv)
/// [`Xpub`]: (bip32::Xpub)
/// [`ExtendedPrivKey`]: (bip32::ExtendedPrivKey)
/// [`ExtendedPubKey`]: (bip32::ExtendedPubKey)
pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
/// Consume `self` and turn it into an [`ExtendedKey`]
#[cfg_attr(
@@ -471,9 +469,9 @@ pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
```rust
use bdk_wallet::bitcoin::Network;
use bdk_wallet::keys::{DerivableKey, ExtendedKey};
use bdk_wallet::keys::bip39::{Mnemonic, Language};
use bdk::bitcoin::Network;
use bdk::keys::{DerivableKey, ExtendedKey};
use bdk::keys::bip39::{Mnemonic, Language};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let xkey: ExtendedKey =
@@ -522,13 +520,13 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for ExtendedKey<Ctx> {
}
}
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpub {
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPubKey {
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
Ok(self.into())
}
}
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpriv {
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPrivKey {
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
Ok(self.into())
}
@@ -672,7 +670,7 @@ where
{
}
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::Xpriv {
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::ExtendedPrivKey {
type Entropy = [u8; 32];
type Options = ();
@@ -683,7 +681,7 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::Xpriv {
entropy: Self::Entropy,
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
// pick a arbitrary network here, but say that we support all of them
let xprv = bip32::Xpriv::new_master(Network::Bitcoin, entropy.as_ref())?;
let xprv = bip32::ExtendedPrivKey::new_master(Network::Bitcoin, entropy.as_ref())?;
Ok(GeneratedKey::new(xprv, any_network()))
}
}
@@ -717,7 +715,7 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for PrivateKey {
let inner = secp256k1::SecretKey::from_slice(&entropy)?;
let private_key = PrivateKey {
compressed: options.compressed,
network: Network::Bitcoin.into(),
network: Network::Bitcoin,
inner,
};
@@ -766,7 +764,7 @@ fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
Ok((pks, key_map, valid_networks))
}
// Used internally by `bdk_wallet::fragment!` to build `pk_k()` fragments
// Used internally by `bdk::fragment!` to build `pk_k()` fragments
#[doc(hidden)]
pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
descriptor_key: Pk,
@@ -780,7 +778,7 @@ pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
Ok((minisc, key_map, valid_networks))
}
// Used internally by `bdk_wallet::fragment!` to build `pk_h()` fragments
// Used internally by `bdk::fragment!` to build `pk_h()` fragments
#[doc(hidden)]
pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
descriptor_key: Pk,
@@ -794,7 +792,7 @@ pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
Ok((minisc, key_map, valid_networks))
}
// Used internally by `bdk_wallet::fragment!` to build `multi()` fragments
// Used internally by `bdk::fragment!` to build `multi()` fragments
#[doc(hidden)]
pub fn make_multi<
Pk: IntoDescriptorKey<Ctx>,
@@ -814,7 +812,7 @@ pub fn make_multi<
Ok((minisc, key_map, valid_networks))
}
// Used internally by `bdk_wallet::descriptor!` to build `sortedmulti()` fragments
// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments
#[doc(hidden)]
pub fn make_sortedmulti<Pk, Ctx, F>(
thresh: usize,
@@ -836,7 +834,7 @@ where
Ok((descriptor, key_map, valid_networks))
}
/// The "identity" conversion is used internally by some `bdk_wallet::fragment`s
/// The "identity" conversion is used internally by some `bdk::fragment`s
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
Ok(self)
@@ -847,7 +845,9 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
let networks = match self {
DescriptorPublicKey::Single(_) => any_network(),
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. }) if xkey.network.is_mainnet() => {
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
if xkey.network == Network::Bitcoin =>
{
mainnet_network()
}
_ => test_networks(),
@@ -880,8 +880,12 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
let networks = match &self {
DescriptorSecretKey::Single(sk) if sk.key.network.is_mainnet() => mainnet_network(),
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. }) if xkey.network.is_mainnet() => {
DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
mainnet_network()
}
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
if xkey.network == Network::Bitcoin =>
{
mainnet_network()
}
_ => test_networks(),
@@ -967,7 +971,7 @@ pub mod test {
#[test]
fn test_keys_generate_xprv() {
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
bip32::Xpriv::generate_with_entropy_default(TEST_ENTROPY).unwrap();
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
assert_eq!(generated_xprv.valid_networks, any_network());
assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q");
@@ -997,6 +1001,6 @@ pub mod test {
.unwrap();
let xprv = xkey.into_xprv(Network::Testnet).unwrap();
assert_eq!(xprv.network, Network::Testnet.into());
assert_eq!(xprv.network, Network::Testnet);
}
}

View File

@@ -17,6 +17,8 @@ extern crate std;
pub extern crate alloc;
pub extern crate bitcoin;
#[cfg(feature = "hardware-signer")]
pub extern crate hwi;
pub extern crate miniscript;
extern crate serde;
extern crate serde_json;

View File

@@ -9,12 +9,11 @@
// You may not use this file except in accordance with one or both of these
// licenses.
//! Additional functions on the `rust-bitcoin` `Psbt` structure.
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
use crate::FeeRate;
use alloc::vec::Vec;
use bitcoin::Amount;
use bitcoin::FeeRate;
use bitcoin::Psbt;
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::TxOut;
// TODO upstream the functions here to `rust-bitcoin`?
@@ -26,36 +25,44 @@ pub trait PsbtUtils {
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats.
/// If the PSBT is missing a TxOut for an input returns None.
fn fee_amount(&self) -> Option<Amount>;
fn fee_amount(&self) -> Option<u64>;
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
/// `Psbt` is finalized and all witness/signature data is added to the
/// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
/// transaction.
/// If the PSBT is missing a TxOut for an input returns None.
fn fee_rate(&self) -> Option<FeeRate>;
}
impl PsbtUtils for Psbt {
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
let tx = &self.unsigned_tx;
let input = self.inputs.get(input_index)?;
match (&input.witness_utxo, &input.non_witness_utxo) {
(Some(_), _) => input.witness_utxo.clone(),
(_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| {
in_tx.output[tx.input[input_index].previous_output.vout as usize].clone()
}),
_ => None,
if input_index >= tx.input.len() {
return None;
}
if let Some(input) = self.inputs.get(input_index) {
if let Some(wit_utxo) = &input.witness_utxo {
Some(wit_utxo.clone())
} else if let Some(in_tx) = &input.non_witness_utxo {
Some(in_tx.output[tx.input[input_index].previous_output.vout as usize].clone())
} else {
None
}
} else {
None
}
}
fn fee_amount(&self) -> Option<Amount> {
fn fee_amount(&self) -> Option<u64> {
let tx = &self.unsigned_tx;
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
utxos.map(|inputs| {
let input_amount: Amount = inputs.iter().map(|i| i.value).sum();
let output_amount: Amount = self.unsigned_tx.output.iter().map(|o| o.value).sum();
let input_amount: u64 = inputs.iter().map(|i| i.value).sum();
let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum();
input_amount
.checked_sub(output_amount)
.expect("input amount must be greater than output amount")
@@ -64,7 +71,9 @@ impl PsbtUtils for Psbt {
fn fee_rate(&self) -> Option<FeeRate> {
let fee_amount = self.fee_amount();
let weight = self.clone().extract_tx().ok()?.weight();
fee_amount.map(|fee| fee / weight)
fee_amount.map(|fee| {
let weight = self.clone().extract_tx().weight();
FeeRate::from_wu(fee, weight)
})
}
}

305
crates/bdk/src/types.rs Normal file
View File

@@ -0,0 +1,305 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
use alloc::boxed::Box;
use core::convert::AsRef;
use core::ops::Sub;
use bdk_chain::ConfirmationTime;
use bitcoin::blockdata::transaction::{OutPoint, TxOut};
use bitcoin::{psbt, Weight};
use serde::{Deserialize, Serialize};
/// Types of keychains
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum KeychainKind {
/// External keychain, used for deriving recipient addresses.
External = 0,
/// Internal keychain, used for deriving change addresses.
Internal = 1,
}
impl KeychainKind {
/// Return [`KeychainKind`] as a byte
pub fn as_byte(&self) -> u8 {
match self {
KeychainKind::External => b'e',
KeychainKind::Internal => b'i',
}
}
}
impl AsRef<[u8]> for KeychainKind {
fn as_ref(&self) -> &[u8] {
match self {
KeychainKind::External => b"e",
KeychainKind::Internal => b"i",
}
}
}
/// Fee rate
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
// Internally stored as satoshi/vbyte
pub struct FeeRate(f32);
impl FeeRate {
/// Create a new instance checking the value provided
///
/// ## Panics
///
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
fn new_checked(value: f32) -> Self {
assert!(value.is_normal() || value == 0.0);
assert!(value.is_sign_positive());
FeeRate(value)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
///
/// ## Panics
///
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
FeeRate::new_checked(btc_per_kvb * 1e5)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
///
/// ## Panics
///
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
FeeRate::new_checked(sat_per_vb)
}
/// Create a new [`FeeRate`] with the default min relay fee value
pub const fn default_min_relay_fee() -> Self {
FeeRate(1.0)
}
/// Calculate fee rate from `fee` and weight units (`wu`).
pub fn from_wu(fee: u64, wu: Weight) -> FeeRate {
Self::from_vb(fee, wu.to_vbytes_ceil() as usize)
}
/// Calculate fee rate from `fee` and `vbytes`.
pub fn from_vb(fee: u64, vbytes: usize) -> FeeRate {
let rate = fee as f32 / vbytes as f32;
Self::from_sat_per_vb(rate)
}
/// Return the value as satoshi/vbyte
pub fn as_sat_per_vb(&self) -> f32 {
self.0
}
/// Return the value as satoshi/kwu
pub fn sat_per_kwu(&self) -> f32 {
self.0 * 250.0_f32
}
/// Calculate absolute fee in Satoshis using size in weight units.
pub fn fee_wu(&self, wu: Weight) -> u64 {
self.fee_vb(wu.to_vbytes_ceil() as usize)
}
/// Calculate absolute fee in Satoshis using size in virtual bytes.
pub fn fee_vb(&self, vbytes: usize) -> u64 {
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
}
}
impl Default for FeeRate {
fn default() -> Self {
FeeRate::default_min_relay_fee()
}
}
impl Sub for FeeRate {
type Output = Self;
fn sub(self, other: FeeRate) -> Self::Output {
FeeRate(self.0 - other.0)
}
}
/// Trait implemented by types that can be used to measure weight units.
pub trait Vbytes {
/// Convert weight units to virtual bytes.
fn vbytes(self) -> usize;
}
impl Vbytes for usize {
fn vbytes(self) -> usize {
// ref: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
(self as f32 / 4.0).ceil() as usize
}
}
/// An unspent output owned by a [`Wallet`].
///
/// [`Wallet`]: crate::Wallet
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LocalOutput {
/// Reference to a transaction output
pub outpoint: OutPoint,
/// Transaction output
pub txout: TxOut,
/// Type of keychain
pub keychain: KeychainKind,
/// Whether this UTXO is spent or not
pub is_spent: bool,
/// The derivation index for the script pubkey in the wallet
pub derivation_index: u32,
/// The confirmation time for transaction containing this utxo
pub confirmation_time: ConfirmationTime,
}
/// A [`Utxo`] with its `satisfaction_weight`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeightedUtxo {
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
/// properly maintain the feerate when adding this input to a transaction during coin selection.
///
/// [weight units]: https://en.bitcoin.it/wiki/Weight_units
pub satisfaction_weight: usize,
/// The UTXO
pub utxo: Utxo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// An unspent transaction output (UTXO).
pub enum Utxo {
/// A UTXO owned by the local wallet.
Local(LocalOutput),
/// A UTXO owned by another wallet.
Foreign {
/// The location of the output.
outpoint: OutPoint,
/// The information about the input we require to add it to a PSBT.
// Box it to stop the type being too big.
psbt_input: Box<psbt::Input>,
},
}
impl Utxo {
/// Get the location of the UTXO
pub fn outpoint(&self) -> OutPoint {
match &self {
Utxo::Local(local) => local.outpoint,
Utxo::Foreign { outpoint, .. } => *outpoint,
}
}
/// Get the `TxOut` of the UTXO
pub fn txout(&self) -> &TxOut {
match &self {
Utxo::Local(local) => &local.txout,
Utxo::Foreign {
outpoint,
psbt_input,
} => {
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
return &prev_tx.output[outpoint.vout as usize];
}
if let Some(txout) = &psbt_input.witness_utxo {
return txout;
}
unreachable!("Foreign UTXOs will always have one of these set")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_store_feerate_in_const() {
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
}
#[test]
#[should_panic]
fn test_invalid_feerate_neg_zero() {
let _ = FeeRate::from_sat_per_vb(-0.0);
}
#[test]
#[should_panic]
fn test_invalid_feerate_neg_value() {
let _ = FeeRate::from_sat_per_vb(-5.0);
}
#[test]
#[should_panic]
fn test_invalid_feerate_nan() {
let _ = FeeRate::from_sat_per_vb(f32::NAN);
}
#[test]
#[should_panic]
fn test_invalid_feerate_inf() {
let _ = FeeRate::from_sat_per_vb(f32::INFINITY);
}
#[test]
fn test_valid_feerate_pos_zero() {
let _ = FeeRate::from_sat_per_vb(0.0);
}
#[test]
fn test_fee_from_btc_per_kvb() {
let fee = FeeRate::from_btc_per_kvb(1e-5);
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_fee_from_sat_per_vbyte() {
let fee = FeeRate::from_sat_per_vb(1.0);
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_fee_default_min_relay_fee() {
let fee = FeeRate::default_min_relay_fee();
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_fee_from_sat_per_kvb() {
let fee = FeeRate::from_sat_per_kvb(1000.0);
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_fee_from_sat_per_kwu() {
let fee = FeeRate::from_sat_per_kwu(250.0);
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
assert_eq!(fee.sat_per_kwu(), 250.0);
}
}

View File

@@ -26,11 +26,13 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk_wallet::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
//! # use bdk_wallet::wallet::error::CreateTxError;
//! # use bdk_wallet::*;
//! # use bdk_wallet::wallet::coin_selection::decide_change;
//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
//! # use bdk::wallet::error::CreateTxError;
//! # use bdk_chain::PersistBackend;
//! # use bdk::*;
//! # use bdk::wallet::coin_selection::decide_change;
//! # use anyhow::Error;
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
//! #[derive(Debug)]
//! struct AlwaysSpendEverything;
//!
@@ -39,7 +41,7 @@
//! &self,
//! required_utxos: Vec<WeightedUtxo>,
//! optional_utxos: Vec<WeightedUtxo>,
//! fee_rate: FeeRate,
//! fee_rate: bdk::FeeRate,
//! target_amount: u64,
//! drain_script: &Script,
//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
@@ -51,17 +53,15 @@
//! .scan(
//! (&mut selected_amount, &mut additional_weight),
//! |(selected_amount, additional_weight), weighted_utxo| {
//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
//! **selected_amount += weighted_utxo.utxo.txout().value;
//! **additional_weight += Weight::from_wu(
//! (TxIn::default().segwit_weight().to_wu()
//! + weighted_utxo.satisfaction_weight as u64)
//! as u64,
//! (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
//! );
//! Some(weighted_utxo.utxo)
//! },
//! )
//! .collect::<Vec<_>>();
//! let additional_fees = (fee_rate * additional_weight).to_sat();
//! let additional_fees = fee_rate.fee_wu(additional_weight);
//! let amount_needed_with_fees = additional_fees + target_amount;
//! if selected_amount < amount_needed_with_fees {
//! return Err(coin_selection::Error::InsufficientFunds {
@@ -91,7 +91,7 @@
//! .unwrap();
//! let psbt = {
//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
//! builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
//! builder.add_recipient(to_address.script_pubkey(), 50_000);
//! builder.finish()?
//! };
//!
@@ -100,16 +100,13 @@
//! # Ok::<(), anyhow::Error>(())
//! ```
use crate::chain::collections::HashSet;
use crate::types::FeeRate;
use crate::wallet::utils::IsDust;
use crate::Utxo;
use crate::WeightedUtxo;
use bitcoin::FeeRate;
use alloc::vec::Vec;
use bitcoin::consensus::encode::serialize;
use bitcoin::OutPoint;
use bitcoin::TxIn;
use bitcoin::{Script, Weight};
use core::convert::TryInto;
@@ -120,6 +117,10 @@ use rand::seq::SliceRandom;
/// overridden
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
// Base weight of a Txin, not counting the weight needed for satisfying it.
// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes)
pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module
#[derive(Debug)]
pub enum Error {
@@ -192,7 +193,7 @@ pub struct CoinSelectionResult {
impl CoinSelectionResult {
/// The total value of the inputs selected.
pub fn selected_amount(&self) -> u64 {
self.selected.iter().map(|u| u.txout().value.to_sat()).sum()
self.selected.iter().map(|u| u.txout().value).sum()
}
/// The total value of the inputs selected from the local wallet.
@@ -200,7 +201,7 @@ impl CoinSelectionResult {
self.selected
.iter()
.filter_map(|u| match u {
Utxo::Local(_) => Some(u.txout().value.to_sat()),
Utxo::Local(_) => Some(u.txout().value),
_ => None,
})
.sum()
@@ -310,12 +311,11 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
// drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
let drain_output_len = serialize(drain_script).len() + 8usize;
let change_fee =
(fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat();
let change_fee = fee_rate.fee_vb(drain_output_len);
let drain_val = remaining_amount.saturating_sub(change_fee);
if drain_val.is_dust(drain_script) {
let dust_threshold = drain_script.minimal_non_dust().to_sat();
let dust_threshold = drain_script.dust_value().to_sat();
Excess::NoChange {
dust_threshold,
change_fee,
@@ -342,13 +342,10 @@ fn select_sorted_utxos(
(&mut selected_amount, &mut fee_amount),
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
if must_use || **selected_amount < target_amount + **fee_amount {
**fee_amount += (fee_rate
* Weight::from_wu(
TxIn::default().segwit_weight().to_wu()
+ weighted_utxo.satisfaction_weight as u64,
))
.to_sat();
**selected_amount += weighted_utxo.utxo.txout().value.to_sat();
**fee_amount += fee_rate.fee_wu(Weight::from_wu(
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
));
**selected_amount += weighted_utxo.utxo.txout().value;
Some(weighted_utxo.utxo)
} else {
None
@@ -388,12 +385,10 @@ struct OutputGroup {
impl OutputGroup {
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
let fee = (fee_rate
* Weight::from_wu(
TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64,
))
.to_sat();
let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64;
let fee = fee_rate.fee_wu(Weight::from_wu(
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
));
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
OutputGroup {
weighted_utxo,
fee,
@@ -459,8 +454,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
.iter()
.fold(0, |acc, x| acc + x.effective_value);
let cost_of_change =
(Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat();
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
// `curr_value` and `curr_available_value` are both the sum of *effective_values* of
// the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
@@ -484,7 +478,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
.chain(optional_utxos.iter())
.fold((0, 0), |(mut fees, mut value), utxo| {
fees += utxo.fee;
value += utxo.weighted_utxo.utxo.txout().value.to_sat();
value += utxo.weighted_utxo.utxo.txout().value;
(fees, value)
});
@@ -551,7 +545,7 @@ impl BranchAndBoundCoinSelection {
mut curr_value: i64,
mut curr_available_value: i64,
target_amount: i64,
cost_of_change: u64,
cost_of_change: f32,
drain_script: &Script,
fee_rate: FeeRate,
) -> Result<CoinSelectionResult, Error> {
@@ -588,7 +582,7 @@ impl BranchAndBoundCoinSelection {
// If we found a solution better than the previous one, or if there wasn't previous
// solution, update the best solution
if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
best_selection.clone_from(&current_selection);
best_selection = current_selection.clone();
best_selection_value = Some(curr_value);
}
@@ -717,44 +711,25 @@ impl BranchAndBoundCoinSelection {
}
}
/// Remove duplicate UTXOs.
///
/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
pub(crate) fn filter_duplicates<I>(required: I, optional: I) -> (I, I)
where
I: IntoIterator<Item = WeightedUtxo> + FromIterator<WeightedUtxo>,
{
let mut visited = HashSet::<OutPoint>::new();
let required = required
.into_iter()
.filter(|utxo| visited.insert(utxo.utxo.outpoint()))
.collect::<I>();
let optional = optional
.into_iter()
.filter(|utxo| visited.insert(utxo.utxo.outpoint()))
.collect::<I>();
(required, optional)
}
#[cfg(test)]
mod test {
use assert_matches::assert_matches;
use core::str::FromStr;
use bdk_chain::ConfirmationTime;
use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
use bitcoin::{OutPoint, ScriptBuf, TxOut};
use super::*;
use crate::types::*;
use crate::wallet::coin_selection::filter_duplicates;
use crate::wallet::Vbytes;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
use rand::{Rng, RngCore, SeedableRng};
// signature len (1WU) + signature and sighash (72WU)
// + pubkey len (1WU) + pubkey (33WU)
const P2WPKH_SATISFACTION_SIZE: usize = 1 + 72 + 1 + 33;
// n. of items on witness (1WU) + signature len (1WU) + signature and sighash (72WU)
// + pubkey len (1WU) + pubkey (33WU) + script sig len (1 byte, 4WU)
const P2WPKH_SATISFACTION_SIZE: usize = 1 + 1 + 72 + 1 + 33 + 4;
const FEE_AMOUNT: u64 = 50;
@@ -770,7 +745,7 @@ mod test {
utxo: Utxo::Local(LocalOutput {
outpoint,
txout: TxOut {
value: Amount::from_sat(value),
value,
script_pubkey: ScriptBuf::new(),
},
keychain: KeychainKind::External,
@@ -824,17 +799,16 @@ mod test {
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
let mut res = Vec::new();
for i in 0..utxos_number {
for _ in 0..utxos_number {
res.push(WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::from_str(&format!(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
i
))
outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
)
.unwrap(),
txout: TxOut {
value: Amount::from_sat(rng.gen_range(0..200000000)),
value: rng.gen_range(0..200000000),
script_pubkey: ScriptBuf::new(),
},
keychain: KeychainKind::External,
@@ -855,26 +829,24 @@ mod test {
}
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
(0..utxos_number)
.map(|i| WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::from_str(&format!(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
i
))
.unwrap(),
txout: TxOut {
value: Amount::from_sat(utxos_value),
script_pubkey: ScriptBuf::new(),
},
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 42,
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
}),
})
.collect()
let utxo = WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
)
.unwrap(),
txout: TxOut {
value: utxos_value,
script_pubkey: ScriptBuf::new(),
},
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 42,
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
}),
};
vec![utxo; utxos_number]
}
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
@@ -882,7 +854,7 @@ mod test {
utxos.shuffle(&mut rng);
utxos[..utxos_picked_len]
.iter()
.map(|u| u.utxo.txout().value.to_sat())
.map(|u| u.utxo.txout().value)
.sum()
}
@@ -896,7 +868,7 @@ mod test {
.coin_select(
utxos,
vec![],
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -917,7 +889,7 @@ mod test {
.coin_select(
utxos,
vec![],
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -938,7 +910,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -960,7 +932,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -978,7 +950,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1000),
FeeRate::from_sat_per_vb(1000.0),
target_amount,
&drain_script,
)
@@ -995,7 +967,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1016,7 +988,7 @@ mod test {
.coin_select(
utxos,
vec![],
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1037,7 +1009,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1059,7 +1031,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1071,18 +1043,14 @@ mod test {
fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
let utxos = get_oldest_first_test_utxos();
let target_amount: u64 = utxos
.iter()
.map(|wu| wu.utxo.txout().value.to_sat())
.sum::<u64>()
- 50;
let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
let drain_script = ScriptBuf::default();
OldestFirstCoinSelection
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1000),
FeeRate::from_sat_per_vb(1000.0),
target_amount,
&drain_script,
)
@@ -1103,7 +1071,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1124,7 +1092,7 @@ mod test {
.coin_select(
utxos.clone(),
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1145,7 +1113,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1170,9 +1138,9 @@ mod test {
));
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
let amount: u64 = required.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
assert_eq!(amount, 100_000);
let amount: u64 = optional.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum();
assert!(amount > 150_000);
let drain_script = ScriptBuf::default();
@@ -1182,7 +1150,7 @@ mod test {
.coin_select(
required,
optional,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1204,7 +1172,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1),
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
@@ -1222,7 +1190,7 @@ mod test {
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(1000),
FeeRate::from_sat_per_vb(1000.0),
target_amount,
&drain_script,
)
@@ -1234,19 +1202,22 @@ mod test {
let utxos = get_test_utxos();
let drain_script = ScriptBuf::default();
let target_amount = 99932; // first utxo's effective value
let feerate = FeeRate::BROADCAST_MIN;
let result = BranchAndBoundCoinSelection::new(0)
.coin_select(vec![], utxos, feerate, target_amount, &drain_script)
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
.unwrap();
assert_eq!(result.selected.len(), 1);
assert_eq!(result.selected_amount(), 100_000);
let input_weight =
TxIn::default().segwit_weight().to_wu() + P2WPKH_SATISFACTION_SIZE as u64;
let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes();
// the final fee rate should be exactly the same as the fee rate given
let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight);
assert_eq!(result_feerate, feerate);
assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON);
}
#[test]
@@ -1262,7 +1233,7 @@ mod test {
.coin_select(
vec![],
optional_utxos,
FeeRate::ZERO,
FeeRate::from_sat_per_vb(0.0),
target_amount,
&drain_script,
)
@@ -1274,7 +1245,7 @@ mod test {
#[test]
#[should_panic(expected = "BnBNoExactMatch")]
fn test_bnb_function_no_exact_match() {
let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
let fee_rate = FeeRate::from_sat_per_vb(10.0);
let utxos: Vec<OutputGroup> = get_test_utxos()
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
@@ -1283,7 +1254,7 @@ mod test {
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
let size_of_change = 31;
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
let drain_script = ScriptBuf::default();
let target_amount = 20_000 + FEE_AMOUNT;
@@ -1304,7 +1275,7 @@ mod test {
#[test]
#[should_panic(expected = "BnBTotalTriesExceeded")]
fn test_bnb_function_tries_exceeded() {
let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
let fee_rate = FeeRate::from_sat_per_vb(10.0);
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
@@ -1313,7 +1284,7 @@ mod test {
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
let size_of_change = 31;
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
let target_amount = 20_000 + FEE_AMOUNT;
let drain_script = ScriptBuf::default();
@@ -1335,9 +1306,9 @@ mod test {
// The match won't be exact but still in the range
#[test]
fn test_bnb_function_almost_exact_match_with_fees() {
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
let fee_rate = FeeRate::from_sat_per_vb(1.0);
let size_of_change = 31;
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
.into_iter()
@@ -1350,7 +1321,7 @@ mod test {
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
// cost_of_change + 5.
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5;
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
let drain_script = ScriptBuf::default();
@@ -1375,7 +1346,7 @@ mod test {
fn test_bnb_function_exact_match_more_utxos() {
let seed = [0; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
let fee_rate = FeeRate::ZERO;
let fee_rate = FeeRate::from_sat_per_vb(0.0);
for _ in 0..200 {
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
@@ -1401,7 +1372,7 @@ mod test {
curr_value,
curr_available_value,
target_amount,
0,
0.0,
&drain_script,
fee_rate,
)
@@ -1417,7 +1388,7 @@ mod test {
let mut utxos = generate_random_utxos(&mut rng, 300);
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
let fee_rate = FeeRate::from_sat_per_vb(1.0);
let utxos: Vec<OutputGroup> = utxos
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
@@ -1446,7 +1417,7 @@ mod test {
let selection = BranchAndBoundCoinSelection::default().coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb_unchecked(10),
FeeRate::from_sat_per_vb(10.0),
500_000,
&drain_script,
);
@@ -1465,14 +1436,14 @@ mod test {
let utxos = get_test_utxos();
let drain_script = ScriptBuf::default();
let (required, optional) = utxos.into_iter().partition(
|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000),
);
let (required, optional) = utxos
.into_iter()
.partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000));
let selection = BranchAndBoundCoinSelection::default().coin_select(
required,
optional,
FeeRate::from_sat_per_vb_unchecked(10),
FeeRate::from_sat_per_vb(10.0),
500_000,
&drain_script,
);
@@ -1494,7 +1465,7 @@ mod test {
let selection = BranchAndBoundCoinSelection::default().coin_select(
utxos,
vec![],
FeeRate::from_sat_per_vb_unchecked(10_000),
FeeRate::from_sat_per_vb(10_000.0),
500_000,
&drain_script,
);
@@ -1507,95 +1478,4 @@ mod test {
})
);
}
#[test]
fn test_filter_duplicates() {
fn utxo(txid: &str, value: u64) -> WeightedUtxo {
WeightedUtxo {
satisfaction_weight: 0,
utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
txout: TxOut {
value: Amount::from_sat(value),
script_pubkey: ScriptBuf::new(),
},
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 0,
confirmation_time: ConfirmationTime::Confirmed {
height: 12345,
time: 12345,
},
}),
}
}
fn to_utxo_vec(utxos: &[(&str, u64)]) -> Vec<WeightedUtxo> {
let mut v = utxos
.iter()
.map(|&(txid, value)| utxo(txid, value))
.collect::<Vec<_>>();
v.sort_by_key(|u| u.utxo.outpoint());
v
}
struct TestCase<'a> {
name: &'a str,
required: &'a [(&'a str, u64)],
optional: &'a [(&'a str, u64)],
exp_required: &'a [(&'a str, u64)],
exp_optional: &'a [(&'a str, u64)],
}
let test_cases = [
TestCase {
name: "no_duplicates",
required: &[("A", 1000), ("B", 2100)],
optional: &[("C", 1000)],
exp_required: &[("A", 1000), ("B", 2100)],
exp_optional: &[("C", 1000)],
},
TestCase {
name: "duplicate_required_utxos",
required: &[("A", 3000), ("B", 1200), ("C", 1234), ("A", 3000)],
optional: &[("D", 2100)],
exp_required: &[("A", 3000), ("B", 1200), ("C", 1234)],
exp_optional: &[("D", 2100)],
},
TestCase {
name: "duplicate_optional_utxos",
required: &[("A", 3000), ("B", 1200)],
optional: &[("C", 5000), ("D", 1300), ("C", 5000)],
exp_required: &[("A", 3000), ("B", 1200)],
exp_optional: &[("C", 5000), ("D", 1300)],
},
TestCase {
name: "duplicate_across_required_and_optional_utxos",
required: &[("A", 3000), ("B", 1200), ("C", 2100)],
optional: &[("A", 3000), ("D", 1200), ("E", 5000)],
exp_required: &[("A", 3000), ("B", 1200), ("C", 2100)],
exp_optional: &[("D", 1200), ("E", 5000)],
},
];
for (i, t) in test_cases.into_iter().enumerate() {
println!("Case {}: {}", i, t.name);
let (required, optional) =
filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional));
assert_eq!(
required,
to_utxo_vec(t.exp_required),
"[{}:{}] unexpected `required` result",
i,
t.name
);
assert_eq!(
optional,
to_utxo_vec(t.exp_optional),
"[{}:{}] unexpected `optional` result",
i,
t.name
);
}
}
}

View File

@@ -14,9 +14,9 @@
use crate::descriptor::policy::PolicyError;
use crate::descriptor::DescriptorError;
use crate::wallet::coin_selection;
use crate::{descriptor, KeychainKind};
use crate::{descriptor, FeeRate, KeychainKind};
use alloc::string::String;
use bitcoin::{absolute, psbt, Amount, OutPoint, Sequence, Txid};
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
use core::fmt;
/// Errors returned by miniscript when updating inconsistent PSBTs
@@ -47,9 +47,11 @@ impl std::error::Error for MiniscriptPsbtError {}
/// Error returned from [`TxBuilder::finish`]
///
/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
pub enum CreateTxError {
pub enum CreateTxError<P> {
/// There was a problem with the descriptors passed in
Descriptor(DescriptorError),
/// We were unable to write wallet data to the persistence backend
Persist(P),
/// There was a problem while extracting and manipulating policies
Policy(PolicyError),
/// Spending policy is not compatible with this [`KeychainKind`]
@@ -76,20 +78,29 @@ pub enum CreateTxError {
},
/// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
FeeTooLow {
/// Required fee absolute value [`Amount`]
required: Amount,
/// Required fee absolute value (satoshi)
required: u64,
},
/// When bumping a tx the fee rate requested is lower than required
FeeRateTooLow {
/// Required fee rate
required: bitcoin::FeeRate,
/// Required fee rate (satoshi/vbyte)
required: FeeRate,
},
/// `manually_selected_only` option is selected but no utxo has been passed
NoUtxosSelected,
/// Output created is under the dust limit, 546 satoshis
OutputBelowDustLimit(usize),
/// The `change_policy` was set but the wallet does not have a change_descriptor
ChangePolicyDescriptor,
/// There was an error with coin selection
CoinSelection(coin_selection::Error),
/// Wallet's UTXO set is not enough to cover recipient's requested plus fee
InsufficientFunds {
/// Sats needed for some transaction
needed: u64,
/// Sats available for spending
available: u64,
},
/// Cannot build a tx without recipients
NoRecipients,
/// Partially signed bitcoin transaction error
@@ -108,10 +119,20 @@ pub enum CreateTxError {
MiniscriptPsbt(MiniscriptPsbtError),
}
impl fmt::Display for CreateTxError {
impl<P> fmt::Display for CreateTxError<P>
where
P: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Descriptor(e) => e.fmt(f),
Self::Persist(e) => {
write!(
f,
"failed to write wallet data to persistence backend: {}",
e
)
}
Self::Policy(e) => e.fmt(f),
CreateTxError::SpendingPolicyRequired(keychain_kind) => {
write!(f, "Spending policy required: {:?}", keychain_kind)
@@ -142,15 +163,13 @@ impl fmt::Display for CreateTxError {
)
}
CreateTxError::FeeTooLow { required } => {
write!(f, "Fee to low: required {}", required.display_dynamic())
write!(f, "Fee to low: required {} sat", required)
}
CreateTxError::FeeRateTooLow { required } => {
write!(
f,
// Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31
//"Fee rate too low: required {required:#}"
"Fee rate too low: required {} sat/vb",
crate::floating_rate!(required)
"Fee rate too low: required {} sat/vbyte",
required.as_sat_per_vb()
)
}
CreateTxError::NoUtxosSelected => {
@@ -159,7 +178,20 @@ impl fmt::Display for CreateTxError {
CreateTxError::OutputBelowDustLimit(limit) => {
write!(f, "Output below the dust limit: {}", limit)
}
CreateTxError::ChangePolicyDescriptor => {
write!(
f,
"The `change_policy` can be set only if the wallet has a change_descriptor"
)
}
CreateTxError::CoinSelection(e) => e.fmt(f),
CreateTxError::InsufficientFunds { needed, available } => {
write!(
f,
"Insufficient funds: {} sat available of {} sat needed",
available, needed
)
}
CreateTxError::NoRecipients => {
write!(f, "Cannot build tx without recipients")
}
@@ -180,38 +212,38 @@ impl fmt::Display for CreateTxError {
}
}
impl From<descriptor::error::Error> for CreateTxError {
impl<P> From<descriptor::error::Error> for CreateTxError<P> {
fn from(err: descriptor::error::Error) -> Self {
CreateTxError::Descriptor(err)
}
}
impl From<PolicyError> for CreateTxError {
impl<P> From<PolicyError> for CreateTxError<P> {
fn from(err: PolicyError) -> Self {
CreateTxError::Policy(err)
}
}
impl From<MiniscriptPsbtError> for CreateTxError {
impl<P> From<MiniscriptPsbtError> for CreateTxError<P> {
fn from(err: MiniscriptPsbtError) -> Self {
CreateTxError::MiniscriptPsbt(err)
}
}
impl From<psbt::Error> for CreateTxError {
impl<P> From<psbt::Error> for CreateTxError<P> {
fn from(err: psbt::Error) -> Self {
CreateTxError::Psbt(err)
}
}
impl From<coin_selection::Error> for CreateTxError {
impl<P> From<coin_selection::Error> for CreateTxError<P> {
fn from(err: coin_selection::Error) -> Self {
CreateTxError::CoinSelection(err)
}
}
#[cfg(feature = "std")]
impl std::error::Error for CreateTxError {}
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for CreateTxError<P> {}
#[derive(Debug)]
/// Error returned from [`Wallet::build_fee_bump`]

View File

@@ -20,8 +20,8 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::*;
//! # use bdk::wallet::export::*;
//! # use bdk::*;
//! let import = r#"{
//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
//! "blockheight":1782088,
@@ -29,9 +29,9 @@
//! }"#;
//!
//! let import = FullyNodedExport::from_str(import)?;
//! let wallet = Wallet::new(
//! let wallet = Wallet::new_no_persist(
//! &import.descriptor(),
//! &import.change_descriptor().expect("change descriptor"),
//! import.change_descriptor().as_ref(),
//! Network::Testnet,
//! )?;
//! # Ok::<_, Box<dyn std::error::Error>>(())
@@ -40,11 +40,11 @@
//! ### Export a `Wallet`
//! ```
//! # use bitcoin::*;
//! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::*;
//! let wallet = Wallet::new(
//! # use bdk::wallet::export::*;
//! # use bdk::*;
//! let wallet = Wallet::new_no_persist(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
//! Network::Testnet,
//! )?;
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
@@ -53,9 +53,9 @@
//! # Ok::<_, Box<dyn std::error::Error>>(())
//! ```
use alloc::string::String;
use core::fmt;
use core::str::FromStr;
use alloc::string::{String, ToString};
use serde::{Deserialize, Serialize};
use miniscript::descriptor::{ShInner, WshInner};
@@ -80,9 +80,9 @@ pub struct FullyNodedExport {
pub label: String,
}
impl fmt::Display for FullyNodedExport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap())
impl ToString for FullyNodedExport {
fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
}
@@ -110,8 +110,8 @@ impl FullyNodedExport {
///
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
/// returned will be `0`.
pub fn export_wallet(
wallet: &Wallet,
pub fn export_wallet<D>(
wallet: &Wallet<D>,
label: &str,
include_blockheight: bool,
) -> Result<Self, &'static str> {
@@ -142,17 +142,19 @@ impl FullyNodedExport {
blockheight,
};
let change_descriptor = {
let descriptor = wallet
.get_descriptor_for_keychain(KeychainKind::Internal)
.to_string_with_secret(
&wallet
.get_signers(KeychainKind::Internal)
.as_key_map(wallet.secp_ctx()),
);
Some(remove_checksum(descriptor))
let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() {
false => None,
true => {
let descriptor = wallet
.get_descriptor_for_keychain(KeychainKind::Internal)
.to_string_with_secret(
&wallet
.get_signers(KeychainKind::Internal)
.as_key_map(wallet.secp_ctx()),
);
Some(remove_checksum(descriptor))
}
};
if export.change_descriptor() != change_descriptor {
return Err("Incompatible change descriptor");
}
@@ -164,7 +166,7 @@ impl FullyNodedExport {
fn check_ms<Ctx: ScriptContext>(
terminal: &Terminal<String, Ctx>,
) -> Result<(), &'static str> {
if let Terminal::Multi(_) = terminal {
if let Terminal::Multi(_, _) = terminal {
Ok(())
} else {
Err("The descriptor contains operators not supported by Bitcoin Core")
@@ -187,7 +189,6 @@ impl FullyNodedExport {
WshInner::SortedMulti(_) => Ok(()),
WshInner::Ms(ms) => check_ms(&ms.node),
},
Descriptor::Tr(_) => Ok(()),
_ => Err("The descriptor is not compatible with Bitcoin Core"),
}
}
@@ -213,20 +214,23 @@ impl FullyNodedExport {
mod test {
use core::str::FromStr;
use crate::std::string::ToString;
use bdk_chain::{BlockId, ConfirmationTime};
use bitcoin::hashes::Hash;
use bitcoin::{transaction, BlockHash, Network, Transaction};
use bitcoin::{BlockHash, Network, Transaction};
use super::*;
use crate::wallet::Wallet;
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
fn get_test_wallet(
descriptor: &str,
change_descriptor: Option<&str>,
network: Network,
) -> Wallet<()> {
let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap();
let transaction = Transaction {
input: vec![],
output: vec![],
version: transaction::Version::non_standard(0),
version: 0,
lock_time: bitcoin::absolute::LockTime::ZERO,
};
wallet
@@ -252,7 +256,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
@@ -264,14 +268,13 @@ mod test {
#[test]
#[should_panic(expected = "Incompatible change descriptor")]
fn test_export_no_change() {
// The wallet's change descriptor has no wildcard. It should be impossible to
// This wallet explicitly doesn't have a change descriptor. It should be impossible to
// export, because exporting this kind of external descriptor normally implies the
// existence of a compatible internal descriptor
// existence of an internal descriptor
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/0)";
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
let wallet = get_test_wallet(descriptor, None, Network::Bitcoin);
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
@@ -284,7 +287,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
@@ -301,7 +304,7 @@ mod test {
[c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
))";
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Testnet);
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Testnet);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
@@ -310,24 +313,12 @@ mod test {
assert_eq!(export.label, "Test Label");
}
#[test]
fn test_export_tr() {
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Testnet);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
assert_eq!(export.blockheight, 5000);
assert_eq!(export.label, "Test Label");
}
#[test]
fn test_export_to_json() {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");

View File

@@ -14,11 +14,11 @@
//! This module contains HWISigner, an implementation of a [TransactionSigner] to be
//! used with hardware wallets.
//! ```no_run
//! # use bdk_wallet::bitcoin::Network;
//! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
//! # use bdk_wallet::wallet::AddressIndex::New;
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
//! # use bdk::bitcoin::Network;
//! # use bdk::signer::SignerOrdering;
//! # use bdk::wallet::hardwaresigner::HWISigner;
//! # use bdk::wallet::AddressIndex::New;
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
//! #
@@ -30,7 +30,7 @@
//! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//!
//! # let mut wallet = Wallet::new(
//! # let mut wallet = Wallet::new_no_persist(
//! # "",
//! # None,
//! # Network::Testnet,
@@ -48,8 +48,8 @@
//! ```
use bitcoin::bip32::Fingerprint;
use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::Psbt;
use hwi::error::Error;
use hwi::types::{HWIChain, HWIDevice};
@@ -87,7 +87,7 @@ impl SignerCommon for HWISigner {
impl TransactionSigner for HWISigner {
fn sign_transaction(
&self,
psbt: &mut Psbt,
psbt: &mut PartiallySignedTransaction,
_sign_options: &crate::SignOptions,
_secp: &crate::wallet::utils::SecpCtx,
) -> Result<(), SignerError> {

View File

@@ -19,12 +19,13 @@
//! # use core::str::FromStr;
//! # use bitcoin::secp256k1::{Secp256k1, All};
//! # use bitcoin::*;
//! # use bdk_wallet::signer::*;
//! # use bdk_wallet::*;
//! # use bitcoin::psbt;
//! # use bdk::signer::*;
//! # use bdk::*;
//! # #[derive(Debug)]
//! # struct CustomHSM;
//! # impl CustomHSM {
//! # fn hsm_sign_input(&self, _psbt: &mut Psbt, _input: usize) -> Result<(), SignerError> {
//! # fn hsm_sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
//! # Ok(())
//! # }
//! # fn connect() -> Self {
@@ -54,7 +55,7 @@
//! impl InputSigner for CustomSigner {
//! fn sign_input(
//! &self,
//! psbt: &mut Psbt,
//! psbt: &mut psbt::PartiallySignedTransaction,
//! input_index: usize,
//! _sign_options: &SignOptions,
//! _secp: &Secp256k1<All>,
@@ -67,9 +68,8 @@
//!
//! let custom_signer = CustomSigner::connect();
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)";
//! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)";
//! let mut wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?;
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),
@@ -80,20 +80,19 @@
//! ```
use crate::collections::BTreeMap;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::cmp::Ordering;
use core::fmt;
use core::ops::{Bound::Included, Deref};
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv};
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
use bitcoin::hashes::hash160;
use bitcoin::secp256k1::Message;
use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
use bitcoin::{ecdsa, psbt, sighash, taproot, transaction};
use bitcoin::{ecdsa, psbt, sighash, taproot};
use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
use bitcoin::{PrivateKey, Psbt, PublicKey};
use bitcoin::{PrivateKey, PublicKey};
use miniscript::descriptor::{
Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
@@ -159,35 +158,25 @@ pub enum SignerError {
NonStandardSighash,
/// Invalid SIGHASH for the signing context in use
InvalidSighash,
/// Error while computing the hash to sign a P2WPKH input.
SighashP2wpkh(sighash::P2wpkhError),
/// Error while computing the hash to sign a Taproot input.
SighashTaproot(sighash::TaprootError),
/// Error while computing the hash, out of bounds access on the transaction inputs.
TxInputsIndexError(transaction::InputsIndexError),
/// Error while computing the hash to sign
SighashError(sighash::Error),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// To be used only by external libraries implementing [`InputSigner`] or
/// [`TransactionSigner`], so that they can return their own custom errors, without having to
/// modify [`SignerError`] in BDK.
External(String),
/// Error while signing using hardware wallets
#[cfg(feature = "hardware-signer")]
HWIError(hwi::error::Error),
}
impl From<transaction::InputsIndexError> for SignerError {
fn from(v: transaction::InputsIndexError) -> Self {
Self::TxInputsIndexError(v)
#[cfg(feature = "hardware-signer")]
impl From<hwi::error::Error> for SignerError {
fn from(e: hwi::error::Error) -> Self {
SignerError::HWIError(e)
}
}
impl From<sighash::P2wpkhError> for SignerError {
fn from(e: sighash::P2wpkhError) -> Self {
Self::SighashP2wpkh(e)
}
}
impl From<sighash::TaprootError> for SignerError {
fn from(e: sighash::TaprootError) -> Self {
Self::SighashTaproot(e)
impl From<sighash::Error> for SignerError {
fn from(e: sighash::Error) -> Self {
SignerError::SighashError(e)
}
}
@@ -205,11 +194,10 @@ impl fmt::Display for SignerError {
Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"),
Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
Self::SighashP2wpkh(err) => write!(f, "Error while computing the hash to sign a P2WPKH input: {}", err),
Self::SighashTaproot(err) => write!(f, "Error while computing the hash to sign a Taproot input: {}", err),
Self::TxInputsIndexError(err) => write!(f, "Error while computing the hash, out of bounds access on the transaction inputs: {}", err),
Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
Self::External(err) => write!(f, "{}", err),
#[cfg(feature = "hardware-signer")]
Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err),
}
}
}
@@ -233,7 +221,7 @@ pub enum SignerContext {
},
}
/// Wrapper to pair a signer with its context
/// Wrapper structure to pair a signer with its context
#[derive(Debug, Clone)]
pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
signer: S,
@@ -282,7 +270,7 @@ pub trait InputSigner: SignerCommon {
/// Sign a single psbt input
fn sign_input(
&self,
psbt: &mut Psbt,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
sign_options: &SignOptions,
secp: &SecpCtx,
@@ -297,7 +285,7 @@ pub trait TransactionSigner: SignerCommon {
/// Sign all the inputs of the psbt
fn sign_transaction(
&self,
psbt: &mut Psbt,
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError>;
@@ -306,7 +294,7 @@ pub trait TransactionSigner: SignerCommon {
impl<T: InputSigner> TransactionSigner for T {
fn sign_transaction(
&self,
psbt: &mut Psbt,
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError> {
@@ -318,7 +306,7 @@ impl<T: InputSigner> TransactionSigner for T {
}
}
impl SignerCommon for SignerWrapper<DescriptorXKey<Xpriv>> {
impl SignerCommon for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.root_fingerprint(secp))
}
@@ -328,10 +316,10 @@ impl SignerCommon for SignerWrapper<DescriptorXKey<Xpriv>> {
}
}
impl InputSigner for SignerWrapper<DescriptorXKey<Xpriv>> {
impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
fn sign_input(
&self,
psbt: &mut Psbt,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
sign_options: &SignOptions,
secp: &SecpCtx,
@@ -414,7 +402,7 @@ fn multikey_to_xkeys<K: InnerXKey + Clone>(
.collect()
}
impl SignerCommon for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
impl SignerCommon for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.root_fingerprint(secp))
}
@@ -424,10 +412,10 @@ impl SignerCommon for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
}
}
impl InputSigner for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
impl InputSigner for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
fn sign_input(
&self,
psbt: &mut Psbt,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
sign_options: &SignOptions,
secp: &SecpCtx,
@@ -456,7 +444,7 @@ impl SignerCommon for SignerWrapper<PrivateKey> {
impl InputSigner for SignerWrapper<PrivateKey> {
fn sign_input(
&self,
psbt: &mut Psbt,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
sign_options: &SignOptions,
secp: &SecpCtx,
@@ -567,24 +555,21 @@ fn sign_psbt_ecdsa(
secret_key: &secp256k1::SecretKey,
pubkey: PublicKey,
psbt_input: &mut psbt::Input,
hash: impl bitcoin::hashes::Hash<Bytes = [u8; 32]>,
sighash_type: EcdsaSighashType,
hash: impl bitcoin::hashes::Hash + bitcoin::secp256k1::ThirtyTwoByteHash,
hash_ty: EcdsaSighashType,
secp: &SecpCtx,
allow_grinding: bool,
) {
let msg = &Message::from_digest(hash.to_byte_array());
let signature = if allow_grinding {
let msg = &Message::from(hash);
let sig = if allow_grinding {
secp.sign_ecdsa_low_r(msg, secret_key)
} else {
secp.sign_ecdsa(msg, secret_key)
};
secp.verify_ecdsa(msg, &signature, &pubkey.inner)
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
.expect("invalid or corrupted ecdsa signature");
let final_signature = ecdsa::Signature {
signature,
sighash_type,
};
let final_signature = ecdsa::Signature { sig, hash_ty };
psbt_input.partial_sigs.insert(pubkey, final_signature);
}
@@ -595,10 +580,10 @@ fn sign_psbt_schnorr(
leaf_hash: Option<taproot::TapLeafHash>,
psbt_input: &mut psbt::Input,
hash: TapSighash,
sighash_type: TapSighashType,
hash_ty: TapSighashType,
secp: &SecpCtx,
) {
let keypair = secp256k1::Keypair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
let keypair = match leaf_hash {
None => keypair
.tap_tweak(secp, psbt_input.tap_merkle_root)
@@ -607,14 +592,11 @@ fn sign_psbt_schnorr(
};
let msg = &Message::from(hash);
let signature = secp.sign_schnorr(msg, &keypair);
secp.verify_schnorr(&signature, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
let sig = secp.sign_schnorr(msg, &keypair);
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
.expect("invalid or corrupted schnorr signature");
let final_signature = taproot::Signature {
signature,
sighash_type,
};
let final_signature = taproot::Signature { sig, hash_ty };
if let Some(lh) = leaf_hash {
psbt_input
@@ -806,16 +788,6 @@ pub struct SignOptions {
/// Defaults to `true` which will remove partial signatures during finalization.
pub remove_partial_sigs: bool,
/// Whether to remove taproot specific fields from the PSBT on finalization.
///
/// For inputs this includes the taproot internal key, merkle root, and individual
/// scripts and signatures. For both inputs and outputs it includes key origin info.
///
/// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
///
/// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
pub remove_taproot_extras: bool,
/// Whether to try finalizing the PSBT after the inputs are signed.
///
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
@@ -840,10 +812,9 @@ pub struct SignOptions {
}
/// Customize which taproot script-path leaves the signer should sign.
#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TapLeavesOptions {
/// The signer will sign all the leaves it has a key for.
#[default]
All,
/// The signer won't sign leaves other than the ones specified. Note that it could still ignore
/// some of the specified leaves, if it doesn't have the right key to sign them.
@@ -854,6 +825,13 @@ pub enum TapLeavesOptions {
None,
}
impl Default for TapLeavesOptions {
fn default() -> Self {
TapLeavesOptions::All
}
}
#[allow(clippy::derivable_impls)]
impl Default for SignOptions {
fn default() -> Self {
SignOptions {
@@ -861,7 +839,6 @@ impl Default for SignOptions {
assume_height: None,
allow_all_sighashes: false,
remove_partial_sigs: true,
remove_taproot_extras: true,
try_finalize: true,
tap_leaves_options: TapLeavesOptions::default(),
sign_with_tap_internal_key: true,
@@ -876,7 +853,7 @@ pub(crate) trait ComputeSighash {
type SighashType;
fn sighash(
psbt: &Psbt,
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
extra: Self::Extra,
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
@@ -888,7 +865,7 @@ impl ComputeSighash for Legacy {
type SighashType = EcdsaSighashType;
fn sighash(
psbt: &Psbt,
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
_extra: (),
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
@@ -937,7 +914,7 @@ impl ComputeSighash for Segwitv0 {
type SighashType = EcdsaSighashType;
fn sighash(
psbt: &Psbt,
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
_extra: (),
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
@@ -948,7 +925,7 @@ impl ComputeSighash for Segwitv0 {
let psbt_input = &psbt.inputs[input_index];
let tx_input = &psbt.unsigned_tx.input[input_index];
let sighash_type = psbt_input
let sighash = psbt_input
.sighash_type
.unwrap_or_else(|| EcdsaSighashType::All.into())
.ecdsa_hash_ty()
@@ -957,7 +934,7 @@ impl ComputeSighash for Segwitv0 {
// Always try first with the non-witness utxo
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
// Check the provided prev-tx
if prev_tx.compute_txid() != tx_input.previous_output.txid {
if prev_tx.txid() != tx_input.previous_output.txid {
return Err(SignerError::InvalidNonWitnessUtxo);
}
@@ -976,39 +953,40 @@ impl ComputeSighash for Segwitv0 {
};
let value = utxo.value;
let mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx);
let sighash = match psbt_input.witness_script {
Some(ref witness_script) => {
sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)?
}
let script = match psbt_input.witness_script {
Some(ref witness_script) => witness_script.clone(),
None => {
if utxo.script_pubkey.is_p2wpkh() {
sighasher.p2wpkh_signature_hash(
input_index,
&utxo.script_pubkey,
value,
sighash_type,
)?
if utxo.script_pubkey.is_v0_p2wpkh() {
utxo.script_pubkey
.p2wpkh_script_code()
.expect("We check above that the spk is a p2wpkh")
} else if psbt_input
.redeem_script
.as_ref()
.map(|s| s.is_p2wpkh())
.map(|s| s.is_v0_p2wpkh())
.unwrap_or(false)
{
let script_pubkey = psbt_input.redeem_script.as_ref().unwrap();
sighasher.p2wpkh_signature_hash(
input_index,
script_pubkey,
value,
sighash_type,
)?
psbt_input
.redeem_script
.as_ref()
.unwrap()
.p2wpkh_script_code()
.expect("We check above that the spk is a p2wpkh")
} else {
return Err(SignerError::MissingWitnessScript);
}
}
};
Ok((sighash, sighash_type))
Ok((
sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash(
input_index,
&script,
value,
sighash,
)?,
sighash,
))
}
}
@@ -1018,7 +996,7 @@ impl ComputeSighash for Tap {
type SighashType = TapSighashType;
fn sighash(
psbt: &Psbt,
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
extra: Self::Extra,
) -> Result<(Self::Sighash, TapSighashType), SignerError> {
@@ -1189,7 +1167,7 @@ mod signers_container_tests {
impl TransactionSigner for DummySigner {
fn sign_transaction(
&self,
_psbt: &mut Psbt,
_psbt: &mut psbt::PartiallySignedTransaction,
_sign_options: &SignOptions,
_secp: &SecpCtx,
) -> Result<(), SignerError> {
@@ -1207,8 +1185,8 @@ mod signers_container_tests {
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
let secp: Secp256k1<All> = Secp256k1::new();
let path = bip32::DerivationPath::from_str(PATH).unwrap();
let tprv = bip32::Xpriv::from_str(tprv).unwrap();
let tpub = bip32::Xpub::from_priv(&secp, &tprv);
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
let tpub = bip32::ExtendedPubKey::from_priv(&secp, &tprv);
let fingerprint = tprv.fingerprint(&secp);
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
let pubkey = (tpub, path).into_descriptor_key().unwrap();

View File

@@ -16,9 +16,11 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk_wallet::*;
//! # use bdk_wallet::wallet::ChangeSet;
//! # use bdk_wallet::wallet::error::CreateTxError;
//! # use bdk::*;
//! # use bdk::wallet::ChangeSet;
//! # use bdk::wallet::error::CreateTxError;
//! # use bdk::wallet::tx_builder::CreateTx;
//! # use bdk_chain::PersistBackend;
//! # use anyhow::Error;
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
//! # let mut wallet = doctest_wallet!();
@@ -27,9 +29,9 @@
//!
//! tx_builder
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
//! .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
//! .add_recipient(to_address.script_pubkey(), 50_000)
//! // With a custom fee rate of 5.0 satoshi/vbyte
//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
//! .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
//! // Only spend non-change outputs
//! .do_not_spend_change()
//! // Turn on RBF signaling
@@ -38,18 +40,36 @@
//! # Ok::<(), anyhow::Error>(())
//! ```
use crate::collections::BTreeMap;
use crate::collections::HashSet;
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use bdk_chain::PersistBackend;
use core::cell::RefCell;
use core::fmt;
use core::marker::PhantomData;
use bitcoin::psbt::{self, Psbt};
use bitcoin::script::PushBytes;
use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
use super::coin_selection::CoinSelectionAlgorithm;
use super::{CreateTxError, Wallet};
use crate::collections::{BTreeMap, HashSet};
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use super::ChangeSet;
use crate::types::{FeeRate, KeychainKind, LocalOutput, WeightedUtxo};
use crate::wallet::CreateTxError;
use crate::{Utxo, Wallet};
/// Context in which the [`TxBuilder`] is valid
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed
/// to bumping the fee of an existing one).
#[derive(Debug, Default, Clone)]
pub struct CreateTx;
impl TxBuilderContext for CreateTx {}
/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction.
#[derive(Debug, Default, Clone)]
pub struct BumpFee;
impl TxBuilderContext for BumpFee {}
/// A transaction builder
///
@@ -61,12 +81,13 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
/// as in the following example:
///
/// ```
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::tx_builder::*;
/// # use bdk::*;
/// # use bdk::wallet::tx_builder::*;
/// # use bitcoin::*;
/// # use core::str::FromStr;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_chain::PersistBackend;
/// # use anyhow::Error;
/// # let mut wallet = doctest_wallet!();
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@@ -76,8 +97,8 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
/// let mut builder = wallet.build_tx();
/// builder
/// .ordering(TxOrdering::Untouched)
/// .add_recipient(addr1.script_pubkey(), Amount::from_sat(50_000))
/// .add_recipient(addr2.script_pubkey(), Amount::from_sat(50_000));
/// .add_recipient(addr1.script_pubkey(), 50_000)
/// .add_recipient(addr2.script_pubkey(), 50_000);
/// builder.finish()?
/// };
///
@@ -86,7 +107,7 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
/// let mut builder = wallet.build_tx();
/// builder.ordering(TxOrdering::Untouched);
/// for addr in &[addr1, addr2] {
/// builder.add_recipient(addr.script_pubkey(), Amount::from_sat(50_000));
/// builder.add_recipient(addr.script_pubkey(), 50_000);
/// }
/// builder.finish()?
/// };
@@ -105,10 +126,11 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
/// [`finish`]: Self::finish
/// [`coin_selection`]: Self::coin_selection
#[derive(Debug)]
pub struct TxBuilder<'a, Cs> {
pub(crate) wallet: Rc<RefCell<&'a mut Wallet>>,
pub struct TxBuilder<'a, D, Cs, Ctx> {
pub(crate) wallet: Rc<RefCell<&'a mut Wallet<D>>>,
pub(crate) params: TxParams,
pub(crate) coin_selection: Cs,
pub(crate) phantom: PhantomData<Ctx>,
}
/// The parameters for transaction creation sans coin selection algorithm.
@@ -141,7 +163,7 @@ pub(crate) struct TxParams {
#[derive(Clone, Copy, Debug)]
pub(crate) struct PreviousFee {
pub absolute: u64,
pub rate: FeeRate,
pub rate: f32,
}
#[derive(Debug, Clone, Copy)]
@@ -152,28 +174,31 @@ pub(crate) enum FeePolicy {
impl Default for FeePolicy {
fn default() -> Self {
FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
FeePolicy::FeeRate(FeeRate::default_min_relay_fee())
}
}
impl<'a, Cs: Clone> Clone for TxBuilder<'a, Cs> {
impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
fn clone(&self) -> Self {
TxBuilder {
wallet: self.wallet.clone(),
params: self.params.clone(),
coin_selection: self.coin_selection.clone(),
phantom: PhantomData,
}
}
}
// Methods supported for any CoinSelectionAlgorithm.
impl<'a, Cs> TxBuilder<'a, Cs> {
/// Set a custom fee rate.
///
/// This method sets the mining fee paid by the transaction as a rate on its size.
/// This means that the total fee paid is equal to `fee_rate` times the size
/// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
/// relay policy.
// methods supported by both contexts, for any CoinSelectionAlgorithm
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> {
/// Set a custom fee rate
/// The fee_rate method sets the mining fee paid by the transaction as a rate on its size.
/// This means that the total fee paid is equal to this rate * size of the transaction in virtual Bytes (vB) or Weight Unit (wu).
/// This rate is internally expressed in satoshis-per-virtual-bytes (sats/vB) using FeeRate::from_sat_per_vb, but can also be set by:
/// * sats/kvB (1000 sats/kvB == 1 sats/vB) using FeeRate::from_sat_per_kvb
/// * btc/kvB (0.00001000 btc/kvB == 1 sats/vB) using FeeRate::from_btc_per_kvb
/// * sats/kwu (250 sats/kwu == 1 sats/vB) using FeeRate::from_sat_per_kwu
/// Default is 1 sat/vB (see min_relay_fee)
///
/// Note that this is really a minimum feerate -- it's possible to
/// overshoot it slightly since adding a change output to drain the remaining
@@ -184,16 +209,16 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
}
/// Set an absolute fee
/// The fee_absolute method refers to the absolute transaction fee in [`Amount`].
/// If anyone sets both the `fee_absolute` method and the `fee_rate` method,
/// the `FeePolicy` enum will be set by whichever method was called last,
/// as the [`FeeRate`] and `FeeAmount` are mutually exclusive.
/// The fee_absolute method refers to the absolute transaction fee in satoshis (sats).
/// If anyone sets both the fee_absolute method and the fee_rate method,
/// the FeePolicy enum will be set by whichever method was called last,
/// as the FeeRate and FeeAmount are mutually exclusive.
///
/// Note that this is really a minimum absolute fee -- it's possible to
/// overshoot it slightly since adding a change output to drain the remaining
/// excess might not be viable.
pub fn fee_absolute(&mut self, fee_amount: Amount) -> &mut Self {
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount.to_sat()));
pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
self
}
@@ -243,7 +268,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
/// # use std::str::FromStr;
/// # use std::collections::BTreeMap;
/// # use bitcoin::*;
/// # use bdk_wallet::*;
/// # use bdk::*;
/// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
/// .unwrap()
@@ -254,7 +279,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
///
/// let builder = wallet
/// .build_tx()
/// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
/// .add_recipient(to_address.script_pubkey(), 50_000)
/// .policy_path(path, KeychainKind::External);
///
/// # Ok::<(), anyhow::Error>(())
@@ -293,9 +318,8 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
for utxo in utxos {
let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
let satisfaction_weight =
descriptor.max_weight_to_satisfy().unwrap().to_wu() as usize;
#[allow(deprecated)]
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
self.params.utxos.push(WeightedUtxo {
satisfaction_weight,
utxo: Utxo::Local(utxo),
@@ -336,9 +360,9 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
/// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
/// of course check the real input weight matches the expected weight prior to broadcasting.
///
/// To guarantee the `max_weight_to_satisfy` is correct, you can require the party providing the
/// To guarantee the `satisfaction_weight` is correct, you can require the party providing the
/// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
/// `script_pubkey` and then ask it for the [`max_weight_to_satisfy`].
/// `script_pubkey` and then ask it for the [`max_satisfaction_weight`].
///
/// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
///
@@ -359,35 +383,19 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
///
/// [`only_witness_utxo`]: Self::only_witness_utxo
/// [`finish`]: Self::finish
/// [`max_weight_to_satisfy`]: miniscript::Descriptor::max_weight_to_satisfy
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
pub fn add_foreign_utxo(
&mut self,
outpoint: OutPoint,
psbt_input: psbt::Input,
satisfaction_weight: usize,
) -> Result<&mut Self, AddForeignUtxoError> {
self.add_foreign_utxo_with_sequence(
outpoint,
psbt_input,
satisfaction_weight,
Sequence::MAX,
)
}
/// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value.
pub fn add_foreign_utxo_with_sequence(
&mut self,
outpoint: OutPoint,
psbt_input: psbt::Input,
satisfaction_weight: usize,
sequence: Sequence,
) -> Result<&mut Self, AddForeignUtxoError> {
if psbt_input.witness_utxo.is_none() {
match psbt_input.non_witness_utxo.as_ref() {
Some(tx) => {
if tx.compute_txid() != outpoint.txid {
if tx.txid() != outpoint.txid {
return Err(AddForeignUtxoError::InvalidTxid {
input_txid: tx.compute_txid(),
input_txid: tx.txid(),
foreign_utxo: outpoint,
});
}
@@ -405,7 +413,6 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
satisfaction_weight,
utxo: Utxo::Foreign {
outpoint,
sequence: Some(sequence),
psbt_input: Box::new(psbt_input),
},
});
@@ -535,17 +542,35 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
/// Choose the coin selection algorithm
///
/// Overrides the [`CoinSelectionAlgorithm`].
/// Overrides the [`DefaultCoinSelectionAlgorithm`].
///
/// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
pub fn coin_selection<P: CoinSelectionAlgorithm>(self, coin_selection: P) -> TxBuilder<'a, P> {
pub fn coin_selection<P: CoinSelectionAlgorithm>(
self,
coin_selection: P,
) -> TxBuilder<'a, D, P, Ctx> {
TxBuilder {
wallet: self.wallet,
params: self.params,
coin_selection,
phantom: PhantomData,
}
}
/// Finish building the transaction.
///
/// Returns a new [`Psbt`] per [`BIP174`].
///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
where
D: PersistBackend<ChangeSet>,
{
self.wallet
.borrow_mut()
.create_tx(self.coin_selection, self.params)
}
/// Enable signaling RBF
///
/// This will use the default nSequence value of `0xFFFFFFFD`.
@@ -590,96 +615,6 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
self.params.allow_dust = allow_dust;
self
}
/// Replace the recipients already added with a new list
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self {
self.params.recipients = recipients
.into_iter()
.map(|(script, amount)| (script, amount.to_sat()))
.collect();
self
}
/// Add a recipient to the internal list
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self {
self.params
.recipients
.push((script_pubkey, amount.to_sat()));
self
}
/// Add data as an output, using OP_RETURN
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
let script = ScriptBuf::new_op_return(data);
self.add_recipient(script, Amount::ZERO);
self
}
/// Sets the address to *drain* excess coins to.
///
/// Usually, when there are excess coins they are sent to a change address generated by the
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
/// coins are too small) it will not be included in the resulting transaction. The only
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
///
/// If you choose not to set any recipients, you should provide the utxos that the
/// transaction should spend via [`add_utxos`].
///
/// # Example
///
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
/// single address.
///
/// ```
/// # use std::str::FromStr;
/// # use bitcoin::*;
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use anyhow::Error;
/// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
/// .unwrap()
/// .assume_checked();
/// # let mut wallet = doctest_wallet!();
/// let mut tx_builder = wallet.build_tx();
///
/// tx_builder
/// // Spend all outputs in this wallet.
/// .drain_wallet()
/// // Send the excess (which is all the coins minus the fee) to this address.
/// .drain_to(to_address.script_pubkey())
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
/// .enable_rbf();
/// let psbt = tx_builder.finish()?;
/// # Ok::<(), anyhow::Error>(())
/// ```
///
/// [`add_recipient`]: Self::add_recipient
/// [`add_utxos`]: Self::add_utxos
/// [`drain_wallet`]: Self::drain_wallet
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
self.params.drain_to = Some(script_pubkey);
self
}
}
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> {
/// Finish building the transaction.
///
/// Returns a new [`Psbt`] per [`BIP174`].
///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
///
/// **WARNING**: To avoid change address reuse you must persist the changes resulting from one
/// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`].
pub fn finish(self) -> Result<Psbt, CreateTxError> {
self.wallet
.borrow_mut()
.create_tx(self.coin_selection, self.params)
}
}
#[derive(Debug)]
@@ -744,11 +679,141 @@ impl fmt::Display for AddForeignUtxoError {
#[cfg(feature = "std")]
impl std::error::Error for AddForeignUtxoError {}
#[derive(Debug)]
/// Error returned from [`TxBuilder::allow_shrinking`]
pub enum AllowShrinkingError {
/// Script/PubKey was not in the original transaction
MissingScriptPubKey(ScriptBuf),
}
impl fmt::Display for AllowShrinkingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingScriptPubKey(script_buf) => write!(
f,
"Script/PubKey was not in the original transaction: {}",
script_buf,
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AllowShrinkingError {}
impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
/// Replace the recipients already added with a new list
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self {
self.params.recipients = recipients;
self
}
/// Add a recipient to the internal list
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: u64) -> &mut Self {
self.params.recipients.push((script_pubkey, amount));
self
}
/// Add data as an output, using OP_RETURN
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
let script = ScriptBuf::new_op_return(data);
self.add_recipient(script, 0u64);
self
}
/// Sets the address to *drain* excess coins to.
///
/// Usually, when there are excess coins they are sent to a change address generated by the
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
/// coins are too small) it will not be included in the resulting transaction. The only
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
///
/// If you choose not to set any recipients, you should either provide the utxos that the
/// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them.
///
/// When bumping the fees of a transaction made with this option, you probably want to
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
///
/// # Example
///
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
/// single address.
///
/// ```
/// # use std::str::FromStr;
/// # use bitcoin::*;
/// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk::wallet::tx_builder::CreateTx;
/// # use bdk_chain::PersistBackend;
/// # use anyhow::Error;
/// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
/// .unwrap()
/// .assume_checked();
/// # let mut wallet = doctest_wallet!();
/// let mut tx_builder = wallet.build_tx();
///
/// tx_builder
/// // Spend all outputs in this wallet.
/// .drain_wallet()
/// // Send the excess (which is all the coins minus the fee) to this address.
/// .drain_to(to_address.script_pubkey())
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
/// .enable_rbf();
/// let psbt = tx_builder.finish()?;
/// # Ok::<(), anyhow::Error>(())
/// ```
///
/// [`allow_shrinking`]: Self::allow_shrinking
/// [`add_recipient`]: Self::add_recipient
/// [`add_utxos`]: Self::add_utxos
/// [`drain_wallet`]: Self::drain_wallet
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
self.params.drain_to = Some(script_pubkey);
self
}
}
// methods supported only by bump_fee
impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
/// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
/// will attempt to find a change output to shrink instead.
///
/// **Note** that the output may shrink to below the dust limit and therefore be removed. If it is
/// preserved then it is currently not guaranteed to be in the same position as it was
/// originally.
///
/// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
/// transaction we are bumping.
pub fn allow_shrinking(
&mut self,
script_pubkey: ScriptBuf,
) -> Result<&mut Self, AllowShrinkingError> {
match self
.params
.recipients
.iter()
.position(|(recipient_script, _)| *recipient_script == script_pubkey)
{
Some(position) => {
self.params.recipients.remove(position);
self.params.drain_to = Some(script_pubkey);
Ok(self)
}
None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)),
}
}
}
/// Ordering of the transaction's inputs and outputs
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub enum TxOrdering {
/// Randomized (default)
#[default]
Shuffle,
/// Unchanged
Untouched,
@@ -756,6 +821,12 @@ pub enum TxOrdering {
Bip69Lexicographic,
}
impl Default for TxOrdering {
fn default() -> Self {
TxOrdering::Shuffle
}
}
impl TxOrdering {
/// Sort transaction inputs and outputs by [`TxOrdering`] variant
pub fn sort_tx(&self, tx: &mut Transaction) {
@@ -809,10 +880,9 @@ impl RbfValue {
}
/// Policy regarding the use of change outputs when creating a transaction
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub enum ChangeSpendPolicy {
/// Use both change and non-change outputs (default)
#[default]
ChangeAllowed,
/// Only use change outputs (see [`TxBuilder::only_spend_change`])
OnlyChange,
@@ -820,6 +890,12 @@ pub enum ChangeSpendPolicy {
ChangeForbidden,
}
impl Default for ChangeSpendPolicy {
fn default() -> Self {
ChangeSpendPolicy::ChangeAllowed
}
}
impl ChangeSpendPolicy {
pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
match self {
@@ -847,8 +923,7 @@ mod test {
use bdk_chain::ConfirmationTime;
use bitcoin::consensus::deserialize;
use bitcoin::hex::FromHex;
use bitcoin::TxOut;
use bitcoin::hashes::hex::FromHex;
use super::*;
@@ -919,7 +994,7 @@ mod test {
.unwrap()
);
assert_eq!(tx.output[0].value.to_sat(), 800);
assert_eq!(tx.output[0].value, 800);
assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA]));
assert_eq!(
tx.output[2].script_pubkey,
@@ -936,7 +1011,7 @@ mod test {
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
vout: 0,
},
txout: TxOut::NULL,
txout: Default::default(),
keychain: KeychainKind::External,
is_spent: false,
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
@@ -947,7 +1022,7 @@ mod test {
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
vout: 1,
},
txout: TxOut::NULL,
txout: Default::default(),
keychain: KeychainKind::Internal,
is_spent: false,
confirmation_time: ConfirmationTime::Confirmed {

View File

@@ -10,7 +10,7 @@
// licenses.
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::{absolute, relative, Script, Sequence};
use bitcoin::{absolute, Script, Sequence};
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
@@ -26,7 +26,7 @@ pub trait IsDust {
impl IsDust for u64 {
fn is_dust(&self, script: &Script) -> bool {
*self < script.minimal_non_dust().to_sat()
*self < script.dust_value().to_sat()
}
}
@@ -95,7 +95,7 @@ impl Older {
}
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
fn check_older(&self, n: relative::LockTime) -> bool {
fn check_older(&self, n: Sequence) -> bool {
if let Some(current_height) = self.current_height {
// TODO: test >= / >
current_height
@@ -138,7 +138,7 @@ mod test {
.require_network(Network::Bitcoin)
.unwrap()
.script_pubkey();
assert!(script_p2wpkh.is_p2wpkh());
assert!(script_p2wpkh.is_v0_p2wpkh());
assert!(293.is_dust(&script_p2wpkh));
assert!(!294.is_dust(&script_p2wpkh));
}

View File

@@ -1,30 +1,30 @@
#![allow(unused)]
use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet};
use bdk_chain::indexed_tx_graph::Indexer;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
use bitcoin::hashes::Hash;
use bitcoin::{
transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
Txid,
};
use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid};
use std::str::FromStr;
/// Return a fake wallet that appears to be funded for testing.
///
/// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
/// sats are the transaction fee.
pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, bitcoin::Txid) {
let mut wallet = Wallet::new(descriptor, change, Network::Regtest).unwrap();
let receive_address = wallet.peek_address(KeychainKind::External, 0).address;
// Return a fake wallet that appears to be funded for testing.
//
// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
// sats are the transaction fee.
pub fn get_funded_wallet_with_change(
descriptor: &str,
change: Option<&str>,
) -> (Wallet, bitcoin::Txid) {
let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap();
let change_address = wallet.get_address(AddressIndex::New).address;
let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
.expect("address")
.require_network(Network::Regtest)
.unwrap();
let tx0 = Transaction {
version: transaction::Version::ONE,
version: 1,
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
@@ -36,17 +36,17 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
witness: Default::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(76_000),
script_pubkey: receive_address.script_pubkey(),
value: 76_000,
script_pubkey: change_address.script_pubkey(),
}],
};
let tx1 = Transaction {
version: transaction::Version::ONE,
version: 1,
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: tx0.compute_txid(),
txid: tx0.txid(),
vout: 0,
},
script_sig: Default::default(),
@@ -55,11 +55,11 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
}],
output: vec![
TxOut {
value: Amount::from_sat(50_000),
script_pubkey: receive_address.script_pubkey(),
value: 50_000,
script_pubkey: change_address.script_pubkey(),
},
TxOut {
value: Amount::from_sat(25_000),
value: 25_000,
script_pubkey: sendto_address.script_pubkey(),
},
],
@@ -96,42 +96,22 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
)
.unwrap();
(wallet, tx1.compute_txid())
(wallet, tx1.txid())
}
/// Return a fake wallet that appears to be funded for testing.
///
/// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
/// sats are the transaction fee.
///
/// Note: the change descriptor will have script type `p2wpkh`. If passing some other script type
/// as argument, make sure you're ok with getting a wallet where the keychains have potentially
/// different script types. Otherwise, use `get_funded_wallet_with_change`.
// Return a fake wallet that appears to be funded for testing.
//
// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
// sats are the transaction fee.
pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) {
let change = get_test_wpkh_change();
get_funded_wallet_with_change(descriptor, change)
}
pub fn get_funded_wallet_wpkh() -> (Wallet, bitcoin::Txid) {
get_funded_wallet_with_change(get_test_wpkh(), get_test_wpkh_change())
get_funded_wallet_with_change(descriptor, None)
}
pub fn get_test_wpkh() -> &'static str {
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
}
pub fn get_test_wpkh_with_change_desc() -> (&'static str, &'static str) {
(
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
get_test_wpkh_change(),
)
}
fn get_test_wpkh_change() -> &'static str {
"wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/0)"
}
pub fn get_test_single_sig_csv() -> &'static str {
// and(pk(Alice),older(6))
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
@@ -167,11 +147,6 @@ pub fn get_test_tr_single_sig_xprv() -> &'static str {
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"
}
pub fn get_test_tr_single_sig_xprv_with_change_desc() -> (&'static str, &'static str) {
("tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/0/*)",
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/1/*)")
}
pub fn get_test_tr_with_taptree_xprv() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
@@ -179,16 +154,3 @@ pub fn get_test_tr_with_taptree_xprv() -> &'static str {
pub fn get_test_tr_dup_keys() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is
/// useful in cases where we want to create a feerate from a `f64`, as the
/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer.
///
/// **Note** this 'quick and dirty' conversion should only be used when the input
/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow,
/// or else the resulting value will be inaccurate.
pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
// 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
FeeRate::from_sat_per_kwu(sat_kwu)
}

View File

@@ -1,5 +1,8 @@
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, TxIn};
use bdk_wallet::{psbt, KeychainKind, SignOptions};
use bdk::bitcoin::TxIn;
use bdk::wallet::AddressIndex;
use bdk::wallet::AddressIndex::New;
use bdk::{psbt, FeeRate, SignOptions};
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
use core::str::FromStr;
mod common;
use common::*;
@@ -12,9 +15,9 @@ const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6
fn test_psbt_malformed_psbt_input_legacy() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.peek_address(KeychainKind::External, 0);
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
builder.add_recipient(send_to.script_pubkey(), 10_000);
let mut psbt = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[0].clone());
let options = SignOptions {
@@ -29,9 +32,9 @@ fn test_psbt_malformed_psbt_input_legacy() {
fn test_psbt_malformed_psbt_input_segwit() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.peek_address(KeychainKind::External, 0);
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
builder.add_recipient(send_to.script_pubkey(), 10_000);
let mut psbt = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[1].clone());
let options = SignOptions {
@@ -45,9 +48,9 @@ fn test_psbt_malformed_psbt_input_segwit() {
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_tx_input() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.peek_address(KeychainKind::External, 0);
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
builder.add_recipient(send_to.script_pubkey(), 10_000);
let mut psbt = builder.finish().unwrap();
psbt.unsigned_tx.input.push(TxIn::default());
let options = SignOptions {
@@ -61,9 +64,9 @@ fn test_psbt_malformed_tx_input() {
fn test_psbt_sign_with_finalized() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.peek_address(KeychainKind::External, 0);
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
builder.add_recipient(send_to.script_pubkey(), 10_000);
let mut psbt = builder.finish().unwrap();
// add a finalized input
@@ -79,13 +82,13 @@ fn test_psbt_sign_with_finalized() {
fn test_psbt_fee_rate_with_witness_utxo() {
use psbt::PsbtUtils;
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
let expected_fee_rate = 1.2345;
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.peek_address(KeychainKind::External, 0);
let addr = wallet.get_address(New);
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(expected_fee_rate);
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let mut psbt = builder.finish().unwrap();
let fee_amount = psbt.fee_amount();
assert!(fee_amount.is_some());
@@ -96,21 +99,21 @@ fn test_psbt_fee_rate_with_witness_utxo() {
assert!(finalized);
let finalized_fee_rate = psbt.fee_rate().unwrap();
assert!(finalized_fee_rate >= expected_fee_rate);
assert!(finalized_fee_rate < unfinalized_fee_rate);
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
}
#[test]
fn test_psbt_fee_rate_with_nonwitness_utxo() {
use psbt::PsbtUtils;
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
let expected_fee_rate = 1.2345;
let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.peek_address(KeychainKind::External, 0);
let addr = wallet.get_address(New);
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(expected_fee_rate);
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let mut psbt = builder.finish().unwrap();
let fee_amount = psbt.fee_amount();
assert!(fee_amount.is_some());
@@ -120,21 +123,21 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() {
assert!(finalized);
let finalized_fee_rate = psbt.fee_rate().unwrap();
assert!(finalized_fee_rate >= expected_fee_rate);
assert!(finalized_fee_rate < unfinalized_fee_rate);
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
}
#[test]
fn test_psbt_fee_rate_with_missing_txout() {
use psbt::PsbtUtils;
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
let expected_fee_rate = 1.2345;
let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wpkh_wallet.peek_address(KeychainKind::External, 0);
let addr = wpkh_wallet.get_address(New);
let mut builder = wpkh_wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(expected_fee_rate);
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let mut wpkh_psbt = builder.finish().unwrap();
wpkh_psbt.inputs[0].witness_utxo = None;
@@ -142,13 +145,11 @@ fn test_psbt_fee_rate_with_missing_txout() {
assert!(wpkh_psbt.fee_amount().is_none());
assert!(wpkh_psbt.fee_rate().is_none());
let desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0)";
let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)";
let (mut pkh_wallet, _) = get_funded_wallet_with_change(desc, change_desc);
let addr = pkh_wallet.peek_address(KeychainKind::External, 0);
let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = pkh_wallet.get_address(New);
let mut builder = pkh_wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(expected_fee_rate);
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let mut pkh_psbt = builder.finish().unwrap();
pkh_psbt.inputs[0].non_witness_utxo = None;
@@ -158,29 +159,18 @@ fn test_psbt_fee_rate_with_missing_txout() {
#[test]
fn test_psbt_multiple_internalkey_signers() {
use bdk_wallet::signer::{SignerContext, SignerOrdering, SignerWrapper};
use bdk_wallet::KeychainKind;
use bitcoin::key::TapTweak;
use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey};
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
use bitcoin::{PrivateKey, TxOut};
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
use bdk::KeychainKind;
use bitcoin::{secp256k1::Secp256k1, PrivateKey};
use miniscript::psbt::PsbtExt;
use std::sync::Arc;
let secp = Secp256k1::new();
let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG";
let desc = format!("tr({})", wif);
let prv = PrivateKey::from_wif(wif).unwrap();
let keypair = Keypair::from_secret_key(&secp, &prv.inner);
let change_desc = "tr(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
let (mut wallet, _) = get_funded_wallet_with_change(&desc, change_desc);
let to_spend = wallet.balance().total();
let send_to = wallet.peek_address(KeychainKind::External, 0);
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.drain_to(send_to.script_pubkey()).drain_wallet();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let mut psbt = builder.finish().unwrap();
let unsigned_tx = psbt.unsigned_tx.clone();
// Adds a signer for the wrong internal key, bdk should not use this key to sign
wallet.add_signer(
KeychainKind::External,
@@ -193,32 +183,10 @@ fn test_psbt_multiple_internalkey_signers() {
},
)),
);
let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
assert!(finalized);
// To verify, we need the signature, message, and pubkey
let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap();
assert!(!witness.is_empty());
let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap();
// the prevout we're spending
let prevouts = &[TxOut {
script_pubkey: send_to.script_pubkey(),
value: to_spend,
}];
let prevouts = Prevouts::All(prevouts);
let input_index = 0;
let mut sighash_cache = SighashCache::new(unsigned_tx);
let sighash = sighash_cache
.taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default)
.unwrap();
let message = Message::from(sighash);
// add tweak. this was taken from `signer::sign_psbt_schnorr`
let keypair = keypair.tap_tweak(&secp, None).to_inner();
let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair);
// Must verify if we used the correct key to sign
let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey);
assert!(verify_res.is_ok(), "The wrong internal key was used");
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
// Checks that we signed using the right key
assert!(
psbt.finalize_mut(&secp).is_ok(),
"The wrong internal key was used"
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
[package]
name = "bdk_bitcoind_rpc"
version = "0.12.0"
version = "0.1.0"
edition = "2021"
rust-version = "1.63"
rust-version = "1.57"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_bitcoind_rpc"
@@ -13,12 +13,14 @@ readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitcoin = { version = "0.32.0", default-features = false }
bitcoincore-rpc = { version = "0.19.0" }
bdk_chain = { path = "../chain", version = "0.16", default-features = false }
# For no-std, remember to enable the bitcoin/no-std feature
bitcoin = { version = "0.30", default-features = false }
bitcoincore-rpc = { version = "0.17" }
bdk_chain = { path = "../chain", version = "0.6", default-features = false }
[dev-dependencies]
bdk_testenv = { path = "../testenv", default-features = false }
bitcoind = { version = "0.33", features = ["25_0"] }
anyhow = { version = "1" }
[features]
default = ["std"]

View File

@@ -14,7 +14,7 @@ use bitcoin::{block::Header, Block, BlockHash, Transaction};
pub use bitcoincore_rpc;
use bitcoincore_rpc::bitcoincore_rpc_json;
/// The [`Emitter`] is used to emit data sourced from [`bitcoincore_rpc::Client`].
/// A structure that emits data sourced from [`bitcoincore_rpc::Client`].
///
/// Refer to [module-level documentation] for more.
///
@@ -43,13 +43,11 @@ pub struct Emitter<'c, C> {
}
impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
/// Construct a new [`Emitter`].
/// Construct a new [`Emitter`] with the given RPC `client`, `last_cp` and `start_height`.
///
/// `last_cp` informs the emitter of the chain we are starting off with. This way, the emitter
/// can start emission from a block that connects to the original chain.
///
/// `start_height` starts emission from a given height (if there are no conflicts with the
/// original chain).
/// * `last_cp` is the check point used to find the latest block which is still part of the best
/// chain.
/// * `start_height` is the block height to start emitting blocks from.
pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self {
Self {
client,
@@ -129,58 +127,13 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
}
/// Emit the next block height and header (if any).
pub fn next_header(&mut self) -> Result<Option<BlockEvent<Header>>, bitcoincore_rpc::Error> {
Ok(poll(self, |hash| self.client.get_block_header(hash))?
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
pub fn next_header(&mut self) -> Result<Option<(u32, Header)>, bitcoincore_rpc::Error> {
poll(self, |hash| self.client.get_block_header(hash))
}
/// Emit the next block height and block (if any).
pub fn next_block(&mut self) -> Result<Option<BlockEvent<Block>>, bitcoincore_rpc::Error> {
Ok(poll(self, |hash| self.client.get_block(hash))?
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
}
}
/// A newly emitted block from [`Emitter`].
#[derive(Debug)]
pub struct BlockEvent<B> {
/// Either a full [`Block`] or [`Header`] of the new block.
pub block: B,
/// The checkpoint of the new block.
///
/// A [`CheckPoint`] is a node of a linked list of [`BlockId`]s. This checkpoint is linked to
/// all [`BlockId`]s originally passed in [`Emitter::new`] as well as emitted blocks since then.
/// These blocks are guaranteed to be of the same chain.
///
/// This is important as BDK structures require block-to-apply to be connected with another
/// block in the original chain.
pub checkpoint: CheckPoint,
}
impl<B> BlockEvent<B> {
/// The block height of this new block.
pub fn block_height(&self) -> u32 {
self.checkpoint.height()
}
/// The block hash of this new block.
pub fn block_hash(&self) -> BlockHash {
self.checkpoint.hash()
}
/// The [`BlockId`] of a previous block that this block connects to.
///
/// This either returns a [`BlockId`] of a previously emitted block or from the chain we started
/// with (passed in as `last_cp` in [`Emitter::new`]).
///
/// This value is derived from [`BlockEvent::checkpoint`].
pub fn connected_to(&self) -> BlockId {
match self.checkpoint.prev() {
Some(prev_cp) => prev_cp.block_id(),
// there is no previous checkpoint, so just connect with itself
None => self.checkpoint.block_id(),
}
pub fn next_block(&mut self) -> Result<Option<(u32, Block)>, bitcoincore_rpc::Error> {
poll(self, |hash| self.client.get_block(hash))
}
}
@@ -250,7 +203,7 @@ where
fn poll<C, V, F>(
emitter: &mut Emitter<C>,
get_item: F,
) -> Result<Option<(CheckPoint, V)>, bitcoincore_rpc::Error>
) -> Result<Option<(u32, V)>, bitcoincore_rpc::Error>
where
C: bitcoincore_rpc::RpcApi,
F: Fn(&BlockHash) -> Result<V, bitcoincore_rpc::Error>,
@@ -262,14 +215,13 @@ where
let hash = res.hash;
let item = get_item(&hash)?;
let new_cp = emitter
emitter.last_cp = emitter
.last_cp
.clone()
.push(BlockId { height, hash })
.expect("must push");
emitter.last_cp = new_cp.clone();
emitter.last_block = Some(res);
return Ok(Some((new_cp, item)));
return Ok(Some((height, item)));
}
PollResponse::NoMoreBlocks => {
emitter.last_block = None;

View File

@@ -2,14 +2,182 @@ use std::collections::{BTreeMap, BTreeSet};
use bdk_bitcoind_rpc::Emitter;
use bdk_chain::{
bitcoin::{Address, Amount, Txid},
bitcoin::{Address, Amount, BlockHash, Txid},
keychain::Balance,
local_chain::{CheckPoint, LocalChain},
local_chain::{self, CheckPoint, LocalChain},
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
};
use bdk_testenv::{anyhow, TestEnv};
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
use bitcoincore_rpc::RpcApi;
use bitcoin::{
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
secp256k1::rand::random, Block, CompactTarget, OutPoint, ScriptBuf, ScriptHash, Transaction,
TxIn, TxOut, WScriptHash,
};
use bitcoincore_rpc::{
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
RpcApi,
};
struct TestEnv {
#[allow(dead_code)]
daemon: bitcoind::BitcoinD,
client: bitcoincore_rpc::Client,
}
impl TestEnv {
fn new() -> anyhow::Result<Self> {
let daemon = match std::env::var_os("TEST_BITCOIND") {
Some(bitcoind_path) => bitcoind::BitcoinD::new(bitcoind_path),
None => bitcoind::BitcoinD::from_downloaded(),
}?;
let client = bitcoincore_rpc::Client::new(
&daemon.rpc_url(),
bitcoincore_rpc::Auth::CookieFile(daemon.params.cookie_file.clone()),
)?;
Ok(Self { daemon, client })
}
fn mine_blocks(
&self,
count: usize,
address: Option<Address>,
) -> anyhow::Result<Vec<BlockHash>> {
let coinbase_address = match address {
Some(address) => address,
None => self.client.get_new_address(None, None)?.assume_checked(),
};
let block_hashes = self
.client
.generate_to_address(count as _, &coinbase_address)?;
Ok(block_hashes)
}
fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
let bt = self.client.get_block_template(
GetBlockTemplateModes::Template,
&[GetBlockTemplateRules::SegWit],
&[],
)?;
let txdata = vec![Transaction {
version: 1,
lock_time: bitcoin::absolute::LockTime::from_height(0)?,
input: vec![TxIn {
previous_output: bitcoin::OutPoint::default(),
script_sig: ScriptBuf::builder()
.push_int(bt.height as _)
// randomn number so that re-mining creates unique block
.push_int(random())
.into_script(),
sequence: bitcoin::Sequence::default(),
witness: bitcoin::Witness::new(),
}],
output: vec![TxOut {
value: 0,
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
}],
}];
let bits: [u8; 4] = bt
.bits
.clone()
.try_into()
.expect("rpc provided us with invalid bits");
let mut block = Block {
header: Header {
version: bitcoin::block::Version::default(),
prev_blockhash: bt.previous_block_hash,
merkle_root: TxMerkleNode::all_zeros(),
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
nonce: 0,
},
txdata,
};
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
for nonce in 0..=u32::MAX {
block.header.nonce = nonce;
if block.header.target().is_met_by(block.block_hash()) {
break;
}
}
self.client.submit_block(&block)?;
Ok((bt.height as usize, block.block_hash()))
}
fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
let mut hash = self.client.get_best_block_hash()?;
for _ in 0..count {
let prev_hash = self.client.get_block_info(&hash)?.previousblockhash;
self.client.invalidate_block(&hash)?;
match prev_hash {
Some(prev_hash) => hash = prev_hash,
None => break,
}
}
Ok(())
}
fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
let start_height = self.client.get_block_count()?;
self.invalidate_blocks(count)?;
let res = self.mine_blocks(count, None);
assert_eq!(
self.client.get_block_count()?,
start_height,
"reorg should not result in height change"
);
res
}
fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
let start_height = self.client.get_block_count()?;
self.invalidate_blocks(count)?;
let res = (0..count)
.map(|_| self.mine_empty_block())
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(
self.client.get_block_count()?,
start_height,
"reorg should not result in height change"
);
Ok(res)
}
fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
let txid = self
.client
.send_to_address(address, amount, None, None, None, None, None, None)?;
Ok(txid)
}
}
fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Update {
let this_id = BlockId {
height,
hash: block.block_hash(),
};
let tip = if block.header.prev_blockhash == BlockHash::all_zeros() {
CheckPoint::new(this_id)
} else {
CheckPoint::new(BlockId {
height: height - 1,
hash: block.header.prev_blockhash,
})
.extend(core::iter::once(this_id))
.expect("must construct checkpoint")
};
local_chain::Update {
tip,
introduce_older_blocks: false,
}
}
/// Ensure that blocks are emitted in order even after reorg.
///
@@ -20,53 +188,44 @@ use bitcoincore_rpc::RpcApi;
#[test]
pub fn test_sync_local_chain() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let network_tip = env.rpc_client().get_block_count()?;
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
let mut emitter = Emitter::new(env.rpc_client(), local_chain.tip(), 0);
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
// Mine some blocks and return the actual block hashes.
// Because initializing `ElectrsD` already mines some blocks, we must include those too when
// returning block hashes.
// mine some blocks and returned the actual block hashes
let exp_hashes = {
let mut hashes = (0..=network_tip)
.map(|height| env.rpc_client().get_block_hash(height))
.collect::<Result<Vec<_>, _>>()?;
hashes.extend(env.mine_blocks(101 - network_tip as usize, None)?);
let mut hashes = vec![env.client.get_block_hash(0)?]; // include genesis block
hashes.extend(env.mine_blocks(101, None)?);
hashes
};
// See if the emitter outputs the right blocks.
// see if the emitter outputs the right blocks
println!("first sync:");
while let Some(emission) = emitter.next_block()? {
let height = emission.block_height();
let hash = emission.block_hash();
while let Some((height, block)) = emitter.next_block()? {
assert_eq!(
emission.block_hash(),
block.block_hash(),
exp_hashes[height as usize],
"emitted block hash is unexpected"
);
let chain_update = block_to_chain_update(&block, height);
assert_eq!(
local_chain.apply_update(emission.checkpoint,)?,
BTreeMap::from([(height, Some(hash))]),
local_chain.apply_update(chain_update)?,
BTreeMap::from([(height, Some(block.block_hash()))]),
"chain update changeset is unexpected",
);
}
assert_eq!(
local_chain
.iter_checkpoints()
.map(|cp| (cp.height(), cp.hash()))
.collect::<BTreeSet<_>>(),
exp_hashes
local_chain.blocks(),
&exp_hashes
.iter()
.enumerate()
.map(|(i, hash)| (i as u32, *hash))
.collect::<BTreeSet<_>>(),
.collect(),
"final local_chain state is unexpected",
);
// Perform reorg.
// perform reorg
let reorged_blocks = env.reorg(6)?;
let exp_hashes = exp_hashes
.iter()
@@ -75,30 +234,30 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
.cloned()
.collect::<Vec<_>>();
// See if the emitter outputs the right blocks.
// see if the emitter outputs the right blocks
println!("after reorg:");
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
while let Some(emission) = emitter.next_block()? {
let height = emission.block_height();
let hash = emission.block_hash();
while let Some((height, block)) = emitter.next_block()? {
assert_eq!(
height, exp_height as u32,
"emitted block has unexpected height"
);
assert_eq!(
hash, exp_hashes[height as usize],
block.block_hash(),
exp_hashes[height as usize],
"emitted block is unexpected"
);
let chain_update = block_to_chain_update(&block, height);
assert_eq!(
local_chain.apply_update(emission.checkpoint,)?,
local_chain.apply_update(chain_update)?,
if exp_height == exp_hashes.len() - reorged_blocks.len() {
core::iter::once((height, Some(hash)))
core::iter::once((height, Some(block.block_hash())))
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
.collect::<bdk_chain::local_chain::ChangeSet>()
} else {
BTreeMap::from([(height, Some(hash))])
BTreeMap::from([(height, Some(block.block_hash()))])
},
"chain update changeset is unexpected",
);
@@ -107,15 +266,12 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
}
assert_eq!(
local_chain
.iter_checkpoints()
.map(|cp| (cp.height(), cp.hash()))
.collect::<BTreeSet<_>>(),
exp_hashes
local_chain.blocks(),
&exp_hashes
.iter()
.enumerate()
.map(|(i, hash)| (i as u32, *hash))
.collect::<BTreeSet<_>>(),
.collect(),
"final local_chain state is unexpected after reorg",
);
@@ -131,25 +287,16 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
let env = TestEnv::new()?;
println!("getting new addresses!");
let addr_0 = env
.rpc_client()
.get_new_address(None, None)?
.assume_checked();
let addr_1 = env
.rpc_client()
.get_new_address(None, None)?
.assume_checked();
let addr_2 = env
.rpc_client()
.get_new_address(None, None)?
.assume_checked();
let addr_0 = env.client.get_new_address(None, None)?.assume_checked();
let addr_1 = env.client.get_new_address(None, None)?.assume_checked();
let addr_2 = env.client.get_new_address(None, None)?.assume_checked();
println!("got new addresses!");
println!("mining block!");
env.mine_blocks(101, None)?;
println!("mined blocks!");
let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
let mut index = SpkTxOutIndex::<usize>::default();
index.insert_spk(0, addr_0.script_pubkey());
@@ -158,12 +305,11 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
index
});
let emitter = &mut Emitter::new(env.rpc_client(), chain.tip(), 0);
let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
while let Some(emission) = emitter.next_block()? {
let height = emission.block_height();
let _ = chain.apply_update(emission.checkpoint)?;
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
while let Some((height, block)) = emitter.next_block()? {
let _ = chain.apply_update(block_to_chain_update(&block, height))?;
let indexed_additions = indexed_tx_graph.apply_block_relevant(block, height);
assert!(indexed_additions.is_empty());
}
@@ -171,7 +317,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
let exp_txids = {
let mut txids = BTreeSet::new();
for _ in 0..3 {
txids.insert(env.rpc_client().send_to_address(
txids.insert(env.client.send_to_address(
&addr_0,
Amount::from_sat(10_000),
None,
@@ -197,7 +343,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
.graph
.txs
.iter()
.map(|tx| tx.compute_txid())
.map(|tx| tx.txid())
.collect::<BTreeSet<Txid>>(),
exp_txids,
"changeset should have the 3 mempool transactions",
@@ -207,7 +353,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
// mine a block that confirms the 3 txs
let exp_block_hash = env.mine_blocks(1, None)?[0];
let exp_block_height = env.rpc_client().get_block_info(&exp_block_hash)?.height as u32;
let exp_block_height = env.client.get_block_info(&exp_block_hash)?.height as u32;
let exp_anchors = exp_txids
.iter()
.map({
@@ -221,10 +367,10 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
// must receive mined block which will confirm the transactions.
{
let emission = emitter.next_block()?.expect("must get mined block");
let height = emission.block_height();
let _ = chain.apply_update(emission.checkpoint)?;
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
let (height, block) = emitter.next_block()?.expect("must get mined block");
let _ = chain
.apply_update(CheckPoint::from_header(&block.header, height).into_update(false))?;
let indexed_additions = indexed_tx_graph.apply_block_relevant(block, height);
assert!(indexed_additions.graph.txs.is_empty());
assert!(indexed_additions.graph.txouts.is_empty());
assert_eq!(indexed_additions.graph.anchors, exp_anchors);
@@ -248,10 +394,10 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
env.rpc_client(),
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.rpc_client().get_block_hash(0)?,
hash: env.client.get_block_hash(0)?,
}),
EMITTER_START_HEIGHT as _,
);
@@ -261,12 +407,9 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
for reorg_count in 1..=10 {
let replaced_blocks = env.reorg_empty_blocks(reorg_count)?;
let next_emission = emitter.next_header()?.expect("must emit block after reorg");
let (height, next_header) = emitter.next_header()?.expect("must emit block after reorg");
assert_eq!(
(
next_emission.block_height() as usize,
next_emission.block_hash()
),
(height as usize, next_header.block_hash()),
replaced_blocks[0],
"block emitted after reorg should be at the reorg height"
);
@@ -282,7 +425,8 @@ fn process_block(
block: Block,
block_height: u32,
) -> anyhow::Result<()> {
recv_chain.apply_update(CheckPoint::from_header(&block.header, block_height))?;
recv_chain
.apply_update(CheckPoint::from_header(&block.header, block_height).into_update(false))?;
let _ = recv_graph.apply_block(block, block_height);
Ok(())
}
@@ -295,9 +439,8 @@ fn sync_from_emitter<C>(
where
C: bitcoincore_rpc::RpcApi,
{
while let Some(emission) = emitter.next_block()? {
let height = emission.block_height();
process_block(recv_chain, recv_graph, emission.block, height)?;
while let Some((height, block)) = emitter.next_block()? {
process_block(recv_chain, recv_graph, block, height)?;
}
Ok(())
}
@@ -324,24 +467,21 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
env.rpc_client(),
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.rpc_client().get_block_hash(0)?,
hash: env.client.get_block_hash(0)?,
}),
0,
);
// setup addresses
let addr_to_mine = env
.rpc_client()
.get_new_address(None, None)?
.assume_checked();
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
// setup receiver
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone());
@@ -357,7 +497,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
// lock outputs that send to `addr_to_track`
let outpoints_to_lock = env
.rpc_client()
.client
.get_transaction(&txid, None)?
.transaction()?
.output
@@ -366,7 +506,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
.map(|(vout, _)| OutPoint::new(txid, vout as _))
.collect::<Vec<_>>();
env.rpc_client().lock_unspent(&outpoints_to_lock)?;
env.client.lock_unspent(&outpoints_to_lock)?;
let _ = env.mine_blocks(1, None)?;
}
@@ -377,7 +517,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT * ADDITIONAL_COUNT as u64,
confirmed: SEND_AMOUNT.to_sat() * ADDITIONAL_COUNT as u64,
..Balance::default()
},
"initial balance must be correct",
@@ -391,8 +531,8 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
trusted_pending: SEND_AMOUNT * reorg_count as u64,
confirmed: SEND_AMOUNT.to_sat() * (ADDITIONAL_COUNT - reorg_count) as u64,
trusted_pending: SEND_AMOUNT.to_sat() * reorg_count as u64,
..Balance::default()
},
"reorg_count: {}",
@@ -415,19 +555,16 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
env.rpc_client(),
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.rpc_client().get_block_hash(0)?,
hash: env.client.get_block_hash(0)?,
}),
0,
);
// mine blocks and sync up emitter
let addr = env
.rpc_client()
.get_new_address(None, None)?
.assume_checked();
let addr = env.client.get_new_address(None, None)?.assume_checked();
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
while emitter.next_header()?.is_some() {}
@@ -440,7 +577,7 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
let emitted_txids = emitter
.mempool()?
.into_iter()
.map(|(tx, _)| tx.compute_txid())
.map(|(tx, _)| tx.txid())
.collect::<BTreeSet<Txid>>();
assert_eq!(
emitted_txids, exp_txids,
@@ -480,19 +617,16 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
env.rpc_client(),
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.rpc_client().get_block_hash(0)?,
hash: env.client.get_block_hash(0)?,
}),
0,
);
// mine blocks to get initial balance, sync emitter up to tip
let addr = env
.rpc_client()
.get_new_address(None, None)?
.assume_checked();
let addr = env.client.get_new_address(None, None)?.assume_checked();
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
while emitter.next_header()?.is_some() {}
@@ -509,7 +643,7 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
emitter
.mempool()?
.into_iter()
.map(|(tx, _)| tx.compute_txid())
.map(|(tx, _)| tx.txid())
.collect::<BTreeSet<_>>(),
tx_introductions.iter().map(|&(_, txid)| txid).collect(),
"first mempool emission should include all txs",
@@ -518,7 +652,7 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
emitter
.mempool()?
.into_iter()
.map(|(tx, _)| tx.compute_txid())
.map(|(tx, _)| tx.txid())
.collect::<BTreeSet<_>>(),
tx_introductions.iter().map(|&(_, txid)| txid).collect(),
"second mempool emission should still include all txs",
@@ -526,8 +660,7 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
// At this point, the emitter has seen all mempool transactions. It should only re-emit those
// that have introduction heights less than the emitter's last-emitted block tip.
while let Some(emission) = emitter.next_header()? {
let height = emission.block_height();
while let Some((height, _)) = emitter.next_header()? {
// We call `mempool()` twice.
// The second call (at height `h`) should skip the tx introduced at height `h`.
for try_index in 0..2 {
@@ -538,7 +671,7 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
let emitted_txids = emitter
.mempool()?
.into_iter()
.map(|(tx, _)| tx.compute_txid())
.map(|(tx, _)| tx.txid())
.collect::<BTreeSet<_>>();
assert_eq!(
emitted_txids, exp_txids,
@@ -568,19 +701,16 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let mut emitter = Emitter::new(
env.rpc_client(),
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.rpc_client().get_block_hash(0)?,
hash: env.client.get_block_hash(0)?,
}),
0,
);
// mine blocks to get initial balance
let addr = env
.rpc_client()
.get_new_address(None, None)?
.assume_checked();
let addr = env.client.get_new_address(None, None)?.assume_checked();
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
// introduce mempool tx at each block extension
@@ -596,9 +726,9 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
emitter
.mempool()?
.into_iter()
.map(|(tx, _)| tx.compute_txid())
.map(|(tx, _)| tx.txid())
.collect::<BTreeSet<_>>(),
env.rpc_client()
env.client
.get_raw_mempool()?
.into_iter()
.collect::<BTreeSet<_>>(),
@@ -617,15 +747,14 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
// emission.
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
let tx_introductions = dbg!(env
.rpc_client()
.client
.get_raw_mempool_verbose()?
.into_iter()
.map(|(txid, entry)| (txid, entry.height as usize))
.collect::<BTreeMap<_, _>>());
// `next_header` emits the replacement block of the reorg
if let Some(emission) = emitter.next_header()? {
let height = emission.block_height();
if let Some((height, _)) = emitter.next_header()? {
println!("\t- replacement height: {}", height);
// the mempool emission (that follows the first block emission after reorg) should only
@@ -633,7 +762,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
let mempool = emitter
.mempool()?
.into_iter()
.map(|(tx, _)| tx.compute_txid())
.map(|(tx, _)| tx.txid())
.collect::<BTreeSet<_>>();
let exp_mempool = tx_introductions
.iter()
@@ -648,7 +777,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
let mempool = emitter
.mempool()?
.into_iter()
.map(|(tx, _)| tx.compute_txid())
.map(|(tx, _)| tx.txid())
.collect::<BTreeSet<_>>();
let exp_mempool = tx_introductions
.iter()
@@ -694,10 +823,10 @@ fn no_agreement_point() -> anyhow::Result<()> {
// start height is 99
let mut emitter = Emitter::new(
env.rpc_client(),
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.rpc_client().get_block_hash(0)?,
hash: env.client.get_block_hash(0)?,
}),
(PREMINE_COUNT - 2) as u32,
);
@@ -706,27 +835,27 @@ fn no_agreement_point() -> anyhow::Result<()> {
env.mine_blocks(PREMINE_COUNT, None)?;
// emit block 99a
let block_header_99a = emitter.next_header()?.expect("block 99a header").block;
let (_, block_header_99a) = emitter.next_header()?.expect("block 99a header");
let block_hash_99a = block_header_99a.block_hash();
let block_hash_98a = block_header_99a.prev_blockhash;
// emit block 100a
let block_header_100a = emitter.next_header()?.expect("block 100a header").block;
let (_, block_header_100a) = emitter.next_header()?.expect("block 100a header");
let block_hash_100a = block_header_100a.block_hash();
// get hash for block 101a
let block_hash_101a = env.rpc_client().get_block_hash(101)?;
let block_hash_101a = env.client.get_block_hash(101)?;
// invalidate blocks 99a, 100a, 101a
env.rpc_client().invalidate_block(&block_hash_99a)?;
env.rpc_client().invalidate_block(&block_hash_100a)?;
env.rpc_client().invalidate_block(&block_hash_101a)?;
env.client.invalidate_block(&block_hash_99a)?;
env.client.invalidate_block(&block_hash_100a)?;
env.client.invalidate_block(&block_hash_101a)?;
// mine new blocks 99b, 100b, 101b
env.mine_blocks(3, None)?;
// emit block header 99b
let block_header_99b = emitter.next_header()?.expect("block 99b header").block;
let (_, block_header_99b) = emitter.next_header()?.expect("block 99b header");
let block_hash_99b = block_header_99b.block_hash();
let block_hash_98b = block_header_99b.prev_blockhash;

View File

@@ -1,8 +1,8 @@
[package]
name = "bdk_chain"
version = "0.16.0"
version = "0.6.0"
edition = "2021"
rust-version = "1.63"
rust-version = "1.57"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_chain"
@@ -13,18 +13,19 @@ readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitcoin = { version = "0.32.0", default-features = false }
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive", "rc"] }
# For no-std, remember to enable the bitcoin/no-std feature
bitcoin = { version = "0.30.0", default-features = false }
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
# note versions > 0.9.1 breaks ours 1.57.0 MSRV.
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
miniscript = { version = "12.0.0", optional = true, default-features = false }
miniscript = { version = "10.0.0", optional = true, default-features = false }
[dev-dependencies]
rand = "0.8"
proptest = "1.2.0"
[features]
default = ["std", "miniscript"]
std = ["bitcoin/std", "miniscript?/std"]
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
default = ["std"]
std = ["bitcoin/std", "miniscript/std"]
serde = ["serde_crate", "bitcoin/serde"]

View File

@@ -9,7 +9,7 @@ use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY};
pub enum ChainPosition<A> {
/// The chain data is seen as confirmed, and in anchored by `A`.
Confirmed(A),
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
/// The chain data is seen in mempool at this given timestamp.
Unconfirmed(u64),
}
@@ -48,14 +48,14 @@ impl<A: Anchor> ChainPosition<A> {
serde(crate = "serde_crate")
)]
pub enum ConfirmationTime {
/// The transaction is confirmed
/// The confirmed variant.
Confirmed {
/// Confirmation height.
height: u32,
/// Confirmation time in unix seconds.
time: u64,
},
/// The transaction is unconfirmed
/// The unconfirmed variant.
Unconfirmed {
/// The last-seen timestamp in unix seconds.
last_seen: u64,
@@ -81,7 +81,7 @@ impl From<ChainPosition<ConfirmationTimeHeightAnchor>> for ConfirmationTime {
height: a.confirmation_height,
time: a.confirmation_time,
},
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
ChainPosition::Unconfirmed(_) => Self::Unconfirmed { last_seen: 0 },
}
}
}
@@ -147,8 +147,6 @@ impl From<(&u32, &BlockHash)> for BlockId {
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
///
/// Note that the confirmation block and the anchor block can be different here.
///
/// Refer to [`Anchor`] for more details.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
@@ -157,12 +155,13 @@ impl From<(&u32, &BlockHash)> for BlockId {
serde(crate = "serde_crate")
)]
pub struct ConfirmationHeightAnchor {
/// The anchor block.
pub anchor_block: BlockId,
/// The exact confirmation height of the transaction.
///
/// It is assumed that this value is never larger than the height of the anchor block.
pub confirmation_height: u32,
/// The anchor block.
pub anchor_block: BlockId,
}
impl Anchor for ConfirmationHeightAnchor {
@@ -187,8 +186,6 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
/// transaction.
///
/// Note that the confirmation block and the anchor block can be different here.
///
/// Refer to [`Anchor`] for more details.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
@@ -197,12 +194,12 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
serde(crate = "serde_crate")
)]
pub struct ConfirmationTimeHeightAnchor {
/// The confirmation height of the transaction being anchored.
pub confirmation_height: u32,
/// The confirmation time of the transaction being anchored.
pub confirmation_time: u64,
/// The anchor block.
pub anchor_block: BlockId,
/// The confirmation height of the chain data being anchored.
pub confirmation_height: u32,
/// The confirmation time of the chain data being anchored.
pub confirmation_time: u64,
}
impl Anchor for ConfirmationTimeHeightAnchor {
@@ -228,12 +225,12 @@ impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
/// A `TxOut` with as much data as we can retrieve about it
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FullTxOut<A> {
/// The position of the transaction in `outpoint` in the overall chain.
pub chain_position: ChainPosition<A>,
/// The location of the `TxOut`.
pub outpoint: OutPoint,
/// The `TxOut`.
pub txout: TxOut,
/// The position of the transaction in `outpoint` in the overall chain.
pub chain_position: ChainPosition<A>,
/// The txid and chain position of the transaction (if any) that has spent this output.
pub spent_by: Option<(ChainPosition<A>, Txid)>,
/// Whether this output is on a coinbase transaction.
@@ -298,35 +295,3 @@ impl<A: Anchor> FullTxOut<A> {
true
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn chain_position_ord() {
let unconf1 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(10);
let unconf2 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(20);
let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
confirmation_height: 9,
anchor_block: BlockId {
height: 20,
..Default::default()
},
});
let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
confirmation_height: 12,
anchor_block: BlockId {
height: 15,
..Default::default()
},
});
assert!(unconf2 > unconf1, "higher last_seen means higher ord");
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
assert!(
conf2 > conf1,
"confirmation_height is higher then it should be higher ord"
);
}
}

View File

@@ -3,7 +3,7 @@ use crate::BlockId;
/// Represents a service that tracks the blockchain.
///
/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
/// is an ancestor of the `chain_tip`.
/// is an ancestor of another "static block".
///
/// [`is_block_in_chain`]: Self::is_block_in_chain
pub trait ChainOracle {

View File

@@ -1,89 +0,0 @@
/// A changeset containing [`crate`] structures typically persisted together.
#[cfg(feature = "miniscript")]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(crate::serde::Deserialize, crate::serde::Serialize),
serde(
crate = "crate::serde",
bound(
deserialize = "A: Ord + crate::serde::Deserialize<'de>, K: Ord + crate::serde::Deserialize<'de>",
serialize = "A: Ord + crate::serde::Serialize, K: Ord + crate::serde::Serialize",
),
)
)]
pub struct CombinedChangeSet<K, A> {
/// Changes to the [`LocalChain`](crate::local_chain::LocalChain).
pub chain: crate::local_chain::ChangeSet,
/// Changes to [`IndexedTxGraph`](crate::indexed_tx_graph::IndexedTxGraph).
pub indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,
/// Stores the network type of the transaction data.
pub network: Option<bitcoin::Network>,
}
#[cfg(feature = "miniscript")]
impl<K, A> core::default::Default for CombinedChangeSet<K, A> {
fn default() -> Self {
Self {
chain: core::default::Default::default(),
indexed_tx_graph: core::default::Default::default(),
network: None,
}
}
}
#[cfg(feature = "miniscript")]
impl<K: Ord, A: crate::Anchor> crate::Append for CombinedChangeSet<K, A> {
fn append(&mut self, other: Self) {
crate::Append::append(&mut self.chain, other.chain);
crate::Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph);
if other.network.is_some() {
debug_assert!(
self.network.is_none() || self.network == other.network,
"network type must either be just introduced or remain the same"
);
self.network = other.network;
}
}
fn is_empty(&self) -> bool {
self.chain.is_empty() && self.indexed_tx_graph.is_empty() && self.network.is_none()
}
}
#[cfg(feature = "miniscript")]
impl<K, A> From<crate::local_chain::ChangeSet> for CombinedChangeSet<K, A> {
fn from(chain: crate::local_chain::ChangeSet) -> Self {
Self {
chain,
..Default::default()
}
}
}
#[cfg(feature = "miniscript")]
impl<K, A> From<crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>>
for CombinedChangeSet<K, A>
{
fn from(
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,
) -> Self {
Self {
indexed_tx_graph,
..Default::default()
}
}
}
#[cfg(feature = "miniscript")]
impl<K, A> From<crate::keychain::ChangeSet<K>> for CombinedChangeSet<K, A> {
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
Self {
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet {
indexer,
..Default::default()
},
..Default::default()
}
}
}

View File

@@ -1,29 +1,10 @@
use crate::{
alloc::{string::ToString, vec::Vec},
miniscript::{Descriptor, DescriptorPublicKey},
};
use bitcoin::hashes::{hash_newtype, sha256, Hash};
hash_newtype! {
/// Represents the ID of a descriptor, defined as the sha256 hash of
/// the descriptor string, checksum excluded.
///
/// This is useful for having a fixed-length unique representation of a descriptor,
/// in particular, we use it to persist application state changes related to the
/// descriptor without having to re-write the whole descriptor each time.
///
pub struct DescriptorId(pub sha256::Hash);
}
use crate::miniscript::{Descriptor, DescriptorPublicKey};
/// A trait to extend the functionality of a miniscript descriptor.
pub trait DescriptorExt {
/// Returns the minimum value (in satoshis) at which an output is broadcastable.
/// Panics if the descriptor wildcard is hardened.
fn dust_value(&self) -> u64;
/// Returns the descriptor id, calculated as the sha256 of the descriptor, checksum not
/// included.
fn descriptor_id(&self) -> DescriptorId;
}
impl DescriptorExt for Descriptor<DescriptorPublicKey> {
@@ -31,14 +12,7 @@ impl DescriptorExt for Descriptor<DescriptorPublicKey> {
self.at_derivation_index(0)
.expect("descriptor can't have hardened derivation")
.script_pubkey()
.minimal_non_dust()
.dust_value()
.to_sat()
}
fn descriptor_id(&self) -> DescriptorId {
let desc = self.to_string();
let desc_without_checksum = desc.split('#').next().expect("Must be here");
let descriptor_bytes = <Vec<u8>>::from(desc_without_checksum.as_bytes());
DescriptorId(sha256::Hash::hash(&descriptor_bytes))
}
}

View File

@@ -1,16 +1,19 @@
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the
//! [`IndexedTxGraph`] documentation for more.
//! Contains the [`IndexedTxGraph`] structure and associated types.
//!
//! This is essentially a [`TxGraph`] combined with an indexer.
use alloc::vec::Vec;
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
use crate::{
keychain,
tx_graph::{self, TxGraph},
Anchor, AnchorFromBlockPosition, Append, BlockId,
};
/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
/// A struct that combines [`TxGraph`] and an [`Indexer`] implementation.
///
/// It ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
/// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
#[derive(Debug)]
pub struct IndexedTxGraph<A, I> {
/// Transaction index.
@@ -143,7 +146,7 @@ where
let mut graph = tx_graph::ChangeSet::default();
for (tx, anchors) in txs {
if self.index.is_tx_relevant(tx) {
let txid = tx.compute_txid();
let txid = tx.txid();
graph.append(self.graph.insert_tx(tx.clone()));
for anchor in anchors {
graph.append(self.graph.insert_anchor(txid, anchor));
@@ -223,26 +226,20 @@ where
/// Irrelevant transactions in `txs` will be ignored.
pub fn apply_block_relevant(
&mut self,
block: &Block,
block: Block,
height: u32,
) -> ChangeSet<A, I::ChangeSet> {
let block_id = BlockId {
hash: block.block_hash(),
height,
};
let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
for (tx_pos, tx) in block.txdata.iter().enumerate() {
changeset.indexer.append(self.index.index_tx(tx));
if self.index.is_tx_relevant(tx) {
let txid = tx.compute_txid();
let anchor = A::from_block_position(block, block_id, tx_pos);
changeset.graph.append(self.graph.insert_tx(tx.clone()));
changeset
.graph
.append(self.graph.insert_anchor(txid, anchor));
}
}
changeset
let txs = block.txdata.iter().enumerate().map(|(tx_pos, tx)| {
(
tx,
core::iter::once(A::from_block_position(&block, block_id, tx_pos)),
)
});
self.batch_insert_relevant(txs)
}
/// Batch insert all transactions of the given `block` of `height`.
@@ -261,7 +258,7 @@ where
let mut graph = tx_graph::ChangeSet::default();
for (tx_pos, tx) in block.txdata.iter().enumerate() {
let anchor = A::from_block_position(&block, block_id, tx_pos);
graph.append(self.graph.insert_anchor(tx.compute_txid(), anchor));
graph.append(self.graph.insert_anchor(tx.txid(), anchor));
graph.append(self.graph.insert_tx(tx.clone()));
}
let indexer = self.index_tx_graph_changeset(&graph);
@@ -269,7 +266,7 @@ where
}
}
/// Represents changes to an [`IndexedTxGraph`].
/// A structure that represents changes to an [`IndexedTxGraph`].
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serde",
@@ -319,9 +316,8 @@ impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
}
}
#[cfg(feature = "miniscript")]
impl<A, K> From<crate::keychain::ChangeSet<K>> for ChangeSet<A, crate::keychain::ChangeSet<K>> {
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
impl<A, K> From<keychain::ChangeSet<K>> for ChangeSet<A, keychain::ChangeSet<K>> {
fn from(indexer: keychain::ChangeSet<K>) -> Self {
Self {
graph: Default::default(),
indexer,
@@ -352,9 +348,3 @@ pub trait Indexer {
/// Determines whether the transaction should be included in the index.
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
}
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
fn as_ref(&self) -> &TxGraph<A> {
&self.graph
}
}

View File

@@ -10,12 +10,76 @@
//!
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
use crate::{collections::BTreeMap, Append};
#[cfg(feature = "miniscript")]
mod txout_index;
use bitcoin::{Amount, ScriptBuf};
#[cfg(feature = "miniscript")]
pub use txout_index::*;
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
/// It maps each keychain `K` to its last revealed index.
///
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet] are
/// monotone in that they will never decrease the revealed derivation index.
///
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(
crate = "serde_crate",
bound(
deserialize = "K: Ord + serde::Deserialize<'de>",
serialize = "K: Ord + serde::Serialize"
)
)
)]
#[must_use]
pub struct ChangeSet<K>(pub BTreeMap<K, u32>);
impl<K> ChangeSet<K> {
/// Get the inner map of the keychain to its new derivation index.
pub fn as_inner(&self) -> &BTreeMap<K, u32> {
&self.0
}
}
impl<K: Ord> Append for ChangeSet<K> {
/// Append another [`ChangeSet`] into self.
///
/// If the keychain already exists, increase the index when the other's index > self's index.
/// If the keychain did not exist, append the new keychain.
fn append(&mut self, mut other: Self) {
self.0.iter_mut().for_each(|(key, index)| {
if let Some(other_index) = other.0.remove(key) {
*index = other_index.max(*index);
}
});
self.0.append(&mut other.0);
}
/// Returns whether the changeset are empty.
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<K> Default for ChangeSet<K> {
fn default() -> Self {
Self(Default::default())
}
}
impl<K> AsRef<BTreeMap<K, u32>> for ChangeSet<K> {
fn as_ref(&self) -> &BTreeMap<K, u32> {
&self.0
}
}
/// Balance, differentiated into various categories.
#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[cfg_attr(
@@ -25,13 +89,13 @@ pub use txout_index::*;
)]
pub struct Balance {
/// All coinbase outputs not yet matured
pub immature: Amount,
pub immature: u64,
/// Unconfirmed UTXOs generated by a wallet tx
pub trusted_pending: Amount,
pub trusted_pending: u64,
/// Unconfirmed UTXOs received from an external wallet
pub untrusted_pending: Amount,
pub untrusted_pending: u64,
/// Confirmed and immediately spendable balance
pub confirmed: Amount,
pub confirmed: u64,
}
impl Balance {
@@ -39,21 +103,16 @@ impl Balance {
///
/// This is the balance you can spend right now that shouldn't get cancelled via another party
/// double spending it.
pub fn trusted_spendable(&self) -> Amount {
pub fn trusted_spendable(&self) -> u64 {
self.confirmed + self.trusted_pending
}
/// Get the whole balance visible to the wallet.
pub fn total(&self) -> Amount {
pub fn total(&self) -> u64 {
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
}
}
/// A tuple of keychain index and `T` representing the indexed value.
pub type Indexed<T> = (u32, T);
/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them.
pub type KeychainIndexed<K, T> = ((K, u32), T);
impl core::fmt::Display for Balance {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
@@ -76,3 +135,40 @@ impl core::ops::Add for Balance {
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn append_keychain_derivation_indices() {
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
enum Keychain {
One,
Two,
Three,
Four,
}
let mut lhs_di = BTreeMap::<Keychain, u32>::default();
let mut rhs_di = BTreeMap::<Keychain, u32>::default();
lhs_di.insert(Keychain::One, 7);
lhs_di.insert(Keychain::Two, 0);
rhs_di.insert(Keychain::One, 3);
rhs_di.insert(Keychain::Two, 5);
lhs_di.insert(Keychain::Three, 3);
rhs_di.insert(Keychain::Four, 4);
let mut lhs = ChangeSet(lhs_di);
let rhs = ChangeSet(rhs_di);
lhs.append(rhs);
// Exiting index doesn't update if the new index in `other` is lower than `self`.
assert_eq!(lhs.0.get(&Keychain::One), Some(&7));
// Existing index updates if the new index in `other` is higher than `self`.
assert_eq!(lhs.0.get(&Keychain::Two), Some(&5));
// Existing index is unchanged if keychain doesn't exist in `other`.
assert_eq!(lhs.0.get(&Keychain::Three), Some(&3));
// New keychain gets added if the keychain is in `other` but not in `self`.
assert_eq!(lhs.0.get(&Keychain::Four), Some(&4));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
//! This crate is a collection of core structures for [Bitcoin Dev Kit].
//! This crate is a collection of core structures for [Bitcoin Dev Kit] (alpha release).
//!
//! The goal of this crate is to give wallets the mechanisms needed to:
//!
@@ -12,8 +12,9 @@
//! you do it synchronously or asynchronously. If you know a fact about the blockchain, you can just
//! tell `bdk_chain`'s APIs about it, and that information will be integrated, if it can be done
//! consistently.
//! 2. Data persistence agnostic -- `bdk_chain` does not care where you cache on-chain data, what you
//! cache or how you retrieve it from persistent storage.
//! 2. Error-free APIs.
//! 3. Data persistence agnostic -- `bdk_chain` does not care where you cache on-chain data, what you
//! cache or how you fetch it.
//!
//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/
@@ -28,7 +29,6 @@ pub use chain_data::*;
pub mod indexed_tx_graph;
pub use indexed_tx_graph::IndexedTxGraph;
pub mod keychain;
pub use keychain::{Indexed, KeychainIndexed};
pub mod local_chain;
mod tx_data_traits;
pub mod tx_graph;
@@ -36,6 +36,8 @@ pub use tx_data_traits::*;
pub use tx_graph::TxGraph;
mod chain_oracle;
pub use chain_oracle::*;
mod persist;
pub use persist::*;
#[doc(hidden)]
pub mod example_utils;
@@ -45,14 +47,11 @@ pub use miniscript;
#[cfg(feature = "miniscript")]
mod descriptor_ext;
#[cfg(feature = "miniscript")]
pub use descriptor_ext::{DescriptorExt, DescriptorId};
pub use descriptor_ext::DescriptorExt;
#[cfg(feature = "miniscript")]
mod spk_iter;
#[cfg(feature = "miniscript")]
pub use spk_iter::*;
mod changeset;
pub use changeset::*;
pub mod spk_client;
#[allow(unused_imports)]
#[macro_use]
@@ -61,6 +60,9 @@ extern crate alloc;
#[cfg(feature = "serde")]
pub extern crate serde_crate as serde;
#[cfg(feature = "bincode")]
extern crate bincode;
#[cfg(feature = "std")]
#[macro_use]
extern crate std;

View File

@@ -1,15 +1,13 @@
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
use core::convert::Infallible;
use core::ops::RangeBounds;
use crate::collections::BTreeMap;
use crate::{BlockId, ChainOracle};
use alloc::sync::Arc;
use bitcoin::block::Header;
use bitcoin::BlockHash;
/// The [`ChangeSet`] represents changes to [`LocalChain`].
/// A structure that represents changes to [`LocalChain`].
///
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
@@ -35,42 +33,12 @@ struct CPInner {
prev: Option<Arc<CPInner>>,
}
impl PartialEq for CheckPoint {
fn eq(&self, other: &Self) -> bool {
let self_cps = self.iter().map(|cp| cp.block_id());
let other_cps = other.iter().map(|cp| cp.block_id());
self_cps.eq(other_cps)
}
}
impl CheckPoint {
/// Construct a new base block at the front of a linked list.
pub fn new(block: BlockId) -> Self {
Self(Arc::new(CPInner { block, prev: None }))
}
/// Construct a checkpoint from a list of [`BlockId`]s in ascending height order.
///
/// # Errors
///
/// This method will error if any of the follow occurs:
///
/// - The `blocks` iterator is empty, in which case, the error will be `None`.
/// - The `blocks` iterator is not in ascending height order.
/// - The `blocks` iterator contains multiple [`BlockId`]s of the same height.
///
/// The error type is the last successful checkpoint constructed (if any).
pub fn from_block_ids(
block_ids: impl IntoIterator<Item = BlockId>,
) -> Result<Self, Option<Self>> {
let mut blocks = block_ids.into_iter();
let mut acc = CheckPoint::new(blocks.next().ok_or(None)?);
for id in blocks {
acc = acc.push(id).map_err(Some)?;
}
Ok(acc)
}
/// Construct a checkpoint from the given `header` and block `height`.
///
/// If `header` is of the genesis block, the checkpoint won't have a [`prev`] node. Otherwise,
@@ -96,6 +64,16 @@ impl CheckPoint {
.expect("must construct checkpoint")
}
/// Convenience method to convert the [`CheckPoint`] into an [`Update`].
///
/// For more information, refer to [`Update`].
pub fn into_update(self, introduce_older_blocks: bool) -> Update {
Update {
tip: self,
introduce_older_blocks,
}
}
/// Puts another checkpoint onto the linked list representing the blockchain.
///
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
@@ -147,82 +125,237 @@ impl CheckPoint {
pub fn iter(&self) -> CheckPointIter {
self.clone().into_iter()
}
}
/// Get checkpoint at `height`.
/// A structure that iterates over checkpoints backwards.
pub struct CheckPointIter {
current: Option<Arc<CPInner>>,
}
impl Iterator for CheckPointIter {
type Item = CheckPoint;
fn next(&mut self) -> Option<Self::Item> {
let current = self.current.clone()?;
self.current = current.prev.clone();
Some(CheckPoint(current))
}
}
impl IntoIterator for CheckPoint {
type Item = CheckPoint;
type IntoIter = CheckPointIter;
fn into_iter(self) -> Self::IntoIter {
CheckPointIter {
current: Some(self.0),
}
}
}
/// A struct to update [`LocalChain`].
///
/// This is used as input for [`LocalChain::apply_update`]. It contains the update's chain `tip` and
/// a flag `introduce_older_blocks` which signals whether this update intends to introduce missing
/// blocks to the original chain.
///
/// Block-by-block syncing mechanisms would typically create updates that builds upon the previous
/// tip. In this case, `introduce_older_blocks` would be `false`.
///
/// Script-pubkey based syncing mechanisms may not introduce transactions in a chronological order
/// so some updates require introducing older blocks (to anchor older transactions). For
/// script-pubkey based syncing, `introduce_older_blocks` would typically be `true`.
#[derive(Debug, Clone)]
pub struct Update {
/// The update chain's new tip.
pub tip: CheckPoint,
/// Whether the update allows for introducing older blocks.
///
/// Returns `None` if checkpoint at `height` does not exist`.
pub fn get(&self, height: u32) -> Option<Self> {
self.range(height..=height).next()
/// Refer to [struct-level documentation] for more.
///
/// [struct-level documentation]: Update
pub introduce_older_blocks: bool,
}
/// This is a local implementation of [`ChainOracle`].
#[derive(Debug, Clone)]
pub struct LocalChain {
tip: CheckPoint,
index: BTreeMap<u32, BlockHash>,
}
impl PartialEq for LocalChain {
fn eq(&self, other: &Self) -> bool {
self.index == other.index
}
}
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
fn from(value: LocalChain) -> Self {
value.index
}
}
impl ChainOracle for LocalChain {
type Error = Infallible;
fn is_block_in_chain(
&self,
block: BlockId,
chain_tip: BlockId,
) -> Result<Option<bool>, Self::Error> {
if block.height > chain_tip.height {
return Ok(None);
}
Ok(
match (
self.index.get(&block.height),
self.index.get(&chain_tip.height),
) {
(Some(cp), Some(tip_cp)) => Some(*cp == block.hash && *tip_cp == chain_tip.hash),
_ => None,
},
)
}
/// Iterate checkpoints over a height range.
///
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
/// height).
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
where
R: RangeBounds<u32>,
{
let start_bound = range.start_bound().cloned();
let end_bound = range.end_bound().cloned();
self.iter()
.skip_while(move |cp| match end_bound {
core::ops::Bound::Included(inc_bound) => cp.height() > inc_bound,
core::ops::Bound::Excluded(exc_bound) => cp.height() >= exc_bound,
core::ops::Bound::Unbounded => false,
})
.take_while(move |cp| match start_bound {
core::ops::Bound::Included(inc_bound) => cp.height() >= inc_bound,
core::ops::Bound::Excluded(exc_bound) => cp.height() > exc_bound,
core::ops::Bound::Unbounded => true,
})
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
Ok(self.tip.block_id())
}
}
impl LocalChain {
/// Get the genesis hash.
pub fn genesis_hash(&self) -> BlockHash {
self.index.get(&0).copied().expect("must have genesis hash")
}
/// Inserts `block_id` at its height within the chain.
///
/// The effect of `insert` depends on whether a height already exists. If it doesn't the
/// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after
/// it. If the height already existed and has a conflicting block hash then it will be purged
/// along with all block followin it. The returned chain will have a tip of the `block_id`
/// passed in. Of course, if the `block_id` was already present then this just returns `self`.
/// Construct [`LocalChain`] from genesis `hash`.
#[must_use]
pub fn insert(self, block_id: BlockId) -> Self {
assert_ne!(block_id.height, 0, "cannot insert the genesis block");
pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
let height = 0;
let chain = Self {
tip: CheckPoint::new(BlockId { height, hash }),
index: core::iter::once((height, hash)).collect(),
};
let changeset = chain.initial_changeset();
(chain, changeset)
}
let mut cp = self.clone();
let mut tail = vec![];
let base = loop {
if cp.height() == block_id.height {
if cp.hash() == block_id.hash {
return self;
}
// if we have a conflict we just return the inserted block because the tail is by
// implication invalid.
tail = vec![];
break cp.prev().expect("can't be called on genesis block");
}
if cp.height() < block_id.height {
break cp;
}
tail.push(cp.block_id());
cp = cp.prev().expect("will break before genesis block");
/// Construct a [`LocalChain`] from an initial `changeset`.
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
let genesis_entry = changeset.get(&0).copied().flatten();
let genesis_hash = match genesis_entry {
Some(hash) => hash,
None => return Err(MissingGenesisError),
};
base.extend(core::iter::once(block_id).chain(tail.into_iter().rev()))
.expect("tail is in order")
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
chain.apply_changeset(&changeset)?;
debug_assert!(chain._check_index_is_consistent_with_tip());
debug_assert!(chain._check_changeset_is_applied(&changeset));
Ok(chain)
}
/// Apply `changeset` to the checkpoint.
fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> {
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
let mut chain = Self {
tip,
index: BTreeMap::new(),
};
chain.reindex(0);
if chain.index.get(&0).copied().is_none() {
return Err(MissingGenesisError);
}
debug_assert!(chain._check_index_is_consistent_with_tip());
Ok(chain)
}
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
///
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
/// all of the same chain.
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
if !blocks.contains_key(&0) {
return Err(MissingGenesisError);
}
let mut tip: Option<CheckPoint> = None;
for block in &blocks {
match tip {
Some(curr) => {
tip = Some(
curr.push(BlockId::from(block))
.expect("BTreeMap is ordered"),
)
}
None => tip = Some(CheckPoint::new(BlockId::from(block))),
}
}
let chain = Self {
index: blocks,
tip: tip.expect("already checked to have genesis"),
};
debug_assert!(chain._check_index_is_consistent_with_tip());
Ok(chain)
}
/// Get the highest checkpoint.
pub fn tip(&self) -> CheckPoint {
self.tip.clone()
}
/// Applies the given `update` to the chain.
///
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
///
/// There must be no ambiguity about which of the existing chain's blocks are still valid and
/// which are now invalid. That is, the new chain must implicitly connect to a definite block in
/// the existing chain and invalidate the block after it (if it exists) by including a block at
/// the same height but with a different hash to explicitly exclude it as a connection point.
///
/// Additionally, an empty chain can be updated with any chain, and a chain with a single block
/// can have it's block invalidated by an update chain with a block at the same height but
/// different hash.
///
/// # Errors
///
/// An error will occur if the update does not correctly connect with `self`.
///
/// Refer to [`Update`] for more about the update struct.
///
/// [module-level documentation]: crate::local_chain
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
let changeset = merge_chains(
self.tip.clone(),
update.tip.clone(),
update.introduce_older_blocks,
)?;
// `._check_index_is_consistent_with_tip` and `._check_changeset_is_applied` is called in
// `.apply_changeset`
self.apply_changeset(&changeset)
.map_err(|_| CannotConnectError {
try_include_height: 0,
})?;
Ok(changeset)
}
/// Apply the given `changeset`.
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
if let Some(start_height) = changeset.keys().next().cloned() {
// changes after point of agreement
let mut extension = BTreeMap::default();
// point of agreement
let mut base: Option<CheckPoint> = None;
for cp in self.iter() {
for cp in self.iter_checkpoints() {
if cp.height() >= start_height {
extension.insert(cp.height(), cp.hash());
} else {
@@ -248,257 +381,13 @@ impl CheckPoint {
.expect("extension is strictly greater than base"),
None => LocalChain::from_blocks(extension)?.tip(),
};
self = new_tip;
self.tip = new_tip;
self.reindex(start_height);
debug_assert!(self._check_index_is_consistent_with_tip());
debug_assert!(self._check_changeset_is_applied(changeset));
}
Ok(self)
}
}
/// Iterates over checkpoints backwards.
pub struct CheckPointIter {
current: Option<Arc<CPInner>>,
}
impl Iterator for CheckPointIter {
type Item = CheckPoint;
fn next(&mut self) -> Option<Self::Item> {
let current = self.current.clone()?;
self.current.clone_from(&current.prev);
Some(CheckPoint(current))
}
}
impl IntoIterator for CheckPoint {
type Item = CheckPoint;
type IntoIter = CheckPointIter;
fn into_iter(self) -> Self::IntoIter {
CheckPointIter {
current: Some(self.0),
}
}
}
/// This is a local implementation of [`ChainOracle`].
#[derive(Debug, Clone, PartialEq)]
pub struct LocalChain {
tip: CheckPoint,
}
impl ChainOracle for LocalChain {
type Error = Infallible;
fn is_block_in_chain(
&self,
block: BlockId,
chain_tip: BlockId,
) -> Result<Option<bool>, Self::Error> {
let chain_tip_cp = match self.tip.get(chain_tip.height) {
// we can only determine whether `block` is in chain of `chain_tip` if `chain_tip` can
// be identified in chain
Some(cp) if cp.hash() == chain_tip.hash => cp,
_ => return Ok(None),
};
match chain_tip_cp.get(block.height) {
Some(cp) => Ok(Some(cp.hash() == block.hash)),
None => Ok(None),
}
}
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
Ok(self.tip.block_id())
}
}
impl LocalChain {
/// Get the genesis hash.
pub fn genesis_hash(&self) -> BlockHash {
self.tip.get(0).expect("genesis must exist").hash()
}
/// Construct [`LocalChain`] from genesis `hash`.
#[must_use]
pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
let height = 0;
let chain = Self {
tip: CheckPoint::new(BlockId { height, hash }),
};
let changeset = chain.initial_changeset();
(chain, changeset)
}
/// Construct a [`LocalChain`] from an initial `changeset`.
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
let genesis_entry = changeset.get(&0).copied().flatten();
let genesis_hash = match genesis_entry {
Some(hash) => hash,
None => return Err(MissingGenesisError),
};
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
chain.apply_changeset(&changeset)?;
debug_assert!(chain._check_changeset_is_applied(&changeset));
Ok(chain)
}
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
let genesis_cp = tip.iter().last().expect("must have at least one element");
if genesis_cp.height() != 0 {
return Err(MissingGenesisError);
}
Ok(Self { tip })
}
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
///
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
/// all of the same chain.
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
if !blocks.contains_key(&0) {
return Err(MissingGenesisError);
}
let mut tip: Option<CheckPoint> = None;
for block in &blocks {
match tip {
Some(curr) => {
tip = Some(
curr.push(BlockId::from(block))
.expect("BTreeMap is ordered"),
)
}
None => tip = Some(CheckPoint::new(BlockId::from(block))),
}
}
Ok(Self {
tip: tip.expect("already checked to have genesis"),
})
}
/// Get the highest checkpoint.
pub fn tip(&self) -> CheckPoint {
self.tip.clone()
}
/// Applies the given `update` to the chain.
///
/// The method returns [`ChangeSet`] on success. This represents the changes applied to `self`.
///
/// There must be no ambiguity about which of the existing chain's blocks are still valid and
/// which are now invalid. That is, the new chain must implicitly connect to a definite block in
/// the existing chain and invalidate the block after it (if it exists) by including a block at
/// the same height but with a different hash to explicitly exclude it as a connection point.
///
/// # Errors
///
/// An error will occur if the update does not correctly connect with `self`.
///
/// [module-level documentation]: crate::local_chain
pub fn apply_update(&mut self, update: CheckPoint) -> Result<ChangeSet, CannotConnectError> {
let (new_tip, changeset) = merge_chains(self.tip.clone(), update)?;
self.tip = new_tip;
self._check_changeset_is_applied(&changeset);
Ok(changeset)
}
/// Update the chain with a given [`Header`] at `height` which you claim is connected to a existing block in the chain.
///
/// This is useful when you have a block header that you want to record as part of the chain but
/// don't necessarily know that the `prev_blockhash` is in the chain.
///
/// This will usually insert two new [`BlockId`]s into the chain: the header's block and the
/// header's `prev_blockhash` block. `connected_to` must already be in the chain but is allowed
/// to be `prev_blockhash` (in which case only one new block id will be inserted).
/// To be successful, `connected_to` must be chosen carefully so that `LocalChain`'s [update
/// rules][`apply_update`] are satisfied.
///
/// # Errors
///
/// [`ApplyHeaderError::InconsistentBlocks`] occurs if the `connected_to` block and the
/// [`Header`] is inconsistent. For example, if the `connected_to` block is the same height as
/// `header` or `prev_blockhash`, but has a different block hash. Or if the `connected_to`
/// height is greater than the header's `height`.
///
/// [`ApplyHeaderError::CannotConnect`] occurs if the internal call to [`apply_update`] fails.
///
/// [`apply_update`]: Self::apply_update
pub fn apply_header_connected_to(
&mut self,
header: &Header,
height: u32,
connected_to: BlockId,
) -> Result<ChangeSet, ApplyHeaderError> {
let this = BlockId {
height,
hash: header.block_hash(),
};
let prev = height.checked_sub(1).map(|prev_height| BlockId {
height: prev_height,
hash: header.prev_blockhash,
});
let conn = match connected_to {
// `connected_to` can be ignored if same as `this` or `prev` (duplicate)
conn if conn == this || Some(conn) == prev => None,
// this occurs if:
// - `connected_to` height is the same as `prev`, but different hash
// - `connected_to` height is the same as `this`, but different hash
// - `connected_to` height is greater than `this` (this is not allowed)
conn if conn.height >= height.saturating_sub(1) => {
return Err(ApplyHeaderError::InconsistentBlocks)
}
conn => Some(conn),
};
let update = CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
.expect("block ids must be in order");
self.apply_update(update)
.map_err(ApplyHeaderError::CannotConnect)
}
/// Update the chain with a given [`Header`] connecting it with the previous block.
///
/// This is a convenience method to call [`apply_header_connected_to`] with the `connected_to`
/// parameter being `height-1:prev_blockhash`. If there is no previous block (i.e. genesis), we
/// use the current block as `connected_to`.
///
/// [`apply_header_connected_to`]: LocalChain::apply_header_connected_to
pub fn apply_header(
&mut self,
header: &Header,
height: u32,
) -> Result<ChangeSet, CannotConnectError> {
let connected_to = match height.checked_sub(1) {
Some(prev_height) => BlockId {
height: prev_height,
hash: header.prev_blockhash,
},
None => BlockId {
height,
hash: header.block_hash(),
},
};
self.apply_header_connected_to(header, height, connected_to)
.map_err(|err| match err {
ApplyHeaderError::InconsistentBlocks => {
unreachable!("connected_to is derived from the block so is always consistent")
}
ApplyHeaderError::CannotConnect(err) => err,
})
}
/// Apply the given `changeset`.
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
let old_tip = self.tip.clone();
let new_tip = old_tip.apply_changeset(changeset)?;
self.tip = new_tip;
debug_assert!(self._check_changeset_is_applied(changeset));
Ok(())
}
@@ -508,16 +397,16 @@ impl LocalChain {
///
/// Replacing the block hash of an existing checkpoint will result in an error.
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
if let Some(original_cp) = self.tip.get(block_id.height) {
let original_hash = original_cp.hash();
if let Some(&original_hash) = self.index.get(&block_id.height) {
if original_hash != block_id.hash {
return Err(AlterCheckPointError {
height: block_id.height,
original_hash,
update_hash: Some(block_id.hash),
});
} else {
return Ok(ChangeSet::default());
}
return Ok(ChangeSet::default());
}
let mut changeset = ChangeSet::default();
@@ -531,51 +420,21 @@ impl LocalChain {
Ok(changeset)
}
/// Removes blocks from (and inclusive of) the given `block_id`.
///
/// This will remove blocks with a height equal or greater than `block_id`, but only if
/// `block_id` exists in the chain.
///
/// # Errors
///
/// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
/// genesis block.
pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
let mut remove_from = Option::<CheckPoint>::None;
let mut changeset = ChangeSet::default();
for cp in self.tip().iter() {
let cp_id = cp.block_id();
if cp_id.height < block_id.height {
/// Reindex the heights in the chain from (and including) `from` height
fn reindex(&mut self, from: u32) {
let _ = self.index.split_off(&from);
for cp in self.iter_checkpoints() {
if cp.height() < from {
break;
}
changeset.insert(cp_id.height, None);
if cp_id == block_id {
remove_from = Some(cp);
}
self.index.insert(cp.height(), cp.hash());
}
self.tip = match remove_from.map(|cp| cp.prev()) {
// The checkpoint below the earliest checkpoint to remove will be the new tip.
Some(Some(new_tip)) => new_tip,
// If there is no checkpoint below the earliest checkpoint to remove, it means the
// "earliest checkpoint to remove" is the genesis block. We disallow removing the
// genesis block.
Some(None) => return Err(MissingGenesisError),
// If there is nothing to remove, we return an empty changeset.
None => return Ok(ChangeSet::default()),
};
Ok(changeset)
}
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
/// recover the current chain.
pub fn initial_changeset(&self) -> ChangeSet {
self.tip
.iter()
.map(|cp| {
let block_id = cp.block_id();
(block_id.height, Some(block_id.hash))
})
.collect()
self.index.iter().map(|(k, v)| (*k, Some(*v))).collect()
}
/// Iterate over checkpoints in descending height order.
@@ -585,49 +444,28 @@ impl LocalChain {
}
}
/// Get a reference to the internal index mapping the height to block hash.
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
&self.index
}
fn _check_index_is_consistent_with_tip(&self) -> bool {
let tip_history = self
.tip
.iter()
.map(|cp| (cp.height(), cp.hash()))
.collect::<BTreeMap<_, _>>();
self.index == tip_history
}
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
let mut curr_cp = self.tip.clone();
for (height, exp_hash) in changeset.iter().rev() {
match curr_cp.get(*height) {
Some(query_cp) => {
if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
return false;
}
curr_cp = query_cp;
}
None => {
if exp_hash.is_some() {
return false;
}
}
for (height, exp_hash) in changeset {
if self.index.get(height) != exp_hash.as_ref() {
return false;
}
}
true
}
/// Get checkpoint at given `height` (if it exists).
///
/// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`].
///
/// [`tip`]: LocalChain::tip
pub fn get(&self, height: u32) -> Option<CheckPoint> {
self.tip.get(height)
}
/// Iterate checkpoints over a height range.
///
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
/// height).
///
/// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`].
///
/// [`tip`]: LocalChain::tip
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
where
R: RangeBounds<u32>,
{
self.tip.range(range)
}
}
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
@@ -697,41 +535,14 @@ impl core::fmt::Display for CannotConnectError {
#[cfg(feature = "std")]
impl std::error::Error for CannotConnectError {}
/// The error type for [`LocalChain::apply_header_connected_to`].
#[derive(Debug, Clone, PartialEq)]
pub enum ApplyHeaderError {
/// Occurs when `connected_to` block conflicts with either the current block or previous block.
InconsistentBlocks,
/// Occurs when the update cannot connect with the original chain.
CannotConnect(CannotConnectError),
}
impl core::fmt::Display for ApplyHeaderError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ApplyHeaderError::InconsistentBlocks => write!(
f,
"the `connected_to` block conflicts with either the current or previous block"
),
ApplyHeaderError::CannotConnect(err) => core::fmt::Display::fmt(err, f),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ApplyHeaderError {}
/// Applies `update_tip` onto `original_tip`.
///
/// On success, a tuple is returned `(changeset, can_replace)`. If `can_replace` is true, then the
/// `update_tip` can replace the `original_tip`.
fn merge_chains(
original_tip: CheckPoint,
update_tip: CheckPoint,
) -> Result<(CheckPoint, ChangeSet), CannotConnectError> {
introduce_older_blocks: bool,
) -> Result<ChangeSet, CannotConnectError> {
let mut changeset = ChangeSet::default();
let mut orig = original_tip.iter();
let mut update = update_tip.iter();
let mut orig = original_tip.into_iter();
let mut update = update_tip.into_iter();
let mut curr_orig = None;
let mut curr_update = None;
let mut prev_orig: Option<CheckPoint> = None;
@@ -740,12 +551,6 @@ fn merge_chains(
let mut prev_orig_was_invalidated = false;
let mut potentially_invalidated_heights = vec![];
// If we can, we want to return the update tip as the new tip because this allows checkpoints
// in multiple locations to keep the same `Arc` pointers when they are being updated from each
// other using this function. We can do this as long as long as the update contains every
// block's height of the original chain.
let mut is_update_height_superset_of_original = true;
// To find the difference between the new chain and the original we iterate over both of them
// from the tip backwards in tandem. We always dealing with the highest one from either chain
// first and move to the next highest. The crucial logic is applied when they have blocks at the
@@ -771,8 +576,6 @@ fn merge_chains(
prev_orig_was_invalidated = false;
prev_orig = curr_orig.take();
is_update_height_superset_of_original = false;
// OPTIMIZATION: we have run out of update blocks so we don't need to continue
// iterating because there's no possibility of adding anything to changeset.
if u.is_none() {
@@ -795,20 +598,12 @@ fn merge_chains(
}
point_of_agreement_found = true;
prev_orig_was_invalidated = false;
// OPTIMIZATION 1 -- If we know that older blocks cannot be introduced without
// invalidation, we can break after finding the point of agreement.
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
// can guarantee that no older blocks are introduced.
if Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
if is_update_height_superset_of_original {
return Ok((update_tip, changeset));
} else {
let new_tip =
original_tip.apply_changeset(&changeset).map_err(|_| {
CannotConnectError {
try_include_height: 0,
}
})?;
return Ok((new_tip, changeset));
}
if !introduce_older_blocks || Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
return Ok(changeset);
}
} else {
// We have an invalidation height so we set the height to the updated hash and
@@ -842,10 +637,5 @@ fn merge_chains(
}
}
let new_tip = original_tip
.apply_changeset(&changeset)
.map_err(|_| CannotConnectError {
try_include_height: 0,
})?;
Ok((new_tip, changeset))
Ok(changeset)
}

View File

@@ -0,0 +1,97 @@
use core::convert::Infallible;
use crate::Append;
/// `Persist` wraps a [`PersistBackend`] (`B`) to create a convenient staging area for changes (`C`)
/// before they are persisted.
///
/// Not all changes to the in-memory representation needs to be written to disk right away, so
/// [`Persist::stage`] can be used to *stage* changes first and then [`Persist::commit`] can be used
/// to write changes to disk.
#[derive(Debug)]
pub struct Persist<B, C> {
backend: B,
stage: C,
}
impl<B, C> Persist<B, C>
where
B: PersistBackend<C>,
C: Default + Append,
{
/// Create a new [`Persist`] from [`PersistBackend`].
pub fn new(backend: B) -> Self {
Self {
backend,
stage: Default::default(),
}
}
/// Stage a `changeset` to be committed later with [`commit`].
///
/// [`commit`]: Self::commit
pub fn stage(&mut self, changeset: C) {
self.stage.append(changeset)
}
/// Get the changes that have not been committed yet.
pub fn staged(&self) -> &C {
&self.stage
}
/// Commit the staged changes to the underlying persistence backend.
///
/// Changes that are committed (if any) are returned.
///
/// # Error
///
/// Returns a backend-defined error if this fails.
pub fn commit(&mut self) -> Result<Option<C>, B::WriteError> {
if self.stage.is_empty() {
return Ok(None);
}
self.backend
.write_changes(&self.stage)
// if written successfully, take and return `self.stage`
.map(|_| Some(core::mem::take(&mut self.stage)))
}
}
/// A persistence backend for [`Persist`].
///
/// `C` represents the changeset; a datatype that records changes made to in-memory data structures
/// that are to be persisted, or retrieved from persistence.
pub trait PersistBackend<C> {
/// The error the backend returns when it fails to write.
type WriteError: core::fmt::Debug;
/// The error the backend returns when it fails to load changesets `C`.
type LoadError: core::fmt::Debug;
/// Writes a changeset to the persistence backend.
///
/// It is up to the backend what it does with this. It could store every changeset in a list or
/// it inserts the actual changes into a more structured database. All it needs to guarantee is
/// that [`load_from_persistence`] restores a keychain tracker to what it should be if all
/// changesets had been applied sequentially.
///
/// [`load_from_persistence`]: Self::load_from_persistence
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
/// Return the aggregate changeset `C` from persistence.
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError>;
}
impl<C> PersistBackend<C> for () {
type WriteError = Infallible;
type LoadError = Infallible;
fn write_changes(&mut self, _changeset: &C) -> Result<(), Self::WriteError> {
Ok(())
}
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
Ok(None)
}
}

View File

@@ -1,389 +0,0 @@
//! Helper types for spk-based blockchain clients.
use crate::{
collections::BTreeMap, keychain::Indexed, local_chain::CheckPoint,
ConfirmationTimeHeightAnchor, TxGraph,
};
use alloc::boxed::Box;
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
use core::marker::PhantomData;
/// Data required to perform a spk-based blockchain client sync.
///
/// A client sync fetches relevant chain data for a known list of scripts, transaction ids and
/// outpoints. The sync process also updates the chain from the given [`CheckPoint`].
pub struct SyncRequest {
/// A checkpoint for the current chain [`LocalChain::tip`].
/// The sync process will return a new chain update that extends this tip.
///
/// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip
pub chain_tip: CheckPoint,
/// Transactions that spend from or to these indexed script pubkeys.
pub spks: Box<dyn ExactSizeIterator<Item = ScriptBuf> + Send>,
/// Transactions with these txids.
pub txids: Box<dyn ExactSizeIterator<Item = Txid> + Send>,
/// Transactions with these outpoints or spent from these outpoints.
pub outpoints: Box<dyn ExactSizeIterator<Item = OutPoint> + Send>,
}
impl SyncRequest {
/// Construct a new [`SyncRequest`] from a given `cp` tip.
pub fn from_chain_tip(cp: CheckPoint) -> Self {
Self {
chain_tip: cp,
spks: Box::new(core::iter::empty()),
txids: Box::new(core::iter::empty()),
outpoints: Box::new(core::iter::empty()),
}
}
/// Set the [`Script`]s that will be synced against.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn set_spks(
mut self,
spks: impl IntoIterator<IntoIter = impl ExactSizeIterator<Item = ScriptBuf> + Send + 'static>,
) -> Self {
self.spks = Box::new(spks.into_iter());
self
}
/// Set the [`Txid`]s that will be synced against.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn set_txids(
mut self,
txids: impl IntoIterator<IntoIter = impl ExactSizeIterator<Item = Txid> + Send + 'static>,
) -> Self {
self.txids = Box::new(txids.into_iter());
self
}
/// Set the [`OutPoint`]s that will be synced against.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn set_outpoints(
mut self,
outpoints: impl IntoIterator<
IntoIter = impl ExactSizeIterator<Item = OutPoint> + Send + 'static,
>,
) -> Self {
self.outpoints = Box::new(outpoints.into_iter());
self
}
/// Chain on additional [`Script`]s that will be synced against.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn chain_spks(
mut self,
spks: impl IntoIterator<
IntoIter = impl ExactSizeIterator<Item = ScriptBuf> + Send + 'static,
Item = ScriptBuf,
>,
) -> Self {
self.spks = Box::new(ExactSizeChain::new(self.spks, spks.into_iter()));
self
}
/// Chain on additional [`Txid`]s that will be synced against.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn chain_txids(
mut self,
txids: impl IntoIterator<
IntoIter = impl ExactSizeIterator<Item = Txid> + Send + 'static,
Item = Txid,
>,
) -> Self {
self.txids = Box::new(ExactSizeChain::new(self.txids, txids.into_iter()));
self
}
/// Chain on additional [`OutPoint`]s that will be synced against.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn chain_outpoints(
mut self,
outpoints: impl IntoIterator<
IntoIter = impl ExactSizeIterator<Item = OutPoint> + Send + 'static,
Item = OutPoint,
>,
) -> Self {
self.outpoints = Box::new(ExactSizeChain::new(self.outpoints, outpoints.into_iter()));
self
}
/// Add a closure that will be called for [`Script`]s previously added to this request.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn inspect_spks(
mut self,
mut inspect: impl FnMut(&Script) + Send + Sync + 'static,
) -> Self {
self.spks = Box::new(self.spks.inspect(move |spk| inspect(spk)));
self
}
/// Add a closure that will be called for [`Txid`]s previously added to this request.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn inspect_txids(mut self, mut inspect: impl FnMut(&Txid) + Send + Sync + 'static) -> Self {
self.txids = Box::new(self.txids.inspect(move |txid| inspect(txid)));
self
}
/// Add a closure that will be called for [`OutPoint`]s previously added to this request.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn inspect_outpoints(
mut self,
mut inspect: impl FnMut(&OutPoint) + Send + Sync + 'static,
) -> Self {
self.outpoints = Box::new(self.outpoints.inspect(move |op| inspect(op)));
self
}
/// Populate the request with revealed script pubkeys from `index` with the given `spk_range`.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[cfg(feature = "miniscript")]
#[must_use]
pub fn populate_with_revealed_spks<K: Clone + Ord + core::fmt::Debug + Send + Sync>(
self,
index: &crate::keychain::KeychainTxOutIndex<K>,
spk_range: impl core::ops::RangeBounds<K>,
) -> Self {
use alloc::borrow::ToOwned;
use alloc::vec::Vec;
self.chain_spks(
index
.revealed_spks(spk_range)
.map(|(_, spk)| spk.to_owned())
.collect::<Vec<_>>(),
)
}
}
/// Data returned from a spk-based blockchain client sync.
///
/// See also [`SyncRequest`].
pub struct SyncResult<A = ConfirmationTimeHeightAnchor> {
/// The update to apply to the receiving [`TxGraph`].
pub graph_update: TxGraph<A>,
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
pub chain_update: CheckPoint,
}
/// Data required to perform a spk-based blockchain client full scan.
///
/// A client full scan iterates through all the scripts for the given keychains, fetching relevant
/// data until some stop gap number of scripts is found that have no data. This operation is
/// generally only used when importing or restoring previously used keychains in which the list of
/// used scripts is not known. The full scan process also updates the chain from the given [`CheckPoint`].
pub struct FullScanRequest<K> {
/// A checkpoint for the current [`LocalChain::tip`].
/// The full scan process will return a new chain update that extends this tip.
///
/// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip
pub chain_tip: CheckPoint,
/// Iterators of script pubkeys indexed by the keychain index.
pub spks_by_keychain: BTreeMap<K, Box<dyn Iterator<Item = Indexed<ScriptBuf>> + Send>>,
}
impl<K: Ord + Clone> FullScanRequest<K> {
/// Construct a new [`FullScanRequest`] from a given `chain_tip`.
#[must_use]
pub fn from_chain_tip(chain_tip: CheckPoint) -> Self {
Self {
chain_tip,
spks_by_keychain: BTreeMap::new(),
}
}
/// Construct a new [`FullScanRequest`] from a given `chain_tip` and `index`.
///
/// Unbounded script pubkey iterators for each keychain (`K`) are extracted using
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`] and is used to populate the
/// [`FullScanRequest`].
///
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::keychain::KeychainTxOutIndex::all_unbounded_spk_iters
#[cfg(feature = "miniscript")]
#[must_use]
pub fn from_keychain_txout_index(
chain_tip: CheckPoint,
index: &crate::keychain::KeychainTxOutIndex<K>,
) -> Self
where
K: core::fmt::Debug,
{
let mut req = Self::from_chain_tip(chain_tip);
for (keychain, spks) in index.all_unbounded_spk_iters() {
req = req.set_spks_for_keychain(keychain, spks);
}
req
}
/// Set the [`Script`]s for a given `keychain`.
///
/// This consumes the [`FullScanRequest`] and returns the updated one.
#[must_use]
pub fn set_spks_for_keychain(
mut self,
keychain: K,
spks: impl IntoIterator<IntoIter = impl Iterator<Item = Indexed<ScriptBuf>> + Send + 'static>,
) -> Self {
self.spks_by_keychain
.insert(keychain, Box::new(spks.into_iter()));
self
}
/// Chain on additional [`Script`]s that will be synced against.
///
/// This consumes the [`FullScanRequest`] and returns the updated one.
#[must_use]
pub fn chain_spks_for_keychain(
mut self,
keychain: K,
spks: impl IntoIterator<IntoIter = impl Iterator<Item = Indexed<ScriptBuf>> + Send + 'static>,
) -> Self {
match self.spks_by_keychain.remove(&keychain) {
// clippy here suggests to remove `into_iter` from `spks.into_iter()`, but doing so
// results in a compilation error
#[allow(clippy::useless_conversion)]
Some(keychain_spks) => self
.spks_by_keychain
.insert(keychain, Box::new(keychain_spks.chain(spks.into_iter()))),
None => self
.spks_by_keychain
.insert(keychain, Box::new(spks.into_iter())),
};
self
}
/// Add a closure that will be called for every [`Script`] previously added to any keychain in
/// this request.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn inspect_spks_for_all_keychains(
mut self,
inspect: impl FnMut(K, u32, &Script) + Send + Sync + Clone + 'static,
) -> Self
where
K: Send + 'static,
{
for (keychain, spks) in core::mem::take(&mut self.spks_by_keychain) {
let mut inspect = inspect.clone();
self.spks_by_keychain.insert(
keychain.clone(),
Box::new(spks.inspect(move |(i, spk)| inspect(keychain.clone(), *i, spk))),
);
}
self
}
/// Add a closure that will be called for every [`Script`] previously added to a given
/// `keychain` in this request.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn inspect_spks_for_keychain(
mut self,
keychain: K,
mut inspect: impl FnMut(u32, &Script) + Send + Sync + 'static,
) -> Self
where
K: Send + 'static,
{
if let Some(spks) = self.spks_by_keychain.remove(&keychain) {
self.spks_by_keychain.insert(
keychain,
Box::new(spks.inspect(move |(i, spk)| inspect(*i, spk))),
);
}
self
}
}
/// Data returned from a spk-based blockchain client full scan.
///
/// See also [`FullScanRequest`].
pub struct FullScanResult<K, A = ConfirmationTimeHeightAnchor> {
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
pub graph_update: TxGraph<A>,
/// The update to apply to the receiving [`TxGraph`].
pub chain_update: CheckPoint,
/// Last active indices for the corresponding keychains (`K`).
pub last_active_indices: BTreeMap<K, u32>,
}
/// A version of [`core::iter::Chain`] which can combine two [`ExactSizeIterator`]s to form a new
/// [`ExactSizeIterator`].
///
/// The danger of this is explained in [the `ExactSizeIterator` docs]
/// (https://doc.rust-lang.org/core/iter/trait.ExactSizeIterator.html#when-shouldnt-an-adapter-be-exactsizeiterator).
/// This does not apply here since it would be impossible to scan an item count that overflows
/// `usize` anyway.
struct ExactSizeChain<A, B, I> {
a: Option<A>,
b: Option<B>,
i: PhantomData<I>,
}
impl<A, B, I> ExactSizeChain<A, B, I> {
fn new(a: A, b: B) -> Self {
ExactSizeChain {
a: Some(a),
b: Some(b),
i: PhantomData,
}
}
}
impl<A, B, I> Iterator for ExactSizeChain<A, B, I>
where
A: Iterator<Item = I>,
B: Iterator<Item = I>,
{
type Item = I;
fn next(&mut self) -> Option<Self::Item> {
if let Some(a) = &mut self.a {
let item = a.next();
if item.is_some() {
return item;
}
self.a = None;
}
if let Some(b) = &mut self.b {
let item = b.next();
if item.is_some() {
return item;
}
self.b = None;
}
None
}
}
impl<A, B, I> ExactSizeIterator for ExactSizeChain<A, B, I>
where
A: ExactSizeIterator<Item = I>,
B: ExactSizeIterator<Item = I>,
{
fn len(&self) -> usize {
let a_len = self.a.as_ref().map(|a| a.len()).unwrap_or(0);
let b_len = self.b.as_ref().map(|a| a.len()).unwrap_or(0);
a_len + b_len
}
}

View File

@@ -1,6 +1,5 @@
use crate::{
bitcoin::{secp256k1::Secp256k1, ScriptBuf},
keychain::Indexed,
miniscript::{Descriptor, DescriptorPublicKey},
};
use core::{borrow::Borrow, ops::Bound, ops::RangeBounds};
@@ -44,24 +43,18 @@ impl<D> SpkIterator<D>
where
D: Borrow<Descriptor<DescriptorPublicKey>>,
{
/// Create a new script pubkey iterator from `descriptor`.
///
/// This iterates from derivation index 0 and stops at index 0x7FFFFFFF (as specified in
/// BIP-32). Non-wildcard descriptors will only return one script pubkey at derivation index 0.
///
/// Use [`new_with_range`](SpkIterator::new_with_range) to create an iterator with a specified
/// derivation index range.
/// Creates a new script pubkey iterator starting at 0 from a descriptor.
pub fn new(descriptor: D) -> Self {
SpkIterator::new_with_range(descriptor, 0..=BIP32_MAX_INDEX)
}
/// Create a new script pubkey iterator from `descriptor` and a given `range`.
///
/// Non-wildcard descriptors will only emit a single script pubkey (at derivation index 0).
/// Wildcard descriptors have an end-bound of 0x7FFFFFFF (inclusive).
///
/// Refer to [`new`](SpkIterator::new) for more.
pub fn new_with_range<R>(descriptor: D, range: R) -> Self
// Creates a new script pubkey iterator from a descriptor with a given range.
// If the descriptor doesn't have a wildcard, we shorten whichever range you pass in
// to have length <= 1. This means that if you pass in 0..0 or 0..1 the range will
// remain the same, but if you pass in 0..10, we'll shorten it to 0..1
// Also note that if the descriptor doesn't have a wildcard, passing in a range starting
// from n > 0, will return an empty iterator.
pub(crate) fn new_with_range<R>(descriptor: D, range: R) -> Self
where
R: RangeBounds<u32>,
{
@@ -80,6 +73,13 @@ where
// Because `end` is exclusive, we want the maximum value to be BIP32_MAX_INDEX + 1.
end = end.min(BIP32_MAX_INDEX + 1);
if !descriptor.borrow().has_wildcard() {
// The length of the range should be at most 1
if end != start {
end = start + 1;
}
}
Self {
next_index: start,
end,
@@ -87,18 +87,13 @@ where
secp: Secp256k1::verification_only(),
}
}
/// Get a reference to the internal descriptor.
pub fn descriptor(&self) -> &D {
&self.descriptor
}
}
impl<D> Iterator for SpkIterator<D>
where
D: Borrow<Descriptor<DescriptorPublicKey>>,
{
type Item = Indexed<ScriptBuf>;
type Item = (u32, ScriptBuf);
fn next(&mut self) -> Option<Self::Item> {
// For non-wildcard descriptors, we expect the first element to be Some((0, spk)), then None after.
@@ -153,18 +148,14 @@ mod test {
Descriptor<DescriptorPublicKey>,
Descriptor<DescriptorPublicKey>,
) {
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
let secp = Secp256k1::signing_only();
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
let _ = txout_index
.insert_descriptor(TestKeychain::External, external_descriptor.clone())
.unwrap();
let _ = txout_index
.insert_descriptor(TestKeychain::Internal, internal_descriptor.clone())
.unwrap();
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
(txout_index, external_descriptor, internal_descriptor)
}
@@ -254,19 +245,18 @@ mod test {
SpkIterator::new_with_range(&no_wildcard_descriptor, 1..=2).next(),
None
);
assert_eq!(
SpkIterator::new_with_range(&no_wildcard_descriptor, 10..11).next(),
None
);
assert_eq!(
SpkIterator::new_with_range(&no_wildcard_descriptor, 10..=10).next(),
None
);
}
// The following dummy traits were created to test if SpkIterator is working properly.
trait TestSendStatic: Send + 'static {
fn test(&self) -> u32 {
20
}
}
impl TestSendStatic for SpkIterator<Descriptor<DescriptorPublicKey>> {
fn test(&self) -> u32 {
20
}
}
}
#[test]
fn spk_iterator_is_send_and_static() {
fn is_send_and_static<A: Send + 'static>() {}
is_send_and_static::<SpkIterator<Descriptor<DescriptorPublicKey>>>()
}

View File

@@ -4,7 +4,7 @@ use crate::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
indexed_tx_graph::Indexer,
};
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
use bitcoin::{self, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
///
@@ -52,7 +52,7 @@ impl<I> Default for SpkTxOutIndex<I> {
}
}
impl<I: Clone + Ord + core::fmt::Debug> Indexer for SpkTxOutIndex<I> {
impl<I: Clone + Ord> Indexer for SpkTxOutIndex<I> {
type ChangeSet = ();
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
@@ -76,7 +76,7 @@ impl<I: Clone + Ord + core::fmt::Debug> Indexer for SpkTxOutIndex<I> {
}
}
impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
impl<I: Clone + Ord> SpkTxOutIndex<I> {
/// Scans a transaction's outputs for matching script pubkeys.
///
/// Typically, this is used in two situations:
@@ -86,7 +86,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
/// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state.
pub fn scan(&mut self, tx: &Transaction) -> BTreeSet<I> {
let mut scanned_indices = BTreeSet::new();
let txid = tx.compute_txid();
let txid = tx.txid();
for (i, txout) in tx.output.iter().enumerate() {
let op = OutPoint::new(txid, i as u32);
if let Some(spk_i) = self.scan_txout(op, txout) {
@@ -168,7 +168,9 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
///
/// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there.
pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
self.txouts.get(&outpoint).map(|v| (&v.0, &v.1))
self.txouts
.get(&outpoint)
.map(|(spk_i, txout)| (spk_i, txout))
}
/// Returns the script that has been inserted at the `index`.
@@ -215,7 +217,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
/// let unused_change_spks =
/// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
/// ```
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)> + Clone
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)>
where
R: RangeBounds<I>,
{
@@ -229,7 +231,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
/// never scanned a transaction output with it.
pub fn is_used(&self, index: &I) -> bool {
!self.unused.contains(index)
self.unused.get(index).is_none()
}
/// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it.
@@ -270,45 +272,37 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
self.spk_indices.get(script)
}
/// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is
/// *sent* when a script pubkey in the `range` is on an input and *received* when it is on an
/// output. For `sent` to be computed correctly, the output being spent must have already been
/// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly,
/// so it will be correct even if it has not been scanned.
pub fn sent_and_received(
&self,
tx: &Transaction,
range: impl RangeBounds<I>,
) -> (Amount, Amount) {
let mut sent = Amount::ZERO;
let mut received = Amount::ZERO;
/// Computes total input value going from script pubkeys in the index (sent) and the total output
/// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
/// correctly, the output being spent must have already been scanned by the index. Calculating
/// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has
/// not been scanned.
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
let mut sent = 0;
let mut received = 0;
for txin in &tx.input {
if let Some((index, txout)) = self.txout(txin.previous_output) {
if range.contains(index) {
sent += txout.value;
}
if let Some((_, txout)) = self.txout(txin.previous_output) {
sent += txout.value;
}
}
for txout in &tx.output {
if let Some(index) = self.index_of_spk(&txout.script_pubkey) {
if range.contains(index) {
received += txout.value;
}
if self.index_of_spk(&txout.script_pubkey).is_some() {
received += txout.value;
}
}
(sent, received)
}
/// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
/// for calling [`sent_and_received`] and subtracting sent from received.
/// Computes the net value that this transaction gives to the script pubkeys in the index and
/// *takes* from the transaction outputs in the index. Shorthand for calling
/// [`sent_and_received`] and subtracting sent from received.
///
/// [`sent_and_received`]: Self::sent_and_received
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<I>) -> SignedAmount {
let (sent, received) = self.sent_and_received(tx, range);
received.to_signed().expect("valid `SignedAmount`")
- sent.to_signed().expect("valid `SignedAmount`")
pub fn net_value(&self, tx: &Transaction) -> i64 {
let (sent, received) = self.sent_and_received(tx);
received as i64 - sent as i64
}
/// Whether any of the inputs of this transaction spend a txout tracked or whether any output

View File

@@ -5,25 +5,21 @@ use alloc::vec::Vec;
/// Trait that "anchors" blockchain data to a specific block of height and hash.
///
/// If transaction A is anchored in block B, and block B is in the best chain, we can
/// [`Anchor`] implementations must be [`Ord`] by the anchor block's [`BlockId`] first.
///
/// I.e. If transaction A is anchored in block B, then if block B is in the best chain, we can
/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean
/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a
/// parent block of B.
///
/// Every [`Anchor`] implementation must contain a [`BlockId`] parameter, and must implement
/// [`Ord`]. When implementing [`Ord`], the anchors' [`BlockId`]s should take precedence
/// over other elements inside the [`Anchor`]s for comparison purposes, i.e., you should first
/// compare the anchors' [`BlockId`]s and then care about the rest.
///
/// The example shows different types of anchors:
/// ```
/// # use bdk_chain::local_chain::LocalChain;
/// # use bdk_chain::tx_graph::TxGraph;
/// # use bdk_chain::BlockId;
/// # use bdk_chain::ConfirmationHeightAnchor;
/// # use bdk_chain::ConfirmationTimeHeightAnchor;
/// # use bdk_chain::example_utils::*;
/// # use bitcoin::hashes::Hash;
///
/// // Initialize the local chain with two blocks.
/// let chain = LocalChain::from_blocks(
/// [
@@ -43,7 +39,7 @@ use alloc::vec::Vec;
/// let mut graph_a = TxGraph::<BlockId>::default();
/// let _ = graph_a.insert_tx(tx.clone());
/// graph_a.insert_anchor(
/// tx.compute_txid(),
/// tx.txid(),
/// BlockId {
/// height: 1,
/// hash: Hash::hash("first".as_bytes()),
@@ -51,14 +47,13 @@ use alloc::vec::Vec;
/// );
///
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationHeightAnchor` as the anchor type.
/// // This anchor records the anchor block and the confirmation height of the transaction.
/// // When a transaction is anchored with `ConfirmationHeightAnchor`, the anchor block and
/// // confirmation block can be different. However, the confirmation block cannot be higher than
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
/// let mut graph_b = TxGraph::<ConfirmationHeightAnchor>::default();
/// let _ = graph_b.insert_tx(tx.clone());
/// graph_b.insert_anchor(
/// tx.compute_txid(),
/// tx.txid(),
/// ConfirmationHeightAnchor {
/// anchor_block: BlockId {
/// height: 2,
@@ -67,25 +62,6 @@ use alloc::vec::Vec;
/// confirmation_height: 1,
/// },
/// );
///
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationTimeHeightAnchor` as the anchor type.
/// // This anchor records the anchor block, the confirmation height and time of the transaction.
/// // When a transaction is anchored with `ConfirmationTimeHeightAnchor`, the anchor block and
/// // confirmation block can be different. However, the confirmation block cannot be higher than
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
/// let mut graph_c = TxGraph::<ConfirmationTimeHeightAnchor>::default();
/// let _ = graph_c.insert_tx(tx.clone());
/// graph_c.insert_anchor(
/// tx.compute_txid(),
/// ConfirmationTimeHeightAnchor {
/// anchor_block: BlockId {
/// height: 2,
/// hash: Hash::hash("third".as_bytes()),
/// },
/// confirmation_height: 1,
/// confirmation_time: 123,
/// },
/// );
/// ```
pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash {
/// Returns the [`BlockId`] that the associated blockchain data is "anchored" in.
@@ -114,28 +90,17 @@ pub trait AnchorFromBlockPosition: Anchor {
}
/// Trait that makes an object appendable.
pub trait Append: Default {
pub trait Append {
/// Append another object of the same type onto `self`.
fn append(&mut self, other: Self);
/// Returns whether the structure is considered empty.
fn is_empty(&self) -> bool;
/// Take the value, replacing it with the default value.
fn take(&mut self) -> Option<Self> {
if self.is_empty() {
None
} else {
Some(core::mem::take(self))
}
}
}
impl<K: Ord, V> Append for BTreeMap<K, V> {
fn append(&mut self, other: Self) {
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
BTreeMap::extend(self, other)
fn append(&mut self, mut other: Self) {
BTreeMap::append(self, &mut other)
}
fn is_empty(&self) -> bool {
@@ -144,10 +109,8 @@ impl<K: Ord, V> Append for BTreeMap<K, V> {
}
impl<T: Ord> Append for BTreeSet<T> {
fn append(&mut self, other: Self) {
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
BTreeSet::extend(self, other)
fn append(&mut self, mut other: Self) {
BTreeSet::append(self, &mut other)
}
fn is_empty(&self) -> bool {

View File

@@ -1,75 +1,39 @@
//! Module for structures that store and traverse transactions.
//!
//! [`TxGraph`] contains transactions and indexes them so you can easily traverse the graph of
//! those transactions. `TxGraph` is *monotone* in that you can always insert a transaction -- it
//! does not care whether that transaction is in the current best chain or whether it conflicts with
//! any of the existing transactions or what order you insert the transactions. This means that you
//! can always combine two [`TxGraph`]s together, without resulting in inconsistencies. Furthermore,
//! there is currently no way to delete a transaction.
//!
//! Transactions can be either whole or partial (i.e., transactions for which we only know some
//! outputs, which we usually call "floating outputs"; these are usually inserted using the
//! [`insert_txout`] method.).
//!
//! The graph contains transactions in the form of [`TxNode`]s. Each node contains the txid, the
//! transaction (whole or partial), the blocks that it is anchored to (see the [`Anchor`]
//! documentation for more details), and the timestamp of the last time we saw the transaction as
//! unconfirmed.
//! [`TxGraph`] is a monotone structure that inserts transactions and indexes the spends. The
//! [`ChangeSet`] structure reports changes of [`TxGraph`] but can also be applied to a
//! [`TxGraph`] as well. Lastly, [`TxDescendants`] is an [`Iterator`] that traverses descendants of
//! a given transaction.
//!
//! Conflicting transactions are allowed to coexist within a [`TxGraph`]. This is useful for
//! identifying and traversing conflicts and descendants of a given transaction. Some [`TxGraph`]
//! methods only consider transactions that are "canonical" (i.e., in the best chain or in mempool).
//! We decide which transactions are canonical based on the transaction's anchors and the
//! `last_seen` (as unconfirmed) timestamp; see the [`try_get_chain_position`] documentation for
//! more details.
//!
//! The [`ChangeSet`] reports changes made to a [`TxGraph`]; it can be used to either save to
//! persistent storage, or to be applied to another [`TxGraph`].
//!
//! Lastly, you can use [`TxAncestors`]/[`TxDescendants`] to traverse ancestors and descendants of
//! a given transaction, respectively.
//! identifying and traversing conflicts and descendants of a given transaction.
//!
//! # Applying changes
//!
//! Methods that change the state of [`TxGraph`] will return [`ChangeSet`]s.
//! [`ChangeSet`]s can be applied back to a [`TxGraph`] or be used to inform persistent storage
//! Methods that apply changes to [`TxGraph`] will return [`ChangeSet`].
//! [`ChangeSet`] can be applied back to a [`TxGraph`] or be used to inform persistent storage
//! of the changes to [`TxGraph`].
//!
//! # Generics
//!
//! Anchors are represented as generics within `TxGraph<A>`. To make use of all functionality of the
//! `TxGraph`, anchors (`A`) should implement [`Anchor`].
//!
//! Anchors are made generic so that different types of data can be stored with how a transaction is
//! *anchored* to a given block. An example of this is storing a merkle proof of the transaction to
//! the confirmation block - this can be done with a custom [`Anchor`] type. The minimal [`Anchor`]
//! type would just be a [`BlockId`] which just represents the height and hash of the block which
//! the transaction is contained in. Note that a transaction can be contained in multiple
//! conflicting blocks (by nature of the Bitcoin network).
//!
//! ```
//! # use bdk_chain::BlockId;
//! # use bdk_chain::tx_graph::TxGraph;
//! # use bdk_chain::example_utils::*;
//! # use bitcoin::Transaction;
//! # let tx_a = tx_from_hex(RAW_TX_1);
//! let mut tx_graph: TxGraph = TxGraph::default();
//! let mut graph: TxGraph = TxGraph::default();
//! let mut another_graph: TxGraph = TxGraph::default();
//!
//! // insert a transaction
//! let changeset = tx_graph.insert_tx(tx_a);
//! let changeset = graph.insert_tx(tx_a);
//!
//! // We can restore the state of the `tx_graph` by applying all
//! // the changesets obtained by mutating the original (the order doesn't matter).
//! let mut restored_tx_graph: TxGraph = TxGraph::default();
//! restored_tx_graph.apply_changeset(changeset);
//!
//! assert_eq!(tx_graph, restored_tx_graph);
//! // the resulting changeset can be applied to another tx graph
//! another_graph.apply_changeset(changeset);
//! ```
//!
//! A [`TxGraph`] can also be updated with another [`TxGraph`] which merges them together.
//! A [`TxGraph`] can also be updated with another [`TxGraph`].
//!
//! ```
//! # use bdk_chain::{Append, BlockId};
//! # use bdk_chain::BlockId;
//! # use bdk_chain::tx_graph::TxGraph;
//! # use bdk_chain::example_utils::*;
//! # use bitcoin::Transaction;
@@ -85,17 +49,14 @@
//! let changeset = graph.apply_update(update);
//! assert!(changeset.is_empty());
//! ```
//! [`try_get_chain_position`]: TxGraph::try_get_chain_position
//! [`insert_txout`]: TxGraph::insert_txout
use crate::{
collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition,
FullTxOut,
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
ChainOracle, ChainPosition, FullTxOut,
};
use alloc::collections::vec_deque::VecDeque;
use alloc::sync::Arc;
use alloc::vec::Vec;
use bitcoin::{Amount, OutPoint, Script, SignedAmount, Transaction, TxOut, Txid};
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
use core::fmt::{self, Formatter};
use core::{
convert::Infallible,
@@ -130,13 +91,13 @@ impl<A> Default for TxGraph<A> {
}
}
/// A transaction node in the [`TxGraph`].
/// An outward-facing view of a (transaction) node in the [`TxGraph`].
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct TxNode<'a, T, A> {
/// Txid of the transaction.
pub txid: Txid,
/// A partial or full representation of the transaction.
pub tx: T,
pub tx: &'a T,
/// The blocks that the transaction is "anchored" in.
pub anchors: &'a BTreeSet<A>,
/// The last-seen unix timestamp of the transaction as unconfirmed.
@@ -147,7 +108,7 @@ impl<'a, T, A> Deref for TxNode<'a, T, A> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.tx
self.tx
}
}
@@ -157,7 +118,7 @@ impl<'a, T, A> Deref for TxNode<'a, T, A> {
/// outputs).
#[derive(Clone, Debug, PartialEq)]
enum TxNodeInternal {
Whole(Arc<Transaction>),
Whole(Transaction),
Partial(BTreeMap<u32, TxOut>),
}
@@ -167,7 +128,7 @@ impl Default for TxNodeInternal {
}
}
/// A transaction that is included in the chain, or is still in mempool.
/// An outwards-facing view of a transaction that is part of the *best chain*'s history.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct CanonicalTx<'a, T, A> {
/// How the transaction is observed as (confirmed or unconfirmed).
@@ -182,7 +143,7 @@ pub enum CalculateFeeError {
/// Missing `TxOut` for one or more of the inputs of the tx
MissingTxOut(Vec<OutPoint>),
/// When the transaction is invalid according to the graph it has a negative fee
NegativeFee(SignedAmount),
NegativeFee(i64),
}
impl fmt::Display for CalculateFeeError {
@@ -196,7 +157,7 @@ impl fmt::Display for CalculateFeeError {
CalculateFeeError::NegativeFee(fee) => write!(
f,
"transaction is invalid according to the graph and has negative fee: {}",
fee.display_dynamic()
fee
),
}
}
@@ -212,7 +173,6 @@ impl<A> TxGraph<A> {
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
TxNodeInternal::Whole(tx) => tx
.as_ref()
.output
.iter()
.enumerate()
@@ -244,13 +204,13 @@ impl<A> TxGraph<A> {
}
/// Iterate over all full transactions in the graph.
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Transaction, A>> {
self.txs
.iter()
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
TxNodeInternal::Whole(tx) => Some(TxNode {
txid,
tx: tx.clone(),
tx,
anchors,
last_seen_unconfirmed: *last_seen,
}),
@@ -263,16 +223,16 @@ impl<A> TxGraph<A> {
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
///
/// [`get_txout`]: Self::get_txout
pub fn get_tx(&self, txid: Txid) -> Option<Arc<Transaction>> {
pub fn get_tx(&self, txid: Txid) -> Option<&Transaction> {
self.get_tx_node(txid).map(|n| n.tx)
}
/// Get a transaction node by txid. This only returns `Some` for full transactions.
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Arc<Transaction>, A>> {
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Transaction, A>> {
match &self.txs.get(&txid)? {
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
txid,
tx: tx.clone(),
tx,
anchors,
last_seen_unconfirmed: *last_seen,
}),
@@ -283,7 +243,7 @@ impl<A> TxGraph<A> {
/// Obtains a single tx output (if any) at the specified outpoint.
pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> {
match &self.txs.get(&outpoint.txid)?.0 {
TxNodeInternal::Whole(tx) => tx.as_ref().output.get(outpoint.vout as usize),
TxNodeInternal::Whole(tx) => tx.output.get(outpoint.vout as usize),
TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout),
}
}
@@ -294,7 +254,6 @@ impl<A> TxGraph<A> {
pub fn tx_outputs(&self, txid: Txid) -> Option<BTreeMap<u32, &TxOut>> {
Some(match &self.txs.get(&txid)?.0 {
TxNodeInternal::Whole(tx) => tx
.as_ref()
.output
.iter()
.enumerate()
@@ -307,7 +266,7 @@ impl<A> TxGraph<A> {
})
}
/// Calculates the fee of a given transaction. Returns [`Amount::ZERO`] if `tx` is a coinbase transaction.
/// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction.
/// Returns `OK(_)` if we have all the [`TxOut`]s being spent by `tx` in the graph (either as
/// the full transactions or individual txouts).
///
@@ -318,20 +277,20 @@ impl<A> TxGraph<A> {
/// Note `tx` does not have to be in the graph for this to work.
///
/// [`insert_txout`]: Self::insert_txout
pub fn calculate_fee(&self, tx: &Transaction) -> Result<Amount, CalculateFeeError> {
if tx.is_coinbase() {
return Ok(Amount::ZERO);
pub fn calculate_fee(&self, tx: &Transaction) -> Result<u64, CalculateFeeError> {
if tx.is_coin_base() {
return Ok(0);
}
let (inputs_sum, missing_outputs) = tx.input.iter().fold(
(SignedAmount::ZERO, Vec::new()),
(0_i64, Vec::new()),
|(mut sum, mut missing_outpoints), txin| match self.get_txout(txin.previous_output) {
None => {
missing_outpoints.push(txin.previous_output);
(sum, missing_outpoints)
}
Some(txout) => {
sum += txout.value.to_signed().expect("valid `SignedAmount`");
sum += txout.value as i64;
(sum, missing_outpoints)
}
},
@@ -343,17 +302,20 @@ impl<A> TxGraph<A> {
let outputs_sum = tx
.output
.iter()
.map(|txout| txout.value.to_signed().expect("valid `SignedAmount`"))
.sum::<SignedAmount>();
.map(|txout| txout.value as i64)
.sum::<i64>();
let fee = inputs_sum - outputs_sum;
fee.to_unsigned()
.map_err(|_| CalculateFeeError::NegativeFee(fee))
if fee < 0 {
Err(CalculateFeeError::NegativeFee(fee))
} else {
Ok(fee as u64)
}
}
/// The transactions spending from this output.
///
/// [`TxGraph`] allows conflicting transactions within the graph. Obviously the transactions in
/// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in
/// the returned set will never be in the same active-chain.
pub fn outspends(&self, outpoint: OutPoint) -> &HashSet<Txid> {
self.spends.get(&outpoint).unwrap_or(&self.empty_outspends)
@@ -369,15 +331,16 @@ impl<A> TxGraph<A> {
&self,
txid: Txid,
) -> impl DoubleEndedIterator<Item = (u32, &HashSet<Txid>)> + '_ {
let start = OutPoint::new(txid, 0);
let end = OutPoint::new(txid, u32::MAX);
let start = OutPoint { txid, vout: 0 };
let end = OutPoint {
txid,
vout: u32::MAX,
};
self.spends
.range(start..=end)
.map(|(outpoint, spends)| (outpoint.vout, spends))
}
}
impl<A: Clone + Ord> TxGraph<A> {
/// Creates an iterator that filters and maps ancestor transactions.
///
/// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx`
@@ -391,10 +354,13 @@ impl<A: Clone + Ord> TxGraph<A> {
///
/// The supplied closure returns an `Option<T>`, allowing the caller to map each `Transaction`
/// it visits and decide whether to visit ancestors.
pub fn walk_ancestors<'g, T, F, O>(&'g self, tx: T, walk_map: F) -> TxAncestors<'g, A, F>
pub fn walk_ancestors<'g, F, O>(
&'g self,
tx: &'g Transaction,
walk_map: F,
) -> TxAncestors<'g, A, F>
where
T: Into<Arc<Transaction>>,
F: FnMut(usize, Arc<Transaction>) -> Option<O> + 'g,
F: FnMut(usize, &'g Transaction) -> Option<O> + 'g,
{
TxAncestors::new_exclude_root(self, tx, walk_map)
}
@@ -415,9 +381,7 @@ impl<A: Clone + Ord> TxGraph<A> {
{
TxDescendants::new_exclude_root(self, txid, walk_map)
}
}
impl<A> TxGraph<A> {
/// Creates an iterator that both filters and maps conflicting transactions (this includes
/// descendants of directly-conflicting transactions, which are also considered conflicts).
///
@@ -430,7 +394,7 @@ impl<A> TxGraph<A> {
where
F: FnMut(usize, Txid) -> Option<O> + 'g,
{
let txids = self.direct_conflicts(tx).map(|(_, txid)| txid);
let txids = self.direct_conflitcs(tx).map(|(_, txid)| txid);
TxDescendants::from_multiple_include_root(self, txids, walk_map)
}
@@ -441,11 +405,11 @@ impl<A> TxGraph<A> {
/// Note that this only returns directly conflicting txids and won't include:
/// - descendants of conflicting transactions (which are technically also conflicting)
/// - transactions conflicting with the given transaction's ancestors
pub fn direct_conflicts<'g>(
pub fn direct_conflitcs<'g>(
&'g self,
tx: &'g Transaction,
) -> impl Iterator<Item = (usize, Txid)> + '_ {
let txid = tx.compute_txid();
let txid = tx.txid();
tx.input
.iter()
.enumerate()
@@ -466,19 +430,6 @@ impl<A> TxGraph<A> {
}
impl<A: Clone + Ord> TxGraph<A> {
/// Transform the [`TxGraph`] to have [`Anchor`]s of another type.
///
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
/// transform it.
pub fn map_anchors<A2: Clone + Ord, F>(self, f: F) -> TxGraph<A2>
where
F: FnMut(A) -> A2,
{
let mut new_graph = TxGraph::<A2>::default();
new_graph.apply_changeset(self.initial_changeset().map_anchors(f));
new_graph
}
/// Construct a new [`TxGraph`] from a list of transactions.
pub fn new(txs: impl IntoIterator<Item = Transaction>) -> Self {
let mut new = Self::default();
@@ -513,20 +464,18 @@ impl<A: Clone + Ord> TxGraph<A> {
/// Inserts the given transaction into [`TxGraph`].
///
/// The [`ChangeSet`] returned will be empty if `tx` already exists.
pub fn insert_tx<T: Into<Arc<Transaction>>>(&mut self, tx: T) -> ChangeSet<A> {
let tx = tx.into();
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A> {
let mut update = Self::default();
update.txs.insert(
tx.compute_txid(),
(TxNodeInternal::Whole(tx), BTreeSet::new(), 0),
);
update
.txs
.insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
self.apply_update(update)
}
/// Batch insert unconfirmed transactions.
///
/// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The
/// *last seen* communicates when the transaction is last seen in mempool which is used for
/// *last seen* communicates when the transaction is last seen in the mempool which is used for
/// conflict-resolution (refer to [`TxGraph::insert_seen_at`] for details).
pub fn batch_insert_unconfirmed(
&mut self,
@@ -534,7 +483,7 @@ impl<A: Clone + Ord> TxGraph<A> {
) -> ChangeSet<A> {
let mut changeset = ChangeSet::<A>::default();
for (tx, seen_at) in txs {
changeset.append(self.insert_seen_at(tx.compute_txid(), seen_at));
changeset.append(self.insert_seen_at(tx.txid(), seen_at));
changeset.append(self.insert_tx(tx));
}
changeset
@@ -552,11 +501,7 @@ impl<A: Clone + Ord> TxGraph<A> {
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
///
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch
/// update all unconfirmed transactions with the latest `seen_at`, see
/// [`update_last_seen_unconfirmed`].
///
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
let mut update = Self::default();
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
@@ -564,65 +509,6 @@ impl<A: Clone + Ord> TxGraph<A> {
self.apply_update(update)
}
/// Update the last seen time for all unconfirmed transactions.
///
/// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting
/// the given `seen_at` for every transaction not yet anchored to a confirmed block,
/// and returns the [`ChangeSet`] after applying all updates to `self`.
///
/// This is useful for keeping track of the latest time a transaction was seen
/// unconfirmed, which is important for evaluating transaction conflicts in the same
/// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for
/// [`try_get_chain_position`].
///
/// A normal use of this method is to call it with the current system time. Although
/// block headers contain a timestamp, using the header time would be less effective
/// at tracking mempool transactions, because it can drift from actual clock time, plus
/// we may want to update a transaction's last seen time repeatedly between blocks.
///
/// # Example
///
/// ```rust
/// # use bdk_chain::example_utils::*;
/// # use std::time::UNIX_EPOCH;
/// # let tx = tx_from_hex(RAW_TX_1);
/// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]);
/// let now = std::time::SystemTime::now()
/// .duration_since(UNIX_EPOCH)
/// .expect("valid duration")
/// .as_secs();
/// let changeset = tx_graph.update_last_seen_unconfirmed(now);
/// assert!(!changeset.last_seen.is_empty());
/// ```
///
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must
/// by strictly greater than what is currently stored for a transaction to have an effect.
/// To insert a last seen time for a single txid, see [`insert_seen_at`].
///
/// [`insert_seen_at`]: Self::insert_seen_at
/// [`try_get_chain_position`]: Self::try_get_chain_position
pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet<A> {
let mut changeset = ChangeSet::default();
let unanchored_txs: Vec<Txid> = self
.txs
.iter()
.filter_map(
|(&txid, (_, anchors, _))| {
if anchors.is_empty() {
Some(txid)
} else {
None
}
},
)
.collect();
for txid in unanchored_txs {
changeset.append(self.insert_seen_at(txid, seen_at));
}
changeset
}
/// Extends this graph with another so that `self` becomes the union of the two sets of
/// transactions.
///
@@ -641,9 +527,8 @@ impl<A: Clone + Ord> TxGraph<A> {
/// Applies [`ChangeSet`] to [`TxGraph`].
pub fn apply_changeset(&mut self, changeset: ChangeSet<A>) {
for wrapped_tx in changeset.txs {
let tx = wrapped_tx.as_ref();
let txid = tx.compute_txid();
for tx in changeset.txs {
let txid = tx.txid();
tx.input
.iter()
@@ -657,26 +542,27 @@ impl<A: Clone + Ord> TxGraph<A> {
match self.txs.get_mut(&txid) {
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
*tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
*tx_node = TxNodeInternal::Whole(tx);
}
Some((TxNodeInternal::Whole(tx), _, _)) => {
debug_assert_eq!(
tx.as_ref().compute_txid(),
tx.txid(),
txid,
"tx should produce txid that is same as key"
);
}
None => {
self.txs.insert(
txid,
(TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), 0),
);
self.txs
.insert(txid, (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
}
}
}
for (outpoint, txout) in changeset.txouts {
let tx_entry = self.txs.entry(outpoint.txid).or_default();
let tx_entry = self
.txs
.entry(outpoint.txid)
.or_insert_with(Default::default);
match tx_entry {
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
@@ -689,13 +575,13 @@ impl<A: Clone + Ord> TxGraph<A> {
for (anchor, txid) in changeset.anchors {
if self.anchors.insert((anchor.clone(), txid)) {
let (_, anchors, _) = self.txs.entry(txid).or_default();
let (_, anchors, _) = self.txs.entry(txid).or_insert_with(Default::default);
anchors.insert(anchor);
}
}
for (txid, new_last_seen) in changeset.last_seen {
let (_, _, last_seen) = self.txs.entry(txid).or_default();
let (_, _, last_seen) = self.txs.entry(txid).or_insert_with(Default::default);
if new_last_seen > *last_seen {
*last_seen = new_last_seen;
}
@@ -707,7 +593,7 @@ impl<A: Clone + Ord> TxGraph<A> {
/// The [`ChangeSet`] would be the set difference between `update` and `self` (transactions that
/// exist in `update` but not in `self`).
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
let mut changeset = ChangeSet::<A>::default();
let mut changeset = ChangeSet::default();
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
@@ -757,22 +643,73 @@ impl<A: Clone + Ord> TxGraph<A> {
}
impl<A: Anchor> TxGraph<A> {
/// Find missing block heights of `chain`.
///
/// This works by scanning through anchors, and seeing whether the anchor block of the anchor
/// exists in the [`LocalChain`]. The returned iterator does not output duplicate heights.
pub fn missing_heights<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
// Map of txids to skip.
//
// Usually, if a height of a tx anchor is missing from the chain, we would want to return
// this height in the iterator. The exception is when the tx is confirmed in chain. All the
// other missing-height anchors of this tx can be skipped.
//
// * Some(true) => skip all anchors of this txid
// * Some(false) => do not skip anchors of this txid
// * None => we do not know whether we can skip this txid
let mut txids_to_skip = HashMap::<Txid, bool>::new();
// Keeps track of the last height emitted so we don't double up.
let mut last_height_emitted = Option::<u32>::None;
self.anchors
.iter()
.filter(move |(_, txid)| {
let skip = *txids_to_skip.entry(*txid).or_insert_with(|| {
let tx_anchors = match self.txs.get(txid) {
Some((_, anchors, _)) => anchors,
None => return true,
};
let mut has_missing_height = false;
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
match chain.blocks().get(&anchor_block.height) {
None => {
has_missing_height = true;
continue;
}
Some(chain_hash) => {
if chain_hash == &anchor_block.hash {
return true;
}
}
}
}
!has_missing_height
});
#[cfg(feature = "std")]
debug_assert!({
println!("txid={} skip={}", txid, skip);
true
});
!skip
})
.filter_map(move |(a, _)| {
let anchor_block = a.anchor_block();
if Some(anchor_block.height) != last_height_emitted
&& !chain.blocks().contains_key(&anchor_block.height)
{
last_height_emitted = Some(anchor_block.height);
Some(anchor_block.height)
} else {
None
}
})
}
/// Get the position of the transaction in `chain` with tip `chain_tip`.
///
/// Chain data is fetched from `chain`, a [`ChainOracle`] implementation.
///
/// This method returns `Ok(None)` if the transaction is not found in the chain, and no longer
/// belongs in the mempool. The following factors are used to approximate whether an
/// unconfirmed transaction exists in the mempool (not evicted):
///
/// 1. Unconfirmed transactions that conflict with confirmed transactions are evicted.
/// 2. Unconfirmed transactions that spend from transactions that are evicted, are also
/// evicted.
/// 3. Given two conflicting unconfirmed transactions, the transaction with the lower
/// `last_seen_unconfirmed` parameter is evicted. A transaction's `last_seen_unconfirmed`
/// parameter is the max of all it's descendants' `last_seen_unconfirmed` parameters. If the
/// final `last_seen_unconfirmed`s are the same, the transaction with the lower `txid` (by
/// lexicographical order) is evicted.
/// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is
/// returned.
///
/// # Error
///
@@ -798,17 +735,17 @@ impl<A: Anchor> TxGraph<A> {
}
}
// The tx is not anchored to a block in the best chain, which means that it
// The tx is not anchored to a block which is in the best chain, which means that it
// might be in mempool, or it might have been dropped already.
// Let's check conflicts to find out!
let tx = match tx_node {
TxNodeInternal::Whole(tx) => {
// A coinbase tx that is not anchored in the best chain cannot be unconfirmed and
// should always be filtered out.
if tx.is_coinbase() {
if tx.is_coin_base() {
return Ok(None);
}
tx.clone()
tx
}
TxNodeInternal::Partial(_) => {
// Partial transactions (outputs only) cannot have conflicts.
@@ -825,8 +762,8 @@ impl<A: Anchor> TxGraph<A> {
// First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the
// resulting array will also include `tx`
let unconfirmed_ancestor_txs =
TxAncestors::new_include_root(self, tx.clone(), |_, ancestor_tx: Arc<Transaction>| {
let tx_node = self.get_tx_node(ancestor_tx.as_ref().compute_txid())?;
TxAncestors::new_include_root(self, tx, |_, ancestor_tx: &Transaction| {
let tx_node = self.get_tx_node(ancestor_tx.txid())?;
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
// the best chain)
for block in tx_node.anchors {
@@ -842,10 +779,8 @@ impl<A: Anchor> TxGraph<A> {
// We determine our tx's last seen, which is the max between our last seen,
// and our unconf descendants' last seen.
let unconfirmed_descendants_txs = TxDescendants::new_include_root(
self,
tx.as_ref().compute_txid(),
|_, descendant_txid: Txid| {
let unconfirmed_descendants_txs =
TxDescendants::new_include_root(self, tx.txid(), |_, descendant_txid: Txid| {
let tx_node = self.get_tx_node(descendant_txid)?;
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
// the best chain)
@@ -857,9 +792,8 @@ impl<A: Anchor> TxGraph<A> {
}
}
Some(Ok(tx_node))
},
)
.collect::<Result<Vec<_>, C::Error>>()?;
})
.collect::<Result<Vec<_>, C::Error>>()?;
let tx_last_seen = unconfirmed_descendants_txs
.iter()
@@ -870,8 +804,7 @@ impl<A: Anchor> TxGraph<A> {
// Now we traverse our ancestors and consider all their conflicts
for tx_node in unconfirmed_ancestor_txs {
// We retrieve all the transactions conflicting with this specific ancestor
let conflicting_txs =
self.walk_conflicts(tx_node.tx.as_ref(), |_, txid| self.get_tx_node(txid));
let conflicting_txs = self.walk_conflicts(tx_node.tx, |_, txid| self.get_tx_node(txid));
// If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then
// this tx cannot exist in the best chain
@@ -885,7 +818,7 @@ impl<A: Anchor> TxGraph<A> {
return Ok(None);
}
if conflicting_tx.last_seen_unconfirmed == *last_seen
&& conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
&& conflicting_tx.txid() > tx.txid()
{
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
return Ok(None);
@@ -978,7 +911,7 @@ impl<A: Anchor> TxGraph<A> {
&'a self,
chain: &'a C,
chain_tip: BlockId,
) -> impl Iterator<Item = Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error>> {
) -> impl Iterator<Item = Result<CanonicalTx<'a, Transaction, A>, C::Error>> {
self.full_txs().filter_map(move |tx| {
self.try_get_chain_position(chain, chain_tip, tx.txid)
.map(|v| {
@@ -1000,7 +933,7 @@ impl<A: Anchor> TxGraph<A> {
&'a self,
chain: &'a C,
chain_tip: BlockId,
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
) -> impl Iterator<Item = CanonicalTx<'a, Transaction, A>> {
self.try_list_chain_txs(chain, chain_tip)
.map(|r| r.expect("oracle is infallible"))
}
@@ -1012,8 +945,7 @@ impl<A: Anchor> TxGraph<A> {
/// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or
/// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
///
/// Floating outputs (i.e., outputs for which we don't have the full transaction in the graph)
/// are ignored.
/// Floating outputs are ignored.
///
/// # Error
///
@@ -1039,7 +971,7 @@ impl<A: Anchor> TxGraph<A> {
None => return Ok(None),
};
let txout = match tx_node.tx.as_ref().output.get(op.vout as usize) {
let txout = match tx_node.tx.output.get(op.vout as usize) {
Some(txout) => txout.clone(),
None => return Ok(None),
};
@@ -1061,7 +993,7 @@ impl<A: Anchor> TxGraph<A> {
txout,
chain_position,
spent_by,
is_on_coinbase: tx_node.tx.is_coinbase(),
is_on_coinbase: tx_node.tx.is_coin_base(),
},
)))
},
@@ -1153,10 +1085,10 @@ impl<A: Anchor> TxGraph<A> {
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
mut trust_predicate: impl FnMut(&OI, &Script) -> bool,
) -> Result<Balance, C::Error> {
let mut immature = Amount::ZERO;
let mut trusted_pending = Amount::ZERO;
let mut untrusted_pending = Amount::ZERO;
let mut confirmed = Amount::ZERO;
let mut immature = 0;
let mut trusted_pending = 0;
let mut untrusted_pending = 0;
let mut confirmed = 0;
for res in self.try_filter_chain_unspents(chain, chain_tip, outpoints) {
let (spk_i, txout) = res?;
@@ -1204,9 +1136,9 @@ impl<A: Anchor> TxGraph<A> {
}
}
/// The [`ChangeSet`] represents changes to a [`TxGraph`].
/// A structure that represents changes to a [`TxGraph`].
///
/// Since [`TxGraph`] is monotone, the "changeset" can only contain transactions to be added and
/// Since [`TxGraph`] is monotone "changeset" can only contain transactions to be added and
/// not removed.
///
/// Refer to [module-level documentation] for more.
@@ -1227,7 +1159,7 @@ impl<A: Anchor> TxGraph<A> {
#[must_use]
pub struct ChangeSet<A = ()> {
/// Added transactions.
pub txs: BTreeSet<Arc<Transaction>>,
pub txs: BTreeSet<Transaction>,
/// Added txouts.
pub txouts: BTreeMap<OutPoint, TxOut>,
/// Added anchors.
@@ -1248,6 +1180,11 @@ impl<A> Default for ChangeSet<A> {
}
impl<A> ChangeSet<A> {
/// Returns true if the [`ChangeSet`] is empty (no transactions or txouts).
pub fn is_empty(&self) -> bool {
self.txs.is_empty() && self.txouts.is_empty()
}
/// Iterates over all outpoints contained within [`ChangeSet`].
pub fn txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
self.txs
@@ -1256,7 +1193,7 @@ impl<A> ChangeSet<A> {
tx.output
.iter()
.enumerate()
.map(move |(vout, txout)| (OutPoint::new(tx.compute_txid(), vout as _), txout))
.map(move |(vout, txout)| (OutPoint::new(tx.txid(), vout as _), txout))
})
.chain(self.txouts.iter().map(|(op, txout)| (*op, txout)))
}
@@ -1265,6 +1202,8 @@ impl<A> ChangeSet<A> {
///
/// This is useful if you want to find which heights you need to fetch data about in order to
/// confirm or exclude these anchors.
///
/// See also: [`TxGraph::missing_heights`]
pub fn anchor_heights(&self) -> impl Iterator<Item = u32> + '_
where
A: Anchor,
@@ -1279,15 +1218,31 @@ impl<A> ChangeSet<A> {
!duplicate
})
}
/// Returns an iterator for the [`anchor_heights`] in this changeset that are not included in
/// `local_chain`. This tells you which heights you need to include in `local_chain` in order
/// for it to conclusively act as a [`ChainOracle`] for the transaction anchors this changeset
/// will add.
///
/// [`ChainOracle`]: crate::ChainOracle
/// [`anchor_heights`]: Self::anchor_heights
pub fn missing_heights_from<'a>(
&'a self,
local_chain: &'a LocalChain,
) -> impl Iterator<Item = u32> + 'a
where
A: Anchor,
{
self.anchor_heights()
.filter(move |height| !local_chain.blocks().contains_key(height))
}
}
impl<A: Ord> Append for ChangeSet<A> {
fn append(&mut self, other: Self) {
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
self.txs.extend(other.txs);
self.txouts.extend(other.txouts);
self.anchors.extend(other.anchors);
fn append(&mut self, mut other: Self) {
self.txs.append(&mut other.txs);
self.txouts.append(&mut other.txouts);
self.anchors.append(&mut other.anchors);
// last_seen timestamps should only increase
self.last_seen.extend(
@@ -1307,26 +1262,6 @@ impl<A: Ord> Append for ChangeSet<A> {
}
}
impl<A: Ord> ChangeSet<A> {
/// Transform the [`ChangeSet`] to have [`Anchor`]s of another type.
///
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
/// transform it.
pub fn map_anchors<A2: Ord, F>(self, mut f: F) -> ChangeSet<A2>
where
F: FnMut(A) -> A2,
{
ChangeSet {
txs: self.txs,
txouts: self.txouts,
anchors: BTreeSet::<(A2, Txid)>::from_iter(
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
),
last_seen: self.last_seen,
}
}
}
impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
fn as_ref(&self) -> &TxGraph<A> {
self
@@ -1337,13 +1272,13 @@ impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
///
/// The iterator excludes partial transactions.
///
/// Returned by the [`walk_ancestors`] method of [`TxGraph`].
/// This `struct` is created by the [`walk_ancestors`] method of [`TxGraph`].
///
/// [`walk_ancestors`]: TxGraph::walk_ancestors
pub struct TxAncestors<'g, A, F> {
graph: &'g TxGraph<A>,
visited: HashSet<Txid>,
queue: VecDeque<(usize, Arc<Transaction>)>,
queue: VecDeque<(usize, &'g Transaction)>,
filter_map: F,
}
@@ -1351,13 +1286,13 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
/// Creates a `TxAncestors` that includes the starting `Transaction` when iterating.
pub(crate) fn new_include_root(
graph: &'g TxGraph<A>,
tx: impl Into<Arc<Transaction>>,
tx: &'g Transaction,
filter_map: F,
) -> Self {
Self {
graph,
visited: Default::default(),
queue: [(0, tx.into())].into(),
queue: [(0, tx)].into(),
filter_map,
}
}
@@ -1365,7 +1300,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
/// Creates a `TxAncestors` that excludes the starting `Transaction` when iterating.
pub(crate) fn new_exclude_root(
graph: &'g TxGraph<A>,
tx: impl Into<Arc<Transaction>>,
tx: &'g Transaction,
filter_map: F,
) -> Self {
let mut ancestors = Self {
@@ -1374,7 +1309,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
queue: Default::default(),
filter_map,
};
ancestors.populate_queue(1, tx.into());
ancestors.populate_queue(1, tx);
ancestors
}
@@ -1387,13 +1322,12 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
filter_map: F,
) -> Self
where
I: IntoIterator,
I::Item: Into<Arc<Transaction>>,
I: IntoIterator<Item = &'g Transaction>,
{
Self {
graph,
visited: Default::default(),
queue: txs.into_iter().map(|tx| (0, tx.into())).collect(),
queue: txs.into_iter().map(|tx| (0, tx)).collect(),
filter_map,
}
}
@@ -1407,8 +1341,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
filter_map: F,
) -> Self
where
I: IntoIterator,
I::Item: Into<Arc<Transaction>>,
I: IntoIterator<Item = &'g Transaction>,
{
let mut ancestors = Self {
graph,
@@ -1417,12 +1350,12 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
filter_map,
};
for tx in txs {
ancestors.populate_queue(1, tx.into());
ancestors.populate_queue(1, tx);
}
ancestors
}
fn populate_queue(&mut self, depth: usize, tx: Arc<Transaction>) {
fn populate_queue(&mut self, depth: usize, tx: &'g Transaction) {
let ancestors = tx
.input
.iter()
@@ -1436,7 +1369,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
impl<'g, A, F, O> Iterator for TxAncestors<'g, A, F>
where
F: FnMut(usize, Arc<Transaction>) -> Option<O>,
F: FnMut(usize, &'g Transaction) -> Option<O>,
{
type Item = O;
@@ -1445,7 +1378,7 @@ where
// we have exhausted all paths when queue is empty
let (ancestor_depth, tx) = self.queue.pop_front()?;
// ignore paths when user filters them out
let item = match (self.filter_map)(ancestor_depth, tx.clone()) {
let item = match (self.filter_map)(ancestor_depth, tx) {
Some(item) => item,
None => continue,
};
@@ -1457,7 +1390,7 @@ where
/// An iterator that traverses transaction descendants.
///
/// Returned by the [`walk_descendants`] method of [`TxGraph`].
/// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`].
///
/// [`walk_descendants`]: TxGraph::walk_descendants
pub struct TxDescendants<'g, A, F> {

View File

@@ -1,7 +1,4 @@
#![cfg(feature = "miniscript")]
mod tx_template;
#[allow(unused_imports)]
pub use tx_template::*;
#[allow(unused_macros)]
@@ -34,9 +31,12 @@ macro_rules! local_chain {
macro_rules! chain_update {
[ $(($height:expr, $hash:expr)), * ] => {{
#[allow(unused_mut)]
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
.expect("chain must have genesis block")
.tip()
bdk_chain::local_chain::Update {
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
.expect("chain must have genesis block")
.tip(),
introduce_older_blocks: true,
}
}};
}
@@ -69,21 +69,9 @@ macro_rules! changeset {
#[allow(unused)]
pub fn new_tx(lt: u32) -> bitcoin::Transaction {
bitcoin::Transaction {
version: bitcoin::transaction::Version::non_standard(0x00),
version: 0x00,
lock_time: bitcoin::absolute::LockTime::from_consensus(lt),
input: vec![],
output: vec![],
}
}
#[allow(unused)]
pub const DESCRIPTORS: [&str; 7] = [
"tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)",
"tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)",
"wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0/*)",
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)",
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)",
"wpkh(xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX/1/0/*)",
// non-wildcard
"wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)",
];

View File

@@ -1,12 +1,10 @@
#![cfg(feature = "miniscript")]
use rand::distributions::{Alphanumeric, DistString};
use std::collections::HashMap;
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
use bdk_chain::{tx_graph::TxGraph, BlockId, SpkTxOutIndex};
use bitcoin::{
locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf,
Sequence, Transaction, TxIn, TxOut, Txid, Witness,
locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction,
TxIn, TxOut, Txid, Witness,
};
use miniscript::Descriptor;
@@ -51,12 +49,11 @@ impl TxOutTemplate {
}
#[allow(dead_code)]
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
) -> (TxGraph<A>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
let (descriptor, _) =
Descriptor::parse_descriptor(&Secp256k1::signing_only(), super::DESCRIPTORS[2]).unwrap();
let mut graph = TxGraph::<A>::default();
pub fn init_graph<'a>(
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, BlockId>>,
) -> (TxGraph<BlockId>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
let mut graph = TxGraph::<BlockId>::default();
let mut spk_index = SpkTxOutIndex::default();
(0..10).for_each(|index| {
spk_index.insert_spk(
@@ -71,7 +68,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() {
let tx = Transaction {
version: transaction::Version::non_standard(0),
version: 0,
lock_time: LockTime::ZERO,
input: tx_tmp
.inputs
@@ -114,25 +111,25 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
.iter()
.map(|output| match &output.spk_index {
None => TxOut {
value: Amount::from_sat(output.value),
value: output.value,
script_pubkey: ScriptBuf::new(),
},
Some(index) => TxOut {
value: Amount::from_sat(output.value),
value: output.value,
script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(),
},
})
.collect(),
};
tx_ids.insert(tx_tmp.tx_name, tx.compute_txid());
tx_ids.insert(tx_tmp.tx_name, tx.txid());
spk_index.scan(&tx);
let _ = graph.insert_tx(tx.clone());
for anchor in tx_tmp.anchors.iter() {
let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone());
let _ = graph.insert_anchor(tx.txid(), *anchor);
}
if let Some(seen_at) = tx_tmp.last_seen {
let _ = graph.insert_seen_at(tx.compute_txid(), seen_at);
let _ = graph.insert_seen_at(tx.txid(), seen_at);
}
}
(graph, spk_index, tx_ids)

View File

@@ -1,20 +1,15 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use std::{collections::BTreeSet, sync::Arc};
use std::collections::BTreeSet;
use crate::common::DESCRIPTORS;
use bdk_chain::{
indexed_tx_graph::{self, IndexedTxGraph},
keychain::{self, Balance, KeychainTxOutIndex},
local_chain::LocalChain,
tx_graph, Append, ChainPosition, ConfirmationHeightAnchor, DescriptorExt,
};
use bitcoin::{
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
};
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
use miniscript::Descriptor;
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
@@ -26,27 +21,24 @@ use miniscript::Descriptor;
/// agnostic.
#[test]
fn insert_relevant_txs() {
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[0])
const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)";
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR)
.expect("must be valid");
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
KeychainTxOutIndex::new(10),
);
let _ = graph
.index
.insert_descriptor((), descriptor.clone())
.unwrap();
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::default();
graph.index.add_keychain((), descriptor);
graph.index.set_lookahead(&(), 10);
let tx_a = Transaction {
output: vec![
TxOut {
value: Amount::from_sat(10_000),
value: 10_000,
script_pubkey: spk_0,
},
TxOut {
value: Amount::from_sat(20_000),
value: 20_000,
script_pubkey: spk_1,
},
],
@@ -55,7 +47,7 @@ fn insert_relevant_txs() {
let tx_b = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.compute_txid(), 0),
previous_output: OutPoint::new(tx_a.txid(), 0),
..Default::default()
}],
..common::new_tx(1)
@@ -63,7 +55,7 @@ fn insert_relevant_txs() {
let tx_c = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.compute_txid(), 1),
previous_output: OutPoint::new(tx_a.txid(), 1),
..Default::default()
}],
..common::new_tx(2)
@@ -73,13 +65,10 @@ fn insert_relevant_txs() {
let changeset = indexed_tx_graph::ChangeSet {
graph: tx_graph::ChangeSet {
txs: txs.iter().cloned().map(Arc::new).collect(),
txs: txs.clone().into(),
..Default::default()
},
indexer: keychain::ChangeSet {
last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(),
keychains_added: [].into(),
},
indexer: keychain::ChangeSet([((), 9_u32)].into()),
};
assert_eq!(
@@ -87,18 +76,10 @@ fn insert_relevant_txs() {
changeset,
);
// The initial changeset will also contain info about the keychain we added
let initial_changeset = indexed_tx_graph::ChangeSet {
graph: changeset.graph,
indexer: keychain::ChangeSet {
last_revealed: changeset.indexer.last_revealed,
keychains_added: [((), descriptor)].into(),
},
};
assert_eq!(graph.initial_changeset(), initial_changeset);
assert_eq!(graph.initial_changeset(), changeset,);
}
#[test]
/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
/// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain).
///
@@ -126,7 +107,7 @@ fn insert_relevant_txs() {
///
/// Finally Add more blocks to local chain until tx1 coinbase maturity hits.
/// Assert maturity at coinbase maturity inflection height. Block height 98 and 99.
#[test]
fn test_list_owned_txouts() {
// Create Local chains
let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
@@ -134,25 +115,15 @@ fn test_list_owned_txouts() {
// Initiate IndexedTxGraph
let (desc_1, _) =
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[2]).unwrap();
let (desc_2, _) =
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap();
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)").unwrap();
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
KeychainTxOutIndex::new(10),
);
let mut graph =
IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::default();
assert!(!graph
.index
.insert_descriptor("keychain_1".into(), desc_1)
.unwrap()
.is_empty());
assert!(!graph
.index
.insert_descriptor("keychain_2".into(), desc_2)
.unwrap()
.is_empty());
graph.index.add_keychain("keychain_1".into(), desc_1);
graph.index.add_keychain("keychain_2".into(), desc_2);
graph.index.set_lookahead_for_all(10);
// Get trusted and untrusted addresses
@@ -162,20 +133,14 @@ fn test_list_owned_txouts() {
{
// we need to scope here to take immutanble reference of the graph
for _ in 0..10 {
let ((_, script), _) = graph
.index
.reveal_next_spk(&"keychain_1".to_string())
.unwrap();
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string());
// TODO Assert indexes
trusted_spks.push(script.to_owned());
}
}
{
for _ in 0..10 {
let ((_, script), _) = graph
.index
.reveal_next_spk(&"keychain_2".to_string())
.unwrap();
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string());
untrusted_spks.push(script.to_owned());
}
}
@@ -189,7 +154,7 @@ fn test_list_owned_txouts() {
..Default::default()
}],
output: vec![TxOut {
value: Amount::from_sat(70000),
value: 70000,
script_pubkey: trusted_spks[0].to_owned(),
}],
..common::new_tx(0)
@@ -198,7 +163,7 @@ fn test_list_owned_txouts() {
// tx2 is an incoming transaction received at untrusted keychain at block 1.
let tx2 = Transaction {
output: vec![TxOut {
value: Amount::from_sat(30000),
value: 30000,
script_pubkey: untrusted_spks[0].to_owned(),
}],
..common::new_tx(0)
@@ -207,11 +172,11 @@ fn test_list_owned_txouts() {
// tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2.
let tx3 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx2.compute_txid(), 0),
previous_output: OutPoint::new(tx2.txid(), 0),
..Default::default()
}],
output: vec![TxOut {
value: Amount::from_sat(10000),
value: 10000,
script_pubkey: trusted_spks[1].to_owned(),
}],
..common::new_tx(0)
@@ -220,7 +185,7 @@ fn test_list_owned_txouts() {
// tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
let tx4 = Transaction {
output: vec![TxOut {
value: Amount::from_sat(20000),
value: 20000,
script_pubkey: untrusted_spks[1].to_owned(),
}],
..common::new_tx(0)
@@ -229,7 +194,7 @@ fn test_list_owned_txouts() {
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
let tx5 = Transaction {
output: vec![TxOut {
value: Amount::from_sat(15000),
value: 15000,
script_pubkey: trusted_spks[2].to_owned(),
}],
..common::new_tx(0)
@@ -247,8 +212,10 @@ fn test_list_owned_txouts() {
(
*tx,
local_chain
.get(height)
.map(|cp| cp.block_id())
.blocks()
.get(&height)
.cloned()
.map(|hash| BlockId { height, hash })
.map(|anchor_block| ConfirmationHeightAnchor {
anchor_block,
confirmation_height: anchor_block.height,
@@ -263,8 +230,9 @@ fn test_list_owned_txouts() {
|height: u32,
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
let chain_tip = local_chain
.get(height)
.map(|cp| cp.block_id())
.blocks()
.get(&height)
.map(|&hash| BlockId { height, hash })
.unwrap_or_else(|| panic!("block must exist at {}", height));
let txouts = graph
.graph()
@@ -359,31 +327,25 @@ fn test_list_owned_txouts() {
balance,
) = fetch(0, &graph);
assert_eq!(confirmed_txouts_txid, [tx1.compute_txid()].into());
assert_eq!(confirmed_txouts_txid, [tx1.txid()].into());
assert_eq!(
unconfirmed_txouts_txid,
[
tx2.compute_txid(),
tx3.compute_txid(),
tx4.compute_txid(),
tx5.compute_txid()
]
.into()
[tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into()
);
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
assert_eq!(
unconfirmed_utxos_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx3.txid(), tx4.txid(), tx5.txid()].into()
);
assert_eq!(
balance,
Balance {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::ZERO // Nothing is confirmed yet
immature: 70000, // immature coinbase
trusted_pending: 25000, // tx3 + tx5
untrusted_pending: 20000, // tx4
confirmed: 0 // Nothing is confirmed yet
}
);
}
@@ -399,29 +361,26 @@ fn test_list_owned_txouts() {
) = fetch(1, &graph);
// tx2 gets into confirmed txout set
assert_eq!(
confirmed_txouts_txid,
[tx1.compute_txid(), tx2.compute_txid()].into()
);
assert_eq!(confirmed_txouts_txid, [tx1.txid(), tx2.txid()].into());
assert_eq!(
unconfirmed_txouts_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx3.txid(), tx4.txid(), tx5.txid()].into()
);
// tx2 doesn't get into confirmed utxos set
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
assert_eq!(
unconfirmed_utxos_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx3.txid(), tx4.txid(), tx5.txid()].into()
);
assert_eq!(
balance,
Balance {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::ZERO // Nothing is confirmed yet
immature: 70000, // immature coinbase
trusted_pending: 25000, // tx3 + tx5
untrusted_pending: 20000, // tx4
confirmed: 0 // Nothing is confirmed yet
}
);
}
@@ -439,30 +398,21 @@ fn test_list_owned_txouts() {
// tx3 now gets into the confirmed txout set
assert_eq!(
confirmed_txouts_txid,
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
);
assert_eq!(
unconfirmed_txouts_txid,
[tx4.compute_txid(), tx5.compute_txid()].into()
[tx1.txid(), tx2.txid(), tx3.txid()].into()
);
assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
// tx3 also gets into confirmed utxo set
assert_eq!(
confirmed_utxos_txid,
[tx1.compute_txid(), tx3.compute_txid()].into()
);
assert_eq!(
unconfirmed_utxos_txid,
[tx4.compute_txid(), tx5.compute_txid()].into()
);
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
assert_eq!(
balance,
Balance {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::from_sat(10000) // tx3 got confirmed
immature: 70000, // immature coinbase
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 10000 // tx3 got confirmed
}
);
}
@@ -479,30 +429,21 @@ fn test_list_owned_txouts() {
assert_eq!(
confirmed_txouts_txid,
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
);
assert_eq!(
unconfirmed_txouts_txid,
[tx4.compute_txid(), tx5.compute_txid()].into()
[tx1.txid(), tx2.txid(), tx3.txid()].into()
);
assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
assert_eq!(
confirmed_utxos_txid,
[tx1.compute_txid(), tx3.compute_txid()].into()
);
assert_eq!(
unconfirmed_utxos_txid,
[tx4.compute_txid(), tx5.compute_txid()].into()
);
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
// Coinbase is still immature
assert_eq!(
balance,
Balance {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::from_sat(10000) // tx1 got matured
immature: 70000, // immature coinbase
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 10000 // tx1 got matured
}
);
}
@@ -515,10 +456,10 @@ fn test_list_owned_txouts() {
assert_eq!(
balance,
Balance {
immature: Amount::ZERO, // coinbase matured
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::from_sat(80000) // tx1 + tx3
immature: 0, // coinbase matured
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 80000 // tx1 + tx3
}
);
}

View File

@@ -5,43 +5,34 @@ mod common;
use bdk_chain::{
collections::BTreeMap,
indexed_tx_graph::Indexer,
keychain::{self, ChangeSet, KeychainTxOutIndex},
Append, DescriptorExt, DescriptorId,
keychain::{self, KeychainTxOutIndex},
Append,
};
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut};
use bitcoin::{secp256k1::Secp256k1, OutPoint, ScriptBuf, Transaction, TxOut};
use miniscript::{Descriptor, DescriptorPublicKey};
use crate::common::DESCRIPTORS;
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
enum TestKeychain {
External,
Internal,
}
fn parse_descriptor(descriptor: &str) -> Descriptor<DescriptorPublicKey> {
fn init_txout_index() -> (
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
Descriptor<DescriptorPublicKey>,
Descriptor<DescriptorPublicKey>,
) {
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::default();
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, descriptor)
.unwrap()
.0
}
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
fn init_txout_index(
external_descriptor: Descriptor<DescriptorPublicKey>,
internal_descriptor: Descriptor<DescriptorPublicKey>,
lookahead: u32,
) -> bdk_chain::keychain::KeychainTxOutIndex<TestKeychain> {
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::new(lookahead);
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
let _ = txout_index
.insert_descriptor(TestKeychain::External, external_descriptor)
.unwrap();
let _ = txout_index
.insert_descriptor(TestKeychain::Internal, internal_descriptor)
.unwrap();
txout_index
(txout_index, external_descriptor, internal_descriptor)
}
fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> ScriptBuf {
@@ -51,219 +42,119 @@ fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Scr
.script_pubkey()
}
// We create two empty changesets lhs and rhs, we then insert various descriptors with various
// last_revealed, append rhs to lhs, and check that the result is consistent with these rules:
// - Existing index doesn't update if the new index in `other` is lower than `self`.
// - Existing index updates if the new index in `other` is higher than `self`.
// - Existing index is unchanged if keychain doesn't exist in `other`.
// - New keychain gets added if the keychain is in `other` but not in `self`.
#[test]
fn append_changesets_check_last_revealed() {
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
let descriptor_ids: Vec<_> = DESCRIPTORS
.iter()
.take(4)
.map(|d| {
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, d)
.unwrap()
.0
.descriptor_id()
})
.collect();
let mut lhs_di = BTreeMap::<DescriptorId, u32>::default();
let mut rhs_di = BTreeMap::<DescriptorId, u32>::default();
lhs_di.insert(descriptor_ids[0], 7);
lhs_di.insert(descriptor_ids[1], 0);
lhs_di.insert(descriptor_ids[2], 3);
rhs_di.insert(descriptor_ids[0], 3); // value less than lhs desc 0
rhs_di.insert(descriptor_ids[1], 5); // value more than lhs desc 1
lhs_di.insert(descriptor_ids[3], 4); // key doesn't exist in lhs
let mut lhs = ChangeSet {
keychains_added: BTreeMap::<(), _>::new(),
last_revealed: lhs_di,
};
let rhs = ChangeSet {
keychains_added: BTreeMap::<(), _>::new(),
last_revealed: rhs_di,
};
lhs.append(rhs);
// Existing index doesn't update if the new index in `other` is lower than `self`.
assert_eq!(lhs.last_revealed.get(&descriptor_ids[0]), Some(&7));
// Existing index updates if the new index in `other` is higher than `self`.
assert_eq!(lhs.last_revealed.get(&descriptor_ids[1]), Some(&5));
// Existing index is unchanged if keychain doesn't exist in `other`.
assert_eq!(lhs.last_revealed.get(&descriptor_ids[2]), Some(&3));
// New keychain gets added if the keychain is in `other` but not in `self`.
assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4));
}
#[test]
fn when_apply_contradictory_changesets_they_are_ignored() {
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
assert_eq!(
txout_index.keychains().collect::<Vec<_>>(),
vec![
(&TestKeychain::External, &external_descriptor),
(&TestKeychain::Internal, &internal_descriptor)
]
);
let changeset = ChangeSet {
keychains_added: [(TestKeychain::External, internal_descriptor.clone())].into(),
last_revealed: [].into(),
};
txout_index.apply_changeset(changeset);
assert_eq!(
txout_index.keychains().collect::<Vec<_>>(),
vec![
(&TestKeychain::External, &external_descriptor),
(&TestKeychain::Internal, &internal_descriptor)
]
);
let changeset = ChangeSet {
keychains_added: [(TestKeychain::Internal, external_descriptor.clone())].into(),
last_revealed: [].into(),
};
txout_index.apply_changeset(changeset);
assert_eq!(
txout_index.keychains().collect::<Vec<_>>(),
vec![
(&TestKeychain::External, &external_descriptor),
(&TestKeychain::Internal, &internal_descriptor)
]
);
}
#[test]
fn test_set_all_derivation_indices() {
use bdk_chain::indexed_tx_graph::Indexer;
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
let (mut txout_index, _, _) = init_txout_index();
let derive_to: BTreeMap<_, _> =
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
let last_revealed: BTreeMap<_, _> = [
(external_descriptor.descriptor_id(), 12),
(internal_descriptor.descriptor_id(), 24),
]
.into();
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to),
ChangeSet {
keychains_added: BTreeMap::new(),
last_revealed: last_revealed.clone()
}
txout_index.reveal_to_target_multi(&derive_to).1.as_inner(),
&derive_to
);
assert_eq!(txout_index.last_revealed_indices(), derive_to);
assert_eq!(txout_index.last_revealed_indices(), &derive_to);
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to),
txout_index.reveal_to_target_multi(&derive_to).1,
keychain::ChangeSet::default(),
"no changes if we set to the same thing"
);
assert_eq!(txout_index.initial_changeset().last_revealed, last_revealed);
assert_eq!(txout_index.initial_changeset().as_inner(), &derive_to);
}
#[test]
fn test_lookahead() {
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 10);
let (mut txout_index, external_desc, internal_desc) = init_txout_index();
// ensure it does not break anything if lookahead is set multiple times
(0..=10).for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::External, lookahead));
(0..=20)
.filter(|v| v % 2 == 0)
.for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::Internal, lookahead));
assert_eq!(txout_index.inner().all_spks().len(), 30);
// given:
// - external lookahead set to 10
// - internal lookahead set to 20
// when:
// - set external derivation index to value higher than last, but within the lookahead value
// expect:
// - scripts cached in spk_txout_index should increase correctly
// - stored scripts of external keychain should be of expected counts
for index in (0..20).skip_while(|i| i % 2 == 1) {
let (revealed_spks, revealed_changeset) = txout_index
.reveal_to_target(&TestKeychain::External, index)
.unwrap();
let (revealed_spks, revealed_changeset) =
txout_index.reveal_to_target(&TestKeychain::External, index);
assert_eq!(
revealed_spks,
vec![(index, spk_at_index(&external_descriptor, index))],
revealed_spks.collect::<Vec<_>>(),
vec![(index, spk_at_index(&external_desc, index))],
);
assert_eq!(
&revealed_changeset.last_revealed,
&[(external_descriptor.descriptor_id(), index)].into()
revealed_changeset.as_inner(),
&[(TestKeychain::External, index)].into()
);
assert_eq!(
txout_index.inner().all_spks().len(),
10 /* external lookahead */ +
10 /* internal lookahead */ +
20 /* internal lookahead */ +
index as usize + 1 /* `derived` count */
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::External)
.revealed_spks_of_keychain(&TestKeychain::External)
.count(),
index as usize + 1,
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::Internal)
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
0,
);
assert_eq!(
txout_index
.unused_keychain_spks(&TestKeychain::External)
.unused_spks_of_keychain(&TestKeychain::External)
.count(),
index as usize + 1,
);
assert_eq!(
txout_index
.unused_keychain_spks(&TestKeychain::Internal)
.unused_spks_of_keychain(&TestKeychain::Internal)
.count(),
0,
);
}
// given:
// - internal lookahead is 10
// - internal lookahead is 20
// - internal derivation index is `None`
// when:
// - derivation index is set ahead of current derivation index + lookahead
// expect:
// - scripts cached in spk_txout_index should increase correctly, a.k.a. no scripts are skipped
let (revealed_spks, revealed_changeset) = txout_index
.reveal_to_target(&TestKeychain::Internal, 24)
.unwrap();
let (revealed_spks, revealed_changeset) =
txout_index.reveal_to_target(&TestKeychain::Internal, 24);
assert_eq!(
revealed_spks,
revealed_spks.collect::<Vec<_>>(),
(0..=24)
.map(|index| (index, spk_at_index(&internal_descriptor, index)))
.map(|index| (index, spk_at_index(&internal_desc, index)))
.collect::<Vec<_>>(),
);
assert_eq!(
&revealed_changeset.last_revealed,
&[(internal_descriptor.descriptor_id(), 24)].into()
revealed_changeset.as_inner(),
&[(TestKeychain::Internal, 24)].into()
);
assert_eq!(
txout_index.inner().all_spks().len(),
10 /* external lookahead */ +
10 /* internal lookahead */ +
20 /* internal lookahead */ +
20 /* external stored index count */ +
25 /* internal stored index count */
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::Internal)
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
25,
);
@@ -288,18 +179,18 @@ fn test_lookahead() {
let tx = Transaction {
output: vec![
TxOut {
script_pubkey: external_descriptor
script_pubkey: external_desc
.at_derivation_index(external_index)
.unwrap()
.script_pubkey(),
value: Amount::from_sat(10_000),
value: 10_000,
},
TxOut {
script_pubkey: internal_descriptor
script_pubkey: internal_desc
.at_derivation_index(internal_index)
.unwrap()
.script_pubkey(),
value: Amount::from_sat(10_000),
value: 10_000,
},
],
..common::new_tx(external_index)
@@ -315,13 +206,13 @@ fn test_lookahead() {
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::External)
.revealed_spks_of_keychain(&TestKeychain::External)
.count(),
last_external_index as usize + 1,
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::Internal)
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
last_internal_index as usize + 1,
);
@@ -335,17 +226,15 @@ fn test_lookahead() {
// - last used index should change as expected
#[test]
fn test_scan_with_lookahead() {
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 10);
let (mut txout_index, external_desc, _) = init_txout_index();
txout_index.set_lookahead_for_all(10);
let spks: BTreeMap<u32, ScriptBuf> = [0, 10, 20, 30]
.into_iter()
.map(|i| {
(
i,
external_descriptor
external_desc
.at_derivation_index(i)
.unwrap()
.script_pubkey(),
@@ -357,13 +246,13 @@ fn test_scan_with_lookahead() {
let op = OutPoint::new(h!("fake tx"), spk_i);
let txout = TxOut {
script_pubkey: spk.clone(),
value: Amount::ZERO,
value: 0,
};
let changeset = txout_index.index_txout(op, &txout);
assert_eq!(
&changeset.last_revealed,
&[(external_descriptor.descriptor_id(), spk_i)].into()
changeset.as_inner(),
&[(TestKeychain::External, spk_i)].into()
);
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::External),
@@ -376,14 +265,14 @@ fn test_scan_with_lookahead() {
}
// now try with index 41 (lookahead surpassed), we expect that the txout to not be indexed
let spk_41 = external_descriptor
let spk_41 = external_desc
.at_derivation_index(41)
.unwrap()
.script_pubkey();
let op = OutPoint::new(h!("fake tx"), 41);
let txout = TxOut {
script_pubkey: spk_41,
value: Amount::ZERO,
value: 0,
};
let changeset = txout_index.index_txout(op, &txout);
assert!(changeset.is_empty());
@@ -392,13 +281,11 @@ fn test_scan_with_lookahead() {
#[test]
#[rustfmt::skip]
fn test_wildcard_derivations() {
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index = init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
let external_spk_0 = external_descriptor.at_derivation_index(0).unwrap().script_pubkey();
let external_spk_16 = external_descriptor.at_derivation_index(16).unwrap().script_pubkey();
let external_spk_26 = external_descriptor.at_derivation_index(26).unwrap().script_pubkey();
let external_spk_27 = external_descriptor.at_derivation_index(27).unwrap().script_pubkey();
let (mut txout_index, external_desc, _) = init_txout_index();
let external_spk_0 = external_desc.at_derivation_index(0).unwrap().script_pubkey();
let external_spk_16 = external_desc.at_derivation_index(16).unwrap().script_pubkey();
let external_spk_26 = external_desc.at_derivation_index(26).unwrap().script_pubkey();
let external_spk_27 = external_desc.at_derivation_index(27).unwrap().script_pubkey();
// - nothing is derived
// - unused list is also empty
@@ -406,13 +293,13 @@ fn test_wildcard_derivations() {
// - next_derivation_index() == (0, true)
// - derive_new() == ((0, <spk>), keychain::ChangeSet)
// - next_unused() == ((0, <spk>), keychain::ChangeSet:is_empty())
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (0_u32, external_spk_0.clone()));
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (0_u32, external_spk_0.clone()));
assert_eq!(&changeset.last_revealed, &[].into());
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
// - derived till 25
// - used all spks till 15.
@@ -426,45 +313,42 @@ fn test_wildcard_derivations() {
(0..=15)
.chain([17, 20, 23])
.for_each(|index| assert!(txout_index.mark_used(TestKeychain::External, index)));
.for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index)));
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (26, true));
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (26, external_spk_26));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (26, external_spk_26.as_script()));
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 26)].into());
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 26)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (16, external_spk_16));
assert_eq!(&changeset.last_revealed, &[].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (16, external_spk_16.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
// - Use all the derived till 26.
// - next_unused() = ((27, <spk>), keychain::ChangeSet)
(0..=26).for_each(|index| {
txout_index.mark_used(TestKeychain::External, index);
txout_index.mark_used(&TestKeychain::External, index);
});
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (27, external_spk_27));
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 27)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (27, external_spk_27.as_script()));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 27)].into());
}
#[test]
fn test_non_wildcard_derivations() {
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
let (no_wildcard_descriptor, _) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[6]).unwrap();
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
let external_spk = no_wildcard_descriptor
.at_derivation_index(0)
.unwrap()
.script_pubkey();
let _ = txout_index
.insert_descriptor(TestKeychain::External, no_wildcard_descriptor.clone())
.unwrap();
txout_index.add_keychain(TestKeychain::External, no_wildcard_descriptor);
// given:
// - `txout_index` with no stored scripts
@@ -472,24 +356,14 @@ fn test_non_wildcard_derivations() {
// - next derivation index should be new
// - when we derive a new script, script @ index 0
// - when we get the next unused script, script @ index 0
assert_eq!(
txout_index.next_index(&TestKeychain::External).unwrap(),
(0, true)
);
let (spk, changeset) = txout_index
.reveal_next_spk(&TestKeychain::External)
.unwrap();
assert_eq!(spk, (0, external_spk.clone()));
assert_eq!(
&changeset.last_revealed,
&[(no_wildcard_descriptor.descriptor_id(), 0)].into()
);
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0, external_spk.as_script()));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
let (spk, changeset) = txout_index
.next_unused_spk(&TestKeychain::External)
.unwrap();
assert_eq!(spk, (0, external_spk.clone()));
assert_eq!(&changeset.last_revealed, &[].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0, external_spk.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
// given:
// - the non-wildcard descriptor already has a stored and used script
@@ -497,267 +371,26 @@ fn test_non_wildcard_derivations() {
// - next derivation index should not be new
// - derive new and next unused should return the old script
// - store_up_to should not panic and return empty changeset
assert_eq!(
txout_index.next_index(&TestKeychain::External).unwrap(),
(0, false)
);
txout_index.mark_used(TestKeychain::External, 0);
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
txout_index.mark_used(&TestKeychain::External, 0);
let (spk, changeset) = txout_index
.reveal_next_spk(&TestKeychain::External)
.unwrap();
assert_eq!(spk, (0, external_spk.clone()));
assert_eq!(&changeset.last_revealed, &[].into());
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0, external_spk.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
let (spk, changeset) = txout_index
.next_unused_spk(&TestKeychain::External)
.unwrap();
assert_eq!(spk, (0, external_spk.clone()));
assert_eq!(&changeset.last_revealed, &[].into());
let (revealed_spks, revealed_changeset) = txout_index
.reveal_to_target(&TestKeychain::External, 200)
.unwrap();
assert_eq!(revealed_spks.len(), 0);
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0, external_spk.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
let (revealed_spks, revealed_changeset) =
txout_index.reveal_to_target(&TestKeychain::External, 200);
assert_eq!(revealed_spks.count(), 0);
assert!(revealed_changeset.is_empty());
// we check that spks_of_keychain returns a SpkIterator with just one element
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::External)
.spks_of_keychain(&TestKeychain::External)
.count(),
1,
);
}
/// Check that calling `lookahead_to_target` stores the expected spks.
#[test]
fn lookahead_to_target() {
#[derive(Default)]
struct TestCase {
/// Global lookahead value.
lookahead: u32,
/// Last revealed index for external keychain.
external_last_revealed: Option<u32>,
/// Last revealed index for internal keychain.
internal_last_revealed: Option<u32>,
/// Call `lookahead_to_target(External, u32)`.
external_target: Option<u32>,
/// Call `lookahead_to_target(Internal, u32)`.
internal_target: Option<u32>,
}
let test_cases = &[
TestCase {
lookahead: 0,
external_target: Some(100),
..Default::default()
},
TestCase {
lookahead: 10,
internal_target: Some(99),
..Default::default()
},
TestCase {
lookahead: 100,
internal_target: Some(9),
external_target: Some(10),
..Default::default()
},
TestCase {
lookahead: 12,
external_last_revealed: Some(2),
internal_last_revealed: Some(2),
internal_target: Some(15),
external_target: Some(13),
},
TestCase {
lookahead: 13,
external_last_revealed: Some(100),
internal_last_revealed: Some(21),
internal_target: Some(120),
external_target: Some(130),
},
];
for t in test_cases {
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut index = init_txout_index(
external_descriptor.clone(),
internal_descriptor.clone(),
t.lookahead,
);
if let Some(last_revealed) = t.external_last_revealed {
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
}
if let Some(last_revealed) = t.internal_last_revealed {
let _ = index.reveal_to_target(&TestKeychain::Internal, last_revealed);
}
let keychain_test_cases = [
(
TestKeychain::External,
t.external_last_revealed,
t.external_target,
),
(
TestKeychain::Internal,
t.internal_last_revealed,
t.internal_target,
),
];
for (keychain, last_revealed, target) in keychain_test_cases {
if let Some(target) = target {
let original_last_stored_index = match last_revealed {
Some(last_revealed) => Some(last_revealed + t.lookahead),
None => t.lookahead.checked_sub(1),
};
let exp_last_stored_index = match original_last_stored_index {
Some(original_last_stored_index) => {
Ord::max(target, original_last_stored_index)
}
None => target,
};
index.lookahead_to_target(&keychain, target);
let keys = index
.inner()
.all_spks()
.range((keychain.clone(), 0)..=(keychain.clone(), u32::MAX))
.map(|(k, _)| k.clone())
.collect::<Vec<_>>();
let exp_keys = core::iter::repeat(keychain)
.zip(0_u32..=exp_last_stored_index)
.collect::<Vec<_>>();
assert_eq!(keys, exp_keys);
}
}
}
}
#[test]
fn insert_descriptor_no_change() {
let secp = Secp256k1::signing_only();
let (desc, _) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0]).unwrap();
let mut txout_index = KeychainTxOutIndex::<()>::default();
assert_eq!(
txout_index.insert_descriptor((), desc.clone()),
Ok(keychain::ChangeSet {
keychains_added: [((), desc.clone())].into(),
last_revealed: Default::default()
}),
);
assert_eq!(
txout_index.insert_descriptor((), desc.clone()),
Ok(keychain::ChangeSet::default()),
"inserting the same descriptor for keychain should return an empty changeset",
);
}
#[test]
#[cfg(not(debug_assertions))]
fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
let desc = parse_descriptor(DESCRIPTORS[0]);
let changesets: &[ChangeSet<TestKeychain>] = &[
ChangeSet {
keychains_added: [(TestKeychain::Internal, desc.clone())].into(),
last_revealed: [].into(),
},
ChangeSet {
keychains_added: [(TestKeychain::External, desc.clone())].into(),
last_revealed: [(desc.descriptor_id(), 12)].into(),
},
];
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
for changeset in changesets {
indexer_a.apply_changeset(changeset.clone());
}
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
let aggregate_changesets = changesets
.iter()
.cloned()
.reduce(|mut agg, cs| {
agg.append(cs);
agg
})
.expect("must aggregate changesets");
indexer_b.apply_changeset(aggregate_changesets);
assert_eq!(
indexer_a.keychains().collect::<Vec<_>>(),
indexer_b.keychains().collect::<Vec<_>>()
);
assert_eq!(
indexer_a.spk_at_index(TestKeychain::External, 0),
indexer_b.spk_at_index(TestKeychain::External, 0)
);
assert_eq!(
indexer_a.spk_at_index(TestKeychain::Internal, 0),
indexer_b.spk_at_index(TestKeychain::Internal, 0)
);
assert_eq!(
indexer_a.last_revealed_indices(),
indexer_b.last_revealed_indices()
);
}
#[test]
fn assigning_same_descriptor_to_multiple_keychains_should_error() {
let desc = parse_descriptor(DESCRIPTORS[0]);
let mut indexer = KeychainTxOutIndex::<TestKeychain>::new(0);
let _ = indexer
.insert_descriptor(TestKeychain::Internal, desc.clone())
.unwrap();
assert!(indexer
.insert_descriptor(TestKeychain::External, desc)
.is_err())
}
#[test]
fn reassigning_keychain_to_a_new_descriptor_should_error() {
let desc1 = parse_descriptor(DESCRIPTORS[0]);
let desc2 = parse_descriptor(DESCRIPTORS[1]);
let mut indexer = KeychainTxOutIndex::<TestKeychain>::new(0);
let _ = indexer.insert_descriptor(TestKeychain::Internal, desc1);
assert!(indexer
.insert_descriptor(TestKeychain::Internal, desc2)
.is_err());
}
#[test]
fn when_querying_over_a_range_of_keychains_the_utxos_should_show_up() {
let mut indexer = KeychainTxOutIndex::<usize>::new(0);
let mut tx = common::new_tx(0);
for (i, descriptor) in DESCRIPTORS.iter().enumerate() {
let descriptor = parse_descriptor(descriptor);
let _ = indexer.insert_descriptor(i, descriptor.clone()).unwrap();
if i != 4 {
// skip one in the middle to see if uncovers any bugs
indexer.reveal_next_spk(&i);
}
tx.output.push(TxOut {
script_pubkey: descriptor.at_derivation_index(0).unwrap().script_pubkey(),
value: Amount::from_sat(10_000),
});
}
let n_spks = DESCRIPTORS.len() - /*we skipped one*/ 1;
let _ = indexer.index_tx(&tx);
assert_eq!(indexer.outpoints().len(), n_spks);
assert_eq!(indexer.revealed_spks(0..DESCRIPTORS.len()).count(), n_spks);
assert_eq!(indexer.revealed_spks(1..4).count(), 4 - 1);
assert_eq!(
indexer.net_value(&tx, 0..DESCRIPTORS.len()).to_sat(),
(10_000 * n_spks) as i64
);
assert_eq!(
indexer.net_value(&tx, 3..6).to_sat(),
(10_000 * (6 - 3 - /*the skipped one*/ 1)) as i64
);
}

View File

@@ -1,16 +1,7 @@
#![cfg(feature = "miniscript")]
use std::ops::{Bound, RangeBounds};
use bdk_chain::{
local_chain::{
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
LocalChain, MissingGenesisError,
},
BlockId,
use bdk_chain::local_chain::{
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
};
use bitcoin::{block::Header, hashes::Hash, BlockHash};
use proptest::prelude::*;
use bitcoin::BlockHash;
#[macro_use]
mod common;
@@ -19,7 +10,7 @@ mod common;
struct TestLocalChain<'a> {
name: &'static str,
chain: LocalChain,
update: CheckPoint,
update: Update,
exp: ExpectedResult<'a>,
}
@@ -297,27 +288,6 @@ fn update_local_chain() {
],
},
},
// Allow update that is shorter than original chain
// | 0 | 1 | 2 | 3 | 4 | 5
// chain | A C D E F
// update | A C D'
TestLocalChain {
name: "allow update that is shorter than original chain",
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D")), (4, h!("E")), (5, h!("F"))],
update: chain_update![(0, h!("_")), (2, h!("C")), (3, h!("D'"))],
exp: ExpectedResult::Ok {
changeset: &[
(3, Some(h!("D'"))),
(4, None),
(5, None),
],
init_changeset: &[
(0, Some(h!("_"))),
(2, Some(h!("C"))),
(3, Some(h!("D'"))),
],
},
},
]
.into_iter()
.for_each(TestLocalChain::run);
@@ -380,469 +350,3 @@ fn local_chain_insert_block() {
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
}
}
#[test]
fn local_chain_disconnect_from() {
struct TestCase {
name: &'static str,
original: LocalChain,
disconnect_from: (u32, BlockHash),
exp_result: Result<ChangeSet, MissingGenesisError>,
exp_final: LocalChain,
}
let test_cases = [
TestCase {
name: "try_replace_genesis_should_fail",
original: local_chain![(0, h!("_"))],
disconnect_from: (0, h!("_")),
exp_result: Err(MissingGenesisError),
exp_final: local_chain![(0, h!("_"))],
},
TestCase {
name: "try_replace_genesis_should_fail_2",
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
disconnect_from: (0, h!("_")),
exp_result: Err(MissingGenesisError),
exp_final: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
},
TestCase {
name: "from_does_not_exist",
original: local_chain![(0, h!("_")), (3, h!("C"))],
disconnect_from: (2, h!("B")),
exp_result: Ok(ChangeSet::default()),
exp_final: local_chain![(0, h!("_")), (3, h!("C"))],
},
TestCase {
name: "from_has_different_blockhash",
original: local_chain![(0, h!("_")), (2, h!("B"))],
disconnect_from: (2, h!("not_B")),
exp_result: Ok(ChangeSet::default()),
exp_final: local_chain![(0, h!("_")), (2, h!("B"))],
},
TestCase {
name: "disconnect_one",
original: local_chain![(0, h!("_")), (2, h!("B"))],
disconnect_from: (2, h!("B")),
exp_result: Ok(ChangeSet::from_iter([(2, None)])),
exp_final: local_chain![(0, h!("_"))],
},
TestCase {
name: "disconnect_three",
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
disconnect_from: (2, h!("B")),
exp_result: Ok(ChangeSet::from_iter([(2, None), (3, None), (4, None)])),
exp_final: local_chain![(0, h!("_"))],
},
];
for (i, t) in test_cases.into_iter().enumerate() {
println!("Case {}: {}", i, t.name);
let mut chain = t.original;
let result = chain.disconnect_from(t.disconnect_from.into());
assert_eq!(
result, t.exp_result,
"[{}:{}] unexpected changeset result",
i, t.name
);
assert_eq!(
chain, t.exp_final,
"[{}:{}] unexpected final chain",
i, t.name
);
}
}
#[test]
fn checkpoint_from_block_ids() {
struct TestCase<'a> {
name: &'a str,
blocks: &'a [(u32, BlockHash)],
exp_result: Result<(), Option<(u32, BlockHash)>>,
}
let test_cases = [
TestCase {
name: "in_order",
blocks: &[(0, h!("A")), (1, h!("B")), (3, h!("D"))],
exp_result: Ok(()),
},
TestCase {
name: "with_duplicates",
blocks: &[(1, h!("B")), (2, h!("C")), (2, h!("C'"))],
exp_result: Err(Some((2, h!("C")))),
},
TestCase {
name: "not_in_order",
blocks: &[(1, h!("B")), (3, h!("D")), (2, h!("C"))],
exp_result: Err(Some((3, h!("D")))),
},
TestCase {
name: "empty",
blocks: &[],
exp_result: Err(None),
},
TestCase {
name: "single",
blocks: &[(21, h!("million"))],
exp_result: Ok(()),
},
];
for (i, t) in test_cases.into_iter().enumerate() {
println!("running test case {}: '{}'", i, t.name);
let result = CheckPoint::from_block_ids(
t.blocks
.iter()
.map(|&(height, hash)| BlockId { height, hash }),
);
match t.exp_result {
Ok(_) => {
assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name);
let result_vec = {
let mut v = result
.unwrap()
.into_iter()
.map(|cp| (cp.height(), cp.hash()))
.collect::<Vec<_>>();
v.reverse();
v
};
assert_eq!(
&result_vec, t.blocks,
"[{}:{}] not equal to original block ids",
i, t.name
);
}
Err(exp_last) => {
assert!(result.is_err(), "[{}:{}] should be Err", i, t.name);
let err = result.unwrap_err();
assert_eq!(
err.as_ref()
.map(|last_cp| (last_cp.height(), last_cp.hash())),
exp_last,
"[{}:{}] error's last cp height should be {:?}, got {:?}",
i,
t.name,
exp_last,
err
);
}
}
}
}
#[test]
fn checkpoint_query() {
struct TestCase {
chain: LocalChain,
/// The heights we want to call [`CheckPoint::query`] with, represented as an inclusive
/// range.
///
/// If a [`CheckPoint`] exists at that height, we expect [`CheckPoint::query`] to return
/// it. If not, [`CheckPoint::query`] should return `None`.
query_range: (u32, u32),
}
let test_cases = [
TestCase {
chain: local_chain![(0, h!("_")), (1, h!("A"))],
query_range: (0, 2),
},
TestCase {
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
query_range: (0, 3),
},
];
for t in test_cases.into_iter() {
let tip = t.chain.tip();
for h in t.query_range.0..=t.query_range.1 {
let query_result = tip.get(h);
// perform an exhausitive search for the checkpoint at height `h`
let exp_hash = t
.chain
.iter_checkpoints()
.find(|cp| cp.height() == h)
.map(|cp| cp.hash());
match query_result {
Some(cp) => {
assert_eq!(Some(cp.hash()), exp_hash);
assert_eq!(cp.height(), h);
}
None => assert!(exp_hash.is_none()),
}
}
}
}
#[test]
fn checkpoint_insert() {
struct TestCase<'a> {
/// The name of the test.
name: &'a str,
/// The original checkpoint chain to call [`CheckPoint::insert`] on.
chain: &'a [(u32, BlockHash)],
/// The `block_id` to insert.
to_insert: (u32, BlockHash),
/// The expected final checkpoint chain after calling [`CheckPoint::insert`].
exp_final_chain: &'a [(u32, BlockHash)],
}
let test_cases = [
TestCase {
name: "insert_above_tip",
chain: &[(1, h!("a")), (2, h!("b"))],
to_insert: (4, h!("d")),
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (4, h!("d"))],
},
TestCase {
name: "insert_already_exists_expect_no_change",
chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
to_insert: (2, h!("b")),
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
},
TestCase {
name: "insert_in_middle",
chain: &[(2, h!("b")), (4, h!("d")), (5, h!("e"))],
to_insert: (3, h!("c")),
exp_final_chain: &[(2, h!("b")), (3, h!("c")), (4, h!("d")), (5, h!("e"))],
},
TestCase {
name: "replace_one",
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e"))],
to_insert: (5, h!("E")),
exp_final_chain: &[(3, h!("c")), (4, h!("d")), (5, h!("E"))],
},
TestCase {
name: "insert_conflict_should_evict",
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e")), (6, h!("f"))],
to_insert: (4, h!("D")),
exp_final_chain: &[(3, h!("c")), (4, h!("D"))],
},
];
fn genesis_block() -> impl Iterator<Item = BlockId> {
core::iter::once((0, h!("_"))).map(BlockId::from)
}
for (i, t) in test_cases.into_iter().enumerate() {
println!("Running [{}] '{}'", i, t.name);
let chain = CheckPoint::from_block_ids(
genesis_block().chain(t.chain.iter().copied().map(BlockId::from)),
)
.expect("test formed incorrectly, must construct checkpoint chain");
let exp_final_chain = CheckPoint::from_block_ids(
genesis_block().chain(t.exp_final_chain.iter().copied().map(BlockId::from)),
)
.expect("test formed incorrectly, must construct checkpoint chain");
assert_eq!(
chain.insert(t.to_insert.into()),
exp_final_chain,
"unexpected final chain"
);
}
}
#[test]
fn local_chain_apply_header_connected_to() {
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
Header {
version: bitcoin::block::Version::default(),
prev_blockhash,
merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(),
time: 0,
bits: bitcoin::CompactTarget::default(),
nonce: 0,
}
}
struct TestCase {
name: &'static str,
chain: LocalChain,
header: Header,
height: u32,
connected_to: BlockId,
exp_result: Result<Vec<(u32, Option<BlockHash>)>, ApplyHeaderError>,
}
let test_cases = [
{
let header = header_from_prev_blockhash(h!("_"));
let hash = header.block_hash();
let height = 1;
let connected_to = BlockId { height, hash };
TestCase {
name: "connected_to_self_header_applied_to_self",
chain: local_chain![(0, h!("_")), (height, hash)],
header,
height,
connected_to,
exp_result: Ok(vec![]),
}
},
{
let prev_hash = h!("A");
let prev_height = 1;
let header = header_from_prev_blockhash(prev_hash);
let hash = header.block_hash();
let height = prev_height + 1;
let connected_to = BlockId {
height: prev_height,
hash: prev_hash,
};
TestCase {
name: "connected_to_prev_header_applied_to_self",
chain: local_chain![(0, h!("_")), (prev_height, prev_hash)],
header,
height,
connected_to,
exp_result: Ok(vec![(height, Some(hash))]),
}
},
{
let header = header_from_prev_blockhash(BlockHash::all_zeros());
let hash = header.block_hash();
let height = 0;
let connected_to = BlockId { height, hash };
TestCase {
name: "genesis_applied_to_self",
chain: local_chain![(0, hash)],
header,
height,
connected_to,
exp_result: Ok(vec![]),
}
},
{
let header = header_from_prev_blockhash(h!("Z"));
let height = 10;
let hash = header.block_hash();
let prev_height = height - 1;
let prev_hash = header.prev_blockhash;
TestCase {
name: "connect_at_connected_to",
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
header,
height: 10,
connected_to: BlockId {
height: 3,
hash: h!("C"),
},
exp_result: Ok(vec![(prev_height, Some(prev_hash)), (height, Some(hash))]),
}
},
{
let prev_hash = h!("A");
let prev_height = 1;
let header = header_from_prev_blockhash(prev_hash);
let connected_to = BlockId {
height: prev_height,
hash: h!("not_prev_hash"),
};
TestCase {
name: "inconsistent_prev_hash",
chain: local_chain![(0, h!("_")), (prev_height, h!("not_prev_hash"))],
header,
height: prev_height + 1,
connected_to,
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
}
},
{
let prev_hash = h!("A");
let prev_height = 1;
let header = header_from_prev_blockhash(prev_hash);
let height = prev_height + 1;
let connected_to = BlockId {
height,
hash: h!("not_current_hash"),
};
TestCase {
name: "inconsistent_current_block",
chain: local_chain![(0, h!("_")), (height, h!("not_current_hash"))],
header,
height,
connected_to,
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
}
},
{
let header = header_from_prev_blockhash(h!("B"));
let height = 3;
let connected_to = BlockId {
height: 4,
hash: h!("D"),
};
TestCase {
name: "connected_to_is_greater",
chain: local_chain![(0, h!("_")), (2, h!("B"))],
header,
height,
connected_to,
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
}
},
];
for (i, t) in test_cases.into_iter().enumerate() {
println!("running test case {}: '{}'", i, t.name);
let mut chain = t.chain;
let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to);
let exp_result = t
.exp_result
.map(|cs| cs.iter().cloned().collect::<ChangeSet>());
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
}
}
fn generate_height_range_bounds(
height_upper_bound: u32,
) -> impl Strategy<Value = (Bound<u32>, Bound<u32>)> {
fn generate_height_bound(height_upper_bound: u32) -> impl Strategy<Value = Bound<u32>> {
prop_oneof![
(0..height_upper_bound).prop_map(Bound::Included),
(0..height_upper_bound).prop_map(Bound::Excluded),
Just(Bound::Unbounded),
]
}
(
generate_height_bound(height_upper_bound),
generate_height_bound(height_upper_bound),
)
}
fn generate_checkpoints(max_height: u32, max_count: usize) -> impl Strategy<Value = CheckPoint> {
proptest::collection::btree_set(1..max_height, 0..max_count).prop_map(|mut heights| {
heights.insert(0); // must have genesis
CheckPoint::from_block_ids(heights.into_iter().map(|height| {
let hash = bitcoin::hashes::Hash::hash(height.to_le_bytes().as_slice());
BlockId { height, hash }
}))
.expect("blocks must be in order as it comes from btreeset")
})
}
proptest! {
#![proptest_config(ProptestConfig {
..Default::default()
})]
/// Ensure that [`CheckPoint::range`] returns the expected checkpoint heights by comparing it
/// against a more primitive approach.
#[test]
fn checkpoint_range(
range in generate_height_range_bounds(21_000),
cp in generate_checkpoints(21_000, 2100)
) {
let exp_heights = cp.iter().map(|cp| cp.height()).filter(|h| range.contains(h)).collect::<Vec<u32>>();
let heights = cp.range(range).map(|cp| cp.height()).collect::<Vec<u32>>();
prop_assert_eq!(heights, exp_heights);
}
}

View File

@@ -1,7 +1,5 @@
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
use bitcoin::{
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
};
use bitcoin::{absolute, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
#[test]
fn spk_txout_sent_and_received() {
@@ -13,70 +11,48 @@ fn spk_txout_sent_and_received() {
index.insert_spk(1, spk2.clone());
let tx1 = Transaction {
version: transaction::Version::TWO,
version: 0x02,
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut {
value: Amount::from_sat(42_000),
value: 42_000,
script_pubkey: spk1.clone(),
}],
};
assert_eq!(
index.sent_and_received(&tx1, ..),
(Amount::from_sat(0), Amount::from_sat(42_000))
);
assert_eq!(
index.sent_and_received(&tx1, ..1),
(Amount::from_sat(0), Amount::from_sat(42_000))
);
assert_eq!(
index.sent_and_received(&tx1, 1..),
(Amount::from_sat(0), Amount::from_sat(0))
);
assert_eq!(index.net_value(&tx1, ..), SignedAmount::from_sat(42_000));
assert_eq!(index.sent_and_received(&tx1), (0, 42_000));
assert_eq!(index.net_value(&tx1), 42_000);
index.index_tx(&tx1);
assert_eq!(
index.sent_and_received(&tx1, ..),
(Amount::from_sat(0), Amount::from_sat(42_000)),
index.sent_and_received(&tx1),
(0, 42_000),
"shouldn't change after scanning"
);
let tx2 = Transaction {
version: transaction::Version::ONE,
version: 0x1,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: tx1.compute_txid(),
txid: tx1.txid(),
vout: 0,
},
..Default::default()
}],
output: vec![
TxOut {
value: Amount::from_sat(20_000),
value: 20_000,
script_pubkey: spk2,
},
TxOut {
script_pubkey: spk1,
value: Amount::from_sat(30_000),
value: 30_000,
},
],
};
assert_eq!(
index.sent_and_received(&tx2, ..),
(Amount::from_sat(42_000), Amount::from_sat(50_000))
);
assert_eq!(
index.sent_and_received(&tx2, ..1),
(Amount::from_sat(42_000), Amount::from_sat(30_000))
);
assert_eq!(
index.sent_and_received(&tx2, 1..),
(Amount::from_sat(0), Amount::from_sat(20_000))
);
assert_eq!(index.net_value(&tx2, ..), SignedAmount::from_sat(8_000));
assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000));
assert_eq!(index.net_value(&tx2), 8_000);
}
#[test]
@@ -97,11 +73,11 @@ fn mark_used() {
assert!(spk_index.is_used(&1));
let tx1 = Transaction {
version: transaction::Version::TWO,
version: 0x02,
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut {
value: Amount::from_sat(42_000),
value: 42_000,
script_pubkey: spk1,
}],
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use std::collections::{BTreeSet, HashSet};
use bdk_chain::{keychain::Balance, BlockId};
use bitcoin::{Amount, OutPoint, Script};
use bitcoin::{OutPoint, Script};
use common::*;
#[allow(dead_code)]
@@ -81,10 +79,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("confirmed_genesis", 0), ("confirmed_conflict", 0)]),
exp_unspents: HashSet::from([("confirmed_conflict", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(20000),
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 20000,
},
},
Scenario {
@@ -112,15 +110,14 @@ fn test_tx_conflict_handling() {
..Default::default()
},
],
// the txgraph is going to pick tx_conflict_2 because of higher lexicographical txid
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]),
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
},
},
Scenario {
@@ -152,10 +149,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]),
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
},
},
Scenario {
@@ -194,10 +191,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]),
exp_unspents: HashSet::from([("tx_conflict_3", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(40000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
immature: 0,
trusted_pending: 40000,
untrusted_pending: 0,
confirmed: 0,
},
},
Scenario {
@@ -229,10 +226,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]),
exp_unspents: HashSet::from([("tx_orphaned_conflict", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
},
},
Scenario {
@@ -264,10 +261,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]),
exp_unspents: HashSet::from([("tx_conflict_1", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(20000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
immature: 0,
trusted_pending: 20000,
untrusted_pending: 0,
confirmed: 0,
},
},
Scenario {
@@ -313,10 +310,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]),
exp_unspents: HashSet::from([("tx_confirmed_conflict", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 50000,
},
},
Scenario {
@@ -358,10 +355,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("C", 0)]),
exp_unspents: HashSet::from([("C", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
},
},
Scenario {
@@ -399,10 +396,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(20000),
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 20000,
},
},
Scenario {
@@ -444,10 +441,10 @@ fn test_tx_conflict_handling() {
]),
exp_unspents: HashSet::from([("C", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
},
},
Scenario {
@@ -489,10 +486,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
},
},
Scenario {
@@ -534,10 +531,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 50000,
},
},
Scenario {
@@ -585,10 +582,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 50000,
},
},
];

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_electrum"
version = "0.15.0"
version = "0.4.0"
edition = "2021"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
@@ -12,9 +12,6 @@ readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk_chain = { path = "../chain", version = "0.16.0" }
electrum-client = { version = "0.20" }
bdk_chain = { path = "../chain", version = "0.6.0", default-features = false }
electrum-client = { version = "0.18" }
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
[dev-dependencies]
bdk_testenv = { path = "../testenv", default-features = false }

View File

@@ -1,7 +1,3 @@
# BDK Electrum
BDK Electrum extends [`electrum-client`] to update [`bdk_chain`] structures
from an Electrum server.
[`electrum-client`]: https://docs.rs/electrum-client/
[`bdk_chain`]: https://docs.rs/bdk-chain/
BDK Electrum client library for updating the keychain tracker.

View File

@@ -1,589 +0,0 @@
use bdk_chain::{
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
collections::{BTreeMap, HashMap, HashSet},
local_chain::CheckPoint,
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
tx_graph::TxGraph,
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
};
use core::str::FromStr;
use electrum_client::{ElectrumApi, Error, HeaderNotification};
use std::sync::{Arc, Mutex};
/// We include a chain suffix of a certain length for the purpose of robustness.
const CHAIN_SUFFIX_LENGTH: u32 = 8;
/// Wrapper around an [`electrum_client::ElectrumApi`] which includes an internal in-memory
/// transaction cache to avoid re-fetching already downloaded transactions.
#[derive(Debug)]
pub struct BdkElectrumClient<E> {
/// The internal [`electrum_client::ElectrumApi`]
pub inner: E,
/// The transaction cache
tx_cache: Mutex<HashMap<Txid, Arc<Transaction>>>,
}
impl<E: ElectrumApi> BdkElectrumClient<E> {
/// Creates a new bdk client from a [`electrum_client::ElectrumApi`]
pub fn new(client: E) -> Self {
Self {
inner: client,
tx_cache: Default::default(),
}
}
/// Inserts transactions into the transaction cache so that the client will not fetch these
/// transactions.
pub fn populate_tx_cache<A>(&self, tx_graph: impl AsRef<TxGraph<A>>) {
let txs = tx_graph
.as_ref()
.full_txs()
.map(|tx_node| (tx_node.txid, tx_node.tx));
let mut tx_cache = self.tx_cache.lock().unwrap();
for (txid, tx) in txs {
tx_cache.insert(txid, tx);
}
}
/// Fetch transaction of given `txid`.
///
/// If it hits the cache it will return the cached version and avoid making the request.
pub fn fetch_tx(&self, txid: Txid) -> Result<Arc<Transaction>, Error> {
let tx_cache = self.tx_cache.lock().unwrap();
if let Some(tx) = tx_cache.get(&txid) {
return Ok(Arc::clone(tx));
}
drop(tx_cache);
let tx = Arc::new(self.inner.transaction_get(&txid)?);
self.tx_cache.lock().unwrap().insert(txid, Arc::clone(&tx));
Ok(tx)
}
/// Broadcasts a transaction to the network.
///
/// This is a re-export of [`ElectrumApi::transaction_broadcast`].
pub fn transaction_broadcast(&self, tx: &Transaction) -> Result<Txid, Error> {
self.inner.transaction_broadcast(tx)
}
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
/// returns updates for [`bdk_chain`] data structures.
///
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
/// see [`FullScanRequest`]
/// - `stop_gap`: the full scan for each keychain stops after a gap of script pubkeys with no
/// associated transactions
/// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
/// request
/// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
pub fn full_scan<K: Ord + Clone>(
&self,
request: FullScanRequest<K>,
stop_gap: usize,
batch_size: usize,
fetch_prev_txouts: bool,
) -> Result<ElectrumFullScanResult<K>, Error> {
let mut request_spks = request.spks_by_keychain;
// We keep track of already-scanned spks just in case a reorg happens and we need to do a
// rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
// cannot be collected. In addition, we keep track of whether an spk has an active tx
// history for determining the `last_active_index`.
// * key: (keychain, spk_index) that identifies the spk.
// * val: (script_pubkey, has_tx_history).
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
let update = loop {
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip.clone())?;
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
let cps = tip
.iter()
.take(10)
.map(|cp| (cp.height(), cp))
.collect::<BTreeMap<u32, CheckPoint>>();
if !request_spks.is_empty() {
if !scanned_spks.is_empty() {
scanned_spks.append(
&mut self.populate_with_spks(
&cps,
&mut graph_update,
&mut scanned_spks
.iter()
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
stop_gap,
batch_size,
)?,
);
}
for (keychain, keychain_spks) in &mut request_spks {
scanned_spks.extend(
self.populate_with_spks(
&cps,
&mut graph_update,
keychain_spks,
stop_gap,
batch_size,
)?
.into_iter()
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
);
}
}
// check for reorgs during scan process
let server_blockhash = self.inner.block_header(tip.height() as usize)?.block_hash();
if tip.hash() != server_blockhash {
continue; // reorg
}
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
self.fetch_prev_txout(&mut graph_update)?;
}
let chain_update = tip;
let keychain_update = request_spks
.into_keys()
.filter_map(|k| {
scanned_spks
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
.rev()
.find(|(_, (_, active))| *active)
.map(|((_, i), _)| (k, *i))
})
.collect::<BTreeMap<_, _>>();
break FullScanResult {
graph_update,
chain_update,
last_active_indices: keychain_update,
};
};
Ok(ElectrumFullScanResult(update))
}
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
/// and returns updates for [`bdk_chain`] data structures.
///
/// - `request`: struct with data required to perform a spk-based blockchain client sync,
/// see [`SyncRequest`]
/// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
/// request
/// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
/// calculation
///
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
/// may include scripts that have been used, use [`full_scan`] with the keychain.
///
/// [`full_scan`]: Self::full_scan
pub fn sync(
&self,
request: SyncRequest,
batch_size: usize,
fetch_prev_txouts: bool,
) -> Result<ElectrumSyncResult, Error> {
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
let mut full_scan_res = self
.full_scan(full_scan_req, usize::MAX, batch_size, false)?
.with_confirmation_height_anchor();
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip)?;
let cps = tip
.iter()
.take(10)
.map(|cp| (cp.height(), cp))
.collect::<BTreeMap<u32, CheckPoint>>();
self.populate_with_txids(&cps, &mut full_scan_res.graph_update, request.txids)?;
self.populate_with_outpoints(&cps, &mut full_scan_res.graph_update, request.outpoints)?;
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
self.fetch_prev_txout(&mut full_scan_res.graph_update)?;
}
Ok(ElectrumSyncResult(SyncResult {
chain_update: full_scan_res.chain_update,
graph_update: full_scan_res.graph_update,
}))
}
/// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
///
/// Transactions that contains an output with requested spk, or spends form an output with
/// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
/// also included.
///
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
fn populate_with_spks<I: Ord + Clone>(
&self,
cps: &BTreeMap<u32, CheckPoint>,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
stop_gap: usize,
batch_size: usize,
) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
let mut unused_spk_count = 0_usize;
let mut scanned_spks = BTreeMap::new();
loop {
let spks = (0..batch_size)
.map_while(|_| spks.next())
.collect::<Vec<_>>();
if spks.is_empty() {
return Ok(scanned_spks);
}
let spk_histories = self
.inner
.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
if spk_history.is_empty() {
scanned_spks.insert(spk_index, (spk, false));
unused_spk_count += 1;
if unused_spk_count > stop_gap {
return Ok(scanned_spks);
}
continue;
} else {
scanned_spks.insert(spk_index, (spk, true));
unused_spk_count = 0;
}
for tx_res in spk_history {
let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?);
if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
}
}
}
}
}
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
// which we do not have by default. This data is needed to calculate the transaction fee.
fn fetch_prev_txout(
&self,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
) -> Result<(), Error> {
let full_txs: Vec<Arc<Transaction>> =
graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
for tx in full_txs {
for vin in &tx.input {
let outpoint = vin.previous_output;
let vout = outpoint.vout;
let prev_tx = self.fetch_tx(outpoint.txid)?;
let txout = prev_tx.output[vout as usize].clone();
let _ = graph_update.insert_txout(outpoint, txout);
}
}
Ok(())
}
/// Populate the `graph_update` with associated transactions/anchors of `outpoints`.
///
/// Transactions in which the outpoint resides, and transactions that spend from the outpoint are
/// included. Anchors of the aforementioned transactions are included.
///
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
fn populate_with_outpoints(
&self,
cps: &BTreeMap<u32, CheckPoint>,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
outpoints: impl IntoIterator<Item = OutPoint>,
) -> Result<(), Error> {
for outpoint in outpoints {
let op_txid = outpoint.txid;
let op_tx = self.fetch_tx(op_txid)?;
let op_txout = match op_tx.output.get(outpoint.vout as usize) {
Some(txout) => txout,
None => continue,
};
debug_assert_eq!(op_tx.compute_txid(), op_txid);
// attempt to find the following transactions (alongside their chain positions), and
// add to our sparsechain `update`:
let mut has_residing = false; // tx in which the outpoint resides
let mut has_spending = false; // tx that spends the outpoint
for res in self.inner.script_get_history(&op_txout.script_pubkey)? {
if has_residing && has_spending {
break;
}
if !has_residing && res.tx_hash == op_txid {
has_residing = true;
let _ = graph_update.insert_tx(Arc::clone(&op_tx));
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
}
}
if !has_spending && res.tx_hash != op_txid {
let res_tx = self.fetch_tx(res.tx_hash)?;
// we exclude txs/anchors that do not spend our specified outpoint(s)
has_spending = res_tx
.input
.iter()
.any(|txin| txin.previous_output == outpoint);
if !has_spending {
continue;
}
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
}
}
}
}
Ok(())
}
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
fn populate_with_txids(
&self,
cps: &BTreeMap<u32, CheckPoint>,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
txids: impl IntoIterator<Item = Txid>,
) -> Result<(), Error> {
for txid in txids {
let tx = match self.fetch_tx(txid) {
Ok(tx) => tx,
Err(electrum_client::Error::Protocol(_)) => continue,
Err(other_err) => return Err(other_err),
};
let spk = tx
.output
.first()
.map(|txo| &txo.script_pubkey)
.expect("tx must have an output");
// because of restrictions of the Electrum API, we have to use the `script_get_history`
// call to get confirmation status of our transaction
let anchor = match self
.inner
.script_get_history(spk)?
.into_iter()
.find(|r| r.tx_hash == txid)
{
Some(r) => determine_tx_anchor(cps, r.height, txid),
None => continue,
};
let _ = graph_update.insert_tx(tx);
if let Some(anchor) = anchor {
let _ = graph_update.insert_anchor(txid, anchor);
}
}
Ok(())
}
}
/// The result of [`BdkElectrumClient::full_scan`].
///
/// This can be transformed into a [`FullScanResult`] with either [`ConfirmationHeightAnchor`] or
/// [`ConfirmationTimeHeightAnchor`] anchor types.
pub struct ElectrumFullScanResult<K>(FullScanResult<K, ConfirmationHeightAnchor>);
impl<K> ElectrumFullScanResult<K> {
/// Return [`FullScanResult`] with [`ConfirmationHeightAnchor`].
pub fn with_confirmation_height_anchor(self) -> FullScanResult<K, ConfirmationHeightAnchor> {
self.0
}
/// Return [`FullScanResult`] with [`ConfirmationTimeHeightAnchor`].
///
/// This requires additional calls to the Electrum server.
pub fn with_confirmation_time_height_anchor(
self,
client: &BdkElectrumClient<impl ElectrumApi>,
) -> Result<FullScanResult<K, ConfirmationTimeHeightAnchor>, Error> {
let res = self.0;
Ok(FullScanResult {
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
chain_update: res.chain_update,
last_active_indices: res.last_active_indices,
})
}
}
/// The result of [`BdkElectrumClient::sync`].
///
/// This can be transformed into a [`SyncResult`] with either [`ConfirmationHeightAnchor`] or
/// [`ConfirmationTimeHeightAnchor`] anchor types.
pub struct ElectrumSyncResult(SyncResult<ConfirmationHeightAnchor>);
impl ElectrumSyncResult {
/// Return [`SyncResult`] with [`ConfirmationHeightAnchor`].
pub fn with_confirmation_height_anchor(self) -> SyncResult<ConfirmationHeightAnchor> {
self.0
}
/// Return [`SyncResult`] with [`ConfirmationTimeHeightAnchor`].
///
/// This requires additional calls to the Electrum server.
pub fn with_confirmation_time_height_anchor(
self,
client: &BdkElectrumClient<impl ElectrumApi>,
) -> Result<SyncResult<ConfirmationTimeHeightAnchor>, Error> {
let res = self.0;
Ok(SyncResult {
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
chain_update: res.chain_update,
})
}
}
fn try_into_confirmation_time_result(
graph_update: TxGraph<ConfirmationHeightAnchor>,
client: &impl ElectrumApi,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let relevant_heights = graph_update
.all_anchors()
.iter()
.map(|(a, _)| a.confirmation_height)
.collect::<HashSet<_>>();
let height_to_time = relevant_heights
.clone()
.into_iter()
.zip(
client
.batch_block_header(relevant_heights)?
.into_iter()
.map(|bh| bh.time as u64),
)
.collect::<HashMap<u32, u64>>();
Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
anchor_block: a.anchor_block,
confirmation_height: a.confirmation_height,
confirmation_time: height_to_time[&a.confirmation_height],
}))
}
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
fn construct_update_tip(
client: &impl ElectrumApi,
prev_tip: CheckPoint,
) -> Result<(CheckPoint, Option<u32>), Error> {
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
let new_tip_height = height as u32;
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
// not need updating. We just return the previous tip and use that as the point of agreement.
if new_tip_height < prev_tip.height() {
return Ok((prev_tip.clone(), Some(prev_tip.height())));
}
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
// to construct our checkpoint update.
let mut new_blocks = {
let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
let hashes = client
.block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
.headers
.into_iter()
.map(|h| h.block_hash());
(start_height..).zip(hashes).collect::<BTreeMap<u32, _>>()
};
// Find the "point of agreement" (if any).
let agreement_cp = {
let mut agreement_cp = Option::<CheckPoint>::None;
for cp in prev_tip.iter() {
let cp_block = cp.block_id();
let hash = match new_blocks.get(&cp_block.height) {
Some(&hash) => hash,
None => {
assert!(
new_tip_height >= cp_block.height,
"already checked that electrum's tip cannot be smaller"
);
let hash = client.block_header(cp_block.height as _)?.block_hash();
new_blocks.insert(cp_block.height, hash);
hash
}
};
if hash == cp_block.hash {
agreement_cp = Some(cp);
break;
}
}
agreement_cp
};
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
let new_tip = new_blocks
.into_iter()
// Prune `new_blocks` to only include blocks that are actually new.
.filter(|(height, _)| Some(*height) > agreement_height)
.map(|(height, hash)| BlockId { height, hash })
.fold(agreement_cp, |prev_cp, block| {
Some(match prev_cp {
Some(cp) => cp.push(block).expect("must extend checkpoint"),
None => CheckPoint::new(block),
})
})
.expect("must have at least one checkpoint");
Ok((new_tip, agreement_height))
}
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
///
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
///
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
fn determine_tx_anchor(
cps: &BTreeMap<u32, CheckPoint>,
raw_height: i32,
txid: Txid,
) -> Option<ConfirmationHeightAnchor> {
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
// height of 0. To avoid invalid representation in our data structures, we manually set
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
// unconfirmed for all other transactions.
if txid
== Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
.expect("must deserialize genesis coinbase txid")
{
let anchor_block = cps.values().next()?.block_id();
return Some(ConfirmationHeightAnchor {
anchor_block,
confirmation_height: 0,
});
}
match raw_height {
h if h <= 0 => {
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
None
}
h => {
let h = h as u32;
let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
if h > anchor_block.height {
None
} else {
Some(ConfirmationHeightAnchor {
anchor_block,
confirmation_height: h,
})
}
}
}
}

View File

@@ -0,0 +1,544 @@
use bdk_chain::{
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
local_chain::{self, CheckPoint},
tx_graph::{self, TxGraph},
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
};
use electrum_client::{Client, ElectrumApi, Error, HeaderNotification};
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fmt::Debug,
str::FromStr,
};
/// We include a chain suffix of a certain length for the purpose of robustness.
const CHAIN_SUFFIX_LENGTH: u32 = 8;
/// Represents updates fetched from an Electrum server, but excludes full transactions.
///
/// To provide a complete update to [`TxGraph`], you'll need to call [`Self::missing_full_txs`] to
/// determine the full transactions missing from [`TxGraph`]. Then call [`Self::into_tx_graph`] to
/// fetch the full transactions from Electrum and finalize the update.
#[derive(Debug, Default, Clone)]
pub struct RelevantTxids(HashMap<Txid, BTreeSet<ConfirmationHeightAnchor>>);
impl RelevantTxids {
/// Determine the full transactions that are missing from `graph`.
///
/// Refer to [`RelevantTxids`] for more details.
pub fn missing_full_txs<A: Anchor>(&self, graph: &TxGraph<A>) -> Vec<Txid> {
self.0
.keys()
.filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
.cloned()
.collect()
}
/// Finalizes the [`TxGraph`] update by fetching `missing` txids from the `client`.
///
/// Refer to [`RelevantTxids`] for more details.
pub fn into_tx_graph(
self,
client: &Client,
seen_at: Option<u64>,
missing: Vec<Txid>,
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
let new_txs = client.batch_transaction_get(&missing)?;
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
for (txid, anchors) in self.0 {
if let Some(seen_at) = seen_at {
let _ = graph.insert_seen_at(txid, seen_at);
}
for anchor in anchors {
let _ = graph.insert_anchor(txid, anchor);
}
}
Ok(graph)
}
/// Finalizes [`RelevantTxids`] with `new_txs` and anchors of type
/// [`ConfirmationTimeHeightAnchor`].
///
/// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
/// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
/// use it.
pub fn into_confirmation_time_tx_graph(
self,
client: &Client,
seen_at: Option<u64>,
missing: Vec<Txid>,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let graph = self.into_tx_graph(client, seen_at, missing)?;
let relevant_heights = {
let mut visited_heights = HashSet::new();
graph
.all_anchors()
.iter()
.map(|(a, _)| a.confirmation_height_upper_bound())
.filter(move |&h| visited_heights.insert(h))
.collect::<Vec<_>>()
};
let height_to_time = relevant_heights
.clone()
.into_iter()
.zip(
client
.batch_block_header(relevant_heights)?
.into_iter()
.map(|bh| bh.time as u64),
)
.collect::<HashMap<u32, u64>>();
let graph_changeset = {
let old_changeset = TxGraph::default().apply_update(graph);
tx_graph::ChangeSet {
txs: old_changeset.txs,
txouts: old_changeset.txouts,
last_seen: old_changeset.last_seen,
anchors: old_changeset
.anchors
.into_iter()
.map(|(height_anchor, txid)| {
let confirmation_height = height_anchor.confirmation_height;
let confirmation_time = height_to_time[&confirmation_height];
let time_anchor = ConfirmationTimeHeightAnchor {
anchor_block: height_anchor.anchor_block,
confirmation_height,
confirmation_time,
};
(time_anchor, txid)
})
.collect(),
}
};
let mut new_graph = TxGraph::default();
new_graph.apply_changeset(graph_changeset);
Ok(new_graph)
}
}
/// Combination of chain and transactions updates from electrum
///
/// We have to update the chain and the txids at the same time since we anchor the txids to
/// the same chain tip that we check before and after we gather the txids.
#[derive(Debug)]
pub struct ElectrumUpdate {
/// Chain update
pub chain_update: local_chain::Update,
/// Transaction updates from electrum
pub relevant_txids: RelevantTxids,
}
/// Trait to extend [`Client`] functionality.
pub trait ElectrumExt {
/// Scan the blockchain (via electrum) for the data specified and returns updates for
/// [`bdk_chain`] data structures.
///
/// - `prev_tip`: the most recent blockchain tip present locally
/// - `keychain_spks`: keychains that we want to scan transactions for
/// - `txids`: transactions for which we want updated [`Anchor`]s
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to included in the update
///
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
/// single batch request.
fn scan<K: Ord + Clone>(
&self,
prev_tip: CheckPoint,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
batch_size: usize,
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
/// Convenience method to call [`scan`] without requiring a keychain.
///
/// [`scan`]: ElectrumExt::scan
fn scan_without_keychain(
&self,
prev_tip: CheckPoint,
misc_spks: impl IntoIterator<Item = ScriptBuf>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
batch_size: usize,
) -> Result<ElectrumUpdate, Error> {
let spk_iter = misc_spks
.into_iter()
.enumerate()
.map(|(i, spk)| (i as u32, spk));
let (electrum_update, _) = self.scan(
prev_tip,
[((), spk_iter)].into(),
txids,
outpoints,
usize::MAX,
batch_size,
)?;
Ok(electrum_update)
}
}
impl ElectrumExt for Client {
fn scan<K: Ord + Clone>(
&self,
prev_tip: CheckPoint,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
batch_size: usize,
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
let mut request_spks = keychain_spks
.into_iter()
.map(|(k, s)| (k, s.into_iter()))
.collect::<BTreeMap<K, _>>();
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
let txids = txids.into_iter().collect::<Vec<_>>();
let outpoints = outpoints.into_iter().collect::<Vec<_>>();
let (electrum_update, keychain_update) = loop {
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
let mut relevant_txids = RelevantTxids::default();
let cps = tip
.iter()
.take(10)
.map(|cp| (cp.height(), cp))
.collect::<BTreeMap<u32, CheckPoint>>();
if !request_spks.is_empty() {
if !scanned_spks.is_empty() {
scanned_spks.append(&mut populate_with_spks(
self,
&cps,
&mut relevant_txids,
&mut scanned_spks
.iter()
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
stop_gap,
batch_size,
)?);
}
for (keychain, keychain_spks) in &mut request_spks {
scanned_spks.extend(
populate_with_spks(
self,
&cps,
&mut relevant_txids,
keychain_spks,
stop_gap,
batch_size,
)?
.into_iter()
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
);
}
}
populate_with_txids(self, &cps, &mut relevant_txids, &mut txids.iter().cloned())?;
let _txs = populate_with_outpoints(
self,
&cps,
&mut relevant_txids,
&mut outpoints.iter().cloned(),
)?;
// check for reorgs during scan process
let server_blockhash = self.block_header(tip.height() as usize)?.block_hash();
if tip.hash() != server_blockhash {
continue; // reorg
}
let chain_update = local_chain::Update {
tip,
introduce_older_blocks: true,
};
let keychain_update = request_spks
.into_keys()
.filter_map(|k| {
scanned_spks
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
.rev()
.find(|(_, (_, active))| *active)
.map(|((_, i), _)| (k, *i))
})
.collect::<BTreeMap<_, _>>();
break (
ElectrumUpdate {
chain_update,
relevant_txids,
},
keychain_update,
);
};
Ok((electrum_update, keychain_update))
}
}
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
fn construct_update_tip(
client: &Client,
prev_tip: CheckPoint,
) -> Result<(CheckPoint, Option<u32>), Error> {
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
let new_tip_height = height as u32;
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
// not need updating. We just return the previous tip and use that as the point of agreement.
if new_tip_height < prev_tip.height() {
return Ok((prev_tip.clone(), Some(prev_tip.height())));
}
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
// to construct our checkpoint update.
let mut new_blocks = {
let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
let hashes = client
.block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
.headers
.into_iter()
.map(|h| h.block_hash());
(start_height..).zip(hashes).collect::<BTreeMap<u32, _>>()
};
// Find the "point of agreement" (if any).
let agreement_cp = {
let mut agreement_cp = Option::<CheckPoint>::None;
for cp in prev_tip.iter() {
let cp_block = cp.block_id();
let hash = match new_blocks.get(&cp_block.height) {
Some(&hash) => hash,
None => {
assert!(
new_tip_height >= cp_block.height,
"already checked that electrum's tip cannot be smaller"
);
let hash = client.block_header(cp_block.height as _)?.block_hash();
new_blocks.insert(cp_block.height, hash);
hash
}
};
if hash == cp_block.hash {
agreement_cp = Some(cp);
break;
}
}
agreement_cp
};
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
let new_tip = new_blocks
.into_iter()
// Prune `new_blocks` to only include blocks that are actually new.
.filter(|(height, _)| Some(*height) > agreement_height)
.map(|(height, hash)| BlockId { height, hash })
.fold(agreement_cp, |prev_cp, block| {
Some(match prev_cp {
Some(cp) => cp.push(block).expect("must extend checkpoint"),
None => CheckPoint::new(block),
})
})
.expect("must have at least one checkpoint");
Ok((new_tip, agreement_height))
}
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
///
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
///
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
fn determine_tx_anchor(
cps: &BTreeMap<u32, CheckPoint>,
raw_height: i32,
txid: Txid,
) -> Option<ConfirmationHeightAnchor> {
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
// height of 0. To avoid invalid representation in our data structures, we manually set
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
// unconfirmed for all other transactions.
if txid
== Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
.expect("must deserialize genesis coinbase txid")
{
let anchor_block = cps.values().next()?.block_id();
return Some(ConfirmationHeightAnchor {
anchor_block,
confirmation_height: 0,
});
}
match raw_height {
h if h <= 0 => {
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
None
}
h => {
let h = h as u32;
let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
if h > anchor_block.height {
None
} else {
Some(ConfirmationHeightAnchor {
anchor_block,
confirmation_height: h,
})
}
}
}
}
fn populate_with_outpoints(
client: &Client,
cps: &BTreeMap<u32, CheckPoint>,
relevant_txids: &mut RelevantTxids,
outpoints: &mut impl Iterator<Item = OutPoint>,
) -> Result<HashMap<Txid, Transaction>, Error> {
let mut full_txs = HashMap::new();
for outpoint in outpoints {
let txid = outpoint.txid;
let tx = client.transaction_get(&txid)?;
debug_assert_eq!(tx.txid(), txid);
let txout = match tx.output.get(outpoint.vout as usize) {
Some(txout) => txout,
None => continue,
};
// attempt to find the following transactions (alongside their chain positions), and
// add to our sparsechain `update`:
let mut has_residing = false; // tx in which the outpoint resides
let mut has_spending = false; // tx that spends the outpoint
for res in client.script_get_history(&txout.script_pubkey)? {
if has_residing && has_spending {
break;
}
if res.tx_hash == txid {
if has_residing {
continue;
}
has_residing = true;
full_txs.insert(res.tx_hash, tx.clone());
} else {
if has_spending {
continue;
}
let res_tx = match full_txs.get(&res.tx_hash) {
Some(tx) => tx,
None => {
let res_tx = client.transaction_get(&res.tx_hash)?;
full_txs.insert(res.tx_hash, res_tx);
full_txs.get(&res.tx_hash).expect("just inserted")
}
};
has_spending = res_tx
.input
.iter()
.any(|txin| txin.previous_output == outpoint);
if !has_spending {
continue;
}
};
let anchor = determine_tx_anchor(cps, res.height, res.tx_hash);
let tx_entry = relevant_txids.0.entry(res.tx_hash).or_default();
if let Some(anchor) = anchor {
tx_entry.insert(anchor);
}
}
}
Ok(full_txs)
}
fn populate_with_txids(
client: &Client,
cps: &BTreeMap<u32, CheckPoint>,
relevant_txids: &mut RelevantTxids,
txids: &mut impl Iterator<Item = Txid>,
) -> Result<(), Error> {
for txid in txids {
let tx = match client.transaction_get(&txid) {
Ok(tx) => tx,
Err(electrum_client::Error::Protocol(_)) => continue,
Err(other_err) => return Err(other_err),
};
let spk = tx
.output
.get(0)
.map(|txo| &txo.script_pubkey)
.expect("tx must have an output");
let anchor = match client
.script_get_history(spk)?
.into_iter()
.find(|r| r.tx_hash == txid)
{
Some(r) => determine_tx_anchor(cps, r.height, txid),
None => continue,
};
let tx_entry = relevant_txids.0.entry(txid).or_default();
if let Some(anchor) = anchor {
tx_entry.insert(anchor);
}
}
Ok(())
}
fn populate_with_spks<I: Ord + Clone>(
client: &Client,
cps: &BTreeMap<u32, CheckPoint>,
relevant_txids: &mut RelevantTxids,
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
stop_gap: usize,
batch_size: usize,
) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
let mut unused_spk_count = 0_usize;
let mut scanned_spks = BTreeMap::new();
loop {
let spks = (0..batch_size)
.map_while(|_| spks.next())
.collect::<Vec<_>>();
if spks.is_empty() {
return Ok(scanned_spks);
}
let spk_histories =
client.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
if spk_history.is_empty() {
scanned_spks.insert(spk_index, (spk, false));
unused_spk_count += 1;
if unused_spk_count > stop_gap {
return Ok(scanned_spks);
}
continue;
} else {
scanned_spks.insert(spk_index, (spk, true));
unused_spk_count = 0;
}
for tx in spk_history {
let tx_entry = relevant_txids.0.entry(tx.tx_hash).or_default();
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
tx_entry.insert(anchor);
}
}
}
}
}

View File

@@ -1,22 +1,30 @@
//! This crate is used for updating structures of [`bdk_chain`] with data from an Electrum server.
//! This crate is used for updating structures of the [`bdk_chain`] crate with data from electrum.
//!
//! The two primary methods are [`BdkElectrumClient::sync`] and [`BdkElectrumClient::full_scan`]. In most cases
//! [`BdkElectrumClient::sync`] is used to sync the transaction histories of scripts that the application
//! cares about, for example the scripts for all the receive addresses of a Wallet's keychain that it
//! has shown a user. [`BdkElectrumClient::full_scan`] is meant to be used when importing or restoring a
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
//! sync or full scan the user receives relevant blockchain data and output updates for
//! [`bdk_chain`].
//! The star of the show is the [`ElectrumExt::scan`] method, which scans for relevant blockchain
//! data (via electrum) and outputs updates for [`bdk_chain`] structures as a tuple of form:
//!
//! Refer to [`example_electrum`] for a complete example.
//! ([`bdk_chain::local_chain::Update`], [`RelevantTxids`], `keychain_update`)
//!
//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum
//! An [`RelevantTxids`] only includes `txid`s and no full transactions. The caller is
//! responsible for obtaining full transactions before applying. This can be done with
//! these steps:
//!
//! 1. Determine which full transactions are missing. The method [`missing_full_txs`] of
//! [`RelevantTxids`] can be used.
//!
//! 2. Obtaining the full transactions. To do this via electrum, the method
//! [`batch_transaction_get`] can be used.
//!
//! Refer to [`bdk_electrum_example`] for a complete example.
//!
//! [`ElectrumClient::scan`]: electrum_client::ElectrumClient::scan
//! [`missing_full_txs`]: RelevantTxids::missing_full_txs
//! [`batch_transaction_get`]: electrum_client::ElectrumApi::batch_transaction_get
//! [`bdk_electrum_example`]: https://github.com/LLFourn/bdk_core_staging/tree/master/bdk_electrum_example
#![warn(missing_docs)]
mod bdk_electrum_client;
pub use bdk_electrum_client::*;
mod electrum_ext;
pub use bdk_chain;
pub use electrum_client;
pub use electrum_ext::*;

View File

@@ -1,220 +0,0 @@
use bdk_chain::{
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
keychain::Balance,
local_chain::LocalChain,
spk_client::SyncRequest,
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
};
use bdk_electrum::BdkElectrumClient;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
fn get_balance(
recv_chain: &LocalChain,
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
) -> anyhow::Result<Balance> {
let chain_tip = recv_chain.tip().block_id();
let outpoints = recv_graph.index.outpoints().clone();
let balance = recv_graph
.graph()
.balance(recv_chain, chain_tip, outpoints, |_, _| true);
Ok(balance)
}
/// Ensure that [`ElectrumExt`] can sync properly.
///
/// 1. Mine 101 blocks.
/// 2. Send a tx.
/// 3. Mine extra block to confirm sent tx.
/// 4. Check [`Balance`] to ensure tx is confirmed.
#[test]
fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
let env = TestEnv::new()?;
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
let client = BdkElectrumClient::new(electrum_client);
// Setup addresses.
let addr_to_mine = env
.bitcoind
.client
.get_new_address(None, None)?
.assume_checked();
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
// Setup receiver.
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone());
recv_index
});
// Mine some blocks.
env.mine_blocks(101, Some(addr_to_mine))?;
// Create transaction that is tracked by our receiver.
env.send(&addr_to_track, SEND_AMOUNT)?;
// Mine a block to confirm sent tx.
env.mine_blocks(1, None)?;
// Sync up to tip.
env.wait_until_electrum_sees_block()?;
let update = client
.sync(
SyncRequest::from_chain_tip(recv_chain.tip())
.chain_spks(core::iter::once(spk_to_track)),
5,
true,
)?
.with_confirmation_time_height_anchor(&client)?;
let _ = recv_chain
.apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
let _ = recv_graph.apply_update(update.graph_update);
// Check to see if tx is confirmed.
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT,
..Balance::default()
},
);
for tx in recv_graph.graph().full_txs() {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transaction's previous outputs.
let fee = recv_graph
.graph()
.calculate_fee(&tx.tx)
.expect("fee must exist");
// Retrieve the fee in the transaction data from `bitcoind`.
let tx_fee = env
.bitcoind
.client
.get_transaction(&tx.txid, None)
.expect("Tx must exist")
.fee
.expect("Fee must exist")
.abs()
.to_unsigned()
.expect("valid `Amount`");
// Check that the calculated fee matches the fee from the transaction data.
assert_eq!(fee, tx_fee);
}
Ok(())
}
/// Ensure that confirmed txs that are reorged become unconfirmed.
///
/// 1. Mine 101 blocks.
/// 2. Mine 8 blocks with a confirmed tx in each.
/// 3. Perform 8 separate reorgs on each block with a confirmed tx.
/// 4. Check [`Balance`] after each reorg to ensure unconfirmed amount is correct.
#[test]
fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
const REORG_COUNT: usize = 8;
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
let env = TestEnv::new()?;
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
let client = BdkElectrumClient::new(electrum_client);
// Setup addresses.
let addr_to_mine = env
.bitcoind
.client
.get_new_address(None, None)?
.assume_checked();
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
// Setup receiver.
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone());
recv_index
});
// Mine some blocks.
env.mine_blocks(101, Some(addr_to_mine))?;
// Create transactions that are tracked by our receiver.
for _ in 0..REORG_COUNT {
env.send(&addr_to_track, SEND_AMOUNT)?;
env.mine_blocks(1, None)?;
}
// Sync up to tip.
env.wait_until_electrum_sees_block()?;
let update = client
.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
5,
false,
)?
.with_confirmation_time_height_anchor(&client)?;
let _ = recv_chain
.apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
let _ = recv_graph.apply_update(update.graph_update.clone());
// Retain a snapshot of all anchors before reorg process.
let initial_anchors = update.graph_update.all_anchors();
// Check if initial balance is correct.
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT * REORG_COUNT as u64,
..Balance::default()
},
"initial balance must be correct",
);
// Perform reorgs with different depths.
for depth in 1..=REORG_COUNT {
env.reorg_empty_blocks(depth)?;
env.wait_until_electrum_sees_block()?;
let update = client
.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
5,
false,
)?
.with_confirmation_time_height_anchor(&client)?;
let _ = recv_chain
.apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
// Check to see if a new anchor is added during current reorg.
if !initial_anchors.is_superset(update.graph_update.all_anchors()) {
println!("New anchor added at reorg depth {}", depth);
}
let _ = recv_graph.apply_update(update.graph_update);
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
trusted_pending: SEND_AMOUNT * depth as u64,
..Balance::default()
},
"reorg_count: {}",
depth,
);
}
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_esplora"
version = "0.15.0"
version = "0.4.0"
edition = "2021"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
@@ -12,23 +12,23 @@ readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk_chain = { path = "../chain", version = "0.16.0", default-features = false }
esplora-client = { version = "0.8.0", default-features = false }
bdk_chain = { path = "../chain", version = "0.6.0", default-features = false }
esplora-client = { version = "0.6.0", default-features = false }
async-trait = { version = "0.1.66", optional = true }
futures = { version = "0.3.26", optional = true }
bitcoin = { version = "0.32.0", optional = true, default-features = false }
miniscript = { version = "12.0.0", optional = true, default-features = false }
# use these dependencies if you need to enable their /no-std features
bitcoin = { version = "0.30.0", optional = true, default-features = false }
miniscript = { version = "10.0.0", optional = true, default-features = false }
[dev-dependencies]
bdk_testenv = { path = "../testenv", default-features = false }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
[features]
default = ["std", "async-https", "blocking-https-rustls"]
std = ["bdk_chain/std", "miniscript?/std"]
default = ["std", "async-https", "blocking"]
std = ["bdk_chain/std"]
async = ["async-trait", "futures", "esplora-client/async"]
async-https = ["async", "esplora-client/async-https"]
async-https-rustls = ["async", "esplora-client/async-https-rustls"]
blocking = ["esplora-client/blocking"]
blocking-https-rustls = ["esplora-client/blocking-https-rustls"]

View File

@@ -30,7 +30,7 @@ use bdk_esplora::EsploraExt;
// use bdk_esplora::EsploraAsyncExt;
```
For full examples, refer to [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
For full examples, refer to [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora) (blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
[`esplora-client`]: https://docs.rs/esplora-client/
[`bdk_chain`]: https://docs.rs/bdk-chain/

View File

@@ -1,21 +1,15 @@
use std::collections::BTreeSet;
use async_trait::async_trait;
use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult};
use bdk_chain::collections::btree_map;
use bdk_chain::{
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
collections::BTreeMap,
local_chain::CheckPoint,
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
collections::{BTreeMap, BTreeSet},
local_chain::{self, CheckPoint},
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
};
use bdk_chain::{Anchor, Indexed};
use esplora_client::{Amount, TxStatus};
use esplora_client::{Error, TxStatus};
use futures::{stream::FuturesOrdered, TryStreamExt};
use crate::anchor_from_status;
/// [`esplora_client::Error`]
type Error = Box<esplora_client::Error>;
use crate::{anchor_from_status, ASSUME_FINAL_DEPTH};
/// Trait to extend the functionality of [`esplora_client::AsyncClient`].
///
@@ -25,251 +19,271 @@ type Error = Box<esplora_client::Error>;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EsploraAsyncExt {
/// Scan keychain scripts for transactions against Esplora, returning an update that can be
/// applied to the receiving structures.
/// Prepare an [`LocalChain`] update with blocks fetched from Esplora.
///
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
/// see [`FullScanRequest`]
/// * `local_tip` is the previous tip of [`LocalChain::tip`].
/// * `request_heights` is the block heights that we are interested in fetching from Esplora.
///
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no
/// associated transactions. `parallel_requests` specifies the max number of HTTP requests to
/// make in parallel.
/// The result of this method can be applied to [`LocalChain::apply_update`].
///
/// ## Note
///
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
///
/// This follows the same approach as other Bitcoin-related software,
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
///
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
async fn full_scan<K: Ord + Clone + Send>(
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
#[allow(clippy::result_large_err)]
async fn update_local_chain(
&self,
request: FullScanRequest<K>,
local_tip: CheckPoint,
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
) -> Result<local_chain::Update, Error>;
/// Scan Esplora for the data specified and return a [`TxGraph`] and a map of last active
/// indices.
///
/// * `keychain_spks`: keychains that we want to scan transactions for
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to include in the update
///
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
#[allow(clippy::result_large_err)]
async fn scan_txs_with_keychains<K: Ord + Clone + Send>(
&self,
keychain_spks: BTreeMap<
K,
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
>,
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize,
parallel_requests: usize,
) -> Result<FullScanResult<K>, Error>;
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
/// specified and return a [`TxGraph`].
/// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain.
///
/// - `request`: struct with data required to perform a spk-based blockchain client sync, see
/// [`SyncRequest`]
///
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
/// may include scripts that have been used, use [`full_scan`] with the keychain.
///
/// [`full_scan`]: EsploraAsyncExt::full_scan
async fn sync(
/// [`scan_txs_with_keychains`]: EsploraAsyncExt::scan_txs_with_keychains
#[allow(clippy::result_large_err)]
async fn scan_txs(
&self,
request: SyncRequest,
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize,
) -> Result<SyncResult, Error>;
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
self.scan_txs_with_keychains(
[(
(),
misc_spks
.into_iter()
.enumerate()
.map(|(i, spk)| (i as u32, spk)),
)]
.into(),
txids,
outpoints,
usize::MAX,
parallel_requests,
)
.await
.map(|(g, _)| g)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EsploraAsyncExt for esplora_client::AsyncClient {
async fn full_scan<K: Ord + Clone + Send>(
async fn update_local_chain(
&self,
request: FullScanRequest<K>,
local_tip: CheckPoint,
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
) -> Result<local_chain::Update, Error> {
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
let new_tip_height = self.get_height().await?;
// atomically fetch blocks from esplora
let mut fetched_blocks = {
let heights = (0..=new_tip_height).rev();
let hashes = self
.get_blocks(Some(new_tip_height))
.await?
.into_iter()
.map(|b| b.id);
heights.zip(hashes).collect::<BTreeMap<u32, BlockHash>>()
};
// fetch heights that the caller is interested in
for height in request_heights {
// do not fetch blocks higher than remote tip
if height > new_tip_height {
continue;
}
// only fetch what is missing
if let btree_map::Entry::Vacant(entry) = fetched_blocks.entry(height) {
let hash = self.get_block_hash(height).await?;
entry.insert(hash);
}
}
// find the earliest point of agreement between local chain and fetched chain
let earliest_agreement_cp = {
let mut earliest_agreement_cp = Option::<CheckPoint>::None;
let local_tip_height = local_tip.height();
for local_cp in local_tip.iter() {
let local_block = local_cp.block_id();
// the updated hash (block hash at this height after the update), can either be:
// 1. a block that already existed in `fetched_blocks`
// 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
// 3. otherwise we can freshly fetch the block from remote, which is safe as it
// is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
// remote tip
let updated_hash = match fetched_blocks.entry(local_block.height) {
btree_map::Entry::Occupied(entry) => *entry.get(),
btree_map::Entry::Vacant(entry) => *entry.insert(
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
local_block.hash
} else {
self.get_block_hash(local_block.height).await?
},
),
};
// since we may introduce blocks below the point of agreement, we cannot break
// here unconditionally - we only break if we guarantee there are no new heights
// below our current local checkpoint
if local_block.hash == updated_hash {
earliest_agreement_cp = Some(local_cp);
let first_new_height = *fetched_blocks
.keys()
.next()
.expect("must have at least one new block");
if first_new_height >= local_block.height {
break;
}
}
}
earliest_agreement_cp
};
let tip = {
// first checkpoint to use for the update chain
let first_cp = match earliest_agreement_cp {
Some(cp) => cp,
None => {
let (&height, &hash) = fetched_blocks
.iter()
.next()
.expect("must have at least one new block");
CheckPoint::new(BlockId { height, hash })
}
};
// transform fetched chain into the update chain
fetched_blocks
// we exclude anything at or below the first cp of the update chain otherwise
// building the chain will fail
.split_off(&(first_cp.height() + 1))
.into_iter()
.map(|(height, hash)| BlockId { height, hash })
.fold(first_cp, |prev_cp, block| {
prev_cp.push(block).expect("must extend checkpoint")
})
};
Ok(local_chain::Update {
tip,
introduce_older_blocks: true,
})
}
async fn scan_txs_with_keychains<K: Ord + Clone + Send>(
&self,
keychain_spks: BTreeMap<
K,
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
>,
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize,
parallel_requests: usize,
) -> Result<FullScanResult<K>, Error> {
let latest_blocks = fetch_latest_blocks(self).await?;
let (graph_update, last_active_indices) = full_scan_for_index_and_graph(
self,
request.spks_by_keychain,
stop_gap,
parallel_requests,
)
.await?;
let chain_update = chain_update(
self,
&latest_blocks,
&request.chain_tip,
graph_update.all_anchors(),
)
.await?;
Ok(FullScanResult {
chain_update,
graph_update,
last_active_indices,
})
}
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
let parallel_requests = Ord::max(parallel_requests, 1);
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
let mut last_active_indexes = BTreeMap::<K, u32>::new();
async fn sync(
&self,
request: SyncRequest,
parallel_requests: usize,
) -> Result<SyncResult, Error> {
let latest_blocks = fetch_latest_blocks(self).await?;
let graph_update = sync_for_index_and_graph(
self,
request.spks,
request.txids,
request.outpoints,
parallel_requests,
)
.await?;
let chain_update = chain_update(
self,
&latest_blocks,
&request.chain_tip,
graph_update.all_anchors(),
)
.await?;
Ok(SyncResult {
chain_update,
graph_update,
})
}
}
for (keychain, spks) in keychain_spks {
let mut spks = spks.into_iter();
let mut last_index = Option::<u32>::None;
let mut last_active_index = Option::<u32>::None;
/// Fetch latest blocks from Esplora in an atomic call.
///
/// We want to do this before fetching transactions and anchors as we cannot fetch latest blocks AND
/// transactions atomically, and the checkpoint tip is used to determine last-scanned block (for
/// block-based chain-sources). Therefore it's better to be conservative when setting the tip (use
/// an earlier tip rather than a later tip) otherwise the caller may accidentally skip blocks when
/// alternating between chain-sources.
async fn fetch_latest_blocks(
client: &esplora_client::AsyncClient,
) -> Result<BTreeMap<u32, BlockHash>, Error> {
Ok(client
.get_blocks(None)
.await?
.into_iter()
.map(|b| (b.time.height, b.id))
.collect())
}
/// Used instead of [`esplora_client::BlockingClient::get_block_hash`].
///
/// This first checks the previously fetched `latest_blocks` before fetching from Esplora again.
async fn fetch_block(
client: &esplora_client::AsyncClient,
latest_blocks: &BTreeMap<u32, BlockHash>,
height: u32,
) -> Result<Option<BlockHash>, Error> {
if let Some(&hash) = latest_blocks.get(&height) {
return Ok(Some(hash));
}
// We avoid fetching blocks higher than previously fetched `latest_blocks` as the local chain
// tip is used to signal for the last-synced-up-to-height.
let &tip_height = latest_blocks
.keys()
.last()
.expect("must have atleast one entry");
if height > tip_height {
return Ok(None);
}
Ok(Some(client.get_block_hash(height).await?))
}
/// Create the [`local_chain::Update`].
///
/// We want to have a corresponding checkpoint per anchor height. However, checkpoints fetched
/// should not surpass `latest_blocks`.
async fn chain_update<A: Anchor>(
client: &esplora_client::AsyncClient,
latest_blocks: &BTreeMap<u32, BlockHash>,
local_tip: &CheckPoint,
anchors: &BTreeSet<(A, Txid)>,
) -> Result<CheckPoint, Error> {
let mut point_of_agreement = None;
let mut conflicts = vec![];
for local_cp in local_tip.iter() {
let remote_hash = match fetch_block(client, latest_blocks, local_cp.height()).await? {
Some(hash) => hash,
None => continue,
};
if remote_hash == local_cp.hash() {
point_of_agreement = Some(local_cp.clone());
break;
} else {
// it is not strictly necessary to include all the conflicted heights (we do need the
// first one) but it seems prudent to make sure the updated chain's heights are a
// superset of the existing chain after update.
conflicts.push(BlockId {
height: local_cp.height(),
hash: remote_hash,
});
}
}
let mut tip = point_of_agreement.expect("remote esplora should have same genesis block");
tip = tip
.extend(conflicts.into_iter().rev())
.expect("evicted are in order");
for anchor in anchors {
let height = anchor.0.anchor_block().height;
if tip.get(height).is_none() {
let hash = match fetch_block(client, latest_blocks, height).await? {
Some(hash) => hash,
None => continue,
};
tip = tip.insert(BlockId { height, hash });
}
}
// insert the most recent blocks at the tip to make sure we update the tip and make the update
// robust.
for (&height, &hash) in latest_blocks.iter() {
tip = tip.insert(BlockId { height, hash });
}
Ok(tip)
}
/// This performs a full scan to get an update for the [`TxGraph`] and
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
client: &esplora_client::AsyncClient,
keychain_spks: BTreeMap<
K,
impl IntoIterator<IntoIter = impl Iterator<Item = Indexed<ScriptBuf>> + Send> + Send,
>,
stop_gap: usize,
parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
let parallel_requests = Ord::max(parallel_requests, 1);
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
let mut last_active_indexes = BTreeMap::<K, u32>::new();
for (keychain, spks) in keychain_spks {
let mut spks = spks.into_iter();
let mut last_index = Option::<u32>::None;
let mut last_active_index = Option::<u32>::None;
loop {
let handles = spks
.by_ref()
.take(parallel_requests)
.map(|(spk_index, spk)| {
let client = client.clone();
async move {
let mut last_seen = None;
let mut spk_txs = Vec::new();
loop {
let txs = client.scripthash_txs(&spk, last_seen).await?;
let tx_count = txs.len();
last_seen = txs.last().map(|tx| tx.txid);
spk_txs.extend(txs);
if tx_count < 25 {
break Result::<_, Error>::Ok((spk_index, spk_txs));
loop {
let handles = spks
.by_ref()
.take(parallel_requests)
.map(|(spk_index, spk)| {
let client = self.clone();
async move {
let mut last_seen = None;
let mut spk_txs = Vec::new();
loop {
let txs = client.scripthash_txs(&spk, last_seen).await?;
let tx_count = txs.len();
last_seen = txs.last().map(|tx| tx.txid);
spk_txs.extend(txs);
if tx_count < 25 {
break Result::<_, Error>::Ok((spk_index, spk_txs));
}
}
}
})
.collect::<FuturesOrdered<_>>();
if handles.is_empty() {
break;
}
for (index, txs) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
last_index = Some(index);
if !txs.is_empty() {
last_active_index = Some(index);
}
for tx in txs {
let _ = graph.insert_tx(tx.to_tx());
if let Some(anchor) = anchor_from_status(&tx.status) {
let _ = graph.insert_anchor(tx.txid, anchor);
}
}
}
let last_index = last_index.expect("Must be set since handles wasn't empty.");
let past_gap_limit = if let Some(i) = last_active_index {
last_index > i.saturating_add(stop_gap as u32)
} else {
last_index >= stop_gap as u32
};
if past_gap_limit {
break;
}
}
if let Some(last_active_index) = last_active_index {
last_active_indexes.insert(keychain, last_active_index);
}
}
let mut txids = txids.into_iter();
loop {
let handles = txids
.by_ref()
.take(parallel_requests)
.filter(|&txid| graph.get_tx(txid).is_none())
.map(|txid| {
let client = self.clone();
async move { client.get_tx_status(&txid).await.map(|s| (txid, s)) }
})
.collect::<FuturesOrdered<_>>();
@@ -277,314 +291,39 @@ async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
break;
}
for (index, txs) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
last_index = Some(index);
if !txs.is_empty() {
last_active_index = Some(index);
for (txid, status) in handles.try_collect::<Vec<(Txid, TxStatus)>>().await? {
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(txid, anchor);
}
for tx in txs {
let _ = graph.insert_tx(tx.to_tx());
if let Some(anchor) = anchor_from_status(&tx.status) {
let _ = graph.insert_anchor(tx.txid, anchor);
}
}
}
let previous_outputs = tx.vin.iter().filter_map(|vin| {
let prevout = vin.prevout.as_ref()?;
Some((
OutPoint {
txid: vin.txid,
vout: vin.vout,
},
TxOut {
script_pubkey: prevout.scriptpubkey.clone(),
value: Amount::from_sat(prevout.value),
},
))
});
for (outpoint, txout) in previous_outputs {
let _ = graph.insert_txout(outpoint, txout);
}
for op in outpoints.into_iter() {
if graph.get_tx(op.txid).is_none() {
if let Some(tx) = self.get_tx(&op.txid).await? {
let _ = graph.insert_tx(tx);
}
let status = self.get_tx_status(&op.txid).await?;
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(op.txid, anchor);
}
}
let last_index = last_index.expect("Must be set since handles wasn't empty.");
let gap_limit_reached = if let Some(i) = last_active_index {
last_index >= i.saturating_add(stop_gap as u32)
} else {
last_index + 1 >= stop_gap as u32
};
if gap_limit_reached {
break;
}
}
if let Some(last_active_index) = last_active_index {
last_active_indexes.insert(keychain, last_active_index);
}
}
Ok((graph, last_active_indexes))
}
async fn sync_for_index_and_graph(
client: &esplora_client::AsyncClient,
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let mut graph = full_scan_for_index_and_graph(
client,
[(
(),
misc_spks
.into_iter()
.enumerate()
.map(|(i, spk)| (i as u32, spk)),
)]
.into(),
usize::MAX,
parallel_requests,
)
.await
.map(|(g, _)| g)?;
let mut txids = txids.into_iter();
loop {
let handles = txids
.by_ref()
.take(parallel_requests)
.filter(|&txid| graph.get_tx(txid).is_none())
.map(|txid| {
let client = client.clone();
async move { client.get_tx_status(&txid).await.map(|s| (txid, s)) }
})
.collect::<FuturesOrdered<_>>();
if handles.is_empty() {
break;
}
for (txid, status) in handles.try_collect::<Vec<(Txid, TxStatus)>>().await? {
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(txid, anchor);
}
}
}
for op in outpoints.into_iter() {
if graph.get_tx(op.txid).is_none() {
if let Some(tx) = client.get_tx(&op.txid).await? {
let _ = graph.insert_tx(tx);
}
let status = client.get_tx_status(&op.txid).await?;
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(op.txid, anchor);
}
}
if let Some(op_status) = client.get_output_status(&op.txid, op.vout as _).await? {
if let Some(txid) = op_status.txid {
if graph.get_tx(txid).is_none() {
if let Some(tx) = client.get_tx(&txid).await? {
let _ = graph.insert_tx(tx);
}
let status = client.get_tx_status(&txid).await?;
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(txid, anchor);
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _).await? {
if let Some(txid) = op_status.txid {
if graph.get_tx(txid).is_none() {
if let Some(tx) = self.get_tx(&txid).await? {
let _ = graph.insert_tx(tx);
}
let status = self.get_tx_status(&txid).await?;
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(txid, anchor);
}
}
}
}
}
}
Ok(graph)
}
#[cfg(test)]
mod test {
use std::{collections::BTreeSet, time::Duration};
use bdk_chain::{
bitcoin::{hashes::Hash, Txid},
local_chain::LocalChain,
BlockId,
};
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
use esplora_client::Builder;
use crate::async_ext::{chain_update, fetch_latest_blocks};
macro_rules! h {
($index:literal) => {{
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
}};
}
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
#[tokio::test]
pub async fn test_finalize_chain_update() -> anyhow::Result<()> {
struct TestCase<'a> {
name: &'a str,
/// Initial blockchain height to start the env with.
initial_env_height: u32,
/// Initial checkpoint heights to start with.
initial_cps: &'a [u32],
/// The final blockchain height of the env.
final_env_height: u32,
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
/// the blockhash from the env.
anchors: &'a [(u32, Txid)],
}
let test_cases = [
TestCase {
name: "chain_extends",
initial_env_height: 60,
initial_cps: &[59, 60],
final_env_height: 90,
anchors: &[],
},
TestCase {
name: "introduce_older_heights",
initial_env_height: 50,
initial_cps: &[10, 15],
final_env_height: 50,
anchors: &[(11, h!("A")), (14, h!("B"))],
},
TestCase {
name: "introduce_older_heights_after_chain_extends",
initial_env_height: 50,
initial_cps: &[10, 15],
final_env_height: 100,
anchors: &[(11, h!("A")), (14, h!("B"))],
},
];
for (i, t) in test_cases.into_iter().enumerate() {
println!("[{}] running test case: {}", i, t.name);
let env = TestEnv::new()?;
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
let client = Builder::new(base_url.as_str()).build_async()?;
// set env to `initial_env_height`
if let Some(to_mine) = t
.initial_env_height
.checked_sub(env.make_checkpoint_tip().height())
{
env.mine_blocks(to_mine as _, None)?;
}
while client.get_height().await? < t.initial_env_height {
std::thread::sleep(Duration::from_millis(10));
}
// craft initial `local_chain`
let local_chain = {
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
// force `chain_update_blocking` to add all checkpoints in `t.initial_cps`
let anchors = t
.initial_cps
.iter()
.map(|&height| -> anyhow::Result<_> {
Ok((
BlockId {
height,
hash: env.bitcoind.client.get_block_hash(height as _)?,
},
Txid::all_zeros(),
))
})
.collect::<anyhow::Result<BTreeSet<_>>>()?;
let update = chain_update(
&client,
&fetch_latest_blocks(&client).await?,
&chain.tip(),
&anchors,
)
.await?;
chain.apply_update(update)?;
chain
};
println!("local chain height: {}", local_chain.tip().height());
// extend env chain
if let Some(to_mine) = t
.final_env_height
.checked_sub(env.make_checkpoint_tip().height())
{
env.mine_blocks(to_mine as _, None)?;
}
while client.get_height().await? < t.final_env_height {
std::thread::sleep(Duration::from_millis(10));
}
// craft update
let update = {
let anchors = t
.anchors
.iter()
.map(|&(height, txid)| -> anyhow::Result<_> {
Ok((
BlockId {
height,
hash: env.bitcoind.client.get_block_hash(height as _)?,
},
txid,
))
})
.collect::<anyhow::Result<_>>()?;
chain_update(
&client,
&fetch_latest_blocks(&client).await?,
&local_chain.tip(),
&anchors,
)
.await?
};
// apply update
let mut updated_local_chain = local_chain.clone();
updated_local_chain.apply_update(update)?;
println!(
"updated local chain height: {}",
updated_local_chain.tip().height()
);
assert!(
{
let initial_heights = local_chain
.iter_checkpoints()
.map(|cp| cp.height())
.collect::<BTreeSet<_>>();
let updated_heights = updated_local_chain
.iter_checkpoints()
.map(|cp| cp.height())
.collect::<BTreeSet<_>>();
updated_heights.is_superset(&initial_heights)
},
"heights from the initial chain must all be in the updated chain",
);
assert!(
{
let exp_anchor_heights = t
.anchors
.iter()
.map(|(h, _)| *h)
.chain(t.initial_cps.iter().copied())
.collect::<BTreeSet<_>>();
let anchor_heights = updated_local_chain
.iter_checkpoints()
.map(|cp| cp.height())
.collect::<BTreeSet<_>>();
anchor_heights.is_superset(&exp_anchor_heights)
},
"anchor heights must all be in updated chain",
);
}
Ok(())
Ok((graph, last_active_indexes))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,4 @@
#![doc = include_str!("../README.md")]
//! This crate is used for updating structures of [`bdk_chain`] with data from an Esplora server.
//!
//! The two primary methods are [`EsploraExt::sync`] and [`EsploraExt::full_scan`]. In most cases
//! [`EsploraExt::sync`] is used to sync the transaction histories of scripts that the application
//! cares about, for example the scripts for all the receive addresses of a Wallet's keychain that it
//! has shown a user. [`EsploraExt::full_scan`] is meant to be used when importing or restoring a
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
//! sync or full scan the user receives relevant blockchain data and output updates for [`bdk_chain`]
//! via a new [`TxGraph`] to be appended to any existing [`TxGraph`] data.
//!
//! Refer to [`example_esplora`] for a complete example.
//!
//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
use esplora_client::TxStatus;
@@ -31,6 +14,8 @@ mod async_ext;
#[cfg(feature = "async")]
pub use async_ext::*;
const ASSUME_FINAL_DEPTH: u32 = 15;
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor> {
if let TxStatus {
block_height: Some(height),

View File

@@ -1,20 +1,68 @@
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
use bdk_esplora::EsploraAsyncExt;
use esplora_client::{self, Builder};
use std::collections::{BTreeSet, HashSet};
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use electrsd::bitcoind::{self, anyhow, BitcoinD};
use electrsd::{Conf, ElectrsD};
use esplora_client::{self, AsyncClient, Builder};
use std::collections::{BTreeMap, HashSet};
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
use bdk_chain::bitcoin::{Address, Amount, Txid};
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
struct TestEnv {
bitcoind: BitcoinD,
#[allow(dead_code)]
electrsd: ElectrsD,
client: AsyncClient,
}
impl TestEnv {
fn new() -> Result<Self, anyhow::Error> {
let bitcoind_exe =
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
let mut electrs_conf = Conf::default();
electrs_conf.http_enabled = true;
let electrs_exe =
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
let client = Builder::new(base_url.as_str()).build_async()?;
Ok(Self {
bitcoind,
electrsd,
client,
})
}
fn mine_blocks(
&self,
count: usize,
address: Option<Address>,
) -> anyhow::Result<Vec<BlockHash>> {
let coinbase_address = match address {
Some(address) => address,
None => self
.bitcoind
.client
.get_new_address(None, None)?
.assume_checked(),
};
let block_hashes = self
.bitcoind
.client
.generate_to_address(count as _, &coinbase_address)?;
Ok(block_hashes)
}
}
#[tokio::test]
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
let client = Builder::new(base_url.as_str()).build_async()?;
let receive_address0 =
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
let receive_address1 =
@@ -47,57 +95,19 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while client.get_height().await.unwrap() < 102 {
while env.client.get_height().await.unwrap() < 102 {
sleep(Duration::from_millis(10))
}
// use a full checkpoint linked list (since this is not what we are testing)
let cp_tip = env.make_checkpoint_tip();
let sync_update = {
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
client.sync(request, 1).await?
};
assert!(
{
let update_cps = sync_update
.chain_update
.iter()
.map(|cp| cp.block_id())
.collect::<BTreeSet<_>>();
let superset_cps = cp_tip
.iter()
.map(|cp| cp.block_id())
.collect::<BTreeSet<_>>();
superset_cps.is_superset(&update_cps)
},
"update should not alter original checkpoint tip since we already started with all checkpoints",
);
let graph_update = sync_update.graph_update;
// Check to see if we have the floating txouts available from our two created transactions'
// previous outputs in order to calculate transaction fees.
for tx in graph_update.full_txs() {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transactions' previous outputs.
let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
// Retrieve the fee in the transaction data from `bitcoind`.
let tx_fee = env
.bitcoind
.client
.get_transaction(&tx.txid, None)
.expect("Tx must exist")
.fee
.expect("Fee must exist")
.abs()
.to_unsigned()
.expect("valid `Amount`");
// Check that the calculated fee matches the fee from the transaction data.
assert_eq!(fee, tx_fee);
}
let graph_update = env
.client
.scan_txs(
misc_spks.into_iter(),
vec![].into_iter(),
vec![].into_iter(),
1,
)
.await?;
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
graph_update_txids.sort();
@@ -107,12 +117,10 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
Ok(())
}
/// Test the bounds of the address scan depending on the `stop_gap`.
/// Test the bounds of the address scan depending on the gap limit.
#[tokio::test]
pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
let client = Builder::new(base_url.as_str()).build_async()?;
let _block_hashes = env.mine_blocks(101, None)?;
// Now let's test the gap limit. First of all get a chain of 10 addresses.
@@ -137,6 +145,8 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
.enumerate()
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
.collect();
let mut keychains = BTreeMap::new();
keychains.insert(0, spks);
// Then receive coins on the 4th address.
let txid_4th_addr = env.bitcoind.client.send_to_address(
@@ -150,37 +160,36 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while client.get_height().await.unwrap() < 103 {
while env.client.get_height().await.unwrap() < 103 {
sleep(Duration::from_millis(10))
}
// use a full checkpoint linked list (since this is not what we are testing)
let cp_tip = env.make_checkpoint_tip();
// A scan with a gap limit of 3 won't find the transaction, but a scan with a gap limit of 4
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 3, 1).await?
};
assert!(full_scan_update.graph_update.full_txs().next().is_none());
assert!(full_scan_update.last_active_indices.is_empty());
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 4, 1).await?
};
assert_eq!(
full_scan_update
.graph_update
.full_txs()
.next()
.unwrap()
.txid,
txid_4th_addr
);
assert_eq!(full_scan_update.last_active_indices[&0], 3);
let (graph_update, active_indices) = env
.client
.scan_txs_with_keychains(
keychains.clone(),
vec![].into_iter(),
vec![].into_iter(),
2,
1,
)
.await?;
assert!(graph_update.full_txs().next().is_none());
assert!(active_indices.is_empty());
let (graph_update, active_indices) = env
.client
.scan_txs_with_keychains(
keychains.clone(),
vec![].into_iter(),
vec![].into_iter(),
3,
1,
)
.await?;
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
assert_eq!(active_indices[&0], 3);
// Now receive a coin on the last address.
let txid_last_addr = env.bitcoind.client.send_to_address(
@@ -194,38 +203,34 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while client.get_height().await.unwrap() < 104 {
while env.client.get_height().await.unwrap() < 104 {
sleep(Duration::from_millis(10))
}
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
// The last active indice won't be updated in the first case but will in the second one.
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 5, 1).await?
};
let txs: HashSet<_> = full_scan_update
.graph_update
.full_txs()
.map(|tx| tx.txid)
.collect();
let (graph_update, active_indices) = env
.client
.scan_txs_with_keychains(
keychains.clone(),
vec![].into_iter(),
vec![].into_iter(),
4,
1,
)
.await?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(full_scan_update.last_active_indices[&0], 3);
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 6, 1).await?
};
let txs: HashSet<_> = full_scan_update
.graph_update
.full_txs()
.map(|tx| tx.txid)
.collect();
assert_eq!(active_indices[&0], 3);
let (graph_update, active_indices) = env
.client
.scan_txs_with_keychains(keychains, vec![].into_iter(), vec![].into_iter(), 5, 1)
.await?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
assert_eq!(full_scan_update.last_active_indices[&0], 9);
assert_eq!(active_indices[&0], 9);
Ok(())
}

View File

@@ -1,20 +1,68 @@
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
use bdk_esplora::EsploraExt;
use esplora_client::{self, Builder};
use std::collections::{BTreeSet, HashSet};
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use electrsd::bitcoind::{self, anyhow, BitcoinD};
use electrsd::{Conf, ElectrsD};
use esplora_client::{self, BlockingClient, Builder};
use std::collections::{BTreeMap, HashSet};
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
use bdk_chain::bitcoin::{Address, Amount, Txid};
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
struct TestEnv {
bitcoind: BitcoinD,
#[allow(dead_code)]
electrsd: ElectrsD,
client: BlockingClient,
}
impl TestEnv {
fn new() -> Result<Self, anyhow::Error> {
let bitcoind_exe =
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
let mut electrs_conf = Conf::default();
electrs_conf.http_enabled = true;
let electrs_exe =
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
let client = Builder::new(base_url.as_str()).build_blocking()?;
Ok(Self {
bitcoind,
electrsd,
client,
})
}
fn mine_blocks(
&self,
count: usize,
address: Option<Address>,
) -> anyhow::Result<Vec<BlockHash>> {
let coinbase_address = match address {
Some(address) => address,
None => self
.bitcoind
.client
.get_new_address(None, None)?
.assume_checked(),
};
let block_hashes = self
.bitcoind
.client
.generate_to_address(count as _, &coinbase_address)?;
Ok(block_hashes)
}
}
#[test]
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
let client = Builder::new(base_url.as_str()).build_blocking();
let receive_address0 =
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
let receive_address1 =
@@ -47,57 +95,16 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while client.get_height().unwrap() < 102 {
while env.client.get_height().unwrap() < 102 {
sleep(Duration::from_millis(10))
}
// use a full checkpoint linked list (since this is not what we are testing)
let cp_tip = env.make_checkpoint_tip();
let sync_update = {
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
client.sync(request, 1)?
};
assert!(
{
let update_cps = sync_update
.chain_update
.iter()
.map(|cp| cp.block_id())
.collect::<BTreeSet<_>>();
let superset_cps = cp_tip
.iter()
.map(|cp| cp.block_id())
.collect::<BTreeSet<_>>();
superset_cps.is_superset(&update_cps)
},
"update should not alter original checkpoint tip since we already started with all checkpoints",
);
let graph_update = sync_update.graph_update;
// Check to see if we have the floating txouts available from our two created transactions'
// previous outputs in order to calculate transaction fees.
for tx in graph_update.full_txs() {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transactions' previous outputs.
let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
// Retrieve the fee in the transaction data from `bitcoind`.
let tx_fee = env
.bitcoind
.client
.get_transaction(&tx.txid, None)
.expect("Tx must exist")
.fee
.expect("Fee must exist")
.abs()
.to_unsigned()
.expect("valid `Amount`");
// Check that the calculated fee matches the fee from the transaction data.
assert_eq!(fee, tx_fee);
}
let graph_update = env.client.scan_txs(
misc_spks.into_iter(),
vec![].into_iter(),
vec![].into_iter(),
1,
)?;
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
graph_update_txids.sort();
@@ -108,12 +115,10 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
Ok(())
}
/// Test the bounds of the address scan depending on the `stop_gap`.
/// Test the bounds of the address scan depending on the gap limit.
#[test]
pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
let client = Builder::new(base_url.as_str()).build_blocking();
let _block_hashes = env.mine_blocks(101, None)?;
// Now let's test the gap limit. First of all get a chain of 10 addresses.
@@ -138,6 +143,8 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
.enumerate()
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
.collect();
let mut keychains = BTreeMap::new();
keychains.insert(0, spks);
// Then receive coins on the 4th address.
let txid_4th_addr = env.bitcoind.client.send_to_address(
@@ -151,37 +158,30 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while client.get_height().unwrap() < 103 {
while env.client.get_height().unwrap() < 103 {
sleep(Duration::from_millis(10))
}
// use a full checkpoint linked list (since this is not what we are testing)
let cp_tip = env.make_checkpoint_tip();
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 3, 1)?
};
assert!(full_scan_update.graph_update.full_txs().next().is_none());
assert!(full_scan_update.last_active_indices.is_empty());
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 4, 1)?
};
assert_eq!(
full_scan_update
.graph_update
.full_txs()
.next()
.unwrap()
.txid,
txid_4th_addr
);
assert_eq!(full_scan_update.last_active_indices[&0], 3);
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
keychains.clone(),
vec![].into_iter(),
vec![].into_iter(),
2,
1,
)?;
assert!(graph_update.full_txs().next().is_none());
assert!(active_indices.is_empty());
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
keychains.clone(),
vec![].into_iter(),
vec![].into_iter(),
3,
1,
)?;
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
assert_eq!(active_indices[&0], 3);
// Now receive a coin on the last address.
let txid_last_addr = env.bitcoind.client.send_to_address(
@@ -195,38 +195,34 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while client.get_height().unwrap() < 104 {
while env.client.get_height().unwrap() < 104 {
sleep(Duration::from_millis(10))
}
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
// The last active indice won't be updated in the first case but will in the second one.
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 5, 1)?
};
let txs: HashSet<_> = full_scan_update
.graph_update
.full_txs()
.map(|tx| tx.txid)
.collect();
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
keychains.clone(),
vec![].into_iter(),
vec![].into_iter(),
4,
1,
)?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(full_scan_update.last_active_indices[&0], 3);
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 6, 1)?
};
let txs: HashSet<_> = full_scan_update
.graph_update
.full_txs()
.map(|tx| tx.txid)
.collect();
assert_eq!(active_indices[&0], 3);
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
keychains,
vec![].into_iter(),
vec![].into_iter(),
5,
1,
)?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
assert_eq!(full_scan_update.last_active_indices[&0], 9);
assert_eq!(active_indices[&0], 9);
Ok(())
}

View File

@@ -1,17 +1,17 @@
[package]
name = "bdk_file_store"
version = "0.13.0"
version = "0.2.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_file_store"
description = "A simple append-only flat file database for persisting bdk_chain data."
description = "A simple append-only flat file implementation of Persist for Bitcoin Dev Kit."
keywords = ["bitcoin", "persist", "persistence", "bdk", "file"]
authors = ["Bitcoin Dev Kit Developers"]
readme = "README.md"
[dependencies]
bdk_chain = { path = "../chain", version = "0.16.0", features = [ "serde", "miniscript" ] }
bdk_chain = { path = "../chain", version = "0.6.0", features = [ "serde", "miniscript" ] }
bincode = { version = "1" }
serde = { version = "1", features = ["derive"] }

View File

@@ -1,7 +1,10 @@
# BDK File Store
This is a simple append-only flat file database for persisting [`bdk_chain`] changesets.
This is a simple append-only flat file implementation of
[`Persist`](`bdk_chain::Persist`).
The main structure is [`Store`] which works with any [`bdk_chain`] based changesets to persist data into a flat file.
The main structure is [`Store`](`crate::Store`), which can be used with [`bdk`]'s
`Wallet` to persist wallet data into a flat file.
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
[`bdk`]: https://docs.rs/bdk/latest
[`bdk_chain`]: https://docs.rs/bdk_chain/latest

View File

@@ -1,7 +1,7 @@
use bincode::Options;
use std::{
fs::File,
io::{self, BufReader, Seek},
io::{self, Seek},
marker::PhantomData,
};
@@ -14,9 +14,8 @@ use crate::bincode_options;
///
/// [`next`]: Self::next
pub struct EntryIter<'t, T> {
/// Buffered reader around the file
db_file: BufReader<&'t mut File>,
finished: bool,
db_file: Option<&'t mut File>,
/// The file position for the first read of `db_file`.
start_pos: Option<u64>,
types: PhantomData<T>,
@@ -25,9 +24,8 @@ pub struct EntryIter<'t, T> {
impl<'t, T> EntryIter<'t, T> {
pub fn new(start_pos: u64, db_file: &'t mut File) -> Self {
Self {
db_file: BufReader::new(db_file),
db_file: Some(db_file),
start_pos: Some(start_pos),
finished: false,
types: PhantomData,
}
}
@@ -40,44 +38,44 @@ where
type Item = Result<T, IterError>;
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
(|| {
if let Some(start) = self.start_pos.take() {
self.db_file.seek(io::SeekFrom::Start(start))?;
}
// closure which reads a single entry starting from `self.pos`
let read_one = |f: &mut File, start_pos: Option<u64>| -> Result<Option<T>, IterError> {
let pos = match start_pos {
Some(pos) => f.seek(io::SeekFrom::Start(pos))?,
None => f.stream_position()?,
};
let pos_before_read = self.db_file.stream_position()?;
match bincode_options().deserialize_from(&mut self.db_file) {
Ok(changeset) => Ok(Some(changeset)),
match bincode_options().deserialize_from(&*f) {
Ok(changeset) => {
f.stream_position()?;
Ok(Some(changeset))
}
Err(e) => {
self.finished = true;
let pos_after_read = self.db_file.stream_position()?;
// allow unexpected EOF if 0 bytes were read
if let bincode::ErrorKind::Io(inner) = &*e {
if inner.kind() == io::ErrorKind::UnexpectedEof
&& pos_after_read == pos_before_read
{
return Ok(None);
if inner.kind() == io::ErrorKind::UnexpectedEof {
let eof = f.seek(io::SeekFrom::End(0))?;
if pos == eof {
return Ok(None);
}
}
}
self.db_file.seek(io::SeekFrom::Start(pos_before_read))?;
f.seek(io::SeekFrom::Start(pos))?;
Err(IterError::Bincode(*e))
}
}
})()
.transpose()
};
let result = read_one(self.db_file.as_mut()?, self.start_pos.take());
if result.is_err() {
self.db_file = None;
}
result.transpose()
}
}
impl<'t, T> Drop for EntryIter<'t, T> {
fn drop(&mut self) {
// This syncs the underlying file's offset with the buffer's position. This way, we
// maintain the correct position to start the next read/write.
if let Ok(pos) = self.db_file.stream_position() {
let _ = self.db_file.get_mut().seek(io::SeekFrom::Start(pos));
}
impl From<io::Error> for IterError {
fn from(value: io::Error) -> Self {
IterError::Io(value)
}
}
@@ -99,10 +97,4 @@ impl core::fmt::Display for IterError {
}
}
impl From<io::Error> for IterError {
fn from(value: io::Error) -> Self {
IterError::Io(value)
}
}
impl std::error::Error for IterError {}

View File

@@ -13,14 +13,14 @@ pub(crate) fn bincode_options() -> impl bincode::Options {
/// Error that occurs due to problems encountered with the file.
#[derive(Debug)]
pub enum FileError {
pub enum FileError<'a> {
/// IO error, this may mean that the file is too short.
Io(io::Error),
/// Magic bytes do not match what is expected.
InvalidMagicBytes { got: Vec<u8>, expected: Vec<u8> },
InvalidMagicBytes { got: Vec<u8>, expected: &'a [u8] },
}
impl core::fmt::Display for FileError {
impl<'a> core::fmt::Display for FileError<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Io(e) => write!(f, "io error trying to read file: {}", e),
@@ -33,10 +33,10 @@ impl core::fmt::Display for FileError {
}
}
impl From<io::Error> for FileError {
impl<'a> From<io::Error> for FileError<'a> {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
impl std::error::Error for FileError {}
impl<'a> std::error::Error for FileError<'a> {}

View File

@@ -1,32 +1,46 @@
use crate::{bincode_options, EntryIter, FileError, IterError};
use bdk_chain::Append;
use bincode::Options;
use std::{
fmt::{self, Debug},
fmt::Debug,
fs::{File, OpenOptions},
io::{self, Read, Seek, Write},
marker::PhantomData,
path::Path,
};
use bdk_chain::{Append, PersistBackend};
use bincode::Options;
use crate::{bincode_options, EntryIter, FileError, IterError};
/// Persists an append-only list of changesets (`C`) to a single file.
///
/// The changesets are the results of altering a tracker implementation (`T`).
#[derive(Debug)]
pub struct Store<C>
where
C: Sync + Send,
{
magic_len: usize,
pub struct Store<'a, C> {
magic: &'a [u8],
db_file: File,
marker: PhantomData<C>,
}
impl<C> Store<C>
impl<'a, C> PersistBackend<C> for Store<'a, C>
where
C: Append
+ serde::Serialize
+ serde::de::DeserializeOwned
+ core::marker::Send
+ core::marker::Sync,
C: Append + serde::Serialize + serde::de::DeserializeOwned,
{
type WriteError = std::io::Error;
type LoadError = IterError;
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError> {
self.append_changeset(changeset)
}
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
self.aggregate_changesets().map_err(|e| e.iter_error)
}
}
impl<'a, C> Store<'a, C>
where
C: Append + serde::Serialize + serde::de::DeserializeOwned,
{
/// Create a new [`Store`] file in write-only mode; error if the file exists.
///
@@ -34,7 +48,7 @@ where
/// the `Store` in the future with [`open`].
///
/// [`open`]: Store::open
pub fn create_new<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
pub fn create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
where
P: AsRef<Path>,
{
@@ -50,11 +64,10 @@ where
.create(true)
.read(true)
.write(true)
.truncate(true)
.open(file_path)?;
f.write_all(magic)?;
Ok(Self {
magic_len: magic.len(),
magic,
db_file: f,
marker: Default::default(),
})
@@ -70,7 +83,7 @@ where
/// [`FileError::InvalidMagicBytes`] error variant will be returned.
///
/// [`create_new`]: Store::create_new
pub fn open<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
pub fn open<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
where
P: AsRef<Path>,
{
@@ -81,24 +94,24 @@ where
if magic_buf != magic {
return Err(FileError::InvalidMagicBytes {
got: magic_buf,
expected: magic.to_vec(),
expected: magic,
});
}
Ok(Self {
magic_len: magic.len(),
magic,
db_file: f,
marker: Default::default(),
})
}
/// Attempt to open existing [`Store`] file; create it if the file is non-existent.
/// Attempt to open existing [`Store`] file; create it if the file is non-existant.
///
/// Internally, this calls either [`open`] or [`create_new`].
///
/// [`open`]: Store::open
/// [`create_new`]: Store::create_new
pub fn open_or_create_new<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
pub fn open_or_create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
where
P: AsRef<Path>,
{
@@ -119,18 +132,18 @@ where
/// always iterate over all entries until `None` is returned if you want your next write to go
/// at the end; otherwise, you will write over existing entries.
pub fn iter_changesets(&mut self) -> EntryIter<C> {
EntryIter::new(self.magic_len as u64, &mut self.db_file)
EntryIter::new(self.magic.len() as u64, &mut self.db_file)
}
/// Loads all the changesets that have been stored as one giant changeset.
///
/// This function returns the aggregate changeset, or `None` if nothing was persisted.
/// If reading or deserializing any of the entries fails, an error is returned that
/// consists of all those it was able to read.
/// This function returns a tuple of the aggregate changeset and a result that indicates
/// whether an error occurred while reading or deserializing one of the entries. If so the
/// changeset will consist of all of those it was able to read.
///
/// You should usually check the error. In many applications, it may make sense to do a full
/// wallet scan with a stop-gap after getting an error, since it is likely that one of the
/// changesets was unable to read changes of the derivation indices of a keychain.
/// changesets it was unable to read changed the derivation indices of the tracker.
///
/// **WARNING**: This method changes the write position of the underlying file. The next
/// changeset will be written over the erroring entry (or the end of the file if none existed).
@@ -168,7 +181,7 @@ where
bincode_options()
.serialize_into(&mut self.db_file, changeset)
.map_err(|e| match *e {
bincode::ErrorKind::Io(error) => error,
bincode::ErrorKind::Io(inner) => inner,
unexpected_err => panic!("unexpected bincode error: {}", unexpected_err),
})?;
@@ -198,7 +211,7 @@ impl<C> std::fmt::Display for AggregateChangesetsError<C> {
}
}
impl<C: fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
impl<C: std::fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
#[cfg(test)]
mod test {
@@ -206,7 +219,6 @@ mod test {
use bincode::DefaultOptions;
use std::{
collections::BTreeSet,
io::{Read, Write},
vec::Vec,
};
@@ -216,7 +228,10 @@ mod test {
const TEST_MAGIC_BYTES: [u8; TEST_MAGIC_BYTES_LEN] =
[98, 100, 107, 102, 115, 49, 49, 49, 49, 49, 49, 49];
type TestChangeSet = BTreeSet<String>;
type TestChangeSet = Vec<String>;
#[derive(Debug)]
struct TestTracker;
/// Check behavior of [`Store::create_new`] and [`Store::open`].
#[test]
@@ -238,7 +253,7 @@ mod test {
fn open_or_create_new() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("db_file");
let changeset = BTreeSet::from(["hello".to_string(), "world".to_string()]);
let changeset = vec!["hello".to_string(), "world".to_string()];
{
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
@@ -289,7 +304,7 @@ mod test {
let mut data = [255_u8; 2000];
data[..TEST_MAGIC_BYTES_LEN].copy_from_slice(&TEST_MAGIC_BYTES);
let changeset = TestChangeSet::from(["one".into(), "two".into(), "three!".into()]);
let changeset = vec!["one".into(), "two".into(), "three!".into()];
let mut file = NamedTempFile::new().unwrap();
file.write_all(&data).expect("should write");
@@ -325,119 +340,4 @@ mod test {
assert_eq!(got_bytes, expected_bytes);
}
#[test]
fn last_write_is_short() {
let temp_dir = tempfile::tempdir().unwrap();
let changesets = [
TestChangeSet::from(["1".into()]),
TestChangeSet::from(["2".into(), "3".into()]),
TestChangeSet::from(["4".into(), "5".into(), "6".into()]),
];
let last_changeset = TestChangeSet::from(["7".into(), "8".into(), "9".into()]);
let last_changeset_bytes = bincode_options().serialize(&last_changeset).unwrap();
for short_write_len in 1..last_changeset_bytes.len() - 1 {
let file_path = temp_dir.path().join(format!("{}.dat", short_write_len));
println!("Test file: {:?}", file_path);
// simulate creating a file, writing data where the last write is incomplete
{
let mut db =
Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap();
for changeset in &changesets {
db.append_changeset(changeset).unwrap();
}
// this is the incomplete write
db.db_file
.write_all(&last_changeset_bytes[..short_write_len])
.unwrap();
}
// load file again and aggregate changesets
// write the last changeset again (this time it succeeds)
{
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
let err = db
.aggregate_changesets()
.expect_err("should return error as last read is short");
assert_eq!(
err.changeset,
changesets.iter().cloned().reduce(|mut acc, cs| {
Append::append(&mut acc, cs);
acc
}),
"should recover all changesets that are written in full",
);
db.db_file.write_all(&last_changeset_bytes).unwrap();
}
// load file again - this time we should successfully aggregate all changesets
{
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
let aggregated_changesets = db
.aggregate_changesets()
.expect("aggregating all changesets should succeed");
assert_eq!(
aggregated_changesets,
changesets
.iter()
.cloned()
.chain(core::iter::once(last_changeset.clone()))
.reduce(|mut acc, cs| {
Append::append(&mut acc, cs);
acc
}),
"should recover all changesets",
);
}
}
}
#[test]
fn write_after_short_read() {
let temp_dir = tempfile::tempdir().unwrap();
let changesets = (0..20)
.map(|n| TestChangeSet::from([format!("{}", n)]))
.collect::<Vec<_>>();
let last_changeset = TestChangeSet::from(["last".into()]);
for read_count in 0..changesets.len() {
let file_path = temp_dir.path().join(format!("{}.dat", read_count));
println!("Test file: {:?}", file_path);
// First, we create the file with all the changesets!
let mut db = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap();
for changeset in &changesets {
db.append_changeset(changeset).unwrap();
}
drop(db);
// We re-open the file and read `read_count` number of changesets.
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
let mut exp_aggregation = db
.iter_changesets()
.take(read_count)
.map(|r| r.expect("must read valid changeset"))
.fold(TestChangeSet::default(), |mut acc, v| {
Append::append(&mut acc, v);
acc
});
// We write after a short read.
db.append_changeset(&last_changeset)
.expect("last write must succeed");
Append::append(&mut exp_aggregation, last_changeset.clone());
drop(db);
// We open the file again and check whether aggregate changeset is expected.
let aggregation = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
.unwrap()
.aggregate_changesets()
.expect("must aggregate changesets")
.unwrap_or_default();
assert_eq!(aggregation, exp_aggregation);
}
}
}

View File

@@ -1,13 +0,0 @@
[package]
name = "bdk_hwi"
version = "0.3.0"
edition = "2021"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
description = "Utilities to use bdk with hardware wallets"
license = "MIT OR Apache-2.0"
readme = "README.md"
[dependencies]
bdk_wallet = { path = "../wallet", version = "1.0.0-alpha.13" }
hwi = { version = "0.9.0", features = [ "miniscript"] }

View File

@@ -1,3 +0,0 @@
# BDK HWI Signer
This crate contains `HWISigner`, an implementation of a `TransactionSigner` to be used with hardware wallets.

View File

@@ -1,41 +0,0 @@
//! HWI Signer
//!
//! This crate contains HWISigner, an implementation of a [`TransactionSigner`] to be
//! used with hardware wallets.
//! ```no_run
//! # use bdk_wallet::bitcoin::Network;
//! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_hwi::HWISigner;
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
//! #
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut devices = HWIClient::enumerate()?;
//! if devices.is_empty() {
//! panic!("No devices found!");
//! }
//! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//!
//! # let mut wallet = Wallet::new(
//! # "",
//! # "",
//! # Network::Testnet,
//! # )?;
//! #
//! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),
//! Arc::new(custom_signer),
//! );
//!
//! # Ok(())
//! # }
//! ```
//!
//! [`TransactionSigner`]: bdk_wallet::wallet::signer::TransactionSigner
mod signer;
pub use signer::*;

View File

@@ -1,94 +0,0 @@
use bdk_wallet::bitcoin::bip32::Fingerprint;
use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1};
use bdk_wallet::bitcoin::Psbt;
use hwi::error::Error;
use hwi::types::{HWIChain, HWIDevice};
use hwi::HWIClient;
use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
#[derive(Debug)]
/// Custom signer for Hardware Wallets
///
/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
pub struct HWISigner {
fingerprint: Fingerprint,
client: HWIClient,
}
impl HWISigner {
/// Create a instance from the specified device and chain
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
let client = HWIClient::get_client(device, false, chain)?;
Ok(HWISigner {
fingerprint: device.fingerprint,
client,
})
}
}
impl SignerCommon for HWISigner {
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
SignerId::Fingerprint(self.fingerprint)
}
}
impl TransactionSigner for HWISigner {
fn sign_transaction(
&self,
psbt: &mut Psbt,
_sign_options: &bdk_wallet::SignOptions,
_secp: &Secp256k1<All>,
) -> Result<(), SignerError> {
psbt.combine(
self.client
.sign_tx(psbt)
.map_err(|e| {
SignerError::External(format!("While signing with hardware wallet: {}", e))
})?
.psbt,
)
.expect("Failed to combine HW signed psbt with passed PSBT");
Ok(())
}
}
// TODO: re-enable this once we have the `get_funded_wallet` test util
// #[cfg(test)]
// mod tests {
// #[test]
// fn test_hardware_signer() {
// use std::sync::Arc;
//
// use bdk_wallet::tests::get_funded_wallet;
// use bdk_wallet::signer::SignerOrdering;
// use bdk_wallet::bitcoin::Network;
// use crate::HWISigner;
// use hwi::HWIClient;
//
// let mut devices = HWIClient::enumerate().unwrap();
// if devices.is_empty() {
// panic!("No devices found!");
// }
// let device = devices.remove(0).unwrap();
// let client = HWIClient::get_client(&device, true, Network::Regtest.into()).unwrap();
// let descriptors = client.get_descriptors::<String>(None).unwrap();
// let custom_signer = HWISigner::from_device(&device, Network::Regtest.into()).unwrap();
//
// let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]);
// wallet.add_signer(
// bdk_wallet::KeychainKind::External,
// SignerOrdering(200),
// Arc::new(custom_signer),
// );
//
// let addr = wallet.get_address(bdk_wallet::wallet::AddressIndex::LastUnused);
// let mut builder = wallet.build_tx();
// builder.drain_to(addr.script_pubkey()).drain_wallet();
// let (mut psbt, _) = builder.finish().unwrap();
//
// let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
// assert!(finalized);
// }
// }

View File

@@ -1,17 +0,0 @@
[package]
name = "bdk_sqlite"
version = "0.2.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_sqlite"
description = "A simple SQLite relational database client for persisting bdk_chain data."
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
authors = ["Bitcoin Dev Kit Developers"]
readme = "README.md"
[dependencies]
bdk_chain = { path = "../chain", version = "0.16.0", features = ["serde", "miniscript"] }
rusqlite = { version = "0.31.0", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@@ -1,8 +0,0 @@
# BDK SQLite
This is a simple [SQLite] relational database client for persisting [`bdk_chain`] changesets.
The main structure is `Store` which persists `CombinedChangeSet` data into a SQLite database file.
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
[SQLite]: https://www.sqlite.org/index.html

View File

@@ -1,69 +0,0 @@
-- schema version control
CREATE TABLE version
(
version INTEGER
) STRICT;
INSERT INTO version
VALUES (1);
-- network is the valid network for all other table data
CREATE TABLE network
(
name TEXT UNIQUE NOT NULL
) STRICT;
-- keychain is the json serialized keychain structure as JSONB,
-- descriptor is the complete descriptor string,
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
-- last revealed index is a u32
CREATE TABLE keychain
(
keychain BLOB PRIMARY KEY NOT NULL,
descriptor TEXT NOT NULL,
descriptor_id BLOB NOT NULL,
last_revealed INTEGER
) STRICT;
-- hash is block hash hex string,
-- block height is a u32,
CREATE TABLE block
(
hash TEXT PRIMARY KEY NOT NULL,
height INTEGER NOT NULL
) STRICT;
-- txid is transaction hash hex string (reversed)
-- whole_tx is a consensus encoded transaction,
-- last seen is a u64 unix epoch seconds
CREATE TABLE tx
(
txid TEXT PRIMARY KEY NOT NULL,
whole_tx BLOB,
last_seen INTEGER
) STRICT;
-- Outpoint txid hash hex string (reversed)
-- Outpoint vout
-- TxOut value as SATs
-- TxOut script consensus encoded
CREATE TABLE txout
(
txid TEXT NOT NULL,
vout INTEGER NOT NULL,
value INTEGER NOT NULL,
script BLOB NOT NULL,
PRIMARY KEY (txid, vout)
) STRICT;
-- join table between anchor and tx
-- block hash hex string
-- anchor is a json serialized Anchor structure as JSONB,
-- txid is transaction hash hex string (reversed)
CREATE TABLE anchor_tx
(
block_hash TEXT NOT NULL,
anchor BLOB NOT NULL,
txid TEXT NOT NULL REFERENCES tx (txid),
UNIQUE (anchor, txid),
FOREIGN KEY (block_hash) REFERENCES block(hash)
) STRICT;

View File

@@ -1,34 +0,0 @@
#![doc = include_str!("../README.md")]
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
#![cfg_attr(docsrs, feature(doc_cfg))]
mod schema;
mod store;
use bdk_chain::bitcoin::Network;
pub use rusqlite;
pub use store::Store;
/// Error that occurs while reading or writing change sets with the SQLite database.
#[derive(Debug)]
pub enum Error {
/// Invalid network, cannot change the one already stored in the database.
Network { expected: Network, given: Network },
/// SQLite error.
Sqlite(rusqlite::Error),
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Network { expected, given } => write!(
f,
"network error trying to read or write change set, expected {}, given {}",
expected, given
),
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e),
}
}
}
impl std::error::Error for Error {}

View File

@@ -1,96 +0,0 @@
use crate::Store;
use rusqlite::{named_params, Connection, Error};
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
const MIGRATIONS: &[&str] = &[SCHEMA_0];
/// Schema migration related functions.
impl<K, A> Store<K, A> {
/// Migrate sqlite db schema to latest version.
pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
let stmts = &MIGRATIONS
.iter()
.flat_map(|stmt| {
// remove comment lines
let s = stmt
.split('\n')
.filter(|l| !l.starts_with("--") && !l.is_empty())
.collect::<Vec<_>>()
.join(" ");
// split into statements
s.split(';')
// remove extra spaces
.map(|s| {
s.trim()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
})
// remove empty statements
.filter(|s| !s.is_empty())
.collect::<Vec<String>>();
let version = Self::get_schema_version(conn)?;
let stmts = &stmts[(version as usize)..];
// begin transaction, all migration statements and new schema version commit or rollback
let tx = conn.transaction()?;
// execute every statement and return `Some` new schema version
// if execution fails, return `Error::Rusqlite`
// if no statements executed returns `None`
let new_version = stmts
.iter()
.enumerate()
.map(|version_stmt| {
tx.execute(version_stmt.1.as_str(), [])
// map result value to next migration version
.map(|_| version_stmt.0 as i32 + version + 1)
})
.last()
.transpose()?;
// if `Some` new statement version, set new schema version
if let Some(version) = new_version {
Self::set_schema_version(&tx, version)?;
}
// commit transaction
tx.commit()?;
Ok(())
}
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
let statement = conn.prepare_cached("SELECT version FROM version");
match statement {
Err(Error::SqliteFailure(e, Some(msg))) => {
if msg == "no such table: version" {
Ok(0)
} else {
Err(Error::SqliteFailure(e, Some(msg)))
}
}
Ok(mut stmt) => {
let mut rows = stmt.query([])?;
match rows.next()? {
Some(row) => {
let version: i32 = row.get(0)?;
Ok(version)
}
None => Ok(0),
}
}
_ => Ok(0),
}
}
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
conn.execute(
"UPDATE version SET version=:version",
named_params! {":version": version},
)
}
}

View File

@@ -1,758 +0,0 @@
use bdk_chain::bitcoin::consensus::{deserialize, serialize};
use bdk_chain::bitcoin::hashes::Hash;
use bdk_chain::bitcoin::{Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut};
use bdk_chain::bitcoin::{BlockHash, Txid};
use bdk_chain::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
use rusqlite::{named_params, Connection};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use crate::Error;
use bdk_chain::CombinedChangeSet;
use bdk_chain::{
indexed_tx_graph, keychain, local_chain, tx_graph, Anchor, Append, DescriptorExt, DescriptorId,
};
/// Persists data in to a relational schema based [SQLite] database file.
///
/// The changesets loaded or stored represent changes to keychain and blockchain data.
///
/// [SQLite]: https://www.sqlite.org/index.html
pub struct Store<K, A> {
// A rusqlite connection to the SQLite database. Uses a Mutex for thread safety.
conn: Mutex<Connection>,
keychain_marker: PhantomData<K>,
anchor_marker: PhantomData<A>,
}
impl<K, A> Debug for Store<K, A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.conn, f)
}
}
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Creates a new store from a [`Connection`].
pub fn new(mut conn: Connection) -> Result<Self, rusqlite::Error> {
Self::migrate(&mut conn)?;
Ok(Self {
conn: Mutex::new(conn),
keychain_marker: Default::default(),
anchor_marker: Default::default(),
})
}
pub(crate) fn db_transaction(&mut self) -> Result<rusqlite::Transaction, Error> {
let connection = self.conn.get_mut().expect("unlocked connection mutex");
connection.transaction().map_err(Error::Sqlite)
}
}
/// Network table related functions.
impl<K, A> Store<K, A> {
/// Insert [`Network`] for which all other tables data is valid.
///
/// Error if trying to insert different network value.
fn insert_network(
current_network: &Option<Network>,
db_transaction: &rusqlite::Transaction,
network_changeset: &Option<Network>,
) -> Result<(), Error> {
if let Some(network) = network_changeset {
match current_network {
// if no network change do nothing
Some(current_network) if current_network == network => Ok(()),
// if new network not the same as current, error
Some(current_network) => Err(Error::Network {
expected: *current_network,
given: *network,
}),
// insert network if none exists
None => {
let insert_network_stmt = &mut db_transaction
.prepare_cached("INSERT INTO network (name) VALUES (:name)")
.expect("insert network statement");
let name = network.to_string();
insert_network_stmt
.execute(named_params! {":name": name })
.map_err(Error::Sqlite)?;
Ok(())
}
}
} else {
Ok(())
}
}
/// Select the valid [`Network`] for this database, or `None` if not set.
fn select_network(db_transaction: &rusqlite::Transaction) -> Result<Option<Network>, Error> {
let mut select_network_stmt = db_transaction
.prepare_cached("SELECT name FROM network WHERE rowid = 1")
.expect("select network statement");
let network = select_network_stmt
.query_row([], |row| {
let network = row.get_unwrap::<usize, String>(0);
let network = Network::from_str(network.as_str()).expect("valid network");
Ok(network)
})
.map_err(Error::Sqlite);
match network {
Ok(network) => Ok(Some(network)),
Err(Error::Sqlite(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
Err(e) => Err(e),
}
}
}
/// Block table related functions.
impl<K, A> Store<K, A> {
/// Insert or delete local chain blocks.
///
/// Error if trying to insert existing block hash.
fn insert_or_delete_blocks(
db_transaction: &rusqlite::Transaction,
chain_changeset: &local_chain::ChangeSet,
) -> Result<(), Error> {
for (height, hash) in chain_changeset.iter() {
match hash {
// add new hash at height
Some(hash) => {
let insert_block_stmt = &mut db_transaction
.prepare_cached("INSERT INTO block (hash, height) VALUES (:hash, :height)")
.expect("insert block statement");
let hash = hash.to_string();
insert_block_stmt
.execute(named_params! {":hash": hash, ":height": height })
.map_err(Error::Sqlite)?;
}
// delete block at height
None => {
let delete_block_stmt = &mut db_transaction
.prepare_cached("DELETE FROM block WHERE height IS :height")
.expect("delete block statement");
delete_block_stmt
.execute(named_params! {":height": height })
.map_err(Error::Sqlite)?;
}
}
}
Ok(())
}
/// Select all blocks.
fn select_blocks(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<u32, Option<BlockHash>>, Error> {
let mut select_blocks_stmt = db_transaction
.prepare_cached("SELECT height, hash FROM block")
.expect("select blocks statement");
let blocks = select_blocks_stmt
.query_map([], |row| {
let height = row.get_unwrap::<usize, u32>(0);
let hash = row.get_unwrap::<usize, String>(1);
let hash = Some(BlockHash::from_str(hash.as_str()).expect("block hash"));
Ok((height, hash))
})
.map_err(Error::Sqlite)?;
blocks
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Keychain table related functions.
///
/// The keychain objects are stored as [`JSONB`] data.
/// [`JSONB`]: https://sqlite.org/json1.html#jsonb
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + Send,
{
/// Insert keychain with descriptor and last active index.
///
/// If keychain exists only update last active index.
fn insert_keychains(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
let keychain_changeset = &tx_graph_changeset.indexer;
for (keychain, descriptor) in keychain_changeset.keychains_added.iter() {
let insert_keychain_stmt = &mut db_transaction
.prepare_cached("INSERT INTO keychain (keychain, descriptor, descriptor_id) VALUES (jsonb(:keychain), :descriptor, :descriptor_id)")
.expect("insert keychain statement");
let keychain_json = serde_json::to_string(keychain).expect("keychain json");
let descriptor_id = descriptor.descriptor_id().to_byte_array();
let descriptor = descriptor.to_string();
insert_keychain_stmt.execute(named_params! {":keychain": keychain_json, ":descriptor": descriptor, ":descriptor_id": descriptor_id })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Update descriptor last revealed index.
fn update_last_revealed(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
let keychain_changeset = &tx_graph_changeset.indexer;
for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() {
let update_last_revealed_stmt = &mut db_transaction
.prepare_cached(
"UPDATE keychain SET last_revealed = :last_revealed
WHERE descriptor_id = :descriptor_id",
)
.expect("update last revealed statement");
let descriptor_id = descriptor_id.to_byte_array();
update_last_revealed_stmt.execute(named_params! {":descriptor_id": descriptor_id, ":last_revealed": * last_revealed })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select keychains added.
fn select_keychains(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<K, Descriptor<DescriptorPublicKey>>, Error> {
let mut select_keychains_added_stmt = db_transaction
.prepare_cached("SELECT json(keychain), descriptor FROM keychain")
.expect("select keychains statement");
let keychains = select_keychains_added_stmt
.query_map([], |row| {
let keychain = row.get_unwrap::<usize, String>(0);
let keychain = serde_json::from_str::<K>(keychain.as_str()).expect("keychain");
let descriptor = row.get_unwrap::<usize, String>(1);
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
Ok((keychain, descriptor))
})
.map_err(Error::Sqlite)?;
keychains
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Select descriptor last revealed indexes.
fn select_last_revealed(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<DescriptorId, u32>, Error> {
let mut select_last_revealed_stmt = db_transaction
.prepare_cached(
"SELECT descriptor, last_revealed FROM keychain WHERE last_revealed IS NOT NULL",
)
.expect("select last revealed statement");
let last_revealed = select_last_revealed_stmt
.query_map([], |row| {
let descriptor = row.get_unwrap::<usize, String>(0);
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
let descriptor_id = descriptor.descriptor_id();
let last_revealed = row.get_unwrap::<usize, u32>(1);
Ok((descriptor_id, last_revealed))
})
.map_err(Error::Sqlite)?;
last_revealed
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Tx (transaction) and txout (transaction output) table related functions.
impl<K, A> Store<K, A> {
/// Insert transactions.
///
/// Error if trying to insert existing txid.
fn insert_txs(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
for tx in tx_graph_changeset.graph.txs.iter() {
let insert_tx_stmt = &mut db_transaction
.prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid")
.expect("insert or update tx whole_tx statement");
let txid = tx.compute_txid().to_string();
let whole_tx = serialize(&tx);
insert_tx_stmt
.execute(named_params! {":txid": txid, ":whole_tx": whole_tx })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all transactions.
fn select_txs(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeSet<Arc<Transaction>>, Error> {
let mut select_tx_stmt = db_transaction
.prepare_cached("SELECT whole_tx FROM tx WHERE whole_tx IS NOT NULL")
.expect("select tx statement");
let txs = select_tx_stmt
.query_map([], |row| {
let whole_tx = row.get_unwrap::<usize, Vec<u8>>(0);
let whole_tx: Transaction = deserialize(&whole_tx).expect("transaction");
Ok(Arc::new(whole_tx))
})
.map_err(Error::Sqlite)?;
txs.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Select all transactions with last_seen values.
fn select_last_seen(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<Txid, u64>, Error> {
// load tx last_seen
let mut select_last_seen_stmt = db_transaction
.prepare_cached("SELECT txid, last_seen FROM tx WHERE last_seen IS NOT NULL")
.expect("select tx last seen statement");
let last_seen = select_last_seen_stmt
.query_map([], |row| {
let txid = row.get_unwrap::<usize, String>(0);
let txid = Txid::from_str(&txid).expect("txid");
let last_seen = row.get_unwrap::<usize, u64>(1);
Ok((txid, last_seen))
})
.map_err(Error::Sqlite)?;
last_seen
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Insert txouts.
///
/// Error if trying to insert existing outpoint.
fn insert_txouts(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
for txout in tx_graph_changeset.graph.txouts.iter() {
let insert_txout_stmt = &mut db_transaction
.prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)")
.expect("insert txout statement");
let txid = txout.0.txid.to_string();
let vout = txout.0.vout;
let value = txout.1.value.to_sat();
let script = txout.1.script_pubkey.as_bytes();
insert_txout_stmt.execute(named_params! {":txid": txid, ":vout": vout, ":value": value, ":script": script })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all transaction outputs.
fn select_txouts(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<OutPoint, TxOut>, Error> {
// load tx outs
let mut select_txout_stmt = db_transaction
.prepare_cached("SELECT txid, vout, value, script FROM txout")
.expect("select txout statement");
let txouts = select_txout_stmt
.query_map([], |row| {
let txid = row.get_unwrap::<usize, String>(0);
let txid = Txid::from_str(&txid).expect("txid");
let vout = row.get_unwrap::<usize, u32>(1);
let outpoint = OutPoint::new(txid, vout);
let value = row.get_unwrap::<usize, u64>(2);
let script_pubkey = row.get_unwrap::<usize, Vec<u8>>(3);
let script_pubkey = ScriptBuf::from_bytes(script_pubkey);
let txout = TxOut {
value: Amount::from_sat(value),
script_pubkey,
};
Ok((outpoint, txout))
})
.map_err(Error::Sqlite)?;
txouts
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Update transaction last seen times.
fn update_last_seen(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() {
let insert_or_update_tx_stmt = &mut db_transaction
.prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid")
.expect("insert or update tx last_seen statement");
let txid = tx_last_seen.0.to_string();
let last_seen = *tx_last_seen.1;
insert_or_update_tx_stmt
.execute(named_params! {":txid": txid, ":last_seen": last_seen })
.map_err(Error::Sqlite)?;
}
Ok(())
}
}
/// Anchor table related functions.
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Insert anchors.
fn insert_anchors(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
// serde_json::to_string
for anchor in tx_graph_changeset.graph.anchors.iter() {
let insert_anchor_stmt = &mut db_transaction
.prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)")
.expect("insert anchor statement");
let block_hash = anchor.0.anchor_block().hash.to_string();
let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json");
let txid = anchor.1.to_string();
insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all anchors.
fn select_anchors(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeSet<(A, Txid)>, Error> {
// serde_json::from_str
let mut select_anchor_stmt = db_transaction
.prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx")
.expect("select anchor statement");
let anchors = select_anchor_stmt
.query_map([], |row| {
let hash = row.get_unwrap::<usize, String>(0);
let hash = BlockHash::from_str(hash.as_str()).expect("block hash");
let anchor = row.get_unwrap::<usize, String>(1);
let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor");
// double check anchor blob block hash matches
assert_eq!(hash, anchor.anchor_block().hash);
let txid = row.get_unwrap::<usize, String>(2);
let txid = Txid::from_str(&txid).expect("txid");
Ok((anchor, txid))
})
.map_err(Error::Sqlite)?;
anchors
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Functions to read and write all [`CombinedChangeSet`] data.
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Write the given `changeset` atomically.
pub fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
// no need to write anything if changeset is empty
if changeset.is_empty() {
return Ok(());
}
let db_transaction = self.db_transaction()?;
let network_changeset = &changeset.network;
let current_network = Self::select_network(&db_transaction)?;
Self::insert_network(&current_network, &db_transaction, network_changeset)?;
let chain_changeset = &changeset.chain;
Self::insert_or_delete_blocks(&db_transaction, chain_changeset)?;
let tx_graph_changeset = &changeset.indexed_tx_graph;
Self::insert_keychains(&db_transaction, tx_graph_changeset)?;
Self::update_last_revealed(&db_transaction, tx_graph_changeset)?;
Self::insert_txs(&db_transaction, tx_graph_changeset)?;
Self::insert_txouts(&db_transaction, tx_graph_changeset)?;
Self::insert_anchors(&db_transaction, tx_graph_changeset)?;
Self::update_last_seen(&db_transaction, tx_graph_changeset)?;
db_transaction.commit().map_err(Error::Sqlite)
}
/// Read the entire database and return the aggregate [`CombinedChangeSet`].
pub fn read(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Error> {
let db_transaction = self.db_transaction()?;
let network = Self::select_network(&db_transaction)?;
let chain = Self::select_blocks(&db_transaction)?;
let keychains_added = Self::select_keychains(&db_transaction)?;
let last_revealed = Self::select_last_revealed(&db_transaction)?;
let txs = Self::select_txs(&db_transaction)?;
let last_seen = Self::select_last_seen(&db_transaction)?;
let txouts = Self::select_txouts(&db_transaction)?;
let anchors = Self::select_anchors(&db_transaction)?;
let graph: tx_graph::ChangeSet<A> = tx_graph::ChangeSet {
txs,
txouts,
anchors,
last_seen,
};
let indexer: keychain::ChangeSet<K> = keychain::ChangeSet {
keychains_added,
last_revealed,
};
let indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>> =
indexed_tx_graph::ChangeSet { graph, indexer };
if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() {
Ok(None)
} else {
Ok(Some(CombinedChangeSet {
chain,
indexed_tx_graph,
network,
}))
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::store::Append;
use bdk_chain::bitcoin::consensus::encode::deserialize;
use bdk_chain::bitcoin::constants::genesis_block;
use bdk_chain::bitcoin::hashes::hex::FromHex;
use bdk_chain::bitcoin::transaction::Transaction;
use bdk_chain::bitcoin::Network::Testnet;
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
use bdk_chain::miniscript::Descriptor;
use bdk_chain::CombinedChangeSet;
use bdk_chain::{
indexed_tx_graph, keychain, tx_graph, BlockId, ConfirmationHeightAnchor,
ConfirmationTimeHeightAnchor, DescriptorExt,
};
use std::str::FromStr;
use std::sync::Arc;
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Serialize, Deserialize)]
enum Keychain {
External { account: u32, name: String },
Internal { account: u32, name: String },
}
#[test]
fn insert_and_load_aggregate_changesets_with_confirmation_time_height_anchor() {
let (test_changesets, agg_test_changesets) =
create_test_changesets(&|height, time, hash| ConfirmationTimeHeightAnchor {
confirmation_height: height,
confirmation_time: time,
anchor_block: (height, hash).into(),
});
let conn = Connection::open_in_memory().expect("in memory connection");
let mut store = Store::<Keychain, ConfirmationTimeHeightAnchor>::new(conn)
.expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write(changeset).expect("write changeset");
});
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
#[test]
fn insert_and_load_aggregate_changesets_with_confirmation_height_anchor() {
let (test_changesets, agg_test_changesets) =
create_test_changesets(&|height, _time, hash| ConfirmationHeightAnchor {
confirmation_height: height,
anchor_block: (height, hash).into(),
});
let conn = Connection::open_in_memory().expect("in memory connection");
let mut store = Store::<Keychain, ConfirmationHeightAnchor>::new(conn)
.expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write(changeset).expect("write changeset");
});
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
#[test]
fn insert_and_load_aggregate_changesets_with_blockid_anchor() {
let (test_changesets, agg_test_changesets) =
create_test_changesets(&|height, _time, hash| BlockId { height, hash });
let conn = Connection::open_in_memory().expect("in memory connection");
let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write(changeset).expect("write changeset");
});
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
fn create_test_changesets<A: Anchor + Copy>(
anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A,
) -> (
Vec<CombinedChangeSet<Keychain, A>>,
CombinedChangeSet<Keychain, A>,
) {
let secp = &secp256k1::Secp256k1::signing_only();
let network_changeset = Some(Testnet);
let block_hash_0: BlockHash = genesis_block(Testnet).block_hash();
let block_hash_1 =
BlockHash::from_str("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206")
.unwrap();
let block_hash_2 =
BlockHash::from_str("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820")
.unwrap();
let block_changeset = [
(0, Some(block_hash_0)),
(1, Some(block_hash_1)),
(2, Some(block_hash_2)),
]
.into();
let ext_keychain = Keychain::External {
account: 0,
name: "ext test".to_string(),
};
let (ext_desc, _ext_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*)").unwrap();
let ext_desc_id = ext_desc.descriptor_id();
let int_keychain = Keychain::Internal {
account: 0,
name: "int test".to_string(),
};
let (int_desc, _int_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/1/*)").unwrap();
let int_desc_id = int_desc.descriptor_id();
let tx0_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap();
let tx0: Arc<Transaction> = Arc::new(deserialize(tx0_hex.as_slice()).unwrap());
let tx1_hex = Vec::<u8>::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
let tx1: Arc<Transaction> = Arc::new(deserialize(tx1_hex.as_slice()).unwrap());
let tx2_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap();
let tx2: Arc<Transaction> = Arc::new(deserialize(tx2_hex.as_slice()).unwrap());
let outpoint0_0 = OutPoint::new(tx0.compute_txid(), 0);
let txout0_0 = tx0.output.first().unwrap().clone();
let outpoint1_0 = OutPoint::new(tx1.compute_txid(), 0);
let txout1_0 = tx1.output.first().unwrap().clone();
let anchor1 = anchor_fn(1, 1296667328, block_hash_1);
let anchor2 = anchor_fn(2, 1296688946, block_hash_2);
let tx_graph_changeset = tx_graph::ChangeSet::<A> {
txs: [tx0.clone(), tx1.clone()].into(),
txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(),
anchors: [(anchor1, tx0.compute_txid()), (anchor1, tx1.compute_txid())].into(),
last_seen: [
(tx0.compute_txid(), 1598918400),
(tx1.compute_txid(), 1598919121),
(tx2.compute_txid(), 1608919121),
]
.into(),
};
let keychain_changeset = keychain::ChangeSet {
keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(),
last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(),
};
let graph_changeset: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset,
indexer: keychain_changeset,
};
// test changesets to write to db
let mut changesets = Vec::new();
changesets.push(CombinedChangeSet {
chain: block_changeset,
indexed_tx_graph: graph_changeset,
network: network_changeset,
});
// create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen
let tx_graph_changeset2 = tx_graph::ChangeSet::<A> {
txs: [tx2.clone()].into(),
txouts: BTreeMap::default(),
anchors: BTreeSet::default(),
last_seen: [(tx2.compute_txid(), 1708919121)].into(),
};
let graph_changeset2: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset2,
indexer: keychain::ChangeSet::default(),
};
changesets.push(CombinedChangeSet {
chain: local_chain::ChangeSet::default(),
indexed_tx_graph: graph_changeset2,
network: None,
});
// create changeset that adds a new anchor2 for tx0 and tx1
let tx_graph_changeset3 = tx_graph::ChangeSet::<A> {
txs: BTreeSet::default(),
txouts: BTreeMap::default(),
anchors: [(anchor2, tx0.compute_txid()), (anchor2, tx1.compute_txid())].into(),
last_seen: BTreeMap::default(),
};
let graph_changeset3: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset3,
indexer: keychain::ChangeSet::default(),
};
changesets.push(CombinedChangeSet {
chain: local_chain::ChangeSet::default(),
indexed_tx_graph: graph_changeset3,
network: None,
});
// aggregated test changesets
let agg_test_changesets =
changesets
.iter()
.fold(CombinedChangeSet::<Keychain, A>::default(), |mut i, cs| {
i.append(cs.clone());
i
});
(changesets, agg_test_changesets)
}
}

View File

@@ -1,22 +0,0 @@
[package]
name = "bdk_testenv"
version = "0.6.0"
edition = "2021"
rust-version = "1.63"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_testenv"
description = "Testing framework for BDK chain sources."
license = "MIT OR Apache-2.0"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk_chain = { path = "../chain", version = "0.16", default-features = false }
electrsd = { version = "0.28.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
[features]
default = ["std"]
std = ["bdk_chain/std"]
serde = ["bdk_chain/serde"]

View File

@@ -1,6 +0,0 @@
# BDK TestEnv
This crate sets up a regtest environment with a single [`bitcoind`] node
connected to an [`electrs`] instance. This framework provides the infrastructure
for testing chain source crates, e.g., [`bdk_chain`], [`bdk_electrum`],
[`bdk_esplora`], etc.

View File

@@ -1,304 +0,0 @@
use bdk_chain::{
bitcoin::{
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
},
local_chain::CheckPoint,
BlockId,
};
use bitcoincore_rpc::{
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
RpcApi,
};
pub use electrsd;
pub use electrsd::bitcoind;
pub use electrsd::bitcoind::anyhow;
pub use electrsd::bitcoind::bitcoincore_rpc;
pub use electrsd::electrum_client;
use electrsd::electrum_client::ElectrumApi;
use std::time::Duration;
/// Struct for running a regtest environment with a single `bitcoind` node with an `electrs`
/// instance connected to it.
pub struct TestEnv {
pub bitcoind: electrsd::bitcoind::BitcoinD,
pub electrsd: electrsd::ElectrsD,
}
impl TestEnv {
/// Construct a new [`TestEnv`] instance with default configurations.
pub fn new() -> anyhow::Result<Self> {
let bitcoind = match std::env::var_os("BITCOIND_EXE") {
Some(bitcoind_path) => electrsd::bitcoind::BitcoinD::new(bitcoind_path),
None => {
let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path()
.expect(
"you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
);
electrsd::bitcoind::BitcoinD::with_conf(
bitcoind_exe,
&electrsd::bitcoind::Conf::default(),
)
}
}?;
let mut electrsd_conf = electrsd::Conf::default();
electrsd_conf.http_enabled = true;
let electrsd = match std::env::var_os("ELECTRS_EXE") {
Some(env_electrs_exe) => {
electrsd::ElectrsD::with_conf(env_electrs_exe, &bitcoind, &electrsd_conf)
}
None => {
let electrs_exe = electrsd::downloaded_exe_path()
.expect("electrs version feature must be enabled");
electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf)
}
}?;
Ok(Self { bitcoind, electrsd })
}
/// Exposes the [`ElectrumApi`] calls from the Electrum client.
pub fn electrum_client(&self) -> &impl ElectrumApi {
&self.electrsd.client
}
/// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
pub fn rpc_client(&self) -> &impl RpcApi {
&self.bitcoind.client
}
// Reset `electrsd` so that new blocks can be seen.
pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
let mut electrsd_conf = electrsd::Conf::default();
electrsd_conf.http_enabled = true;
let electrsd = match std::env::var_os("ELECTRS_EXE") {
Some(env_electrs_exe) => {
electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
}
None => {
let electrs_exe = electrsd::downloaded_exe_path()
.expect("electrs version feature must be enabled");
electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
}
}?;
self.electrsd = electrsd;
Ok(self)
}
/// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
/// `address`.
pub fn mine_blocks(
&self,
count: usize,
address: Option<Address>,
) -> anyhow::Result<Vec<BlockHash>> {
let coinbase_address = match address {
Some(address) => address,
None => self
.bitcoind
.client
.get_new_address(None, None)?
.assume_checked(),
};
let block_hashes = self
.bitcoind
.client
.generate_to_address(count as _, &coinbase_address)?;
Ok(block_hashes)
}
/// Mine a block that is guaranteed to be empty even with transactions in the mempool.
pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
let bt = self.bitcoind.client.get_block_template(
GetBlockTemplateModes::Template,
&[GetBlockTemplateRules::SegWit],
&[],
)?;
let txdata = vec![Transaction {
version: transaction::Version::ONE,
lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
input: vec![TxIn {
previous_output: bdk_chain::bitcoin::OutPoint::default(),
script_sig: ScriptBuf::builder()
.push_int(bt.height as _)
// randomn number so that re-mining creates unique block
.push_int(random())
.into_script(),
sequence: bdk_chain::bitcoin::Sequence::default(),
witness: bdk_chain::bitcoin::Witness::new(),
}],
output: vec![TxOut {
value: Amount::ZERO,
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
}],
}];
let bits: [u8; 4] = bt
.bits
.clone()
.try_into()
.expect("rpc provided us with invalid bits");
let mut block = Block {
header: Header {
version: bdk_chain::bitcoin::block::Version::default(),
prev_blockhash: bt.previous_block_hash,
merkle_root: TxMerkleNode::all_zeros(),
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
nonce: 0,
},
txdata,
};
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
for nonce in 0..=u32::MAX {
block.header.nonce = nonce;
if block.header.target().is_met_by(block.block_hash()) {
break;
}
}
self.bitcoind.client.submit_block(&block)?;
Ok((bt.height as usize, block.block_hash()))
}
/// This method waits for the Electrum notification indicating that a new block has been mined.
pub fn wait_until_electrum_sees_block(&self) -> anyhow::Result<()> {
self.electrsd.client.block_headers_subscribe()?;
let mut delay = Duration::from_millis(64);
loop {
self.electrsd.trigger()?;
self.electrsd.client.ping()?;
if self.electrsd.client.block_headers_pop()?.is_some() {
return Ok(());
}
if delay.as_millis() < 512 {
delay = delay.mul_f32(2.0);
}
std::thread::sleep(delay);
}
}
/// Invalidate a number of blocks of a given size `count`.
pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
let mut hash = self.bitcoind.client.get_best_block_hash()?;
for _ in 0..count {
let prev_hash = self
.bitcoind
.client
.get_block_info(&hash)?
.previousblockhash;
self.bitcoind.client.invalidate_block(&hash)?;
match prev_hash {
Some(prev_hash) => hash = prev_hash,
None => break,
}
}
Ok(())
}
/// Reorg a number of blocks of a given size `count`.
/// Refer to [`TestEnv::mine_empty_block`] for more information.
pub fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
let start_height = self.bitcoind.client.get_block_count()?;
self.invalidate_blocks(count)?;
let res = self.mine_blocks(count, None);
assert_eq!(
self.bitcoind.client.get_block_count()?,
start_height,
"reorg should not result in height change"
);
res
}
/// Reorg with a number of empty blocks of a given size `count`.
pub fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
let start_height = self.bitcoind.client.get_block_count()?;
self.invalidate_blocks(count)?;
let res = (0..count)
.map(|_| self.mine_empty_block())
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(
self.bitcoind.client.get_block_count()?,
start_height,
"reorg should not result in height change"
);
Ok(res)
}
/// Send a tx of a given `amount` to a given `address`.
pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
let txid = self
.bitcoind
.client
.send_to_address(address, amount, None, None, None, None, None, None)?;
Ok(txid)
}
/// Create a checkpoint linked list of all the blocks in the chain.
pub fn make_checkpoint_tip(&self) -> CheckPoint {
CheckPoint::from_block_ids((0_u32..).map_while(|height| {
self.bitcoind
.client
.get_block_hash(height as u64)
.ok()
.map(|hash| BlockId { height, hash })
}))
.expect("must craft tip")
}
/// Get the genesis hash of the blockchain.
pub fn genesis_hash(&self) -> anyhow::Result<BlockHash> {
let hash = self.bitcoind.client.get_block_hash(0)?;
Ok(hash)
}
}
#[cfg(test)]
mod test {
use crate::TestEnv;
use electrsd::bitcoind::{anyhow::Result, bitcoincore_rpc::RpcApi};
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
#[test]
fn test_reorg_is_detected_in_electrsd() -> Result<()> {
let env = TestEnv::new()?;
// Mine some blocks.
env.mine_blocks(101, None)?;
env.wait_until_electrum_sees_block()?;
let height = env.bitcoind.client.get_block_count()?;
let blocks = (0..=height)
.map(|i| env.bitcoind.client.get_block_hash(i))
.collect::<Result<Vec<_>, _>>()?;
// Perform reorg on six blocks.
env.reorg(6)?;
env.wait_until_electrum_sees_block()?;
let reorged_height = env.bitcoind.client.get_block_count()?;
let reorged_blocks = (0..=height)
.map(|i| env.bitcoind.client.get_block_hash(i))
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(height, reorged_height);
// Block hashes should not be equal on the six reorged blocks.
for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
match i <= height as usize - 6 {
true => assert_eq!(block, reorged_block),
false => assert_ne!(block, reorged_block),
}
}
Ok(())
}
}

View File

@@ -1,96 +0,0 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
extern crate bdk_wallet;
extern crate bitcoin;
extern crate miniscript;
extern crate serde_json;
use std::error::Error;
use std::str::FromStr;
use bitcoin::Network;
use miniscript::policy::Concrete;
use miniscript::Descriptor;
use bdk_wallet::{KeychainKind, Wallet};
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
/// can be derived from the policy.
///
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
fn main() -> Result<(), Box<dyn Error>> {
// We start with a miniscript policy string
let policy_str = "or(
10@thresh(4,
pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)
),1@and(
older(4209713),
thresh(2,
pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068)
)
)
)"
.replace(&[' ', '\n', '\t'][..], "");
println!("Compiling policy: \n{}", policy_str);
// Parse the string as a [`Concrete`] type miniscript policy.
let policy = Concrete::<String>::from_str(&policy_str)?;
// Create a `wsh` type descriptor from the policy.
// `policy.compile()` returns the resulting miniscript from the policy.
let descriptor = Descriptor::new_wsh(policy.compile()?)?.to_string();
println!("Compiled into Descriptor: \n{}", descriptor);
// Do the same for another (internal) keychain
let policy_str = "or(
10@thresh(2,
pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec)
),1@and(
pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),
older(12960)
)
)"
.replace(&[' ', '\n', '\t'][..], "");
println!("Compiling internal policy: \n{}", policy_str);
let policy = Concrete::<String>::from_str(&policy_str)?;
let internal_descriptor = Descriptor::new_wsh(policy.compile()?)?.to_string();
println!(
"Compiled into internal Descriptor: \n{}",
internal_descriptor
);
// Create a new wallet from descriptors
let mut wallet = Wallet::new(&descriptor, &internal_descriptor, Network::Regtest)?;
println!(
"First derived address from the descriptor: \n{}",
wallet.next_unused_address(KeychainKind::External),
);
// BDK also has it's own `Policy` structure to represent the spending condition in a more
// human readable json format.
let spending_policy = wallet.policies(KeychainKind::External)?;
println!(
"The BDK spending policy: \n{}",
serde_json::to_string_pretty(&spending_policy)?
);
Ok(())
}

View File

@@ -1,135 +0,0 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
use alloc::boxed::Box;
use core::convert::AsRef;
use bdk_chain::ConfirmationTime;
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
use bitcoin::psbt;
use serde::{Deserialize, Serialize};
/// Types of keychains
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum KeychainKind {
/// External keychain, used for deriving recipient addresses.
External = 0,
/// Internal keychain, used for deriving change addresses.
Internal = 1,
}
impl KeychainKind {
/// Return [`KeychainKind`] as a byte
pub fn as_byte(&self) -> u8 {
match self {
KeychainKind::External => b'e',
KeychainKind::Internal => b'i',
}
}
}
impl AsRef<[u8]> for KeychainKind {
fn as_ref(&self) -> &[u8] {
match self {
KeychainKind::External => b"e",
KeychainKind::Internal => b"i",
}
}
}
/// An unspent output owned by a [`Wallet`].
///
/// [`Wallet`]: crate::Wallet
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LocalOutput {
/// Reference to a transaction output
pub outpoint: OutPoint,
/// Transaction output
pub txout: TxOut,
/// Type of keychain
pub keychain: KeychainKind,
/// Whether this UTXO is spent or not
pub is_spent: bool,
/// The derivation index for the script pubkey in the wallet
pub derivation_index: u32,
/// The confirmation time for transaction containing this utxo
pub confirmation_time: ConfirmationTime,
}
/// A [`Utxo`] with its `satisfaction_weight`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeightedUtxo {
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
/// properly maintain the feerate when adding this input to a transaction during coin selection.
///
/// [weight units]: https://en.bitcoin.it/wiki/Weight_units
pub satisfaction_weight: usize,
/// The UTXO
pub utxo: Utxo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// An unspent transaction output (UTXO).
pub enum Utxo {
/// A UTXO owned by the local wallet.
Local(LocalOutput),
/// A UTXO owned by another wallet.
Foreign {
/// The location of the output.
outpoint: OutPoint,
/// The nSequence value to set for this input.
sequence: Option<Sequence>,
/// The information about the input we require to add it to a PSBT.
// Box it to stop the type being too big.
psbt_input: Box<psbt::Input>,
},
}
impl Utxo {
/// Get the location of the UTXO
pub fn outpoint(&self) -> OutPoint {
match &self {
Utxo::Local(local) => local.outpoint,
Utxo::Foreign { outpoint, .. } => *outpoint,
}
}
/// Get the `TxOut` of the UTXO
pub fn txout(&self) -> &TxOut {
match &self {
Utxo::Local(local) => &local.txout,
Utxo::Foreign {
outpoint,
psbt_input,
..
} => {
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
return &prev_tx.output[outpoint.vout as usize];
}
if let Some(txout) = &psbt_input.witness_utxo {
return txout;
}
unreachable!("Foreign UTXOs will always have one of these set")
}
}
}
/// Get the sequence number if an explicit sequence number has to be set for this input.
pub fn sequence(&self) -> Option<Sequence> {
match self {
Utxo::Local(_) => None,
Utxo::Foreign { sequence, .. } => *sequence,
}
}
}

View File

@@ -1,68 +0,0 @@
# Example RPC CLI
### Simple Regtest Test
1. Start local regtest bitcoind.
```
mkdir -p /tmp/regtest/bitcoind
bitcoind -regtest -server -fallbackfee=0.0002 -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -daemon
```
2. Create a test bitcoind wallet and set bitcoind env.
```
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -named createwallet wallet_name="test"
export RPC_URL=127.0.0.1:18443
export RPC_USER=<your-rpc-username>
export RPC_PASS=<your-rpc-password>
```
3. Get test bitcoind wallet info.
```
bitcoin-cli -rpcwallet="test" -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -regtest getwalletinfo
```
4. Get new test bitcoind wallet address.
```
BITCOIND_ADDRESS=$(bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getnewaddress)
echo $BITCOIND_ADDRESS
```
5. Generate 101 blocks with reward to test bitcoind wallet address.
```
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> generatetoaddress 101 $BITCOIND_ADDRESS
```
6. Verify test bitcoind wallet balance.
```
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getbalances
```
7. Set descriptor env and get address from RPC CLI wallet.
```
export DESCRIPTOR="wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)"
cargo run -- --network regtest address next
```
8. Send 5 test bitcoin to RPC CLI wallet.
```
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> sendtoaddress <address> 5
```
9. Sync blockchain with RPC CLI wallet.
```
cargo run -- --network regtest sync
<CNTRL-C to stop syncing>
```
10. Get RPC CLI wallet unconfirmed balances.
```
cargo run -- --network regtest balance
```
11. Generate 1 block with reward to test bitcoind wallet address.
```
bitcoin-cli -datadir=/tmp/regtest/bitcoind -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -regtest generatetoaddress 10 $BITCOIND_ADDRESS
```
12. Sync the blockchain with RPC CLI wallet.
```
cargo run -- --network regtest sync
<CNTRL-C to stop syncing>
```
13. Get RPC CLI wallet confirmed balances.
```
cargo run -- --network regtest balance
```
14. Get RPC CLI wallet transactions.
```
cargo run -- --network regtest txout list
```

View File

@@ -12,10 +12,10 @@ use bdk_bitcoind_rpc::{
Emitter,
};
use bdk_chain::{
bitcoin::{constants::genesis_block, Block, Transaction},
bitcoin::{Block, Transaction},
indexed_tx_graph, keychain,
local_chain::{self, LocalChain},
Append, ConfirmationTimeHeightAnchor, IndexedTxGraph,
local_chain::{self, CheckPoint, LocalChain},
ConfirmationTimeHeightAnchor, IndexedTxGraph,
};
use example_cli::{
anyhow,
@@ -42,7 +42,7 @@ type ChangeSet = (
#[derive(Debug)]
enum Emission {
Block(bdk_bitcoind_rpc::BlockEvent<Block>),
Block { height: u32, block: Block },
Mempool(Vec<(Transaction, u64)>),
Tip(u32),
}
@@ -64,6 +64,9 @@ struct RpcArgs {
/// Starting block height to fallback to if no point of agreement if found
#[clap(env = "FALLBACK_HEIGHT", long, default_value = "0")]
fallback_height: u32,
/// The unused-scripts lookahead will be kept at this size
#[clap(long, default_value = "10")]
lookahead: u32,
}
impl From<RpcArgs> for Auth {
@@ -110,22 +113,17 @@ enum RpcCommands {
fn main() -> anyhow::Result<()> {
let start = Instant::now();
let example_cli::Init {
args,
keymap,
index,
db,
init_changeset,
} = example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
let (args, keymap, index, db, init_changeset) =
example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
println!(
"[{:>10}s] loaded initial changeset from db",
start.elapsed().as_secs_f32()
);
let (init_chain_changeset, init_graph_changeset) = init_changeset;
let graph = Mutex::new({
let mut graph = IndexedTxGraph::new(index);
graph.apply_changeset(init_graph_changeset);
graph.apply_changeset(init_changeset.1);
graph
});
println!(
@@ -133,15 +131,7 @@ fn main() -> anyhow::Result<()> {
start.elapsed().as_secs_f32()
);
let chain = Mutex::new(if init_chain_changeset.is_empty() {
let genesis_hash = genesis_block(args.network).block_hash();
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
let mut db = db.lock().unwrap();
db.append_changeset(&(chain_changeset, Default::default()))?;
chain
} else {
LocalChain::from_changeset(init_chain_changeset)?
});
let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)?);
println!(
"[{:>10}s] loaded local chain from changeset",
start.elapsed().as_secs_f32()
@@ -150,7 +140,7 @@ fn main() -> anyhow::Result<()> {
let rpc_cmd = match args.command {
example_cli::Commands::ChainSpecific(rpc_cmd) => rpc_cmd,
general_cmd => {
return example_cli::handle_commands(
let res = example_cli::handle_commands(
&graph,
&db,
&chain,
@@ -163,42 +153,45 @@ fn main() -> anyhow::Result<()> {
},
general_cmd,
);
db.lock().unwrap().commit()?;
return res;
}
};
match rpc_cmd {
RpcCommands::Sync { rpc_args } => {
let RpcArgs {
fallback_height, ..
fallback_height,
lookahead,
..
} = rpc_args;
graph.lock().unwrap().index.set_lookahead_for_all(lookahead);
let chain_tip = chain.lock().unwrap().tip();
let rpc_client = rpc_args.new_client()?;
let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
let mut db_stage = ChangeSet::default();
let mut last_db_commit = Instant::now();
let mut last_print = Instant::now();
while let Some(emission) = emitter.next_block()? {
let height = emission.block_height();
while let Some((height, block)) = emitter.next_block()? {
let mut chain = chain.lock().unwrap();
let mut graph = graph.lock().unwrap();
let mut db = db.lock().unwrap();
let chain_update =
CheckPoint::from_header(&block.header, height).into_update(false);
let chain_changeset = chain
.apply_update(emission.checkpoint)
.apply_update(chain_update)
.expect("must always apply as we receive blocks in order from emitter");
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
db_stage.append((chain_changeset, graph_changeset));
let graph_changeset = graph.apply_block_relevant(block, height);
db.stage((chain_changeset, graph_changeset));
// commit staged db changes in intervals
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
let db = &mut *db.lock().unwrap();
last_db_commit = Instant::now();
if let Some(changeset) = db_stage.take() {
db.append_changeset(&changeset)?;
}
db.commit()?;
println!(
"[{:>10}s] committed to db (took {}s)",
start.elapsed().as_secs_f32(),
@@ -233,19 +226,20 @@ fn main() -> anyhow::Result<()> {
mempool_txs.iter().map(|(tx, time)| (tx, *time)),
);
{
let db = &mut *db.lock().unwrap();
db_stage.append((local_chain::ChangeSet::default(), graph_changeset));
if let Some(changeset) = db_stage.take() {
db.append_changeset(&changeset)?;
}
let mut db = db.lock().unwrap();
db.stage((local_chain::ChangeSet::default(), graph_changeset));
db.commit()?; // commit one last time
}
}
RpcCommands::Live { rpc_args } => {
let RpcArgs {
fallback_height, ..
fallback_height,
lookahead,
..
} = rpc_args;
let sigterm_flag = start_ctrlc_handler();
graph.lock().unwrap().index.set_lookahead_for_all(lookahead);
let last_cp = chain.lock().unwrap().tip();
println!(
@@ -262,8 +256,7 @@ fn main() -> anyhow::Result<()> {
loop {
match emitter.next_block()? {
Some(block_emission) => {
let height = block_emission.block_height();
Some((height, block)) => {
if sigterm_flag.load(Ordering::Acquire) {
break;
}
@@ -271,7 +264,7 @@ fn main() -> anyhow::Result<()> {
block_count = rpc_client.get_block_count()? as u32;
tx.send(Emission::Tip(block_count))?;
}
tx.send(Emission::Block(block_emission))?;
tx.send(Emission::Block { height, block })?;
}
None => {
if await_flag(&sigterm_flag, MEMPOOL_EMIT_DELAY) {
@@ -293,20 +286,20 @@ fn main() -> anyhow::Result<()> {
let mut tip_height = 0_u32;
let mut last_db_commit = Instant::now();
let mut last_print = Option::<Instant>::None;
let mut db_stage = ChangeSet::default();
for emission in rx {
let mut db = db.lock().unwrap();
let mut graph = graph.lock().unwrap();
let mut chain = chain.lock().unwrap();
let changeset = match emission {
Emission::Block(block_emission) => {
let height = block_emission.block_height();
Emission::Block { height, block } => {
let chain_update =
CheckPoint::from_header(&block.header, height).into_update(false);
let chain_changeset = chain
.apply_update(block_emission.checkpoint)
.apply_update(chain_update)
.expect("must always apply as we receive blocks in order from emitter");
let graph_changeset =
graph.apply_block_relevant(&block_emission.block, height);
let graph_changeset = graph.apply_block_relevant(block, height);
(chain_changeset, graph_changeset)
}
Emission::Mempool(mempool_txs) => {
@@ -320,14 +313,12 @@ fn main() -> anyhow::Result<()> {
continue;
}
};
db_stage.append(changeset);
db.stage(changeset);
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
let db = &mut *db.lock().unwrap();
last_db_commit = Instant::now();
if let Some(changeset) = db_stage.take() {
db.append_changeset(&changeset)?;
}
db.commit()?;
println!(
"[{:>10}s] committed to db (took {}s)",
start.elapsed().as_secs_f32(),

View File

@@ -3,15 +3,12 @@ use anyhow::Context;
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
use bdk_file_store::Store;
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::Debug;
use std::{cmp::Reverse, collections::BTreeMap, path::PathBuf, sync::Mutex, time::Duration};
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration};
use bdk_chain::{
bitcoin::{
absolute, address,
secp256k1::Secp256k1,
sighash::{Prevouts, SighashCache},
transaction, Address, Amount, Network, Sequence, Transaction, TxIn, TxOut,
absolute, address, psbt::Prevouts, secp256k1::Secp256k1, sighash::SighashCache, Address,
Network, Sequence, Transaction, TxIn, TxOut,
},
indexed_tx_graph::{self, IndexedTxGraph},
keychain::{self, KeychainTxOutIndex},
@@ -20,7 +17,7 @@ use bdk_chain::{
descriptor::{DescriptorSecretKey, KeyMap},
Descriptor, DescriptorPublicKey,
},
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut,
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend,
};
pub use bdk_file_store;
pub use clap;
@@ -32,6 +29,7 @@ pub type KeychainChangeSet<A> = (
local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>>,
);
pub type Database<'m, C> = Persist<Store<'m, C>, C>;
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
@@ -55,6 +53,7 @@ pub struct Args<CS: clap::Subcommand, S: clap::Args> {
pub command: Commands<CS, S>,
}
#[allow(clippy::almost_swapped)]
#[derive(Subcommand, Debug, Clone)]
pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
#[clap(flatten)]
@@ -74,9 +73,7 @@ pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
},
/// Send coins to an address.
Send {
/// Amount to send in satoshis
value: u64,
/// Destination address
address: Address<address::NetworkUnchecked>,
#[clap(short, default_value = "bnb")]
coin_select: CoinSelectionAlgo,
@@ -138,6 +135,7 @@ impl core::fmt::Display for CoinSelectionAlgo {
}
}
#[allow(clippy::almost_swapped)]
#[derive(Subcommand, Debug, Clone)]
pub enum AddressCmd {
/// Get the next unused address.
@@ -146,17 +144,14 @@ pub enum AddressCmd {
New,
/// List all addresses
List {
/// List change addresses
#[clap(long)]
change: bool,
},
/// Get last revealed address index for each keychain.
Index,
}
#[derive(Subcommand, Debug, Clone)]
pub enum TxOutCmd {
/// List transaction outputs.
List {
/// Return only spent outputs.
#[clap(short, long)]
@@ -190,20 +185,18 @@ impl core::fmt::Display for Keychain {
}
}
pub struct CreateTxChange {
pub index_changeset: keychain::ChangeSet<Keychain>,
pub change_keychain: Keychain,
pub index: u32,
}
#[allow(clippy::type_complexity)]
pub fn create_tx<A: Anchor, O: ChainOracle>(
graph: &mut KeychainTxGraph<A>,
chain: &O,
keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
cs_algorithm: CoinSelectionAlgo,
address: Address,
value: u64,
) -> anyhow::Result<(Transaction, Option<CreateTxChange>)>
) -> anyhow::Result<(
Transaction,
Option<(keychain::ChangeSet<Keychain>, (Keychain, u32))>,
)>
where
O::Error: std::error::Error + Send + Sync + 'static,
{
@@ -237,7 +230,7 @@ where
.iter()
.map(|(plan, utxo)| {
WeightedValue::new(
utxo.txout.value.to_sat(),
utxo.txout.value,
plan.expected_weight() as _,
plan.witness_version().is_some(),
)
@@ -245,33 +238,29 @@ where
.collect();
let mut outputs = vec![TxOut {
value: Amount::from_sat(value),
value,
script_pubkey: address.script_pubkey(),
}];
let internal_keychain = if graph
.index
.keychains()
.any(|(k, _)| *k == Keychain::Internal)
{
let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() {
Keychain::Internal
} else {
Keychain::External
};
let ((change_index, change_script), change_changeset) = graph
.index
.next_unused_spk(&internal_keychain)
.expect("Must exist");
let ((change_index, change_script), change_changeset) =
graph.index.next_unused_spk(&internal_keychain);
changeset.append(change_changeset);
// Clone to drop the immutable reference.
let change_script = change_script.into();
let change_plan = bdk_tmp_plan::plan_satisfaction(
&graph
.index
.keychains()
.find(|(k, _)| *k == &internal_keychain)
.get(&internal_keychain)
.expect("must exist")
.1
.at_derivation_index(change_index)
.expect("change_index can't be hardened"),
&assets,
@@ -279,7 +268,7 @@ where
.expect("failed to obtain change plan");
let mut change_output = TxOut {
value: Amount::ZERO,
value: 0,
script_pubkey: change_script,
};
@@ -288,9 +277,8 @@ where
min_drain_value: graph
.index
.keychains()
.find(|(k, _)| *k == &internal_keychain)
.get(&internal_keychain)
.expect("must exist")
.1
.dust_value(),
..CoinSelectorOpt::fund_outputs(
&outputs,
@@ -318,13 +306,13 @@ where
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
if let Some(drain_value) = selection_meta.drain_value {
change_output.value = Amount::from_sat(drain_value);
change_output.value = drain_value;
// if the selection tells us to use change and the change value is sufficient, we add it as an output
outputs.push(change_output)
}
let mut transaction = Transaction {
version: transaction::Version::TWO,
version: 0x02,
// because the temporary planning module does not support timelocks, we can use the chain
// tip as the `lock_time` for anti-fee-sniping purposes
lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
@@ -400,11 +388,7 @@ where
}
let change_info = if selection_meta.drain_value.is_some() {
Some(CreateTxChange {
index_changeset: changeset,
change_keychain: internal_keychain,
index: change_index,
})
Some((changeset, (internal_keychain, change_index)))
} else {
None
};
@@ -412,57 +396,50 @@ where
Ok((transaction, change_info))
}
// Alias the elements of `Result` of `planned_utxos`
pub type PlannedUtxo<K, A> = (bdk_tmp_plan::Plan<K>, FullTxOut<A>);
#[allow(clippy::type_complexity)]
pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>(
graph: &KeychainTxGraph<A>,
chain: &O,
assets: &bdk_tmp_plan::Assets<K>,
) -> Result<Vec<PlannedUtxo<K, A>>, O::Error> {
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
let chain_tip = chain.get_chain_tip()?;
let outpoints = graph.index.outpoints();
let outpoints = graph.index.outpoints().iter().cloned();
graph
.graph()
.try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned())
.filter_map(|r| -> Option<Result<PlannedUtxo<K, A>, _>> {
let (k, i, full_txo) = match r {
Err(err) => return Some(Err(err)),
Ok(((k, i), full_txo)) => (k, i, full_txo),
};
let desc = graph
.index
.keychains()
.find(|(keychain, _)| *keychain == &k)
.expect("keychain must exist")
.1
.at_derivation_index(i)
.expect("i can't be hardened");
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
Some(Ok((plan, full_txo)))
})
.try_filter_chain_unspents(chain, chain_tip, outpoints)
.filter_map(
#[allow(clippy::type_complexity)]
|r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<A>), _>> {
let (k, i, full_txo) = match r {
Err(err) => return Some(Err(err)),
Ok(((k, i), full_txo)) => (k, i, full_txo),
};
let desc = graph
.index
.keychains()
.get(&k)
.expect("keychain must exist")
.at_derivation_index(i)
.expect("i can't be hardened");
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
Some(Ok((plan, full_txo)))
},
)
.collect()
}
pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainOracle, C>(
graph: &Mutex<KeychainTxGraph<A>>,
db: &Mutex<Store<C>>,
db: &Mutex<Database<C>>,
chain: &Mutex<O>,
keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
network: Network,
broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
cmd: Commands<CS, S>,
) -> anyhow::Result<()>
where
O::Error: std::error::Error + Send + Sync + 'static,
C: Default
+ Append
+ DeserializeOwned
+ Serialize
+ From<KeychainChangeSet<A>>
+ Send
+ Sync
+ Debug,
C: Default + Append + DeserializeOwned + Serialize + From<KeychainChangeSet<A>>,
{
match cmd {
Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
@@ -478,15 +455,15 @@ where
_ => unreachable!("only these two variants exist in match arm"),
};
let ((spk_i, spk), index_changeset) =
spk_chooser(index, &Keychain::External).expect("Must exist");
let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
let db = &mut *db.lock().unwrap();
db.append_changeset(&C::from((
db.stage(C::from((
local_chain::ChangeSet::default(),
indexed_tx_graph::ChangeSet::from(index_changeset),
)))?;
let addr = Address::from_script(spk.as_script(), network)
.context("failed to derive address")?;
)));
db.commit()?;
let addr =
Address::from_script(spk, network).context("failed to derive address")?;
println!("[address @ {}] {}", spk_i, addr);
Ok(())
}
@@ -501,14 +478,14 @@ where
true => Keychain::Internal,
false => Keychain::External,
};
for (spk_i, spk) in index.revealed_keychain_spks(&target_keychain) {
for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
let address = Address::from_script(spk, network)
.expect("should always be able to derive address");
println!(
"{:?} {} used:{}",
spk_i,
address,
index.is_used(target_keychain, spk_i)
index.is_used(&(target_keychain, spk_i))
);
}
Ok(())
@@ -520,11 +497,11 @@ where
let chain = &*chain.lock().unwrap();
fn print_balances<'a>(
title_str: &'a str,
items: impl IntoIterator<Item = (&'a str, Amount)>,
items: impl IntoIterator<Item = (&'a str, u64)>,
) {
println!("{}:", title_str);
for (name, amount) in items.into_iter() {
println!(" {:<10} {:>12} sats", name, amount.to_sat())
println!(" {:<10} {:>12} sats", name, amount)
}
}
@@ -561,7 +538,7 @@ where
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
let chain_tip = chain.get_chain_tip()?;
let outpoints = graph.index.outpoints();
let outpoints = graph.index.outpoints().iter().cloned();
match txout_cmd {
TxOutCmd::List {
@@ -572,7 +549,7 @@ where
} => {
let txouts = graph
.graph()
.try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned())
.try_filter_chain_txouts(chain, chain_tip, outpoints)
.filter(|r| match r {
Ok((_, full_txo)) => match (spent, unspent) {
(true, false) => full_txo.spent_by.is_some(),
@@ -618,27 +595,23 @@ where
let (tx, change_info) =
create_tx(graph, chain, keymap, coin_select, address, value)?;
if let Some(CreateTxChange {
index_changeset,
change_keychain,
index,
}) = change_info
{
if let Some((index_changeset, (change_keychain, index))) = change_info {
// We must first persist to disk the fact that we've got a new address from the
// change keychain so future scans will find the tx we're about to broadcast.
// If we're unable to persist this, then we don't want to broadcast.
{
let db = &mut *db.lock().unwrap();
db.append_changeset(&C::from((
db.stage(C::from((
local_chain::ChangeSet::default(),
indexed_tx_graph::ChangeSet::from(index_changeset),
)))?;
)));
db.commit()?;
}
// We don't want other callers/threads to use this address while we're using it
// but we also don't want to scan the tx we just created because it's not
// technically in the blockchain yet.
graph.index.mark_used(change_keychain, index);
graph.index.mark_used(&change_keychain, index);
(tx, Some((change_keychain, index)))
} else {
(tx, None)
@@ -647,23 +620,23 @@ where
match (broadcast)(chain_specific, &transaction) {
Ok(_) => {
println!("Broadcasted Tx : {}", transaction.compute_txid());
println!("Broadcasted Tx : {}", transaction.txid());
let keychain_changeset = graph.lock().unwrap().insert_tx(transaction);
// We know the tx is at least unconfirmed now. Note if persisting here fails,
// it's not a big deal since we can always find it again form
// blockchain.
db.lock().unwrap().append_changeset(&C::from((
db.lock().unwrap().stage(C::from((
local_chain::ChangeSet::default(),
keychain_changeset,
)))?;
)));
Ok(())
}
Err(e) => {
if let Some((keychain, index)) = change_index {
// We failed to broadcast, so allow our change address to be used in the future
graph.lock().unwrap().index.unmark_used(keychain, index);
graph.lock().unwrap().index.unmark_used(&keychain, index);
}
Err(e)
}
@@ -672,38 +645,19 @@ where
}
}
/// The initial state returned by [`init`].
pub struct Init<CS: clap::Subcommand, S: clap::Args, C>
where
C: Default + Append + Serialize + DeserializeOwned + Debug + Send + Sync + 'static,
{
/// Arguments parsed by the cli.
pub args: Args<CS, S>,
/// Descriptor keymap.
pub keymap: KeyMap,
/// Keychain-txout index.
pub index: KeychainTxOutIndex<Keychain>,
/// Persistence backend.
pub db: Mutex<Store<C>>,
/// Initial changeset.
pub init_changeset: C,
}
/// Parses command line arguments and initializes all components, creating
/// a file store with the given parameters, or loading one if it exists.
pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
db_magic: &[u8],
#[allow(clippy::type_complexity)]
pub fn init<'m, CS: clap::Subcommand, S: clap::Args, C>(
db_magic: &'m [u8],
db_default_path: &str,
) -> anyhow::Result<Init<CS, S, C>>
) -> anyhow::Result<(
Args<CS, S>,
KeyMap,
KeychainTxOutIndex<Keychain>,
Mutex<Database<'m, C>>,
C,
)>
where
C: Default
+ Append
+ Serialize
+ DeserializeOwned
+ Debug
+ core::marker::Send
+ core::marker::Sync
+ 'static,
C: Default + Append + Serialize + DeserializeOwned,
{
if std::env::var("BDK_DB_PATH").is_err() {
std::env::set_var("BDK_DB_PATH", db_default_path);
@@ -713,11 +667,9 @@ where
let mut index = KeychainTxOutIndex::<Keychain>::default();
// TODO: descriptors are already stored in the db, so we shouldn't re-insert
// them in the index here. However, the keymap is not stored in the database.
let (descriptor, mut keymap) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
let _ = index.insert_descriptor(Keychain::External, descriptor)?;
index.add_keychain(Keychain::External, descriptor);
if let Some((internal_descriptor, internal_keymap)) = args
.change_descriptor
@@ -726,22 +678,22 @@ where
.transpose()?
{
keymap.extend(internal_keymap);
let _ = index.insert_descriptor(Keychain::Internal, internal_descriptor)?;
index.add_keychain(Keychain::Internal, internal_descriptor);
}
let mut db_backend = match Store::<C>::open_or_create_new(db_magic, &args.db_path) {
let mut db_backend = match Store::<'m, C>::open_or_create_new(db_magic, &args.db_path) {
Ok(db_backend) => db_backend,
// we cannot return `err` directly as it has lifetime `'m`
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
};
let init_changeset = db_backend.aggregate_changesets()?.unwrap_or_default();
let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
Ok(Init {
Ok((
args,
keymap,
index,
db: Mutex::new(db_backend),
Mutex::new(Database::new(db_backend)),
init_changeset,
})
))
}

View File

@@ -1,20 +1,19 @@
use std::{
collections::BTreeMap,
io::{self, Write},
sync::Mutex,
};
use bdk_chain::{
bitcoin::{constants::genesis_block, Address, Network, Txid},
collections::BTreeSet,
bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid},
indexed_tx_graph::{self, IndexedTxGraph},
keychain,
local_chain::{self, LocalChain},
spk_client::{FullScanRequest, SyncRequest},
Append, ConfirmationHeightAnchor,
};
use bdk_electrum::{
electrum_client::{self, Client, ElectrumApi},
BdkElectrumClient,
ElectrumExt, ElectrumUpdate,
};
use example_cli::{
anyhow::{self, Context},
@@ -104,15 +103,8 @@ type ChangeSet = (
);
fn main() -> anyhow::Result<()> {
let example_cli::Init {
args,
keymap,
index,
db,
init_changeset,
} = example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
let (disk_local_chain, disk_tx_graph) = init_changeset;
let (args, keymap, index, db, (disk_local_chain, disk_tx_graph)) =
example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
let graph = Mutex::new({
let mut graph = IndexedTxGraph::new(index);
@@ -120,17 +112,12 @@ fn main() -> anyhow::Result<()> {
graph
});
let chain = Mutex::new({
let genesis_hash = genesis_block(args.network).block_hash();
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
chain.apply_changeset(&disk_local_chain)?;
chain
});
let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)?);
let electrum_cmd = match &args.command {
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
general_cmd => {
return example_cli::handle_commands(
let res = example_cli::handle_commands(
&graph,
&db,
&chain,
@@ -143,63 +130,57 @@ fn main() -> anyhow::Result<()> {
},
general_cmd.clone(),
);
db.lock().unwrap().commit()?;
return res;
}
};
let client = BdkElectrumClient::new(electrum_cmd.electrum_args().client(args.network)?);
let client = electrum_cmd.electrum_args().client(args.network)?;
// Tell the electrum client about the txs we've already got locally so it doesn't re-download them
client.populate_tx_cache(&*graph.lock().unwrap());
let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() {
let response = match electrum_cmd.clone() {
ElectrumCommands::Scan {
stop_gap,
scan_options,
..
} => {
let request = {
let (keychain_spks, tip) = {
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
FullScanRequest::from_chain_tip(chain.tip())
.set_spks_for_keychain(
Keychain::External,
graph
.index
.unbounded_spk_iter(&Keychain::External)
.into_iter()
.flatten(),
)
.set_spks_for_keychain(
Keychain::Internal,
graph
.index
.unbounded_spk_iter(&Keychain::Internal)
.into_iter()
.flatten(),
)
.inspect_spks_for_all_keychains({
let mut once = BTreeSet::new();
move |k, spk_i, _| {
if once.insert(k) {
eprint!("\nScanning {}: {} ", k, spk_i);
} else {
eprint!("{} ", spk_i);
let keychain_spks = graph
.index
.spks_of_all_keychains()
.into_iter()
.map(|(keychain, iter)| {
let mut first = true;
let spk_iter = iter.inspect(move |(i, _)| {
if first {
eprint!("\nscanning {}: ", keychain);
first = false;
}
io::stdout().flush().expect("must flush");
}
eprint!("{} ", i);
let _ = io::stdout().flush();
});
(keychain, spk_iter)
})
.collect::<BTreeMap<_, _>>();
let tip = chain.tip();
(keychain_spks, tip)
};
let res = client
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
client
.scan(
tip,
keychain_spks,
core::iter::empty(),
core::iter::empty(),
stop_gap,
scan_options.batch_size,
)
.context("scanning the blockchain")?
.with_confirmation_height_anchor();
(
res.chain_update,
res.graph_update,
Some(res.last_active_indices),
)
}
ElectrumCommands::Sync {
mut unused_spks,
@@ -212,6 +193,7 @@ fn main() -> anyhow::Result<()> {
// Get a short lock on the tracker to get the spks we're interested in
let graph = graph.lock().unwrap();
let chain = chain.lock().unwrap();
let chain_tip = chain.tip().block_id();
if !(all_spks || unused_spks || utxos || unconfirmed) {
unused_spks = true;
@@ -221,136 +203,131 @@ fn main() -> anyhow::Result<()> {
unused_spks = false;
}
let chain_tip = chain.tip();
let mut request = SyncRequest::from_chain_tip(chain_tip.clone());
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::ScriptBuf>> =
Box::new(core::iter::empty());
if all_spks {
let all_spks = graph
.index
.revealed_spks(..)
.map(|(index, spk)| (index, spk.to_owned()))
.all_spks()
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>();
request = request.chain_spks(all_spks.into_iter().map(|((k, spk_i), spk)| {
eprint!("Scanning {}: {}", k, spk_i);
spk
}));
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
eprintln!("scanning {:?}", index);
script
})));
}
if unused_spks {
let unused_spks = graph
.index
.unused_spks()
.map(|(index, spk)| (index, spk.to_owned()))
.unused_spks(..)
.map(|(k, v)| (*k, ScriptBuf::from(v)))
.collect::<Vec<_>>();
request =
request.chain_spks(unused_spks.into_iter().map(move |((k, spk_i), spk)| {
eprint!(
"Checking if address {} {}:{} has been used",
Address::from_script(&spk, args.network).unwrap(),
k,
spk_i,
);
spk
}));
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
eprintln!(
"Checking if address {} {:?} has been used",
Address::from_script(&script, args.network).unwrap(),
index
);
script
})));
}
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
if utxos {
let init_outpoints = graph.index.outpoints();
let init_outpoints = graph.index.outpoints().iter().cloned();
let utxos = graph
.graph()
.filter_chain_unspents(
&*chain,
chain_tip.block_id(),
init_outpoints.iter().cloned(),
)
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
.map(|(_, utxo)| utxo)
.collect::<Vec<_>>();
request = request.chain_outpoints(utxos.into_iter().map(|utxo| {
eprint!(
"Checking if outpoint {} (value: {}) has been spent",
utxo.outpoint, utxo.txout.value
);
utxo.outpoint
}));
outpoints = Box::new(
utxos
.into_iter()
.inspect(|utxo| {
eprintln!(
"Checking if outpoint {} (value: {}) has been spent",
utxo.outpoint, utxo.txout.value
);
})
.map(|utxo| utxo.outpoint),
);
};
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
if unconfirmed {
let unconfirmed_txids = graph
.graph()
.list_chain_txs(&*chain, chain_tip.block_id())
.list_chain_txs(&*chain, chain_tip)
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
.map(|canonical_tx| canonical_tx.tx_node.txid)
.collect::<Vec<Txid>>();
request = request.chain_txids(
unconfirmed_txids
.into_iter()
.inspect(|txid| eprint!("Checking if {} is confirmed yet", txid)),
);
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
eprintln!("Checking if {} is confirmed yet", txid);
}));
}
let total_spks = request.spks.len();
let total_txids = request.txids.len();
let total_ops = request.outpoints.len();
request = request
.inspect_spks({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32)
}
})
.inspect_txids({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32)
}
})
.inspect_outpoints({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32)
}
});
let res = client
.sync(request, scan_options.batch_size, false)
.context("scanning the blockchain")?
.with_confirmation_height_anchor();
let tip = chain.tip();
// drop lock on graph and chain
drop((graph, chain));
(res.chain_update, res.graph_update, None)
let electrum_update = client
.scan_without_keychain(tip, spks, txids, outpoints, scan_options.batch_size)
.context("scanning the blockchain")?;
(electrum_update, BTreeMap::new())
}
};
let (
ElectrumUpdate {
chain_update,
relevant_txids,
},
keychain_update,
) = response;
let missing_txids = {
let graph = &*graph.lock().unwrap();
relevant_txids.missing_full_txs(graph.graph())
};
let now = std::time::UNIX_EPOCH
.elapsed()
.expect("must get time")
.as_secs();
let _ = graph_update.update_last_seen_unconfirmed(now);
let graph_update = relevant_txids.into_tx_graph(&client, Some(now), missing_txids)?;
let db_changeset = {
let mut chain = chain.lock().unwrap();
let mut graph = graph.lock().unwrap();
let chain_changeset = chain.apply_update(chain_update)?;
let chain = chain.apply_update(chain_update)?;
let mut indexed_tx_graph_changeset =
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
if let Some(keychain_update) = keychain_update {
let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update);
indexed_tx_graph_changeset.append(keychain_changeset.into());
}
indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
let indexed_tx_graph = {
let mut changeset =
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
let (_, indexer) = graph.index.reveal_to_target_multi(&keychain_update);
changeset.append(indexed_tx_graph::ChangeSet {
indexer,
..Default::default()
});
changeset.append(graph.apply_update(graph_update));
changeset
};
(chain_changeset, indexed_tx_graph_changeset)
(chain, indexed_tx_graph)
};
let mut db = db.lock().unwrap();
db.append_changeset(&db_changeset)?;
db.stage(db_changeset);
db.commit()?;
Ok(())
}

View File

@@ -1,15 +1,14 @@
use std::{
collections::BTreeSet,
collections::{BTreeMap, BTreeSet},
io::{self, Write},
sync::Mutex,
};
use bdk_chain::{
bitcoin::{constants::genesis_block, Address, Network, Txid},
bitcoin::{constants::genesis_block, Address, Network, OutPoint, ScriptBuf, Txid},
indexed_tx_graph::{self, IndexedTxGraph},
keychain,
local_chain::{self, LocalChain},
spk_client::{FullScanRequest, SyncRequest},
Append, ConfirmationTimeHeightAnchor,
};
@@ -61,7 +60,6 @@ enum EsploraCommands {
esplora_args: EsploraArgs,
},
}
impl EsploraCommands {
fn esplora_args(&self) -> EsploraArgs {
match self {
@@ -88,7 +86,7 @@ impl EsploraArgs {
_ => panic!("unsupported network"),
});
let client = esplora_client::Builder::new(esplora_url).build_blocking();
let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
Ok(client)
}
}
@@ -101,13 +99,8 @@ pub struct ScanOptions {
}
fn main() -> anyhow::Result<()> {
let example_cli::Init {
args,
keymap,
index,
db,
init_changeset,
} = example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
let (args, keymap, index, db, init_changeset) =
example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
let genesis_hash = genesis_block(args.network).block_hash();
@@ -132,7 +125,7 @@ fn main() -> anyhow::Result<()> {
example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
// These are general commands handled by example_cli. Execute the cmd and return.
general_cmd => {
return example_cli::handle_commands(
let res = example_cli::handle_commands(
&graph,
&db,
&chain,
@@ -147,70 +140,72 @@ fn main() -> anyhow::Result<()> {
},
general_cmd.clone(),
);
db.lock().unwrap().commit()?;
return res;
}
};
let client = esplora_cmd.esplora_args().client(args.network)?;
// Prepare the `IndexedTxGraph` and `LocalChain` updates based on whether we are scanning or
// syncing.
//
// Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing.
// Scanning: We are iterating through spks of all keychains and scanning for transactions for
// each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap`
// number of consecutive spks have no transaction history. A Scan is done in situations of
// wallet restoration. It is a special case. Applications should use "sync" style updates
// after an initial scan.
//
// Syncing: We only check for specified spks, utxos and txids to update their confirmation
// status or fetch missing transactions.
let (local_chain_changeset, indexed_tx_graph_changeset) = match &esplora_cmd {
let indexed_tx_graph_changeset = match &esplora_cmd {
EsploraCommands::Scan {
stop_gap,
scan_options,
..
} => {
let request = {
let chain_tip = chain.lock().expect("mutex must not be poisoned").tip();
let indexed_graph = &*graph.lock().expect("mutex must not be poisoned");
FullScanRequest::from_keychain_txout_index(chain_tip, &indexed_graph.index)
.inspect_spks_for_all_keychains({
let mut once = BTreeSet::<Keychain>::new();
move |keychain, spk_i, _| {
if once.insert(keychain) {
eprint!("\nscanning {}: ", keychain);
}
eprint!("{} ", spk_i);
// Flush early to ensure we print at every iteration.
let _ = io::stderr().flush();
let keychain_spks = graph
.lock()
.expect("mutex must not be poisoned")
.index
.spks_of_all_keychains()
.into_iter()
// This `map` is purely for logging.
.map(|(keychain, iter)| {
let mut first = true;
let spk_iter = iter.inspect(move |(i, _)| {
if first {
eprint!("\nscanning {}: ", keychain);
first = false;
}
})
};
eprint!("{} ", i);
// Flush early to ensure we print at every iteration.
let _ = io::stderr().flush();
});
(keychain, spk_iter)
})
.collect::<BTreeMap<_, _>>();
// The client scans keychain spks for transaction histories, stopping after `stop_gap`
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
// represents the last active spk derivation indices of keychains
// (`keychain_indices_update`).
let mut update = client
.full_scan(request, *stop_gap, scan_options.parallel_requests)
let (graph_update, last_active_indices) = client
.scan_txs_with_keychains(
keychain_spks,
core::iter::empty(),
core::iter::empty(),
*stop_gap,
scan_options.parallel_requests,
)
.context("scanning for transactions")?;
// We want to keep track of the latest time a transaction was seen unconfirmed.
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let _ = update.graph_update.update_last_seen_unconfirmed(now);
let mut graph = graph.lock().expect("mutex must not be poisoned");
let mut chain = chain.lock().expect("mutex must not be poisoned");
// Because we did a stop gap based scan we are likely to have some updates to our
// deriviation indices. Usually before a scan you are on a fresh wallet with no
// addresses derived so we need to derive up to last active addresses the scan found
// before adding the transactions.
(chain.apply_update(update.chain_update)?, {
let index_changeset = graph
.index
.reveal_to_target_multi(&update.last_active_indices);
let mut indexed_tx_graph_changeset = graph.apply_update(update.graph_update);
indexed_tx_graph_changeset.append(index_changeset.into());
indexed_tx_graph_changeset
})
let (_, index_changeset) = graph.index.reveal_to_target_multi(&last_active_indices);
let mut indexed_tx_graph_changeset = graph.apply_update(graph_update);
indexed_tx_graph_changeset.append(index_changeset.into());
indexed_tx_graph_changeset
}
EsploraCommands::Sync {
mut unused_spks,
@@ -231,67 +226,64 @@ fn main() -> anyhow::Result<()> {
unused_spks = false;
}
let local_tip = chain.lock().expect("mutex must not be poisoned").tip();
// Spks, outpoints and txids we want updates on will be accumulated here.
let mut request = SyncRequest::from_chain_tip(local_tip.clone());
let mut spks: Box<dyn Iterator<Item = ScriptBuf>> = Box::new(core::iter::empty());
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
// Get a short lock on the structures to get spks, utxos, and txs that we are interested
// in.
{
let graph = graph.lock().unwrap();
let chain = chain.lock().unwrap();
let chain_tip = chain.tip().block_id();
if *all_spks {
let all_spks = graph
.index
.revealed_spks(..)
.map(|((k, i), spk)| (k, i, spk.to_owned()))
.all_spks()
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>();
request = request.chain_spks(all_spks.into_iter().map(|(k, i, spk)| {
eprint!("scanning {}:{}", k, i);
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
eprintln!("scanning {:?}", index);
// Flush early to ensure we print at every iteration.
let _ = io::stderr().flush();
spk
}));
script
})));
}
if unused_spks {
let unused_spks = graph
.index
.unused_spks()
.map(|(index, spk)| (index, spk.to_owned()))
.unused_spks(..)
.map(|(k, v)| (*k, v.to_owned()))
.collect::<Vec<_>>();
request =
request.chain_spks(unused_spks.into_iter().map(move |((k, i), spk)| {
eprint!(
"Checking if address {} {}:{} has been used",
Address::from_script(&spk, args.network).unwrap(),
k,
i,
);
// Flush early to ensure we print at every iteration.
let _ = io::stderr().flush();
spk
}));
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
eprintln!(
"Checking if address {} {:?} has been used",
Address::from_script(&script, args.network).unwrap(),
index
);
// Flush early to ensure we print at every iteration.
let _ = io::stderr().flush();
script
})));
}
if utxos {
// We want to search for whether the UTXO is spent, and spent by which
// transaction. We provide the outpoint of the UTXO to
// `EsploraExt::update_tx_graph_without_keychain`.
let init_outpoints = graph.index.outpoints();
let init_outpoints = graph.index.outpoints().iter().cloned();
let utxos = graph
.graph()
.filter_chain_unspents(
&*chain,
local_tip.block_id(),
init_outpoints.iter().cloned(),
)
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
.map(|(_, utxo)| utxo)
.collect::<Vec<_>>();
request = request.chain_outpoints(
outpoints = Box::new(
utxos
.into_iter()
.inspect(|utxo| {
eprint!(
eprintln!(
"Checking if outpoint {} (value: {}) has been spent",
utxo.outpoint, utxo.txout.value
);
@@ -307,60 +299,56 @@ fn main() -> anyhow::Result<()> {
// `EsploraExt::update_tx_graph_without_keychain`.
let unconfirmed_txids = graph
.graph()
.list_chain_txs(&*chain, local_tip.block_id())
.list_chain_txs(&*chain, chain_tip)
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
.map(|canonical_tx| canonical_tx.tx_node.txid)
.collect::<Vec<Txid>>();
request = request.chain_txids(unconfirmed_txids.into_iter().inspect(|txid| {
eprint!("Checking if {} is confirmed yet", txid);
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
eprintln!("Checking if {} is confirmed yet", txid);
// Flush early to ensure we print at every iteration.
let _ = io::stderr().flush();
}));
}
}
let total_spks = request.spks.len();
let total_txids = request.txids.len();
let total_ops = request.outpoints.len();
request = request
.inspect_spks({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32)
}
})
.inspect_txids({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32)
}
})
.inspect_outpoints({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32)
}
});
let mut update = client.sync(request, scan_options.parallel_requests)?;
let graph_update =
client.scan_txs(spks, txids, outpoints, scan_options.parallel_requests)?;
// Update last seen unconfirmed
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let _ = update.graph_update.update_last_seen_unconfirmed(now);
(
chain.lock().unwrap().apply_update(update.chain_update)?,
graph.lock().unwrap().apply_update(update.graph_update),
)
graph.lock().unwrap().apply_update(graph_update)
}
};
println!();
// Now that we're done updating the `IndexedTxGraph`, it's time to update the `LocalChain`! We
// want the `LocalChain` to have data about all the anchors in the `TxGraph` - for this reason,
// we want retrieve the blocks at the heights of the newly added anchors that are missing from
// our view of the chain.
let (missing_block_heights, tip) = {
let chain = &*chain.lock().unwrap();
let missing_block_heights = indexed_tx_graph_changeset
.graph
.missing_heights_from(chain)
.collect::<BTreeSet<_>>();
let tip = chain.tip();
(missing_block_heights, tip)
};
println!("prev tip: {}", tip.height());
println!("missing block heights: {:?}", missing_block_heights);
// Here, we actually fetch the missing blocks and create a `local_chain::Update`.
let chain_changeset = {
let chain_update = client
.update_local_chain(tip, missing_block_heights)
.context("scanning for blocks")?;
println!("new tip: {}", chain_update.tip.height());
chain.lock().unwrap().apply_update(chain_update)?
};
// We persist the changes
let mut db = db.lock().unwrap();
db.append_changeset(&(local_chain_changeset, indexed_tx_graph_changeset))?;
db.stage((chain_changeset, indexed_tx_graph_changeset));
db.commit()?;
Ok(())
}

View File

@@ -4,7 +4,7 @@ version = "0.2.0"
edition = "2021"
[dependencies]
bdk_wallet = { path = "../../crates/wallet" }
bdk = { path = "../../crates/bdk" }
bdk_electrum = { path = "../../crates/electrum" }
bdk_file_store = { path = "../../crates/file_store" }
anyhow = "1"

Some files were not shown because too many files have changed in this diff Show More