Skip to content

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

Merged
piotr-roslaniec merged 4 commits into
mainfrom
feat/floor-all-wallet-tx-fees
Aug 7, 2026
Merged

Apply the safe minimum fee floor to all wallet transactions#4192
piotr-roslaniec merged 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

@lrsaturnino lrsaturnino left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blocking issues found.

@piotr-roslaniec
piotr-roslaniec merged commit ab088e2 into main Aug 7, 2026
30 checks passed
@piotr-roslaniec
piotr-roslaniec deleted the feat/floor-all-wallet-tx-fees branch August 7, 2026 07:22
piotr-roslaniec added a commit that referenced this pull request Aug 18, 2026
## Summary

Adds a follower-side soft (log-only) check that the leader's proposed
deposit
sweep fee is not below the safe minimum. The on-chain
`WalletProposalValidator`
only bounds the sweep fee from above, so a misbehaving or unpatched
leader can
propose a fee at the ~1 sat/vByte relay floor that would otherwise be
signed -
the same underpricing that jams the wallet
(#4171).
Each node recomputes the safe minimum and logs a warning when the
proposal is
below it.

The check is intentionally **log-only, not a rejection**: rejecting a
below-floor proposal would, during a mixed-version rollout, split
signers
(patched nodes reject, unpatched nodes sign) and could stall signing.
Hard
enforcement belongs on-chain in the `WalletProposalValidator` or behind
a
coordinated all-nodes upgrade.

## Contents

This branch is rebased on current `main` and, in addition to the soft
check,
carries the safe-minimum sweep-fee floor applied to all wallet
transactions
(the `feat/floor-all-wallet-tx-fees` work, also open as #4179 / #4192).
The
floor commit is bundled here so the branch builds and passes CI against
`main`;
if the floor lands separately first, this branch should be rebased to
drop it.

## Review follow-ups addressed

Incorporates the multi-agent review of the soft check:

- Warn when `SweepTxFee` is `nil` instead of silently skipping the
check; a
  missing fee gets its own distinct log line.
- Compare the proposed fee with `big.Int.Cmp` instead of `Int64()`,
which is
  undefined above `MaxInt64`.
- Replace the literal-pinned constant drift guard with a direct
comparison of
the (now exported) `pkg/tbtc` mirrors against the canonical `pkg/tbtcpg`
  constants, so drift is caught regardless of which side changes. A
literal-based guard could be defeated by updating `tbtcpg` and the
literal
together while forgetting the mirror; comparing the live values closes
that
  gap.

## Supersedes

Replaces #4180, whose head lived on a fork branch that had drifted onto
an old
`main` and become `CONFLICTING`. This PR is the same work rebased clean
onto
current `main`.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Improvements**
- Enhanced minimum-fee enforcement across wallet, moving-funds, and
redemption transactions, including a safety buffer and caps for maximum
fees.
- Deposit sweep proposals now emit warnings when the provided sweep fee
is missing or below the recommended minimum.
- Fee sizing now uses a consistent worst-case estimate to keep fee
calculations aligned.
- **Bug Fixes**
- Updated fee estimation to better prevent proposals that could
otherwise be constructed with unsafe or capped-out fee values.
- Improved validation behavior for edge cases such as invalid fee sizes
or nil fee inputs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

3 participants