Skip to content

perf(escrow,milestones): O(n log n) largest-remainder dust distribution - #61

Merged
chonilius merged 2 commits into
MergeFi:mainfrom
aristotle224:perf/compute-split-ologn
Aug 17, 2026
Merged

perf(escrow,milestones): O(n log n) largest-remainder dust distribution#61
chonilius merged 2 commits into
MergeFi:mainfrom
aristotle224:perf/compute-split-ologn

Conversation

@aristotle224

Copy link
Copy Markdown
Contributor

Description

Closes #55 — makes compute_split's dust-distribution step O(n log n) instead of O(n²).

Before

The dust loop (contracts/escrow/src/lib.rs:295-317, byte-identical in contracts/milestones/src/lib.rs:279-301) did a full linear scan over all n recipients per dust unit to find the largest remaining remainder. With up to n-1 dust units worst case, that's ~n²/2 comparisons on every split.

After

Build an (index, remainder, address) record per recipient, heapsort once by (remainder desc, address asc, index asc), then award one unit to each of the first dust entries in a linear pass.

  • Behavior is unchanged: each award only consumes the chosen entry and never affects any other remainder, so "repeatedly pick the max" ≡ "take the top dust of a single sort".
  • The index tie-break preserves the old loop's first-occurrence-wins among duplicate addresses.
  • dust ≤ n-1 always, so the first dust sorted entries always exist.
  • Heapsort is in-place, non-recursive, allocation-free — safe under #![no_std] (soroban Vec has no sort_by).

Motivation

Compounds with #8 (unbounded recipients): the quadratic cost scaled with both recipient count and dust. This flattens the curve so recipient-count growth (the #8 axis) no longer carries a per-dust O(n) penalty.

Tests

  • New test_large_split_distributes_dust_by_largest_remainder (escrow + milestones): 60 recipients, 40 dust units, 47 tied remainders exercising the address tie-break; every share matched against a reference copy of the old O(n²) approach, total payout = total distributable.
  • All existing tests pass unmodified, including test_adversarial_ordering_resistance and both *_rounding_dust_by_largest_remainder tests.
  • cargo test --workspace, cargo clippy --workspace --all-targets, and cargo fmt --check all clean.

Replace the O(n²) repeated-linear-scan dust loop in compute_split with a
single heapsort by (remainder desc, address asc, index asc) followed by a
linear pass awarding one unit to each of the first `dust` entries. The two
approaches are equivalent: each award only consumes the chosen entry, so
repeated largest-remainder selection is identical to taking the top-dust
sorted entries. Heapsort is in-place, non-recursive, and allocation-free,
so it is safe under #![no_std].

Adds test_large_split_distributes_dust_by_largest_remainder to both
contracts: 60 recipients with 40 dust units and 47 tied remainders,
verified against a reference copy of the old O(n²) scan.

Closes MergeFi#55.
@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

@aristotle224 is attempting to deploy a commit to the chonilius' projects Team on Vercel.

A member of the Team first needs to authorize it.

@chonilius
chonilius merged commit 688902c into MergeFi:main Aug 17, 2026
0 of 3 checks passed
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.

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

2 participants