Add Target::max_weight, built on the metric-decides-change refactor (#49)#51
Draft
evanlinjin wants to merge 2 commits into
Draft
Add Target::max_weight, built on the metric-decides-change refactor (#49)#51evanlinjin wants to merge 2 commits into
evanlinjin wants to merge 2 commits into
Conversation
30550ca to
4858294
Compare
Add an optional `Target::max_weight` cap on the resulting transaction weight (WU) — relevant e.g. for TRUC/BIP-431 packages capped at 10,000 or 1,000 vB. It's a feasibility constraint (the sibling of the value target: a lower bound on value, this an upper bound on weight). Enforcement: - `CoinSelector::is_within_max_weight` — the anti-monotone weight predicate, kept separate from the monotone value-only `is_target_met`. - `is_selection_possible` decomposes into an exact value probe plus an exact weight probe: candidates are grouped into weight classes (few in practice) and we enumerate how many to take from each, checking the real target predicates; it falls back to a greedy probe only when the classes are too many to enumerate. - `select_until_target_met` now returns `SelectError::MaxWeightExceeded` instead of silently returning an over-cap selection. - `LowestFee`: `drain_value` refuses a change output that would bust the cap; `score` rejects any over-cap selection; `bound` hard-prunes subtrees whose lightest (no-drain) form already busts the cap. A cap-agnostic `fee_score` is split out for the bound's internal estimates so they stay admissible. The economic change decision keeps the existing bound proof valid under the cap, so the hard-prune suffices (no extra changeless term). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thread `max_weight` through `Target`, `StrategyParams` and every proptest, so BnB is compared against exhaustive search *with* the cap active. Add `exact_selection_possible` (exhaustive oracle reusing the real target predicates, independent of the BnB prune) and a `bnb_respects_max_weight` proptest asserting BnB feasibility matches it and that `is_selection_possible` agrees exactly with the oracle (validating the weight-class enumeration). Fix `compare_against_benchmarks`: filter benchmarks by `score().is_some()` (a valid solution) rather than value-only `is_target_met`, and add a value-per-weight greedy benchmark so the comparison isn't vacuous under a binding cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4858294 to
1afc3ed
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Alternative to #48 for capping transaction weight during coin selection (e.g. for TRUC / BIP-431 packages limited to 10,000 / 1,000 vB).
This version is built on top of the metric-decides-change refactor that just landed in #49, and that turns out to matter a lot for correctness — see below.
Approach
Target::max_weight: Option<u64>— a feasibility constraint (the sibling of the value target: a lower bound on value, this an upper bound on weight).is_target_metis kept value-only / monotone; the weight cap is a separate, anti-monotone predicateCoinSelector::is_within_max_weight. Keeping these apart is what preserves the monotonicity that BnB bounds andis_selection_possiblerely on.LowestFee::drain_valuerefuses a change output that would bust the cap,scorerejects any over-cap selection, andboundhard-prunes subtrees whose lightest (no-drain) form already busts the cap.is_selection_possiblebecomes an exact value probe plus a greedy value-per-weight weight probe.Why build on #49 instead of folding into
Target/is_target_metFolding the cap into
is_target_met(as in #48) makes that predicate anti-monotone, which breaks two things:is_selection_possiblestarts returning false negatives (it selects all effective inputs — max value and max weight — then checks the now-weight-awareis_target_met).LowestFee's bound becomes inadmissible. When the cap forces a descendant to be changeless (the change is refused for weight), that descendant can score below an ancestor's bound, so BnB prunes the branch and returns a suboptimal selection.The second point is the interesting one: on the pre-#49
min_valuechange policy this bug is real and reproducible. On the #49 base it disappears — because change is now decided economically (only added when it lowers the long-term fee), the bound's existing "a descendant can't improve by removing worthwhile change" proof stays valid even when the cap forces changelessness. So the weight hard-prune is sufficient and no extra bound term is needed. This is the core reason this PR targets the refactored base.Testing
max_weightis threaded through everyLowestFeeproptest, so BnB is compared against exhaustive search with the cap active.exact_selection_possibleoracle (exhaustive, reuses the realis_target_met/is_within_max_weight, independent of the BnB prune) + abnb_respects_max_weightproptest (4096 cases) asserting BnB feasibility matches the oracle and thatis_selection_possiblenever over-claims.compare_against_benchmarksto filter benchmarks byscore().is_some()rather than value-onlyis_target_met, so an over-cap benchmark (e.g.select_allunder a tight cap) isn't mistaken for a valid solution.Note
Breaking change: the
Target { .. }literal now requires amax_weightfield.🤖 Generated with Claude Code