Compare commits

..

55 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
Steve Myers
f741122ffb Merge bitcoindevkit/bdk#1158: doc(bdk): Clarify the absolute_fee docs
0ecc0280c0 doc(bdk): Clarify the absolute_fee, fee_rate docs (Daniela Brozzoni)

Pull request description:

  Fixes #1066

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  notmandatory:
    ACK 0ecc0280c0

Tree-SHA512: 152e48b86c21d4fad711abf76dd8fdc0e8955030331005c1ba6ff0c866c52870161f91ec740838f8238c5ad1c620e06212099308a55d130699cb18e4666e3f3f
2023-12-05 07:35:01 -06:00
Steve Myers
959b4f8172 Merge bitcoindevkit/bdk#1179: build(esplora): Add async-https-rustls flag to esplora client
6817ca9bcb ci: pin hyper-rustls version to 0.24.0 for 1.57 MSRV (thunderbiscuit)
4ee41dbc40 build(esplora): Add async-https-rustls flag to esplora client (thunderbiscuit)

Pull request description:

  ### Description
  The bdk_esplora crate currently doesn't expose the [`async-https-rustls` flag offered by the rust-esplora-client](ef1925e1ee/Cargo.toml (L44)) crate and instead requires users to build using the `default-tls` flag on reqwest, which uses the platform-specific openssl library when compiling. This creates complications for cross-compilation, notably for our Android builds that currently support 3 architectures (`arm64-v8a`, `armeabi-v7a`, and `x86_64`). In order to solve this we can either compile the openssl libraries for each of the platforms we want to support, or use the rustls-tls version of reqwest. The second options is much easier and requires less fiddling with the internals of the Android native development kit and cross-compilation rabbit holes.

  Before we merge this I want to make sure I understand the tradeoffs between the `native-tls` and the `rustls-tls` and confirm that there are not potential issues there, but from what I understand they should provide the same functionality/security, and because these are already available/exposed in reqwest and rust-esplora-client, I think this should be a fairly straightforward additional feature we offer.

  ### Changelog notice
  ```txt
  Added:
    - New async-https-rustls feature flag for the bdk_esplora crate, allowing to compile rust-esplora-client using rustls-tls instead of the default native-tls.
  ```

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  notmandatory:
    ACK 6817ca9bcb
  realeinherjar:
    ACK 6817ca9bcb
  danielabrozzoni:
    ACK 6817ca9bcb

Tree-SHA512: 1d417da7cf85e157d71f56442a06e817e8741822d7bff9089f7fbb70ff8b4854f1f52befbc348b849e9c98cae848b792d426cd5bf551e7a9089b15467d28efdd
2023-12-04 22:30:12 -06:00
志宇
55b680c194 Merge bitcoindevkit/bdk#1225: esplora: fix incorrect gap limit check in blocking client
43aed386bc esplora: also test the gap limit bounds in the async client (Antoine Poinsot)
cb713e5b8c esplora: also fix the gap limit check in the async client (Antoine Poinsot)
2c4e90a76f esplora: scan gap limit bounds testing (Antoine Poinsot)
18bd329617 esplora: fix incorrect gap limit check in blocking client (Antoine Poinsot)

Pull request description:

  The gap limit was checked such as if the last_index was not None but the last_active_index was, we'd consider having reached it. But the last_index is never None for this check. This effectively made it so the gap limit was always 1: if the first address isn't used last_active_index will be None and we'd return immediately.

  Fix this by avoiding error-prone Option comparisons and correctly checking we've reached the gap limit before breaking out of the loop.

ACKs for top commit:
  danielabrozzoni:
    ACK 43aed386bc
  evanlinjin:
    ACK 43aed386bc

Tree-SHA512: c6a172e0627380add56aec79e7fe36c0358e092b59f0bea8885d3524e65c10336291943efe6aeb5cc83f90c4f2146ed63215e56e08583d703b6ab57284487f03
2023-11-27 09:40:50 +08:00
Antoine Poinsot
43aed386bc esplora: also test the gap limit bounds in the async client 2023-11-24 12:21:16 +01:00
Antoine Poinsot
cb713e5b8c esplora: also fix the gap limit check in the async client 2023-11-24 12:21:14 +01:00
Antoine Poinsot
2c4e90a76f esplora: scan gap limit bounds testing 2023-11-24 12:21:13 +01:00
Antoine Poinsot
18bd329617 esplora: fix incorrect gap limit check in blocking client
The gap limit was checked such as if the last_index was not None but the
last_active_index was, we'd consider having reached it. But the
last_index is never None for this check. This effectively made it so the
gap limit was always 1: if the first address isn't used
last_active_index will be None and we'd return immediately.

Fix this by avoiding error-prone Option comparisons and correctly
checking we've reached the gap limit before breaking out of the loop.
2023-11-24 12:21:12 +01:00
Daniela Brozzoni
9e681b39fb Merge bitcoindevkit/bdk#1190: Add Wallet::list_output method
278210bb89 docs(bdk): clarify `insert_txout` docs (志宇)
6fb45d8a73 test(bdk): add `test_list_output` (志宇)
e803ee9010 feat(bdk): add `Wallet::list_output` method (志宇)
82632897aa refactor(bdk)!: rename `LocalUtxo` to `LocalOutput` (志宇)

Pull request description:

  Fixes #1184

  ### Description

  Introduce `Wallet::list_output` method that lists all outputs (both spent and unspent) in a consistent history.

  ### Changelog notice

  - Rename `LocalUtxo` to `LocalOutput`.
  - Add `Wallet::list_output` method.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature

ACKs for top commit:
  notmandatory:
    re-ACK 278210bb89
  danielabrozzoni:
    ACK 278210bb89

Tree-SHA512: 151af0e05e55d9c682271ef0c7a82e189531db963f65aa62c2ba0507f203dde39ab7561521c56e72c26830828e94ff96b7bd7e3f46082b23f79c5e0f89470136
2023-11-21 16:54:21 +01:00
thunderbiscuit
6817ca9bcb ci: pin hyper-rustls version to 0.24.0 for 1.57 MSRV 2023-11-20 20:02:31 -05:00
Steve Myers
73862be3ba Merge bitcoindevkit/bdk#1204: chore: remove bdk dependency on log and dev dependency on env_logger
02fa340896 chore: remove bdk dependency on log and dev dependency on env_logger (Steve Myers)

Pull request description:

  ### Description

  As suggested by @TheBlueMatt we shouldn't use the `log` crate because even though it is a rust-lang project is may depend on some other crates that aren't as well maintained. Since we were only use `log` in a few places and not it any other bdk workspace projects except the `bdk` package I removed it.

  ### Notes to the reviewers

  For consistency I also removed the `env_logger` from the dev dependencies since it was only being used in a few places in a couple examples and println! is adequate for examples.

  ### Changelog notice

  None

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

Top commit has no ACKs.

Tree-SHA512: beb9e4d465112090093590c45e3e5e2286a99a819312512eb3e5b40d0eade9740314bb8e45a2ad3fa0a4c86e32711d2ef8f966842815874b9315fd0b63bc7283
2023-11-20 15:37:34 -08:00
Steve Myers
02fa340896 chore: remove bdk dependency on log and dev dependency on env_logger 2023-11-20 15:18:29 -08:00
thunderbiscuit
4ee41dbc40 build(esplora): Add async-https-rustls flag to esplora client 2023-11-20 16:16:27 -05:00
志宇
278210bb89 docs(bdk): clarify insert_txout docs
Inserted txouts will not be shown in `list_unspent` or `list_output`.
2023-11-21 05:06:53 +08:00
志宇
6fb45d8a73 test(bdk): add test_list_output 2023-11-21 04:38:04 +08:00
志宇
e803ee9010 feat(bdk): add Wallet::list_output method 2023-11-21 04:38:03 +08:00
志宇
82632897aa refactor(bdk)!: rename LocalUtxo to LocalOutput 2023-11-21 04:35:00 +08:00
Steve Myers
46d39beb2c Merge bitcoindevkit/bdk#1028: Add CreateTxError and use as error type for TxBuilder::finish()
00ec19ef2d ci: fix MSRV pinning for rustls 0.21.9 (Steve Myers)
77f9977c02 feat(wallet): Add infallible Wallet get_address(), get_internal_address functions (Steve Myers)
9e7d99e3bf refactor(bdk)!: add context specific error types, remove top level error mod (Steve Myers)

Pull request description:

  ### Description

  To remove some places where there were `.expect("TODO")` I added a new `CreateTxError` type which is returned from `TxBuilder::finish()`. I also updated related tests and doc tests.

  Fixes https://github.com/bitcoindevkit/bdk/issues/996#issuecomment-1621036206

  Also added fallible `Wallet::try_get_address()` and `try_get_internal_address()`  to return `Result` with a possible `D:WriteError` when a PersistBackend is used. This should fix #996.

  I removed catch-all bdk::Error and replaced usages with new types and updated related functions, fixes #994.

  ### Notes to the reviewers

  ~~I didn't add all possible bdk::Error types that `Wallet::create_tx()` and `TxBuilder::finish()` functions might throw. It's probably not too much more work but will take a bit more research so I want to make sure this is the right general approach first.~~

  I added `anyhow` to the dev-dependencies so I could remove some `.expect()` lines from the docs tests and make the examples closer to what an end user should do.  I also used the `anyhow!()` macro to replace a few places that were using the `bdk::Error::Generic` in example code.

  I also moved the module level error.rs file to wallet/error.rs so no one would be tempted to make any new catch all errors and to make it clear that all the errors in it are wallet module related.

  ### Changelog notice

  Changed

  - Updated bdk module to use new context specific error types
    - wallet: MiniscriptPsbtError, CreateTxError, BuildFeeBumpError error enums
    - coin_selection: module Error enum
  - Renamed fallible Wallet address functions to try_get_address() and try_get_internal_address()

  Removed

  - Removed catch-all top level bdk::Error enum
  - Removed impl_error macro

  Added

  -  Added infallible Wallet get_address(), get_internal_address functions

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### Bugfixes:

  * [x] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

Top commit has no ACKs.

Tree-SHA512: a87c0856d71f9c945d12b6de6d368f49bd62d73886ac46ac83d00ddb81f2c38c5233ba053e40c76dea73ee7bfc19dac510eec5d7c9026ae50a2dc7308ac4786f
2023-11-16 12:36:49 -06:00
Steve Myers
00ec19ef2d ci: fix MSRV pinning for rustls 0.21.9 2023-11-16 11:56:09 -06:00
Steve Myers
77f9977c02 feat(wallet): Add infallible Wallet get_address(), get_internal_address functions
refactor!(wallet)!: rename fallible Wallet try_get_address(), try_get_internal_address functions
2023-11-16 11:18:11 -06:00
Steve Myers
9e7d99e3bf refactor(bdk)!: add context specific error types, remove top level error mod
refactor(bdk)!: remove impl_error macro
refactor(wallet)!: add MiniscriptPsbtError, CreateTxError, BuildFeeBumpError error enums
refactor(coin_selection)!: add module Error enum
test(bdk): use anyhow dev-dependency for all tests
2023-11-16 10:24:35 -06:00
Daniela Brozzoni
cc552c5f91 Merge bitcoindevkit/bdk#1220: chore: fix typos and remove unused speculos dockerfiles
27a63abd1e chore: typos fixed (Einherjar)

Pull request description:

  ### Description

  Fixes the typos and remove unused speculos dockerfiles that was done in #1165.
  Moving these changes into this PR to be merged first.
  Then, we can rebase #1165 and make it only related to CI and Nix.
  (Maybe do a big squash 😄)

  ## Note to Reviewers

  About the speculos stuff, we are not using them, removed in #793,
  more specifically in 3f5a78ae3b.

  ### Changelog notice

  - Fix typos in codebase and docs
  - Remove unused CI tests with hardware signer Dockerfiles

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    ACK 27a63abd1e

Tree-SHA512: a01101d0741e2b0e1d1254b5cae670c5a936bb0b89332c102feb57d58d2b9ae995ed4436068b0dc5fae73dbe22431c3143d6e04cdc12eab991bd726cfd2fbe25
2023-11-16 14:32:28 +01:00
Einherjar
27a63abd1e chore: typos fixed 2023-11-16 07:25:20 -06:00
Steve Myers
bc8d6a396b Merge bitcoindevkit/bdk#1178: LocalChain with hardwired genesis block
f1b112e8f9 docs(bitcoind_rpc): update docs for `Emitter::new` (志宇)
9a250baf62 chore: make clippy happy (志宇)
79b84bed0e feat(bdk): changeset's `Append` impl checks that network is consistent (志宇)
06a956ad20 feat!: change `load_from_persistence` to return an option (志宇)
c3265e2514 test(bdk): add tests for wallet constructor methods (志宇)
96f1d94e2c test(file_store): add construction method tests (志宇)
1886dc4fe7 chore(examples): use `Wallet::new_or_load` method where appropriate (志宇)
24994a3ed4 feat(file_store)!: have separate methods for creating and opening Store (志宇)
d294e2e318 feat(wallet)!: add `new_or_load` methods (志宇)
7c6cbc4d9f chore(file_store): rm empty test file (志宇)
6cf3963c6c feat(bdk)!: have separate methods for creating and loading `Wallet` (志宇)
7d5f31f6cc feat(chain, file_store): add `is_empty` method to `PersistBackend` trait (志宇)
5998a22819 feat!: `LocalChain` with hardwired genesis checkpoint (志宇)

Pull request description:

  closes #1079
  closes #1107

  ### Description

  Many methods of `TxGraph` require a `chain_tip: BlockId` input to use against a `ChainOracle` implementation. This is used to ask the `ChainOracle` implementation whether a certain block exists in the chain identified by the `chain_tip`. This guarantees that the `TxGraph` methods will return a consistent history of transactions.

  However, the `ChainOracle` trait's `get_chain_tip` method returns an option of `BlockId`. It becomes unclear what to do when `get_chain_tip` returns `None`.

  This PR changes the `ChainOracle::get_chain_tip` method to always return a `BlockId` (no `Option`). `LocalChain` now hardwires the genesis block in order to implement `ChainOracle`.

  `bdk::Wallet` and `bdk_file_store::Store` are changed to have separate constructor methods for initializing a fresh instance and recovering a previous instance from persistence.

  ### Notes to the reviewers

  ### Changelog notice

  - Changed `ChainOracle::get_chain_tip` method to return a `BlockId` instead of an `Option` of a `BlockId`.
  - Refactored `LocalChain` so that the genesis `BlockId` is hardwired. This way, the `ChainOracle::get_chain_tip` implementation can always return a tip.
  - Add `is_empty` method to `PersistBackend`. This returns true when there is no data in the persistence.
  - Changed `Wallet::new` to initialize a fresh wallet only.
  - Added `Wallet::load` to restore an instance of a wallet.
  - Replaced `Store::new` with separate methods to create/open the database file.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature

Top commit has no ACKs.

Tree-SHA512: 31b75fb53cc451f1fce7e409f1112c43973db7e8b5b31640e01e5b52089683b60320565427d6ea0478ff4c8680dbdb9272fdab08aef69d30f257da52e731e1a3
2023-11-15 18:07:47 -06:00
志宇
f1b112e8f9 docs(bitcoind_rpc): update docs for Emitter::new 2023-11-16 07:23:56 +08:00
志宇
9a250baf62 chore: make clippy happy 2023-11-16 07:17:16 +08:00
志宇
79b84bed0e feat(bdk): changeset's Append impl checks that network is consistent 2023-11-16 07:07:49 +08:00
志宇
06a956ad20 feat!: change load_from_persistence to return an option
`PersistBackend::is_empty` is removed. Instead, `load_from_persistence`
returns an option of the changeset. `None` means persistence is empty.
This is a better API than a separate method. We can now differentiate
between a persisted single changeset and nothing persisted.

`Store::aggregate_changeset` is refactored to return a `Result` instead
of a tuple. A new error type (`AggregateChangesetsError`) is introduced
to include the partially-aggregated changeset in the error. This is a
more idiomatic API.
2023-11-16 07:07:49 +08:00
志宇
c3265e2514 test(bdk): add tests for wallet constructor methods 2023-11-16 07:07:49 +08:00
志宇
96f1d94e2c test(file_store): add construction method tests 2023-11-16 07:07:48 +08:00
志宇
1886dc4fe7 chore(examples): use Wallet::new_or_load method where appropriate 2023-11-16 07:07:48 +08:00
志宇
24994a3ed4 feat(file_store)!: have separate methods for creating and opening Store 2023-11-16 07:07:48 +08:00
志宇
d294e2e318 feat(wallet)!: add new_or_load methods
These methods try to load wallet from persistence and initializes the
wallet instead if non-existant.

An internal helper method `create_signers` is added to reuse code.
Documentation is also improved.
2023-11-16 07:07:48 +08:00
志宇
7c6cbc4d9f chore(file_store): rm empty test file 2023-11-16 07:04:08 +08:00
志宇
6cf3963c6c feat(bdk)!: have separate methods for creating and loading Wallet
`Wallet::new` now creates a new wallet. `Wallet::load` loads an existing
wallet. The network type is now recoverable from persistence. Error
types have been simplified.
2023-11-16 07:04:08 +08:00
志宇
7d5f31f6cc feat(chain, file_store): add is_empty method to PersistBackend trait 2023-11-16 07:01:56 +08:00
志宇
5998a22819 feat!: LocalChain with hardwired genesis checkpoint
This ensures that `LocalChain` will always have a tip. The `ChainOracle`
trait's `get_chain_tip` method no longer needs to return an option.
2023-11-16 06:41:18 +08:00
Steve Myers
d6a0cf0795 Merge commit 'refs/pull/1121/head' of github.com:bitcoindevkit/bdk 2023-11-14 11:51:48 -06:00
Einherjar
6e27e66738 feat: add dependabot
Fixes #1118.
Adds `dependabot.yml` to `.github/` to check for `"github-action"`
updates on a `"weekly"` basis.
This does not touch Rust code or Cargo workflows.

It will  create PRs and we would need to approve them
(they would be subject to the same merge policy)
to instantiate the proposed dependabots into `master`.
2023-11-14 11:26:52 -06:00
Daniela Brozzoni
f382fa9230 Merge bitcoindevkit/bdk#1202: fix(chain): filter coinbase tx not in best chain
991cb77b6f fix(chain): filter coinbase tx not in best chain (Wei Chen)

Pull request description:

  ### Description

  Fixes #1144.
  Coinbase transactions cannot exist in the mempool and be unconfirmed. `TxGraph::try_get_chain_position` should always return `None` for coinbase transactions not anchored in best chain.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  notmandatory:
    ACK 991cb77b6f
  danielabrozzoni:
    ACK 991cb77b6f

Tree-SHA512: 9e06d8404708eee050c96807a876a470303f4983666c82c56c17d97c2d4b72784e75271279fd393c53a6a967a352aea1ef2762da71ac4bb58f7a0c2f05354948
2023-11-14 14:58:46 +01:00
Daniela Brozzoni
e71770f93e Merge bitcoindevkit/bdk#1206: chore: rename ConfirmationTimeAnchor to ConfirmationTimeHeightAnchor
0112c67b60 chore: rename `ConfirmationTimeAnchor` to `ConfirmationTimeHeightAnchor` (Wei Chen)

Pull request description:

  ### Description

  Closes #1187.
  An `Anchor` implementation that records both height and time should have both attributes included in the name.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  notmandatory:
    ACK 0112c67b60

Tree-SHA512: 024cbc83c8aca36baeaf2ce36979d62f235ffea7702e7ac8d4e7669cbc1730f7e1469ba78bf3da6c5a14abedbf1a9e832bdd66fdaa154ad2bef29cb187e1c504
2023-11-14 14:52:30 +01:00
Daniela Brozzoni
298f6cb1e8 ci: Pin jobserver after cc
Since now only cc depends on jobserver, it should be pinned after we pin cc
2023-11-13 19:06:54 +01:00
Daniela Brozzoni
3fdab87ee7 Merge bitcoindevkit/bdk#1200: fix(bdk): Check if we're using the correct internal key before signing
e553231eae fix(bdk): Check if we're using the correct... ...internal key before signing (Daniela Brozzoni)

Pull request description:

  ### Description

  Fixes #1142

  We would previously sign with whatever x_only_pubkey we had in hand, without first checking if it was the right key or not. This effectively meant that adding multiple taproot PrivateKey signers would produce unbroadcastable transactions.

  ### Changelog notice

  - Fix a bug related to taproot signing with internal keys. We would previously sign with the first private key we had, without checking if it was the correct internal key or not.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  evanlinjin:
    ACK e553231eae

Tree-SHA512: c4abbcd27935b8ce80a70b6e0843507866e3d075939f0b01504c090929ed96b4b9c6fee599f701e69960a6c86175682cc6d7f8cc4c3fb1d08a74b7563f8ca145
2023-11-13 10:09:02 +01:00
Daniela Brozzoni
855c61a6ab Merge bitcoindevkit/bdk#1145: fix(electrum): fixed chain sync issue
1010efd8d6 fix(electrum): fixed chain sync issue (Wei Chen)

Pull request description:

  ### Description

  This may or may not fix #1125.
  Fixed what appeared to be a logic error in `construct_update_tip` in `electrum_ext.rs` that caused the local chain tip to always be a block behind the newest confirmed block.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    ACK 1010efd8d6 - although I've been able to reproduce the issue in #1125, I'm convinced that this PR fixes at least a bug, as demonstrated in #1171 (yet to be reviewed and merged).

Tree-SHA512: 92790e9072d17be74d2cd24bec3503e1ad5d97f728ee81490eeb09ac3f8d4a3a7e8d9628e943bc801246d5bfd345152c11d5dbe25246f5a57b3118727d3ae315
2023-11-13 09:36:10 +01:00
Wei Chen
0112c67b60 chore: rename ConfirmationTimeAnchor to ConfirmationTimeHeightAnchor
An `Anchor` implementation that records both height and time should have
both attributes included in the name.
2023-11-12 21:31:47 +08:00
Wei Chen
1010efd8d6 fix(electrum): fixed chain sync issue
Fixed a logic error in `construct_update_tip` in `electrum_ext.rs` that caused
the local chain tip to always be a block behind the newest confirmed block.
2023-11-11 20:52:37 +08:00
Wei Chen
991cb77b6f fix(chain): filter coinbase tx not in best chain
Coinbase transactions cannot exist in the mempool and be unconfirmed.
`TxGraph::try_get_chain_position` should always return `None` for coinbase
transactions not anchored in best chain.
2023-11-11 02:55:58 +08:00
Daniela Brozzoni
e553231eae fix(bdk): Check if we're using the correct...
...internal key before signing

Fixes #1142

We would previously sign with whatever x_only_pubkey we had in hand,
without first checking if it was the right key or not. This effectively
meant that adding multiple taproot PrivateKey signers would produce
unbroadcastable transactions.
2023-11-10 18:25:46 +01:00
Daniela Brozzoni
0a7b60f0f7 Merge bitcoindevkit/bdk#1109: Further improve unconfirmed tx conflict resolution
afbf83c8b0 chain(fix): conflict resolution for txs with same last_seen (Wei Chen)

Pull request description:

  ### Description

  Fixes #1102. If a conflicting tx has the same `last_seen`, then we check lexicographical sorting of txids.

  ### Notes to the reviewers

  The tests for this fix exist in the `TxTemplate` structure in #1064 which may need to be merged first.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  danielabrozzoni:
    ACK afbf83c8b0

Tree-SHA512: 91b8fbff305b715247501b861ab7ea9e9d9ef99248b05d14e01aacf7e64ad7826f35773e8998cf421dbd04f663714026084c6e817ac5365bce4844c8ea3b7e3f
2023-11-09 10:07:18 +01:00
Daniela Brozzoni
0ecc0280c0 doc(bdk): Clarify the absolute_fee, fee_rate docs
Fixes #1066
2023-11-09 09:00:49 +01:00
Wei Chen
afbf83c8b0 chain(fix): conflict resolution for txs with same last_seen
The tx conflict `Scenario` test for unconfirmed txs with the same
last_seen has been amended for its corresponding conflict
resolution bug fix.
2023-11-09 05:46:09 +08:00
志宇
2f2f138595 Merge bitcoindevkit/bdk#1182: chore: fix MSRV for flate2
95250fc44e ci(chain): downgrade hashbrown dependency to 0.9.1 to fix ahash related MSRV issue (Steve Myers)
f17df1e133 ci: more fixed dependencies for MSRV 1.57.0 (Vladimir Fomene)

