Skip to content

compute_split's O(n²) largest-remainder dust distribution should be O(n log n) — compounds with #8's unbounded recipients #55

Description

@chonilius

Overview

compute_split's largest-remainder dust-distribution loop (contracts/escrow/src/lib.rs:295-317, byte-identical in contracts/milestones/src/lib.rs:279-301) is algorithmically O(n²) in the number of recipients, not O(n log n) as a largest-remainder allocation can be implemented:

let mut dust = distributable - allocated;
while dust > 0 {
    let mut best_index: u32 = 0;
    let mut best_remainder: i128 = -1;
    for (i, remainder) in remainders.iter().enumerate() {
        // ...linear scan to find the current largest remainder...
    }
    // ...award one unit to best_index, mark it consumed (-1)...
    dust -= 1;
}

Each iteration of the outer while dust > 0 loop performs a full linear scan of remainders (an inner for loop over all n recipients) to find the single largest remaining value, then "consumes" it and repeats. Since dust can be as large as recipients.len() - 1 in the worst case (the maximum possible leftover under integer division with basis points summing to exactly 10000), the total work is O(dust × n), which is O(n²) in the worst case — e.g. a 100-recipient split with maximal dust does on the order of 10,000 comparison operations just for remainder distribution, on top of the O(n) work already done to compute each share/remainder pair.

A largest-remainder allocation can be computed in O(n log n) instead: sort (index, remainder) pairs once by remainder descending (with the existing address-based tie-break as a secondary sort key), then award one unit to each of the first dust entries in that sorted order — no repeated re-scanning needed. This directly compounds with #8 ("Unbounded recipients: Vec<(Address, u32)> in release/release_issue risks resource-limit transaction failure"): the larger a team split is allowed to grow (the exact axis #8 is about bounding), the worse this algorithm's quadratic cost gets, meaning the two issues' severities are multiplicative, not independent — a fix for #8 that raises or removes the practical recipient-count ceiling makes this issue's cost curve correspondingly steeper, and a fix for this issue increases the recipient count #8's eventual bound can safely allow before hitting Soroban's CPU-instruction budget.

This is distinct from #17 ("property-based fuzz harness for compute_split") and #23 ("Soroban instruction/resource-budget regression benchmark suite") — both of those are about testing/measuring behavior and cost; this issue is a specific, concrete algorithmic fix for an inefficiency identified by reading the code directly, independent of whether either testing effort has landed yet.

Requirements

Acceptance Criteria

  • Dust distribution reimplemented as O(n log n) in both escrow and milestones
  • All existing tests in both escrow::test and milestones::test (particularly test_release_distributes_rounding_dust_by_largest_remainder and test_adversarial_ordering_resistance) pass unmodified, proving behavior preservation
  • New test with a larger recipient count (e.g. 50+) exercising multiple dust units distributed correctly under the new algorithm
  • cargo test --workspace passes

Additional Notes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignperformancePerformance/optimization issuevery hardVery difficult task, expert-level effort required

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions