Skip to content

chore(stellar): Adjust fee according to CAP-0015#680

Merged
tirumerla merged 5 commits into
mainfrom
fix/fee-bump-num-ops
Mar 11, 2026
Merged

chore(stellar): Adjust fee according to CAP-0015#680
tirumerla merged 5 commits into
mainfrom
fix/fee-bump-num-ops

Conversation

@tirumerla

@tirumerla tirumerla commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix fee bump calculation bug according to CAP-0015

Summary by CodeRabbit

  • Bug Fixes

    • Improved fee-bump transaction fee calculation to gracefully handle edge cases where maximum fee constraints are insufficient.
    • Enhanced overflow validation and error handling for transaction fee conversions.
  • New Features

    • Implemented improved fee-bump transaction fee scaling that accounts for inner transaction operation counts and applies proper mathematical scaling.

@tirumerla tirumerla requested a review from a team as a code owner March 2, 2026 22:27
@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 14dcd31b-7a51-45f6-8365-6e8ccc0119cb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request enhances fee-bump transaction handling in the Stellar domain by implementing CAP-0015-aware fee scaling. The change introduces a new helper function to correctly compute scaled fees based on operation counts and updates the fee-bump calculation logic to handle the scaling semantics, including changes to error handling and expanded test coverage.

Changes

Cohort / File(s) Summary
CAP-0015 Fee-Bump Scaling
src/domain/transaction/stellar/prepare/common.rs
Added scale_fee_for_fee_bump helper to compute ceil-divided fee scaling with validation and overflow checks. Modified calculate_fee_bump_required_fee to derive inner_num_ops and fee_bump_num_ops, apply scaling logic, and remove error condition when max_fee is insufficient (now uses scaled required_fee instead). Updated logging to include inner_fee and fee-bump metadata. Expanded test suite to cover CAP-0015 scaling semantics, overflow handling, and invalid input validation. Added extract_operations import to support operation counting.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • zeljkoX

Poem

🐰 A fee-bump tale with CAP-0015 flair,
Operations counted with meticulous care,
Ceiling divisions scale with graceful precision,
Overflow guards each computational decision,
Fee-bumps now scaled, both proper and fair! 📈

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete against the template. While it includes a Summary section with the fix described, it is missing the Testing Process section and does not reference related issues or confirm unit tests were added. Add a Testing Process section describing how the changes were tested, and complete the checklist by referencing related issues and confirming unit tests were added as mentioned in the AI summary.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references the main change: adjusting fees according to CAP-0015, which aligns with the core functionality added to handle fee-bump calculation according to this standard.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/fee-bump-num-ops

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

@codecov

codecov Bot commented Mar 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.93084% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.96%. Comparing base (99027e5) to head (438798d).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
src/domain/transaction/stellar/prepare/common.rs 91.93% 28 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
ai 0.26% <0.00%> (-0.01%) ⬇️
dev 90.95% <91.93%> (+0.05%) ⬆️
properties 0.01% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

@@            Coverage Diff             @@
##             main     #680      +/-   ##
==========================================
+ Coverage   90.91%   90.96%   +0.04%     
==========================================
  Files         288      288              
  Lines      117665   118548     +883     
==========================================
+ Hits       106981   107843     +862     
- Misses      10684    10705      +21     
Files with missing lines Coverage Δ
src/domain/transaction/stellar/prepare/common.rs 92.54% <91.93%> (+3.51%) ⬆️

... and 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/domain/transaction/stellar/prepare/common.rs (1)

219-223: ⚠️ Potential issue | 🟡 Minor

Validate negative max_fee consistently before branching.

max_fee < 0 is currently accepted on the existing-resource-fee path (Line 272 via required_fee.max(max_fee)), but rejected on other paths via scale_fee_for_fee_bump. Add a single guard near function entry for consistent behavior.

