Skip to content

Apply the safe minimum fee floor to all wallet transactions#4192

Open
piotr-roslaniec wants to merge 4 commits into
mainfrom
feat/floor-all-wallet-tx-fees
Open

Apply the safe minimum fee floor to all wallet transactions#4192
piotr-roslaniec wants to merge 4 commits into
mainfrom
feat/floor-all-wallet-tx-fees

Conversation

@piotr-roslaniec

@piotr-roslaniec piotr-roslaniec commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What

Applies the safe minimum fee-rate floor (5 sat/vByte) plus a 25% buffer to
all wallet Bitcoin transactions - deposit sweeps, redemptions, moving funds,
and moved-funds sweeps - via the shared applyWalletTxFeeFloor helper in
pkg/tbtcpg/fee.go. A fee oracle can return an unusably low estimate in an
uncongested mempool; because these transactions are not RBF-enabled, an
underpriced one can get stuck and jam the wallet. The floor keeps the fee safely
above the relay floor while staying far below the Bridge maximum.

This carries forward the work from #4179 (opened from a fork that could not be
iterated on) rebased cleanly onto current main, and folds in the fixes from a
multi-agent review of that change.

Review fixes included

  • Restore the truncation invariant dropped when the deposit-sweep fee logic
    was extracted into the shared helper: the 25% buffer is applied to the
    truncated per-vByte rate, which is lossless only because EstimateFee returns
    an exact multiple of the vsize. Documented so a future change to that contract
    is caught rather than silently under-pricing.
  • Correct the estimatedFee/txVsize unit description (satoshis and
    vBytes, not "sat/vByte").
  • Redemption diagnostic: emit a distinct WARN in ProposeRedemption when the
    floored per-request fee share would exceed the per-request maximum fee, so
    operators can distinguish this from a generic on-chain validation failure. Fee
    behavior is unchanged; the per-request cap remains enforced by on-chain
    validation.
  • Test coverage for previously untested branches: the redemption
    raw-estimate-above-cap error, and the minimum-floor clamp on the moving-funds
    and moved-funds-sweep paths.

Notes

Testing

gofmt, go build, go vet, staticcheck, and go test ./pkg/tbtcpg/...
(91 tests) all pass on current main.

Summary by CodeRabbit

  • Bug Fixes
    • Applied consistent wallet safe-minimum fee buffering across moved-funds, moving-funds, deposit sweeps, and redemptions.
    • Stopped fee estimation/proposal when the safe minimum would exceed the configured maximum total fee.
    • Improved redemption handling by enforcing a maximum total fee bound early and emitting clearer warnings when per-request fees are likely to be rejected.
  • Tests
    • Updated and expanded fee-floor, buffering, cap, and invalid-input coverage across moving-funds, moved-funds, deposit sweep, and redemption scenarios.

mswilkison and others added 3 commits July 23, 2026 11:42
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 #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 redemption fee estimator bounds the floored/buffered fee by the total
cap (txMaxTotalFee) only. The prior comment claimed this made the floor
unable to produce a fee the Bridge would reject, which is inaccurate: the
per-request TxMaxFee (snapshotted per request) is a separate cap enforced
only by on-chain validation, and the floor can raise a request's fee share
above it while the total stays within bounds.

Correct the ProposeRedemption and EstimateRedemptionFee comments, document
that applyWalletTxFeeFloor bounds only the total fee (and its caller
precondition), and note why the final clamp is floor-safe. Comments only;
no behavior change.
Restore the truncation invariant comment dropped when the deposit-sweep
fee logic was extracted into applyWalletTxFeeFloor: the 25% buffer is
applied to the truncated per-vByte rate, which is lossless only because
EstimateFee returns an exact multiple of the vsize. Also correct the
estimatedFee/txVsize unit description (satoshis and vBytes, not
sat/vByte).

Emit a distinct warning in ProposeRedemption when the floored
per-request fee share would exceed the per-request maximum fee, so
operators can tell this apart from a generic on-chain validation
failure. Fee behavior is unchanged.

Add coverage for previously untested branches: the redemption
raw-estimate-above-cap error, and the minimum-floor clamp on the
moving-funds and moved-funds-sweep paths.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a shared wallet transaction fee-floor helper and applies it to deposit sweeps, moved-funds sweeps, moving-funds transactions, and redemptions. Redemption fee estimation now accepts and enforces the Bridge total-fee cap.

Changes

Wallet fee floor enforcement

Layer / File(s) Summary
Shared fee-floor helper
pkg/tbtcpg/fee.go, pkg/tbtcpg/fee_test.go
Adds minimum-rate enforcement, 25% fee buffering, maximum-fee capping, input validation, and table-driven coverage.
Sweep and moving-funds integration
pkg/tbtcpg/deposit_sweep.go, pkg/tbtcpg/deposit_sweep_fee_test.go, pkg/tbtcpg/moved_funds_sweep.go, pkg/tbtcpg/moved_funds_sweep_test.go, pkg/tbtcpg/moving_funds.go, pkg/tbtcpg/moving_funds_test.go
Routes fee estimation through the shared helper and updates expected fees and error assertions for floor, buffer, and cap behavior.
Redemption fee estimation flow
pkg/tbtcpg/redemptions.go, pkg/tbtcpg/redemptions_test.go
Passes txMaxTotalFee into redemption fee estimation, rejects raw fees above the cap, applies the wallet fee floor, and warns when the per-request fee exceeds txMaxFee.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProposeRedemption
  participant GetRedemptionParameters
  participant EstimateRedemptionFee
  participant applyWalletTxFeeFloor
  ProposeRedemption->>GetRedemptionParameters: fetch txMaxTotalFee
  GetRedemptionParameters-->>ProposeRedemption: return redemption parameters
  ProposeRedemption->>EstimateRedemptionFee: estimate with total-fee cap
  EstimateRedemptionFee->>applyWalletTxFeeFloor: apply floor and cap
  applyWalletTxFeeFloor-->>EstimateRedemptionFee: return adjusted fee or error
  EstimateRedemptionFee-->>ProposeRedemption: return fee result
Loading

Possibly related PRs

Suggested reviewers: lrsaturnino

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: applying a safe minimum fee floor across wallet transactions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/floor-all-wallet-tx-fees

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/tbtcpg/redemptions_test.go (1)

38-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the post-buffer cap clamp.

Add a case where the raw estimate is below the cap but its 25% buffer exceeds it (for example, 16 sat/vByte with a 4,500 sat cap should return 4,500). This verifies EstimateRedemptionFee forwards the cap to the shared helper correctly.

Proposed test case
+		"buffered fee is clamped to the cap": {
+			estimateSatPerVByte: 16,   // raw: 4000
+			txMaxTotalFee:       4500, // buffered: 5000
+			expectedFee:         4500,
+		},
🤖 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 38 - 63, Add a table-driven case
in the redemption fee tests for a raw estimate below the cap whose 25% buffer
exceeds it, such as 16 sat/vByte with a 4,500 sat maximum, and assert the
returned fee equals the cap. Keep the case focused on validating
EstimateRedemptionFee’s cap forwarding to the shared helper.
🤖 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/deposit_sweep.go`:
- Around line 664-672: Update the fee-floor calculation in the sweep flow around
applyWalletTxFeeFloor so transactionSize accounts for each deposit’s actual
input type, including larger legacy P2SH inputs, or uses a conservative
upper-bound vsize. Pass that corrected size to applyWalletTxFeeFloor so
legacy-deposit sweeps cannot fall below the intended 5 sat/vByte floor.

---

Nitpick comments:
In `@pkg/tbtcpg/redemptions_test.go`:
- Around line 38-63: Add a table-driven case in the redemption fee tests for a
raw estimate below the cap whose 25% buffer exceeds it, such as 16 sat/vByte
with a 4,500 sat maximum, and assert the returned fee equals the cap. Keep the
case focused on validating EstimateRedemptionFee’s cap forwarding to the shared
helper.
🪄 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: 7a8859ec-4ed4-482b-87e6-3be44603d1e8

📥 Commits

Reviewing files that changed from the base of the PR and between 5245f66 and 6a70608.

📒 Files selected for processing (10)
  • pkg/tbtcpg/deposit_sweep.go
  • pkg/tbtcpg/deposit_sweep_fee_test.go
  • pkg/tbtcpg/fee.go
  • pkg/tbtcpg/fee_test.go
  • pkg/tbtcpg/moved_funds_sweep.go
  • pkg/tbtcpg/moved_funds_sweep_test.go
  • pkg/tbtcpg/moving_funds.go
  • pkg/tbtcpg/moving_funds_test.go
  • pkg/tbtcpg/redemptions.go
  • pkg/tbtcpg/redemptions_test.go

Comment on lines 664 to +672
// Caveat: transactionSize assumes all deposit inputs are witness (P2WSH), per
// this function's doc comment. A sweep that includes legacy P2SH deposits has
// a larger on-wire vsize than estimated here, so the effective on-wire rate
// can land slightly below the floor for such (rare) sweeps. It still dominates
// the 1 sat/vByte relay floor this fix targets; a fully accurate floor would
// require deposit-type-aware sizing.
// rate is an exact integer here because EstimateFee returns totalFee as
// satPerVByteFee * transactionSize (an exact multiple of the size), so the
// buffer is applied without truncation loss. If that contract changes, apply
// the buffer to totalFee directly instead of to the truncated rate.
rate := totalFee / transactionSize
rate = (rate*5 + 3) / 4 // ceil(rate * 1.25)
if rate < minSweepTxSatPerVByteFee {
rate = minSweepTxSatPerVByteFee
}
totalFee = rate * transactionSize
if uint64(totalFee) > totalMaxFee {
// totalMaxFee is bounded by Bitcoin's total supply (~2.1e15 sat), far
// below math.MaxInt64, so this narrowing cast cannot overflow.
totalFee = int64(totalMaxFee)
totalFee, err = applyWalletTxFeeFloor(totalFee, transactionSize, totalMaxFee)
if err != nil {
return 0, 0, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not knowingly underprice legacy-deposit sweeps.

The estimator charges the floor against an all-P2WSH vsize, while this comment confirms P2SH inputs can make the serialized transaction larger. Those sweeps can therefore be broadcast below 5 sat/vByte—the condition this change is intended to prevent. Calculate vsize from the actual deposit input types (or use a conservative size) before applying the floor.

🤖 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/deposit_sweep.go` around lines 664 - 672, Update the fee-floor
calculation in the sweep flow around applyWalletTxFeeFloor so transactionSize
accounts for each deposit’s actual input type, including larger legacy P2SH
inputs, or uses a conservative upper-bound vsize. Pass that corrected size to
applyWalletTxFeeFloor so legacy-deposit sweeps cannot fall below the intended 5
sat/vByte floor.

…ning

Address review findings on the wallet fee-floor work:

- Add an exported ErrMaxFeeTooLow sentinel returned by applyWalletTxFeeFloor
  when the safe minimum floor exceeds the Bridge maximum, mirroring the
  existing ErrFeeTooHigh / ErrSweepTxFeeTooHigh raw-estimate sentinels so the
  floor-too-high condition is programmatically distinguishable.
- Base the redemption per-request fee warning on the worst-case (last-request)
  share floor(total/count)+total%count, matching the on-chain fee distribution,
  instead of the even floor share that could miss a remainder-only rejection.
- Cover the floor-exceeds-cap error path in the moving-funds and
  moved-funds-sweep estimators, add exact-cap boundary cases to the fee-floor
  helper test, and assert the per-request fee warning fires on the last-request
  share.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/tbtcpg/redemptions_test.go (1)

308-356: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the actual minimum-fee-floor path.

The 25 sat/vByte estimate buffers to 32 sat/vByte, so this test never invokes the 5 sat/vByte floor. Use a 1 sat/vByte estimate and assert the 1015-sat floored fee with 339/338-sat boundary caps.

Proposed test adjustment
- btcChain.SetEstimateSatPerVByteFee(1, 25)
+ btcChain.SetEstimateSatPerVByteFee(1, 1)

- txMaxFee:      3000, // 2166 <= 3000
+ txMaxFee:      339, // 339 <= 339

- txMaxFee:      2165,
+ txMaxFee:      338,

- RedemptionTxFee: big.NewInt(6496),
+ RedemptionTxFee: big.NewInt(1015),
🤖 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 308 - 356, Update the redemption
fee test around the redeemersOutputScripts table and SetEstimateSatPerVByteFee
call to use a 1 sat/vByte estimate, exercising the 5 sat/vByte minimum floor.
Expect the resulting RedemptionProposal.RedemptionTxFee to be 1015 satoshis, and
adjust the test cases to cover 339-sat per-request caps versus the 338-sat
boundary where the final 338/339 split triggers the warning.
🤖 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.

Nitpick comments:
In `@pkg/tbtcpg/redemptions_test.go`:
- Around line 308-356: Update the redemption fee test around the
redeemersOutputScripts table and SetEstimateSatPerVByteFee call to use a 1
sat/vByte estimate, exercising the 5 sat/vByte minimum floor. Expect the
resulting RedemptionProposal.RedemptionTxFee to be 1015 satoshis, and adjust the
test cases to cover 339-sat per-request caps versus the 338-sat boundary where
the final 338/339 split triggers the warning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3b3917-38ea-44c3-b0fc-3ee9b66fb2b5

📥 Commits

Reviewing files that changed from the base of the PR and between 6a70608 and 1128d43.

📒 Files selected for processing (6)
  • pkg/tbtcpg/fee.go
  • pkg/tbtcpg/fee_test.go
  • pkg/tbtcpg/moved_funds_sweep_test.go
  • pkg/tbtcpg/moving_funds_test.go
  • pkg/tbtcpg/redemptions.go
  • pkg/tbtcpg/redemptions_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/tbtcpg/fee_test.go
  • pkg/tbtcpg/moved_funds_sweep_test.go
  • pkg/tbtcpg/redemptions.go
  • pkg/tbtcpg/moving_funds_test.go

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.

2 participants