Pull request description:

ACKs for top commit:
  notmandatory:
    ACK 95250fc44e
  realeinherjar:
    ACK 95250fc44e
  evanlinjin:
    ACK 95250fc44e

Tree-SHA512: ad090713d97cf778598bb4acee200d7acbc987fe74964f171cc9939149251bddce9474b750371df26d3f6548780f4db2c17b3fe2cf5f6d627c808d682c929918
2023-11-06 11:15:16 +08:00
Steve Myers
95250fc44e ci(chain): downgrade hashbrown dependency to 0.9.1 to fix ahash related MSRV issue 2023-11-03 21:51:04 -05:00
Vladimir Fomene
f17df1e133 ci: more fixed dependencies for MSRV 1.57.0 2023-11-03 21:50:11 -05:00
Steve Myers
3569acca0b chore: add meta data to bitcoind_rpc crate 2023-10-12 09:33:34 -05:00
61 changed files with 2375 additions and 1141 deletions

8
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
# Set update schedule for GitHub Actions
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"

View File

@@ -27,12 +27,13 @@ jobs:
uses: Swatinem/rust-cache@v2.2.1 uses: Swatinem/rust-cache@v2.2.1
- name: Install grcov - name: Install grcov
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
# TODO: re-enable the hwi tests
- name: Build simulator image - name: Build simulator image
run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger
- name: Run simulator image - name: Run simulator image
run: docker run --name simulator --network=host hwi/ledger_emulator & run: docker run --name simulator --network=host hwi/ledger_emulator &
- name: Install Python - name: Install Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.9' python-version: '3.9'
- name: Install python dependencies - name: Install python dependencies

View File

@@ -32,20 +32,23 @@ jobs:
run: | run: |
cargo update -p log --precise "0.4.18" cargo update -p log --precise "0.4.18"
cargo update -p tempfile --precise "3.6.0" cargo update -p tempfile --precise "3.6.0"
cargo update -p rustls:0.21.7 --precise "0.21.1"
cargo update -p rustls:0.20.9 --precise "0.20.8"
cargo update -p tokio:1.33.0 --precise "1.29.1"
cargo update -p tokio-util --precise "0.7.8"
cargo update -p flate2:1.0.27 --precise "1.0.26"
cargo update -p reqwest --precise "0.11.18" 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 h2 --precise "0.3.20"
cargo update -p rustls-webpki:0.100.3 --precise "0.100.1" cargo update -p rustls-webpki:0.100.3 --precise "0.100.1"
cargo update -p rustls-webpki:0.101.6 --precise "0.101.1" cargo update -p rustls-webpki:0.101.7 --precise "0.101.1"
cargo update -p zip:0.6.6 --precise "0.6.2" cargo update -p zip --precise "0.6.2"
cargo update -p time --precise "0.3.13" cargo update -p time --precise "0.3.13"
cargo update -p cc --precise "1.0.81"
cargo update -p byteorder --precise "1.4.3" cargo update -p byteorder --precise "1.4.3"
cargo update -p webpki --precise "0.22.2" 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" cargo update -p jobserver --precise "0.1.26"
- name: Build - name: Build
run: cargo build ${{ matrix.features }} run: cargo build ${{ matrix.features }}

View File

@@ -517,7 +517,7 @@ final transaction is created by calling `finish` on the builder.
- Default to SIGHASH_ALL if not specified - Default to SIGHASH_ALL if not specified
- Replace ChangeSpendPolicy::filter_utxos with a predicate - Replace ChangeSpendPolicy::filter_utxos with a predicate
- Make 'unspendable' into a HashSet - Make 'unspendable' into a HashSet
- Stop implicitly enforcing manaul selection by .add_utxo - Stop implicitly enforcing manual selection by .add_utxo
- Rename DumbCS to LargestFirstCoinSelection - Rename DumbCS to LargestFirstCoinSelection
- Rename must_use_utxos to required_utxos - Rename must_use_utxos to required_utxos
- Rename may_use_utxos to optional_uxtos - Rename may_use_utxos to optional_uxtos

View File

@@ -69,34 +69,40 @@ To build with the MSRV you will need to pin dependencies as follows:
cargo update -p log --precise "0.4.18" cargo update -p log --precise "0.4.18"
# tempfile 3.7.0 has MSRV 1.63.0+ # tempfile 3.7.0 has MSRV 1.63.0+
cargo update -p tempfile --precise "3.6.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+ # rustls 0.21.7 has MSRV 1.60.0+
cargo update -p rustls:0.21.7 --precise "0.21.1" cargo update -p rustls:0.21.9 --precise "0.21.1"
# rustls 0.20.9 has MSRV 1.60.0+ # rustls 0.20.9 has MSRV 1.60.0+
cargo update -p rustls:0.20.9 --precise "0.20.8" cargo update -p rustls:0.20.9 --precise "0.20.8"
# tokio 1.33 has MSRV 1.63.0+ # tokio 1.33 has MSRV 1.63.0+
cargo update -p tokio:1.33.0 --precise "1.29.1" cargo update -p tokio --precise "1.29.1"
# tokio-util 0.7.9 doesn't build with MSRV 1.57.0 # tokio-util 0.7.9 doesn't build with MSRV 1.57.0
cargo update -p tokio-util --precise "0.7.8" cargo update -p tokio-util --precise "0.7.8"
# flate2 1.0.27 has MSRV 1.63.0+ # flate2 1.0.27 has MSRV 1.63.0+
cargo update -p flate2:1.0.27 --precise "1.0.26" cargo update -p flate2 --precise "1.0.26"
# reqwest 0.11.19 has MSRV 1.63.0+
cargo update -p reqwest --precise "0.11.18"
# h2 0.3.21 has MSRV 1.63.0+ # h2 0.3.21 has MSRV 1.63.0+
cargo update -p h2 --precise "0.3.20" cargo update -p h2 --precise "0.3.20"
# rustls-webpki 0.100.3 has MSRV 1.60.0+ # rustls-webpki 0.100.3 has MSRV 1.60.0+
cargo update -p rustls-webpki:0.100.3 --precise "0.100.1" cargo update -p rustls-webpki:0.100.3 --precise "0.100.1"
# rustls-webpki 0.101.2 has MSRV 1.60.0+ # rustls-webpki 0.101.2 has MSRV 1.60.0+
cargo update -p rustls-webpki:0.101.6 --precise "0.101.1" cargo update -p rustls-webpki:0.101.7 --precise "0.101.1"
# zip 0.6.6 has MSRV 1.59.0+ # zip 0.6.6 has MSRV 1.59.0+
cargo update -p zip:0.6.6 --precise "0.6.2" cargo update -p zip --precise "0.6.2"
# time 0.3.14 has MSRV 1.59.0+ # time 0.3.14 has MSRV 1.59.0+
cargo update -p time --precise "0.3.13" cargo update -p time --precise "0.3.13"
# cc 1.0.82 has MSRV 1.61.0+
cargo update -p cc --precise "1.0.81"
# byteorder 1.5.0 has MSRV 1.60.0+ # byteorder 1.5.0 has MSRV 1.60.0+
cargo update -p byteorder --precise "1.4.3" cargo update -p byteorder --precise "1.4.3"
# webpki 0.22.4 requires `ring:0.17.2` which has MSRV 1.61.0+ # webpki 0.22.4 requires `ring:0.17.2` which has MSRV 1.61.0+
cargo update -p webpki --precise "0.22.2" 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+ # jobserver 0.1.27 has MSRV 1.66.0+
cargo update -p jobserver --precise "0.1.26" cargo update -p jobserver --precise "0.1.26"
``` ```

View File

@@ -13,7 +13,6 @@ edition = "2021"
rust-version = "1.57" rust-version = "1.57"
[dependencies] [dependencies]
log = "0.4"
rand = "^0.8" rand = "^0.8"
miniscript = { version = "10.0.0", features = ["serde"], 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 } bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
@@ -45,8 +44,10 @@ dev-getrandom-wasm = ["getrandom/js"]
[dev-dependencies] [dev-dependencies]
lazy_static = "1.4" lazy_static = "1.4"
env_logger = "0.7"
assert_matches = "1.5.0" assert_matches = "1.5.0"
tempfile = "3"
bdk_file_store = { path = "../file_store" }
anyhow = "1"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@@ -11,15 +11,12 @@
extern crate bdk; extern crate bdk;
extern crate bitcoin; extern crate bitcoin;
extern crate log;
extern crate miniscript; extern crate miniscript;
extern crate serde_json; extern crate serde_json;
use std::error::Error; use std::error::Error;
use std::str::FromStr; use std::str::FromStr;
use log::info;
use bitcoin::Network; use bitcoin::Network;
use miniscript::policy::Concrete; use miniscript::policy::Concrete;
use miniscript::Descriptor; use miniscript::Descriptor;
@@ -36,13 +33,9 @@ use bdk::{KeychainKind, Wallet};
/// This example demonstrates the interaction between a bdk wallet and miniscript policy. /// This example demonstrates the interaction between a bdk wallet and miniscript policy.
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
// We start with a generic miniscript policy string // 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))))"; 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))))";
info!("Compiling policy: \n{}", policy_str); println!("Compiling policy: \n{}", policy_str);
// Parse the string as a [`Concrete`] type miniscript policy. // Parse the string as a [`Concrete`] type miniscript policy.
let policy = Concrete::<String>::from_str(policy_str)?; let policy = Concrete::<String>::from_str(policy_str)?;
@@ -51,12 +44,12 @@ fn main() -> Result<(), Box<dyn Error>> {
// `policy.compile()` returns the resulting miniscript from the policy. // `policy.compile()` returns the resulting miniscript from the policy.
let descriptor = Descriptor::new_wsh(policy.compile()?)?; let descriptor = Descriptor::new_wsh(policy.compile()?)?;
info!("Compiled into following Descriptor: \n{}", descriptor); println!("Compiled into following Descriptor: \n{}", descriptor);
// Create a new wallet from this descriptor // Create a new wallet from this descriptor
let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?; let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?;
info!( println!(
"First derived address from the descriptor: \n{}", "First derived address from the descriptor: \n{}",
wallet.get_address(New) wallet.get_address(New)
); );
@@ -64,7 +57,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// BDK also has it's own `Policy` structure to represent the spending condition in a more // BDK also has it's own `Policy` structure to represent the spending condition in a more
// human readable json format. // human readable json format.
let spending_policy = wallet.policies(KeychainKind::External)?; let spending_policy = wallet.policies(KeychainKind::External)?;
info!( println!(
"The BDK spending policy: \n{}", "The BDK spending policy: \n{}",
serde_json::to_string_pretty(&spending_policy)? serde_json::to_string_pretty(&spending_policy)?
); );

View File

@@ -6,6 +6,7 @@
// You may not use this file except in accordance with one or both of these // You may not use this file except in accordance with one or both of these
// licenses. // licenses.
use anyhow::anyhow;
use bdk::bitcoin::bip32::DerivationPath; use bdk::bitcoin::bip32::DerivationPath;
use bdk::bitcoin::secp256k1::Secp256k1; use bdk::bitcoin::secp256k1::Secp256k1;
use bdk::bitcoin::Network; use bdk::bitcoin::Network;
@@ -14,13 +15,11 @@ use bdk::descriptor::IntoWalletDescriptor;
use bdk::keys::bip39::{Language, Mnemonic, WordCount}; use bdk::keys::bip39::{Language, Mnemonic, WordCount};
use bdk::keys::{GeneratableKey, GeneratedKey}; use bdk::keys::{GeneratableKey, GeneratedKey};
use bdk::miniscript::Tap; use bdk::miniscript::Tap;
use bdk::Error as BDK_Error;
use std::error::Error;
use std::str::FromStr; use std::str::FromStr;
/// This example demonstrates how to generate a mnemonic phrase /// This example demonstrates how to generate a mnemonic phrase
/// using BDK and use that to generate a descriptor string. /// using BDK and use that to generate a descriptor string.
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), anyhow::Error> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
// In this example we are generating a 12 words mnemonic phrase // In this example we are generating a 12 words mnemonic phrase
@@ -28,7 +27,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// using their respective `WordCount` variant. // using their respective `WordCount` variant.
let mnemonic: GeneratedKey<_, Tap> = let mnemonic: GeneratedKey<_, Tap> =
Mnemonic::generate((WordCount::Words12, Language::English)) Mnemonic::generate((WordCount::Words12, Language::English))
.map_err(|_| BDK_Error::Generic("Mnemonic generation error".to_string()))?; .map_err(|_| anyhow!("Mnemonic generation error"))?;
println!("Mnemonic phrase: {}", *mnemonic); println!("Mnemonic phrase: {}", *mnemonic);
let mnemonic_with_passphrase = (mnemonic, None); let mnemonic_with_passphrase = (mnemonic, None);

View File

@@ -10,8 +10,6 @@
// licenses. // licenses.
extern crate bdk; extern crate bdk;
extern crate env_logger;
extern crate log;
use std::error::Error; use std::error::Error;
use bdk::bitcoin::Network; use bdk::bitcoin::Network;
@@ -29,10 +27,6 @@ use bdk::wallet::signer::SignersContainer;
/// one of the Extend Private key. /// one of the Extend Private key.
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
let secp = bitcoin::secp256k1::Secp256k1::new(); let secp = bitcoin::secp256k1::Secp256k1::new();
// The descriptor used in the example // The descriptor used in the example
@@ -48,7 +42,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// But they can be used as independent tools also. // But they can be used as independent tools also.
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?; let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
log::info!("Example Descriptor for policy analysis : {}", wallet_desc); println!("Example Descriptor for policy analysis : {}", wallet_desc);
// Create the signer with the keymap and descriptor. // Create the signer with the keymap and descriptor.
let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp); let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp);
@@ -60,7 +54,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)? .extract_policy(&signers_container, BuildSatisfaction::None, &secp)?
.expect("We expect a policy"); .expect("We expect a policy");
log::info!("Derived Policy for the descriptor {:#?}", policy); println!("Derived Policy for the descriptor {:#?}", policy);
Ok(()) Ok(())
} }

View File

@@ -10,7 +10,6 @@
// licenses. // licenses.
//! Descriptor errors //! Descriptor errors
use core::fmt; use core::fmt;
/// Errors related to the parsing and usage of descriptors /// Errors related to the parsing and usage of descriptors
@@ -87,9 +86,38 @@ impl fmt::Display for Error {
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl std::error::Error for Error {} impl std::error::Error for Error {}
impl_error!(bitcoin::bip32::Error, Bip32); impl From<bitcoin::bip32::Error> for Error {
impl_error!(bitcoin::base58::Error, Base58); fn from(err: bitcoin::bip32::Error) -> Self {
impl_error!(bitcoin::key::Error, Pk); Error::Bip32(err)
impl_error!(miniscript::Error, Miniscript); }
impl_error!(bitcoin::hashes::hex::Error, Hex); }
impl_error!(crate::descriptor::policy::PolicyError, Policy);
impl From<bitcoin::base58::Error> for Error {
fn from(err: bitcoin::base58::Error) -> Self {
Error::Base58(err)
}
}
impl From<bitcoin::key::Error> for Error {
fn from(err: bitcoin::key::Error) -> Self {
Error::Pk(err)
}
}
impl From<miniscript::Error> for Error {
fn from(err: miniscript::Error) -> Self {
Error::Miniscript(err)
}
}
impl From<bitcoin::hashes::hex::Error> for Error {
fn from(err: bitcoin::hashes::hex::Error) -> Self {
Error::Hex(err)
}
}
impl From<crate::descriptor::policy::PolicyError> for Error {
fn from(err: crate::descriptor::policy::PolicyError) -> Self {
Error::Policy(err)
}
}

View File

@@ -488,11 +488,6 @@ impl DescriptorMeta for ExtendedDescriptor {
) { ) {
Some(derive_path) Some(derive_path)
} else { } else {
log::debug!(
"Key `{}` derived with {} yields an unexpected key",
root_fingerprint,
derive_path
);
None None
} }
}); });

View File

@@ -33,13 +33,14 @@
//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp)); //! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?; //! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
//! println!("policy: {}", serde_json::to_string(&policy).unwrap()); //! println!("policy: {}", serde_json::to_string(&policy).unwrap());
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), anyhow::Error>(())
//! ``` //! ```
use crate::collections::{BTreeMap, HashSet, VecDeque}; use crate::collections::{BTreeMap, HashSet, VecDeque};
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use core::cmp::max; use core::cmp::max;
use core::fmt; use core::fmt;
use serde::ser::SerializeMap; use serde::ser::SerializeMap;
@@ -57,9 +58,6 @@ use miniscript::{
Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey, Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
}; };
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use crate::descriptor::ExtractPolicy; use crate::descriptor::ExtractPolicy;
use crate::keys::ExtScriptContext; use crate::keys::ExtScriptContext;
use crate::wallet::signer::{SignerId, SignersContainer}; use crate::wallet::signer::{SignerId, SignersContainer};
@@ -521,7 +519,7 @@ pub enum PolicyError {
impl fmt::Display for PolicyError { impl fmt::Display for PolicyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::NotEnoughItemsSelected(err) => write!(f, "Not enought items selected: {}", err), Self::NotEnoughItemsSelected(err) => write!(f, "Not enough items selected: {}", err),
Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index), Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index),
Self::AddOnLeaf => write!(f, "Add on leaf"), Self::AddOnLeaf => write!(f, "Add on leaf"),
Self::AddOnPartialComplete => write!(f, "Add on partial complete"), Self::AddOnPartialComplete => write!(f, "Add on partial complete"),

View File

@@ -1,201 +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 crate::bitcoin::Network;
use crate::{descriptor, wallet};
use alloc::{string::String, vec::Vec};
use bitcoin::{OutPoint, Txid};
use core::fmt;
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
#[derive(Debug)]
pub enum Error {
/// Generic error
Generic(String),
/// Cannot build a tx without recipients
NoRecipients,
/// `manually_selected_only` option is selected but no utxo has been passed
NoUtxosSelected,
/// Output created is under the dust limit, 546 satoshis
OutputBelowDustLimit(usize),
/// 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,
},
/// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow
/// exponentially, thus a limit is set, and when hit, this error is thrown
BnBTotalTriesExceeded,
/// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for
/// the desired outputs plus fee, if there is not such combination this error is thrown
BnBNoExactMatch,
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo,
/// Thrown when a tx is not found in the internal database
TransactionNotFound,
/// Happens when trying to bump a transaction that is already confirmed
TransactionConfirmed,
/// Trying to replace a tx that has a sequence >= `0xFFFFFFFE`
IrreplaceableTransaction,
/// When bumping a tx the fee rate requested is lower than required
FeeRateTooLow {
/// Required fee rate (satoshi/vbyte)
required: crate::types::FeeRate,
},
/// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
FeeTooLow {
/// Required fee absolute value (satoshi)
required: u64,
},
/// Node doesn't have data to estimate a fee rate
FeeRateUnavailable,
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
/// explicit origin provided
///
/// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs
MissingKeyOrigin(String),
/// Error while working with [`keys`](crate::keys)
Key(crate::keys::KeyError),
/// Descriptor checksum mismatch
ChecksumMismatch,
/// Spending policy is not compatible with this [`KeychainKind`](crate::types::KeychainKind)
SpendingPolicyRequired(crate::types::KeychainKind),
/// Error while extracting and manipulating policies
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
/// Signing error
Signer(crate::wallet::signer::SignerError),
/// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
InvalidOutpoint(OutPoint),
/// Error related to the parsing and usage of descriptors
Descriptor(crate::descriptor::error::Error),
/// Miniscript error
Miniscript(miniscript::Error),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// BIP32 error
Bip32(bitcoin::bip32::Error),
/// Partially signed bitcoin transaction error
Psbt(bitcoin::psbt::Error),
}
/// Errors returned by miniscript when updating inconsistent PSBTs
#[derive(Debug, Clone)]
pub enum MiniscriptPsbtError {
Conversion(miniscript::descriptor::ConversionError),
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
OutputUpdate(miniscript::psbt::OutputUpdateError),
}
impl fmt::Display for MiniscriptPsbtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Conversion(err) => write!(f, "Conversion error: {}", err),
Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for MiniscriptPsbtError {}
#[cfg(feature = "std")]
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Generic(err) => write!(f, "Generic error: {}", err),
Self::NoRecipients => write!(f, "Cannot build tx without recipients"),
Self::NoUtxosSelected => write!(f, "No UTXO selected"),
Self::OutputBelowDustLimit(limit) => {
write!(f, "Output below the dust limit: {}", limit)
}
Self::InsufficientFunds { needed, available } => write!(
f,
"Insufficient funds: {} sat available of {} sat needed",
available, needed
),
Self::BnBTotalTriesExceeded => {
write!(f, "Branch and bound coin selection: total tries exceeded")
}
Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"),
Self::UnknownUtxo => write!(f, "UTXO not found in the internal database"),
Self::TransactionNotFound => {
write!(f, "Transaction not found in the internal database")
}
Self::TransactionConfirmed => write!(f, "Transaction already confirmed"),
Self::IrreplaceableTransaction => write!(f, "Transaction can't be replaced"),
Self::FeeRateTooLow { required } => write!(
f,
"Fee rate too low: required {} sat/vbyte",
required.as_sat_per_vb()
),
Self::FeeTooLow { required } => write!(f, "Fee to low: required {} sat", required),
Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
Self::MissingKeyOrigin(err) => write!(f, "Missing key origin: {}", err),
Self::Key(err) => write!(f, "Key error: {}", err),
Self::ChecksumMismatch => write!(f, "Descriptor checksum mismatch"),
Self::SpendingPolicyRequired(keychain_kind) => {
write!(f, "Spending policy required: {:?}", keychain_kind)
}
Self::InvalidPolicyPathError(err) => write!(f, "Invalid policy path: {}", err),
Self::Signer(err) => write!(f, "Signer error: {}", err),
Self::InvalidOutpoint(outpoint) => write!(
f,
"Requested outpoint doesn't exist in the tx: {}",
outpoint
),
Self::Descriptor(err) => write!(f, "Descriptor error: {}", err),
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
Self::Psbt(err) => write!(f, "PSBT error: {}", err),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
macro_rules! impl_error {
( $from:ty, $to:ident ) => {
impl_error!($from, $to, Error);
};
( $from:ty, $to:ident, $impl_for:ty ) => {
impl core::convert::From<$from> for $impl_for {
fn from(err: $from) -> Self {
<$impl_for>::$to(err)
}
}
};
}
impl_error!(descriptor::error::Error, Descriptor);
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
impl_error!(wallet::signer::SignerError, Signer);
impl From<crate::keys::KeyError> for Error {
fn from(key_error: crate::keys::KeyError) -> Error {
match key_error {
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
crate::keys::KeyError::InvalidChecksum => Error::ChecksumMismatch,
e => Error::Key(e),
}
}
}
impl_error!(miniscript::Error, Miniscript);
impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
impl_error!(bitcoin::bip32::Error, Bip32);
impl_error!(bitcoin::psbt::Error, Psbt);

View File

@@ -413,7 +413,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
/// } /// }
/// ``` /// ```
/// ///
/// Types that don't internally encode the [`Network`](bitcoin::Network) in which they are valid need some extra /// 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 /// steps to override the set of valid networks, otherwise only the network specified in the
/// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid. /// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid.
/// ///
@@ -932,8 +932,17 @@ pub enum KeyError {
Miniscript(miniscript::Error), Miniscript(miniscript::Error),
} }
impl_error!(miniscript::Error, Miniscript, KeyError); impl From<miniscript::Error> for KeyError {
impl_error!(bitcoin::bip32::Error, Bip32, KeyError); fn from(err: miniscript::Error) -> Self {
KeyError::Miniscript(err)
}
}
impl From<bip32::Error> for KeyError {
fn from(err: bip32::Error) -> Self {
KeyError::Bip32(err)
}
}
impl fmt::Display for KeyError { impl fmt::Display for KeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@@ -19,7 +19,6 @@ pub extern crate alloc;
pub extern crate bitcoin; pub extern crate bitcoin;
#[cfg(feature = "hardware-signer")] #[cfg(feature = "hardware-signer")]
pub extern crate hwi; pub extern crate hwi;
extern crate log;
pub extern crate miniscript; pub extern crate miniscript;
extern crate serde; extern crate serde;
extern crate serde_json; extern crate serde_json;
@@ -27,9 +26,6 @@ extern crate serde_json;
#[cfg(feature = "keys-bip39")] #[cfg(feature = "keys-bip39")]
extern crate bip39; extern crate bip39;
#[allow(unused_imports)]
#[macro_use]
pub(crate) mod error;
pub mod descriptor; pub mod descriptor;
pub mod keys; pub mod keys;
pub mod psbt; pub mod psbt;
@@ -38,7 +34,6 @@ pub mod wallet;
pub use descriptor::template; pub use descriptor::template;
pub use descriptor::HdKeyPaths; pub use descriptor::HdKeyPaths;
pub use error::Error;
pub use types::*; pub use types::*;
pub use wallet::signer; pub use wallet::signer;
pub use wallet::signer::SignOptions; pub use wallet::signer::SignOptions;

View File

@@ -161,7 +161,7 @@ impl Vbytes for usize {
/// ///
/// [`Wallet`]: crate::Wallet /// [`Wallet`]: crate::Wallet
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LocalUtxo { pub struct LocalOutput {
/// Reference to a transaction output /// Reference to a transaction output
pub outpoint: OutPoint, pub outpoint: OutPoint,
/// Transaction output /// Transaction output
@@ -192,7 +192,7 @@ pub struct WeightedUtxo {
/// An unspent transaction output (UTXO). /// An unspent transaction output (UTXO).
pub enum Utxo { pub enum Utxo {
/// A UTXO owned by the local wallet. /// A UTXO owned by the local wallet.
Local(LocalUtxo), Local(LocalOutput),
/// A UTXO owned by another wallet. /// A UTXO owned by another wallet.
Foreign { Foreign {
/// The location of the output. /// The location of the output.

View File

@@ -26,9 +26,12 @@
//! ``` //! ```
//! # use std::str::FromStr; //! # use std::str::FromStr;
//! # use bitcoin::*; //! # use bitcoin::*;
//! # use bdk::wallet::{self, coin_selection::*}; //! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
//! # use bdk::wallet::error::CreateTxError;
//! # use bdk_chain::PersistBackend;
//! # use bdk::*; //! # use bdk::*;
//! # use bdk::wallet::coin_selection::decide_change; //! # use bdk::wallet::coin_selection::decide_change;
//! # use anyhow::Error;
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; //! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
//! #[derive(Debug)] //! #[derive(Debug)]
//! struct AlwaysSpendEverything; //! struct AlwaysSpendEverything;
@@ -41,7 +44,7 @@
//! fee_rate: bdk::FeeRate, //! fee_rate: bdk::FeeRate,
//! target_amount: u64, //! target_amount: u64,
//! drain_script: &Script, //! drain_script: &Script,
//! ) -> Result<CoinSelectionResult, bdk::Error> { //! ) -> Result<CoinSelectionResult, coin_selection::Error> {
//! let mut selected_amount = 0; //! let mut selected_amount = 0;
//! let mut additional_weight = Weight::ZERO; //! let mut additional_weight = Weight::ZERO;
//! let all_utxos_selected = required_utxos //! let all_utxos_selected = required_utxos
@@ -61,7 +64,7 @@
//! let additional_fees = fee_rate.fee_wu(additional_weight); //! let additional_fees = fee_rate.fee_wu(additional_weight);
//! let amount_needed_with_fees = additional_fees + target_amount; //! let amount_needed_with_fees = additional_fees + target_amount;
//! if selected_amount < amount_needed_with_fees { //! if selected_amount < amount_needed_with_fees {
//! return Err(bdk::Error::InsufficientFunds { //! return Err(coin_selection::Error::InsufficientFunds {
//! needed: amount_needed_with_fees, //! needed: amount_needed_with_fees,
//! available: selected_amount, //! available: selected_amount,
//! }); //! });
@@ -94,19 +97,20 @@
//! //!
//! // inspect, sign, broadcast, ... //! // inspect, sign, broadcast, ...
//! //!
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), anyhow::Error>(())
//! ``` //! ```
use crate::types::FeeRate; use crate::types::FeeRate;
use crate::wallet::utils::IsDust; use crate::wallet::utils::IsDust;
use crate::Utxo;
use crate::WeightedUtxo; use crate::WeightedUtxo;
use crate::{error::Error, Utxo};
use alloc::vec::Vec; use alloc::vec::Vec;
use bitcoin::consensus::encode::serialize; use bitcoin::consensus::encode::serialize;
use bitcoin::{Script, Weight}; use bitcoin::{Script, Weight};
use core::convert::TryInto; use core::convert::TryInto;
use core::fmt::{self, Formatter};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
@@ -117,6 +121,43 @@ pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) // prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes)
pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; 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 {
/// 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,
},
/// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for
/// the desired outputs plus fee, if there is not such combination this error is thrown
BnBNoExactMatch,
/// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow
/// exponentially, thus a limit is set, and when hit, this error is thrown
BnBTotalTriesExceeded,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InsufficientFunds { needed, available } => write!(
f,
"Insufficient funds: {} sat available of {} sat needed",
available, needed
),
Self::BnBTotalTriesExceeded => {
write!(f, "Branch and bound coin selection: total tries exceeded")
}
Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
#[derive(Debug)] #[derive(Debug)]
/// Remaining amount after performing coin selection /// Remaining amount after performing coin selection
pub enum Excess { pub enum Excess {
@@ -213,12 +254,6 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection {
target_amount: u64, target_amount: u64,
drain_script: &Script, drain_script: &Script,
) -> Result<CoinSelectionResult, Error> { ) -> Result<CoinSelectionResult, Error> {
log::debug!(
"target_amount = `{}`, fee_rate = `{:?}`",
target_amount,
fee_rate
);
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted, // We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
// initially smallest to largest, before being reversed with `.rev()`. // initially smallest to largest, before being reversed with `.rev()`.
let utxos = { let utxos = {
@@ -311,13 +346,6 @@ fn select_sorted_utxos(
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64, (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
)); ));
**selected_amount += weighted_utxo.utxo.txout().value; **selected_amount += weighted_utxo.utxo.txout().value;
log::debug!(
"Selected {}, updated fee_amount = `{}`",
weighted_utxo.utxo.outpoint(),
fee_amount
);
Some(weighted_utxo.utxo) Some(weighted_utxo.utxo)
} else { } else {
None None
@@ -714,7 +742,7 @@ mod test {
.unwrap(); .unwrap();
WeightedUtxo { WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE, satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
utxo: Utxo::Local(LocalUtxo { utxo: Utxo::Local(LocalOutput {
outpoint, outpoint,
txout: TxOut { txout: TxOut {
value, value,
@@ -774,7 +802,7 @@ mod test {
for _ in 0..utxos_number { for _ in 0..utxos_number {
res.push(WeightedUtxo { res.push(WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE, satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
utxo: Utxo::Local(LocalUtxo { utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::from_str( outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
) )
@@ -803,7 +831,7 @@ mod test {
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> { fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
let utxo = WeightedUtxo { let utxo = WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE, satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
utxo: Utxo::Local(LocalUtxo { utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::from_str( outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
) )
@@ -836,7 +864,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 250_000 + FEE_AMOUNT; let target_amount = 250_000 + FEE_AMOUNT;
let result = LargestFirstCoinSelection::default() let result = LargestFirstCoinSelection
.coin_select( .coin_select(
utxos, utxos,
vec![], vec![],
@@ -857,7 +885,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 20_000 + FEE_AMOUNT; let target_amount = 20_000 + FEE_AMOUNT;
let result = LargestFirstCoinSelection::default() let result = LargestFirstCoinSelection
.coin_select( .coin_select(
utxos, utxos,
vec![], vec![],
@@ -878,7 +906,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 20_000 + FEE_AMOUNT; let target_amount = 20_000 + FEE_AMOUNT;
let result = LargestFirstCoinSelection::default() let result = LargestFirstCoinSelection
.coin_select( .coin_select(
vec![], vec![],
utxos, utxos,
@@ -900,7 +928,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 500_000 + FEE_AMOUNT; let target_amount = 500_000 + FEE_AMOUNT;
LargestFirstCoinSelection::default() LargestFirstCoinSelection
.coin_select( .coin_select(
vec![], vec![],
utxos, utxos,
@@ -918,7 +946,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 250_000 + FEE_AMOUNT; let target_amount = 250_000 + FEE_AMOUNT;
LargestFirstCoinSelection::default() LargestFirstCoinSelection
.coin_select( .coin_select(
vec![], vec![],
utxos, utxos,
@@ -935,7 +963,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 180_000 + FEE_AMOUNT; let target_amount = 180_000 + FEE_AMOUNT;
let result = OldestFirstCoinSelection::default() let result = OldestFirstCoinSelection
.coin_select( .coin_select(
vec![], vec![],
utxos, utxos,
@@ -956,7 +984,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 20_000 + FEE_AMOUNT; let target_amount = 20_000 + FEE_AMOUNT;
let result = OldestFirstCoinSelection::default() let result = OldestFirstCoinSelection
.coin_select( .coin_select(
utxos, utxos,
vec![], vec![],
@@ -977,7 +1005,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 20_000 + FEE_AMOUNT; let target_amount = 20_000 + FEE_AMOUNT;
let result = OldestFirstCoinSelection::default() let result = OldestFirstCoinSelection
.coin_select( .coin_select(
vec![], vec![],
utxos, utxos,
@@ -999,7 +1027,7 @@ mod test {
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
let target_amount = 600_000 + FEE_AMOUNT; let target_amount = 600_000 + FEE_AMOUNT;
OldestFirstCoinSelection::default() OldestFirstCoinSelection
.coin_select( .coin_select(
vec![], vec![],
utxos, utxos,
@@ -1018,7 +1046,7 @@ mod test {
let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50; let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
let drain_script = ScriptBuf::default(); let drain_script = ScriptBuf::default();
OldestFirstCoinSelection::default() OldestFirstCoinSelection
.coin_select( .coin_select(
vec![], vec![],
utxos, utxos,

View File

@@ -0,0 +1,292 @@
// 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.
//! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
use crate::descriptor::policy::PolicyError;
use crate::descriptor::DescriptorError;
use crate::wallet::coin_selection;
use crate::{descriptor, FeeRate, KeychainKind};
use alloc::string::String;
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
use core::fmt;
/// Errors returned by miniscript when updating inconsistent PSBTs
#[derive(Debug, Clone)]
pub enum MiniscriptPsbtError {
/// Descriptor key conversion error
Conversion(miniscript::descriptor::ConversionError),
/// Return error type for PsbtExt::update_input_with_descriptor
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
/// Return error type for PsbtExt::update_output_with_descriptor
OutputUpdate(miniscript::psbt::OutputUpdateError),
}
impl fmt::Display for MiniscriptPsbtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Conversion(err) => write!(f, "Conversion error: {}", err),
Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for MiniscriptPsbtError {}
#[derive(Debug)]
/// Error returned from [`TxBuilder::finish`]
///
/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
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`]
SpendingPolicyRequired(KeychainKind),
/// Requested invalid transaction version '0'
Version0,
/// Requested transaction version `1`, but at least `2` is needed to use OP_CSV
Version1Csv,
/// Requested `LockTime` is less than is required to spend from this script
LockTime {
/// Requested `LockTime`
requested: absolute::LockTime,
/// Required `LockTime`
required: absolute::LockTime,
},
/// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE
RbfSequence,
/// Cannot enable RBF with `Sequence` given a required OP_CSV
RbfSequenceCsv {
/// Given RBF `Sequence`
rbf: Sequence,
/// Required OP_CSV `Sequence`
csv: Sequence,
},
/// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
FeeTooLow {
/// Required fee absolute value (satoshi)
required: u64,
},
/// When bumping a tx the fee rate requested is lower than required
FeeRateTooLow {
/// 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
Psbt(psbt::Error),
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
/// explicit origin provided
///
/// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs
MissingKeyOrigin(String),
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo,
/// Missing non_witness_utxo on foreign utxo for given `OutPoint`
MissingNonWitnessUtxo(OutPoint),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
}
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)
}
CreateTxError::Version0 => {
write!(f, "Invalid version `0`")
}
CreateTxError::Version1Csv => {
write!(
f,
"TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
)
}
CreateTxError::LockTime {
requested,
required,
} => {
write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested)
}
CreateTxError::RbfSequence => {
write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")
}
CreateTxError::RbfSequenceCsv { rbf, csv } => {
write!(
f,
"Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
rbf, csv
)
}
CreateTxError::FeeTooLow { required } => {
write!(f, "Fee to low: required {} sat", required)
}
CreateTxError::FeeRateTooLow { required } => {
write!(
f,
"Fee rate too low: required {} sat/vbyte",
required.as_sat_per_vb()
)
}
CreateTxError::NoUtxosSelected => {
write!(f, "No UTXO selected")
}
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")
}
CreateTxError::Psbt(e) => e.fmt(f),
CreateTxError::MissingKeyOrigin(err) => {
write!(f, "Missing key origin: {}", err)
}
CreateTxError::UnknownUtxo => {
write!(f, "UTXO not found in the internal database")
}
CreateTxError::MissingNonWitnessUtxo(outpoint) => {
write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint)
}
CreateTxError::MiniscriptPsbt(err) => {
write!(f, "Miniscript PSBT error: {}", err)
}
}
}
}
impl<P> From<descriptor::error::Error> for CreateTxError<P> {
fn from(err: descriptor::error::Error) -> Self {
CreateTxError::Descriptor(err)
}
}
impl<P> From<PolicyError> for CreateTxError<P> {
fn from(err: PolicyError) -> Self {
CreateTxError::Policy(err)
}
}
impl<P> From<MiniscriptPsbtError> for CreateTxError<P> {
fn from(err: MiniscriptPsbtError) -> Self {
CreateTxError::MiniscriptPsbt(err)
}
}
impl<P> From<psbt::Error> for CreateTxError<P> {
fn from(err: psbt::Error) -> Self {
CreateTxError::Psbt(err)
}
}
impl<P> From<coin_selection::Error> for CreateTxError<P> {
fn from(err: coin_selection::Error) -> Self {
CreateTxError::CoinSelection(err)
}
}
#[cfg(feature = "std")]
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for CreateTxError<P> {}
#[derive(Debug)]
/// Error returned from [`Wallet::build_fee_bump`]
///
/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump
pub enum BuildFeeBumpError {
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo(OutPoint),
/// Thrown when a tx is not found in the internal database
TransactionNotFound(Txid),
/// Happens when trying to bump a transaction that is already confirmed
TransactionConfirmed(Txid),
/// Trying to replace a tx that has a sequence >= `0xFFFFFFFE`
IrreplaceableTransaction(Txid),
/// Node doesn't have data to estimate a fee rate
FeeRateUnavailable,
}
impl fmt::Display for BuildFeeBumpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownUtxo(outpoint) => write!(
f,
"UTXO not found in the internal database with txid: {}, vout: {}",
outpoint.txid, outpoint.vout
),
Self::TransactionNotFound(txid) => {
write!(
f,
"Transaction not found in the internal database with txid: {}",
txid
)
}
Self::TransactionConfirmed(txid) => {
write!(f, "Transaction already confirmed with txid: {}", txid)
}
Self::IrreplaceableTransaction(txid) => {
write!(f, "Transaction can't be replaced with txid: {}", txid)
}
Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for BuildFeeBumpError {}

File diff suppressed because it is too large Load Diff

View File

@@ -76,7 +76,7 @@
//! Arc::new(custom_signer) //! Arc::new(custom_signer)
//! ); //! );
//! //!
//! # Ok::<_, bdk::Error>(()) //! # Ok::<_, anyhow::Error>(())
//! ``` //! ```
use crate::collections::BTreeMap; use crate::collections::BTreeMap;
@@ -103,6 +103,7 @@ use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
use super::utils::SecpCtx; use super::utils::SecpCtx;
use crate::descriptor::{DescriptorMeta, XKeyUtils}; use crate::descriptor::{DescriptorMeta, XKeyUtils};
use crate::psbt::PsbtUtils; use crate::psbt::PsbtUtils;
use crate::wallet::error::MiniscriptPsbtError;
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among /// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
/// multiple of them /// multiple of them
@@ -159,6 +160,8 @@ pub enum SignerError {
InvalidSighash, InvalidSighash,
/// Error while computing the hash to sign /// Error while computing the hash to sign
SighashError(sighash::Error), SighashError(sighash::Error),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// Error while signing using hardware wallets /// Error while signing using hardware wallets
#[cfg(feature = "hardware-signer")] #[cfg(feature = "hardware-signer")]
HWIError(hwi::error::Error), HWIError(hwi::error::Error),
@@ -192,6 +195,7 @@ impl fmt::Display for SignerError {
Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"), Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"), Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err), Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
#[cfg(feature = "hardware-signer")] #[cfg(feature = "hardware-signer")]
Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err), Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err),
} }
@@ -459,20 +463,23 @@ impl InputSigner for SignerWrapper<PrivateKey> {
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner); let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
if let SignerContext::Tap { is_internal_key } = self.ctx { if let SignerContext::Tap { is_internal_key } = self.ctx {
if is_internal_key if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key {
&& psbt.inputs[input_index].tap_key_sig.is_none() if is_internal_key
&& sign_options.sign_with_tap_internal_key && psbt.inputs[input_index].tap_key_sig.is_none()
{ && sign_options.sign_with_tap_internal_key
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?; && x_only_pubkey == psbt_internal_key
sign_psbt_schnorr( {
&self.inner, let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
x_only_pubkey, sign_psbt_schnorr(
None, &self.inner,
&mut psbt.inputs[input_index], x_only_pubkey,
hash, None,
hash_ty, &mut psbt.inputs[input_index],
secp, hash,
); hash_ty,
secp,
);
}
} }
if let Some((leaf_hashes, _)) = if let Some((leaf_hashes, _)) =
@@ -751,7 +758,7 @@ pub struct SignOptions {
/// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been /// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
/// provided /// provided
/// ///
/// Defaults to `false` to mitigate the "SegWit bug" which chould trick the wallet into /// Defaults to `false` to mitigate the "SegWit bug" which should trick the wallet into
/// paying a fee larger than expected. /// paying a fee larger than expected.
/// ///
/// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for /// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for

View File

@@ -17,7 +17,11 @@
//! # use std::str::FromStr; //! # use std::str::FromStr;
//! # use bitcoin::*; //! # use bitcoin::*;
//! # use bdk::*; //! # use bdk::*;
//! # use bdk::wallet::ChangeSet;
//! # use bdk::wallet::error::CreateTxError;
//! # use bdk::wallet::tx_builder::CreateTx; //! # use bdk::wallet::tx_builder::CreateTx;
//! # use bdk_chain::PersistBackend;
//! # use anyhow::Error;
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); //! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
//! # let mut wallet = doctest_wallet!(); //! # let mut wallet = doctest_wallet!();
//! // create a TxBuilder from a wallet //! // create a TxBuilder from a wallet
@@ -33,7 +37,7 @@
//! // Turn on RBF signaling //! // Turn on RBF signaling
//! .enable_rbf(); //! .enable_rbf();
//! let psbt = tx_builder.finish()?; //! let psbt = tx_builder.finish()?;
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), anyhow::Error>(())
//! ``` //! ```
use crate::collections::BTreeMap; use crate::collections::BTreeMap;
@@ -41,15 +45,18 @@ use crate::collections::HashSet;
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use bdk_chain::PersistBackend; use bdk_chain::PersistBackend;
use core::cell::RefCell; use core::cell::RefCell;
use core::fmt;
use core::marker::PhantomData; use core::marker::PhantomData;
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt}; use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction}; use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use super::ChangeSet; use super::ChangeSet;
use crate::types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}; use crate::types::{FeeRate, KeychainKind, LocalOutput, WeightedUtxo};
use crate::{Error, Utxo, Wallet}; use crate::wallet::CreateTxError;
use crate::{Utxo, Wallet};
/// Context in which the [`TxBuilder`] is valid /// Context in which the [`TxBuilder`] is valid
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
@@ -78,6 +85,10 @@ impl TxBuilderContext for BumpFee {}
/// # use bdk::wallet::tx_builder::*; /// # use bdk::wallet::tx_builder::*;
/// # use bitcoin::*; /// # use bitcoin::*;
/// # use core::str::FromStr; /// # use core::str::FromStr;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_chain::PersistBackend;
/// # use anyhow::Error;
/// # let mut wallet = doctest_wallet!(); /// # let mut wallet = doctest_wallet!();
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); /// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
/// # let addr2 = addr1.clone(); /// # let addr2 = addr1.clone();
@@ -102,7 +113,7 @@ impl TxBuilderContext for BumpFee {}
/// }; /// };
/// ///
/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]); /// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]);
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), anyhow::Error>(())
/// ``` /// ```
/// ///
/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`. /// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`.
@@ -182,12 +193,16 @@ impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> { impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> {
/// Set a custom fee rate /// Set a custom fee rate
/// The fee_rate method sets the mining fee paid by the transaction as a rate on its size. /// 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 Weigth Unit (wu). /// 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: /// 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 /// * 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 /// * 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 /// * sats/kwu (250 sats/kwu == 1 sats/vB) using FeeRate::from_sat_per_kwu
/// Default is 1 sat/vB (see min_relay_fee) /// 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
/// excess might not be viable.
pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self { pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate)); self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
self self
@@ -198,6 +213,10 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// If anyone sets both the fee_absolute method and the fee_rate method, /// 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, /// the FeePolicy enum will be set by whichever method was called last,
/// as the FeeRate and FeeAmount are mutually exclusive. /// 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: u64) -> &mut Self { pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount)); self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
self self
@@ -263,7 +282,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// .add_recipient(to_address.script_pubkey(), 50_000) /// .add_recipient(to_address.script_pubkey(), 50_000)
/// .policy_path(path, KeychainKind::External); /// .policy_path(path, KeychainKind::External);
/// ///
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), anyhow::Error>(())
/// ``` /// ```
pub fn policy_path( pub fn policy_path(
&mut self, &mut self,
@@ -285,12 +304,16 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// ///
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
/// the "utxos" and the "unspendable" list, it will be spent. /// the "utxos" and the "unspendable" list, it will be spent.
pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, Error> { pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> {
{ {
let wallet = self.wallet.borrow(); let wallet = self.wallet.borrow();
let utxos = outpoints let utxos = outpoints
.iter() .iter()
.map(|outpoint| wallet.get_utxo(*outpoint).ok_or(Error::UnknownUtxo)) .map(|outpoint| {
wallet
.get_utxo(*outpoint)
.ok_or(AddUtxoError::UnknownUtxo(*outpoint))
})
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
for utxo in utxos { for utxo in utxos {
@@ -311,7 +334,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// ///
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
/// the "utxos" and the "unspendable" list, it will be spent. /// the "utxos" and the "unspendable" list, it will be spent.
pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, Error> { pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, AddUtxoError> {
self.add_utxos(&[outpoint]) self.add_utxos(&[outpoint])
} }
@@ -366,23 +389,22 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
outpoint: OutPoint, outpoint: OutPoint,
psbt_input: psbt::Input, psbt_input: psbt::Input,
satisfaction_weight: usize, satisfaction_weight: usize,
) -> Result<&mut Self, Error> { ) -> Result<&mut Self, AddForeignUtxoError> {
if psbt_input.witness_utxo.is_none() { if psbt_input.witness_utxo.is_none() {
match psbt_input.non_witness_utxo.as_ref() { match psbt_input.non_witness_utxo.as_ref() {
Some(tx) => { Some(tx) => {
if tx.txid() != outpoint.txid { if tx.txid() != outpoint.txid {
return Err(Error::Generic( return Err(AddForeignUtxoError::InvalidTxid {
"Foreign utxo outpoint does not match PSBT input".into(), input_txid: tx.txid(),
)); foreign_utxo: outpoint,
});
} }
if tx.output.len() <= outpoint.vout as usize { if tx.output.len() <= outpoint.vout as usize {
return Err(Error::InvalidOutpoint(outpoint)); return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
} }
} }
None => { None => {
return Err(Error::Generic( return Err(AddForeignUtxoError::MissingUtxo);
"Foreign utxo missing witness_utxo or non_witness_utxo".into(),
))
} }
} }
} }
@@ -520,7 +542,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// Choose the coin selection algorithm /// Choose the coin selection algorithm
/// ///
/// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm). /// 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. /// 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>( pub fn coin_selection<P: CoinSelectionAlgorithm>(
@@ -537,10 +559,10 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// Finish building the transaction. /// Finish building the transaction.
/// ///
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction. /// Returns a new [`Psbt`] per [`BIP174`].
/// ///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
pub fn finish(self) -> Result<Psbt, Error> pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
where where
D: PersistBackend<ChangeSet>, D: PersistBackend<ChangeSet>,
{ {
@@ -595,6 +617,90 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
} }
} }
#[derive(Debug)]
/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
pub enum AddUtxoError {
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo(OutPoint),
}
impl fmt::Display for AddUtxoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownUtxo(outpoint) => write!(
f,
"UTXO not found in the internal database for txid: {} with vout: {}",
outpoint.txid, outpoint.vout
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AddUtxoError {}
#[derive(Debug)]
/// Error returned from [`TxBuilder::add_foreign_utxo`].
pub enum AddForeignUtxoError {
/// Foreign utxo outpoint txid does not match PSBT input txid
InvalidTxid {
/// PSBT input txid
input_txid: Txid,
/// Foreign UTXO outpoint
foreign_utxo: OutPoint,
},
/// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
InvalidOutpoint(OutPoint),
/// Foreign utxo missing witness_utxo or non_witness_utxo
MissingUtxo,
}
impl fmt::Display for AddForeignUtxoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidTxid {
input_txid,
foreign_utxo,
} => write!(
f,
"Foreign UTXO outpoint txid: {} does not match PSBT input txid: {}",
foreign_utxo.txid, input_txid,
),
Self::InvalidOutpoint(outpoint) => write!(
f,
"Requested outpoint doesn't exist for txid: {} with vout: {}",
outpoint.txid, outpoint.vout,
),
Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"),
}
}
}
#[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> { impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
/// Replace the recipients already added with a new list /// Replace the recipients already added with a new list
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self { pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self {
@@ -639,7 +745,11 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bitcoin::*; /// # use bitcoin::*;
/// # use bdk::*; /// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk::wallet::tx_builder::CreateTx; /// # use bdk::wallet::tx_builder::CreateTx;
/// # use bdk_chain::PersistBackend;
/// # use anyhow::Error;
/// # let to_address = /// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
/// .unwrap() /// .unwrap()
@@ -655,7 +765,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)) /// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
/// .enable_rbf(); /// .enable_rbf();
/// let psbt = tx_builder.finish()?; /// let psbt = tx_builder.finish()?;
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), anyhow::Error>(())
/// ``` /// ```
/// ///
/// [`allow_shrinking`]: Self::allow_shrinking /// [`allow_shrinking`]: Self::allow_shrinking
@@ -680,7 +790,10 @@ impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
/// ///
/// Returns an `Err` if `script_pubkey` can't be found among the recipients of the /// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
/// transaction we are bumping. /// transaction we are bumping.
pub fn allow_shrinking(&mut self, script_pubkey: ScriptBuf) -> Result<&mut Self, Error> { pub fn allow_shrinking(
&mut self,
script_pubkey: ScriptBuf,
) -> Result<&mut Self, AllowShrinkingError> {
match self match self
.params .params
.recipients .recipients
@@ -692,10 +805,7 @@ impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
self.params.drain_to = Some(script_pubkey); self.params.drain_to = Some(script_pubkey);
Ok(self) Ok(self)
} }
None => Err(Error::Generic(format!( None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)),
"{} was not in the original transaction",
script_pubkey
))),
} }
} }
} }
@@ -787,7 +897,7 @@ impl Default for ChangeSpendPolicy {
} }
impl ChangeSpendPolicy { impl ChangeSpendPolicy {
pub(crate) fn is_satisfied_by(&self, utxo: &LocalUtxo) -> bool { pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
match self { match self {
ChangeSpendPolicy::ChangeAllowed => true, ChangeSpendPolicy::ChangeAllowed => true,
ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal, ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal,
@@ -892,11 +1002,11 @@ mod test {
); );
} }
fn get_test_utxos() -> Vec<LocalUtxo> { fn get_test_utxos() -> Vec<LocalOutput> {
use bitcoin::hashes::Hash; use bitcoin::hashes::Hash;
vec![ vec![
LocalUtxo { LocalOutput {
outpoint: OutPoint { outpoint: OutPoint {
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
vout: 0, vout: 0,
@@ -907,7 +1017,7 @@ mod test {
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
derivation_index: 0, derivation_index: 0,
}, },
LocalUtxo { LocalOutput {
outpoint: OutPoint { outpoint: OutPoint {
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
vout: 1, vout: 1,

View File

@@ -1,6 +1,6 @@
#![allow(unused)] #![allow(unused)]
use bdk::{wallet::AddressIndex, KeychainKind, LocalUtxo, Wallet}; use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet};
use bdk_chain::indexed_tx_graph::Indexer; use bdk_chain::indexed_tx_graph::Indexer;
use bdk_chain::{BlockId, ConfirmationTime}; use bdk_chain::{BlockId, ConfirmationTime};
use bitcoin::hashes::Hash; use bitcoin::hashes::Hash;

View File

@@ -156,3 +156,37 @@ fn test_psbt_fee_rate_with_missing_txout() {
assert!(pkh_psbt.fee_amount().is_none()); assert!(pkh_psbt.fee_amount().is_none());
assert!(pkh_psbt.fee_rate().is_none()); assert!(pkh_psbt.fee_rate().is_none());
} }
#[test]
fn test_psbt_multiple_internalkey_signers() {
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 (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.add_recipient(send_to.script_pubkey(), 10_000);
let mut psbt = builder.finish().unwrap();
// Adds a signer for the wrong internal key, bdk should not use this key to sign
wallet.add_signer(
KeychainKind::External,
// A signerordering lower than 100, bdk will use this signer first
SignerOrdering(0),
Arc::new(SignerWrapper::new(
PrivateKey::from_wif("5J5PZqvCe1uThJ3FZeUUFLCh2FuK9pZhtEK4MzhNmugqTmxCdwE").unwrap(),
SignerContext::Tap {
is_internal_key: true,
},
)),
);
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"
);
}

View File

@@ -1,11 +1,15 @@
use std::str::FromStr;
use assert_matches::assert_matches; use assert_matches::assert_matches;
use bdk::descriptor::calc_checksum; use bdk::descriptor::calc_checksum;
use bdk::psbt::PsbtUtils; use bdk::psbt::PsbtUtils;
use bdk::signer::{SignOptions, SignerError}; use bdk::signer::{SignOptions, SignerError};
use bdk::wallet::coin_selection::LargestFirstCoinSelection; use bdk::wallet::coin_selection::{self, LargestFirstCoinSelection};
use bdk::wallet::error::CreateTxError;
use bdk::wallet::tx_builder::AddForeignUtxoError;
use bdk::wallet::AddressIndex::*; use bdk::wallet::AddressIndex::*;
use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet}; use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet};
use bdk::{Error, FeeRate, KeychainKind}; use bdk::{FeeRate, KeychainKind};
use bdk_chain::COINBASE_MATURITY; use bdk_chain::COINBASE_MATURITY;
use bdk_chain::{BlockId, ConfirmationTime}; use bdk_chain::{BlockId, ConfirmationTime};
use bitcoin::hashes::Hash; use bitcoin::hashes::Hash;
@@ -17,7 +21,6 @@ use bitcoin::{
}; };
use bitcoin::{psbt, Network}; use bitcoin::{psbt, Network};
use bitcoin::{BlockHash, Txid}; use bitcoin::{BlockHash, Txid};
use core::str::FromStr;
mod common; mod common;
use common::*; use common::*;
@@ -42,14 +45,14 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) ->
} }
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
let height = match wallet.latest_checkpoint() { let latest_cp = wallet.latest_checkpoint();
Some(cp) => ConfirmationTime::Confirmed { let height = latest_cp.height();
height: cp.height(), let anchor = if height == 0 {
time: 0, ConfirmationTime::Unconfirmed { last_seen: 0 }
}, } else {
None => ConfirmationTime::Unconfirmed { last_seen: 0 }, ConfirmationTime::Confirmed { height, time: 0 }
}; };
receive_output(wallet, value, height) receive_output(wallet, value, anchor)
} }
// The satisfaction size of a P2WPKH is 112 WU = // The satisfaction size of a P2WPKH is 112 WU =
@@ -60,6 +63,101 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
// OP_PUSH. // OP_PUSH.
const P2WPKH_FAKE_WITNESS_SIZE: usize = 106; const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
#[test]
fn load_recovers_wallet() {
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join("store.db");
// create new wallet
let wallet_keychains = {
let db = bdk_file_store::Store::create_new(DB_MAGIC, &file_path).expect("must create db");
let wallet =
Wallet::new(get_test_wpkh(), None, db, Network::Testnet).expect("must init wallet");
wallet.keychains().clone()
};
// recover wallet
{
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
let wallet = Wallet::load(get_test_wpkh(), None, db).expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert_eq!(wallet.spk_index().keychains(), &wallet_keychains);
}
}
#[test]
fn new_or_load() {
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join("store.db");
// init wallet when non-existant
let wallet_keychains = {
let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path)
.expect("must create db");
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
.expect("must init wallet");
wallet.keychains().clone()
};
// wrong network
{
let db =
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
let err = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Bitcoin)
.expect_err("wrong network");
assert!(
matches!(
err,
bdk::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
got: Some(Network::Testnet),
expected: Network::Bitcoin
}
),
"err: {}",
err,
);
}
// wrong genesis hash
{
let exp_blockhash = BlockHash::all_zeros();
let got_blockhash =
bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash();
let db =
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
let err = Wallet::new_or_load_with_genesis_hash(
get_test_wpkh(),
None,
db,
Network::Testnet,
exp_blockhash,
)
.expect_err("wrong genesis hash");
assert!(
matches!(
err,
bdk::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
if got == Some(got_blockhash) && expected == exp_blockhash
),
"err: {}",
err,
);
}
// all parameters match
{
let db =
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
.expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert_eq!(wallet.keychains(), &wallet_keychains);
}
}
#[test] #[test]
fn test_descriptor_checksum() { fn test_descriptor_checksum() {
let (wallet, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _) = get_funded_wallet(get_test_wpkh());
@@ -139,6 +237,25 @@ fn test_get_funded_wallet_tx_fee_rate() {
assert_eq!(tx_fee_rate.as_sat_per_vb(), 8.849558); assert_eq!(tx_fee_rate.as_sat_per_vb(), 8.849558);
} }
#[test]
fn test_list_output() {
let (wallet, txid) = get_funded_wallet(get_test_wpkh());
let txos = wallet
.list_output()
.map(|op| (op.outpoint, op))
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(txos.len(), 2);
for (op, txo) in txos {
if op.txid == txid {
assert_eq!(txo.txout.value, 50_000);
assert!(!txo.is_spent);
} else {
assert_eq!(txo.txout.value, 76_000);
assert!(txo.is_spent);
}
}
}
macro_rules! assert_fee_rate { macro_rules! assert_fee_rate {
($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({ ($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
let psbt = $psbt.clone(); let psbt = $psbt.clone();
@@ -213,7 +330,6 @@ fn test_create_tx_manually_selected_empty_utxos() {
} }
#[test] #[test]
#[should_panic(expected = "Invalid version `0`")]
fn test_create_tx_version_0() { fn test_create_tx_version_0() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@@ -221,13 +337,10 @@ fn test_create_tx_version_0() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.version(0); .version(0);
builder.finish().unwrap(); assert!(matches!(builder.finish(), Err(CreateTxError::Version0)));
} }
#[test] #[test]
#[should_panic(
expected = "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
)]
fn test_create_tx_version_1_csv() { fn test_create_tx_version_1_csv() {
let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@@ -235,7 +348,7 @@ fn test_create_tx_version_1_csv() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.version(1); .version(1);
builder.finish().unwrap(); assert!(matches!(builder.finish(), Err(CreateTxError::Version1Csv)));
} }
#[test] #[test]
@@ -277,7 +390,7 @@ fn test_create_tx_fee_sniping_locktime_last_sync() {
// If there's no current_height we're left with using the last sync height // If there's no current_height we're left with using the last sync height
assert_eq!( assert_eq!(
psbt.unsigned_tx.lock_time.to_consensus_u32(), psbt.unsigned_tx.lock_time.to_consensus_u32(),
wallet.latest_checkpoint().unwrap().height() wallet.latest_checkpoint().height()
); );
} }
@@ -323,9 +436,6 @@ fn test_create_tx_custom_locktime_compatible_with_cltv() {
} }
#[test] #[test]
#[should_panic(
expected = "TxBuilder requested timelock of `Blocks(Height(50000))`, but at least `Blocks(Height(100000))` is required to spend from this script"
)]
fn test_create_tx_custom_locktime_incompatible_with_cltv() { fn test_create_tx_custom_locktime_incompatible_with_cltv() {
let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@@ -333,7 +443,9 @@ fn test_create_tx_custom_locktime_incompatible_with_cltv() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.nlocktime(absolute::LockTime::from_height(50000).unwrap()); .nlocktime(absolute::LockTime::from_height(50000).unwrap());
builder.finish().unwrap(); assert!(matches!(builder.finish(),
Err(CreateTxError::LockTime { requested, required })
if requested.to_consensus_u32() == 50_000 && required.to_consensus_u32() == 100_000));
} }
#[test] #[test]
@@ -362,9 +474,6 @@ fn test_create_tx_with_default_rbf_csv() {
} }
#[test] #[test]
#[should_panic(
expected = "Cannot enable RBF with nSequence `Sequence(3)` given a required OP_CSV of `Sequence(6)`"
)]
fn test_create_tx_with_custom_rbf_csv() { fn test_create_tx_with_custom_rbf_csv() {
let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@@ -372,7 +481,9 @@ fn test_create_tx_with_custom_rbf_csv() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(Sequence(3)); .enable_rbf_with_sequence(Sequence(3));
builder.finish().unwrap(); assert!(matches!(builder.finish(),
Err(CreateTxError::RbfSequenceCsv { rbf, csv })
if rbf.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6));
} }
#[test] #[test]
@@ -387,7 +498,6 @@ fn test_create_tx_no_rbf_cltv() {
} }
#[test] #[test]
#[should_panic(expected = "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")]
fn test_create_tx_invalid_rbf_sequence() { fn test_create_tx_invalid_rbf_sequence() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@@ -395,7 +505,7 @@ fn test_create_tx_invalid_rbf_sequence() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); .enable_rbf_with_sequence(Sequence(0xFFFFFFFE));
builder.finish().unwrap(); assert!(matches!(builder.finish(), Err(CreateTxError::RbfSequence)));
} }
#[test] #[test]
@@ -423,9 +533,6 @@ fn test_create_tx_default_sequence() {
} }
#[test] #[test]
#[should_panic(
expected = "The `change_policy` can be set only if the wallet has a change_descriptor"
)]
fn test_create_tx_change_policy_no_internal() { fn test_create_tx_change_policy_no_internal() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@@ -433,7 +540,10 @@ fn test_create_tx_change_policy_no_internal() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.do_not_spend_change(); .do_not_spend_change();
builder.finish().unwrap(); assert!(matches!(
builder.finish(),
Err(CreateTxError::ChangePolicyDescriptor)
));
} }
macro_rules! check_fee { macro_rules! check_fee {
@@ -1140,7 +1250,6 @@ fn test_calculate_fee_with_missing_foreign_utxo() {
} }
#[test] #[test]
#[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")]
fn test_add_foreign_utxo_invalid_psbt_input() { fn test_add_foreign_utxo_invalid_psbt_input() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let outpoint = wallet.list_unspent().next().expect("must exist").outpoint; let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
@@ -1151,9 +1260,9 @@ fn test_add_foreign_utxo_invalid_psbt_input() {
.unwrap(); .unwrap();
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder let result =
.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction) builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction);
.unwrap(); assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo)));
} }
#[test] #[test]
@@ -1197,7 +1306,7 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
satisfaction_weight satisfaction_weight
) )
.is_ok(), .is_ok(),
"shoulld be ok when outpoint does match psbt_input" "should be ok when outpoint does match psbt_input"
); );
} }
@@ -1615,7 +1724,7 @@ fn test_bump_fee_drain_wallet() {
.insert_tx( .insert_tx(
tx.clone(), tx.clone(),
ConfirmationTime::Confirmed { ConfirmationTime::Confirmed {
height: wallet.latest_checkpoint().unwrap().height(), height: wallet.latest_checkpoint().height(),
time: 42_000, time: 42_000,
}, },
) )
@@ -1917,7 +2026,7 @@ fn test_bump_fee_add_input_change_dust() {
let mut tx = psbt.extract_tx(); let mut tx = psbt.extract_tx();
for txin in &mut tx.input { for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realisitc weight txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realistic weight
} }
let original_tx_weight = tx.weight(); let original_tx_weight = tx.weight();
assert_eq!(tx.input.len(), 1); assert_eq!(tx.input.len(), 1);
@@ -2435,7 +2544,7 @@ fn test_sign_nonstandard_sighash() {
); );
assert_matches!( assert_matches!(
result, result,
Err(bdk::Error::Signer(SignerError::NonStandardSighash)), Err(SignerError::NonStandardSighash),
"Signing failed with the wrong error type" "Signing failed with the wrong error type"
); );
@@ -2852,7 +2961,7 @@ fn test_taproot_sign_missing_witness_utxo() {
); );
assert_matches!( assert_matches!(
result, result,
Err(Error::Signer(SignerError::MissingWitnessUtxo)), Err(SignerError::MissingWitnessUtxo),
"Signing should have failed with the correct error because the witness_utxo is missing" "Signing should have failed with the correct error because the witness_utxo is missing"
); );
@@ -3085,7 +3194,7 @@ fn test_taproot_script_spend_sign_exclude_some_leaves() {
.values() .values()
.map(|(script, version)| TapLeafHash::from_script(script, *version)) .map(|(script, version)| TapLeafHash::from_script(script, *version))
.collect(); .collect();
let included_script_leaves = vec![script_leaves.pop().unwrap()]; let included_script_leaves = [script_leaves.pop().unwrap()];
let excluded_script_leaves = script_leaves; let excluded_script_leaves = script_leaves;
assert!( assert!(
@@ -3193,7 +3302,7 @@ fn test_taproot_sign_non_default_sighash() {
); );
assert_matches!( assert_matches!(
result, result,
Err(Error::Signer(SignerError::NonStandardSighash)), Err(SignerError::NonStandardSighash),
"Signing failed with the wrong error type" "Signing failed with the wrong error type"
); );
@@ -3211,7 +3320,7 @@ fn test_taproot_sign_non_default_sighash() {
); );
assert_matches!( assert_matches!(
result, result,
Err(Error::Signer(SignerError::MissingWitnessUtxo)), Err(SignerError::MissingWitnessUtxo),
"Signing failed with the wrong error type" "Signing failed with the wrong error type"
); );
@@ -3299,10 +3408,12 @@ fn test_spend_coinbase() {
.current_height(confirmation_height); .current_height(confirmation_height);
assert!(matches!( assert!(matches!(
builder.finish(), builder.finish(),
Err(Error::InsufficientFunds { Err(CreateTxError::CoinSelection(
needed: _, coin_selection::Error::InsufficientFunds {
available: 0 needed: _,
}) available: 0
}
))
)); ));
// Still unspendable... // Still unspendable...
@@ -3312,10 +3423,12 @@ fn test_spend_coinbase() {
.current_height(not_yet_mature_time); .current_height(not_yet_mature_time);
assert_matches!( assert_matches!(
builder.finish(), builder.finish(),
Err(Error::InsufficientFunds { Err(CreateTxError::CoinSelection(
needed: _, coin_selection::Error::InsufficientFunds {
available: 0 needed: _,
}) available: 0
}
))
); );
wallet wallet
@@ -3351,7 +3464,10 @@ fn test_allow_dust_limit() {
builder.add_recipient(addr.script_pubkey(), 0); builder.add_recipient(addr.script_pubkey(), 0);
assert_matches!(builder.finish(), Err(Error::OutputBelowDustLimit(0))); assert_matches!(
builder.finish(),
Err(CreateTxError::OutputBelowDustLimit(0))
);
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();

View File

@@ -2,6 +2,13 @@
name = "bdk_bitcoind_rpc" name = "bdk_bitcoind_rpc"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
rust-version = "1.57"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_bitcoind_rpc"
description = "This crate is used for emitting blockchain data from the `bitcoind` RPC interface."
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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -0,0 +1,3 @@
# BDK Bitcoind RPC
This crate is used for emitting blockchain data from the `bitcoind` RPC interface.

View File

@@ -25,7 +25,7 @@ pub struct Emitter<'c, C> {
/// The checkpoint of the last-emitted block that is in the best chain. If it is later found /// The checkpoint of the last-emitted block that is in the best chain. If it is later found
/// that the block is no longer in the best chain, it will be popped off from here. /// that the block is no longer in the best chain, it will be popped off from here.
last_cp: Option<CheckPoint>, last_cp: CheckPoint,
/// The block result returned from rpc of the last-emitted block. As this result contains the /// The block result returned from rpc of the last-emitted block. As this result contains the
/// next block's block hash (which we use to fetch the next block), we set this to `None` /// next block's block hash (which we use to fetch the next block), we set this to `None`
@@ -43,29 +43,16 @@ pub struct Emitter<'c, C> {
} }
impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> { impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
/// Construct a new [`Emitter`] with the given RPC `client` and `start_height`. /// Construct a new [`Emitter`] with the given RPC `client`, `last_cp` and `start_height`.
/// ///
/// `start_height` is the block height to start emitting blocks from. /// * `last_cp` is the check point used to find the latest block which is still part of the best
pub fn from_height(client: &'c C, start_height: u32) -> Self { /// 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 { Self {
client, client,
start_height, start_height,
last_cp: None, last_cp,
last_block: None,
last_mempool_time: 0,
last_mempool_tip: None,
}
}
/// Construct a new [`Emitter`] with the given RPC `client` and `checkpoint`.
///
/// `checkpoint` is used to find the latest block which is still part of the best chain. The
/// [`Emitter`] will emit blocks starting right above this block.
pub fn from_checkpoint(client: &'c C, checkpoint: CheckPoint) -> Self {
Self {
client,
start_height: 0,
last_cp: Some(checkpoint),
last_block: None, last_block: None,
last_mempool_time: 0, last_mempool_time: 0,
last_mempool_tip: None, last_mempool_tip: None,
@@ -134,7 +121,7 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
self.last_mempool_time = latest_time; self.last_mempool_time = latest_time;
self.last_mempool_tip = self.last_cp.as_ref().map(|cp| cp.height()); self.last_mempool_tip = Some(self.last_cp.height());
Ok(txs_to_emit) Ok(txs_to_emit)
} }
@@ -156,7 +143,8 @@ enum PollResponse {
/// Fetched block is not in the best chain. /// Fetched block is not in the best chain.
BlockNotInBestChain, BlockNotInBestChain,
AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint), AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint),
AgreementPointNotFound, /// Force the genesis checkpoint down the receiver's throat.
AgreementPointNotFound(BlockHash),
} }
fn poll_once<C>(emitter: &Emitter<C>) -> Result<PollResponse, bitcoincore_rpc::Error> fn poll_once<C>(emitter: &Emitter<C>) -> Result<PollResponse, bitcoincore_rpc::Error>
@@ -166,45 +154,50 @@ where
let client = emitter.client; let client = emitter.client;
if let Some(last_res) = &emitter.last_block { if let Some(last_res) = &emitter.last_block {
assert!( let next_hash = if last_res.height < emitter.start_height as _ {
emitter.last_cp.is_some(), // enforce start height
"must not have block result without last cp" let next_hash = client.get_block_hash(emitter.start_height as _)?;
); // make sure last emission is still in best chain
if client.get_block_hash(last_res.height as _)? != last_res.hash {
let next_hash = match last_res.nextblockhash { return Ok(PollResponse::BlockNotInBestChain);
None => return Ok(PollResponse::NoMoreBlocks), }
Some(next_hash) => next_hash, next_hash
} else {
match last_res.nextblockhash {
None => return Ok(PollResponse::NoMoreBlocks),
Some(next_hash) => next_hash,
}
}; };
let res = client.get_block_info(&next_hash)?; let res = client.get_block_info(&next_hash)?;
if res.confirmations < 0 { if res.confirmations < 0 {
return Ok(PollResponse::BlockNotInBestChain); return Ok(PollResponse::BlockNotInBestChain);
} }
return Ok(PollResponse::Block(res)); return Ok(PollResponse::Block(res));
} }
if emitter.last_cp.is_none() { for cp in emitter.last_cp.iter() {
let hash = client.get_block_hash(emitter.start_height as _)?; let res = match client.get_block_info(&cp.hash()) {
// block not in best chain
let res = client.get_block_info(&hash)?; Ok(res) if res.confirmations < 0 => continue,
if res.confirmations < 0 { Ok(res) => res,
return Ok(PollResponse::BlockNotInBestChain); Err(e) if e.is_not_found_error() => {
} if cp.height() > 0 {
return Ok(PollResponse::Block(res)); continue;
} }
// if we can't find genesis block, we can't create an update that connects
for cp in emitter.last_cp.iter().flat_map(CheckPoint::iter) { break;
let res = client.get_block_info(&cp.hash())?; }
if res.confirmations < 0 { Err(e) => return Err(e),
// block is not in best chain };
continue;
}
// agreement point found // agreement point found
return Ok(PollResponse::AgreementFound(res, cp)); return Ok(PollResponse::AgreementFound(res, cp));
} }
Ok(PollResponse::AgreementPointNotFound) let genesis_hash = client.get_block_hash(0)?;
Ok(PollResponse::AgreementPointNotFound(genesis_hash))
} }
fn poll<C, V, F>( fn poll<C, V, F>(
@@ -222,25 +215,12 @@ where
let hash = res.hash; let hash = res.hash;
let item = get_item(&hash)?; let item = get_item(&hash)?;
let this_id = BlockId { height, hash }; emitter.last_cp = emitter
let prev_id = res.previousblockhash.map(|prev_hash| BlockId { .last_cp
height: height - 1, .clone()
hash: prev_hash, .push(BlockId { height, hash })
}); .expect("must push");
match (&mut emitter.last_cp, prev_id) {
(Some(cp), _) => *cp = cp.clone().push(this_id).expect("must push"),
(last_cp, None) => *last_cp = Some(CheckPoint::new(this_id)),
// When the receiver constructs a local_chain update from a block, the previous
// checkpoint is also included in the update. We need to reflect this state in
// `Emitter::last_cp` as well.
(last_cp, Some(prev_id)) => {
*last_cp = Some(CheckPoint::new(prev_id).push(this_id).expect("must push"))
}
}
emitter.last_block = Some(res); emitter.last_block = Some(res);
return Ok(Some((height, item))); return Ok(Some((height, item)));
} }
PollResponse::NoMoreBlocks => { PollResponse::NoMoreBlocks => {
@@ -254,9 +234,6 @@ where
PollResponse::AgreementFound(res, cp) => { PollResponse::AgreementFound(res, cp) => {
let agreement_h = res.height as u32; let agreement_h = res.height as u32;
// get rid of evicted blocks
emitter.last_cp = Some(cp);
// The tip during the last mempool emission needs to in the best chain, we reduce // The tip during the last mempool emission needs to in the best chain, we reduce
// it if it is not. // it if it is not.
if let Some(h) = emitter.last_mempool_tip.as_mut() { if let Some(h) = emitter.last_mempool_tip.as_mut() {
@@ -264,15 +241,17 @@ where
*h = agreement_h; *h = agreement_h;
} }
} }
// get rid of evicted blocks
emitter.last_cp = cp;
emitter.last_block = Some(res); emitter.last_block = Some(res);
continue; continue;
} }
PollResponse::AgreementPointNotFound => { PollResponse::AgreementPointNotFound(genesis_hash) => {
// We want to clear `last_cp` and set `start_height` to the first checkpoint's emitter.last_cp = CheckPoint::new(BlockId {
// height. This way, the first checkpoint in `LocalChain` can be replaced. height: 0,
if let Some(last_cp) = emitter.last_cp.take() { hash: genesis_hash,
emitter.start_height = last_cp.height(); });
}
emitter.last_block = None; emitter.last_block = None;
continue; continue;
} }

View File

@@ -188,8 +188,8 @@ fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Up
#[test] #[test]
pub fn test_sync_local_chain() -> anyhow::Result<()> { pub fn test_sync_local_chain() -> anyhow::Result<()> {
let env = TestEnv::new()?; let env = TestEnv::new()?;
let mut local_chain = LocalChain::default(); let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
let mut emitter = Emitter::from_height(&env.client, 0); let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
// mine some blocks and returned the actual block hashes // mine some blocks and returned the actual block hashes
let exp_hashes = { let exp_hashes = {
@@ -296,7 +296,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
env.mine_blocks(101, None)?; env.mine_blocks(101, None)?;
println!("mined blocks!"); println!("mined blocks!");
let mut chain = LocalChain::default(); let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({ let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
let mut index = SpkTxOutIndex::<usize>::default(); let mut index = SpkTxOutIndex::<usize>::default();
index.insert_spk(0, addr_0.script_pubkey()); index.insert_spk(0, addr_0.script_pubkey());
@@ -305,7 +305,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
index index
}); });
let emitter = &mut Emitter::from_height(&env.client, 0); let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
while let Some((height, block)) = emitter.next_block()? { while let Some((height, block)) = emitter.next_block()? {
let _ = chain.apply_update(block_to_chain_update(&block, height))?; let _ = chain.apply_update(block_to_chain_update(&block, height))?;
@@ -393,7 +393,14 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
const CHAIN_TIP_HEIGHT: usize = 110; const CHAIN_TIP_HEIGHT: usize = 110;
let env = TestEnv::new()?; let env = TestEnv::new()?;
let mut emitter = Emitter::from_height(&env.client, EMITTER_START_HEIGHT as _); let mut emitter = Emitter::new(
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.client.get_block_hash(0)?,
}),
EMITTER_START_HEIGHT as _,
);
env.mine_blocks(CHAIN_TIP_HEIGHT, None)?; env.mine_blocks(CHAIN_TIP_HEIGHT, None)?;
while emitter.next_header()?.is_some() {} while emitter.next_header()?.is_some() {}
@@ -442,9 +449,7 @@ fn get_balance(
recv_chain: &LocalChain, recv_chain: &LocalChain,
recv_graph: &IndexedTxGraph<BlockId, SpkTxOutIndex<()>>, recv_graph: &IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
) -> anyhow::Result<Balance> { ) -> anyhow::Result<Balance> {
let chain_tip = recv_chain let chain_tip = recv_chain.tip().block_id();
.tip()
.map_or(BlockId::default(), |cp| cp.block_id());
let outpoints = recv_graph.index.outpoints().clone(); let outpoints = recv_graph.index.outpoints().clone();
let balance = recv_graph let balance = recv_graph
.graph() .graph()
@@ -461,7 +466,14 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
const SEND_AMOUNT: Amount = Amount::from_sat(10_000); const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
let env = TestEnv::new()?; let env = TestEnv::new()?;
let mut emitter = Emitter::from_height(&env.client, 0); let mut emitter = Emitter::new(
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.client.get_block_hash(0)?,
}),
0,
);
// setup addresses // setup addresses
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked(); let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
@@ -469,7 +481,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?; let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
// setup receiver // setup receiver
let mut recv_chain = LocalChain::default(); let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({ let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
let mut recv_index = SpkTxOutIndex::default(); let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone()); recv_index.insert_spk((), spk_to_track.clone());
@@ -542,7 +554,14 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
const MEMPOOL_TX_COUNT: usize = 2; const MEMPOOL_TX_COUNT: usize = 2;
let env = TestEnv::new()?; let env = TestEnv::new()?;
let mut emitter = Emitter::from_height(&env.client, 0); let mut emitter = Emitter::new(
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.client.get_block_hash(0)?,
}),
0,
);
// mine blocks and sync up emitter // mine blocks and sync up emitter
let addr = env.client.get_new_address(None, None)?.assume_checked(); let addr = env.client.get_new_address(None, None)?.assume_checked();
@@ -597,7 +616,14 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
const MEMPOOL_TX_COUNT: usize = 21; const MEMPOOL_TX_COUNT: usize = 21;
let env = TestEnv::new()?; let env = TestEnv::new()?;
let mut emitter = Emitter::from_height(&env.client, 0); let mut emitter = Emitter::new(
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.client.get_block_hash(0)?,
}),
0,
);
// mine blocks to get initial balance, sync emitter up to tip // mine blocks to get initial balance, sync emitter up to tip
let addr = env.client.get_new_address(None, None)?.assume_checked(); let addr = env.client.get_new_address(None, None)?.assume_checked();
@@ -674,7 +700,14 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
const PREMINE_COUNT: usize = 101; const PREMINE_COUNT: usize = 101;
let env = TestEnv::new()?; let env = TestEnv::new()?;
let mut emitter = Emitter::from_height(&env.client, 0); let mut emitter = Emitter::new(
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.client.get_block_hash(0)?,
}),
0,
);
// mine blocks to get initial balance // mine blocks to get initial balance
let addr = env.client.get_new_address(None, None)?.assume_checked(); let addr = env.client.get_new_address(None, None)?.assume_checked();
@@ -702,7 +735,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
"first mempool emission should include all txs", "first mempool emission should include all txs",
); );
// perform reorgs at different heights, these reorgs will not comfirm transactions in the // perform reorgs at different heights, these reorgs will not confirm transactions in the
// mempool // mempool
for reorg_count in 1..TIP_DIFF { for reorg_count in 1..TIP_DIFF {
println!("REORG COUNT: {}", reorg_count); println!("REORG COUNT: {}", reorg_count);
@@ -775,10 +808,10 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
/// If blockchain re-org includes the start height, emit new start height block /// If blockchain re-org includes the start height, emit new start height block
/// ///
/// 1. mine 101 blocks /// 1. mine 101 blocks
/// 2. emmit blocks 99a, 100a /// 2. emit blocks 99a, 100a
/// 3. invalidate blocks 99a, 100a, 101a /// 3. invalidate blocks 99a, 100a, 101a
/// 4. mine new blocks 99b, 100b, 101b /// 4. mine new blocks 99b, 100b, 101b
/// 5. emmit block 99b /// 5. emit block 99b
/// ///
/// The block hash of 99b should be different than 99a, but their previous block hashes should /// The block hash of 99b should be different than 99a, but their previous block hashes should
/// be the same. /// be the same.
@@ -789,7 +822,14 @@ fn no_agreement_point() -> anyhow::Result<()> {
let env = TestEnv::new()?; let env = TestEnv::new()?;
// start height is 99 // start height is 99
let mut emitter = Emitter::from_height(&env.client, (PREMINE_COUNT - 2) as u32); let mut emitter = Emitter::new(
&env.client,
CheckPoint::new(BlockId {
height: 0,
hash: env.client.get_block_hash(0)?,
}),
(PREMINE_COUNT - 2) as u32,
);
// mine 101 blocks // mine 101 blocks
env.mine_blocks(PREMINE_COUNT, None)?; env.mine_blocks(PREMINE_COUNT, None)?;

View File

@@ -18,8 +18,8 @@ bitcoin = { version = "0.30.0", default-features = false }
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] } serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
# Use hashbrown as a feature flag to have HashSet and HashMap from it. # Use hashbrown as a feature flag to have HashSet and HashMap from it.
# note version 0.13 breaks outs MSRV. # note versions > 0.9.1 breaks ours 1.57.0 MSRV.
hashbrown = { version = "0.11", optional = true, features = ["serde"] } hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
miniscript = { version = "10.0.0", optional = true, default-features = false } miniscript = { version = "10.0.0", optional = true, default-features = false }
[dev-dependencies] [dev-dependencies]

View File

@@ -74,8 +74,8 @@ impl ConfirmationTime {
} }
} }
impl From<ChainPosition<ConfirmationTimeAnchor>> for ConfirmationTime { impl From<ChainPosition<ConfirmationTimeHeightAnchor>> for ConfirmationTime {
fn from(observed_as: ChainPosition<ConfirmationTimeAnchor>) -> Self { fn from(observed_as: ChainPosition<ConfirmationTimeHeightAnchor>) -> Self {
match observed_as { match observed_as {
ChainPosition::Confirmed(a) => Self::Confirmed { ChainPosition::Confirmed(a) => Self::Confirmed {
height: a.confirmation_height, height: a.confirmation_height,
@@ -193,7 +193,7 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
derive(serde::Deserialize, serde::Serialize), derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate") serde(crate = "serde_crate")
)] )]
pub struct ConfirmationTimeAnchor { pub struct ConfirmationTimeHeightAnchor {
/// The anchor block. /// The anchor block.
pub anchor_block: BlockId, pub anchor_block: BlockId,
/// The confirmation height of the chain data being anchored. /// The confirmation height of the chain data being anchored.
@@ -202,7 +202,7 @@ pub struct ConfirmationTimeAnchor {
pub confirmation_time: u64, pub confirmation_time: u64,
} }
impl Anchor for ConfirmationTimeAnchor { impl Anchor for ConfirmationTimeHeightAnchor {
fn anchor_block(&self) -> BlockId { fn anchor_block(&self) -> BlockId {
self.anchor_block self.anchor_block
} }
@@ -212,7 +212,7 @@ impl Anchor for ConfirmationTimeAnchor {
} }
} }
impl AnchorFromBlockPosition for ConfirmationTimeAnchor { impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
Self { Self {
anchor_block: block_id, anchor_block: block_id,

View File

@@ -21,5 +21,5 @@ pub trait ChainOracle {
) -> Result<Option<bool>, Self::Error>; ) -> Result<Option<bool>, Self::Error>;
/// Get the best chain's chain tip. /// Get the best chain's chain tip.
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error>; fn get_chain_tip(&self) -> Result<BlockId, Self::Error>;
} }

View File

@@ -160,7 +160,7 @@ where
/// Batch insert unconfirmed transactions, filtering out those that are irrelevant. /// Batch insert unconfirmed transactions, filtering out those that are irrelevant.
/// ///
/// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`. /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`.
/// Irrelevant tansactions in `txs` will be ignored. /// Irrelevant transactions in `txs` will be ignored.
/// ///
/// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The /// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The
/// *last seen* communicates when the transaction is last seen in the mempool which is used for /// *last seen* communicates when the transaction is last seen in the mempool which is used for
@@ -223,7 +223,7 @@ where
/// [`AnchorFromBlockPosition::from_block_position`]. /// [`AnchorFromBlockPosition::from_block_position`].
/// ///
/// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`. /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`.
/// Irrelevant tansactions in `txs` will be ignored. /// Irrelevant transactions in `txs` will be ignored.
pub fn apply_block_relevant( pub fn apply_block_relevant(
&mut self, &mut self,
block: Block, block: Block,

View File

@@ -179,9 +179,9 @@ pub struct Update {
} }
/// This is a local implementation of [`ChainOracle`]. /// This is a local implementation of [`ChainOracle`].
#[derive(Debug, Default, Clone)] #[derive(Debug, Clone)]
pub struct LocalChain { pub struct LocalChain {
tip: Option<CheckPoint>, tip: CheckPoint,
index: BTreeMap<u32, BlockHash>, index: BTreeMap<u32, BlockHash>,
} }
@@ -197,12 +197,6 @@ impl From<LocalChain> for BTreeMap<u32, BlockHash> {
} }
} }
impl From<BTreeMap<u32, BlockHash>> for LocalChain {
fn from(value: BTreeMap<u32, BlockHash>) -> Self {
Self::from_blocks(value)
}
}
impl ChainOracle for LocalChain { impl ChainOracle for LocalChain {
type Error = Infallible; type Error = Infallible;
@@ -225,39 +219,71 @@ impl ChainOracle for LocalChain {
) )
} }
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> { fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
Ok(self.tip.as_ref().map(|tip| tip.block_id())) Ok(self.tip.block_id())
} }
} }
impl LocalChain { impl LocalChain {
/// Get the genesis hash.
pub fn genesis_hash(&self) -> BlockHash {
self.index.get(&0).copied().expect("must have genesis 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 }),
index: core::iter::once((height, hash)).collect(),
};
let changeset = chain.initial_changeset();
(chain, changeset)
}
/// Construct a [`LocalChain`] from an initial `changeset`. /// Construct a [`LocalChain`] from an initial `changeset`.
pub fn from_changeset(changeset: ChangeSet) -> Self { pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
let mut chain = Self::default(); let genesis_entry = changeset.get(&0).copied().flatten();
chain.apply_changeset(&changeset); 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_index_is_consistent_with_tip()); debug_assert!(chain._check_index_is_consistent_with_tip());
debug_assert!(chain._check_changeset_is_applied(&changeset)); debug_assert!(chain._check_changeset_is_applied(&changeset));
chain Ok(chain)
} }
/// Construct a [`LocalChain`] from a given `checkpoint` tip. /// Construct a [`LocalChain`] from a given `checkpoint` tip.
pub fn from_tip(tip: CheckPoint) -> Self { pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
let mut chain = Self { let mut chain = Self {
tip: Some(tip), tip,
..Default::default() index: BTreeMap::new(),
}; };
chain.reindex(0); chain.reindex(0);
if chain.index.get(&0).copied().is_none() {
return Err(MissingGenesisError);
}
debug_assert!(chain._check_index_is_consistent_with_tip()); debug_assert!(chain._check_index_is_consistent_with_tip());
chain Ok(chain)
} }
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`]. /// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
/// ///
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are /// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
/// all of the same chain. /// all of the same chain.
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Self { 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; let mut tip: Option<CheckPoint> = None;
for block in &blocks { for block in &blocks {
@@ -272,25 +298,20 @@ impl LocalChain {
} }
} }
let chain = Self { index: blocks, tip }; let chain = Self {
index: blocks,
tip: tip.expect("already checked to have genesis"),
};
debug_assert!(chain._check_index_is_consistent_with_tip()); debug_assert!(chain._check_index_is_consistent_with_tip());
Ok(chain)
chain
} }
/// Get the highest checkpoint. /// Get the highest checkpoint.
pub fn tip(&self) -> Option<CheckPoint> { pub fn tip(&self) -> CheckPoint {
self.tip.clone() self.tip.clone()
} }
/// Returns whether the [`LocalChain`] is empty (has no checkpoints).
pub fn is_empty(&self) -> bool {
let res = self.tip.is_none();
debug_assert_eq!(res, self.index.is_empty());
res
}
/// Applies the given `update` to the chain. /// Applies the given `update` to the chain.
/// ///
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`. /// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
@@ -312,34 +333,28 @@ impl LocalChain {
/// ///
/// [module-level documentation]: crate::local_chain /// [module-level documentation]: crate::local_chain
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> { pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
match self.tip() { let changeset = merge_chains(
Some(original_tip) => { self.tip.clone(),
let changeset = merge_chains( update.tip.clone(),
original_tip, update.introduce_older_blocks,
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); self.apply_changeset(&changeset)
.map_err(|_| CannotConnectError {
// return early as `apply_changeset` already calls `check_consistency` try_include_height: 0,
Ok(changeset) })?;
} Ok(changeset)
None => {
*self = Self::from_tip(update.tip);
let changeset = self.initial_changeset();
debug_assert!(self._check_index_is_consistent_with_tip());
debug_assert!(self._check_changeset_is_applied(&changeset));
Ok(changeset)
}
}
} }
/// Apply the given `changeset`. /// Apply the given `changeset`.
pub fn apply_changeset(&mut self, changeset: &ChangeSet) { pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
if let Some(start_height) = changeset.keys().next().cloned() { if let Some(start_height) = changeset.keys().next().cloned() {
// changes after point of agreement
let mut extension = BTreeMap::default(); let mut extension = BTreeMap::default();
// point of agreement
let mut base: Option<CheckPoint> = None; let mut base: Option<CheckPoint> = None;
for cp in self.iter_checkpoints() { for cp in self.iter_checkpoints() {
if cp.height() >= start_height { if cp.height() >= start_height {
extension.insert(cp.height(), cp.hash()); extension.insert(cp.height(), cp.hash());
@@ -359,12 +374,12 @@ impl LocalChain {
} }
}; };
} }
let new_tip = match base { let new_tip = match base {
Some(base) => Some( Some(base) => base
base.extend(extension.into_iter().map(BlockId::from)) .extend(extension.into_iter().map(BlockId::from))
.expect("extension is strictly greater than base"), .expect("extension is strictly greater than base"),
), None => LocalChain::from_blocks(extension)?.tip(),
None => LocalChain::from_blocks(extension).tip(),
}; };
self.tip = new_tip; self.tip = new_tip;
self.reindex(start_height); self.reindex(start_height);
@@ -372,6 +387,8 @@ impl LocalChain {
debug_assert!(self._check_index_is_consistent_with_tip()); debug_assert!(self._check_index_is_consistent_with_tip());
debug_assert!(self._check_changeset_is_applied(changeset)); debug_assert!(self._check_changeset_is_applied(changeset));
} }
Ok(())
} }
/// Insert a [`BlockId`]. /// Insert a [`BlockId`].
@@ -379,13 +396,13 @@ impl LocalChain {
/// # Errors /// # Errors
/// ///
/// Replacing the block hash of an existing checkpoint will result in an error. /// Replacing the block hash of an existing checkpoint will result in an error.
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, InsertBlockError> { pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
if let Some(&original_hash) = self.index.get(&block_id.height) { if let Some(&original_hash) = self.index.get(&block_id.height) {
if original_hash != block_id.hash { if original_hash != block_id.hash {
return Err(InsertBlockError { return Err(AlterCheckPointError {
height: block_id.height, height: block_id.height,
original_hash, original_hash,
update_hash: block_id.hash, update_hash: Some(block_id.hash),
}); });
} else { } else {
return Ok(ChangeSet::default()); return Ok(ChangeSet::default());
@@ -394,7 +411,12 @@ impl LocalChain {
let mut changeset = ChangeSet::default(); let mut changeset = ChangeSet::default();
changeset.insert(block_id.height, Some(block_id.hash)); changeset.insert(block_id.height, Some(block_id.hash));
self.apply_changeset(&changeset); self.apply_changeset(&changeset)
.map_err(|_| AlterCheckPointError {
height: 0,
original_hash: self.genesis_hash(),
update_hash: changeset.get(&0).cloned().flatten(),
})?;
Ok(changeset) Ok(changeset)
} }
@@ -418,7 +440,7 @@ impl LocalChain {
/// Iterate over checkpoints in descending height order. /// Iterate over checkpoints in descending height order.
pub fn iter_checkpoints(&self) -> CheckPointIter { pub fn iter_checkpoints(&self) -> CheckPointIter {
CheckPointIter { CheckPointIter {
current: self.tip.as_ref().map(|tip| tip.0.clone()), current: Some(self.tip.0.clone()),
} }
} }
@@ -431,7 +453,6 @@ impl LocalChain {
let tip_history = self let tip_history = self
.tip .tip
.iter() .iter()
.flat_map(CheckPoint::iter)
.map(|cp| (cp.height(), cp.hash())) .map(|cp| (cp.height(), cp.hash()))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
self.index == tip_history self.index == tip_history
@@ -447,29 +468,52 @@ impl LocalChain {
} }
} }
/// Represents a failure when trying to insert a checkpoint into [`LocalChain`]. /// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct InsertBlockError { pub struct MissingGenesisError;
/// The checkpoints' height.
pub height: u32,
/// Original checkpoint's block hash.
pub original_hash: BlockHash,
/// Update checkpoint's block hash.
pub update_hash: BlockHash,
}
impl core::fmt::Display for InsertBlockError { impl core::fmt::Display for MissingGenesisError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!( write!(
f, f,
"failed to insert block at height {} as block hashes conflict: original={}, update={}", "cannot construct `LocalChain` without a genesis checkpoint"
self.height, self.original_hash, self.update_hash
) )
} }
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl std::error::Error for InsertBlockError {} impl std::error::Error for MissingGenesisError {}
/// Represents a failure when trying to insert/remove a checkpoint to/from [`LocalChain`].
#[derive(Clone, Debug, PartialEq)]
pub struct AlterCheckPointError {
/// The checkpoint's height.
pub height: u32,
/// The original checkpoint's block hash which cannot be replaced/removed.
pub original_hash: BlockHash,
/// The attempted update to the `original_block` hash.
pub update_hash: Option<BlockHash>,
}
impl core::fmt::Display for AlterCheckPointError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.update_hash {
Some(update_hash) => write!(
f,
"failed to insert block at height {}: original={} update={}",
self.height, self.original_hash, update_hash
),
None => write!(
f,
"failed to remove block at height {}: original={}",
self.height, self.original_hash
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AlterCheckPointError {}
/// Occurs when an update does not have a common checkpoint with the original chain. /// Occurs when an update does not have a common checkpoint with the original chain.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View File

@@ -79,10 +79,10 @@ pub trait PersistBackend<C> {
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>; fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
/// Return the aggregate changeset `C` from persistence. /// Return the aggregate changeset `C` from persistence.
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError>; fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError>;
} }
impl<C: Default> PersistBackend<C> for () { impl<C> PersistBackend<C> for () {
type WriteError = Infallible; type WriteError = Infallible;
type LoadError = Infallible; type LoadError = Infallible;
@@ -91,7 +91,7 @@ impl<C: Default> PersistBackend<C> for () {
Ok(()) Ok(())
} }
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> { fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
Ok(C::default()) Ok(None)
} }
} }

View File

@@ -57,6 +57,7 @@ use crate::{
use alloc::collections::vec_deque::VecDeque; use alloc::collections::vec_deque::VecDeque;
use alloc::vec::Vec; use alloc::vec::Vec;
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
use core::fmt::{self, Formatter};
use core::{ use core::{
convert::Infallible, convert::Infallible,
ops::{Deref, RangeInclusive}, ops::{Deref, RangeInclusive},
@@ -145,6 +146,26 @@ pub enum CalculateFeeError {
NegativeFee(i64), NegativeFee(i64),
} }
impl fmt::Display for CalculateFeeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
CalculateFeeError::MissingTxOut(outpoints) => write!(
f,
"missing `TxOut` for one or more of the inputs of the tx: {:?}",
outpoints
),
CalculateFeeError::NegativeFee(fee) => write!(
f,
"transaction is invalid according to the graph and has negative fee: {}",
fee
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for CalculateFeeError {}
impl<A> TxGraph<A> { impl<A> TxGraph<A> {
/// Iterate over all tx outputs known by [`TxGraph`]. /// Iterate over all tx outputs known by [`TxGraph`].
/// ///
@@ -480,7 +501,7 @@ impl<A: Clone + Ord> TxGraph<A> {
/// Inserts the given `seen_at` for `txid` into [`TxGraph`]. /// Inserts the given `seen_at` for `txid` into [`TxGraph`].
/// ///
/// Note that [`TxGraph`] only keeps track of the lastest `seen_at`. /// 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> { pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
let mut update = Self::default(); let mut update = Self::default();
let (_, _, update_last_seen) = update.txs.entry(txid).or_default(); let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
@@ -718,7 +739,14 @@ impl<A: Anchor> TxGraph<A> {
// might be in mempool, or it might have been dropped already. // might be in mempool, or it might have been dropped already.
// Let's check conflicts to find out! // Let's check conflicts to find out!
let tx = match tx_node { let tx = match tx_node {
TxNodeInternal::Whole(tx) => tx, 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_coin_base() {
return Ok(None);
}
tx
}
TxNodeInternal::Partial(_) => { TxNodeInternal::Partial(_) => {
// Partial transactions (outputs only) cannot have conflicts. // Partial transactions (outputs only) cannot have conflicts.
return Ok(None); return Ok(None);
@@ -789,6 +817,12 @@ impl<A: Anchor> TxGraph<A> {
if conflicting_tx.last_seen_unconfirmed > tx_last_seen { if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
return Ok(None); return Ok(None);
} }
if conflicting_tx.last_seen_unconfirmed == *last_seen
&& conflicting_tx.txid() > tx.txid()
{
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
return Ok(None);
}
} }
} }

View File

@@ -23,6 +23,7 @@ macro_rules! local_chain {
[ $(($height:expr, $block_hash:expr)), * ] => {{ [ $(($height:expr, $block_hash:expr)), * ] => {{
#[allow(unused_mut)] #[allow(unused_mut)]
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect()) bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
.expect("chain must have genesis block")
}}; }};
} }
@@ -32,8 +33,8 @@ macro_rules! chain_update {
#[allow(unused_mut)] #[allow(unused_mut)]
bdk_chain::local_chain::Update { bdk_chain::local_chain::Update {
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect()) tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
.tip() .expect("chain must have genesis block")
.expect("must have tip"), .tip(),
introduce_older_blocks: true, introduce_older_blocks: true,
} }
}}; }};

View File

@@ -1,7 +1,7 @@
#[macro_use] #[macro_use]
mod common; mod common;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::BTreeSet;
use bdk_chain::{ use bdk_chain::{
indexed_tx_graph::{self, IndexedTxGraph}, indexed_tx_graph::{self, IndexedTxGraph},
@@ -9,9 +9,7 @@ use bdk_chain::{
local_chain::LocalChain, local_chain::LocalChain,
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor, tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
}; };
use bitcoin::{ use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
};
use miniscript::Descriptor; use miniscript::Descriptor;
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
@@ -112,11 +110,8 @@ fn insert_relevant_txs() {
fn test_list_owned_txouts() { fn test_list_owned_txouts() {
// Create Local chains // Create Local chains
let local_chain = LocalChain::from( let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
(0..150) .expect("must have genesis hash");
.map(|i| (i as u32, h!("random")))
.collect::<BTreeMap<u32, BlockHash>>(),
);
// Initiate IndexedTxGraph // Initiate IndexedTxGraph

View File

@@ -312,7 +312,7 @@ fn test_wildcard_derivations() {
let _ = txout_index.reveal_to_target(&TestKeychain::External, 25); let _ = txout_index.reveal_to_target(&TestKeychain::External, 25);
(0..=15) (0..=15)
.chain(vec![17, 20, 23].into_iter()) .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), (26, true)); assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));

View File

@@ -1,4 +1,6 @@
use bdk_chain::local_chain::{CannotConnectError, ChangeSet, InsertBlockError, LocalChain, Update}; use bdk_chain::local_chain::{
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
};
use bitcoin::BlockHash; use bitcoin::BlockHash;
#[macro_use] #[macro_use]
@@ -68,10 +70,10 @@ fn update_local_chain() {
[ [
TestLocalChain { TestLocalChain {
name: "add first tip", name: "add first tip",
chain: local_chain![], chain: local_chain![(0, h!("A"))],
update: chain_update![(0, h!("A"))], update: chain_update![(0, h!("A"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(0, Some(h!("A")))], changeset: &[],
init_changeset: &[(0, Some(h!("A")))], init_changeset: &[(0, Some(h!("A")))],
}, },
}, },
@@ -86,18 +88,18 @@ fn update_local_chain() {
}, },
TestLocalChain { TestLocalChain {
name: "two disjoint chains cannot merge", name: "two disjoint chains cannot merge",
chain: local_chain![(0, h!("A"))], chain: local_chain![(0, h!("_")), (1, h!("A"))],
update: chain_update![(1, h!("B"))], update: chain_update![(0, h!("_")), (2, h!("B"))],
exp: ExpectedResult::Err(CannotConnectError { exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 0, try_include_height: 1,
}), }),
}, },
TestLocalChain { TestLocalChain {
name: "two disjoint chains cannot merge (existing chain longer)", name: "two disjoint chains cannot merge (existing chain longer)",
chain: local_chain![(1, h!("A"))], chain: local_chain![(0, h!("_")), (2, h!("A"))],
update: chain_update![(0, h!("B"))], update: chain_update![(0, h!("_")), (1, h!("B"))],
exp: ExpectedResult::Err(CannotConnectError { exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 1, try_include_height: 2,
}), }),
}, },
TestLocalChain { TestLocalChain {
@@ -111,54 +113,54 @@ fn update_local_chain() {
}, },
// Introduce an older checkpoint (B) // Introduce an older checkpoint (B)
// | 0 | 1 | 2 | 3 // | 0 | 1 | 2 | 3
// chain | C D // chain | _ C D
// update | B C // update | _ B C
TestLocalChain { TestLocalChain {
name: "can introduce older checkpoint", name: "can introduce older checkpoint",
chain: local_chain![(2, h!("C")), (3, h!("D"))], chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D"))],
update: chain_update![(1, h!("B")), (2, h!("C"))], update: chain_update![(0, h!("_")), (1, h!("B")), (2, h!("C"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(1, Some(h!("B")))], changeset: &[(1, Some(h!("B")))],
init_changeset: &[(1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))], init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
}, },
}, },
// Introduce an older checkpoint (A) that is not directly behind PoA // Introduce an older checkpoint (A) that is not directly behind PoA
// | 2 | 3 | 4 // | 0 | 2 | 3 | 4
// chain | B C // chain | _ B C
// update | A C // update | _ A C
TestLocalChain { TestLocalChain {
name: "can introduce older checkpoint 2", name: "can introduce older checkpoint 2",
chain: local_chain![(3, h!("B")), (4, h!("C"))], chain: local_chain![(0, h!("_")), (3, h!("B")), (4, h!("C"))],
update: chain_update![(2, h!("A")), (4, h!("C"))], update: chain_update![(0, h!("_")), (2, h!("A")), (4, h!("C"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(2, Some(h!("A")))], changeset: &[(2, Some(h!("A")))],
init_changeset: &[(2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))], init_changeset: &[(0, Some(h!("_"))), (2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
} }
}, },
// Introduce an older checkpoint (B) that is not the oldest checkpoint // Introduce an older checkpoint (B) that is not the oldest checkpoint
// | 1 | 2 | 3 // | 0 | 1 | 2 | 3
// chain | A C // chain | _ A C
// update | B C // update | _ B C
TestLocalChain { TestLocalChain {
name: "can introduce older checkpoint 3", name: "can introduce older checkpoint 3",
chain: local_chain![(1, h!("A")), (3, h!("C"))], chain: local_chain![(0, h!("_")), (1, h!("A")), (3, h!("C"))],
update: chain_update![(2, h!("B")), (3, h!("C"))], update: chain_update![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(2, Some(h!("B")))], changeset: &[(2, Some(h!("B")))],
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))], init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
} }
}, },
// Introduce two older checkpoints below the PoA // Introduce two older checkpoints below the PoA
// | 1 | 2 | 3 // | 0 | 1 | 2 | 3
// chain | C // chain | _ C
// update | A B C // update | _ A B C
TestLocalChain { TestLocalChain {
name: "introduce two older checkpoints below PoA", name: "introduce two older checkpoints below PoA",
chain: local_chain![(3, h!("C"))], chain: local_chain![(0, h!("_")), (3, h!("C"))],
update: chain_update![(1, h!("A")), (2, h!("B")), (3, h!("C"))], update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))], changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))],
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))], init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
}, },
}, },
TestLocalChain { TestLocalChain {
@@ -172,45 +174,46 @@ fn update_local_chain() {
}, },
// B and C are in both chain and update // B and C are in both chain and update
// | 0 | 1 | 2 | 3 | 4 // | 0 | 1 | 2 | 3 | 4
// chain | B C // chain | _ B C
// update | A B C D // update | _ A B C D
// This should succeed with the point of agreement being C and A should be added in addition. // This should succeed with the point of agreement being C and A should be added in addition.
TestLocalChain { TestLocalChain {
name: "two points of agreement", name: "two points of agreement",
chain: local_chain![(1, h!("B")), (2, h!("C"))], chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
update: chain_update![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))], update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(0, Some(h!("A"))), (3, Some(h!("D")))], changeset: &[(1, Some(h!("A"))), (4, Some(h!("D")))],
init_changeset: &[ init_changeset: &[
(0, Some(h!("A"))), (0, Some(h!("_"))),
(1, Some(h!("B"))), (1, Some(h!("A"))),
(2, Some(h!("C"))), (2, Some(h!("B"))),
(3, Some(h!("D"))), (3, Some(h!("C"))),
(4, Some(h!("D"))),
], ],
}, },
}, },
// Update and chain does not connect: // Update and chain does not connect:
// | 0 | 1 | 2 | 3 | 4 // | 0 | 1 | 2 | 3 | 4
// chain | B C // chain | _ B C
// update | A B D // update | _ A B D
// This should fail as we cannot figure out whether C & D are on the same chain // This should fail as we cannot figure out whether C & D are on the same chain
TestLocalChain { TestLocalChain {
name: "update and chain does not connect", name: "update and chain does not connect",
chain: local_chain![(1, h!("B")), (2, h!("C"))], chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
update: chain_update![(0, h!("A")), (1, h!("B")), (3, h!("D"))], update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (4, h!("D"))],
exp: ExpectedResult::Err(CannotConnectError { exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 2, try_include_height: 3,
}), }),
}, },
// Transient invalidation: // Transient invalidation:
// | 0 | 1 | 2 | 3 | 4 | 5 // | 0 | 1 | 2 | 3 | 4 | 5
// chain | A B C E // chain | _ B C E
// update | A B' C' D // update | _ B' C' D
// This should succeed and invalidate B,C and E with point of agreement being A. // This should succeed and invalidate B,C and E with point of agreement being A.
TestLocalChain { TestLocalChain {
name: "transitive invalidation applies to checkpoints higher than invalidation", name: "transitive invalidation applies to checkpoints higher than invalidation",
chain: local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))], chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
update: chain_update![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))], update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[ changeset: &[
(2, Some(h!("B'"))), (2, Some(h!("B'"))),
@@ -219,7 +222,7 @@ fn update_local_chain() {
(5, None), (5, None),
], ],
init_changeset: &[ init_changeset: &[
(0, Some(h!("A"))), (0, Some(h!("_"))),
(2, Some(h!("B'"))), (2, Some(h!("B'"))),
(3, Some(h!("C'"))), (3, Some(h!("C'"))),
(4, Some(h!("D"))), (4, Some(h!("D"))),
@@ -228,13 +231,13 @@ fn update_local_chain() {
}, },
// Transient invalidation: // Transient invalidation:
// | 0 | 1 | 2 | 3 | 4 // | 0 | 1 | 2 | 3 | 4
// chain | B C E // chain | _ B C E
// update | B' C' D // update | _ B' C' D
// This should succeed and invalidate B, C and E with no point of agreement // This should succeed and invalidate B, C and E with no point of agreement
TestLocalChain { TestLocalChain {
name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement", name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement",
chain: local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))], chain: local_chain![(0, h!("_")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))], update: chain_update![(0, h!("_")), (1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[ changeset: &[
(1, Some(h!("B'"))), (1, Some(h!("B'"))),
@@ -243,6 +246,7 @@ fn update_local_chain() {
(4, None) (4, None)
], ],
init_changeset: &[ init_changeset: &[
(0, Some(h!("_"))),
(1, Some(h!("B'"))), (1, Some(h!("B'"))),
(2, Some(h!("C'"))), (2, Some(h!("C'"))),
(3, Some(h!("D"))), (3, Some(h!("D"))),
@@ -250,16 +254,16 @@ fn update_local_chain() {
}, },
}, },
// Transient invalidation: // Transient invalidation:
// | 0 | 1 | 2 | 3 | 4 // | 0 | 1 | 2 | 3 | 4 | 5
// chain | A B C E // chain | _ A B C E
// update | B' C' D // update | _ B' C' D
// This should fail since although it tells us that B and C are invalid it doesn't tell us whether // This should fail since although it tells us that B and C are invalid it doesn't tell us whether
// A was invalid. // A was invalid.
TestLocalChain { TestLocalChain {
name: "invalidation but no connection", name: "invalidation but no connection",
chain: local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))], chain: local_chain![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))], update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 0 }), exp: ExpectedResult::Err(CannotConnectError { try_include_height: 1 }),
}, },
// Introduce blocks between two points of agreement // Introduce blocks between two points of agreement
// | 0 | 1 | 2 | 3 | 4 | 5 // | 0 | 1 | 2 | 3 | 4 | 5
@@ -294,44 +298,44 @@ fn local_chain_insert_block() {
struct TestCase { struct TestCase {
original: LocalChain, original: LocalChain,
insert: (u32, BlockHash), insert: (u32, BlockHash),
expected_result: Result<ChangeSet, InsertBlockError>, expected_result: Result<ChangeSet, AlterCheckPointError>,
expected_final: LocalChain, expected_final: LocalChain,
} }
let test_cases = [ let test_cases = [
TestCase { TestCase {
original: local_chain![], original: local_chain![(0, h!("_"))],
insert: (5, h!("block5")), insert: (5, h!("block5")),
expected_result: Ok([(5, Some(h!("block5")))].into()), expected_result: Ok([(5, Some(h!("block5")))].into()),
expected_final: local_chain![(5, h!("block5"))], expected_final: local_chain![(0, h!("_")), (5, h!("block5"))],
}, },
TestCase { TestCase {
original: local_chain![(3, h!("A"))], original: local_chain![(0, h!("_")), (3, h!("A"))],
insert: (4, h!("B")), insert: (4, h!("B")),
expected_result: Ok([(4, Some(h!("B")))].into()), expected_result: Ok([(4, Some(h!("B")))].into()),
expected_final: local_chain![(3, h!("A")), (4, h!("B"))], expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
}, },
TestCase { TestCase {
original: local_chain![(4, h!("B"))], original: local_chain![(0, h!("_")), (4, h!("B"))],
insert: (3, h!("A")), insert: (3, h!("A")),
expected_result: Ok([(3, Some(h!("A")))].into()), expected_result: Ok([(3, Some(h!("A")))].into()),
expected_final: local_chain![(3, h!("A")), (4, h!("B"))], expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
}, },
TestCase { TestCase {
original: local_chain![(2, h!("K"))], original: local_chain![(0, h!("_")), (2, h!("K"))],
insert: (2, h!("K")), insert: (2, h!("K")),
expected_result: Ok([].into()), expected_result: Ok([].into()),
expected_final: local_chain![(2, h!("K"))], expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
}, },
TestCase { TestCase {
original: local_chain![(2, h!("K"))], original: local_chain![(0, h!("_")), (2, h!("K"))],
insert: (2, h!("J")), insert: (2, h!("J")),
expected_result: Err(InsertBlockError { expected_result: Err(AlterCheckPointError {
height: 2, height: 2,
original_hash: h!("K"), original_hash: h!("K"),
update_hash: h!("J"), update_hash: Some(h!("J")),
}), }),
expected_final: local_chain![(2, h!("K"))], expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
}, },
]; ];

View File

@@ -15,7 +15,7 @@ use std::vec;
#[test] #[test]
fn insert_txouts() { fn insert_txouts() {
// 2 (Outpoint, TxOut) tupples that denotes original data in the graph, as partial transactions. // 2 (Outpoint, TxOut) tuples that denotes original data in the graph, as partial transactions.
let original_ops = [ let original_ops = [
( (
OutPoint::new(h!("tx1"), 1), OutPoint::new(h!("tx1"), 1),
@@ -33,7 +33,7 @@ fn insert_txouts() {
), ),
]; ];
// Another (OutPoint, TxOut) tupple to be used as update as partial transaction. // Another (OutPoint, TxOut) tuple to be used as update as partial transaction.
let update_ops = [( let update_ops = [(
OutPoint::new(h!("tx2"), 0), OutPoint::new(h!("tx2"), 0),
TxOut { TxOut {
@@ -511,11 +511,13 @@ fn test_calculate_fee_on_coinbase() {
// where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc. // where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc.
#[test] #[test]
fn test_walk_ancestors() { fn test_walk_ancestors() {
let local_chain: LocalChain = (0..=20) let local_chain = LocalChain::from_blocks(
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) (0..=20)
.collect::<BTreeMap<u32, BlockHash>>() .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.into(); .collect(),
let tip = local_chain.tip().expect("must have tip"); )
.expect("must contain genesis hash");
let tip = local_chain.tip();
let tx_a0 = Transaction { let tx_a0 = Transaction {
input: vec![TxIn { input: vec![TxIn {
@@ -839,11 +841,13 @@ fn test_descendants_no_repeat() {
#[test] #[test]
fn test_chain_spends() { fn test_chain_spends() {
let local_chain: LocalChain = (0..=100) let local_chain = LocalChain::from_blocks(
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) (0..=100)
.collect::<BTreeMap<u32, BlockHash>>() .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.into(); .collect(),
let tip = local_chain.tip().expect("must have tip"); )
.expect("must have genesis hash");
let tip = local_chain.tip();
// The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx. // The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx.
// The parent tx is confirmed at block 95. // The parent tx is confirmed at block 95.
@@ -906,18 +910,15 @@ fn test_chain_spends() {
let _ = graph.insert_tx(tx_1.clone()); let _ = graph.insert_tx(tx_1.clone());
let _ = graph.insert_tx(tx_2.clone()); let _ = graph.insert_tx(tx_2.clone());
[95, 98] for (ht, tx) in [(95, &tx_0), (98, &tx_1)] {
.iter() let _ = graph.insert_anchor(
.zip([&tx_0, &tx_1].into_iter()) tx.txid(),
.for_each(|(ht, tx)| { ConfirmationHeightAnchor {
let _ = graph.insert_anchor( anchor_block: tip.block_id(),
tx.txid(), confirmation_height: ht,
ConfirmationHeightAnchor { },
anchor_block: tip.block_id(), );
confirmation_height: *ht, }
},
);
});
// Assert that confirmed spends are returned correctly. // Assert that confirmed spends are returned correctly.
assert_eq!( assert_eq!(
@@ -1078,7 +1079,7 @@ fn test_missing_blocks() {
g g
}, },
chain: { chain: {
let mut c = LocalChain::default(); let (mut c, _) = LocalChain::from_genesis_hash(h!("genesis"));
for (height, hash) in chain { for (height, hash) in chain {
let _ = c.insert_block(BlockId { let _ = c.insert_block(BlockId {
height: *height, height: *height,

View File

@@ -39,21 +39,61 @@ fn test_tx_conflict_handling() {
(5, h!("F")), (5, h!("F")),
(6, h!("G")) (6, h!("G"))
); );
let chain_tip = local_chain let chain_tip = local_chain.tip().block_id();
.tip()
.map(|cp| cp.block_id())
.unwrap_or_default();
let scenarios = [ let scenarios = [
Scenario {
name: "coinbase tx cannot be in mempool and be unconfirmed",
tx_templates: &[
TxTemplate {
tx_name: "unconfirmed_coinbase",
inputs: &[TxInTemplate::Coinbase],
outputs: &[TxOutTemplate::new(5000, Some(0))],
..Default::default()
},
TxTemplate {
tx_name: "confirmed_genesis",
inputs: &[TxInTemplate::Bogus],
outputs: &[TxOutTemplate::new(10000, Some(1))],
anchors: &[block_id!(1, "B")],
last_seen: None,
},
TxTemplate {
tx_name: "unconfirmed_conflict",
inputs: &[
TxInTemplate::PrevTx("confirmed_genesis", 0),
TxInTemplate::PrevTx("unconfirmed_coinbase", 0)
],
outputs: &[TxOutTemplate::new(20000, Some(2))],
..Default::default()
},
TxTemplate {
tx_name: "confirmed_conflict",
inputs: &[TxInTemplate::PrevTx("confirmed_genesis", 0)],
outputs: &[TxOutTemplate::new(20000, Some(3))],
anchors: &[block_id!(4, "E")],
..Default::default()
},
],
exp_chain_txs: HashSet::from(["confirmed_genesis", "confirmed_conflict"]),
exp_chain_txouts: HashSet::from([("confirmed_genesis", 0), ("confirmed_conflict", 0)]),
exp_unspents: HashSet::from([("confirmed_conflict", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 20000,
},
},
Scenario { Scenario {
name: "2 unconfirmed txs with same last_seens conflict", name: "2 unconfirmed txs with same last_seens conflict",
tx_templates: &[ tx_templates: &[
TxTemplate { TxTemplate {
tx_name: "tx1", tx_name: "tx1",
inputs: &[TxInTemplate::Bogus],
outputs: &[TxOutTemplate::new(40000, Some(0))], outputs: &[TxOutTemplate::new(40000, Some(0))],
anchors: &[block_id!(1, "B")], anchors: &[block_id!(1, "B")],
last_seen: None, last_seen: None,
..Default::default()
}, },
TxTemplate { TxTemplate {
tx_name: "tx_conflict_1", tx_name: "tx_conflict_1",
@@ -70,14 +110,12 @@ fn test_tx_conflict_handling() {
..Default::default() ..Default::default()
}, },
], ],
// correct output if filtered by fee rate: tx1, tx_conflict_1 exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]),
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1", "tx_conflict_2"]), exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0), ("tx_conflict_2", 0)]), exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
// correct output if filtered by fee rate: tx_conflict_1
exp_unspents: HashSet::from([("tx_conflict_1", 0), ("tx_conflict_2", 0)]),
exp_balance: Balance { exp_balance: Balance {
immature: 0, immature: 0,
trusted_pending: 50000, // correct output if filtered by fee rate: 20000 trusted_pending: 30000,
untrusted_pending: 0, untrusted_pending: 0,
confirmed: 0, confirmed: 0,
}, },

View File

@@ -2,7 +2,7 @@ use bdk_chain::{
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid}, bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
local_chain::{self, CheckPoint}, local_chain::{self, CheckPoint},
tx_graph::{self, TxGraph}, tx_graph::{self, TxGraph},
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeAnchor, Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
}; };
use electrum_client::{Client, ElectrumApi, Error, HeaderNotification}; use electrum_client::{Client, ElectrumApi, Error, HeaderNotification};
use std::{ use std::{
@@ -11,8 +11,8 @@ use std::{
str::FromStr, str::FromStr,
}; };
/// We assume that a block of this depth and deeper cannot be reorged. /// We include a chain suffix of a certain length for the purpose of robustness.
const ASSUME_FINAL_DEPTH: u32 = 8; const CHAIN_SUFFIX_LENGTH: u32 = 8;
/// Represents updates fetched from an Electrum server, but excludes full transactions. /// Represents updates fetched from an Electrum server, but excludes full transactions.
/// ///
@@ -57,7 +57,7 @@ impl RelevantTxids {
} }
/// Finalizes [`RelevantTxids`] with `new_txs` and anchors of type /// Finalizes [`RelevantTxids`] with `new_txs` and anchors of type
/// [`ConfirmationTimeAnchor`]. /// [`ConfirmationTimeHeightAnchor`].
/// ///
/// **Note:** The confirmation time might not be precisely correct if there has been a reorg. /// **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 /// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
@@ -67,7 +67,7 @@ impl RelevantTxids {
client: &Client, client: &Client,
seen_at: Option<u64>, seen_at: Option<u64>,
missing: Vec<Txid>, missing: Vec<Txid>,
) -> Result<TxGraph<ConfirmationTimeAnchor>, Error> { ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let graph = self.into_tx_graph(client, seen_at, missing)?; let graph = self.into_tx_graph(client, seen_at, missing)?;
let relevant_heights = { let relevant_heights = {
@@ -103,7 +103,7 @@ impl RelevantTxids {
.map(|(height_anchor, txid)| { .map(|(height_anchor, txid)| {
let confirmation_height = height_anchor.confirmation_height; let confirmation_height = height_anchor.confirmation_height;
let confirmation_time = height_to_time[&confirmation_height]; let confirmation_time = height_to_time[&confirmation_height];
let time_anchor = ConfirmationTimeAnchor { let time_anchor = ConfirmationTimeHeightAnchor {
anchor_block: height_anchor.anchor_block, anchor_block: height_anchor.anchor_block,
confirmation_height, confirmation_height,
confirmation_time, confirmation_time,
@@ -148,7 +148,7 @@ pub trait ElectrumExt {
/// single batch request. /// single batch request.
fn scan<K: Ord + Clone>( fn scan<K: Ord + Clone>(
&self, &self,
prev_tip: Option<CheckPoint>, prev_tip: CheckPoint,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>, keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
txids: impl IntoIterator<Item = Txid>, txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
@@ -161,7 +161,7 @@ pub trait ElectrumExt {
/// [`scan`]: ElectrumExt::scan /// [`scan`]: ElectrumExt::scan
fn scan_without_keychain( fn scan_without_keychain(
&self, &self,
prev_tip: Option<CheckPoint>, prev_tip: CheckPoint,
misc_spks: impl IntoIterator<Item = ScriptBuf>, misc_spks: impl IntoIterator<Item = ScriptBuf>,
txids: impl IntoIterator<Item = Txid>, txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
@@ -188,7 +188,7 @@ pub trait ElectrumExt {
impl ElectrumExt for Client { impl ElectrumExt for Client {
fn scan<K: Ord + Clone>( fn scan<K: Ord + Clone>(
&self, &self,
prev_tip: Option<CheckPoint>, prev_tip: CheckPoint,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>, keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
txids: impl IntoIterator<Item = Txid>, txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
@@ -289,25 +289,23 @@ impl ElectrumExt for Client {
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. /// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
fn construct_update_tip( fn construct_update_tip(
client: &Client, client: &Client,
prev_tip: Option<CheckPoint>, prev_tip: CheckPoint,
) -> Result<(CheckPoint, Option<u32>), Error> { ) -> Result<(CheckPoint, Option<u32>), Error> {
let HeaderNotification { height, .. } = client.block_headers_subscribe()?; let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
let new_tip_height = height as u32; let new_tip_height = height as u32;
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do // 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. // not need updating. We just return the previous tip and use that as the point of agreement.
if let Some(prev_tip) = prev_tip.as_ref() { if new_tip_height < prev_tip.height() {
if new_tip_height < prev_tip.height() { return Ok((prev_tip.clone(), Some(prev_tip.height())));
return Ok((prev_tip.clone(), Some(prev_tip.height())));
}
} }
// Atomically fetch the latest `ASSUME_FINAL_DEPTH` count of blocks from Electrum. We use this // Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
// to construct our checkpoint update. // to construct our checkpoint update.
let mut new_blocks = { let mut new_blocks = {
let start_height = new_tip_height.saturating_sub(ASSUME_FINAL_DEPTH); let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
let hashes = client let hashes = client
.block_headers(start_height as _, ASSUME_FINAL_DEPTH as _)? .block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
.headers .headers
.into_iter() .into_iter()
.map(|h| h.block_hash()); .map(|h| h.block_hash());
@@ -317,7 +315,7 @@ fn construct_update_tip(
// Find the "point of agreement" (if any). // Find the "point of agreement" (if any).
let agreement_cp = { let agreement_cp = {
let mut agreement_cp = Option::<CheckPoint>::None; let mut agreement_cp = Option::<CheckPoint>::None;
for cp in prev_tip.iter().flat_map(CheckPoint::iter) { for cp in prev_tip.iter() {
let cp_block = cp.block_id(); let cp_block = cp.block_id();
let hash = match new_blocks.get(&cp_block.height) { let hash = match new_blocks.get(&cp_block.height) {
Some(&hash) => hash, Some(&hash) => hash,

View File

@@ -30,4 +30,5 @@ default = ["std", "async-https", "blocking"]
std = ["bdk_chain/std"] std = ["bdk_chain/std"]
async = ["async-trait", "futures", "esplora-client/async"] async = ["async-trait", "futures", "esplora-client/async"]
async-https = ["async", "esplora-client/async-https"] async-https = ["async", "esplora-client/async-https"]
async-https-rustls = ["async", "esplora-client/async-https-rustls"]
blocking = ["esplora-client/blocking"] blocking = ["esplora-client/blocking"]

View File

@@ -4,7 +4,7 @@ use bdk_chain::{
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
local_chain::{self, CheckPoint}, local_chain::{self, CheckPoint},
BlockId, ConfirmationTimeAnchor, TxGraph, BlockId, ConfirmationTimeHeightAnchor, TxGraph,
}; };
use esplora_client::{Error, TxStatus}; use esplora_client::{Error, TxStatus};
use futures::{stream::FuturesOrdered, TryStreamExt}; use futures::{stream::FuturesOrdered, TryStreamExt};
@@ -32,7 +32,7 @@ pub trait EsploraAsyncExt {
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
async fn update_local_chain( async fn update_local_chain(
&self, &self,
local_tip: Option<CheckPoint>, local_tip: CheckPoint,
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send, request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
) -> Result<local_chain::Update, Error>; ) -> Result<local_chain::Update, Error>;
@@ -40,7 +40,7 @@ pub trait EsploraAsyncExt {
/// indices. /// indices.
/// ///
/// * `keychain_spks`: keychains that we want to scan transactions for /// * `keychain_spks`: keychains that we want to scan transactions for
/// * `txids`: transactions for which we want updated [`ConfirmationTimeAnchor`]s /// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we /// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to include in the update /// want to include in the update
/// ///
@@ -58,7 +58,7 @@ pub trait EsploraAsyncExt {
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send, outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize, stop_gap: usize,
parallel_requests: usize, parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeAnchor>, BTreeMap<K, u32>), Error>; ) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
/// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain. /// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain.
/// ///
@@ -70,7 +70,7 @@ pub trait EsploraAsyncExt {
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send, txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send, outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize, parallel_requests: usize,
) -> Result<TxGraph<ConfirmationTimeAnchor>, Error> { ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
self.scan_txs_with_keychains( self.scan_txs_with_keychains(
[( [(
(), (),
@@ -95,7 +95,7 @@ pub trait EsploraAsyncExt {
impl EsploraAsyncExt for esplora_client::AsyncClient { impl EsploraAsyncExt for esplora_client::AsyncClient {
async fn update_local_chain( async fn update_local_chain(
&self, &self,
local_tip: Option<CheckPoint>, local_tip: CheckPoint,
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send, request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
) -> Result<local_chain::Update, Error> { ) -> Result<local_chain::Update, Error> {
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>(); let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
@@ -129,41 +129,39 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
let earliest_agreement_cp = { let earliest_agreement_cp = {
let mut earliest_agreement_cp = Option::<CheckPoint>::None; let mut earliest_agreement_cp = Option::<CheckPoint>::None;
if let Some(local_tip) = local_tip { let local_tip_height = local_tip.height();
let local_tip_height = local_tip.height(); for local_cp in local_tip.iter() {
for local_cp in local_tip.iter() { let local_block = local_cp.block_id();
let local_block = local_cp.block_id();
// the updated hash (block hash at this height after the update), can either be: // the updated hash (block hash at this height after the update), can either be:
// 1. a block that already existed in `fetched_blocks` // 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 // 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 // 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 // is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
// remote tip // remote tip
let updated_hash = match fetched_blocks.entry(local_block.height) { let updated_hash = match fetched_blocks.entry(local_block.height) {
btree_map::Entry::Occupied(entry) => *entry.get(), btree_map::Entry::Occupied(entry) => *entry.get(),
btree_map::Entry::Vacant(entry) => *entry.insert( btree_map::Entry::Vacant(entry) => *entry.insert(
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH { if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
local_block.hash local_block.hash
} else { } else {
self.get_block_hash(local_block.height).await? self.get_block_hash(local_block.height).await?
}, },
), ),
}; };
// since we may introduce blocks below the point of agreement, we cannot break // 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 // here unconditionally - we only break if we guarantee there are no new heights
// below our current local checkpoint // below our current local checkpoint
if local_block.hash == updated_hash { if local_block.hash == updated_hash {
earliest_agreement_cp = Some(local_cp); earliest_agreement_cp = Some(local_cp);
let first_new_height = *fetched_blocks let first_new_height = *fetched_blocks
.keys() .keys()
.next() .next()
.expect("must have at least one new block"); .expect("must have at least one new block");
if first_new_height >= local_block.height { if first_new_height >= local_block.height {
break; break;
}
} }
} }
} }
@@ -211,10 +209,10 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send, outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize, stop_gap: usize,
parallel_requests: usize, parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeAnchor>, BTreeMap<K, u32>), Error> { ) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>); type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
let parallel_requests = Ord::max(parallel_requests, 1); let parallel_requests = Ord::max(parallel_requests, 1);
let mut graph = TxGraph::<ConfirmationTimeAnchor>::default(); let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
let mut last_active_indexes = BTreeMap::<K, u32>::new(); let mut last_active_indexes = BTreeMap::<K, u32>::new();
for (keychain, spks) in keychain_spks { for (keychain, spks) in keychain_spks {
@@ -261,7 +259,13 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
} }
} }
if last_index > last_active_index.map(|i| i.saturating_add(stop_gap as u32)) { 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; break;
} }
} }

View File

@@ -5,7 +5,7 @@ use bdk_chain::collections::{BTreeMap, BTreeSet};
use bdk_chain::{ use bdk_chain::{
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
local_chain::{self, CheckPoint}, local_chain::{self, CheckPoint},
BlockId, ConfirmationTimeAnchor, TxGraph, BlockId, ConfirmationTimeHeightAnchor, TxGraph,
}; };
use esplora_client::{Error, TxStatus}; use esplora_client::{Error, TxStatus};
@@ -30,7 +30,7 @@ pub trait EsploraExt {
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
fn update_local_chain( fn update_local_chain(
&self, &self,
local_tip: Option<CheckPoint>, local_tip: CheckPoint,
request_heights: impl IntoIterator<Item = u32>, request_heights: impl IntoIterator<Item = u32>,
) -> Result<local_chain::Update, Error>; ) -> Result<local_chain::Update, Error>;
@@ -38,7 +38,7 @@ pub trait EsploraExt {
/// indices. /// indices.
/// ///
/// * `keychain_spks`: keychains that we want to scan transactions for /// * `keychain_spks`: keychains that we want to scan transactions for
/// * `txids`: transactions for which we want updated [`ConfirmationTimeAnchor`]s /// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we /// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to include in the update /// want to include in the update
/// ///
@@ -53,7 +53,7 @@ pub trait EsploraExt {
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize, stop_gap: usize,
parallel_requests: usize, parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeAnchor>, BTreeMap<K, u32>), Error>; ) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
/// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain. /// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain.
/// ///
@@ -65,7 +65,7 @@ pub trait EsploraExt {
txids: impl IntoIterator<Item = Txid>, txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
parallel_requests: usize, parallel_requests: usize,
) -> Result<TxGraph<ConfirmationTimeAnchor>, Error> { ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
self.scan_txs_with_keychains( self.scan_txs_with_keychains(
[( [(
(), (),
@@ -87,7 +87,7 @@ pub trait EsploraExt {
impl EsploraExt for esplora_client::BlockingClient { impl EsploraExt for esplora_client::BlockingClient {
fn update_local_chain( fn update_local_chain(
&self, &self,
local_tip: Option<CheckPoint>, local_tip: CheckPoint,
request_heights: impl IntoIterator<Item = u32>, request_heights: impl IntoIterator<Item = u32>,
) -> Result<local_chain::Update, Error> { ) -> Result<local_chain::Update, Error> {
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>(); let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
@@ -120,41 +120,39 @@ impl EsploraExt for esplora_client::BlockingClient {
let earliest_agreement_cp = { let earliest_agreement_cp = {
let mut earliest_agreement_cp = Option::<CheckPoint>::None; let mut earliest_agreement_cp = Option::<CheckPoint>::None;
if let Some(local_tip) = local_tip { let local_tip_height = local_tip.height();
let local_tip_height = local_tip.height(); for local_cp in local_tip.iter() {
for local_cp in local_tip.iter() { let local_block = local_cp.block_id();
let local_block = local_cp.block_id();
// the updated hash (block hash at this height after the update), can either be: // the updated hash (block hash at this height after the update), can either be:
// 1. a block that already existed in `fetched_blocks` // 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 // 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 // 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 // is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
// remote tip // remote tip
let updated_hash = match fetched_blocks.entry(local_block.height) { let updated_hash = match fetched_blocks.entry(local_block.height) {
btree_map::Entry::Occupied(entry) => *entry.get(), btree_map::Entry::Occupied(entry) => *entry.get(),
btree_map::Entry::Vacant(entry) => *entry.insert( btree_map::Entry::Vacant(entry) => *entry.insert(
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH { if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
local_block.hash local_block.hash
} else { } else {
self.get_block_hash(local_block.height)? self.get_block_hash(local_block.height)?
}, },
), ),
}; };
// since we may introduce blocks below the point of agreement, we cannot break // 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 // here unconditionally - we only break if we guarantee there are no new heights
// below our current local checkpoint // below our current local checkpoint
if local_block.hash == updated_hash { if local_block.hash == updated_hash {
earliest_agreement_cp = Some(local_cp); earliest_agreement_cp = Some(local_cp);
let first_new_height = *fetched_blocks let first_new_height = *fetched_blocks
.keys() .keys()
.next() .next()
.expect("must have at least one new block"); .expect("must have at least one new block");
if first_new_height >= local_block.height { if first_new_height >= local_block.height {
break; break;
}
} }
} }
} }
@@ -199,10 +197,10 @@ impl EsploraExt for esplora_client::BlockingClient {
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize, stop_gap: usize,
parallel_requests: usize, parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeAnchor>, BTreeMap<K, u32>), Error> { ) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>); type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
let parallel_requests = Ord::max(parallel_requests, 1); let parallel_requests = Ord::max(parallel_requests, 1);
let mut graph = TxGraph::<ConfirmationTimeAnchor>::default(); let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
let mut last_active_indexes = BTreeMap::<K, u32>::new(); let mut last_active_indexes = BTreeMap::<K, u32>::new();
for (keychain, spks) in keychain_spks { for (keychain, spks) in keychain_spks {
@@ -252,7 +250,13 @@ impl EsploraExt for esplora_client::BlockingClient {
} }
} }
if last_index > last_active_index.map(|i| i.saturating_add(stop_gap as u32)) { 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; break;
} }
} }

View File

@@ -1,5 +1,5 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
use bdk_chain::{BlockId, ConfirmationTimeAnchor}; use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
use esplora_client::TxStatus; use esplora_client::TxStatus;
pub use esplora_client; pub use esplora_client;
@@ -16,7 +16,7 @@ pub use async_ext::*;
const ASSUME_FINAL_DEPTH: u32 = 15; const ASSUME_FINAL_DEPTH: u32 = 15;
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeAnchor> { fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor> {
if let TxStatus { if let TxStatus {
block_height: Some(height), block_height: Some(height),
block_hash: Some(hash), block_hash: Some(hash),
@@ -24,7 +24,7 @@ fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeAnchor> {
.. ..
} = status.clone() } = status.clone()
{ {
Some(ConfirmationTimeAnchor { Some(ConfirmationTimeHeightAnchor {
anchor_block: BlockId { height, hash }, anchor_block: BlockId { height, hash },
confirmation_height: height, confirmation_height: height,
confirmation_time: time, confirmation_time: time,

View File

@@ -3,6 +3,7 @@ use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use electrsd::bitcoind::{self, anyhow, BitcoinD}; use electrsd::bitcoind::{self, anyhow, BitcoinD};
use electrsd::{Conf, ElectrsD}; use electrsd::{Conf, ElectrsD};
use esplora_client::{self, AsyncClient, Builder}; use esplora_client::{self, AsyncClient, Builder};
use std::collections::{BTreeMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
@@ -115,3 +116,121 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
assert_eq!(graph_update_txids, expected_txids); assert_eq!(graph_update_txids, expected_txids);
Ok(()) Ok(())
} }
/// Test the bounds of the address scan depending on the gap limit.
#[tokio::test]
pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let _block_hashes = env.mine_blocks(101, None)?;
// Now let's test the gap limit. First of all get a chain of 10 addresses.
let addresses = [
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
];
let addresses: Vec<_> = addresses
.into_iter()
.map(|s| Address::from_str(s).unwrap().assume_checked())
.collect();
let spks: Vec<_> = addresses
.iter()
.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(
&addresses[3],
Amount::from_sat(10000),
None,
None,
None,
None,
Some(1),
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while env.client.get_height().await.unwrap() < 103 {
sleep(Duration::from_millis(10))
}
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
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(
&addresses[addresses.len() - 1],
Amount::from_sat(10000),
None,
None,
None,
None,
Some(1),
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while env.client.get_height().await.unwrap() < 104 {
sleep(Duration::from_millis(10))
}
// 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 (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!(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!(active_indices[&0], 9);
Ok(())
}

View File

@@ -3,6 +3,7 @@ use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use electrsd::bitcoind::{self, anyhow, BitcoinD}; use electrsd::bitcoind::{self, anyhow, BitcoinD};
use electrsd::{Conf, ElectrsD}; use electrsd::{Conf, ElectrsD};
use esplora_client::{self, BlockingClient, Builder}; use esplora_client::{self, BlockingClient, Builder};
use std::collections::{BTreeMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
@@ -110,5 +111,118 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
let mut expected_txids = vec![txid1, txid2]; let mut expected_txids = vec![txid1, txid2];
expected_txids.sort(); expected_txids.sort();
assert_eq!(graph_update_txids, expected_txids); assert_eq!(graph_update_txids, expected_txids);
Ok(())
}
/// Test the bounds of the address scan depending on the gap limit.
#[test]
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let _block_hashes = env.mine_blocks(101, None)?;
// Now let's test the gap limit. First of all get a chain of 10 addresses.
let addresses = [
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
];
let addresses: Vec<_> = addresses
.into_iter()
.map(|s| Address::from_str(s).unwrap().assume_checked())
.collect();
let spks: Vec<_> = addresses
.iter()
.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(
&addresses[3],
Amount::from_sat(10000),
None,
None,
None,
None,
Some(1),
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while env.client.get_height().unwrap() < 103 {
sleep(Duration::from_millis(10))
}
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
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(
&addresses[addresses.len() - 1],
Amount::from_sat(10000),
None,
None,
None,
None,
Some(1),
None,
)?;
let _block_hashes = env.mine_blocks(1, None)?;
while env.client.get_height().unwrap() < 104 {
sleep(Duration::from_millis(10))
}
// 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 (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!(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!(active_indices[&0], 9);
Ok(()) Ok(())
} }

View File

@@ -23,7 +23,7 @@ pub struct Store<'a, C> {
impl<'a, C> PersistBackend<C> for Store<'a, C> impl<'a, C> PersistBackend<C> for Store<'a, C>
where where
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned, C: Append + serde::Serialize + serde::de::DeserializeOwned,
{ {
type WriteError = std::io::Error; type WriteError = std::io::Error;
@@ -33,30 +33,64 @@ where
self.append_changeset(changeset) self.append_changeset(changeset)
} }
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> { fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
let (changeset, result) = self.aggregate_changesets(); self.aggregate_changesets().map_err(|e| e.iter_error)
result.map(|_| changeset)
} }
} }
impl<'a, C> Store<'a, C> impl<'a, C> Store<'a, C>
where where
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned, C: Append + serde::Serialize + serde::de::DeserializeOwned,
{ {
/// Creates a new store from a [`File`]. /// Create a new [`Store`] file in write-only mode; error if the file exists.
/// ///
/// The file must have been opened with read and write permissions. /// `magic` is the prefixed bytes to write to the new file. This will be checked when opening
/// the `Store` in the future with [`open`].
/// ///
/// `magic` is the expected prefixed bytes of the file. If this does not match, an error will be /// [`open`]: Store::open
/// returned. pub fn create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
where
P: AsRef<Path>,
{
if file_path.as_ref().exists() {
// `io::Error` is used instead of a variant on `FileError` because there is already a
// nightly-only `File::create_new` method
return Err(FileError::Io(io::Error::new(
io::ErrorKind::Other,
"file already exists",
)));
}
let mut f = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(file_path)?;
f.write_all(magic)?;
Ok(Self {
magic,
db_file: f,
marker: Default::default(),
})
}
/// Open an existing [`Store`].
/// ///
/// [`File`]: std::fs::File /// Use [`create_new`] to create a new `Store`.
pub fn new(magic: &'a [u8], mut db_file: File) -> Result<Self, FileError> { ///
db_file.rewind()?; /// # Errors
///
/// If the prefixed bytes of the opened file does not match the provided `magic`, the
/// [`FileError::InvalidMagicBytes`] error variant will be returned.
///
/// [`create_new`]: Store::create_new
pub fn open<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
where
P: AsRef<Path>,
{
let mut f = OpenOptions::new().read(true).write(true).open(file_path)?;
let mut magic_buf = vec![0_u8; magic.len()]; let mut magic_buf = vec![0_u8; magic.len()];
db_file.read_exact(magic_buf.as_mut())?; f.read_exact(&mut magic_buf)?;
if magic_buf != magic { if magic_buf != magic {
return Err(FileError::InvalidMagicBytes { return Err(FileError::InvalidMagicBytes {
got: magic_buf, got: magic_buf,
@@ -66,35 +100,26 @@ where
Ok(Self { Ok(Self {
magic, magic,
db_file, db_file: f,
marker: Default::default(), marker: Default::default(),
}) })
} }
/// Creates or loads a store from `db_path`. /// Attempt to open existing [`Store`] file; create it if the file is non-existant.
/// ///
/// If no file exists there, it will be created. /// Internally, this calls either [`open`] or [`create_new`].
/// ///
/// Refer to [`new`] for documentation on the `magic` input. /// [`open`]: Store::open
/// /// [`create_new`]: Store::create_new
/// [`new`]: Self::new pub fn open_or_create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
pub fn new_from_path<P>(magic: &'a [u8], db_path: P) -> Result<Self, FileError>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let already_exists = db_path.as_ref().exists(); if file_path.as_ref().exists() {
Self::open(magic, file_path)
let mut db_file = OpenOptions::new() } else {
.read(true) Self::create_new(magic, file_path)
.write(true)
.create(true)
.open(db_path)?;
if !already_exists {
db_file.write_all(magic)?;
} }
Self::new(magic, db_file)
} }
/// Iterates over the stored changeset from first to last, changing the seek position at each /// Iterates over the stored changeset from first to last, changing the seek position at each
@@ -122,16 +147,24 @@ where
/// ///
/// **WARNING**: This method changes the write position of the underlying file. The next /// **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). /// changeset will be written over the erroring entry (or the end of the file if none existed).
pub fn aggregate_changesets(&mut self) -> (C, Result<(), IterError>) { pub fn aggregate_changesets(&mut self) -> Result<Option<C>, AggregateChangesetsError<C>> {
let mut changeset = C::default(); let mut changeset = Option::<C>::None;
let result = (|| { for next_changeset in self.iter_changesets() {
for next_changeset in self.iter_changesets() { let next_changeset = match next_changeset {
changeset.append(next_changeset?); Ok(next_changeset) => next_changeset,
Err(iter_error) => {
return Err(AggregateChangesetsError {
changeset,
iter_error,
})
}
};
match &mut changeset {
Some(changeset) => changeset.append(next_changeset),
changeset => *changeset = Some(next_changeset),
} }
Ok(()) }
})(); Ok(changeset)
(changeset, result)
} }
/// Append a new changeset to the file and truncate the file to the end of the appended /// Append a new changeset to the file and truncate the file to the end of the appended
@@ -162,6 +195,24 @@ where
} }
} }
/// Error type for [`Store::aggregate_changesets`].
#[derive(Debug)]
pub struct AggregateChangesetsError<C> {
/// The partially-aggregated changeset.
pub changeset: Option<C>,
/// The error returned by [`EntryIter`].
pub iter_error: IterError,
}
impl<C> std::fmt::Display for AggregateChangesetsError<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.iter_error, f)
}
}
impl<C: std::fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@@ -182,13 +233,50 @@ mod test {
#[derive(Debug)] #[derive(Debug)]
struct TestTracker; struct TestTracker;
/// Check behavior of [`Store::create_new`] and [`Store::open`].
#[test]
fn construct_store() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("db_file");
let _ = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
.expect_err("must not open as file does not exist yet");
let _ = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path)
.expect("must create file");
// cannot create new as file already exists
let _ = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path)
.expect_err("must fail as file already exists now");
let _ = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
.expect("must open as file exists now");
}
#[test]
fn open_or_create_new() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("db_file");
let changeset = vec!["hello".to_string(), "world".to_string()];
{
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
.expect("must create");
assert!(file_path.exists());
db.append_changeset(&changeset).expect("must succeed");
}
{
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
.expect("must recover");
let recovered_changeset = db.aggregate_changesets().expect("must succeed");
assert_eq!(recovered_changeset, Some(changeset));
}
}
#[test] #[test]
fn new_fails_if_file_is_too_short() { fn new_fails_if_file_is_too_short() {
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
file.write_all(&TEST_MAGIC_BYTES[..TEST_MAGIC_BYTES_LEN - 1]) file.write_all(&TEST_MAGIC_BYTES[..TEST_MAGIC_BYTES_LEN - 1])
.expect("should write"); .expect("should write");
match Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap()) { match Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()) {
Err(FileError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof), Err(FileError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof),
unexpected => panic!("unexpected result: {:?}", unexpected), unexpected => panic!("unexpected result: {:?}", unexpected),
}; };
@@ -202,7 +290,7 @@ mod test {
file.write_all(invalid_magic_bytes.as_bytes()) file.write_all(invalid_magic_bytes.as_bytes())
.expect("should write"); .expect("should write");
match Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap()) { match Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()) {
Err(FileError::InvalidMagicBytes { got, .. }) => { Err(FileError::InvalidMagicBytes { got, .. }) => {
assert_eq!(got, invalid_magic_bytes.as_bytes()) assert_eq!(got, invalid_magic_bytes.as_bytes())
} }
@@ -221,8 +309,8 @@ mod test {
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
file.write_all(&data).expect("should write"); file.write_all(&data).expect("should write");
let mut store = Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap()) let mut store =
.expect("should open"); Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()).expect("should open");
match store.iter_changesets().next() { match store.iter_changesets().next() {
Some(Err(IterError::Bincode(_))) => {} Some(Err(IterError::Bincode(_))) => {}
unexpected_res => panic!("unexpected result: {:?}", unexpected_res), unexpected_res => panic!("unexpected result: {:?}", unexpected_res),

View File

@@ -1 +0,0 @@

View File

@@ -15,7 +15,7 @@ use bdk_chain::{
bitcoin::{Block, Transaction}, bitcoin::{Block, Transaction},
indexed_tx_graph, keychain, indexed_tx_graph, keychain,
local_chain::{self, CheckPoint, LocalChain}, local_chain::{self, CheckPoint, LocalChain},
ConfirmationTimeAnchor, IndexedTxGraph, ConfirmationTimeHeightAnchor, IndexedTxGraph,
}; };
use example_cli::{ use example_cli::{
anyhow, anyhow,
@@ -32,12 +32,12 @@ const CHANNEL_BOUND: usize = 10;
const STDOUT_PRINT_DELAY: Duration = Duration::from_secs(6); const STDOUT_PRINT_DELAY: Duration = Duration::from_secs(6);
/// Delay between mempool emissions. /// Delay between mempool emissions.
const MEMPOOL_EMIT_DELAY: Duration = Duration::from_secs(30); const MEMPOOL_EMIT_DELAY: Duration = Duration::from_secs(30);
/// Delay for committing to persistance. /// Delay for committing to persistence.
const DB_COMMIT_DELAY: Duration = Duration::from_secs(60); const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
type ChangeSet = ( type ChangeSet = (
local_chain::ChangeSet, local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<ConfirmationTimeAnchor, keychain::ChangeSet<Keychain>>, indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain::ChangeSet<Keychain>>,
); );
#[derive(Debug)] #[derive(Debug)]
@@ -131,7 +131,7 @@ fn main() -> anyhow::Result<()> {
start.elapsed().as_secs_f32() start.elapsed().as_secs_f32()
); );
let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)); let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)?);
println!( println!(
"[{:>10}s] loaded local chain from changeset", "[{:>10}s] loaded local chain from changeset",
start.elapsed().as_secs_f32() start.elapsed().as_secs_f32()
@@ -170,10 +170,7 @@ fn main() -> anyhow::Result<()> {
let chain_tip = chain.lock().unwrap().tip(); let chain_tip = chain.lock().unwrap().tip();
let rpc_client = rpc_args.new_client()?; let rpc_client = rpc_args.new_client()?;
let mut emitter = match chain_tip { let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
Some(cp) => Emitter::from_checkpoint(&rpc_client, cp),
None => Emitter::from_height(&rpc_client, fallback_height),
};
let mut last_db_commit = Instant::now(); let mut last_db_commit = Instant::now();
let mut last_print = Instant::now(); let mut last_print = Instant::now();
@@ -187,7 +184,7 @@ fn main() -> anyhow::Result<()> {
CheckPoint::from_header(&block.header, height).into_update(false); CheckPoint::from_header(&block.header, height).into_update(false);
let chain_changeset = chain let chain_changeset = chain
.apply_update(chain_update) .apply_update(chain_update)
.expect("must always apply as we recieve blocks in order from emitter"); .expect("must always apply as we receive blocks in order from emitter");
let graph_changeset = graph.apply_block_relevant(block, height); let graph_changeset = graph.apply_block_relevant(block, height);
db.stage((chain_changeset, graph_changeset)); db.stage((chain_changeset, graph_changeset));
@@ -196,7 +193,7 @@ fn main() -> anyhow::Result<()> {
last_db_commit = Instant::now(); last_db_commit = Instant::now();
db.commit()?; db.commit()?;
println!( println!(
"[{:>10}s] commited to db (took {}s)", "[{:>10}s] committed to db (took {}s)",
start.elapsed().as_secs_f32(), start.elapsed().as_secs_f32(),
last_db_commit.elapsed().as_secs_f32() last_db_commit.elapsed().as_secs_f32()
); );
@@ -205,23 +202,22 @@ fn main() -> anyhow::Result<()> {
// print synced-to height and current balance in intervals // print synced-to height and current balance in intervals
if last_print.elapsed() >= STDOUT_PRINT_DELAY { if last_print.elapsed() >= STDOUT_PRINT_DELAY {
last_print = Instant::now(); last_print = Instant::now();
if let Some(synced_to) = chain.tip() { let synced_to = chain.tip();
let balance = { let balance = {
graph.graph().balance( graph.graph().balance(
&*chain, &*chain,
synced_to.block_id(), synced_to.block_id(),
graph.index.outpoints().iter().cloned(), graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal, |(k, _), _| k == &Keychain::Internal,
) )
}; };
println!( println!(
"[{:>10}s] synced to {} @ {} | total: {} sats", "[{:>10}s] synced to {} @ {} | total: {} sats",
start.elapsed().as_secs_f32(), start.elapsed().as_secs_f32(),
synced_to.hash(), synced_to.hash(),
synced_to.height(), synced_to.height(),
balance.total() balance.total()
); );
}
} }
} }
@@ -253,10 +249,7 @@ fn main() -> anyhow::Result<()> {
let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND); let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND);
let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> { let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> {
let rpc_client = rpc_args.new_client()?; let rpc_client = rpc_args.new_client()?;
let mut emitter = match last_cp { let mut emitter = Emitter::new(&rpc_client, last_cp, fallback_height);
Some(cp) => Emitter::from_checkpoint(&rpc_client, cp),
None => Emitter::from_height(&rpc_client, fallback_height),
};
let mut block_count = rpc_client.get_block_count()? as u32; let mut block_count = rpc_client.get_block_count()? as u32;
tx.send(Emission::Tip(block_count))?; tx.send(Emission::Tip(block_count))?;
@@ -305,7 +298,7 @@ fn main() -> anyhow::Result<()> {
CheckPoint::from_header(&block.header, height).into_update(false); CheckPoint::from_header(&block.header, height).into_update(false);
let chain_changeset = chain let chain_changeset = chain
.apply_update(chain_update) .apply_update(chain_update)
.expect("must always apply as we recieve blocks in order from emitter"); .expect("must always apply as we receive blocks in order from emitter");
let graph_changeset = graph.apply_block_relevant(block, height); let graph_changeset = graph.apply_block_relevant(block, height);
(chain_changeset, graph_changeset) (chain_changeset, graph_changeset)
} }
@@ -327,7 +320,7 @@ fn main() -> anyhow::Result<()> {
last_db_commit = Instant::now(); last_db_commit = Instant::now();
db.commit()?; db.commit()?;
println!( println!(
"[{:>10}s] commited to db (took {}s)", "[{:>10}s] committed to db (took {}s)",
start.elapsed().as_secs_f32(), start.elapsed().as_secs_f32(),
last_db_commit.elapsed().as_secs_f32() last_db_commit.elapsed().as_secs_f32()
); );
@@ -335,24 +328,23 @@ fn main() -> anyhow::Result<()> {
if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY { if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY {
last_print = Some(Instant::now()); last_print = Some(Instant::now());
if let Some(synced_to) = chain.tip() { let synced_to = chain.tip();
let balance = { let balance = {
graph.graph().balance( graph.graph().balance(
&*chain, &*chain,
synced_to.block_id(), synced_to.block_id(),
graph.index.outpoints().iter().cloned(), graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal, |(k, _), _| k == &Keychain::Internal,
) )
}; };
println!( println!(
"[{:>10}s] synced to {} @ {} / {} | total: {} sats", "[{:>10}s] synced to {} @ {} / {} | total: {} sats",
start.elapsed().as_secs_f32(), start.elapsed().as_secs_f32(),
synced_to.hash(), synced_to.hash(),
synced_to.height(), synced_to.height(),
tip_height, tip_height,
balance.total() balance.total()
); );
}
} }
} }

View File

@@ -78,7 +78,7 @@ pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
#[clap(short, default_value = "bnb")] #[clap(short, default_value = "bnb")]
coin_select: CoinSelectionAlgo, coin_select: CoinSelectionAlgo,
#[clap(flatten)] #[clap(flatten)]
chain_specfic: S, chain_specific: S,
}, },
} }
@@ -315,10 +315,8 @@ where
version: 0x02, version: 0x02,
// because the temporary planning module does not support timelocks, we can use the chain // because the temporary planning module does not support timelocks, we can use the chain
// tip as the `lock_time` for anti-fee-sniping purposes // tip as the `lock_time` for anti-fee-sniping purposes
lock_time: chain lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
.get_chain_tip()? .expect("invalid height"),
.and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok())
.unwrap_or(absolute::LockTime::ZERO),
input: selected_txos input: selected_txos
.iter() .iter()
.map(|(_, utxo)| TxIn { .map(|(_, utxo)| TxIn {
@@ -404,7 +402,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
chain: &O, chain: &O,
assets: &bdk_tmp_plan::Assets<K>, assets: &bdk_tmp_plan::Assets<K>,
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> { ) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
let chain_tip = chain.get_chain_tip()?.unwrap_or_default(); let chain_tip = chain.get_chain_tip()?;
let outpoints = graph.index.outpoints().iter().cloned(); let outpoints = graph.index.outpoints().iter().cloned();
graph graph
.graph() .graph()
@@ -509,7 +507,7 @@ where
let balance = graph.graph().try_balance( let balance = graph.graph().try_balance(
chain, chain,
chain.get_chain_tip()?.unwrap_or_default(), chain.get_chain_tip()?,
graph.index.outpoints().iter().cloned(), graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal, |(k, _), _| k == &Keychain::Internal,
)?; )?;
@@ -539,7 +537,7 @@ where
Commands::TxOut { txout_cmd } => { Commands::TxOut { txout_cmd } => {
let graph = &*graph.lock().unwrap(); let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap(); let chain = &*chain.lock().unwrap();
let chain_tip = chain.get_chain_tip()?.unwrap_or_default(); let chain_tip = chain.get_chain_tip()?;
let outpoints = graph.index.outpoints().iter().cloned(); let outpoints = graph.index.outpoints().iter().cloned();
match txout_cmd { match txout_cmd {
@@ -587,7 +585,7 @@ where
value, value,
address, address,
coin_select, coin_select,
chain_specfic, chain_specific,
} => { } => {
let chain = &*chain.lock().unwrap(); let chain = &*chain.lock().unwrap();
let address = address.require_network(network)?; let address = address.require_network(network)?;
@@ -620,7 +618,7 @@ where
} }
}; };
match (broadcast)(chain_specfic, &transaction) { match (broadcast)(chain_specific, &transaction) {
Ok(_) => { Ok(_) => {
println!("Broadcasted Tx : {}", transaction.txid()); println!("Broadcasted Tx : {}", transaction.txid());
@@ -683,13 +681,13 @@ where
index.add_keychain(Keychain::Internal, internal_descriptor); index.add_keychain(Keychain::Internal, internal_descriptor);
} }
let mut db_backend = match Store::<'m, C>::new_from_path(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, Ok(db_backend) => db_backend,
// we cannot return `err` directly as it has lifetime `'m` // we cannot return `err` directly as it has lifetime `'m`
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)), Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
}; };
let init_changeset = db_backend.load_from_persistence()?; let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
Ok(( Ok((
args, args,

View File

@@ -112,7 +112,7 @@ fn main() -> anyhow::Result<()> {
graph graph
}); });
let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)); let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)?);
let electrum_cmd = match &args.command { let electrum_cmd = match &args.command {
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd, example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
@@ -193,7 +193,7 @@ fn main() -> anyhow::Result<()> {
// Get a short lock on the tracker to get the spks we're interested in // Get a short lock on the tracker to get the spks we're interested in
let graph = graph.lock().unwrap(); let graph = graph.lock().unwrap();
let chain = chain.lock().unwrap(); let chain = chain.lock().unwrap();
let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default(); let chain_tip = chain.tip().block_id();
if !(all_spks || unused_spks || utxos || unconfirmed) { if !(all_spks || unused_spks || utxos || unconfirmed) {
unused_spks = true; unused_spks = true;

View File

@@ -5,11 +5,11 @@ use std::{
}; };
use bdk_chain::{ use bdk_chain::{
bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid}, bitcoin::{constants::genesis_block, Address, Network, OutPoint, ScriptBuf, Txid},
indexed_tx_graph::{self, IndexedTxGraph}, indexed_tx_graph::{self, IndexedTxGraph},
keychain, keychain,
local_chain::{self, CheckPoint, LocalChain}, local_chain::{self, LocalChain},
Append, ConfirmationTimeAnchor, Append, ConfirmationTimeHeightAnchor,
}; };
use bdk_esplora::{esplora_client, EsploraExt}; use bdk_esplora::{esplora_client, EsploraExt};
@@ -25,7 +25,7 @@ const DB_PATH: &str = ".bdk_esplora_example.db";
type ChangeSet = ( type ChangeSet = (
local_chain::ChangeSet, local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<ConfirmationTimeAnchor, keychain::ChangeSet<Keychain>>, indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain::ChangeSet<Keychain>>,
); );
#[derive(Subcommand, Debug, Clone)] #[derive(Subcommand, Debug, Clone)]
@@ -102,9 +102,11 @@ fn main() -> anyhow::Result<()> {
let (args, keymap, index, db, init_changeset) = let (args, keymap, index, db, init_changeset) =
example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?; example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
let genesis_hash = genesis_block(args.network).block_hash();
let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset; let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset;
// Contruct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in // Construct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in
// `Mutex` to display how they can be used in a multithreaded context. Technically the mutexes // `Mutex` to display how they can be used in a multithreaded context. Technically the mutexes
// aren't strictly needed here. // aren't strictly needed here.
let graph = Mutex::new({ let graph = Mutex::new({
@@ -113,8 +115,8 @@ fn main() -> anyhow::Result<()> {
graph graph
}); });
let chain = Mutex::new({ let chain = Mutex::new({
let mut chain = LocalChain::default(); let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
chain.apply_changeset(&init_chain_changeset); chain.apply_changeset(&init_chain_changeset)?;
chain chain
}); });
@@ -234,7 +236,7 @@ fn main() -> anyhow::Result<()> {
{ {
let graph = graph.lock().unwrap(); let graph = graph.lock().unwrap();
let chain = chain.lock().unwrap(); let chain = chain.lock().unwrap();
let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default(); let chain_tip = chain.tip().block_id();
if *all_spks { if *all_spks {
let all_spks = graph let all_spks = graph
@@ -332,7 +334,7 @@ fn main() -> anyhow::Result<()> {
(missing_block_heights, tip) (missing_block_heights, tip)
}; };
println!("prev tip: {}", tip.as_ref().map_or(0, CheckPoint::height)); println!("prev tip: {}", tip.height());
println!("missing block heights: {:?}", missing_block_heights); println!("missing block heights: {:?}", missing_block_heights);
// Here, we actually fetch the missing blocks and create a `local_chain::Update`. // Here, we actually fetch the missing blocks and create a `local_chain::Update`.

View File

@@ -7,3 +7,4 @@ edition = "2021"
bdk = { path = "../../crates/bdk" } bdk = { path = "../../crates/bdk" }
bdk_electrum = { path = "../../crates/electrum" } bdk_electrum = { path = "../../crates/electrum" }
bdk_file_store = { path = "../../crates/file_store" } bdk_file_store = { path = "../../crates/file_store" }
anyhow = "1"

View File

@@ -16,20 +16,20 @@ use bdk_electrum::{
}; };
use bdk_file_store::Store; use bdk_file_store::Store;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), anyhow::Error> {
let db_path = std::env::temp_dir().join("bdk-electrum-example"); let db_path = std::env::temp_dir().join("bdk-electrum-example");
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?; let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
let mut wallet = Wallet::new( let mut wallet = Wallet::new_or_load(
external_descriptor, external_descriptor,
Some(internal_descriptor), Some(internal_descriptor),
db, db,
Network::Testnet, Network::Testnet,
)?; )?;
let address = wallet.get_address(bdk::wallet::AddressIndex::New); let address = wallet.try_get_address(bdk::wallet::AddressIndex::New)?;
println!("Generated Address: {}", address); println!("Generated Address: {}", address);
let balance = wallet.get_balance(); let balance = wallet.get_balance();

View File

@@ -10,3 +10,4 @@ bdk = { path = "../../crates/bdk" }
bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] } bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
bdk_file_store = { path = "../../crates/file_store" } bdk_file_store = { path = "../../crates/file_store" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
anyhow = "1"

View File

@@ -14,20 +14,20 @@ const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5; const PARALLEL_REQUESTS: usize = 5;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), anyhow::Error> {
let db_path = std::env::temp_dir().join("bdk-esplora-async-example"); let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?; let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
let mut wallet = Wallet::new( let mut wallet = Wallet::new_or_load(
external_descriptor, external_descriptor,
Some(internal_descriptor), Some(internal_descriptor),
db, db,
Network::Testnet, Network::Testnet,
)?; )?;
let address = wallet.get_address(AddressIndex::New); let address = wallet.try_get_address(AddressIndex::New)?;
println!("Generated Address: {}", address); println!("Generated Address: {}", address);
let balance = wallet.get_balance(); let balance = wallet.get_balance();

View File

@@ -10,3 +10,4 @@ publish = false
bdk = { path = "../../crates/bdk" } bdk = { path = "../../crates/bdk" }
bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] } bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
bdk_file_store = { path = "../../crates/file_store" } bdk_file_store = { path = "../../crates/file_store" }
anyhow = "1"

View File

@@ -13,20 +13,20 @@ use bdk::{
use bdk_esplora::{esplora_client, EsploraExt}; use bdk_esplora::{esplora_client, EsploraExt};
use bdk_file_store::Store; use bdk_file_store::Store;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), anyhow::Error> {
let db_path = std::env::temp_dir().join("bdk-esplora-example"); let db_path = std::env::temp_dir().join("bdk-esplora-example");
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?; let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
let mut wallet = Wallet::new( let mut wallet = Wallet::new_or_load(
external_descriptor, external_descriptor,
Some(internal_descriptor), Some(internal_descriptor),
db, db,
Network::Testnet, Network::Testnet,
)?; )?;
let address = wallet.get_address(AddressIndex::New); let address = wallet.try_get_address(AddressIndex::New)?;
println!("Generated Address: {}", address); println!("Generated Address: {}", address);
let balance = wallet.get_balance(); let balance = wallet.get_balance();

View File

@@ -315,7 +315,7 @@ where
self.set_sequence.clone() self.set_sequence.clone()
} }
/// The minmum required transaction version required on the transaction using the plan. /// The minimum required transaction version required on the transaction using the plan.
pub fn min_version(&self) -> Option<u32> { pub fn min_version(&self) -> Option<u32> {
if let Some(_) = self.set_sequence { if let Some(_) = self.set_sequence {
Some(2) Some(2)