Suggested patch
 pub async fn calculate_fee_bump_required_fee<P>(
     inner_envelope: &TransactionEnvelope,
     max_fee: i64,
     provider: &P,
 ) -> Result<u32, TransactionError>
 where
     P: StellarProviderTrait + Send + Sync,
 {
+    if max_fee < 0 {
+        return Err(TransactionError::ValidationError(format!(
+            "Invalid max_fee: {max_fee} (must be non-negative)"
+        )));
+    }
+
     // CAP-0015: fee-bump effective num_ops = inner_num_ops + 1
     let inner_num_ops = extract_operations(inner_envelope)

Also applies to: 263-277

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/transaction/stellar/prepare/common.rs` around lines 219 - 223, In
calculate_fee_bump_required_fee, add an early guard that rejects negative
max_fee values before any branching so behavior is consistent across paths: if
max_fee < 0 return an appropriate TransactionError immediately; this ensures
both the path that uses required_fee.max(max_fee) and the path that calls
scale_fee_for_fee_bump see the same validation. Reference the function
calculate_fee_bump_required_fee and the helpers/use-sites scale_fee_for_fee_bump
and required_fee.max(max_fee) when making the change.
🧹 Nitpick comments (2)
src/domain/transaction/stellar/prepare/common.rs (2)

848-850: Update the stale test comment to match behavior.

The comment says “return required_fee” but the assertion expects max_fee via max(required_fee, max_fee).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/transaction/stellar/prepare/common.rs` around lines 848 - 850,
Update the stale test comment in
src/domain/transaction/stellar/prepare/common.rs to reflect actual behavior: it
should state that the function returns the larger of required_fee and max_fee
(i.e., max(required_fee, max_fee) = 200000) because the assertion checks
result.unwrap() == max_fee as u32; mention required_fee (100200), max_fee
(200000), and that max(...) yields 200000 to match the assertion using
result.unwrap().

322-330: Deduplicate repeated non-simulation scaling/conversion logic.

The same scale_fee_for_fee_bump(max_fee, ...) + u32::try_from block appears twice. Extracting a small local helper reduces drift risk.

Refactor sketch
+    let scale_and_convert_max_fee = || -> Result<u32, TransactionError> {
+        let required_fee = scale_fee_for_fee_bump(max_fee, inner_num_ops, fee_bump_num_ops)?;
+        u32::try_from(required_fee).map_err(|_| {
+            TransactionError::ValidationError(format!(
+                "Fee conversion overflow: required fee {required_fee} exceeds u32::MAX"
+            ))
+        })
+    };
+
     if xdr_needs_simulation(inner_envelope).unwrap_or(false) {
         ...
         match simulate_if_needed(inner_envelope, provider).await? {
             Some(sim_resp) => { ... }
-            None => {
-                // No simulation needed, scale max_fee for fee-bump
-                let required_fee =
-                    scale_fee_for_fee_bump(max_fee, inner_num_ops, fee_bump_num_ops)?;
-                u32::try_from(required_fee).map_err(|_| {
-                    TransactionError::ValidationError(format!(
-                        "Fee conversion overflow: required fee {required_fee} exceeds u32::MAX"
-                    ))
-                })
-            }
+            None => scale_and_convert_max_fee(),
         }
     } else {
-        // No simulation needed, scale max_fee for fee-bump
-        let required_fee = scale_fee_for_fee_bump(max_fee, inner_num_ops, fee_bump_num_ops)?;
-        u32::try_from(required_fee).map_err(|_| {
-            TransactionError::ValidationError(format!(
-                "Fee conversion overflow: required fee {required_fee} exceeds u32::MAX"
-            ))
-        })
+        scale_and_convert_max_fee()
     }

Also applies to: 333-340

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/transaction/stellar/prepare/common.rs` around lines 322 - 330, The
code repeats the sequence scale_fee_for_fee_bump(max_fee, ..., ...) followed by
u32::try_from(required_fee).map_err(|_| TransactionError::ValidationError(...)),
so extract a small helper (e.g., fn scale_and_convert_fee(max_fee: i64, num_ops:
u32, desc: &str) -> Result<u32, TransactionError>) inside the same module that
calls scale_fee_for_fee_bump, performs u32::try_from and maps the overflow to
the existing TransactionError::ValidationError message (preserving the "Fee
conversion overflow: required fee {required_fee} exceeds u32::MAX" wording),
then replace both occurrences with calls to that helper to remove duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/domain/transaction/stellar/prepare/common.rs`:
- Around line 219-223: In calculate_fee_bump_required_fee, add an early guard
that rejects negative max_fee values before any branching so behavior is
consistent across paths: if max_fee < 0 return an appropriate TransactionError
immediately; this ensures both the path that uses required_fee.max(max_fee) and
the path that calls scale_fee_for_fee_bump see the same validation. Reference
the function calculate_fee_bump_required_fee and the helpers/use-sites
scale_fee_for_fee_bump and required_fee.max(max_fee) when making the change.

---

Nitpick comments:
In `@src/domain/transaction/stellar/prepare/common.rs`:
- Around line 848-850: Update the stale test comment in
src/domain/transaction/stellar/prepare/common.rs to reflect actual behavior: it
should state that the function returns the larger of required_fee and max_fee
(i.e., max(required_fee, max_fee) = 200000) because the assertion checks
result.unwrap() == max_fee as u32; mention required_fee (100200), max_fee
(200000), and that max(...) yields 200000 to match the assertion using
result.unwrap().
- Around line 322-330: The code repeats the sequence
scale_fee_for_fee_bump(max_fee, ..., ...) followed by
u32::try_from(required_fee).map_err(|_| TransactionError::ValidationError(...)),
so extract a small helper (e.g., fn scale_and_convert_fee(max_fee: i64, num_ops:
u32, desc: &str) -> Result<u32, TransactionError>) inside the same module that
calls scale_fee_for_fee_bump, performs u32::try_from and maps the overflow to
the existing TransactionError::ValidationError message (preserving the "Fee
conversion overflow: required fee {required_fee} exceeds u32::MAX" wording),
then replace both occurrences with calls to that helper to remove duplication.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16dc170 and 0fff297.

📒 Files selected for processing (1)
  • src/domain/transaction/stellar/prepare/common.rs

@NicoMolinaOZ NicoMolinaOZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! thanks
I have been testing sending parallel txs and I haven't seen errors, looks good

Comment thread src/domain/transaction/stellar/prepare/common.rs Outdated
Comment thread src/domain/transaction/stellar/prepare/common.rs Outdated
@dylankilkenny

Copy link
Copy Markdown
Member

Good catch on this! One small issue was how this code was calculating the new fee, we were scaling the inners inclusion fee AND resource fee, when it should just be the inclusion fee, per the fee bump spec. I pushed some changes to resolve that.

Also I reverted the change that would override the max_fee that the client sets. This would break assumptions for all stellar transactions not just channel accounts txs

Signed-off-by: Dylan Kilkenny <dylankilkenny95@gmail.com>
Comment thread src/domain/transaction/stellar/prepare/common.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adjusts Stellar fee-bump fee bidding logic to comply with CAP-0015, fixing incorrect fee scaling and improving handling of Soroban resource fees during fee-bump preparation.

Changes:

  • Updates fee-bump required-fee calculation to scale inclusion-fee rate by (inner_num_ops + 1) and carry Soroban resource fee once.
  • Adds helper functions for extracting inner fees, computing required fee-bump fee, and validating max_fee conversions/limits.
  • Expands unit test coverage for Soroban/classic fee-bump scenarios and simulation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

#[cfg(test)]
mod calculate_fee_bump_required_fee_tests {
use super::*;
use crate::models::TransactionError;

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

Redundant import: use super::*; already brings TransactionError into scope from the parent module, so use crate::models::TransactionError; is unnecessary here. Removing it avoids duplication and keeps the test module imports minimal.

Suggested change
use crate::models::TransactionError;

Copilot uses AI. Check for mistakes.
Comment on lines +224 to +229
let inner_num_ops = extract_operations(inner_envelope)
.map(|ops| ops.len() as i64)
.unwrap_or(1)
.max(1);
let fee_bump_num_ops = inner_num_ops + 1; // CAP-0015

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

inner_num_ops silently falls back to 1 if extract_operations(inner_envelope) returns an error (unwrap_or(1)). This can mask XDR parsing / envelope-shape issues and lead to an incorrect required fee. Prefer propagating the extraction error (mapping it into TransactionError) and only defaulting/clamping based on the actual length when extraction succeeds.

Suggested change
let inner_num_ops = extract_operations(inner_envelope)
.map(|ops| ops.len() as i64)
.unwrap_or(1)
.max(1);
let fee_bump_num_ops = inner_num_ops + 1; // CAP-0015
let inner_num_ops = extract_operations(inner_envelope)?
.len() as i64;
let inner_num_ops = inner_num_ops.max(1);
let fee_bump_num_ops = inner_num_ops + 1; // CAP-0015

Copilot uses AI. Check for mistakes.
Comment on lines +307 to +313
let inner_inclusion_total = inner_tx_fee.saturating_sub(resource_fee);
let inner_fee_rate =
ceil_div(inner_inclusion_total, inner_num_ops).max(STELLAR_DEFAULT_TRANSACTION_FEE as i64);

debug!(
"Simulation complete. Inclusion fee: {}, Resource fee: {}, Total: {}",
inclusion_fee, resource_fee, required_fee
);
inner_fee_rate
.checked_mul(fee_bump_num_ops)
.ok_or_else(|| {

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

calculate_fee_bump_fee is documented as computing the minimum fee-bump fee, but the current math rounds up the per-op rate first (ceil_div(inner_inclusion_total, inner_num_ops)) and then multiplies by fee_bump_num_ops. This can overestimate the minimum required fee (e.g., inner inclusion total 201 over 2 ops only needs 302 for 3 ops, but the function returns 303), which may incorrectly reject valid max_fee bids. Consider computing the scaled inclusion total with a single ceiling at the end (e.g., ceil_div(inner_inclusion_total * fee_bump_num_ops, inner_num_ops) with overflow checks) and then applying the base-fee minimum constraint.

Copilot uses AI. Check for mistakes.
@NicoMolinaOZ

Copy link
Copy Markdown
Contributor

LGTM, I have reviewed using some AI tools, and it seems to be ok, let me know @tirumerla if we need some manual testing before merging

@tirumerla

Copy link
Copy Markdown
Contributor Author

LGTM, I have reviewed using some AI tools, and it seems to be ok, let me know @tirumerla if we need some manual testing before merging

Thanks should be ok to merge

@tirumerla tirumerla merged commit f720040 into main Mar 11, 2026
30 checks passed
@tirumerla tirumerla deleted the fix/fee-bump-num-ops branch March 11, 2026 20:49
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants