Skip to content
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

Introduce new bdk_coin_select implementation #1072

Closed
wants to merge 28 commits into from

Conversation

evanlinjin
Copy link
Member

@evanlinjin evanlinjin commented Aug 9, 2023

Description

This replaces #924 (which in turn, ports over LLFourn/bdk_core_staging#46).

As stated by @LLFourn in #924 (comment), we will complete the following in this PR:

Notes to the reviewers

~

Changelog notice

Introduce new bdk_coin_select module

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • 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

@evanlinjin evanlinjin force-pushed the new_bdk_coin_select branch 2 times, most recently from ea80a62 to df70b71 Compare August 9, 2023 14:27
@notmandatory
Copy link
Member

notmandatory commented Aug 10, 2023

This looks like something worthy of a 2.0 release milestone. Will current 1.0 coin selection be at least on par with pre-1.0 BDK coin selection?

@LLFourn
Copy link
Contributor

LLFourn commented Aug 10, 2023

This looks like something worthy of a 2.0 release milestone. Will current 1.0 coin selection be at least on par with pre-1.0 BDK coin selection?

The plan for this PR is to release the crate on crates.io. Gutting BDK wallet coin selection and replacing it with this is another problem altogether. Using it in bdk wallet would require the planning module #1068 is first ready to be released. I think the plan is to have a crate bdk_tx_builder or something that will put all these things together and can be used without Wallet.

@notmandatory
Copy link
Member

This looks like something worthy of a 2.0 release milestone. Will current 1.0 coin selection be at least on par with pre-1.0 BDK coin selection?

The plan for this PR is to release the crate on crates.io. Gutting BDK wallet coin selection and replacing it with this is another problem altogether. Using it in bdk wallet would require the planning module #1068 is first ready to be released. I think the plan is to have a crate bdk_tx_builder or something that will put all these things together and can be used without Wallet.

Got it, I won't mark this for any BDK major release milestones. But eventually integrating this into BDK along with #1068 sounds like a good goals for a future 2.0 milestone.

@notmandatory
Copy link
Member

Per discussion in bitcoindevkit/.github#72 this will be stand-alone framework, not to be integrate with BDK wallet until a future 2.0 milestone.

@evanlinjin evanlinjin force-pushed the new_bdk_coin_select branch 3 times, most recently from 758d854 to b317c43 Compare August 18, 2023 09:30
@nondiremanuel
Copy link

Even if it will be ready together with alpha.3, being that it won't be integrated until a future 2.0 version, I would assign it to the 2.0 milestone. In general I propose to use milestones to signal which will be the release that will include a specific PR, regardless of the timing of development (so even if it is ready earlier), but maybe I didn't understand correctly the topic here

@LLFourn
Copy link
Contributor

LLFourn commented Aug 21, 2023

Depending on how things go we could replace the existing coin selection with this one without doing the full redesign of tx building in bdk and put in alpha.3. For example, instead of:

tx_builder = tx_builder.coin_selection::<SomeCoinSelectionAlgo>();

This could be improved to:

tx_builder = tx_builder.coin_selection(|coin_selector| {
    /*This will be called once we've got all the candidates. Do your coin selection with coin_selector and return it*/
});

Internally tx_builder would store a where for<'a> F: FnOnce(CoinSelector<'a>) -> Result<CoinSelector<'a>, E> which would have a sensible default (F and E would become type parameters on TxBuilder). The benefit would be getting the flexibility and power and correctness of the new coin selection code. This would likely fix #1066 which arises mainly from the current architecture of coin selection where each coin selection algorithm can choose to return coin selections that violate the constraints given to it (in this case absolute fee).

@evanlinjin would be a better judge of whether this is a good idea.

@evanlinjin evanlinjin force-pushed the new_bdk_coin_select branch 5 times, most recently from a3bdfb9 to 03a7f1a Compare August 22, 2023 07:19
@evanlinjin
Copy link
Member Author

@darosior I'll give a guesstimate of two more weeks before this is in a good state.

@darosior
Copy link
Contributor

darosior commented Aug 23, 2023 via email

@evanlinjin evanlinjin marked this pull request as ready for review November 14, 2023 21:04
@@ -836,7 +836,7 @@ mod test {
let drain_script = ScriptBuf::default();
let target_amount = 250_000 + FEE_AMOUNT;

let result = LargestFirstCoinSelection::default()
let result = LargestFirstCoinSelection
Copy link
Contributor

Choose a reason for hiding this comment

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

huh why are we changing this file?

}

cp Cargo.toml Cargo.tmp.toml
cp Cargo.1.48.0.toml Cargo.toml
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should instead just put this code in a separate repo in the bdk org and have our own MSRV for it.

use bitcoin::{ Address, Network, Transaction, TxIn, TxOut };

// You should use miniscript to figure out the satisfaction weight for your coins!
const TR_SATISFACTION_WEIGHT: u32 = 66;
Copy link
Contributor

Choose a reason for hiding this comment

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

We actually have this as a const in bdk_coin_select. Keep the comment saying something "In general, you can use miniscript to figure out the satisfaction weight of your inputs".

input_count: 1,
// the value of the input
value: 1_000_000,
// the total weight of the input(s). This doesn't include
Copy link
Contributor

Choose a reason for hiding this comment

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

What doesn't it include (I know this was probably my fault)?. Maybe just remove that.

lock_time: bitcoin::absolute::LockTime::from_height(0).unwrap(),
version: 0x02,
};
let base_weight = base_tx.weight().to_wu() as u32;
Copy link
Contributor

@LLFourn LLFourn Nov 15, 2023

Choose a reason for hiding this comment

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

since rust bitcoin uses u64 here should we also? It can be u32 internally but we can cast it.

&mut self,
metric: M,
max_rounds: usize,
) -> Result<Ordf32, NoBnbSolution> {
Copy link
Contributor

Choose a reason for hiding this comment

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

When i was suggesting this method I was thinking that it would fall back to select_until_target_met if it can't find anything. This would mean you don't need this error type -- rather you can just have InsufficientFunds.

Do you think it's worth having run_bnb_with_fallback() that does that?

Also another sneaky request -- if #[cfg(feature = "std")] can you add a method that lets you pass in a std::time::Duration and checks the time passed every 1_000 rounds and stops after the time has been exceeded.

/// Fee rate
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
// Internally stored as satoshi/weight unit
pub struct FeeRate(Ordf32);
Copy link
Contributor

Choose a reason for hiding this comment

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

might be worth manually implementing Debug so the format prints sats per vb in the first place like: 8 sat/vb (2 sat/wu)

}

