Skip to content

Add Target::max_weight, built on the metric-decides-change refactor (#49)#51

Draft
evanlinjin wants to merge 2 commits into
bitcoindevkit:masterfrom
evanlinjin:feat/max-weight-on-target
Draft

Add Target::max_weight, built on the metric-decides-change refactor (#49)#51
evanlinjin wants to merge 2 commits into
bitcoindevkit:masterfrom
evanlinjin:feat/max-weight-on-target

Conversation

@evanlinjin

Copy link
Copy Markdown
Member

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_met is kept value-only / monotone; the weight cap is a separate, anti-monotone predicate CoinSelector::is_within_max_weight. Keeping these apart is what preserves the monotonicity that BnB bounds and is_selection_possible rely on.
  • Enforcement lives in the metric (which, post-feat!: BnB metrics decide the change output themselves #49, owns the change decision): LowestFee::drain_value refuses a change output that would bust the cap, score rejects any over-cap selection, and bound hard-prunes subtrees whose lightest (no-drain) form already busts the cap.
  • is_selection_possible becomes an exact value probe plus a greedy value-per-weight weight probe.

Why build on #49 instead of folding into Target/is_target_met

Folding the cap into is_target_met (as in #48) makes that predicate anti-monotone, which breaks two things:

  1. is_selection_possible starts returning false negatives (it selects all effective inputs — max value and max weight — then checks the now-weight-aware is_target_met).
  2. More seriously, 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_value change 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_weight is threaded through every LowestFee proptest, so BnB is compared against exhaustive search with the cap active.
  • New exact_selection_possible oracle (exhaustive, reuses the real is_target_met/is_within_max_weight, independent of the BnB prune) + a bnb_respects_max_weight proptest (4096 cases) asserting BnB feasibility matches the oracle and that is_selection_possible never over-claims.
  • Fixed compare_against_benchmarks to filter benchmarks by score().is_some() rather than value-only is_target_met, so an over-cap benchmark (e.g. select_all under a tight cap) isn't mistaken for a valid solution.

Note

Breaking change: the Target { .. } literal now requires a max_weight field.

🤖 Generated with Claude Code

@evanlinjin evanlinjin force-pushed the feat/max-weight-on-target branch from 30550ca to 4858294 Compare July 2, 2026 13:24
@evanlinjin evanlinjin self-assigned this Jul 2, 2026
@evanlinjin evanlinjin marked this pull request as draft July 2, 2026 13:32
evanlinjin and others added 2 commits July 2, 2026 17:37
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>
@evanlinjin evanlinjin force-pushed the feat/max-weight-on-target branch from 4858294 to 1afc3ed Compare July 2, 2026 17:38
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.

1 participant