Skip to content

Conversation

ValuedMammal
Copy link
Collaborator

@ValuedMammal ValuedMammal commented Jun 7, 2025

This PR adds methods to Wallet to lock and unlock a UTXO by outpoint, to query the locked outpoints, and updates the wallet ChangeSet to persist the lock status of an outpoint.

fixes #166
fixes #245

Considerations for broadcast scenarios:

When a UTXO is locked, it won't be selected for a transaction. This is useful in cases where the user needs to permanently exclude an output from being spent, or reserve it until some time in the future and continue making transactions in the meantime. To eventually spend the coin the user has to explicitly unlock it. This process could perhaps be facilitated by implementing an automatic lock expiry based on block height.

If you submit a transaction to the network and then lock the inputs, then they won't be re-selected in subsequent transactions. This prevents us from inadvertently "double-spending" ourselves. Once broadcast, the lock effect depends on the fate of the transaction: If it confirms and the outpoint is still locked, then the lock is no longer useful, because the input can't be double spent anyway by virtue of consensus. If a conflict confirms which spends the same output, the lock is also irrelevant. If a conflict confirms but doesn't spend the already locked output then the lock remains in effect.

If the transaction is somehow dropped from the mempool but otherwise still valid, we can either rebroadcast the tx or create a replacement. As far as I can tell the implementation doesn't prevent us from creating a Replace-By-Fee transaction regardless of the lock status, since the inputs to the RBF are considered "manually selected".

To avoid the risk of double payment (sending two txs to the same payment invoice) one needs to examine the details of the second transaction to ensure that it actually replaces the first one, otherwise we've simply made a donation to our counterparty.

Changelog notice

Added these methods to `Wallet`

    - `lock_outpoint`
    - `unlock_outpoint`
    - `list_locked_outpoints`
    - `list_locked_unspent`
    - `is_outpoint_locked`

Added

   - New type `wallet::locked_outpoints::ChangeSet`
   - BREAKING: New member field `wallet::ChangeSet::locked_outpoints`

Checklists

All Submissions:

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • This pull request breaks the existing API
  • I'm linking the issue being fixed by this PR

@ValuedMammal ValuedMammal moved this to Todo in BDK Wallet Jun 7, 2025
@ValuedMammal ValuedMammal added this to the Wallet 3.0.0 milestone Jun 7, 2025
@ValuedMammal ValuedMammal self-assigned this Jun 7, 2025
@coveralls
Copy link

coveralls commented Jun 7, 2025

Pull Request Test Coverage Report for Build 17143814147

Details

  • 102 of 122 (83.61%) changed or added relevant lines in 4 files are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage increased (+0.02%) to 84.936%

Changes Missing Coverage Covered Lines Changed/Added Lines %
wallet/src/wallet/changeset.rs 46 50 92.0%
wallet/src/wallet/persisted.rs 0 6 0.0%
wallet/src/wallet/mod.rs 50 60 83.33%
Files with Coverage Reduction New Missed Lines %
wallet/src/wallet/mod.rs 1 82.02%
Totals Coverage Status
Change from base Build 17132535147: 0.02%
Covered Lines: 6766
Relevant Lines: 7966

💛 - Coveralls

@ChidiChuks
Copy link

How do you handle UTXO locking in concurrent wallet instances?

Copy link
Contributor

@nymius nymius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand a little bit more on how this mechanism could be used in broadcast scenarios?
From the test I guess all the logic of why a UTxO becomes available again is kept by the lib user? Like unlock UTxO after a height in the future if there is not transaction spending it in the blockchain.

This is a competing solution for #257, right?

@evanlinjin
Copy link
Member

How do you handle UTXO locking in concurrent wallet instances?

@ChidiChuks could you be a bit more specific with this question? Do you mean sharing lock status between multiple instances of the same wallet?

@evanlinjin
Copy link
Member

This is a competing solution for #257, right?

@nymius My understanding is that this is NOT a competing solution with #257. UTXO-locking will not fully solve #166 as we want unbroadcasted outputs to be available for selection. In fact, UTXO-locking is already available by using TxBuilder::add_unspendable. The only difference here is that locked outpoints are persisted with BDK. So this is more of a convenience feature that users are requesting.

Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this work.

I propose adding the following methods:

  • list_locked_outpoints - Lists all locked outpoints.
  • list_locked_unspents - Lists locked outpoints that are unspent.

Imagine a situation where the caller broadcasts a tx and locks the prevouts. Then the tx gets evicted from the mempool. It will be helpful to list locked unspents at this point to recover those prevouts.

@notmandatory notmandatory mentioned this pull request Jun 12, 2025
14 tasks
@ValuedMammal ValuedMammal marked this pull request as ready for review June 16, 2025 17:29
@ValuedMammal ValuedMammal moved this from Todo to Needs Review in BDK Wallet Jun 16, 2025
@ValuedMammal
Copy link
Collaborator Author

ValuedMammal commented Jun 16, 2025

@evanlinjin I took all of your suggestions other than I think the proposed Wallet::list_locked_unspents is not yet implemented. edit: I have some ideas for this though.

@ValuedMammal ValuedMammal moved this from Needs Review to In Progress in BDK Wallet Jun 18, 2025
@ValuedMammal ValuedMammal moved this from In Progress to Needs Review in BDK Wallet Jun 23, 2025
Copy link
Member

@notmandatory notmandatory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to see this feature moving along. In addition to helping L2 users like @tnull it also helps support one of my personal goals of improving feature parity with the bitcoin core wallet.

@notmandatory notmandatory added new feature New feature or request api A breaking API change labels Jun 25, 2025
Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation looks good. Just need a bit more clarity on expiration_height before ACK.

@ValuedMammal ValuedMammal moved this from Needs Review to In Progress in BDK Wallet Jun 27, 2025
@ValuedMammal ValuedMammal force-pushed the feat/lock-unspent branch 2 times, most recently from 37fac7d to ee0a88b Compare July 14, 2025 17:26
Comment on lines 2686 to 2693
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub struct UtxoLock {
/// Outpoint.
pub outpoint: OutPoint,
/// Whether the outpoint is locked.
pub is_locked: bool,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can get away with removing this for now.

  • Internally, we can use locked_outpoints: Map<OutPoint, Option<bool>>.
  • We can have list_locked_outpoints() -> Iterator<OutPoint> instead of locked_outpoints() -> Map<OutPoint, UtxoLock>.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

@ValuedMammal ValuedMammal force-pushed the feat/lock-unspent branch 2 times, most recently from 892ffd0 to a6381de Compare August 3, 2025 22:00
@ValuedMammal ValuedMammal moved this from In Progress to Needs Review in BDK Wallet Aug 13, 2025
New APIs added for locking and unlocking a UTXO by outpoint
and to query the locked outpoints. Locking an outpoint
means that it is excluded from coin selection.

- Add `Wallet::lock_outpoint`
- Add `Wallet::unlock_outpoint`
- Add `Wallet::is_outpoint_locked`
- Add `Wallet::list_locked_outpoints`
- Add `Wallet::list_locked_unspent`

`test_lock_outpoint_persist` tests the lock/unlock
functionality and that the lock status is persistent.

BREAKING: Added `locked_outpoints` member field to ChangeSet.
A SQLite migration is included for adding the locked outpoints table.
..by `Box`ing the descriptor in `LoadMismatch` enum,
and by boxing the ChangeSet in `DataAlreadyExists` variant of
`CreateWithPersistError`.

We allow the large_enum_variant lint for `FileStoreError` for now,
as it is planned to be fixed in a future version of `bdk_file_store`.
The example crates contain code that isn't accepted by the
1.63.0 version of the compiler (e.g. `is_multiple_of`).

Since the MSRV of the wallet library is currently 1.63.0, and
clippy is configured to adhere to that, we exclude the example
crates from the clippy check enforced by CI.
@@ -109,6 +110,7 @@ pub struct Wallet {
stage: ChangeSet,
network: Network,
secp: SecpCtx,
locked_outpoints: BTreeMap<OutPoint, Option<bool>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would using a HashSet<OutPoint> here be more appropriate?

  • I don't think ordering is important when returning OutPoints.
  • Option<false> and None don’t seem to be distinguished in the current logic.

@evanlinjin
Copy link
Member

Other than my comment above, everything looks good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api A breaking API change new feature New feature or request
Projects
Status: Needs Review
Development

Successfully merging this pull request may close these issues.

ci: fix large enum and error types to clippy recommended sizes Let TxBuilder avoid previously used UTXOs (UTXO locking)
7 participants