refactor!: eliminate redundant satisfaction weight in SelectorParams#39
refactor!: eliminate redundant satisfaction weight in SelectorParams#39evanlinjin wants to merge 3 commits intobitcoindevkit:masterfrom
Conversation
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.
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.
e0a17bc to
ffd1211
Compare
aagbotemi
left a comment
There was a problem hiding this comment.
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.
nymius
left a comment
There was a problem hiding this comment.
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.
|
Appreciate your efforts, I'd just ask that you stick to the contribution guidelines in particular with regard to the contribution workflow.
|
Added: #40
These changes can't be split — they're one refactor of
Splitting would create intermediate states where the API is half-migrated.
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. |
|
From what I gather your two primary concerns are to
|
src/selector.rs
Outdated
| spend_weight: if self.change_policy.considers_waste() { | ||
| // This code assumes that the change spend transaction is segwit. | ||
| bitcoin::TxIn::default().segwit_weight().to_wu() | ||
| + self.change_script.satisfaction_weight()?.to_wu() | ||
| } else { | ||
| // Spend weight is not needed. | ||
| 0 | ||
| }, |
There was a problem hiding this comment.
🔧 It'd be better to not assume the spending transaction is segwit, unless the change script specifically encodes a witness program. You could make it a configurable is_segwit flag as part of the ChangePolicy.
🔧 Change outputs don't typically have a spend weight of 0. Recall that the cost of change is property of the output script, which has implications for coin selection, and less so with the ChangePolicy enum. So I'd say remove the considers_waste function.
There was a problem hiding this comment.
Change outputs don't typically have a spend weight of 0.
Yes, but we aren't using that value at this point anyway.
There was a problem hiding this comment.
The spend_weight: 0 for NoDust is intentional — bdk_coin_select::ChangePolicy::min_value doesn't use spend weight in its calculations. The reason we gate on considers_waste() is that ChangeScript::from_script allows satisfaction_weight: None, so callers using NoDust with a raw script don't need to provide a satisfaction weight they'll never use. Always computing the real spend weight would force every caller to provide one, making the API more burdensome for no practical benefit.
Regarding the segwit assumption — agreed it's not ideal, but whether the future spending tx is segwit depends on ALL its inputs, not just the change script. We can't know that at coin selection time. That said, assuming segwit is a conservative estimate (slightly overestimates spend cost by ~1 WU), so it seems like a reasonable default.
src/selector.rs
Outdated
| ChangeScript::Script { | ||
| satisfaction_weight, | ||
| .. | ||
| } => satisfaction_weight.ok_or(SelectorError::MissingSatisfactionWeight), |
There was a problem hiding this comment.
We're getting the satisfaction weight from the user, so just make it non-optional rather than introduce a new error path.
There was a problem hiding this comment.
The satisfaction weight may not always be easy to compute for raw scripts (e.g. silent payments or exotic script types). Making it non-optional forces callers to compute it even when their ChangePolicy doesn't need it. The error path exists precisely to catch the mismatch at runtime rather than burden all callers upfront.
There was a problem hiding this comment.
@nymius can you explain why change output's spend weight cannot be determined for a silent payment tx?
Aren't silent payment outputs always P2TR keyspend path? If this is the case, we can remove the Option<spend weight> for ChangeScript as @ValuedMammal suggested and have a cleaner API.
Edit: I could also be misinterpreting this comment here: #18 (comment)
There was a problem hiding this comment.
Aren't silent payment outputs always P2TR keyspend path?
Yes, that's true and naturally, it also applies to silent payment change outputs.
Edit: I could also be misinterpreting this comment here: #18 (comment)
I must have eluded the almost fixed size of p2tr keypath spend witnesses there, and if not, I was giving more importance to the +-1 WU difference than I should have.
Remove the separate `ChangePolicy` type and inline its fields directly as `change_min_value` and `change_longterm_feerate` on `SelectorParams`. Also rename `dust_relay_feerate` to `change_dust_relay_feerate` for consistency, simplify `ChangeScript`'s satisfaction_weight from `Option<Weight>` to `Weight`, and remove the now-unnecessary `MissingSatisfactionWeight` error variant. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Fixes #40
Fixes #42
Summary
This PR fixes two issues with
SelectorParams:1. Redundant satisfaction weight (invalid representation)
SelectorParamspreviously had two independent fields that could both specify the change output's satisfaction weight:change_script: ScriptSource— when this is theDescriptorvariant, the satisfaction weight is derivable from the descriptor viamax_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 optionalsatisfaction_weight, or holds aDefiniteDescriptorfrom which it is derived automatically.DrainWeightsis now computed internally fromChangeScript— there is a single source of truth.ChangeScript::Descriptoralso accepts optionalsatisfaction_assets— when provided, the satisfaction weight is computed viaPlanfor a tighter estimate (useful for multisig/complex descriptors). Otherwise it falls back tomax_weight_to_satisfy.The raw
bdk_coin_select::ChangePolicyis also replaced with aChangePolicyenum (NoDust,NoDustLeastWaste) that is converted to thebdk_coin_selecttype internally. This isDebug + Clone + PartialEq + Eq.Both
ChangeScriptandChangePolicyhave 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
AbsoluteFeeimplementationFeeStrategy::AbsoluteFeewas implemented by smuggling the fee intovalue_sumand 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 directtarget_feerate: FeeRatefield.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_scriptis 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::Scriptmakes this explicit.Context
ChangePolicyTypeforbdk_coin_select::ChangePolicy#32 (comment)