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
Additional Notes
Overview
compute_split's largest-remainder dust-distribution loop (contracts/escrow/src/lib.rs:295-317, byte-identical incontracts/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:Each iteration of the outer
while dust > 0loop performs a full linear scan ofremainders(an innerforloop over allnrecipients) to find the single largest remaining value, then "consumes" it and repeats. Sincedustcan be as large asrecipients.len() - 1in 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 eachshare/remainderpair.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 firstdustentries in that sorted order — no repeated re-scanning needed. This directly compounds with #8 ("Unboundedrecipients: Vec<(Address, u32)>inrelease/release_issuerisks 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
escrow::compute_splitandmilestones::compute_split(kept in sync manually today, or via Extractcompute_splitinto a shared crate with proof of behavioral equivalence #16's shared-crate extraction if that lands first) using a single sort by(remainder desc, address asc)followed by a linear pass awarding dust to the firstdustentries, replacing the current repeated-linear-scan approach.compute_splitunder zero-recipient-adjacent, duplicate-address, and self-referential edge cases #28/Build a property-based fuzz harness forcompute_splitcovering adversarial recipient vectors #17's existing and eventual edge-case tests should pass unmodified against the new implementation.Acceptance Criteria
escrowandmilestonesescrow::testandmilestones::test(particularlytest_release_distributes_rounding_dust_by_largest_remainderandtest_adversarial_ordering_resistance) pass unmodified, proving behavior preservationcargo test --workspacepassesAdditional Notes
contracts/escrow/src/lib.rs:295-317(the O(n²) loop), identical atcontracts/milestones/src/lib.rs:279-301.soroban_sdk::Vec<T>doesn't expose a built-insort_bythe waystd::vec::Vecdoes (no-std, host-object-backed collection) — implementation will likely need either a manual sort (e.g. insertion sort is fine for reasonably smallnand would still be an improvement, though a proper comparison sort is preferable) or collecting into a local array/fixed-size structure ifno_stdconstraints makealloc-based sorting awkward; this is worth scoping carefully given the#![no_std]constraint (contracts/escrow/src/lib.rs:8) shared by all three contracts.recipients: Vec<(Address, u32)>inrelease/release_issuerisks resource-limit transaction failure #8 (unbounded recipients — directly compounding, as explained above); Build a property-based fuzz harness forcompute_splitcovering adversarial recipient vectors #17 (compute_split fuzz harness — this issue's fix must remain behaviorally identical under whatever adversarial vectors that harness eventually generates); Build a Soroban instruction/resource-budget regression benchmark suite for all entrypoints #23 (resource-budget benchmarking — the natural place to quantify this issue's improvement once both exist); Extractcompute_splitinto a shared crate with proof of behavioral equivalence #16 (shared-crate extraction — if it lands first, this issue's fix only needs to be implemented once instead of twice).