-
Notifications
You must be signed in to change notification settings - Fork 307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement utxo reservation #913
base: master
Are you sure you want to change the base?
Implement utxo reservation #913
Conversation
When creating transactions sequentially, this change locks UTXOs once used by a transaction, so that it can't be used as input by a following transaction. These UTXOs are freed once the transaction is cancelled or broadcasted.
Concept ACK. I also suggested that in response to #849. Can you change the PR to only modify bdk::Wallet? |
Thanks @LLFourn for the feedback! Let me work on that update. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK, thanks for working on this! I think I'll do something similar to what you did with include_reserved_utxos
in the new TxBuilder.
I left some comments regarding the Wallet API - I'm worried that by exposing include_reserved_utxos
frequently we might make the API harder to use, without a real benefit.
Also, should we have some method on Wallet
to query and modify the reserved utxos?
@@ -388,9 +388,9 @@ impl<D> Wallet<D> { | |||
|
|||
/// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the | |||
/// wallet's database. | |||
pub fn get_utxo(&self, op: OutPoint) -> Option<LocalUtxo> { | |||
pub fn get_utxo(&self, op: OutPoint, include_reserved_utxos: bool) -> Option<LocalUtxo> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should have a include_reserved_utxos
parameter here... If I want bdk to retrieve a specific outpoint, I usually don't care if it's reserved or not.
I would just call full_utxos(true)
a couple of lines below and that's it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
KeychainKind::External => false, | ||
KeychainKind::Internal => true, | ||
}) | ||
pub fn get_balance(&self, include_reserved_utxos: bool) -> Balance { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly to above, when users ask for a balance I suppose they always want the reserved utxos anyways
@@ -274,12 +276,20 @@ 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 | |||
/// 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, if you want to manually add utxos you probably don't care whether they're unlocked or not.
Unless... we might want to return a specific error (not UnknownUtxo
, a more detailed one, like LockedUtxo
) if you try to add a locked utxo to a txbuilder that doesn't have include_reserved_utxos
. I'm not sure exactly what's the way forward here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will prefer creating an error like LockedUtxo
. Make more sense to me.
@danielabrozzoni, what do you think the potential use cases will be for adding an API for querying and modifying the locked utxos? |
Think about a scenario where there are multiple bdk wallets able to communicate between each other, each one of them crafting transactions and not broadcasting them immediately. In this case you don't want to have conflicting txs and you want to manually tell each wallet "also lock this specific utxo". But it's a rather exotic scenario so we don't have to implement it right now. |
I could see this useful for things you queue up. I could see using this when a user queues some utxos for a coinjoin, until that coinjoin happens those utxos should be locked, so if they go to send those coins should be locked in the coin selection screen. Being able to see which utxos are locked would be important for that. |
This would be nice in the case a payjoin fails because someone goes offline and the receiver bans their utxo (to prevent probing), the signing algorithm could automatically pick a utxo outside of the reserved set. |
I'm curious what the status of this PR is. Tracking and expiring intermediate but unconfirmed payjoin states (pending original psbt vs augmented payjoin psbt) requires locking. Otherwise subsequent sign attempts spend outputs of pending payjoin state that may never end up confirmed on chain. @vladimirfomene are you still invested in seeing this PR completed? Would either of you like a hand or consider my contributions to get this reservation issue fixed? |
@DanGould reconsidering the problem here. Is there something wrong with just using: Also you could consider just adding the transaction to the wallet with |
Edit: I believe I've been able to fix this problem by having payjoin only contribute confirmed inputs. This is a slight degradation vs being able spend unconfirmed-but-seen-in-mempool transactions but it works without reservation for now. Please bear with me as I outline in detail what I'm trying to do and where I've run into the snag. I'm trying to watch unconfirmed, pending transactions that one of the two payjoin parties have signed but which may never be broadcast, and are out of the one party's control to broadcast. I am running BDK 1.0.0-alpha.5. Payjoin SenderThe payjoin sender first signs an Original PSBT that's a typical transaction sending funds to the receiver. It wants to add this transaction to its wallet because, even though it never plans on broadcasting this TX, if for some reason the payjoin session times out, the receiver may extract and broadcast this original fallback transaction, making the sender's original tx change spendable. If the sender receives a payjoin PSBT with a signed receiver input, it can sign and broadcast the payjoin and remove the Original tx from its wallet because those funds have already been spent in the payjoin. Payjoin ReceiverThe payjoin receiver augments an incoming original PSBT with its input to create a payjoin PSBT missing the sender's signature, since the sender's original PSBT sighash_all signature becomes invalid when the transaction is augmented. The receiver adds this payjoin transaction to its wallet, sends it to the sender to receive a signature, and waits to see it broadcast. If that transaction never gets signed, the receiver may choose to broadcast the original PSBT tx if it's an automated payment processor or otherwise let the payjoin session expire. During the time this payjoin PSBT in the hands of the sender in the wild and not considered expired nor seen in mempool, the receiver shouldn't try to create more transactions spending the same input as the payjoin PSBT nor spend change outputs from the payjoin PSBT since they might never be confirmed. I would understand these to be 'reserved' utxos. I wasn't considering `list_transactions` implementationpub fn list_transactions(
&self,
include_raw: bool,
) -> Result<Vec<TransactionDetails>, MutinyError> {
if let Ok(wallet) = self.wallet.try_read() {
let txs = wallet
.transactions()
.filter_map(|tx| {
// skip txs that were not relevant to our bdk wallet
if wallet.spk_index().is_tx_relevant(tx.tx_node.tx) {
let (sent, received) = wallet.spk_index().sent_and_received(tx.tx_node.tx);
let transaction = if include_raw {
Some(tx.tx_node.tx.clone())
} else {
None
};
// todo bdk is making an easy function for this
// calculate fee if possible
let inputs = tx
.tx_node
.tx
.input
.iter()
.map(|txin| {
wallet
.spk_index()
.txout(txin.previous_output)
.map(|(_, _, txout)| txout.value)
})
.sum::<Option<u64>>();
let outputs = tx.tx_node.tx.output.iter().map(|txout| txout.value).sum();
let fee = inputs.map(|inputs| inputs.saturating_sub(outputs));
Some(TransactionDetails {
transaction,
txid: tx.tx_node.txid,
received,
sent,
fee,
confirmation_time: tx.chain_position.cloned().into(),
labels: vec![],
})
} else {
None
}
})
.collect();
return Ok(txs);
}
log_error!(
self.logger,
"Could not get wallet lock to list transactions"
);
Err(MutinyError::WalletOperationFailed)
} My current attempt (one commit in a PR) has the sender Similarly, the receiver also calls However, I'm running into a problem where subsequent attempts at receiving payjoin try to spend the output of the prior payjoin tx in a new payjoin. That prior payjoin change output has never been broadcast, so the sender rejects it because it does not exist from the sender perspective. My understanding is that if the payjoin tx input and output was marked as unspendable or "reserved," then it would NOT appear as a (here's the original referenced commit in a branch separate from the PR in case that PR is updated) |
@DanGould thanks for the context.
The feature this PR is aimed at providing was more about stopping multiple threads from creating transactions that spend the same utxos. The plan wasn't to persist this list of reserved utxos (just have them in memory). It was just there to "lock" the utxo in the short time between a transaction being created and it being confirmed to broadcast. This might be useful to you too though. I think the thing to keep in mind is that we don't want to persist these lists of reserved utxos if you want that you'll have to do it at the application level. I'd hesitate to encourage you to take on this PR as I there could be quite some bikesheading around what to do with the balance, when to unreserve utxos etc. Only take it on if you have significant time to dedicate to figuring out the right answers to these (and other) questions. |
Description
This PR solves issue #849. The current design keeps a set of UTXOs that have been reserved by transactions. UTXOs are released when a transaction is canceled. I'm currently thinking it might also make sense to release UTXOs once a transaction gets broadcasted (this idea is not implemented in this PR).
Notes to the reviewers
With the new
ChainOracle
design, it no longer makes sense to have this as part of the keychain tracker. It might make sense to have this inside the bdk wallet.Changelog notice
Checklists
All Submissions:
cargo fmt
andcargo clippy
before committingNew Features:
Bugfixes: