Skip to content

fix(utils): convert _quickSort tail recursion to iterative loop (fixes #6289)#6476

Open
yw13931835525-cyber wants to merge 1 commit intoOpenZeppelin:masterfrom
yw13931835525-cyber:fix/quicksort-iterative-stack-depth
Open

fix(utils): convert _quickSort tail recursion to iterative loop (fixes #6289)#6476
yw13931835525-cyber wants to merge 1 commit intoOpenZeppelin:masterfrom
yw13931835525-cyber:fix/quicksort-iterative-stack-depth

Conversation

@yw13931835525-cyber
Copy link
Copy Markdown

Summary

Fixes #6289 — converts _quickSort() from a two-recursive-call approach to an iterative loop for the larger partition, raising the maximum sortable array size from ~170 elements to >1000 elements.

Motivation

The original _quickSort() makes two recursive calls per invocation. Since Solidity does not optimize tail recursion, each recursive call consumes EVM stack depth (~1024 limit, ~169 recursion depth for this function). Arrays of 170+ elements caused EvmError: StackOverflow.

The proposed fix converts one recursive call into a while (true) loop:

  • The smaller partition is processed recursively (bounded depth)
  • The larger partition is processed iteratively (no stack growth)

Changes

contracts/utils/Arrays.sol

-    function _quickSort(uint256 begin, uint256 end, ...) private pure {
+    // @custom:oz-reentrancy-enginespec note
+    // The function reverts if called with arrays larger than ~170 elements due to EVM stack depth limits
+    // when using two recursive calls. This limit is raised to >1000 elements by using an iterative
+    // approach for the larger partition (tail recursion optimization via loop).
+    function _quickSort(uint256 begin, uint256 end, ...) private pure {
         unchecked {
-            if (end - begin < 0x40) return;
-            ...
-            _quickSort(begin, pos, comp); // Sort the left side
-            _quickSort(pos + 0x20, end, comp); // Sort the right side
+            while (true) {
+                if (end - begin < 0x40) return;
+                ...
+                _swap(begin, pos);
+                if (pos - begin < end - (pos + 0x20)) {
+                    _quickSort(begin, pos, comp);
+                    begin = pos + 0x20;
+                } else {
+                    _quickSort(pos + 0x20, end, comp);
+                    end = pos;
+                }
+            }
         }
     }

Testing

Added two new tests:

  • testSortLargeArrayDescending — N=500, descending input, verifies ascending output
  • testSortVeryLargeArrayRandom — N=1000, deterministic pseudo-random input

Note: forge is not available in this environment. Tests should be run via GitHub Actions CI on the PR.

References

Fixes OpenZeppelin#6289

Converts one of the two recursive calls in _quickSort to a while loop,
targeting the larger partition. This raises the maximum sortable array
size from ~170 elements (limited by EVM stack depth of 1024) to >1000
elements.

The fix follows the approach described in the issue:
- The larger partition is processed iteratively (loop)
- The smaller partition is processed recursively (call)
- This maintains O(n log n) time complexity while eliminating stack overflow

Added testSortLargeArrayDescending (N=500) and testSortVeryLargeArrayRandom (N=1000) to verify the fix.
@yw13931835525-cyber yw13931835525-cyber requested a review from a team as a code owner April 11, 2026 17:19
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 11, 2026

⚠️ No Changeset found

Latest commit: 6779c66

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Walkthrough

The _quickSort function in the Arrays utility contract has been optimized using tail-recursion techniques to increase the maximum sortable array size. The function previously used two recursive calls, which caused stack overflow for arrays larger than approximately 170 elements due to Solidity's lack of tail-recursion optimization. The updated implementation replaces one recursive call with a loop-based iteration by comparing partition sizes and updating loop boundaries. Additionally, two new test functions have been added to validate sorting behavior on large arrays with 500 and 1000 elements, extending test coverage for previously problematic array sizes.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: converting tail recursion to an iterative loop in _quickSort.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining motivation, implementation details, and added tests.
Linked Issues check ✅ Passed The PR implements the exact solution proposed in issue #6289: converting the larger partition recursion to a while(true) loop, addressing the stack overflow limitation for arrays ≥170 elements.
Out of Scope Changes check ✅ Passed All changes are directly related to the stated objective: modifying _quickSort() implementation and adding corresponding tests; no unrelated code modifications present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 (2)
test/utils/Arrays.t.sol (1)

410-412: Consider using assertTrue() or reusing the _assertSort helper.

The assertions compare booleans to true, which is less idiomatic than using assertTrue(). Additionally, testSortVeryLargeArrayRandom could reuse the existing _assertSort helper.

♻️ Suggested improvements

For testSortLargeArrayDescending (strict ascending check):

         for (uint256 i = 1; i < N; i++) {
-            assertEq(a[i - 1] < a[i], true);
+            assertTrue(a[i - 1] < a[i]);
         }

For testSortVeryLargeArrayRandom (non-decreasing check), reuse the existing helper:

         Arrays.sort(a);
-        // Verify sorted ascending
-        for (uint256 i = 1; i < N; i++) {
-            assertEq(a[i - 1] <= a[i], true);
-        }
+        _assertSort(a);

Also applies to: 427-429

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

In `@test/utils/Arrays.t.sol` around lines 410 - 412, Replace boolean comparisons
like assertEq(a[i - 1] < a[i], true) with the idiomatic assertTrue(...) and
reuse the existing _assertSort helper where appropriate: update
testSortVeryLargeArrayRandom to call _assertSort(a) (which performs the
non-decreasing check) instead of duplicating the loop, and ensure
testSortLargeArrayDescending uses assertTrue for the strict ascending check
(e.g., assertTrue(a[i - 1] < a[i])) to match intent; update occurrences around
testSortVeryLargeArrayRandom and testSortLargeArrayDescending accordingly.
contracts/utils/Arrays.sol (1)

115-118: Consider clarifying the comment to reflect current behavior.

The comment describes the old limitation before explaining the fix, which could be slightly confusing. Consider rephrasing to emphasize the current capability.

📝 Suggested documentation improvement
-    // `@custom`:oz-reentrancy-enginespec note
-    // The function reverts if called with arrays larger than ~170 elements due to EVM stack depth limits
-    // when using two recursive calls. This limit is raised to >1000 elements by using an iterative
-    // approach for the larger partition (tail recursion optimization via loop).
+    // `@custom`:oz-reentrancy-enginespec note
+    // Uses tail-recursion optimization (iterative loop for larger partition, recursion for smaller)
+    // to support arrays >1000 elements. Without this optimization, EVM stack depth limits would
+    // cause stack overflow for arrays ≥170 elements due to dual recursive calls.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/utils/Arrays.sol` around lines 115 - 118, Update the comment block
tagged with `@custom`:oz-reentrancy-enginespec to lead with the current behavior
rather than the historical limitation: explicitly state that the function now
supports partitions larger than ~1000 elements by converting one recursive
branch to an iterative loop (tail-recursion elimination), and then note that
previously a purely recursive approach would revert around ~170 elements due to
EVM stack depth. Keep the same technical details (approximate limits and that
the larger partition is handled iteratively) and replace the existing wording so
readers immediately see the current capability before the historical note.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/utils/Arrays.t.sol`:
- Around line 419-424: The assignment to variable seed uses keccak256 which
returns bytes32, causing a type mismatch; change the assignment to cast the
keccak256 result to uint256 (e.g., set seed = uint256(keccak256(abi.encode(seed,
i)))) so seed remains uint256 and subsequent use in a[i] = uint256(seed) % (N *
10) is valid; update the line that calls keccak256(abi.encode(seed, i))
accordingly and keep references to seed, keccak256, abi.encode, and array a.

---

Nitpick comments:
In `@contracts/utils/Arrays.sol`:
- Around line 115-118: Update the comment block tagged with
`@custom`:oz-reentrancy-enginespec to lead with the current behavior rather than
the historical limitation: explicitly state that the function now supports
partitions larger than ~1000 elements by converting one recursive branch to an
iterative loop (tail-recursion elimination), and then note that previously a
purely recursive approach would revert around ~170 elements due to EVM stack
depth. Keep the same technical details (approximate limits and that the larger
partition is handled iteratively) and replace the existing wording so readers
immediately see the current capability before the historical note.

In `@test/utils/Arrays.t.sol`:
- Around line 410-412: Replace boolean comparisons like assertEq(a[i - 1] <
a[i], true) with the idiomatic assertTrue(...) and reuse the existing
_assertSort helper where appropriate: update testSortVeryLargeArrayRandom to
call _assertSort(a) (which performs the non-decreasing check) instead of
duplicating the loop, and ensure testSortLargeArrayDescending uses assertTrue
for the strict ascending check (e.g., assertTrue(a[i - 1] < a[i])) to match
intent; update occurrences around testSortVeryLargeArrayRandom and
testSortLargeArrayDescending accordingly.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 241b02cd-b4c7-4acb-98d5-707b222ceec3

📥 Commits

Reviewing files that changed from the base of the PR and between 9cfdccd and 6779c66.

📒 Files selected for processing (2)
  • contracts/utils/Arrays.sol
  • test/utils/Arrays.t.sol

Comment thread test/utils/Arrays.t.sol
Comment on lines +419 to +424
uint256 seed = 42;
for (uint256 i = 0; i < N; i++) {
// Deterministic pseudo-random for reproducibility
seed = keccak256(abi.encode(seed, i));
a[i] = uint256(seed) % (N * 10);
}
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.

⚠️ Potential issue | 🔴 Critical

Type mismatch: keccak256 returns bytes32, not uint256.

Line 422 assigns a bytes32 value to a uint256 variable without explicit conversion. In Solidity 0.8.x, this implicit conversion is not allowed and will cause a compilation error.

🐛 Proposed fix
         uint256 seed = 42;
         for (uint256 i = 0; i < N; i++) {
             // Deterministic pseudo-random for reproducibility
-            seed = keccak256(abi.encode(seed, i));
-            a[i] = uint256(seed) % (N * 10);
+            seed = uint256(keccak256(abi.encode(seed, i)));
+            a[i] = seed % (N * 10);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint256 seed = 42;
for (uint256 i = 0; i < N; i++) {
// Deterministic pseudo-random for reproducibility
seed = keccak256(abi.encode(seed, i));
a[i] = uint256(seed) % (N * 10);
}
uint256 seed = 42;
for (uint256 i = 0; i < N; i++) {
// Deterministic pseudo-random for reproducibility
seed = uint256(keccak256(abi.encode(seed, i)));
a[i] = seed % (N * 10);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/utils/Arrays.t.sol` around lines 419 - 424, The assignment to variable
seed uses keccak256 which returns bytes32, causing a type mismatch; change the
assignment to cast the keccak256 result to uint256 (e.g., set seed =
uint256(keccak256(abi.encode(seed, i)))) so seed remains uint256 and subsequent
use in a[i] = uint256(seed) % (N * 10) is valid; update the line that calls
keccak256(abi.encode(seed, i)) accordingly and keep references to seed,
keccak256, abi.encode, and array a.

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.

Function _quickSort() can be optimized

1 participant