Give CoinSelector its target instead of threading it through every call - #59
Open
evanlinjin wants to merge 1 commit into
Open
Give CoinSelector its target instead of threading it through every call#59evanlinjin wants to merge 1 commit into
CoinSelector its target instead of threading it through every call#59evanlinjin wants to merge 1 commit into
Conversation
…call
A selector was built for one target and evaluated against it throughout,
but every method took the target as a parameter, so nothing stopped
`cs.excess(target_a, drain)` being followed by `cs.is_funded(target_b)`.
The correctness arguments in the metrics are all stated at a fixed target
-- `LowestFee::bound`'s proof that a changeless superset always costs
more, `Changeless::change_unavoidable`'s assumption that the drain
decision is monotone in the excess -- and were held together by
convention rather than by types.
`CoinSelector::new` now takes the target and owns it. Twenty signatures
*lose* a parameter rather than gaining one: fifteen public methods
(`excess`, `implied_fee`, `is_funded`, `drain`, `select_until_target_met`,
the four `*_excess`, ...), plus `bnb_solutions` and `run_bnb`, plus all
three `BnbMetric` methods.
The crate had already reached this conclusion one layer down: `BnbIter`
stored the target as a field, took it once in `BnbIter::new`, and then
re-passed it into `metric.score` and `metric.bound` at every node. That
field and the re-threading are both gone.
This is a breaking change, and it reaches `BnbMetric`, so metrics
implemented outside this crate need their signatures updated:
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32>;
fn bound(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32>;
fn drain(&mut self, cs: &CoinSelector<'_>) -> Drain;
`CoinSelector::target()` exposes the target for metrics that need to read
it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
evanlinjin
force-pushed
the
refactor/selector-owns-target
branch
from
August 4, 2026 07:41
2b749af to
c0c8ae2
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.
Why
A
CoinSelectoris built for one target and evaluated against it throughout — yet every method took the target as a parameter. Nothing stoppedcs.excess(target_a, drain)being followed bycs.is_funded(target_b), and the metrics' correctness arguments (e.g.LowestFee::bound's proof that a changeless superset always costs more) are all stated at a fixed target, held together by convention rather than by types.The crate had already reached this conclusion one layer down:
BnbIterstored the target as a field, took it once innew, and re-passed it intometric.score/metric.boundat every node. This moves the binding up to where it belongs and deletes the re-threading.It also unblocks follow-up work: with the selector knowing its target feerate, ancestor-aware CPFP pricing (#24) can be derived internally at the right rate instead of being validated at every call site. That branch is based on this one.
What
CoinSelector::new(candidates, target)owns the target;CoinSelector::target()exposes it for metrics that read it. Twenty signatures lose a parameter: fifteen public methods (excess,implied_fee,is_funded,drain,select_until_target_met, the four*_excess, …),bnb_solutions/run_bnb, and all threeBnbMetricmethods. Across the existing tests and benches, no selector was ever evaluated against more than one target — the per-call flexibility had no consumer.Breaking changes
External
BnbMetricimplementations drop thetarget: Targetparameter:Call sites move the target from each method call to
CoinSelector::new. To evaluate a second target, build a second selector.🤖 Generated with Claude Code