Skip to content

Add MaxWeight to cap transaction weight during selection#48

Open
aagbotemi wants to merge 1 commit into
bitcoindevkit:masterfrom
aagbotemi:feat/max-weight-metric
Open

Add MaxWeight to cap transaction weight during selection#48
aagbotemi wants to merge 1 commit into
bitcoindevkit:masterfrom
aagbotemi:feat/max-weight-metric

Conversation

@aagbotemi

@aagbotemi aagbotemi commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Description

max_weight field added to Target, making the maximum transaction weight a selection constraint enforced in is_target_met.

Notes for review

  • The weight cap lives on Target and flows through is_target_met/is_target_met_with_drain.
  • select_until_target_met now returns SelectError { InsufficientFunds, MaxWeightExceeded } so a weight cap failure isn't mislabeled as a funds shortfall.

Changelog notice

  • Added max_weight field to Target
  • select_until_target_met returns SelectError

@murchandamus murchandamus left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I’m not actively contributing to this repository, so please feel free to ignore me. I noticed that this PR only adds MaxWeight for BnB, but a maximum weight would be especially relevant in the context of creating TRUC transactions that are limited to 10,000 vB or 1000 vB respectively, so it would be relevant to all appropriate for all coinselection algorithms.

@evanlinjin

evanlinjin commented Jun 27, 2026

Copy link
Copy Markdown
Member

@murchandamus Thank you for the insight and you have a point. Maybe the best move is to make MaxWeight part of the Target as it is a selection constraint.

This will also make bitcoindevkit/bdk-tx#47 implementation cleaner.

@aagbotemi aagbotemi force-pushed the feat/max-weight-metric branch 3 times, most recently from 4572958 to 9e7f498 Compare June 27, 2026 12:00
@aagbotemi

Copy link
Copy Markdown
Contributor Author

I’m not actively contributing to this repository, so please feel free to ignore me. I noticed that this PR only adds MaxWeight for BnB, but a maximum weight would be especially relevant in the context of creating TRUC transactions that are limited to 10,000 vB or 1000 vB respectively, so it would be relevant to all appropriate for all coinselection algorithms.

Thanks @murchandamus for the review.

Maybe the best move is to make MaxWeight part of the Target as it is a selection constraint.

This will also make bitcoindevkit/bdk-tx#47 implementation cleaner.

Thanks @evanlinjin, I took your suggestion and moved the cap onto Target as a selection constraint.

@aagbotemi aagbotemi force-pushed the feat/max-weight-metric branch 2 times, most recently from c63c995 to 92d2425 Compare June 27, 2026 14:12
Comment thread src/coin_selector.rs
Comment on lines -470 to -480
debug_assert_eq!(
self.is_target_met(target),
self.is_target_met_with_drain(
target,
Drain {
weights: change_policy.drain_weights,
value: excess as u64
}
),
"if the target is met without a drain it must be met after adding the drain"
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's the motivation to remove this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I intended to use excess. Now that max_weight is on Target, is_target_met and is_target_met_with_drain both check the cap, and they can differ on weight. The replacement check only the value it was meant to guard, independent of weight.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In this case, it's better to change the check rather than remove it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, I have changed the check

Comment thread src/coin_selector.rs Outdated
@aagbotemi aagbotemi force-pushed the feat/max-weight-metric branch from 92d2425 to 0b8b3e5 Compare June 27, 2026 16:50
@aagbotemi aagbotemi requested a review from evanlinjin June 27, 2026 17:02
Comment thread src/coin_selector.rs Outdated
.ok_or_else(|| InsufficientFunds {
missing: self.excess(target, Drain::NONE).unsigned_abs(),
.ok_or_else(|| {
let excess = self.excess(target, Drain::NONE);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Excess can be negative because inputs are low in effective value and high in weight -- in which case, we should return MaxWeightExceeded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the catch. I've fixed it.

Comment thread src/coin_selector.rs Outdated
Comment on lines +470 to +483
debug_assert_eq!(
self.is_target_met(target),
self.is_target_met_with_drain(
debug_assert!(
self.excess(
target,
Drain {
weights: change_policy.drain_weights,
value: excess as u64
}
),
"if the target is met without a drain it must be met after adding the drain"
value: excess as u64,
},
) >= 0,
"if the target value is met without a drain it must be met after adding the drain"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't the check be: if the target is met without a drain, it must be met after adding the drain, unless if max weight is exceeded?

@evanlinjin evanlinjin Jun 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is wrong. Adding a drain output increases the weight of the transaction so we cannot guarantee that it'll be within our max weight target (as the debug_assert implies).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good. We can either keep a value-only check (excess(target, drain) >= 0, scoped to value only) or drop the assert, since with drain.value = excess here it's always true. Open to your thoughts.

@aagbotemi aagbotemi force-pushed the feat/max-weight-metric branch 2 times, most recently from 0df09d3 to 8a733e2 Compare June 28, 2026 22:50
@aagbotemi aagbotemi force-pushed the feat/max-weight-metric branch from 8a733e2 to 543821e Compare June 28, 2026 23:02
@evanlinjin

Copy link
Copy Markdown
Member

We need a bit more thought looking at how adding max_weight to Target changes that other parts of the codebase.

  • The logic of is_selection_possible is now wrong. Previously, Target was a purely monotone predicate -- it's fields calculated excess which can monotonously be increased by adding more effective inputs. Putting max_weight in Target changed this property as max_weight puts an upper bound on the weights of the inputs added. To "fix" it, we need to do a more expensive knapsack algorithm -- is this the best move?

  • How does the max_weight parameter affect the LowestFee metric? I've noticed that proptests are not updated to test with different max_weight values.

@evanlinjin

Copy link
Copy Markdown
Member

@aagbotemi I'm going to have a go at finding the right solution. The problem is a lot more complex than initially thought.

@aagbotemi

Copy link
Copy Markdown
Contributor Author

I went down some rabbit hole. Making is_selection_possible exact under a weight cap turns it into a knapsack-style problem where we have to search subsets. Any exact approach is too heavy for a simple pre-check, and BnB already does that kind of search. The idea I was working with earlier is to have is_selection_possible ignore max_weight. Then false still means truly impossible, true means possible ignoring the cap, and selection itself reports MaxWeightExceeded. I also wrote a proptest that brute-forces every subset to verify any solution we pick here. Interested to hear findings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants