Add follower-side soft check for below-floor sweep fees#4180
Add follower-side soft check for below-floor sweep fees#4180mswilkison wants to merge 9 commits into
Conversation
A deposit sweep could be broadcast at the 1 sat/vByte relay floor when the fee oracle returned an unusably low estimate in an uncongested mempool. Because sweeps are not RBF-enabled, such a transaction can get stuck in the mempool and jam the wallet: no new sweep can be built while the previous one is unconfirmed, so eligible deposits pile up until it confirms or is evicted. Clamp the estimated sweep fee up to a conservative minimum (minSweepTxSatPerVByteFee) before the existing Bridge maximum-fee check, so a sweep is never broadcast near the relay floor. Refs threshold-network#4171 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Apply the Bridge maximum-fee check to the raw estimate before raising the fee to the minimum, and bound the minimum itself by the maximum, so raising the fee to the floor can never spuriously trigger the "exceeds maximum fee" error (the two were previously coupled in the wrong order). - Add a unit test asserting a low estimate is floored to the minimum while a healthy estimate is left unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eeds the cap - Return an error when the minimum safe fee (minSweepTxSatPerVByteFee sat/vByte) exceeds the Bridge maximum, instead of silently lowering the fee below the floor this PR is meant to enforce (lrsaturnino review). - Apply a 25% buffer over the oracle estimate (max(floor, ceil(rate*1.25))) so the fee keeps a margin during the estimate-to-broadcast delay and stays adaptive under congestion, per the threshold-network#4171 reference design. - Add a test asserting the cap-below-floor case returns an error, and update the buffered-fee expectation in the deposit sweep scenario testdata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Assert the error message contains "minimum safe sweep fee" so the test distinguishes the floor-exceeds-cap error from the raw-fee-exceeds-cap error, rather than accepting any non-nil error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Note that the static floor + 25% buffer exist only because the current sweep path is fire-and-forget and non-RBF; when RBF/fee-bumping (Part B, threshold-network#4171) lands the policy should be revisited rather than carried forward. Also note the floor is computed against a witness-only vsize estimate, so sweeps with legacy P2SH deposits can land slightly below the floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughFee estimation now applies a buffered minimum wallet fee rate with maximum-fee bounds across deposit sweeps, moved funds, moving funds, and redemptions. Deposit sweep validation also logs underpriced proposals without rejecting them, with updated tests and fixtures. ChangesWallet transaction fee floor
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ProposalEstimator
participant applyWalletTxFeeFloor
participant BridgeParameters
participant FeeProposal
ProposalEstimator->>BridgeParameters: read maximum total fee
ProposalEstimator->>applyWalletTxFeeFloor: raw fee, transaction vsize, maximum fee
applyWalletTxFeeFloor->>FeeProposal: buffered, floored, capped fee
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Checkov (3.3.8)pkg/tbtcpg/internal/test/testdata/propose_sweep_scenario_0.jsonTraceback (most recent call last): Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Cover the branch where the 25%-buffered fee exceeds the Bridge maximum and is bounded down to the cap (per CodeRabbit review on threshold-network#4172). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract the 25% buffer + minimum floor + Bridge-max bound into a shared applyWalletTxFeeFloor helper and a shared minWalletTxSatPerVByteFee const, then apply it to redemptions, moving funds, and moved funds sweeps in addition to deposit sweeps. These are all non-RBF wallet transactions that jam the wallet if they get stuck at the relay floor, so the same protection applies (per lrsaturnino review on threshold-network#4172). EstimateRedemptionFee now takes the redemption tx max total fee so the floor can be bounded by it; the caller fetches it from GetRedemptionParameters. deposit sweep fee estimation is refactored onto the shared helper with no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The on-chain WalletProposalValidator bounds the sweep fee only from above, so a misbehaving or unpatched coordination leader can propose a sweep at the ~1 sat/vByte relay floor that patched followers would still sign - the same underpricing that jams the wallet (threshold-network#4171). ValidateDepositSweepProposal now recomputes the safe minimum and warns if the proposed fee is below it. The check is intentionally log-only, not a rejection: rejecting a below-floor proposal during a mixed-version rollout would split signers and could stall signing. Hard enforcement belongs on-chain in the WalletProposalValidator or behind a coordinated all-nodes upgrade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b55e6e9 to
7d8909e
Compare
af7c525 to
9785c88
Compare
Fixes part **A** of #4171: a deposit sweep could be broadcast at the ~1 sat/vByte relay floor (a low-but-valid oracle estimate in an uncongested mempool), and since sweeps are not RBF-enabled it could get stuck and jam the wallet. Observed live on mainnet. ## Change `estimateDepositsSweepFee`: - **Errors** (instead of silently lowering the fee) if the raw estimate exceeds the Bridge maximum (uneconomical), or if the minimum safe fee (`minSweepTxSatPerVByteFee`, 5 sat/vByte) exceeds the maximum — so a sweep is never broadcast below the intended floor. - Otherwise applies `max(floor, ceil(rate × 1.25))`: a 25% buffer over the oracle estimate to keep a margin during the estimate-to-broadcast delay and stay adaptive under congestion (per the #4171 reference design), floored at 5 sat/vByte, bounded above by the Bridge maximum. The floor constant documents that it (and the buffer) is a stopgap for the current non-RBF path and should be revisited when RBF lands; the P2SH-vsize interaction is noted at the floor site. ## Tests Unit test asserts a low estimate is floored to 5, an estimate above the floor is buffered by 25%, and a Bridge maximum below the floor returns an error (message-pinned to the floor branch). The deposit-sweep scenario testdata is updated for the buffered fee. `go test ./pkg/tbtcpg/...` passes. ## Follow-ups - **#4179** — applies the same floor to redemptions, moving funds, and moved funds sweeps via a shared helper (stacked on this PR). - **#4180** — follower-side soft check that a leader's proposed sweep fee is not below the floor (log-only; full enforcement belongs on-chain in `WalletProposalValidator`). Stacked on #4179. - **Part B of #4171 (RBF + fee-bumping)** — the durable recovery fix; a stuck sweep should be replaceable rather than jamming the wallet. Larger change, targeted at FROST/ROAST. The static floor + buffer here is a stopgap and should be revisited when RBF lands. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved deposit sweep fee estimation to avoid underpriced, non-RBF-able sweeps stalling in the mempool. - Introduced a minimum safe fee-rate floor and updated failure behavior when safe fees can’t fit within the Bridge maximum. - Added a 25% buffered fee-rate (rounded up), enforced minimum/maximum caps, and updated informational sat/vByte reporting. - Enhanced proposal-time warning logs when minimum-safe-fee behavior prevents fee selection. - **Tests** - Added coverage for minimum-floor, buffer, and maximum-cap fee behaviors, including rounding and error cases. - Updated the expected sweep fee in the proposal scenario test data. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
The follower-side soft check in pkg/tbtc hand-copies the safe minimum sweep-fee rate and worst-case deposit script size from pkg/tbtcpg, because pkg/tbtcpg imports pkg/tbtc and the canonical constants cannot be imported back without a dependency cycle. Only sync comments kept them aligned, so silent drift would make the check compute a wrong floor. Export the canonical constants (MinWalletTxSatPerVByteFee, DepositScriptByteSize) and add a guard test in an external tbtc_test package - which can import pkg/tbtcpg without a cycle - that fails if the canonical values drift from the pkg/tbtc mirrors.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
pkg/tbtcpg/moved_funds_sweep_test.go (1)
436-447: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd integration coverage for every fee-floor outcome.
The updated tests exercise buffering and capping, but neither estimator verifies the minimum-rate floor or the error returned when the minimum floor cannot fit within the maximum fee.
pkg/tbtcpg/moved_funds_sweep_test.go#L436-L447: add low-estimate and floor-over-cap cases.pkg/tbtcpg/moving_funds_test.go#L660-L662: add the same minimum-floor and floor-over-cap coverage.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/tbtcpg/moved_funds_sweep_test.go` around lines 436 - 447, Add integration test cases in pkg/tbtcpg/moved_funds_sweep_test.go (lines 436-447) and pkg/tbtcpg/moving_funds_test.go (lines 660-662) covering both a low estimate raised to the minimum fee-rate floor and a maximum fee below that floor, asserting the expected fee and returned error for each outcome. Use the existing estimator test cases and symbols in each file, with no production-code changes.pkg/tbtc/sweep_fee_sync_test.go (1)
21-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThis does not actually detect a stale
pkg/tbtcmirror.The assertions compare canonical values only with test literals; changing
pkg/tbtc/deposit_sweep.goto a different private value still passes. Add a behavior-level test forValidateDepositSweepProposalusing canonicaltbtcpgvalues: no warning exactly at the threshold and a warning one satoshi below it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/tbtc/sweep_fee_sync_test.go` around lines 21 - 45, Extend TestSweepFeeConstantsMirrorTbtcpg with a behavior-level test for ValidateDepositSweepProposal that uses canonical tbtcpg values, asserting no warning at the exact minimum threshold and a warning when the value is one satoshi below it. Ensure the test exercises the pkg/tbtc mirror rather than comparing only against duplicated literals.pkg/tbtcpg/redemptions_test.go (1)
44-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing coverage for the new "raw estimate exceeds cap" branch.
EstimateRedemptionFee(pkg/tbtcpg/redemptions.go, Lines 516-521) now errors early when the raw, unbuffered estimate already exceedstxMaxTotalFee("estimated fee exceeds the maximum fee"). None of the three table cases exercise this branch — the existing error case instead hits the floor-vs-cap check insideapplyWalletTxFeeFloor. Consider adding a case whereestimateSatPerVByte * vsize > txMaxTotalFee.Example additional test case
"raw estimate above the cap returns an error": { estimateSatPerVByte: 500, txMaxTotalFee: 1000, // below raw 500*250=125000 expectErrorContains: "estimated fee exceeds the maximum fee", },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/tbtcpg/redemptions_test.go` around lines 44 - 59, Add a table-driven case in the redemption fee tests for the raw-estimate cap branch in EstimateRedemptionFee: choose estimateSatPerVByte and txMaxTotalFee values where estimateSatPerVByte multiplied by vsize exceeds the cap, and assert the error contains "estimated fee exceeds the maximum fee". Keep the existing floor-vs-cap error case unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/tbtcpg/fee.go`:
- Around line 62-68: Update the fee calculation around rate and totalFee so the
25% buffer is applied before integer division truncates fractional estimates,
preserving ceil(estimatedFee × 1.25). Then apply MinWalletTxSatPerVByteFee as
the minimum rate and retain the existing total fee calculation and cap behavior.
---
Nitpick comments:
In `@pkg/tbtc/sweep_fee_sync_test.go`:
- Around line 21-45: Extend TestSweepFeeConstantsMirrorTbtcpg with a
behavior-level test for ValidateDepositSweepProposal that uses canonical tbtcpg
values, asserting no warning at the exact minimum threshold and a warning when
the value is one satoshi below it. Ensure the test exercises the pkg/tbtc mirror
rather than comparing only against duplicated literals.
In `@pkg/tbtcpg/moved_funds_sweep_test.go`:
- Around line 436-447: Add integration test cases in
pkg/tbtcpg/moved_funds_sweep_test.go (lines 436-447) and
pkg/tbtcpg/moving_funds_test.go (lines 660-662) covering both a low estimate
raised to the minimum fee-rate floor and a maximum fee below that floor,
asserting the expected fee and returned error for each outcome. Use the existing
estimator test cases and symbols in each file, with no production-code changes.
In `@pkg/tbtcpg/redemptions_test.go`:
- Around line 44-59: Add a table-driven case in the redemption fee tests for the
raw-estimate cap branch in EstimateRedemptionFee: choose estimateSatPerVByte and
txMaxTotalFee values where estimateSatPerVByte multiplied by vsize exceeds the
cap, and assert the error contains "estimated fee exceeds the maximum fee". Keep
the existing floor-vs-cap error case unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7f91cfed-17a6-4b1f-a1e5-5375293c8f4d
📒 Files selected for processing (13)
pkg/tbtc/deposit_sweep.gopkg/tbtc/sweep_fee_sync_test.gopkg/tbtcpg/deposit_sweep.gopkg/tbtcpg/deposit_sweep_fee_test.gopkg/tbtcpg/fee.gopkg/tbtcpg/fee_test.gopkg/tbtcpg/internal/test/testdata/propose_sweep_scenario_0.jsonpkg/tbtcpg/moved_funds_sweep.gopkg/tbtcpg/moved_funds_sweep_test.gopkg/tbtcpg/moving_funds.gopkg/tbtcpg/moving_funds_test.gopkg/tbtcpg/redemptions.gopkg/tbtcpg/redemptions_test.go
| rate := estimatedFee / txVsize | ||
| rate = (rate*5 + 3) / 4 // ceil(rate * 1.25) | ||
| if rate < MinWalletTxSatPerVByteFee { | ||
| rate = MinWalletTxSatPerVByteFee | ||
| } | ||
|
|
||
| totalFee := rate * txVsize |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve the 25% buffer for fractional fee rates.
Integer-dividing before buffering underprices non-integral estimates. For estimatedFee=999 and txVsize=200, this returns 1000, while ceil(999 * 1.25) is 1249. Buffer the total fee directly (or round the raw rate up before applying the buffer), then apply the floor and cap.
Proposed fix
- rate := estimatedFee / txVsize
- rate = (rate*5 + 3) / 4 // ceil(rate * 1.25)
- if rate < MinWalletTxSatPerVByteFee {
- rate = MinWalletTxSatPerVByteFee
- }
-
- totalFee := rate * txVsize
+ floorFee := MinWalletTxSatPerVByteFee * txVsize
+ bufferedFee := estimatedFee + (estimatedFee+3)/4 // ceil(estimatedFee * 1.25)
+ totalFee := bufferedFee
+ if totalFee < floorFee {
+ totalFee = floorFee
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| rate := estimatedFee / txVsize | |
| rate = (rate*5 + 3) / 4 // ceil(rate * 1.25) | |
| if rate < MinWalletTxSatPerVByteFee { | |
| rate = MinWalletTxSatPerVByteFee | |
| } | |
| totalFee := rate * txVsize | |
| floorFee := MinWalletTxSatPerVByteFee * txVsize | |
| bufferedFee := estimatedFee + (estimatedFee+3)/4 // ceil(estimatedFee * 1.25) | |
| totalFee := bufferedFee | |
| if totalFee < floorFee { | |
| totalFee = floorFee | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pkg/tbtcpg/fee.go` around lines 62 - 68, Update the fee calculation around
rate and totalFee so the 25% buffer is applied before integer division truncates
fractional estimates, preserving ceil(estimatedFee × 1.25). Then apply
MinWalletTxSatPerVByteFee as the minimum rate and retain the existing total fee
calculation and cap behavior.
Follows up on #4172 / #4179 (lrsaturnino review): the fee floor is enforced on the fee-generation side, but nothing enforces it on the follower/validation side.
ValidateDepositSweepProposaldelegates to the chain validator, and the on-chainWalletProposalValidatoronly bounds the sweep fee from above (plusfee > 0) — no minimum. So a misbehaving or unpatched coordination leader can propose a sweep at the ~1 sat/vByte relay floor and get patched followers to sign it — the #4171 jam scenario.Change
ValidateDepositSweepProposalnow recomputes the safe minimum sweep fee (same vsize estimate + floor as the generator) and logs a warning if the proposed fee is below it.This is intentionally log-only, not a rejection. Rejecting a below-floor proposal here would, during a mixed-version rollout, split signers (patched nodes reject, unpatched nodes sign) and could stall signing below threshold. This PR gives operators detection of an underpricing leader without a liveness risk.
Why not hard enforcement here?
Hard enforcement of a fee minimum belongs where all signers apply the same rule with no version skew — on-chain in
WalletProposalValidator(tbtc-v2 repo), or behind a coordinated all-nodes keep-client upgrade. This PR is the safe keep-client-side increment; the on-chain change is the tracked follow-up.Notes
pkg/tbtc(with keep-in-sync comments) becausepkg/tbtcpgimportspkg/tbtc, so this package cannot import the canonical values without a dependency cycle.ValidateDepositSweepProposalmock harness purely to assert a log line.Summary by CodeRabbit