-
Notifications
You must be signed in to change notification settings - Fork 64
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
Apply coin selection for spend #560
Conversation
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 need to look more into the tradeoffs of various coin selection algorithms to review the strategy used here, but the approach of selecting coins if none are passed in createspend
looks good to me.
src/commands/mod.rs
Outdated
/// Select coins for spend. | ||
pub fn select_coins_for_spend( |
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 know you've made it a command to test, but in the end i think it should be a helper. Probably in src/commands/utils.
src/commands/mod.rs
Outdated
&self, | ||
spend_amount: bitcoin::Amount, | ||
spend_feerate_vb: u64, | ||
long_term_feerate_vb: u64, |
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.
We should probably make this a constant.
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.
That sounds good. If I understand well, bitcoin core uses a value based on the config option -consolidatefeerate
, which has a default value of 10 sat/vbyte:
- https://github.com/bitcoin/bitcoin/blob/master/src/wallet/wallet.h#L87
- https://github.com/bitcoin/bitcoin/blob/master/src/wallet/spend.cpp#L842
We could follow a similar approach, giving the user the option to set their own value if they wish. I think using "consolidation" instead of "long-term" in the name would make it easier to reason about for the user when choosing a value.
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.
Another option is to use a rolling average of the feerate of the user's previous transactions. I think this will lead to reasonable estimates about whether the current feerate is higher and lower than what they usually experience. Of course you need to have a reasonable sample size over enough time to have good results.
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.
Good point. @jp1ac4 let's stick with a constant for now (we'll need one as a fallback anyways), we can implement what Lloyd suggests in a follow-up.
src/commands/mod.rs
Outdated
|
||
let cs = CoinSelector::new(&candidates, base_weight); | ||
// TODO: use our own custom metric function. | ||
let solutions = cs.branch_and_bound(Waste { |
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.
Using the waste metric for coin selection could lead to huge consolidations. See for instance this comment from upstream (and the linked stackexchange discussion of the algorithm): https://github.com/bitcoindevkit/bdk/pull/924/files#diff-f1378f655c539699f8c398a3b3a11d02c939875b3e2d9fdb2a9685b9202fd70fR4-R18.
We should probably do some consolidation for Liana but i'm afraid using only the waste metric could lead to unreasonable selections.
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.
That's true, especially when the long-term feerate is higher than the spend feerate. I'll give it further thought.
As an additional safeguard, for now at least, we could let the user in the GUI edit the list of automatically selected coins when creating a new spend transaction.
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.
Yes I still think that waste by itself is a pretty bad default. To get this effect of "some consolidation" I like the idea of combining waste with a "minimize fee" metric that takes into account the fee paid when spending a change output as part of the cost. I'll try and add this weighted combination feature. I expect that finding via branch and bound a weighted combination of two metrics will be less efficient in practice because the lower bound estimation will be too loose. In other words the combined estimator may predict there will be a low waste and a low fee on some child branch but these local minimums will occur at different selections and so there really is no selection that has a low waste and low fee. This will cause the BnB algorithm to end up exploring a lot of child branches unnecessarily. At worst this degenerates to exhaustive search but hopefully it won't be too bad.
The other step is of course to add the minimize fee metric which is by itself a good default.
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.
Thanks for trying out my bdk coin selection work. It was really helpful to see what was unclear for improving and documenting the API. Cheers! I left some comments.
src/commands/mod.rs
Outdated
.enumerate() | ||
.filter_map(|(i, sol)| Some((i, sol?))) | ||
.last() | ||
.expect("it should have found solution"); |
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 you necessarily know that it would find a solution here? What if you don't have enough coins.
Also for a large enough number of inputs this could take forever (literally) to return (especially if there is no solution) so you should always .take(100_000)
or something on this iterator so that it terminates after 100_000
iterations or so.
I think I should add an explicit API for doing:
- Run this X amount of times and give me the best solution
- If no best solution according to metric just select coins until satisfied
- otherwise return error with the amount of sats we were missing.
src/commands/mod.rs
Outdated
|
||
let cs = CoinSelector::new(&candidates, base_weight); | ||
// TODO: use our own custom metric function. | ||
let solutions = cs.branch_and_bound(Waste { |
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.
Yes I still think that waste by itself is a pretty bad default. To get this effect of "some consolidation" I like the idea of combining waste with a "minimize fee" metric that takes into account the fee paid when spending a change output as part of the cost. I'll try and add this weighted combination feature. I expect that finding via branch and bound a weighted combination of two metrics will be less efficient in practice because the lower bound estimation will be too loose. In other words the combined estimator may predict there will be a low waste and a low fee on some child branch but these local minimums will occur at different selections and so there really is no selection that has a low waste and low fee. This will cause the BnB algorithm to end up exploring a lot of child branches unnecessarily. At worst this degenerates to exhaustive search but hopefully it won't be too bad.
The other step is of course to add the minimize fee metric which is by itself a good default.
src/commands/mod.rs
Outdated
|
||
// TODO: check what weights we need to add to `TXIN_BASE_WEIGHT` to give this candidate's weight | ||
let added_weight = | ||
coin_desc.witness_script().len() + (coin_desc.script_pubkey().len() * 4); |
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.
This should be the satisfaction weight i.e. witness weight. Miniscript can calculate the max_weight_to_satisfy
which should work well. We're working on a more fine grained way of estimating satisfaction weight in rust-bitcoin/rust-miniscript#481 rather than just using the maximum (e.g. tr key path spends will be much cheaper than particular leaves so using the max can overshoot badly in some cases).
src/commands/mod.rs
Outdated
&self, | ||
spend_amount: bitcoin::Amount, | ||
spend_feerate_vb: u64, | ||
long_term_feerate_vb: u64, |
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.
Another option is to use a rolling average of the feerate of the user's previous transactions. I think this will lead to reasonable estimates about whether the current feerate is higher and lower than what they usually experience. Of course you need to have a reasonable sample size over enough time to have good results.
src/commands/mod.rs
Outdated
let drain = Drain { | ||
weight: TXOUT_BASE_WEIGHT, | ||
spend_weight: TXIN_BASE_WEIGHT, | ||
value: 0, // value is ignored when using `change_policy::min_value` below |
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.
We probably should have a version of Drain
like DrainWeight
to avoid having to make comments like these.
src/commands/mod.rs
Outdated
}; | ||
// Ensure any change output is not too small. | ||
let change_policy = change_policy::min_value(drain, DUST_OUTPUT_SATS); | ||
// TODO: is this the correct `base_weight` for `CoinSelector`? |
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.
You need to add the outputs.
We should probably add a fund_outputs(&[(weight, value))
constructor which should be used by default where you pass in the weights of the outputs you are trying to fund and their values so you don't have to construct an actual bitcoin::Transaction
.
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.
Ah yes thanks, I was thinking only about change outputs and forgot about including the non-change outputs :)
We should probably add a
fund_outputs(&[(weight, value))
constructor which should be used by default where you pass in the weights of the outputs you are trying to fund and their values so you don't have to construct an actualbitcoin::Transaction
.
That sounds good. I'll look out for this in your PR if you have a chance to include it.
src/commands/mod.rs
Outdated
let target = Target { | ||
value: spend_amount.to_sat(), | ||
feerate, | ||
min_fee: 0, // TODO: Perhaps for RBF `min_fee` will need to be passed as argument to ensure absolute fee of replacement is high enough. |
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.
Indeed. This is one of the main reasons this exists on Target
.
Hi @LLFourn, thanks very much for looking through the changes and adding comments. I really appreciate it. I'll go through those and reply as required. |
src/commands/mod.rs
Outdated
let final_coins_outpoints: &[bitcoin::OutPoint] = if coins_outpoints.is_empty() { | ||
log::info!("No outpoints specified. Selecting coins..."); | ||
let candidate_coins: Vec<Vec<Coin>> = db_conn | ||
// TODO: Will replacement transactions select coins here or elsewhere? |
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.
A possibility for replacement transactions is to select all confirmed unspent coins and then to add the coins spent by the to-be-replaced transaction to the vector.
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've removed this comment for now as I will leave RBF considerations for a separate PR.
candidate_coins: Vec<Vec<Coin>>, // Each inner vector of coins should be spent together | ||
destinations: &HashMap<bitcoin::Address, u64>, | ||
feerate_vb: u64, | ||
min_fee: u64, |
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.
Since it's optional (only used for RBF AFAICT) it should be an Option
instead of setting it to 0.
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.
For now I've removed this parameter completely as I'm still not sure how the RBF interface will work and so will focus on non-replacement transactions 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.
Oops, I just added min_fee
again and this comment re-appeared 😄
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.
Would you prefer this to be Option
still?
src/commands/utils.rs
Outdated
} | ||
let max_input_weight = | ||
TXIN_BASE_WEIGHT + control.config.main_descriptor.max_sat_weight() as u32; | ||
// TODO: more accurate input weight estimate using https://github.com/rust-bitcoin/rust-miniscript/pull/481 |
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 it should be a TODO, as it's pretty far down the road and quite unclear how we'd use 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.
That's true, I've removed it now.
src/commands/utils.rs
Outdated
input_count: coins.len(), | ||
value: coins.iter().map(|c| c.amount.to_sat()).sum(), | ||
weight: max_input_weight * coins.len() as u32, | ||
is_segwit: true, // TODO: can we assume this or need to check for each coin? |
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.
Yes, Liana only ever supports receiving on Segwit scripts.
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.
Perfect thanks, I've updated the comment.
src/commands/utils.rs
Outdated
// Transaction base weight calculated from transaction with no inputs | ||
let base_weight = bitcoin::Transaction { | ||
input: Vec::new(), | ||
output: destinations | ||
.iter() | ||
.map(|(address, value)| bitcoin::TxOut { | ||
value: *value, | ||
script_pubkey: address.script_pubkey(), | ||
}) | ||
.collect(), | ||
lock_time: bitcoin::PackedLockTime(0), | ||
version: 2, | ||
} | ||
.weight() as u32; |
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.
This would not account for the Segwit marker flag nor the size of the witnesses. But the CoinSelector
is taking care of this. May be worth a comment as i had to double check it does take care of 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.
Ah thanks, I've added a comment.
src/commands/utils.rs
Outdated
.take(100_000); // Ensure iterator does not take too long or run forever, e.g. if there are no solutions. | ||
|
||
// TODO: use explicit API to handle different cases of solution being found (https://github.com/wizardsardine/liana/pull/560#discussion_r1236232805) | ||
if let Some(Some(best)) = solutions.filter(|sol| sol.is_some()).last() { |
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.
Any reason for taking the last? I guess it doesn't matter if the API gets reworked anyways.
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 think this should correspond to the solution with best metric score, as the iterator only returns solutions that give a lower score than the best score so far and otherwise returns Some(None)
. But we can check again once the reworked API is ready.
src/commands/utils.rs
Outdated
|
||
/// Select coins for spend. | ||
pub fn select_coins_for_spend( | ||
control: &DaemonControl, |
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.
This is a bit ugly to pass a reference to the caller here, i'd say just pass in the maximum satisfaction weight which is the only thing you use it for anyways.
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 agree and have now changed this.
Hi @darosior, regarding GUI integration, I think there are two options:
Did you have in mind one of these options? |
Hey, yes. Thanks for bringing that up. I think we should always auto-select coins and the user can manually opt to select different ones. I don't think this needs another command, we might as well reuse Also, you probably want to work on the GUI integration in another PR. So you can update the |
Yep that makes sense thanks. I agree that it's better to give the user the option to edit the selection. And it's true that a separate command shouldn't be required as we can determine the selected coins from the return value of
Oh yes good point, I'll add GUI changes in a separate branch and PR. |
I've added a basic proof of concept for the GUI in https://github.com/jp1ac4/liana/tree/add-auto-coin-selection-to-gui. As the approach discussed above seems like it should work, I'll leave the GUI changes for now and will focus on adding tests to this current PR. |
Regarding the failure in the Rust v1.48 CI i've just merged #561 which bumps our MSRV from 1.48 to 1.54. I guess it could help. |
d714cea
to
b561dd6
Compare
Thanks for that. I've rebased on master (and took the chance to squash my commits as I was getting a conflict otherwise from an intermediate change) and all checks are now passing 💯 |
I created a couple conflicts by upgrading rust-miniscript and rust-jsonrpc, but they should be trivial. |
337515e
to
7a78f00
Compare
I've now rebased on master and resolved the conflicts. |
Thanks. For anyone following along: i discussed it with @jp1ac4 elsewhere and we decided to put this on hold until upstream is merged. Efforts will be moved elsewhere for now but we'll get back to this eventually. |
Thanks the effort here was really useful for me to learn some strengths/weaknesses of the API. |
7a78f00
to
c3b85b1
Compare
I've updated my previous changes based on the changes in bitcoindevkit/bdk#1072. Now, the As well as any general refactoring and improvements, I still need to add tests. |
c3b85b1
to
92cef7b
Compare
176859e commands: add weights before converting to vbytes (jp1ac4) Pull request description: The `sanity_check_psbt()` function checks, among other things, that the feerate in sats/vbytes is in the required range. When estimating the transaction size, converting each part to vbytes first can lead to a larger result due to rounding multiple times and therefore a lower feerate, which could fall below 1 and fail the sanity check. This issue has arisen in #560 when creating a transaction using coin selection. ACKs for top commit: darosior: utACK 176859e -- great catch Tree-SHA512: 5e4fc795c5977c71a0f02b21c09cd71d59c9412f94bcafd5a4adb429c4ae3a568a25c4174bd305e0be927f06ae4e9be9aef791ffbbee2a7ac1c40740f639ab10
704d824
to
243e044
Compare
This is now ready for an initial review. I've moved some existing code in |
243e044
to
1fcb332
Compare
Rebased on master. |
src/commands/mod.rs
Outdated
let final_coins_outpoints: &[bitcoin::OutPoint] = if use_coin_selection { | ||
log::info!("No outpoints specified. Selecting coins..."); | ||
let candidate_coins: Vec<Coin> = db_conn | ||
.coins(&[CoinStatus::Confirmed], &[]) |
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 think I should also include CoinStatus::Unconfirmed
here to be consistent with the coins available for selection in the GUI.
Regarding considering unconfirmed coins. Considering unconfirmed coins in the selection makes things more difficult. Now we need to pay fees for the unconfirmed parent as well, which also affects the cost of including an input. And we need to be careful not to create a too large unconfirmed chain of transactions. Also unconfirmed coins shouldn't be introduced in a replacement transaction once we do RBF. I don't think we should introduce the complexity of correctly dealing with unconfirmed transactions just yet. This leaves us with two imperfect possibilities:
I'm in favour of the latter, but i'm happy to reconsider. |
Ah I see, I'm also happy with the latter approach in that case. We can consider any extra information to give the user as part of #563. |
This may not be a problem, but I've found that when a transaction will give a change amount close to the dust threshold of 5000 sats, there can be a difference between the transaction created with auto vs manual selection, even if the same coins are used as inputs. I created two PSBTs with a target feerate of 10 sats/vb. This one used auto selection and gave change amount of 5000 sats: Here, I selected the same coins manually and there was no change output, leading to a higher estimated feerate: I signed and broadcast the first PSBT to make sure its feerate was not lower than the 10 sats/vb: The feerate was 10.7 sats/vb according to https://mempool.space/signet/tx/6f3df02ced55effd873df27ed7a266bda729d41d905bbfbf49d313672b162b4f. |
Based on further discussion, I'll change |
Rebased it for you here during my review, if that helps: https://github.com/darosior/liana/tree/rebased_pr_560. |
1fcb332
to
e6f08e7
Compare
I've just pushed my latest changes, which include some failing tests that I'm still investigating. The main difference is that now the I've removed some code from |
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.
Approach ACK. Consolidating the change output calculation is also a great improvement.
I've got a number of review comments but i didn't find any bug, it's overall pretty well though-out. I've started cleaning up and adding comments to a bunch of things as i was playing with the code in this PR. Pushed those here if you'd like to include some of the changes: https://github.com/darosior/liana/tree/pr_560_modified. It also addresses most of my review comments.
Haven't looked at the tests yet, will continue playing with this a bit more.
I've mostly addressed your comments now in 2a1fcd9, including with changes from your branch (https://github.com/darosior/liana/tree/pr_560_modified - thanks 🙏). I still need to fix the failing functional test. Will squash commits again once no more changes are planned. |
5aab76f
to
b090618
Compare
It can happen that attempting a self-send of a coin near the dust limit will result in a transaction with no outputs, which then returns a
This has now been fixed. It was related to the order of transaction inputs no longer being guaranteed to be the same as the order in the |
Yes. Good catch. |
I've now added a check for a self-send with no change and updated the test. This check was there before and I had accidentally removed it as part of the other updates to I also updated a comment and removed an unused variable. |
ae5c1c3
to
4bca5c8
Compare
When creating a new spend, if coin outpoints are not provided, then coins will be selected automatically. This automatic selection is such that the transaction fee is minimized, taking into account the cost of creating any change output now and the cost of spending it in the future. If change is added, it must reduce the transaction waste and be above the dust threshold. This same policy is applied also in the case of manual coin selection, replacing the previous logic for determining the change amount. This ensures that creating a spend with auto-selection and another with manual selection using the same auto-selected coins will give the same change amount.
4bca5c8
to
cfa0f91
Compare
The first force-push to 4bca5c8 included the following changes:
The second force-push to cfa0f91 was to rebase on master as there was a conflict. |
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.
ACK cfa0f91
One thing i wanted to convince myself of is that we would never create a too-low-fee transaction which would never broadcast. I tested creating randomly sized transactions spending random coin values at 1sat/vb of feerate. Never failed so far. My hacks:
diff --git a/tests/test_spend.py b/tests/test_spend.py
index 8ad1849..e91ad41 100644
--- a/tests/test_spend.py
+++ b/tests/test_spend.py
@@ -322,3 +322,55 @@ def test_coin_selection(lianad, bitcoind):
assert auto_psbt.tx.vout[0].scriptPubKey == manual_psbt.tx.vout[0].scriptPubKey
# Change amount is the same (change address will be different).
assert auto_psbt.tx.vout[1].nValue == manual_psbt.tx.vout[1].nValue
+
+ # Get rid of the leftover coins.
+ res = lianad.rpc.createspend({bitcoind.rpc.getnewaddress(): 69_450}, [], 1)
+ psbt = PSBT.from_base64(res["psbt"])
+ assert len(psbt.o) == 1
+ signed_psbt = lianad.signer.sign_psbt(psbt)
+ lianad.rpc.updatespend(signed_psbt.to_base64())
+ spend_txid = signed_psbt.tx.txid().hex()
+ lianad.rpc.broadcastspend(spend_txid)
+ bitcoind.generate_block(1, wait_for_mempool=spend_txid)
+ wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 0)
+
+ import random, logging
+ descs = bitcoind.rpc.listdescriptors()["descriptors"]
+ bitcoind_desc = next(d["desc"] for d in descs if d["desc"].startswith("wpkh"))
+ bitcoind_addrs = bitcoind.rpc.deriveaddresses(bitcoind_desc, 10_000)
+ bitcoind.generate_block(5)
+ for _ in range(100):
+ # Receive a random value coin.
+ value = random.randrange(int(COIN / 10_000), 100 * COIN)
+ addr = lianad.rpc.getnewaddress()["address"]
+ deposit = bitcoind.rpc.sendtoaddress(addr, value / COIN)
+ bitcoind.generate_block(1, wait_for_mempool=deposit)
+ wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
+
+ # Create a randomly sized set of destinations to make the size of the transaction vary.
+ # A single input single output tx is 127 vbytes. Compute the fee accounting for 31 vb per
+ # added output. This is the minimum possible fee attached before createspend starts failing.
+ dest_count = random.randrange(1, 3_000 if value >= COIN else 100)
+ rest = value % dest_count
+ out_value = int(value // dest_count)
+ fee = 127 + 31 * (dest_count - 1)
+ if dest_count >= 0xfd:
+ fee += 2 # out count compactsize len
+ dest = {bitcoind_addrs[0]: out_value + rest - fee}
+ dest.update({bitcoind_addrs[i + 1]: out_value for i in range(dest_count - 1)})
+
+ logging.info(f"Value: {value / COIN} BTC. Dest count: {dest_count}. Fee: {fee}.")
+
+ # Create a 1sat/vbyte PSBT. It never gets created with a feerate lower than 1sat/vb.
+ # We can always sign and broadcast it.
+ res = lianad.rpc.createspend(dest, [], 1)
+ logging.debug(f"PSBT: {res['psbt']}")
+ psbt = PSBT.from_base64(res["psbt"])
+ assert len(psbt.o) == dest_count
+ signed_psbt = lianad.signer.sign_psbt(psbt)
+ lianad.rpc.updatespend(signed_psbt.to_base64())
+ spend_txid = signed_psbt.tx.txid().hex()
+ lianad.rpc.broadcastspend(spend_txid)
+ logging.debug(f"Mem entry: {bitcoind.rpc.getmempoolentry(spend_txid)}")
+ bitcoind.generate_block(1, wait_for_mempool=spend_txid)
+ wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 0)
# The transaction must contain the spent transaction for each input. | ||
# We don't make assumptions about the ordering of PSBT inputs. | ||
assert sorted( | ||
[psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in spend_psbt.i] | ||
) == sorted( | ||
[bytes.fromhex(bitcoind.rpc.gettransaction(op[:64])["hex"]) for op in outpoints] | ||
) |
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.
nit, for next time: no need to create a temporary list in this case.
diff --git a/tests/test_rpc.py b/tests/test_rpc.py
index e492da6..5fff40c 100644
--- a/tests/test_rpc.py
+++ b/tests/test_rpc.py
@@ -402,9 +402,9 @@ def test_create_spend(lianad, bitcoind):
# The transaction must contain the spent transaction for each input.
# We don't make assumptions about the ordering of PSBT inputs.
assert sorted(
- [psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in spend_psbt.i]
+ psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in spend_psbt.i
) == sorted(
- [bytes.fromhex(bitcoind.rpc.gettransaction(op[:64])["hex"]) for op in outpoints]
+ bytes.fromhex(bitcoind.rpc.gettransaction(op[:64])["hex"]) for op in outpoints
)
# We can sign it and broadcast it.
|
||
# Now create a transaction with manual coin selection using the same outpoints. | ||
outpoints = [ | ||
f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in auto_psbt.tx.vin |
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.
nit:
f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in auto_psbt.tx.vin | |
f"{txin.prevout.hash:64x}:{txin.prevout.n}" for txin in auto_psbt.tx.vin |
I don't think the 0 is needed?
Actually i tested and neither is the 64:
f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in auto_psbt.tx.vin | |
f"{txin.prevout.hash:x}:{txin.prevout.n}" for txin in auto_psbt.tx.vin |
def test_coin_selection(lianad, bitcoind): | ||
"""We can create a spend using coin selection.""" |
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.
Could use some insane value sanity checks. But i'll do some of those when tackling #800.
I'm going to merge this. I think it's in good shape, and we are going to continue testing it anyways (with the GUI integration and the RBF work). We can also update to the new upstream API in a followup. Congrats on getting this almost-5-months old PR to the finish line @jp1ac4! |
9e2407e gui: auto-select coins for spend (jp1ac4) Pull request description: This adds automated coin selection from #560 to the GUI. The new "Auto-select" button uses automated coin selection to select coins for a spend, which the user can then modify as required. The button can only be clicked once the recipients and feerate have been validated, and it does not appear when creating a self-send. I haven't added any validation on the amount before making the button clickable, but it may be hard to anticipate all coin selection errors once fees are taken into consideration. ACKs for top commit: darosior: light ACK 9e2407e Tree-SHA512: 6ce3389d849470b3beb6ac8df75d2c3b7b6c04ee881dd0e9116c4d87f54376a8ed6666cbfd0ff0152a3eb839c8f7f17a175fe078ef030f03451430b84ab40cb6
These are some initial changes towards #51.
I've added a
selectcoinsforspend
command that applies BnB coin selection using a waste metric.This coin selection is then used in
createspend
if no coins are specified.@darosior The changes are still in their early stages so I'm creating this draft PR to facilitate discussion about the approach in general and specific details.