/// Return the value as satoshi/wu.
pub fn spwu(&self) -> f32 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub fn spwu(&self) -> f32 {
pub fn sat_per_wu(&self) -> f32 {

we can keep fn spwu as pub(crate) but in the API it should be more descriptive.

match drain_weights {
// with change
Some(drain_weights) => {
(cs.input_weight() + drain_weights.output_weight) as f32
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably worth throwing in the base_weight in here too to make the metric match the result.

I had a helper method on CoinSelector for this: implied_fee -- might be more elegant to make it public and use it. You're not taking into account the min_fee which seems like it could cause the wrong answer.

Some(_) => {
let selected_value = cs.selected_value();
assert!(selected_value >= self.target.value);
(cs.selected_value() - self.target.value) as f32
Copy link
Contributor

@LLFourn LLFourn Nov 15, 2023

Choose a reason for hiding this comment

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

what's going on here? When you do have a change output why do you add this extra input minus target to the value 😕.

Copy link

Choose a reason for hiding this comment

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

Should this rather be in the None case for the excess? I've just encountered one scenario where I get the same score for two different changeless selections, but one of these selections has greater excess.

@darosior
Copy link
Contributor

Another API consideration: for RBF the minimum absolute fee of a replacement transaction depends on the size of itself (since by rule 4 the fee must be at least the sum of the replaced transactions' fees plus 1sat/vb worth of the replacement transaction's size).

Therefore when selecting coins for the replacement transaction the caller can't set min_fee to the right value, since it depends on the result of the coin selection it's trying to get. For now i think our best bet is to try again with min_fee = prev_min_fee + replacement_tx_size until it either errors or succeeds, but it's meh.

h/t @jp1ac4.

@nondiremanuel nondiremanuel modified the milestones: 2.0.0-alpha.0, 1.0.0-alpha.4 Nov 20, 2023
@LLFourn
Copy link
Contributor

LLFourn commented Nov 21, 2023

Another API consideration: for RBF the minimum absolute fee of a replacement transaction depends on the size of itself (since by rule 4 the fee must be at least the sum of the replaced transactions' fees plus 1sat/vb worth of the replacement transaction's size).

Therefore when selecting coins for the replacement transaction the caller can't set min_fee to the right value, since it depends on the result of the coin selection it's trying to get. For now i think our best bet is to try again with min_fee = prev_min_fee + replacement_tx_size until it either errors or succeeds, but it's meh.

h/t @jp1ac4.

The min_fee is for rule 3. You use the previous feerate + 1 s/vb to satisfy rule 4. At least this is what i had in mind let me know if I missed something!

@darosior
Copy link
Contributor

The min_fee is for rule 3. You use the previous feerate + 1 s/vb to satisfy rule 4. At least this is what i had in mind let me know if I missed something!

Unfortunately it's not that simple. Rule 4 is about absolute fees. Consider this situation (Tx (fee, vsize)). We try replacing the following transaction chain: A (100, 100) <- B (100, 100). We set min_fee to 200 and feerate to 2 s/vb (ie previous feerate + 1 s/vb). We get a replacement A' (200, 100).

This fails the Rule 4 check since A' should not only have a higher feerate than conflicts (6) and a higher fee than the sum of the conflicts' fees (3), but it should also have 1 s/vb * its size additional fees (4). A' should have a fee of 300.

Of course in this trivial example we could have set min_fee to 300. But what if the coin selection makes it so A' is 200 vbytes large instead? Failure again. It's really something which can't be done outside the coin selection.

@LLFourn
Copy link
Contributor

LLFourn commented Nov 22, 2023

@darosior thanks for the correction. I always simplified this rule to "higher feerate" in my mind. TIL. I don't blame myself too much since this is a very bizarre rule. It obviates the need for rule 3 (satisfying rule 4 will always satisfy rule 3). I do believe that higher abs fee + 1svb higher feerate than tx being replaced is far more intuitive and simpler.

I feel as if there was some discussion about simplifying these rules led by @glozow. I wonder if there was any progress.

So if I understand correctly we should add another constraint to Target that handles RBF constraints which handles the abs fee of what is being replaced and has a "inremental relay" feerate. I think we should do this in a follow up PR @evanlinjin.

Once @evanlinjin responds to my comments above we're going to:

  1. Move this crate to a new repo because it is so nicely decoupled from everything else
  2. Release it
  3. Make this PR just update the examples and use the released version (luckily we didn't impl RBF in the examples yet!)

@darosior
Copy link
Contributor

darosior commented Nov 23, 2023

I do believe that higher abs fee + 1svb higher feerate than tx being replaced is far more intuitive and simpler.

And doesn't achieve the purpose of this rule, which is to prevent free relay/churn. For instance with your rule i can replace txA(100_000,100_000) hundreds (almost a thousand) of times without paying any more fee by every time crafting a smaller txA'(100_000,50_000), txA'(100_000,33_333), etc.. replacement.

I feel as if there was some discussion about simplifying these rules led by @glozow. I wonder if there was any progress.

There's been plenty of discussions around improving RBF, but it was mostly in the context of solving pinning (transaction v3 and ephemeral anchors). There's also been work into trying to patch some holes in the current RBF and make it more incentive compatible along with other mempool operations (for instance, correctly considering the ancestor feerate) but it turns out it's very complicated to do without a whole rewrite of the mempool. This culminated in the Cluster Mempool proposal, which should make for a more approachable interface for wallets.

TL;DR: there is many issues with RBF. People are aware. It's hard. :)

@LLFourn
Copy link
Contributor

LLFourn commented Nov 27, 2023

I do believe that higher abs fee + 1svb higher feerate than tx being replaced is far more intuitive and simpler.

And doesn't achieve the purpose of this rule, which is to prevent free relay/churn. For instance with your rule i can replace txA(100_000,100_000) hundreds (almost a thousand) of times without paying any more fee by every time crafting a smaller txA'(100_000,50_000), txA'(100_000,33_333), etc.. replacement.

My claim was that transaction fees (abs fee, and feerate) should just be enforced to be monotonically increasing which is contradiction to "I can replace hundreds of times without paying any more fee". Your response sounds like you thought I was suggesting that enforcing only a greater feerate was sufficient. If my understanding is wrong please make another attempt to clean it up!

There's been plenty of discussions around improving RBF, but it was mostly in the context of solving pinning (transaction v3 and ephemeral anchors). There's also been work into trying to patch some holes in the current RBF and make it more incentive compatible along with other mempool operations (for instance, correctly considering the ancestor feerate) but it turns out it's very complicated to do without a whole rewrite of the mempool. This culminated in the Cluster Mempool proposal, which should make for a more approachable interface for wallets.

TL;DR: there is many issues with RBF. People are aware. It's hard. :)

Thanks the cluster mempool work looks like it could be a great starting point to go deeper into mempool theory.

@darosior
Copy link
Contributor

(I'm happy to move this discussion elsewhere btw, it looks like we are slowly shifting OT of the code review here)


I do believe that higher abs fee + 1svb higher feerate than tx being replaced is far more intuitive and simpler.

And doesn't achieve the purpose of this rule, which is to prevent free relay/churn. For instance with your rule i can replace txA(100_000,100_000) hundreds (almost a thousand) of times without paying any more fee by every time crafting a smaller txA'(100_000,50_000), txA'(100_000,33_333), etc.. replacement.

My claim was that transaction fees (abs fee, and feerate) should just be enforced to be monotonically increasing which is contradiction to "I can replace hundreds of times without paying any more fee".

My bad then i indeed misunderstood "higher abs fee + 1svb" for "higher or equal abs fee + 1svb". But i'm still confused because it was in the context of your criticism of rule 4. Are you saying as long as it's paying as small as 1 satoshi more in abs fees we should accept it? That's not free, but still pretty cheap churn+relay. And that's precisely the rationale given for rule 4:

Try to prevent DoS attacks where an attacker causes the network to repeatedly relay transactions each paying a tiny additional amount in fees, e.g. just 1 satoshi.

@LLFourn
Copy link
Contributor

LLFourn commented Nov 29, 2023

(I'm happy to move this discussion elsewhere btw, it looks like we are slowly shifting OT of the code review here)

All good this discussion will be useful when we come to fix this.

My bad then i indeed misunderstood "higher abs fee + 1svb" for "higher or equal abs fee + 1svb". But i'm still confused because it was in the context of your criticism of rule 4. Are you saying as long as it's paying as small as 1 satoshi more in abs fees we should accept it? That's not free, but still pretty cheap churn+relay. And that's precisely the rationale given for rule 4:

No my bad looking back at it I used "+" to mean logical conjunction. Whoops.

Right I am suggesting that 1 satoshi more in abs fee is good enough for a small tx replacing a bigger tx (since that's the scenario where the abs fee would be stricter than the feerate rule). Or if 1 sat was not a big enough step up have a minimum like you do for feerate e.g. 100 sats.

After thinking about this my misunderstanding is that this would be simpler -- but actually it's not. Providing the previous tx fee and the prev feerate is actually equivalent to providing the previous transaction fee and weight. I don't know how I got stuck on this. I am still confused as to why there is rule 3 when rule 4 suffices for everything.

So I propose we do something like this for the target:

enum TargetFee {
    Rate(FeeRate),
    Replacement {
        prev_fee: u64,
        prev_weight: u32,
        incremental_relay_feerate: FeeRate,
    }
}

No this doesn't make sense. It should be:

struct TargetFee {
    feerate: FeeRate,
    replacement: Option<Replacement>,
}

struct Replacement {
    prev_fee: u64,
    prev_weight: u32,
    incremental_relay_feerate: FeeRate,
}

Feerate is orthogonal to whether we are replacing. It's two different constraints. So I'd say get rid of min_fee and do this.

@evanlinjin
Copy link
Member Author

bdk_coin_select now exists in a separate repository so we can close this PR.

@evanlinjin evanlinjin closed this Dec 11, 2023
@notmandatory
Copy link
Member

@evanlinjin should we have an issue for bdk to switch to using this new crate?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

6 participants