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 #924

Closed
wants to merge 8 commits into from

Conversation

LLFourn
Copy link
Contributor

@LLFourn LLFourn commented Mar 23, 2023

This is porting over LLFourn/bdk_core_staging#46

I made it compile on 1.48.0 and added a script that we can use in CI to check that all the crates that should be able to compile on 1.48.0 can.

TODO:

  • finish documentation
  • update examples to use this

I've had a request to do a release of the separately before it is used in bdk itself so that other projects can try it out.

cc @darosior

@notmandatory
Copy link
Member

The script approach looks good to me. I tried my sub-directory idea (cargo +1.48.0 build --manifest-path msrv-1.48.0/Cargo.toml) and it doesn't work.

#[derive(Debug, Clone)]
pub struct CoinSelector<'a> {
base_weight: u32,
candidates: &'a [Candidate],
Copy link
Member

@evanlinjin evanlinjin May 31, 2023

Choose a reason for hiding this comment

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

I think it will be much more helpful if we had:

pub struct CoinSelector<'c, C> {
    candidates: &'c [C],
    /* ... */
}

impl<'c, C: AsRef<Candidate>> CoinSelector<'c, C> {}

This way C can contain everything required to complete the rest of tx building.

You can even do:

    impl AsRef<Candidate> for Candidate {
        fn as_ref(&self) -> &Candidate {
            self
        }
    }

Copy link
Contributor Author

@LLFourn LLFourn Jun 7, 2023

Choose a reason for hiding this comment

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

I think to do this properly we'd have to make Candidate a trait. I'd leave it like this for now until we see how tx building turns out and make it more flexible where we find pain points.

on the other hand there's no real harm to doing what you suggested now so maybe I can give it a shot.

Copy link
Contributor

@darosior darosior left a comment

Choose a reason for hiding this comment

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

Impressive work, thanks for making it a separate crate.

We started poking at using this in Liana (see wizardsardine/liana#560), so i'm curious what's the state of this PR?


/// The weight of a TXOUT without the `scriptPubkey` (and script pubkey length field).
/// Just the weight of the value field.
pub const TXOUT_BASE_WEIGHT: u32 = 4 * core::mem::size_of::<u64>() as u32; // just the value
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Technically it's an i64, but the size is the same anyways.

Suggested change
pub const TXOUT_BASE_WEIGHT: u32 = 4 * core::mem::size_of::<u64>() as u32; // just the value
pub const TXOUT_BASE_WEIGHT: u32 = 4 * core::mem::size_of::<i64>() as u32; // just the value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually didn't know that bitcoin core treated these at i64 internally. I think from a consensus perspective either is correct? i.e. if we changed the definition to be u64 the protocol doesn't change at all.


pub fn new_tr_keyspend() -> Self {
Self {
weight: TXOUT_BASE_WEIGHT + TR_SPK_WEIGHT,
Copy link
Contributor

Choose a reason for hiding this comment

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

You are missing the scriptPubKey length (and therefore 4 weight units)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep well spotted. I added a 1 byte spk length to TXOUT_BASE_WEIGHT. All this weight stuff was added on a whim while I was trying to use this and is not very well tested. I'm not sure if it's worth having since you should probably get the numbers from rust bitcoin and rust miniscript which do a better job of telling you this stuff now.

it's inconsistent to not include it and I confused myself.
@LLFourn
Copy link
Contributor Author

LLFourn commented Jun 21, 2023

Hey @darosior. Thanks for taking a stab at using this that was really helpful. I think the status is waiting for @evanlinjin to help me finish it. Here's a list of TODOs:

  1. Add minimize fee metric (it's a better default that waste see Apply coin selection for spend wizardsardine/liana#560 (comment))
  2. Allow easy weighted combination of different metrics (see same link above)
  3. Document everything including nice README.md exmaples
  4. Make DWIM branch and bound helper function (Apply coin selection for spend wizardsardine/liana#560 (comment))
  5. Make DrainWeight (Apply coin selection for spend wizardsardine/liana#560 (comment))
  6. Add CoinSelector::fund_outputs (see Apply coin selection for spend wizardsardine/liana#560 (comment)) -- another point is that in general we need to know the number of outputs to get the varint for the outputs correct which I think we are cheating on atm.
  7. Test cases against real world transactions to see if coin selections have precisely the real world feerate that was expected. (EDIT: I've just done a few of these against the feerate shown mempool.space).
  8. Rewrite examples to use this crate.
  9. Remove all TODO in the new crate and make issues for further work.

I wonder if @evanlinjin can put this as priority after #1002.

fn default() -> Self {
Self {
feerate: FeeRate::default_min_relay_fee(),
min_fee: 0, // TODO figure out what the actual network rule is for this
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no explicit standardness limit for absolute fee (aside from replacement rules). It's implicitly bounded by the minimum feerate rule since transactions can't be empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. I'll leave removing all these TODOs around the code for @evanlinjin who I think created most of them. Things that need addressing should that when this is merged it's TODO free.

Comment on lines +27 to +28
/// The minimum fee the selection must have
pub min_fee: u64,
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this only needs be specified for RBF, how about making it Optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would mean Some(0) and None have the same semantics which I think outweighs the better intuition provided by having it as an Option. Code docs should for sure explain that the only time you need to use this is to comply with RBF rules.

Comment on lines 180 to 190
let witness_header_extra_weight = self
.selected()
.find(|(_, wv)| wv.is_segwit)
.map(|_| 2)
.unwrap_or(0);
let vin_count_varint_extra_weight = {
let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::<usize>();
(varint_size(input_count) - 1) * 4
};

self.selected_weight() + witness_header_extra_weight + vin_count_varint_extra_weight
Copy link
Contributor

@darosior darosior Jun 27, 2023

Choose a reason for hiding this comment

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

For transaction that have at least one Segwit input, you also need to consider the added witness stack size for all inputs that are not spending from a Segwit Script. This is always 1 per legacy input since for them this stack is always empty.

More information about this here: https://github.com/bitcoin/bitcoin/pull/26567/files#diff-6e06b309cd494ef5da4e78aa0929a980767edd12342137f268b9219167064d13R62-R69.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I fixed this and added some tests 4e0c907

Interestingly doing this properly broke some prop tests. The prop tester would find edge cases where the branch and bound algorithm found what looked like a good solution with lots of legacy inputs but then it had to add one segwit input and made the weight blow up for the existing inputs. This could probably be fixed but I don't think it's worth the effort and code complexity just to have slightly better selections when you mix legacy and segwit for waste metric etc.

see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
@darosior
Copy link
Contributor

darosior commented Aug 7, 2023

So #1002 was replaced by #1034 which was merged. @evanlinjin do you still have this as a priority?

@evanlinjin
Copy link
Member

@darosior thanks for asking. I'm literally planning to start work on this tomorrow. This will be a priority. I should have a rough timeline of when I think I can get this completed by the end of the week.

Also note that I also need to look into a couple of bugs as well. Will keep you posted.

@darosior
Copy link
Contributor

darosior commented Aug 7, 2023

That's great news! Thanks, please do.

evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 9, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 9, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 9, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 10, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
@notmandatory
Copy link
Member

This is replaced by #1072.

evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 21, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 24, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 26, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 27, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Aug 27, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Nov 2, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Nov 6, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
evanlinjin pushed a commit to evanlinjin/bdk that referenced this pull request Nov 13, 2023
see:
bitcoindevkit#924 (comment)

Was a PITA since branch and bound is hard to do with this interference
between segiwt and legacy weights. It would find solutions that looked
good until you add the final input which was segwit and then the
solution would be suboptimal and fail the test.
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.

4 participants