Skip to content

refactor!: eliminate redundant satisfaction weight in SelectorParams#39

Open
evanlinjin wants to merge 2 commits intobitcoindevkit:masterfrom
evanlinjin:fix/selector-params
Open

refactor!: eliminate redundant satisfaction weight in SelectorParams#39
evanlinjin wants to merge 2 commits intobitcoindevkit:masterfrom
evanlinjin:fix/selector-params

Conversation

@evanlinjin
Copy link
Member

@evanlinjin evanlinjin commented Feb 17, 2026

Fixes #40

Summary

This PR fixes two issues with SelectorParams:

1. Redundant satisfaction weight (invalid representation)

SelectorParams previously had two independent fields that could both specify the change output's satisfaction weight:

  • change_script: ScriptSource — when this is the Descriptor variant, the satisfaction weight is derivable from the descriptor via max_weight_to_satisfy.
  • change_policy: ChangePolicy — also encodes the satisfaction weight (the spend cost of the change output).

Nothing enforced that these agreed with each other, so callers could silently provide inconsistent values.

Fixed by introducing ChangeScript, which bundles a raw script with an optional satisfaction_weight, or holds a DefiniteDescriptor from which it is derived automatically. DrainWeights is now computed internally from ChangeScript — there is a single source of truth.

ChangeScript::Descriptor also accepts optional satisfaction_assets — when provided, the satisfaction weight is computed via Plan for a tighter estimate (useful for multisig/complex descriptors). Otherwise it falls back to max_weight_to_satisfy.

The raw bdk_coin_select::ChangePolicy is also replaced with a ChangePolicy enum (NoDust, NoDustLeastWaste) that is converted to the bdk_coin_select type internally. This is Debug + Clone + PartialEq + Eq.

Both ChangeScript and ChangePolicy have constructor methods to reduce boilerplate:

  • ChangeScript::from_descriptor(desc), from_descriptor_with_assets(desc, assets), from_script(script, weight)
  • ChangePolicy::no_dust(), no_dust_least_waste(rate), with a builder-style .min_value(amt)

2. Hacky AbsoluteFee implementation

FeeStrategy::AbsoluteFee was implemented by smuggling the fee into value_sum and setting the feerate to zero. This meant the coin selector had no awareness of the fee, which interacted poorly with RBF logic (the zero feerate bypasses the original tx's feerate floor). Removed in favor of a direct target_feerate: FeeRate field.

Design rationale

The satisfaction weight is a physical property of the script — it describes how much it costs to spend a particular output. It is not a policy choice. When change_script is a descriptor, the satisfaction weight is literally derived from it. The fact that it affects coin selection doesn't make it part of the change policy, any more than the dust threshold is (and dust is already derived from the script).

For the raw script case (e.g. silent payments), the satisfaction weight can't be derived and must be provided externally — but it's still a property of the script, not a policy decision. Bundling it into ChangeScript::Script makes this explicit.

Context

Previously `SelectorParams` had both `change_script: ScriptSource` and
`change_weight: DrainWeights`, which could disagree. Replace these with
`ChangeScript` (which bundles the script with its satisfaction weight)
and `ChangePolicy` (an enum instead of raw `bdk_coin_select::ChangePolicy`).
`DrainWeights` is now derived internally from `ChangeScript`.

Also removes `FeeStrategy` in favor of a direct `target_feerate` field,
as the `AbsoluteFee` variant had a hacky implementation that conflicted
with RBF logic.
@evanlinjin evanlinjin marked this pull request as draft February 17, 2026 17:24
Add ChangeScript::from_descriptor, from_descriptor_with_assets, and
from_script constructors to reduce boilerplate. Add ChangePolicy::no_dust
and no_dust_least_waste constructors with a builder-style min_value method.

Also adds satisfaction_assets field to ChangeScript::Descriptor for
tighter weight estimates via Plan, and folds satisfaction weight errors
into SelectorError.
@evanlinjin evanlinjin self-assigned this Feb 18, 2026
@evanlinjin evanlinjin marked this pull request as ready for review February 18, 2026 14:47
@evanlinjin evanlinjin added the bug Something isn't working label Feb 18, 2026
Copy link
Collaborator

@aagbotemi aagbotemi left a comment

Choose a reason for hiding this comment

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

Since the previous implementation for AbsoluteFee was broken for RBF, removing it is correct. Targeting absolute fee is a legitimate use case and the open PR on bdk_coin_select #38 adding TargetFee::absolute as a field addresses this. Once that merges, SelectorParams and to_cs_target() should be updated by exposing and mapping onto it.

Copy link
Contributor

@nymius nymius left a comment

Choose a reason for hiding this comment

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

cACK ffd1211

This is similar to #18 original's idea.

I have to review closer the RBF considerations, but I understand the removal of FeeStrategy as it is mainly needed for the absolute fee case, and it can be covered in other better ways.

I agree that ChangePolicy is a decision based on constraints, and the weight derived from ChangeScript is a property, so I don't see any concern raising from clearly reflecting that in the API.

/// Either target a specific feerate or an absolute fee.
pub fee_strategy: FeeStrategy,
/// The actual feerate of the resulting transaction may be higher due to RBF requirements or
/// rounding.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't this be considered external to the target feerate?
When including the target feerate, can't the feerate be computed externally based on RBF rules?

Copy link
Member Author

Choose a reason for hiding this comment

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

target_feerate is a lower bound, not an exact target. The actual feerate can be higher for multiple reasons — RBF feerate floor is one, but coin selection also raises it when dropping the change output reduces waste. The doc comment is just stating this fact.

Making the caller compute the RBF-adjusted feerate externally would just push complexity outward for no gain — the library already has RbfParams with the original tx feerates, so it can enforce the floor automatically.

@ValuedMammal
Copy link
Collaborator

Appreciate your efforts, I'd just ask that you stick to the contribution guidelines in particular with regard to the contribution workflow.

  • Can you link to the issue being fixed, or open one if you haven't already
  • If you do a PR can you try to fix one issue at a time
  • Can you include a test if adding a feature or fixing a bug

@evanlinjin
Copy link
Member Author

evanlinjin commented Mar 5, 2026

@ValuedMammal

Can you link to the issue being fixed, or open one if you haven't already

Added: #40

If you do a PR can you try to fix one issue at a time

These changes can't be split — they're one refactor of SelectorParams:

  • ChangeScript fixes the redundant satisfaction weight.
  • ChangePolicy enum is required by the fix — the old bdk_coin_select::ChangePolicy bundled DrainWeights which contained the satisfaction weight. Can't remove the redundancy without replacing this type.
  • Removing FeeStrategy::AbsoluteFee is entangled with the same to_cs_target() being refactored.
  • Constructor methods are just helpers for the new types.

Splitting would create intermediate states where the API is half-migrated.

Can you include a test if adding a feature or fixing a bug

You can't really "test" that two fields can't disagree anymore - the fix is that the problematic API shape no longer exists. The compiler enforces it.

@oleonardolima oleonardolima self-requested a review March 6, 2026 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SelectorParams has redundant satisfaction weight fields that can silently disagree

4 participants