From 4fec53077d4b162e19e06d60def755069e7f5e92 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 16 Jun 2024 22:30:08 +0200 Subject: [PATCH 01/36] implement binary heap --- contracts/utils/Comparators.sol | 13 ++ contracts/utils/structs/Heaps.sol | 213 ++++++++++++++++++++++++++++++ test/utils/structs/Heaps.t.sol | 135 +++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 contracts/utils/Comparators.sol create mode 100644 contracts/utils/structs/Heaps.sol create mode 100644 test/utils/structs/Heaps.t.sol diff --git a/contracts/utils/Comparators.sol b/contracts/utils/Comparators.sol new file mode 100644 index 00000000000..3a63aa0e8ee --- /dev/null +++ b/contracts/utils/Comparators.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +library Comparators { + function lt(uint256 a, uint256 b) internal pure returns (bool) { + return a < b; + } + + function gt(uint256 a, uint256 b) internal pure returns (bool) { + return a > b; + } +} diff --git a/contracts/utils/structs/Heaps.sol b/contracts/utils/structs/Heaps.sol new file mode 100644 index 00000000000..c34fc5e8518 --- /dev/null +++ b/contracts/utils/structs/Heaps.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SafeCast} from "../math/SafeCast.sol"; +import {Comparators} from "../Comparators.sol"; +import {Panic} from "../Panic.sol"; + +library Heaps { + using SafeCast for uint256; + + /** + * A Heap is represented as an array of Node objects. In this array we store two overlapping structures: + * - A tree structure, where index 0 is the root, and for each index i, the childs are 2*i+1 and 2*i+2. + * For each index in this tree we have the `index` pointer that gives the position of the corresponding value. + * - An array of values (payload). At each index we store a uint256 `value` and `lookup`, the index of the node + * that points to this value. + * + * Some invariant: + * ``` + * i == heap.data[heap[data].index].lookup // for all index i + * i == heap.data[heap[data].lookup].index // for all index i + * ``` + * + * The structure is order so that each node is bigger then its parent. An immediate consequence is that the + * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` + * + * This stucture is designed for the following complexities: + * - insert: 0(log(n)) + * - pop (remove smallest value in set): O(log(n)) + * - top (get smallest value in set): O(1) + */ + struct Heap { + Node[] data; + } + + // Index and lookup are bounded by the size of the structure. We could reasonnably limit that to uint20 (1 billion elemets) + // Then could also limit the value to uint216 so that the entier structure fits into a single slot. + struct Node { + uint256 value; + uint32 index; // position -> value + uint32 lookup; // value -> position + } + + /** + * @dev Lookup the root element of the heap. + */ + function top(Heap storage self) internal view returns (uint256) { + return self.data[self.data[0].index].value; + } + + /** + * @dev Remove (and return) the root element for the heap using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function pop(Heap storage self) internal returns (uint256) { + return pop(self, Comparators.lt); + } + + /** + * @dev Remove (and return) the root element for the heap using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function pop(Heap storage self, function(uint256, uint256) view returns (bool) comp) internal returns (uint256) { + uint32 length = size(self); + + if (length == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + + uint32 last = length - 1; // could be unchecked (check above) + + // get root location (in the data array) and value + uint32 rootIdx = self.data[0].index; + uint256 rootValue = self.data[rootIdx].value; + + // if root is not the last element of the data array (that will get pop-ed), reorder the data array. + if (rootIdx != last) { + // get details about the value stored in the last element of the array (that will get pop-ed) + uint32 lastDataIdx = self.data[last].lookup; + uint256 lastDataValue = self.data[last].value; + // copy these values to the location of the root (that is safe, and that we no longer use) + self.data[rootIdx].value = lastDataValue; + self.data[rootIdx].lookup = lastDataIdx; + // update the tree node that used to point to that last element (value now located where the root was) + self.data[lastDataIdx].index = rootIdx; + } + + // get last leaf location (in the data array) and value + uint32 lastIdx = self.data[last].index; + uint256 lastValue = self.data[lastIdx].value; + + // move the last leaf to the root, pop last leaf ... + self.data[0].index = lastIdx; + self.data[lastIdx].lookup = 0; + self.data.pop(); + + // ... and heapify + _heapifyDown(self, last, 0, lastValue, comp); + + // return root value + return rootValue; + } + + /** + * @dev Insert a new element in the heap using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function insert(Heap storage self, uint256 value) internal { + insert(self, value, Comparators.lt); + } + + /** + * @dev Insert a new element in the heap using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function insert(Heap storage self, uint256 value, function(uint256, uint256) view returns (bool) comp) internal { + uint32 length = size(self); + self.data.push(Node({index: length, lookup: length, value: value})); + _heapifyUp(self, length, value, comp); + } + + /** + * @dev Returns the number of elements in the heap. + */ + function size(Heap storage self) internal view returns (uint32) { + return self.data.length.toUint32(); + } + + /* + * @dev Swap node `i` and `j` in the tree. + */ + function _swap(Heap storage self, uint32 i, uint32 j) private { + uint32 ii = self.data[i].index; + uint32 jj = self.data[j].index; + // update pointers to the data (swap the value) + self.data[i].index = jj; + self.data[j].index = ii; + // update lookup pointers for consistency + self.data[ii].lookup = j; + self.data[jj].lookup = i; + } + + /** + * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a + * comparator, and moving toward the leafs of the underlying tree. + * + * Note: This is a private function that is called in a trusted context with already cached parameters. `length` + * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These + * parameters are not verified. It is the caller role to make sure the parameters are correct. + */ + function _heapifyDown( + Heap storage self, + uint32 length, + uint32 pos, + uint256 value, + function(uint256, uint256) view returns (bool) comp + ) private { + uint32 left = 2 * pos + 1; + uint32 right = 2 * pos + 2; + + if (right < length) { + uint256 lValue = self.data[self.data[left].index].value; + uint256 rValue = self.data[self.data[right].index].value; + if (comp(lValue, value) || comp(rValue, value)) { + if (comp(lValue, rValue)) { + _swap(self, pos, left); + _heapifyDown(self, length, left, value, comp); + } else { + _swap(self, pos, right); + _heapifyDown(self, length, right, value, comp); + } + } + } else if (left < length) { + uint256 lValue = self.data[self.data[left].index].value; + if (comp(lValue, value)) { + _swap(self, pos, left); + _heapifyDown(self, length, left, value, comp); + } + } + } + + /** + * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a + * comparator, and moving toward the root of the underlying tree. + * + * Note: This is a private function that is called in a trusted context with already cached parameters. `value` + * could be extracted from `self` and `pos`, but that would require redundant storage read. This parameters is not + * verified. It is the caller role to make sure the parameters are correct. + */ + function _heapifyUp( + Heap storage self, + uint32 pos, + uint256 value, + function(uint256, uint256) view returns (bool) comp + ) private { + unchecked { + while (pos > 0) { + uint32 parent = (pos - 1) / 2; + uint256 parentValue = self.data[self.data[parent].index].value; + if (comp(parentValue, value)) break; + _swap(self, pos, parent); + pos = parent; + } + } + } +} diff --git a/test/utils/structs/Heaps.t.sol b/test/utils/structs/Heaps.t.sol new file mode 100644 index 00000000000..1a132fecb04 --- /dev/null +++ b/test/utils/structs/Heaps.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Heaps} from "@openzeppelin/contracts/utils/structs/Heaps.sol"; +import {Comparators} from "@openzeppelin/contracts/utils/Comparators.sol"; + +contract HeapTest is Test { + using Heaps for *; + + Heaps.Heap internal heap; + + function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal { + for (uint32 i = 0; i < heap.size(); ++i) { + // lookups + assertEq(i, heap.data[heap.data[i].index].lookup); + + // ordering: each node has a value bigger then its parent + if (i > 0) + assertFalse(comp(heap.data[heap.data[i].index].value, heap.data[heap.data[(i - 1) / 2].index].value)); + } + } + + function testUnit() public { + // + assertEq(heap.size(), 0); + _validateHeap(Comparators.lt); + + heap.insert(712); // 712 + assertEq(heap.size(), 1); + _validateHeap(Comparators.lt); + + heap.insert(20); // 20, 712 + assertEq(heap.size(), 2); + _validateHeap(Comparators.lt); + + heap.insert(4337); // 20, 712, 4337 + assertEq(heap.size(), 3); + _validateHeap(Comparators.lt); + + assertEq(heap.pop(), 20); // 712, 4337 + assertEq(heap.size(), 2); + _validateHeap(Comparators.lt); + + heap.insert(1559); // 712, 1559, 4337 + assertEq(heap.size(), 3); + _validateHeap(Comparators.lt); + + heap.insert(155); // 155, 712, 1559, 4337 + assertEq(heap.size(), 4); + _validateHeap(Comparators.lt); + + heap.insert(7702); // 155, 712, 1559, 4337, 7702 + assertEq(heap.size(), 5); + _validateHeap(Comparators.lt); + + assertEq(heap.pop(), 155); // 712, 1559, 4337, 7702 + assertEq(heap.size(), 4); + _validateHeap(Comparators.lt); + + heap.insert(721); // 712, 721, 1559, 4337, 7702 + assertEq(heap.size(), 5); + _validateHeap(Comparators.lt); + + assertEq(heap.pop(), 712); // 721, 1559, 4337, 7702 + assertEq(heap.size(), 4); + _validateHeap(Comparators.lt); + + assertEq(heap.pop(), 721); // 1559, 4337, 7702 + assertEq(heap.size(), 3); + _validateHeap(Comparators.lt); + + assertEq(heap.pop(), 1559); // 4337, 7702 + assertEq(heap.size(), 2); + _validateHeap(Comparators.lt); + + assertEq(heap.pop(), 4337); // 7702 + assertEq(heap.size(), 1); + _validateHeap(Comparators.lt); + + assertEq(heap.pop(), 7702); // + assertEq(heap.size(), 0); + _validateHeap(Comparators.lt); + } + + function testFuzz(uint256[] calldata input) public { + vm.assume(input.length < 0x20); + + uint256 min = type(uint256).max; + for (uint256 i; i < input.length; ++i) { + heap.insert(input[i]); + _validateHeap(Comparators.lt); + + min = Math.min(min, input[i]); + assertEq(heap.top(), min); + } + + uint256 max = 0; + for (uint256 i; i < input.length; ++i) { + uint256 top = heap.top(); + uint256 pop = heap.pop(); + _validateHeap(Comparators.lt); + + assertEq(pop, top); + assertGe(pop, max); + max = pop; + } + } + + function testFuzzGt(uint256[] calldata input) public { + vm.assume(input.length < 0x20); + + uint256 max = 0; + for (uint256 i; i < input.length; ++i) { + heap.insert(input[i], Comparators.gt); + _validateHeap(Comparators.gt); + + max = Math.max(max, input[i]); + assertEq(heap.top(), max); + } + + uint256 min = type(uint256).max; + for (uint256 i; i < input.length; ++i) { + uint256 top = heap.top(); + uint256 pop = heap.pop(Comparators.gt); + _validateHeap(Comparators.gt); + + assertEq(pop, top); + assertLe(pop, min); + min = pop; + } + } +} From 8fa2eeb87554e689b64be90d89360036d2636176 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 16 Jun 2024 22:37:47 +0200 Subject: [PATCH 02/36] codespell & lib naming --- contracts/mocks/Stateless.sol | 1 + .../utils/structs/{Heaps.sol => Heap.sol} | 41 +++++++++++-------- .../utils/structs/{Heaps.t.sol => Heap.t.sol} | 6 +-- 3 files changed, 28 insertions(+), 20 deletions(-) rename contracts/utils/structs/{Heaps.sol => Heap.sol} (88%) rename test/utils/structs/{Heaps.t.sol => Heap.t.sol} (96%) diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 6bf78babc7d..0f44f4bd858 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -22,6 +22,7 @@ import {ERC165} from "../utils/introspection/ERC165.sol"; import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol"; import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol"; import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; +import {Heap} from "../utils/structs/Heap.sol"; import {Math} from "../utils/math/Math.sol"; import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; diff --git a/contracts/utils/structs/Heaps.sol b/contracts/utils/structs/Heap.sol similarity index 88% rename from contracts/utils/structs/Heaps.sol rename to contracts/utils/structs/Heap.sol index c34fc5e8518..8dd70490205 100644 --- a/contracts/utils/structs/Heaps.sol +++ b/contracts/utils/structs/Heap.sol @@ -6,12 +6,12 @@ import {SafeCast} from "../math/SafeCast.sol"; import {Comparators} from "../Comparators.sol"; import {Panic} from "../Panic.sol"; -library Heaps { +library Heap { using SafeCast for uint256; /** * A Heap is represented as an array of Node objects. In this array we store two overlapping structures: - * - A tree structure, where index 0 is the root, and for each index i, the childs are 2*i+1 and 2*i+2. + * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. * For each index in this tree we have the `index` pointer that gives the position of the corresponding value. * - An array of values (payload). At each index we store a uint256 `value` and `lookup`, the index of the node * that points to this value. @@ -25,18 +25,18 @@ library Heaps { * The structure is order so that each node is bigger then its parent. An immediate consequence is that the * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` * - * This stucture is designed for the following complexities: + * This structure is designed for the following complexities: * - insert: 0(log(n)) * - pop (remove smallest value in set): O(log(n)) * - top (get smallest value in set): O(1) */ - struct Heap { - Node[] data; + struct Uint256Heap { + Uint256HeapNode[] data; } - // Index and lookup are bounded by the size of the structure. We could reasonnably limit that to uint20 (1 billion elemets) + // Index and lookup are bounded by the size of the structure. We could reasonably limit that to uint20 (1 billion entries) // Then could also limit the value to uint216 so that the entier structure fits into a single slot. - struct Node { + struct Uint256HeapNode { uint256 value; uint32 index; // position -> value uint32 lookup; // value -> position @@ -45,7 +45,7 @@ library Heaps { /** * @dev Lookup the root element of the heap. */ - function top(Heap storage self) internal view returns (uint256) { + function top(Uint256Heap storage self) internal view returns (uint256) { return self.data[self.data[0].index].value; } @@ -55,7 +55,7 @@ library Heaps { * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ - function pop(Heap storage self) internal returns (uint256) { + function pop(Uint256Heap storage self) internal returns (uint256) { return pop(self, Comparators.lt); } @@ -65,7 +65,10 @@ library Heaps { * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ - function pop(Heap storage self, function(uint256, uint256) view returns (bool) comp) internal returns (uint256) { + function pop( + Uint256Heap storage self, + function(uint256, uint256) view returns (bool) comp + ) internal returns (uint256) { uint32 length = size(self); if (length == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); @@ -110,7 +113,7 @@ library Heaps { * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ - function insert(Heap storage self, uint256 value) internal { + function insert(Uint256Heap storage self, uint256 value) internal { insert(self, value, Comparators.lt); } @@ -120,23 +123,27 @@ library Heaps { * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ - function insert(Heap storage self, uint256 value, function(uint256, uint256) view returns (bool) comp) internal { + function insert( + Uint256Heap storage self, + uint256 value, + function(uint256, uint256) view returns (bool) comp + ) internal { uint32 length = size(self); - self.data.push(Node({index: length, lookup: length, value: value})); + self.data.push(Uint256HeapNode({index: length, lookup: length, value: value})); _heapifyUp(self, length, value, comp); } /** * @dev Returns the number of elements in the heap. */ - function size(Heap storage self) internal view returns (uint32) { + function size(Uint256Heap storage self) internal view returns (uint32) { return self.data.length.toUint32(); } /* * @dev Swap node `i` and `j` in the tree. */ - function _swap(Heap storage self, uint32 i, uint32 j) private { + function _swap(Uint256Heap storage self, uint32 i, uint32 j) private { uint32 ii = self.data[i].index; uint32 jj = self.data[j].index; // update pointers to the data (swap the value) @@ -156,7 +163,7 @@ library Heaps { * parameters are not verified. It is the caller role to make sure the parameters are correct. */ function _heapifyDown( - Heap storage self, + Uint256Heap storage self, uint32 length, uint32 pos, uint256 value, @@ -195,7 +202,7 @@ library Heaps { * verified. It is the caller role to make sure the parameters are correct. */ function _heapifyUp( - Heap storage self, + Uint256Heap storage self, uint32 pos, uint256 value, function(uint256, uint256) view returns (bool) comp diff --git a/test/utils/structs/Heaps.t.sol b/test/utils/structs/Heap.t.sol similarity index 96% rename from test/utils/structs/Heaps.t.sol rename to test/utils/structs/Heap.t.sol index 1a132fecb04..7fb289f566d 100644 --- a/test/utils/structs/Heaps.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -4,13 +4,13 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Heaps} from "@openzeppelin/contracts/utils/structs/Heaps.sol"; +import {Heap} from "@openzeppelin/contracts/utils/structs/Heap.sol"; import {Comparators} from "@openzeppelin/contracts/utils/Comparators.sol"; contract HeapTest is Test { - using Heaps for *; + using Heap for *; - Heaps.Heap internal heap; + Heap.Uint256Heap internal heap; function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal { for (uint32 i = 0; i < heap.size(); ++i) { From 792fcbae2dc5eb4a164cd5b84fae6116ba66b610 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 16 Jun 2024 23:04:47 +0200 Subject: [PATCH 03/36] tests --- contracts/utils/README.adoc | 6 ++ contracts/utils/structs/Heap.sol | 36 +++++---- test/utils/structs/Heap.t.sol | 70 ++--------------- test/utils/structs/Heap.test.js | 130 +++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 77 deletions(-) create mode 100644 test/utils/structs/Heap.test.js diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 74281b3c597..dc0978589eb 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -23,6 +23,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be removed added or remove from both sides. Useful for FIFO and LIFO structures. * {CircularBuffer}: A data structure to store the last N values pushed to it. * {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time. + * {Heap}: A library that implement https://en.wikipedia.org/wiki/Binary_heap[binary heap] in storage. * {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions. * {Create2}: Wrapper around the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode] for safe use without having to deal with low-level assembly. * {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type. @@ -36,6 +37,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Context}: An utility for abstracting the sender and calldata in the current execution context. * {Packing}: A library for packing and unpacking multiple values into bytes32 * {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes]. + * {Comparators}: A library that contains comparator functions to use with with the {Heap} library. [NOTE] ==== @@ -100,6 +102,8 @@ Ethereum contracts have no native concept of an interface, so applications must {{Checkpoints}} +{{Heap}} + {{MerkleTree}} == Libraries @@ -127,3 +131,5 @@ Ethereum contracts have no native concept of an interface, so applications must {{Packing}} {{Panic}} + +{{Comparators}} diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 8dd70490205..2fa386721a5 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -69,11 +69,11 @@ library Heap { Uint256Heap storage self, function(uint256, uint256) view returns (bool) comp ) internal returns (uint256) { - uint32 length = size(self); + uint32 size = length(self); - if (length == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - uint32 last = length - 1; // could be unchecked (check above) + uint32 last = size - 1; // could be unchecked (check above) // get root location (in the data array) and value uint32 rootIdx = self.data[0].index; @@ -128,18 +128,26 @@ library Heap { uint256 value, function(uint256, uint256) view returns (bool) comp ) internal { - uint32 length = size(self); - self.data.push(Uint256HeapNode({index: length, lookup: length, value: value})); - _heapifyUp(self, length, value, comp); + uint32 size = length(self); + self.data.push(Uint256HeapNode({index: size, lookup: size, value: value})); + _heapifyUp(self, size, value, comp); } /** * @dev Returns the number of elements in the heap. */ - function size(Uint256Heap storage self) internal view returns (uint32) { + function length(Uint256Heap storage self) internal view returns (uint32) { return self.data.length.toUint32(); } + function clear(Uint256Heap storage self) internal { + Uint256HeapNode[] storage data = self.data; + /// @solidity memory-safe-assembly + assembly { + sstore(data.slot, 0) + } + } + /* * @dev Swap node `i` and `j` in the tree. */ @@ -158,13 +166,13 @@ library Heap { * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a * comparator, and moving toward the leafs of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. `length` + * Note: This is a private function that is called in a trusted context with already cached parameters. `lesizength` * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ function _heapifyDown( Uint256Heap storage self, - uint32 length, + uint32 size, uint32 pos, uint256 value, function(uint256, uint256) view returns (bool) comp @@ -172,23 +180,23 @@ library Heap { uint32 left = 2 * pos + 1; uint32 right = 2 * pos + 2; - if (right < length) { + if (right < size) { uint256 lValue = self.data[self.data[left].index].value; uint256 rValue = self.data[self.data[right].index].value; if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { _swap(self, pos, left); - _heapifyDown(self, length, left, value, comp); + _heapifyDown(self, size, left, value, comp); } else { _swap(self, pos, right); - _heapifyDown(self, length, right, value, comp); + _heapifyDown(self, size, right, value, comp); } } - } else if (left < length) { + } else if (left < size) { uint256 lValue = self.data[self.data[left].index].value; if (comp(lValue, value)) { _swap(self, pos, left); - _heapifyDown(self, length, left, value, comp); + _heapifyDown(self, size, left, value, comp); } } } diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index 7fb289f566d..6bdcde6edf9 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -13,7 +13,7 @@ contract HeapTest is Test { Heap.Uint256Heap internal heap; function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal { - for (uint32 i = 0; i < heap.size(); ++i) { + for (uint32 i = 0; i < heap.length(); ++i) { // lookups assertEq(i, heap.data[heap.data[i].index].lookup); @@ -23,74 +23,14 @@ contract HeapTest is Test { } } - function testUnit() public { - // - assertEq(heap.size(), 0); - _validateHeap(Comparators.lt); - - heap.insert(712); // 712 - assertEq(heap.size(), 1); - _validateHeap(Comparators.lt); - - heap.insert(20); // 20, 712 - assertEq(heap.size(), 2); - _validateHeap(Comparators.lt); - - heap.insert(4337); // 20, 712, 4337 - assertEq(heap.size(), 3); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 20); // 712, 4337 - assertEq(heap.size(), 2); - _validateHeap(Comparators.lt); - - heap.insert(1559); // 712, 1559, 4337 - assertEq(heap.size(), 3); - _validateHeap(Comparators.lt); - - heap.insert(155); // 155, 712, 1559, 4337 - assertEq(heap.size(), 4); - _validateHeap(Comparators.lt); - - heap.insert(7702); // 155, 712, 1559, 4337, 7702 - assertEq(heap.size(), 5); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 155); // 712, 1559, 4337, 7702 - assertEq(heap.size(), 4); - _validateHeap(Comparators.lt); - - heap.insert(721); // 712, 721, 1559, 4337, 7702 - assertEq(heap.size(), 5); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 712); // 721, 1559, 4337, 7702 - assertEq(heap.size(), 4); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 721); // 1559, 4337, 7702 - assertEq(heap.size(), 3); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 1559); // 4337, 7702 - assertEq(heap.size(), 2); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 4337); // 7702 - assertEq(heap.size(), 1); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 7702); // - assertEq(heap.size(), 0); - _validateHeap(Comparators.lt); - } - function testFuzz(uint256[] calldata input) public { vm.assume(input.length < 0x20); + assertEq(heap.length(), 0); uint256 min = type(uint256).max; for (uint256 i; i < input.length; ++i) { heap.insert(input[i]); + assertEq(heap.length(), i); _validateHeap(Comparators.lt); min = Math.min(min, input[i]); @@ -101,6 +41,7 @@ contract HeapTest is Test { for (uint256 i; i < input.length; ++i) { uint256 top = heap.top(); uint256 pop = heap.pop(); + assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.lt); assertEq(pop, top); @@ -111,10 +52,12 @@ contract HeapTest is Test { function testFuzzGt(uint256[] calldata input) public { vm.assume(input.length < 0x20); + assertEq(heap.length(), 0); uint256 max = 0; for (uint256 i; i < input.length; ++i) { heap.insert(input[i], Comparators.gt); + assertEq(heap.length(), i); _validateHeap(Comparators.gt); max = Math.max(max, input[i]); @@ -125,6 +68,7 @@ contract HeapTest is Test { for (uint256 i; i < input.length; ++i) { uint256 top = heap.top(); uint256 pop = heap.pop(Comparators.gt); + assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.gt); assertEq(pop, top); diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js new file mode 100644 index 00000000000..4dcf268f2b9 --- /dev/null +++ b/test/utils/structs/Heap.test.js @@ -0,0 +1,130 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); + +async function fixture() { + const mock = await ethers.deployContract('$Heap'); + return { mock }; +} + +describe('Heap', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('starts empty', async function () { + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + expect(await this.mock.$length(0)).to.equal(0n); + }); + + it('pop from empty', async function () { + await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + }); + + it('clear', async function () { + await this.mock.$insert(0, 42n); + expect(await this.mock.$length(0)).to.equal(1n); + expect(await this.mock.$top(0)).to.equal(42n); + + await this.mock.$clear(0); + expect(await this.mock.$length(0)).to.equal(0n); + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + + it('support duplicated items', async function () { + expect(await this.mock.$length(0)).to.equal(0n); + + // insert 5 times + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + + // pop 5 times + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + + // poping a 6th time panics + await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + }); + + it('insert and pop', async function () { + expect(await this.mock.$length(0)).to.equal(0n); + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + + await this.mock.$insert(0, 712n); // 712 + + expect(await this.mock.$length(0)).to.equal(1n); + expect(await this.mock.$top(0)).to.equal(712n); + + await this.mock.$insert(0, 20n); // 20, 712 + + expect(await this.mock.$length(0)).to.equal(2n); + expect(await this.mock.$top(0)).to.equal(20n); + + await this.mock.$insert(0, 4337n); // 20, 712, 4337 + + expect(await this.mock.$length(0)).to.equal(3n); + expect(await this.mock.$top(0)).to.equal(20n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(20n); // 712, 4337 + + expect(await this.mock.$length(0)).to.equal(2n); + expect(await this.mock.$top(0)).to.equal(712n); + + await this.mock.$insert(0, 1559n); // 712, 1559, 4337 + + expect(await this.mock.$length(0)).to.equal(3n); + expect(await this.mock.$top(0)).to.equal(712n); + + await this.mock.$insert(0, 155n); // 155, 712, 1559, 4337 + + expect(await this.mock.$length(0)).to.equal(4n); + expect(await this.mock.$top(0)).to.equal(155n); + + await this.mock.$insert(0, 7702n); // 155, 712, 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(5n); + expect(await this.mock.$top(0)).to.equal(155n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(155n); // 712, 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(4n); + expect(await this.mock.$top(0)).to.equal(712n); + + await this.mock.$insert(0, 721n); // 712, 721, 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(5n); + expect(await this.mock.$top(0)).to.equal(712n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(712n); // 721, 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(4n); + expect(await this.mock.$top(0)).to.equal(721n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(721n); // 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(3n); + expect(await this.mock.$top(0)).to.equal(1559n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(1559n); // 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(2n); + expect(await this.mock.$top(0)).to.equal(4337n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(4337n); // 7702 + + expect(await this.mock.$length(0)).to.equal(1n); + expect(await this.mock.$top(0)).to.equal(7702n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(7702n); // + + expect(await this.mock.$length(0)).to.equal(0n); + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); +}); From 0c860055d2530333c102ec47348fa2f9347db3a8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 16 Jun 2024 23:20:42 +0200 Subject: [PATCH 04/36] fix fuzzing tests --- test/utils/structs/Heap.t.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index 6bdcde6edf9..35c1abf52c8 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -28,9 +28,9 @@ contract HeapTest is Test { assertEq(heap.length(), 0); uint256 min = type(uint256).max; - for (uint256 i; i < input.length; ++i) { + for (uint256 i = 0; i < input.length; ++i) { heap.insert(input[i]); - assertEq(heap.length(), i); + assertEq(heap.length(), i + 1); _validateHeap(Comparators.lt); min = Math.min(min, input[i]); @@ -38,7 +38,7 @@ contract HeapTest is Test { } uint256 max = 0; - for (uint256 i; i < input.length; ++i) { + for (uint256 i = 0; i < input.length; ++i) { uint256 top = heap.top(); uint256 pop = heap.pop(); assertEq(heap.length(), input.length - i - 1); @@ -55,9 +55,9 @@ contract HeapTest is Test { assertEq(heap.length(), 0); uint256 max = 0; - for (uint256 i; i < input.length; ++i) { + for (uint256 i = 0; i < input.length; ++i) { heap.insert(input[i], Comparators.gt); - assertEq(heap.length(), i); + assertEq(heap.length(), i + 1); _validateHeap(Comparators.gt); max = Math.max(max, input[i]); @@ -65,7 +65,7 @@ contract HeapTest is Test { } uint256 min = type(uint256).max; - for (uint256 i; i < input.length; ++i) { + for (uint256 i = 0; i < input.length; ++i) { uint256 top = heap.top(); uint256 pop = heap.pop(Comparators.gt); assertEq(heap.length(), input.length - i - 1); From 248baf67bafdf7f6f384e37d2415d6ecd8e7f97e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 16 Jun 2024 23:21:39 +0200 Subject: [PATCH 05/36] codespell --- test/utils/structs/Heap.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index 4dcf268f2b9..6fc00ddd157 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -49,7 +49,7 @@ describe('Heap', function () { await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); - // poping a 6th time panics + // popping a 6th time panics await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); }); From 53db2abf66486b6972e9eef0c6840d425f8babf5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 17 Jun 2024 13:23:22 +0200 Subject: [PATCH 06/36] update --- test/utils/structs/Heap.test.js | 111 +++++++++++--------------------- 1 file changed, 39 insertions(+), 72 deletions(-) diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index 6fc00ddd157..197c67d24de 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -54,77 +54,44 @@ describe('Heap', function () { }); it('insert and pop', async function () { - expect(await this.mock.$length(0)).to.equal(0n); - await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - - await this.mock.$insert(0, 712n); // 712 - - expect(await this.mock.$length(0)).to.equal(1n); - expect(await this.mock.$top(0)).to.equal(712n); - - await this.mock.$insert(0, 20n); // 20, 712 - - expect(await this.mock.$length(0)).to.equal(2n); - expect(await this.mock.$top(0)).to.equal(20n); - - await this.mock.$insert(0, 4337n); // 20, 712, 4337 - - expect(await this.mock.$length(0)).to.equal(3n); - expect(await this.mock.$top(0)).to.equal(20n); - - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(20n); // 712, 4337 - - expect(await this.mock.$length(0)).to.equal(2n); - expect(await this.mock.$top(0)).to.equal(712n); - - await this.mock.$insert(0, 1559n); // 712, 1559, 4337 - - expect(await this.mock.$length(0)).to.equal(3n); - expect(await this.mock.$top(0)).to.equal(712n); - - await this.mock.$insert(0, 155n); // 155, 712, 1559, 4337 - - expect(await this.mock.$length(0)).to.equal(4n); - expect(await this.mock.$top(0)).to.equal(155n); - - await this.mock.$insert(0, 7702n); // 155, 712, 1559, 4337, 7702 - - expect(await this.mock.$length(0)).to.equal(5n); - expect(await this.mock.$top(0)).to.equal(155n); - - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(155n); // 712, 1559, 4337, 7702 - - expect(await this.mock.$length(0)).to.equal(4n); - expect(await this.mock.$top(0)).to.equal(712n); - - await this.mock.$insert(0, 721n); // 712, 721, 1559, 4337, 7702 - - expect(await this.mock.$length(0)).to.equal(5n); - expect(await this.mock.$top(0)).to.equal(712n); - - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(712n); // 721, 1559, 4337, 7702 - - expect(await this.mock.$length(0)).to.equal(4n); - expect(await this.mock.$top(0)).to.equal(721n); - - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(721n); // 1559, 4337, 7702 - - expect(await this.mock.$length(0)).to.equal(3n); - expect(await this.mock.$top(0)).to.equal(1559n); - - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(1559n); // 4337, 7702 - - expect(await this.mock.$length(0)).to.equal(2n); - expect(await this.mock.$top(0)).to.equal(4337n); - - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(4337n); // 7702 - - expect(await this.mock.$length(0)).to.equal(1n); - expect(await this.mock.$top(0)).to.equal(7702n); - - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(7702n); // - - expect(await this.mock.$length(0)).to.equal(0n); - await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + const heap = []; + for (const { op, value } of [ + { op: 'insert', value: 712 }, // [712] + { op: 'insert', value: 20 }, // [20, 712] + { op: 'insert', value: 4337 }, // [20, 712, 4437] + { op: 'pop' }, // 20, [712, 4437] + { op: 'insert', value: 1559 }, // [712, 1159, 4437] + { op: 'insert', value: 155 }, // [155, 712, 1159, 4437] + { op: 'insert', value: 7702 }, // [155, 712, 1159, 4437, 7702] + { op: 'pop' }, // 155, [712, 1159, 4437, 7702] + { op: 'insert', value: 721 }, // [712, 721, 1159, 4437, 7702] + { op: 'pop' }, // 712, [721, 1159, 4437, 7702] + { op: 'pop' }, // 721, [1159, 4437, 7702] + { op: 'pop' }, // 1159, [4437, 7702] + { op: 'pop' }, // 4437, [7702] + { op: 'pop' }, // 7702, [] + { op: 'pop' }, // panic + ]) { + switch (op) { + case 'insert': + await this.mock.$insert(0, value); + heap.push(value); + heap.sort((a, b) => a - b); + break; + case 'pop': + if (heap.length == 0) { + await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + } else { + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(heap.shift()); + } + break; + } + expect(await this.mock.$length(0)).to.equal(heap.length); + if (heap.length == 0) { + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + } else { + expect(await this.mock.$top(0)).to.equal(heap[0]); + } + } }); }); From 945e0f4e5d5ce66b8dcd4172b6f1a79cf1ff45f4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 17 Jun 2024 20:43:13 +0200 Subject: [PATCH 07/36] procedural generation --- contracts/utils/structs/Heap.sol | 298 +++++++++++++++++++++--- scripts/generate/run.js | 4 +- scripts/generate/templates/Heap.js | 258 ++++++++++++++++++++ scripts/generate/templates/Heap.opts.js | 13 ++ scripts/generate/templates/Heap.t.js | 88 +++++++ test/utils/structs/Heap.t.sol | 76 +++++- 6 files changed, 707 insertions(+), 30 deletions(-) create mode 100644 scripts/generate/templates/Heap.js create mode 100644 scripts/generate/templates/Heap.opts.js create mode 100644 scripts/generate/templates/Heap.t.js diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 2fa386721a5..6722f0eb389 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/Heap.js. pragma solidity ^0.8.20; @@ -7,7 +8,7 @@ import {Comparators} from "../Comparators.sol"; import {Panic} from "../Panic.sol"; library Heap { - using SafeCast for uint256; + using SafeCast for *; /** * A Heap is represented as an array of Node objects. In this array we store two overlapping structures: @@ -34,8 +35,6 @@ library Heap { Uint256HeapNode[] data; } - // Index and lookup are bounded by the size of the structure. We could reasonably limit that to uint20 (1 billion entries) - // Then could also limit the value to uint216 so that the entier structure fits into a single slot. struct Uint256HeapNode { uint256 value; uint32 index; // position -> value @@ -46,7 +45,7 @@ library Heap { * @dev Lookup the root element of the heap. */ function top(Uint256Heap storage self) internal view returns (uint256) { - return self.data[self.data[0].index].value; + return _unsafeNodeAccess(self, self.data[0].index).value; } /** @@ -76,35 +75,38 @@ library Heap { uint32 last = size - 1; // could be unchecked (check above) // get root location (in the data array) and value - uint32 rootIdx = self.data[0].index; - uint256 rootValue = self.data[rootIdx].value; + Uint256HeapNode storage rootNode = _unsafeNodeAccess(self, 0); + uint32 rootIdx = rootNode.index; + Uint256HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); + Uint256HeapNode storage lastNode = _unsafeNodeAccess(self, last); + uint256 rootDataValue = rootData.value; // if root is not the last element of the data array (that will get pop-ed), reorder the data array. if (rootIdx != last) { // get details about the value stored in the last element of the array (that will get pop-ed) - uint32 lastDataIdx = self.data[last].lookup; - uint256 lastDataValue = self.data[last].value; + uint32 lastDataIdx = lastNode.lookup; + uint256 lastDataValue = lastNode.value; // copy these values to the location of the root (that is safe, and that we no longer use) - self.data[rootIdx].value = lastDataValue; - self.data[rootIdx].lookup = lastDataIdx; + rootData.value = lastDataValue; + rootData.lookup = lastDataIdx; // update the tree node that used to point to that last element (value now located where the root was) - self.data[lastDataIdx].index = rootIdx; + _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; } // get last leaf location (in the data array) and value - uint32 lastIdx = self.data[last].index; - uint256 lastValue = self.data[lastIdx].value; + uint32 lastIdx = lastNode.index; + uint256 lastValue = _unsafeNodeAccess(self, lastIdx).value; // move the last leaf to the root, pop last leaf ... - self.data[0].index = lastIdx; - self.data[lastIdx].lookup = 0; + rootNode.index = lastIdx; + _unsafeNodeAccess(self, lastIdx).lookup = 0; self.data.pop(); // ... and heapify _heapifyDown(self, last, 0, lastValue, comp); // return root value - return rootValue; + return rootDataValue; } /** @@ -152,21 +154,23 @@ library Heap { * @dev Swap node `i` and `j` in the tree. */ function _swap(Uint256Heap storage self, uint32 i, uint32 j) private { - uint32 ii = self.data[i].index; - uint32 jj = self.data[j].index; + Uint256HeapNode storage ni = _unsafeNodeAccess(self, i); + Uint256HeapNode storage nj = _unsafeNodeAccess(self, j); + uint32 ii = ni.index; + uint32 jj = nj.index; // update pointers to the data (swap the value) - self.data[i].index = jj; - self.data[j].index = ii; + ni.index = jj; + nj.index = ii; // update lookup pointers for consistency - self.data[ii].lookup = j; - self.data[jj].lookup = i; + _unsafeNodeAccess(self, ii).lookup = j; + _unsafeNodeAccess(self, jj).lookup = i; } /** * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a * comparator, and moving toward the leafs of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. `lesizength` + * Note: This is a private function that is called in a trusted context with already cached parameters. `length` * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ @@ -181,8 +185,8 @@ library Heap { uint32 right = 2 * pos + 2; if (right < size) { - uint256 lValue = self.data[self.data[left].index].value; - uint256 rValue = self.data[self.data[right].index].value; + uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; + uint256 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, right).index).value; if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { _swap(self, pos, left); @@ -193,7 +197,7 @@ library Heap { } } } else if (left < size) { - uint256 lValue = self.data[self.data[left].index].value; + uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; if (comp(lValue, value)) { _swap(self, pos, left); _heapifyDown(self, size, left, value, comp); @@ -218,11 +222,251 @@ library Heap { unchecked { while (pos > 0) { uint32 parent = (pos - 1) / 2; - uint256 parentValue = self.data[self.data[parent].index].value; + uint256 parentValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, parent).index).value; if (comp(parentValue, value)) break; _swap(self, pos, parent); pos = parent; } } } + + function _unsafeNodeAccess( + Uint256Heap storage self, + uint32 pos + ) private pure returns (Uint256HeapNode storage result) { + assembly ("memory-safe") { + mstore(0x00, self.slot) + result.slot := add(keccak256(0x00, 0x20), mul(pos, 2)) + } + } + + /** + * A Heap is represented as an array of Node objects. In this array we store two overlapping structures: + * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. + * For each index in this tree we have the `index` pointer that gives the position of the corresponding value. + * - An array of values (payload). At each index we store a uint208 `value` and `lookup`, the index of the node + * that points to this value. + * + * Some invariant: + * ``` + * i == heap.data[heap[data].index].lookup // for all index i + * i == heap.data[heap[data].lookup].index // for all index i + * ``` + * + * The structure is order so that each node is bigger then its parent. An immediate consequence is that the + * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` + * + * This structure is designed for the following complexities: + * - insert: 0(log(n)) + * - pop (remove smallest value in set): O(log(n)) + * - top (get smallest value in set): O(1) + */ + struct Uint208Heap { + Uint208HeapNode[] data; + } + + struct Uint208HeapNode { + uint208 value; + uint24 index; // position -> value + uint24 lookup; // value -> position + } + + /** + * @dev Lookup the root element of the heap. + */ + function top(Uint208Heap storage self) internal view returns (uint208) { + return _unsafeNodeAccess(self, self.data[0].index).value; + } + + /** + * @dev Remove (and return) the root element for the heap using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function pop(Uint208Heap storage self) internal returns (uint208) { + return pop(self, Comparators.lt); + } + + /** + * @dev Remove (and return) the root element for the heap using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function pop( + Uint208Heap storage self, + function(uint256, uint256) view returns (bool) comp + ) internal returns (uint208) { + uint24 size = length(self); + + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + + uint24 last = size - 1; // could be unchecked (check above) + + // get root location (in the data array) and value + Uint208HeapNode storage rootNode = _unsafeNodeAccess(self, 0); + uint24 rootIdx = rootNode.index; + Uint208HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); + Uint208HeapNode storage lastNode = _unsafeNodeAccess(self, last); + uint208 rootDataValue = rootData.value; + + // if root is not the last element of the data array (that will get pop-ed), reorder the data array. + if (rootIdx != last) { + // get details about the value stored in the last element of the array (that will get pop-ed) + uint24 lastDataIdx = lastNode.lookup; + uint208 lastDataValue = lastNode.value; + // copy these values to the location of the root (that is safe, and that we no longer use) + rootData.value = lastDataValue; + rootData.lookup = lastDataIdx; + // update the tree node that used to point to that last element (value now located where the root was) + _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; + } + + // get last leaf location (in the data array) and value + uint24 lastIdx = lastNode.index; + uint208 lastValue = _unsafeNodeAccess(self, lastIdx).value; + + // move the last leaf to the root, pop last leaf ... + rootNode.index = lastIdx; + _unsafeNodeAccess(self, lastIdx).lookup = 0; + self.data.pop(); + + // ... and heapify + _heapifyDown(self, last, 0, lastValue, comp); + + // return root value + return rootDataValue; + } + + /** + * @dev Insert a new element in the heap using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function insert(Uint208Heap storage self, uint208 value) internal { + insert(self, value, Comparators.lt); + } + + /** + * @dev Insert a new element in the heap using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function insert( + Uint208Heap storage self, + uint208 value, + function(uint256, uint256) view returns (bool) comp + ) internal { + uint24 size = length(self); + self.data.push(Uint208HeapNode({index: size, lookup: size, value: value})); + _heapifyUp(self, size, value, comp); + } + + /** + * @dev Returns the number of elements in the heap. + */ + function length(Uint208Heap storage self) internal view returns (uint24) { + return self.data.length.toUint24(); + } + + function clear(Uint208Heap storage self) internal { + Uint208HeapNode[] storage data = self.data; + /// @solidity memory-safe-assembly + assembly { + sstore(data.slot, 0) + } + } + + /* + * @dev Swap node `i` and `j` in the tree. + */ + function _swap(Uint208Heap storage self, uint24 i, uint24 j) private { + Uint208HeapNode storage ni = _unsafeNodeAccess(self, i); + Uint208HeapNode storage nj = _unsafeNodeAccess(self, j); + uint24 ii = ni.index; + uint24 jj = nj.index; + // update pointers to the data (swap the value) + ni.index = jj; + nj.index = ii; + // update lookup pointers for consistency + _unsafeNodeAccess(self, ii).lookup = j; + _unsafeNodeAccess(self, jj).lookup = i; + } + + /** + * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a + * comparator, and moving toward the leafs of the underlying tree. + * + * Note: This is a private function that is called in a trusted context with already cached parameters. `length` + * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These + * parameters are not verified. It is the caller role to make sure the parameters are correct. + */ + function _heapifyDown( + Uint208Heap storage self, + uint24 size, + uint24 pos, + uint208 value, + function(uint256, uint256) view returns (bool) comp + ) private { + uint24 left = 2 * pos + 1; + uint24 right = 2 * pos + 2; + + if (right < size) { + uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; + uint208 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, right).index).value; + if (comp(lValue, value) || comp(rValue, value)) { + if (comp(lValue, rValue)) { + _swap(self, pos, left); + _heapifyDown(self, size, left, value, comp); + } else { + _swap(self, pos, right); + _heapifyDown(self, size, right, value, comp); + } + } + } else if (left < size) { + uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; + if (comp(lValue, value)) { + _swap(self, pos, left); + _heapifyDown(self, size, left, value, comp); + } + } + } + + /** + * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a + * comparator, and moving toward the root of the underlying tree. + * + * Note: This is a private function that is called in a trusted context with already cached parameters. `value` + * could be extracted from `self` and `pos`, but that would require redundant storage read. This parameters is not + * verified. It is the caller role to make sure the parameters are correct. + */ + function _heapifyUp( + Uint208Heap storage self, + uint24 pos, + uint208 value, + function(uint256, uint256) view returns (bool) comp + ) private { + unchecked { + while (pos > 0) { + uint24 parent = (pos - 1) / 2; + uint208 parentValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, parent).index).value; + if (comp(parentValue, value)) break; + _swap(self, pos, parent); + pos = parent; + } + } + } + + function _unsafeNodeAccess( + Uint208Heap storage self, + uint24 pos + ) private pure returns (Uint208HeapNode storage result) { + assembly ("memory-safe") { + mstore(0x00, self.slot) + result.slot := add(keccak256(0x00, 0x20), mul(pos, 1)) + } + } } diff --git a/scripts/generate/run.js b/scripts/generate/run.js index f8ec17606ac..801e1eee90c 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -33,9 +33,10 @@ function generateFromTemplate(file, template, outputPrefix = '') { // Contracts for (const [file, template] of Object.entries({ 'utils/math/SafeCast.sol': './templates/SafeCast.js', + 'utils/structs/Checkpoints.sol': './templates/Checkpoints.js', 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', - 'utils/structs/Checkpoints.sol': './templates/Checkpoints.js', + 'utils/structs/Heap.sol': './templates/Heap.js', 'utils/SlotDerivation.sol': './templates/SlotDerivation.js', 'utils/StorageSlot.sol': './templates/StorageSlot.js', 'utils/Arrays.sol': './templates/Arrays.js', @@ -48,6 +49,7 @@ for (const [file, template] of Object.entries({ // Tests for (const [file, template] of Object.entries({ 'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js', + 'utils/structs/Heap.t.sol': './templates/Heap.t.js', 'utils/Packing.t.sol': './templates/Packing.t.js', 'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js', })) { diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js new file mode 100644 index 00000000000..0e37d3c20c2 --- /dev/null +++ b/scripts/generate/templates/Heap.js @@ -0,0 +1,258 @@ +const format = require('../format-lines'); +const { TYPES } = require('./Heap.opts'); +const { capitalize } = require('../../helpers'); + +/* eslint-disable max-len */ +const header = `\ +pragma solidity ^0.8.20; + +import {SafeCast} from "../math/SafeCast.sol"; +import {Comparators} from "../Comparators.sol"; +import {Panic} from "../Panic.sol"; +`; + +const generate = ({ struct, node, valueType, indexType, blockSize }) => `\ +/** + * A Heap is represented as an array of Node objects. In this array we store two overlapping structures: + * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. + * For each index in this tree we have the \`index\` pointer that gives the position of the corresponding value. + * - An array of values (payload). At each index we store a ${valueType} \`value\` and \`lookup\`, the index of the node + * that points to this value. + * + * Some invariant: + * \`\`\` + * i == heap.data[heap[data].index].lookup // for all index i + * i == heap.data[heap[data].lookup].index // for all index i + * \`\`\` + * + * The structure is order so that each node is bigger then its parent. An immediate consequence is that the + * smallest value is the one at the root. It can be retrieved in O(1) at \`heap.data[heap.data[0].index].value\` + * + * This structure is designed for the following complexities: + * - insert: 0(log(n)) + * - pop (remove smallest value in set): O(log(n)) + * - top (get smallest value in set): O(1) + */ +struct ${struct} { + ${node}[] data; +} + +struct ${node} { + ${valueType} value; + ${indexType} index; // position -> value + ${indexType} lookup; // value -> position +} + +/** + * @dev Lookup the root element of the heap. + */ +function top(${struct} storage self) internal view returns (${valueType}) { + return _unsafeNodeAccess(self, self.data[0].index).value; +} + +/** + * @dev Remove (and return) the root element for the heap using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ +function pop(${struct} storage self) internal returns (${valueType}) { + return pop(self, Comparators.lt); +} + +/** + * @dev Remove (and return) the root element for the heap using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ +function pop( + ${struct} storage self, + function(uint256, uint256) view returns (bool) comp +) internal returns (${valueType}) { + ${indexType} size = length(self); + + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + + ${indexType} last = size - 1; // could be unchecked (check above) + + // get root location (in the data array) and value + ${node} storage rootNode = _unsafeNodeAccess(self, 0); + ${indexType} rootIdx = rootNode.index; + ${node} storage rootData = _unsafeNodeAccess(self, rootIdx); + ${node} storage lastNode = _unsafeNodeAccess(self, last); + ${valueType} rootDataValue = rootData.value; + + // if root is not the last element of the data array (that will get pop-ed), reorder the data array. + if (rootIdx != last) { + // get details about the value stored in the last element of the array (that will get pop-ed) + ${indexType} lastDataIdx = lastNode.lookup; + ${valueType} lastDataValue = lastNode.value; + // copy these values to the location of the root (that is safe, and that we no longer use) + rootData.value = lastDataValue; + rootData.lookup = lastDataIdx; + // update the tree node that used to point to that last element (value now located where the root was) + _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; + } + + // get last leaf location (in the data array) and value + ${indexType} lastIdx = lastNode.index; + ${valueType} lastValue = _unsafeNodeAccess(self, lastIdx).value; + + // move the last leaf to the root, pop last leaf ... + rootNode.index = lastIdx; + _unsafeNodeAccess(self, lastIdx).lookup = 0; + self.data.pop(); + + // ... and heapify + _heapifyDown(self, last, 0, lastValue, comp); + + // return root value + return rootDataValue; +} + +/** + * @dev Insert a new element in the heap using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ +function insert(${struct} storage self, ${valueType} value) internal { + insert(self, value, Comparators.lt); +} + +/** + * @dev Insert a new element in the heap using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ +function insert( + ${struct} storage self, + ${valueType} value, + function(uint256, uint256) view returns (bool) comp +) internal { + ${indexType} size = length(self); + self.data.push(${struct}Node({index: size, lookup: size, value: value})); + _heapifyUp(self, size, value, comp); +} + +/** + * @dev Returns the number of elements in the heap. + */ +function length(${struct} storage self) internal view returns (${indexType}) { + return self.data.length.to${capitalize(indexType)}(); +} + +function clear(${struct} storage self) internal { + ${struct}Node[] storage data = self.data; + /// @solidity memory-safe-assembly + assembly { + sstore(data.slot, 0) + } +} + +/* + * @dev Swap node \`i\` and \`j\` in the tree. + */ +function _swap(${struct} storage self, ${indexType} i, ${indexType} j) private { + ${node} storage ni = _unsafeNodeAccess(self, i); + ${node} storage nj = _unsafeNodeAccess(self, j); + ${indexType} ii = ni.index; + ${indexType} jj = nj.index; + // update pointers to the data (swap the value) + ni.index = jj; + nj.index = ii; + // update lookup pointers for consistency + _unsafeNodeAccess(self, ii).lookup = j; + _unsafeNodeAccess(self, jj).lookup = i; +} + +/** + * @dev Perform heap maintenance on \`self\`, starting at position \`pos\` (with the \`value\`), using \`comp\` as a + * comparator, and moving toward the leafs of the underlying tree. + * + * Note: This is a private function that is called in a trusted context with already cached parameters. \`length\` + * and \`value\` could be extracted from \`self\` and \`pos\`, but that would require redundant storage read. These + * parameters are not verified. It is the caller role to make sure the parameters are correct. + */ +function _heapifyDown( + ${struct} storage self, + ${indexType} size, + ${indexType} pos, + ${valueType} value, + function(uint256, uint256) view returns (bool) comp +) private { + ${indexType} left = 2 * pos + 1; + ${indexType} right = 2 * pos + 2; + + if (right < size) { + ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; + ${valueType} rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, right).index).value; + if (comp(lValue, value) || comp(rValue, value)) { + if (comp(lValue, rValue)) { + _swap(self, pos, left); + _heapifyDown(self, size, left, value, comp); + } else { + _swap(self, pos, right); + _heapifyDown(self, size, right, value, comp); + } + } + } else if (left < size) { + ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; + if (comp(lValue, value)) { + _swap(self, pos, left); + _heapifyDown(self, size, left, value, comp); + } + } +} + +/** + * @dev Perform heap maintenance on \`self\`, starting at position \`pos\` (with the \`value\`), using \`comp\` as a + * comparator, and moving toward the root of the underlying tree. + * + * Note: This is a private function that is called in a trusted context with already cached parameters. \`value\` + * could be extracted from \`self\` and \`pos\`, but that would require redundant storage read. This parameters is not + * verified. It is the caller role to make sure the parameters are correct. + */ +function _heapifyUp( + ${struct} storage self, + ${indexType} pos, + ${valueType} value, + function(uint256, uint256) view returns (bool) comp +) private { + unchecked { + while (pos > 0) { + ${indexType} parent = (pos - 1) / 2; + ${valueType} parentValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, parent).index).value; + if (comp(parentValue, value)) break; + _swap(self, pos, parent); + pos = parent; + } + } +} + +function _unsafeNodeAccess( + ${struct} storage self, + ${indexType} pos +) private pure returns (${node} storage result) { + assembly ("memory-safe") { + mstore(0x00, self.slot) + result.slot := add(keccak256(0x00, 0x20), mul(pos, ${blockSize})) + } +} +`; + +// GENERATE +module.exports = format( + header, + 'library Heap {', + format( + [].concat( + 'using SafeCast for *;', + '', + TYPES.map(type => generate(type)), + ), + ).trimEnd(), + '}', +); diff --git a/scripts/generate/templates/Heap.opts.js b/scripts/generate/templates/Heap.opts.js new file mode 100644 index 00000000000..32bcce1c560 --- /dev/null +++ b/scripts/generate/templates/Heap.opts.js @@ -0,0 +1,13 @@ +const makeType = (valueSize, indexSize) => ({ + struct: `Uint${valueSize}Heap`, + node: `Uint${valueSize}HeapNode`, + valueSize, + valueType: `uint${valueSize}`, + indexSize, + indexType: `uint${indexSize}`, + blockSize: Math.ceil((valueSize + 2 * indexSize) / 256), +}); + +module.exports = { + TYPES: [makeType(256, 32), makeType(208, 24)], +}; diff --git a/scripts/generate/templates/Heap.t.js b/scripts/generate/templates/Heap.t.js new file mode 100644 index 00000000000..4a8bbe3b5dd --- /dev/null +++ b/scripts/generate/templates/Heap.t.js @@ -0,0 +1,88 @@ +const format = require('../format-lines'); +const { TYPES } = require('./Heap.opts'); + +/* eslint-disable max-len */ +const header = `\ +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Heap} from "@openzeppelin/contracts/utils/structs/Heap.sol"; +import {Comparators} from "@openzeppelin/contracts/utils/Comparators.sol"; +`; + +const generate = ({ struct, valueType }) => `\ +contract ${struct}Test is Test { + using Heap for Heap.${struct}; + + Heap.${struct} internal heap; + + function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal { + for (uint32 i = 0; i < heap.length(); ++i) { + // lookups + assertEq(i, heap.data[heap.data[i].index].lookup); + + // ordering: each node has a value bigger then its parent + if (i > 0) + assertFalse(comp(heap.data[heap.data[i].index].value, heap.data[heap.data[(i - 1) / 2].index].value)); + } + } + + function testFuzz(${valueType}[] calldata input) public { + vm.assume(input.length < 0x20); + assertEq(heap.length(), 0); + + uint256 min = type(uint256).max; + for (uint256 i = 0; i < input.length; ++i) { + heap.insert(input[i]); + assertEq(heap.length(), i + 1); + _validateHeap(Comparators.lt); + + min = Math.min(min, input[i]); + assertEq(heap.top(), min); + } + + uint256 max = 0; + for (uint256 i = 0; i < input.length; ++i) { + ${valueType} top = heap.top(); + ${valueType} pop = heap.pop(); + assertEq(heap.length(), input.length - i - 1); + _validateHeap(Comparators.lt); + + assertEq(pop, top); + assertGe(pop, max); + max = pop; + } + } + + function testFuzzGt(${valueType}[] calldata input) public { + vm.assume(input.length < 0x20); + assertEq(heap.length(), 0); + + uint256 max = 0; + for (uint256 i = 0; i < input.length; ++i) { + heap.insert(input[i], Comparators.gt); + assertEq(heap.length(), i + 1); + _validateHeap(Comparators.gt); + + max = Math.max(max, input[i]); + assertEq(heap.top(), max); + } + + uint256 min = type(uint256).max; + for (uint256 i = 0; i < input.length; ++i) { + ${valueType} top = heap.top(); + ${valueType} pop = heap.pop(Comparators.gt); + assertEq(heap.length(), input.length - i - 1); + _validateHeap(Comparators.gt); + + assertEq(pop, top); + assertLe(pop, min); + min = pop; + } + } +} +`; + +// GENERATE +module.exports = format(header, ...TYPES.map(type => generate(type))); diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index 35c1abf52c8..262686a86d2 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/Heap.t.js. pragma solidity ^0.8.20; @@ -7,8 +8,8 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Heap} from "@openzeppelin/contracts/utils/structs/Heap.sol"; import {Comparators} from "@openzeppelin/contracts/utils/Comparators.sol"; -contract HeapTest is Test { - using Heap for *; +contract Uint256HeapTest is Test { + using Heap for Heap.Uint256Heap; Heap.Uint256Heap internal heap; @@ -77,3 +78,74 @@ contract HeapTest is Test { } } } + +contract Uint208HeapTest is Test { + using Heap for Heap.Uint208Heap; + + Heap.Uint208Heap internal heap; + + function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal { + for (uint32 i = 0; i < heap.length(); ++i) { + // lookups + assertEq(i, heap.data[heap.data[i].index].lookup); + + // ordering: each node has a value bigger then its parent + if (i > 0) + assertFalse(comp(heap.data[heap.data[i].index].value, heap.data[heap.data[(i - 1) / 2].index].value)); + } + } + + function testFuzz(uint208[] calldata input) public { + vm.assume(input.length < 0x20); + assertEq(heap.length(), 0); + + uint256 min = type(uint256).max; + for (uint256 i = 0; i < input.length; ++i) { + heap.insert(input[i]); + assertEq(heap.length(), i + 1); + _validateHeap(Comparators.lt); + + min = Math.min(min, input[i]); + assertEq(heap.top(), min); + } + + uint256 max = 0; + for (uint256 i = 0; i < input.length; ++i) { + uint208 top = heap.top(); + uint208 pop = heap.pop(); + assertEq(heap.length(), input.length - i - 1); + _validateHeap(Comparators.lt); + + assertEq(pop, top); + assertGe(pop, max); + max = pop; + } + } + + function testFuzzGt(uint208[] calldata input) public { + vm.assume(input.length < 0x20); + assertEq(heap.length(), 0); + + uint256 max = 0; + for (uint256 i = 0; i < input.length; ++i) { + heap.insert(input[i], Comparators.gt); + assertEq(heap.length(), i + 1); + _validateHeap(Comparators.gt); + + max = Math.max(max, input[i]); + assertEq(heap.top(), max); + } + + uint256 min = type(uint256).max; + for (uint256 i = 0; i < input.length; ++i) { + uint208 top = heap.top(); + uint208 pop = heap.pop(Comparators.gt); + assertEq(heap.length(), input.length - i - 1); + _validateHeap(Comparators.gt); + + assertEq(pop, top); + assertLe(pop, min); + min = pop; + } + } +} From df82b15633358e4f47b2a7d59a36eb929c8fc2c7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 17 Jun 2024 20:56:37 +0200 Subject: [PATCH 08/36] testing --- test/utils/structs/Heap.test.js | 166 ++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 73 deletions(-) diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index 197c67d24de..294efc6579f 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -3,6 +3,8 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); +const { TYPES } = require('../../../scripts/generate/templates/Heap.opts'); + async function fixture() { const mock = await ethers.deployContract('$Heap'); return { mock }; @@ -13,85 +15,103 @@ describe('Heap', function () { Object.assign(this, await loadFixture(fixture)); }); - it('starts empty', async function () { - await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - expect(await this.mock.$length(0)).to.equal(0n); - }); + for (const { struct, valueType } of TYPES) { + describe(struct, function () { + const returnEvent = `return$pop_Heap_${struct}`; - it('pop from empty', async function () { - await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); - }); + beforeEach(async function () { + this.helper = { + clear: (...args) => this.mock[`$clear_Heap_${struct}`](0, ...args), + insert: (...args) => this.mock[`$insert(uint256,${valueType})`](0, ...args), + length: (...args) => this.mock[`$length_Heap_${struct}`](0, ...args), + pop: (...args) => this.mock[`$pop_Heap_${struct}`](0, ...args), + top: (...args) => this.mock[`$top_Heap_${struct}`](0, ...args), + }; + }); - it('clear', async function () { - await this.mock.$insert(0, 42n); - expect(await this.mock.$length(0)).to.equal(1n); - expect(await this.mock.$top(0)).to.equal(42n); + it('starts empty', async function () { + await expect(this.helper.top()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + expect(await this.helper.length()).to.equal(0n); + }); - await this.mock.$clear(0); - expect(await this.mock.$length(0)).to.equal(0n); - await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - }); + it('pop from empty', async function () { + await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + }); - it('support duplicated items', async function () { - expect(await this.mock.$length(0)).to.equal(0n); - - // insert 5 times - await this.mock.$insert(0, 42n); - await this.mock.$insert(0, 42n); - await this.mock.$insert(0, 42n); - await this.mock.$insert(0, 42n); - await this.mock.$insert(0, 42n); - - // pop 5 times - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); - - // popping a 6th time panics - await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); - }); + it('clear', async function () { + await this.helper.insert(42n); + + expect(await this.helper.length()).to.equal(1n); + expect(await this.helper.top()).to.equal(42n); + + await this.helper.clear(); - it('insert and pop', async function () { - const heap = []; - for (const { op, value } of [ - { op: 'insert', value: 712 }, // [712] - { op: 'insert', value: 20 }, // [20, 712] - { op: 'insert', value: 4337 }, // [20, 712, 4437] - { op: 'pop' }, // 20, [712, 4437] - { op: 'insert', value: 1559 }, // [712, 1159, 4437] - { op: 'insert', value: 155 }, // [155, 712, 1159, 4437] - { op: 'insert', value: 7702 }, // [155, 712, 1159, 4437, 7702] - { op: 'pop' }, // 155, [712, 1159, 4437, 7702] - { op: 'insert', value: 721 }, // [712, 721, 1159, 4437, 7702] - { op: 'pop' }, // 712, [721, 1159, 4437, 7702] - { op: 'pop' }, // 721, [1159, 4437, 7702] - { op: 'pop' }, // 1159, [4437, 7702] - { op: 'pop' }, // 4437, [7702] - { op: 'pop' }, // 7702, [] - { op: 'pop' }, // panic - ]) { - switch (op) { - case 'insert': - await this.mock.$insert(0, value); - heap.push(value); - heap.sort((a, b) => a - b); - break; - case 'pop': + expect(await this.helper.length()).to.equal(0n); + await expect(this.helper.top()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + + it('support duplicated items', async function () { + expect(await this.helper.length()).to.equal(0n); + + // insert 5 times + await this.helper.insert(42n); + await this.helper.insert(42n); + await this.helper.insert(42n); + await this.helper.insert(42n); + await this.helper.insert(42n); + + // pop 5 times + await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); + + // popping a 6th time panics + await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + }); + + it('insert and pop', async function () { + const heap = []; + for (const { op, value } of [ + { op: 'insert', value: 712 }, // [712] + { op: 'insert', value: 20 }, // [20, 712] + { op: 'insert', value: 4337 }, // [20, 712, 4437] + { op: 'pop' }, // 20, [712, 4437] + { op: 'insert', value: 1559 }, // [712, 1159, 4437] + { op: 'insert', value: 155 }, // [155, 712, 1159, 4437] + { op: 'insert', value: 7702 }, // [155, 712, 1159, 4437, 7702] + { op: 'pop' }, // 155, [712, 1159, 4437, 7702] + { op: 'insert', value: 721 }, // [712, 721, 1159, 4437, 7702] + { op: 'pop' }, // 712, [721, 1159, 4437, 7702] + { op: 'pop' }, // 721, [1159, 4437, 7702] + { op: 'pop' }, // 1159, [4437, 7702] + { op: 'pop' }, // 4437, [7702] + { op: 'pop' }, // 7702, [] + { op: 'pop' }, // panic + ]) { + switch (op) { + case 'insert': + await this.helper.insert(value); + heap.push(value); + heap.sort((a, b) => a - b); + break; + case 'pop': + if (heap.length == 0) { + await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + } else { + await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(heap.shift()); + } + break; + } + expect(await this.helper.length()).to.equal(heap.length); if (heap.length == 0) { - await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + await expect(this.helper.top()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); } else { - await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(heap.shift()); + expect(await this.helper.top()).to.equal(heap[0]); } - break; - } - expect(await this.mock.$length(0)).to.equal(heap.length); - if (heap.length == 0) { - await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - } else { - expect(await this.mock.$top(0)).to.equal(heap[0]); - } - } - }); + } + }); + }); + } }); From 8b965fc9d474b06c56365d43a168ac9d3f3f5829 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 09:34:44 +0200 Subject: [PATCH 09/36] overflow handling --- contracts/utils/structs/Heap.sol | 60 +++++++++++++++++++----------- scripts/generate/templates/Heap.js | 30 +++++++++------ 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 6722f0eb389..1e3c4816bc9 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -131,6 +131,9 @@ library Heap { function(uint256, uint256) view returns (bool) comp ) internal { uint32 size = length(self); + if (size == type(uint32).max) { + Panic.panic(Panic.RESOURCE_ERROR); + } self.data.push(Uint256HeapNode({index: size, lookup: size, value: value})); _heapifyUp(self, size, value, comp); } @@ -181,26 +184,31 @@ library Heap { uint256 value, function(uint256, uint256) view returns (bool) comp ) private { - uint32 left = 2 * pos + 1; - uint32 right = 2 * pos + 2; + uint256 left = 2 * pos + 1; // this could overflow uint32 + uint256 right = 2 * pos + 2; // this could overflow uint32 if (right < size) { - uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; - uint256 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, right).index).value; + // the check guarantees that `left` and `right` are both valid uint32 + uint32 lIndex = uint32(left); + uint32 rIndex = uint32(right); + uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; + uint256 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { - _swap(self, pos, left); - _heapifyDown(self, size, left, value, comp); + _swap(self, pos, lIndex); + _heapifyDown(self, size, lIndex, value, comp); } else { - _swap(self, pos, right); - _heapifyDown(self, size, right, value, comp); + _swap(self, pos, rIndex); + _heapifyDown(self, size, rIndex, value, comp); } } } else if (left < size) { - uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; + // the check guarantees that `left` is a valid uint32 + uint32 lIndex = uint32(left); + uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; if (comp(lValue, value)) { - _swap(self, pos, left); - _heapifyDown(self, size, left, value, comp); + _swap(self, pos, lIndex); + _heapifyDown(self, size, lIndex, value, comp); } } } @@ -361,6 +369,9 @@ library Heap { function(uint256, uint256) view returns (bool) comp ) internal { uint24 size = length(self); + if (size == type(uint24).max) { + Panic.panic(Panic.RESOURCE_ERROR); + } self.data.push(Uint208HeapNode({index: size, lookup: size, value: value})); _heapifyUp(self, size, value, comp); } @@ -411,26 +422,31 @@ library Heap { uint208 value, function(uint256, uint256) view returns (bool) comp ) private { - uint24 left = 2 * pos + 1; - uint24 right = 2 * pos + 2; + uint256 left = 2 * pos + 1; // this could overflow uint24 + uint256 right = 2 * pos + 2; // this could overflow uint24 if (right < size) { - uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; - uint208 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, right).index).value; + // the check guarantees that `left` and `right` are both valid uint32 + uint24 lIndex = uint24(left); + uint24 rIndex = uint24(right); + uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; + uint208 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { - _swap(self, pos, left); - _heapifyDown(self, size, left, value, comp); + _swap(self, pos, lIndex); + _heapifyDown(self, size, lIndex, value, comp); } else { - _swap(self, pos, right); - _heapifyDown(self, size, right, value, comp); + _swap(self, pos, rIndex); + _heapifyDown(self, size, rIndex, value, comp); } } } else if (left < size) { - uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; + // the check guarantees that `left` is a valid uint32 + uint24 lIndex = uint24(left); + uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; if (comp(lValue, value)) { - _swap(self, pos, left); - _heapifyDown(self, size, left, value, comp); + _swap(self, pos, lIndex); + _heapifyDown(self, size, lIndex, value, comp); } } } diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index 0e37d3c20c2..4acaea12013 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -133,6 +133,9 @@ function insert( function(uint256, uint256) view returns (bool) comp ) internal { ${indexType} size = length(self); + if (size == type(${indexType}).max) { + Panic.panic(Panic.RESOURCE_ERROR); + } self.data.push(${struct}Node({index: size, lookup: size, value: value})); _heapifyUp(self, size, value, comp); } @@ -183,26 +186,31 @@ function _heapifyDown( ${valueType} value, function(uint256, uint256) view returns (bool) comp ) private { - ${indexType} left = 2 * pos + 1; - ${indexType} right = 2 * pos + 2; + uint256 left = 2 * pos + 1; // this could overflow ${indexType} + uint256 right = 2 * pos + 2; // this could overflow ${indexType} if (right < size) { - ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; - ${valueType} rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, right).index).value; + // the check guarantees that \`left\` and \`right\` are both valid uint32 + ${indexType} lIndex = ${indexType}(left); + ${indexType} rIndex = ${indexType}(right); + ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; + ${valueType} rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { - _swap(self, pos, left); - _heapifyDown(self, size, left, value, comp); + _swap(self, pos, lIndex); + _heapifyDown(self, size, lIndex, value, comp); } else { - _swap(self, pos, right); - _heapifyDown(self, size, right, value, comp); + _swap(self, pos, rIndex); + _heapifyDown(self, size, rIndex, value, comp); } } } else if (left < size) { - ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, left).index).value; + // the check guarantees that \`left\` is a valid uint32 + ${indexType} lIndex = ${indexType}(left); + ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; if (comp(lValue, value)) { - _swap(self, pos, left); - _heapifyDown(self, size, left, value, comp); + _swap(self, pos, lIndex); + _heapifyDown(self, size, lIndex, value, comp); } } } From e952cf6966d337102413e0beb4de5468d3d3a1e8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 10:09:16 +0200 Subject: [PATCH 10/36] add replace and changeset --- .changeset/fluffy-buses-jump.md | 5 ++ .changeset/great-pianos-work.md | 5 ++ contracts/utils/structs/Heap.sol | 80 ++++++++++++++++++++++++++++++ scripts/generate/templates/Heap.js | 40 +++++++++++++++ test/utils/structs/Heap.test.js | 45 +++++++++++------ 5 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 .changeset/fluffy-buses-jump.md create mode 100644 .changeset/great-pianos-work.md diff --git a/.changeset/fluffy-buses-jump.md b/.changeset/fluffy-buses-jump.md new file mode 100644 index 00000000000..7d0e5fb1bef --- /dev/null +++ b/.changeset/fluffy-buses-jump.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Comparator`: A library of comparator functions, usefull for customizing the behavior of the Heap structure. diff --git a/.changeset/great-pianos-work.md b/.changeset/great-pianos-work.md new file mode 100644 index 00000000000..8dfd83c4329 --- /dev/null +++ b/.changeset/great-pianos-work.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Heap`: A data structure that implements a heap based priority-queue. diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 1e3c4816bc9..23f66593256 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -138,6 +138,46 @@ library Heap { _heapifyUp(self, size, value, comp); } + /** + * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function replace(Uint256Heap storage self, uint256 newValue) internal returns (uint256) { + return replace(self, newValue, Comparators.lt); + } + + /** + * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function replace( + Uint256Heap storage self, + uint256 newValue, + function(uint256, uint256) view returns (bool) comp + ) internal returns (uint256) { + uint32 size = length(self); + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + + // position of the node that holds the data for the root + uint32 rootIdx = _unsafeNodeAccess(self, 0).index; + // storage pointer to the node that holds the data for the root + Uint256HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); + + // cache old value and replace it + uint256 oldValue = rootData.value; + rootData.value = newValue; + + // re-heapify + _heapifyDown(self, size, 0, newValue, comp); + + // return old root value + return oldValue; + } + /** * @dev Returns the number of elements in the heap. */ @@ -376,6 +416,46 @@ library Heap { _heapifyUp(self, size, value, comp); } + /** + * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function replace(Uint208Heap storage self, uint208 newValue) internal returns (uint208) { + return replace(self, newValue, Comparators.lt); + } + + /** + * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ + function replace( + Uint208Heap storage self, + uint208 newValue, + function(uint256, uint256) view returns (bool) comp + ) internal returns (uint208) { + uint24 size = length(self); + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + + // position of the node that holds the data for the root + uint24 rootIdx = _unsafeNodeAccess(self, 0).index; + // storage pointer to the node that holds the data for the root + Uint208HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); + + // cache old value and replace it + uint208 oldValue = rootData.value; + rootData.value = newValue; + + // re-heapify + _heapifyDown(self, size, 0, newValue, comp); + + // return old root value + return oldValue; + } + /** * @dev Returns the number of elements in the heap. */ diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index 4acaea12013..b85e11aca45 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -140,6 +140,46 @@ function insert( _heapifyUp(self, size, value, comp); } +/** + * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ +function replace(${struct} storage self, ${valueType} newValue) internal returns (${valueType}) { + return replace(self, newValue, Comparators.lt); +} + +/** + * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. + * + * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * during the lifecycle of a heap will result in undefined behavior. + */ +function replace( + ${struct} storage self, + ${valueType} newValue, + function(uint256, uint256) view returns (bool) comp +) internal returns (${valueType}) { + ${indexType} size = length(self); + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + + // position of the node that holds the data for the root + ${indexType} rootIdx = _unsafeNodeAccess(self, 0).index; + // storage pointer to the node that holds the data for the root + ${node} storage rootData = _unsafeNodeAccess(self, rootIdx); + + // cache old value and replace it + ${valueType} oldValue = rootData.value; + rootData.value = newValue; + + // re-heapify + _heapifyDown(self, size, 0, newValue, comp); + + // return old root value + return oldValue; +} + /** * @dev Returns the number of elements in the heap. */ diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index 294efc6579f..2cf52f366c9 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -17,12 +17,14 @@ describe('Heap', function () { for (const { struct, valueType } of TYPES) { describe(struct, function () { - const returnEvent = `return$pop_Heap_${struct}`; + const popEvent = `return$pop_Heap_${struct}`; + const replaceEvent = `return$replace_Heap_${struct}_${valueType}`; beforeEach(async function () { this.helper = { clear: (...args) => this.mock[`$clear_Heap_${struct}`](0, ...args), insert: (...args) => this.mock[`$insert(uint256,${valueType})`](0, ...args), + replace: (...args) => this.mock[`$replace(uint256,${valueType})`](0, ...args), length: (...args) => this.mock[`$length_Heap_${struct}`](0, ...args), pop: (...args) => this.mock[`$pop_Heap_${struct}`](0, ...args), top: (...args) => this.mock[`$top_Heap_${struct}`](0, ...args), @@ -61,34 +63,36 @@ describe('Heap', function () { await this.helper.insert(42n); // pop 5 times - await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); - await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); - await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); - await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); - await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); + await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); // popping a 6th time panics await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); }); - it('insert and pop', async function () { + it('insert, pop ansd replace', async function () { const heap = []; for (const { op, value } of [ { op: 'insert', value: 712 }, // [712] { op: 'insert', value: 20 }, // [20, 712] { op: 'insert', value: 4337 }, // [20, 712, 4437] { op: 'pop' }, // 20, [712, 4437] - { op: 'insert', value: 1559 }, // [712, 1159, 4437] - { op: 'insert', value: 155 }, // [155, 712, 1159, 4437] - { op: 'insert', value: 7702 }, // [155, 712, 1159, 4437, 7702] - { op: 'pop' }, // 155, [712, 1159, 4437, 7702] - { op: 'insert', value: 721 }, // [712, 721, 1159, 4437, 7702] - { op: 'pop' }, // 712, [721, 1159, 4437, 7702] - { op: 'pop' }, // 721, [1159, 4437, 7702] - { op: 'pop' }, // 1159, [4437, 7702] + { op: 'insert', value: 1559 }, // [712, 1559, 4437] + { op: 'insert', value: 165 }, // [165, 712, 1559, 4437] + { op: 'insert', value: 155 }, // [155, 165, 712, 1559, 4437] + { op: 'insert', value: 7702 }, // [155, 165, 712, 1559, 4437, 7702] + { op: 'pop' }, // 155, [165, 712, 1559, 4437, 7702] + { op: 'replace', value: 721 }, // [712, 721, 1559, 4437, 7702] + { op: 'pop' }, // 712, [721, 1559, 4437, 7702] + { op: 'pop' }, // 721, [1559, 4437, 7702] + { op: 'pop' }, // 1559, [4437, 7702] { op: 'pop' }, // 4437, [7702] { op: 'pop' }, // 7702, [] { op: 'pop' }, // panic + { op: 'replace', value: '1363' }, // panic ]) { switch (op) { case 'insert': @@ -100,7 +104,16 @@ describe('Heap', function () { if (heap.length == 0) { await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); } else { - await expect(this.helper.pop()).to.emit(this.mock, returnEvent).withArgs(heap.shift()); + await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(heap.shift()); + } + break; + case 'replace': + if (heap.length == 0) { + await expect(this.helper.replace(value)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + } else { + await expect(this.helper.replace(value)).to.emit(this.mock, replaceEvent).withArgs(heap.shift()); + heap.push(value); + heap.sort((a, b) => a - b); } break; } From f5fa2743ef7081898a4f99df6e71eb9cccd19f49 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 10:15:25 +0200 Subject: [PATCH 11/36] rename top -> peek --- contracts/utils/structs/Heap.sol | 22 ++++++++++++++-------- scripts/generate/templates/Heap.js | 11 +++++++---- test/utils/structs/Heap.t.sol | 16 ++++++++-------- test/utils/structs/Heap.test.js | 12 ++++++------ 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 23f66593256..248a71a1baf 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -27,9 +27,10 @@ library Heap { * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` * * This structure is designed for the following complexities: - * - insert: 0(log(n)) - * - pop (remove smallest value in set): O(log(n)) - * - top (get smallest value in set): O(1) + * - peek (get the smallest value in set): O(1) + * - insert (insert a value in the set): 0(log(n)) + * - pop (remove the smallest value in set): O(log(n)) + * - replace (replace the smallest value in set with a new value): O(log(n)) */ struct Uint256Heap { Uint256HeapNode[] data; @@ -44,7 +45,7 @@ library Heap { /** * @dev Lookup the root element of the heap. */ - function top(Uint256Heap storage self) internal view returns (uint256) { + function peek(Uint256Heap storage self) internal view returns (uint256) { return _unsafeNodeAccess(self, self.data[0].index).value; } @@ -140,6 +141,7 @@ library Heap { /** * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. + * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. @@ -150,6 +152,7 @@ library Heap { /** * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. + * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. @@ -305,9 +308,10 @@ library Heap { * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` * * This structure is designed for the following complexities: - * - insert: 0(log(n)) - * - pop (remove smallest value in set): O(log(n)) - * - top (get smallest value in set): O(1) + * - peek (get the smallest value in set): O(1) + * - insert (insert a value in the set): 0(log(n)) + * - pop (remove the smallest value in set): O(log(n)) + * - replace (replace the smallest value in set with a new value): O(log(n)) */ struct Uint208Heap { Uint208HeapNode[] data; @@ -322,7 +326,7 @@ library Heap { /** * @dev Lookup the root element of the heap. */ - function top(Uint208Heap storage self) internal view returns (uint208) { + function peek(Uint208Heap storage self) internal view returns (uint208) { return _unsafeNodeAccess(self, self.data[0].index).value; } @@ -418,6 +422,7 @@ library Heap { /** * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. + * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. @@ -428,6 +433,7 @@ library Heap { /** * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. + * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index b85e11aca45..4038219916e 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -29,9 +29,10 @@ const generate = ({ struct, node, valueType, indexType, blockSize }) => `\ * smallest value is the one at the root. It can be retrieved in O(1) at \`heap.data[heap.data[0].index].value\` * * This structure is designed for the following complexities: - * - insert: 0(log(n)) - * - pop (remove smallest value in set): O(log(n)) - * - top (get smallest value in set): O(1) + * - peek (get the smallest value in set): O(1) + * - insert (insert a value in the set): 0(log(n)) + * - pop (remove the smallest value in set): O(log(n)) + * - replace (replace the smallest value in set with a new value): O(log(n)) */ struct ${struct} { ${node}[] data; @@ -46,7 +47,7 @@ struct ${node} { /** * @dev Lookup the root element of the heap. */ -function top(${struct} storage self) internal view returns (${valueType}) { +function peek(${struct} storage self) internal view returns (${valueType}) { return _unsafeNodeAccess(self, self.data[0].index).value; } @@ -142,6 +143,7 @@ function insert( /** * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. + * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. @@ -152,6 +154,7 @@ function replace(${struct} storage self, ${valueType} newValue) internal returns /** * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. + * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index 262686a86d2..873f4c16a64 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -35,12 +35,12 @@ contract Uint256HeapTest is Test { _validateHeap(Comparators.lt); min = Math.min(min, input[i]); - assertEq(heap.top(), min); + assertEq(heap.peek(), min); } uint256 max = 0; for (uint256 i = 0; i < input.length; ++i) { - uint256 top = heap.top(); + uint256 top = heap.peek(); uint256 pop = heap.pop(); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.lt); @@ -62,12 +62,12 @@ contract Uint256HeapTest is Test { _validateHeap(Comparators.gt); max = Math.max(max, input[i]); - assertEq(heap.top(), max); + assertEq(heap.peek(), max); } uint256 min = type(uint256).max; for (uint256 i = 0; i < input.length; ++i) { - uint256 top = heap.top(); + uint256 top = heap.peek(); uint256 pop = heap.pop(Comparators.gt); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.gt); @@ -106,12 +106,12 @@ contract Uint208HeapTest is Test { _validateHeap(Comparators.lt); min = Math.min(min, input[i]); - assertEq(heap.top(), min); + assertEq(heap.peek(), min); } uint256 max = 0; for (uint256 i = 0; i < input.length; ++i) { - uint208 top = heap.top(); + uint208 top = heap.peek(); uint208 pop = heap.pop(); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.lt); @@ -133,12 +133,12 @@ contract Uint208HeapTest is Test { _validateHeap(Comparators.gt); max = Math.max(max, input[i]); - assertEq(heap.top(), max); + assertEq(heap.peek(), max); } uint256 min = type(uint256).max; for (uint256 i = 0; i < input.length; ++i) { - uint208 top = heap.top(); + uint208 top = heap.peek(); uint208 pop = heap.pop(Comparators.gt); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.gt); diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index 2cf52f366c9..c4e9c28ae84 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -27,12 +27,12 @@ describe('Heap', function () { replace: (...args) => this.mock[`$replace(uint256,${valueType})`](0, ...args), length: (...args) => this.mock[`$length_Heap_${struct}`](0, ...args), pop: (...args) => this.mock[`$pop_Heap_${struct}`](0, ...args), - top: (...args) => this.mock[`$top_Heap_${struct}`](0, ...args), + peek: (...args) => this.mock[`$peek_Heap_${struct}`](0, ...args), }; }); it('starts empty', async function () { - await expect(this.helper.top()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + await expect(this.helper.peek()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); expect(await this.helper.length()).to.equal(0n); }); @@ -44,12 +44,12 @@ describe('Heap', function () { await this.helper.insert(42n); expect(await this.helper.length()).to.equal(1n); - expect(await this.helper.top()).to.equal(42n); + expect(await this.helper.peek()).to.equal(42n); await this.helper.clear(); expect(await this.helper.length()).to.equal(0n); - await expect(this.helper.top()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + await expect(this.helper.peek()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); }); it('support duplicated items', async function () { @@ -119,9 +119,9 @@ describe('Heap', function () { } expect(await this.helper.length()).to.equal(heap.length); if (heap.length == 0) { - await expect(this.helper.top()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + await expect(this.helper.peek()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); } else { - expect(await this.helper.top()).to.equal(heap[0]); + expect(await this.helper.peek()).to.equal(heap[0]); } } }); From 1f0fef0ab137007d62beb025550ce2b40a2c17a2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 10:17:59 +0200 Subject: [PATCH 12/36] internal renaming --- scripts/generate/templates/Heap.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index 4038219916e..b513e27bf88 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -106,7 +106,7 @@ function pop( self.data.pop(); // ... and heapify - _heapifyDown(self, last, 0, lastValue, comp); + _siftDown(self, last, 0, lastValue, comp); // return root value return rootDataValue; @@ -138,7 +138,7 @@ function insert( Panic.panic(Panic.RESOURCE_ERROR); } self.data.push(${struct}Node({index: size, lookup: size, value: value})); - _heapifyUp(self, size, value, comp); + _siftUp(self, size, value, comp); } /** @@ -177,7 +177,7 @@ function replace( rootData.value = newValue; // re-heapify - _heapifyDown(self, size, 0, newValue, comp); + _siftDown(self, size, 0, newValue, comp); // return old root value return oldValue; @@ -222,7 +222,7 @@ function _swap(${struct} storage self, ${indexType} i, ${indexType} j) private { * and \`value\` could be extracted from \`self\` and \`pos\`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ -function _heapifyDown( +function _siftDown( ${struct} storage self, ${indexType} size, ${indexType} pos, @@ -241,10 +241,10 @@ function _heapifyDown( if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { _swap(self, pos, lIndex); - _heapifyDown(self, size, lIndex, value, comp); + _siftDown(self, size, lIndex, value, comp); } else { _swap(self, pos, rIndex); - _heapifyDown(self, size, rIndex, value, comp); + _siftDown(self, size, rIndex, value, comp); } } } else if (left < size) { @@ -253,7 +253,7 @@ function _heapifyDown( ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; if (comp(lValue, value)) { _swap(self, pos, lIndex); - _heapifyDown(self, size, lIndex, value, comp); + _siftDown(self, size, lIndex, value, comp); } } } @@ -266,7 +266,7 @@ function _heapifyDown( * could be extracted from \`self\` and \`pos\`, but that would require redundant storage read. This parameters is not * verified. It is the caller role to make sure the parameters are correct. */ -function _heapifyUp( +function _siftUp( ${struct} storage self, ${indexType} pos, ${valueType} value, From d0972a3998a80c5701783138ddcc0485c792db3b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 10:22:14 +0200 Subject: [PATCH 13/36] codespell --- .changeset/fluffy-buses-jump.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fluffy-buses-jump.md b/.changeset/fluffy-buses-jump.md index 7d0e5fb1bef..0525a4d8e43 100644 --- a/.changeset/fluffy-buses-jump.md +++ b/.changeset/fluffy-buses-jump.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Comparator`: A library of comparator functions, usefull for customizing the behavior of the Heap structure. +`Comparator`: A library of comparator functions, useful for customizing the behavior of the Heap structure. From 8e3dda6f83d316257159a1e6336984e63ae80328 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 10:35:14 +0200 Subject: [PATCH 14/36] regenerate --- contracts/utils/structs/Heap.sol | 32 ++++++++++++++++---------------- test/utils/structs/Heap.t.sol | 16 ++++++++-------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 248a71a1baf..86a70872b17 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -104,7 +104,7 @@ library Heap { self.data.pop(); // ... and heapify - _heapifyDown(self, last, 0, lastValue, comp); + _siftDown(self, last, 0, lastValue, comp); // return root value return rootDataValue; @@ -136,7 +136,7 @@ library Heap { Panic.panic(Panic.RESOURCE_ERROR); } self.data.push(Uint256HeapNode({index: size, lookup: size, value: value})); - _heapifyUp(self, size, value, comp); + _siftUp(self, size, value, comp); } /** @@ -175,7 +175,7 @@ library Heap { rootData.value = newValue; // re-heapify - _heapifyDown(self, size, 0, newValue, comp); + _siftDown(self, size, 0, newValue, comp); // return old root value return oldValue; @@ -220,7 +220,7 @@ library Heap { * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ - function _heapifyDown( + function _siftDown( Uint256Heap storage self, uint32 size, uint32 pos, @@ -239,10 +239,10 @@ library Heap { if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { _swap(self, pos, lIndex); - _heapifyDown(self, size, lIndex, value, comp); + _siftDown(self, size, lIndex, value, comp); } else { _swap(self, pos, rIndex); - _heapifyDown(self, size, rIndex, value, comp); + _siftDown(self, size, rIndex, value, comp); } } } else if (left < size) { @@ -251,7 +251,7 @@ library Heap { uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; if (comp(lValue, value)) { _swap(self, pos, lIndex); - _heapifyDown(self, size, lIndex, value, comp); + _siftDown(self, size, lIndex, value, comp); } } } @@ -264,7 +264,7 @@ library Heap { * could be extracted from `self` and `pos`, but that would require redundant storage read. This parameters is not * verified. It is the caller role to make sure the parameters are correct. */ - function _heapifyUp( + function _siftUp( Uint256Heap storage self, uint32 pos, uint256 value, @@ -385,7 +385,7 @@ library Heap { self.data.pop(); // ... and heapify - _heapifyDown(self, last, 0, lastValue, comp); + _siftDown(self, last, 0, lastValue, comp); // return root value return rootDataValue; @@ -417,7 +417,7 @@ library Heap { Panic.panic(Panic.RESOURCE_ERROR); } self.data.push(Uint208HeapNode({index: size, lookup: size, value: value})); - _heapifyUp(self, size, value, comp); + _siftUp(self, size, value, comp); } /** @@ -456,7 +456,7 @@ library Heap { rootData.value = newValue; // re-heapify - _heapifyDown(self, size, 0, newValue, comp); + _siftDown(self, size, 0, newValue, comp); // return old root value return oldValue; @@ -501,7 +501,7 @@ library Heap { * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ - function _heapifyDown( + function _siftDown( Uint208Heap storage self, uint24 size, uint24 pos, @@ -520,10 +520,10 @@ library Heap { if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { _swap(self, pos, lIndex); - _heapifyDown(self, size, lIndex, value, comp); + _siftDown(self, size, lIndex, value, comp); } else { _swap(self, pos, rIndex); - _heapifyDown(self, size, rIndex, value, comp); + _siftDown(self, size, rIndex, value, comp); } } } else if (left < size) { @@ -532,7 +532,7 @@ library Heap { uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; if (comp(lValue, value)) { _swap(self, pos, lIndex); - _heapifyDown(self, size, lIndex, value, comp); + _siftDown(self, size, lIndex, value, comp); } } } @@ -545,7 +545,7 @@ library Heap { * could be extracted from `self` and `pos`, but that would require redundant storage read. This parameters is not * verified. It is the caller role to make sure the parameters are correct. */ - function _heapifyUp( + function _siftUp( Uint208Heap storage self, uint24 pos, uint208 value, diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index 873f4c16a64..262686a86d2 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -35,12 +35,12 @@ contract Uint256HeapTest is Test { _validateHeap(Comparators.lt); min = Math.min(min, input[i]); - assertEq(heap.peek(), min); + assertEq(heap.top(), min); } uint256 max = 0; for (uint256 i = 0; i < input.length; ++i) { - uint256 top = heap.peek(); + uint256 top = heap.top(); uint256 pop = heap.pop(); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.lt); @@ -62,12 +62,12 @@ contract Uint256HeapTest is Test { _validateHeap(Comparators.gt); max = Math.max(max, input[i]); - assertEq(heap.peek(), max); + assertEq(heap.top(), max); } uint256 min = type(uint256).max; for (uint256 i = 0; i < input.length; ++i) { - uint256 top = heap.peek(); + uint256 top = heap.top(); uint256 pop = heap.pop(Comparators.gt); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.gt); @@ -106,12 +106,12 @@ contract Uint208HeapTest is Test { _validateHeap(Comparators.lt); min = Math.min(min, input[i]); - assertEq(heap.peek(), min); + assertEq(heap.top(), min); } uint256 max = 0; for (uint256 i = 0; i < input.length; ++i) { - uint208 top = heap.peek(); + uint208 top = heap.top(); uint208 pop = heap.pop(); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.lt); @@ -133,12 +133,12 @@ contract Uint208HeapTest is Test { _validateHeap(Comparators.gt); max = Math.max(max, input[i]); - assertEq(heap.peek(), max); + assertEq(heap.top(), max); } uint256 min = type(uint256).max; for (uint256 i = 0; i < input.length; ++i) { - uint208 top = heap.peek(); + uint208 top = heap.top(); uint208 pop = heap.pop(Comparators.gt); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.gt); From 38e18130e367afaff99c4827be150e603c5a7099 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 10:35:52 +0200 Subject: [PATCH 15/36] auto regenerate --- .githooks/pre-push | 1 + 1 file changed, 1 insertion(+) diff --git a/.githooks/pre-push b/.githooks/pre-push index a51f3884a5d..39e6b4800f4 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -3,5 +3,6 @@ set -euo pipefail if [ "${CI:-"false"}" != "true" ]; then + npm run generate npm run lint fi From 02f224d8ba608de89eb0a96dbbc9aae32ef4a0e7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 10:38:04 +0200 Subject: [PATCH 16/36] Update .githooks/pre-push --- .githooks/pre-push | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 39e6b4800f4..359b840020a 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -3,6 +3,6 @@ set -euo pipefail if [ "${CI:-"false"}" != "true" ]; then - npm run generate + npm run generate npm run lint fi From 7e884814005282069c41fcf4ae09f01b2e77c3b2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 10:40:34 +0200 Subject: [PATCH 17/36] up --- scripts/generate/templates/Heap.t.js | 8 ++++---- test/utils/structs/Heap.t.sol | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/generate/templates/Heap.t.js b/scripts/generate/templates/Heap.t.js index 4a8bbe3b5dd..37e1774da53 100644 --- a/scripts/generate/templates/Heap.t.js +++ b/scripts/generate/templates/Heap.t.js @@ -39,12 +39,12 @@ contract ${struct}Test is Test { _validateHeap(Comparators.lt); min = Math.min(min, input[i]); - assertEq(heap.top(), min); + assertEq(heap.peek(), min); } uint256 max = 0; for (uint256 i = 0; i < input.length; ++i) { - ${valueType} top = heap.top(); + ${valueType} top = heap.peek(); ${valueType} pop = heap.pop(); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.lt); @@ -66,12 +66,12 @@ contract ${struct}Test is Test { _validateHeap(Comparators.gt); max = Math.max(max, input[i]); - assertEq(heap.top(), max); + assertEq(heap.peek(), max); } uint256 min = type(uint256).max; for (uint256 i = 0; i < input.length; ++i) { - ${valueType} top = heap.top(); + ${valueType} top = heap.peek(); ${valueType} pop = heap.pop(Comparators.gt); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.gt); diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index 262686a86d2..873f4c16a64 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -35,12 +35,12 @@ contract Uint256HeapTest is Test { _validateHeap(Comparators.lt); min = Math.min(min, input[i]); - assertEq(heap.top(), min); + assertEq(heap.peek(), min); } uint256 max = 0; for (uint256 i = 0; i < input.length; ++i) { - uint256 top = heap.top(); + uint256 top = heap.peek(); uint256 pop = heap.pop(); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.lt); @@ -62,12 +62,12 @@ contract Uint256HeapTest is Test { _validateHeap(Comparators.gt); max = Math.max(max, input[i]); - assertEq(heap.top(), max); + assertEq(heap.peek(), max); } uint256 min = type(uint256).max; for (uint256 i = 0; i < input.length; ++i) { - uint256 top = heap.top(); + uint256 top = heap.peek(); uint256 pop = heap.pop(Comparators.gt); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.gt); @@ -106,12 +106,12 @@ contract Uint208HeapTest is Test { _validateHeap(Comparators.lt); min = Math.min(min, input[i]); - assertEq(heap.top(), min); + assertEq(heap.peek(), min); } uint256 max = 0; for (uint256 i = 0; i < input.length; ++i) { - uint208 top = heap.top(); + uint208 top = heap.peek(); uint208 pop = heap.pop(); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.lt); @@ -133,12 +133,12 @@ contract Uint208HeapTest is Test { _validateHeap(Comparators.gt); max = Math.max(max, input[i]); - assertEq(heap.top(), max); + assertEq(heap.peek(), max); } uint256 min = type(uint256).max; for (uint256 i = 0; i < input.length; ++i) { - uint208 top = heap.top(); + uint208 top = heap.peek(); uint208 pop = heap.pop(Comparators.gt); assertEq(heap.length(), input.length - i - 1); _validateHeap(Comparators.gt); From b2fda31bd633b310717b6f92afedde714f89efd5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 11:47:32 +0200 Subject: [PATCH 18/36] up --- .githooks/pre-push | 2 +- contracts/utils/structs/Heap.sol | 154 +++++++++++++++-------------- scripts/generate/templates/Heap.js | 71 ++++++------- 3 files changed, 115 insertions(+), 112 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 359b840020a..f028ce58e0b 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -3,6 +3,6 @@ set -euo pipefail if [ "${CI:-"false"}" != "true" ]; then - npm run generate + npm run test:generation npm run lint fi diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 86a70872b17..8601c7853c7 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -46,6 +46,7 @@ library Heap { * @dev Lookup the root element of the heap. */ function peek(Uint256Heap storage self) internal view returns (uint256) { + // self.data[0] will `ARRAY_ACCESS_OUT_OF_BOUNDS` panic if heap is empty. return _unsafeNodeAccess(self, self.data[0].index).value; } @@ -69,45 +70,46 @@ library Heap { Uint256Heap storage self, function(uint256, uint256) view returns (bool) comp ) internal returns (uint256) { - uint32 size = length(self); - - if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - - uint32 last = size - 1; // could be unchecked (check above) - - // get root location (in the data array) and value - Uint256HeapNode storage rootNode = _unsafeNodeAccess(self, 0); - uint32 rootIdx = rootNode.index; - Uint256HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); - Uint256HeapNode storage lastNode = _unsafeNodeAccess(self, last); - uint256 rootDataValue = rootData.value; - - // if root is not the last element of the data array (that will get pop-ed), reorder the data array. - if (rootIdx != last) { - // get details about the value stored in the last element of the array (that will get pop-ed) - uint32 lastDataIdx = lastNode.lookup; - uint256 lastDataValue = lastNode.value; - // copy these values to the location of the root (that is safe, and that we no longer use) - rootData.value = lastDataValue; - rootData.lookup = lastDataIdx; - // update the tree node that used to point to that last element (value now located where the root was) - _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; - } + unchecked { + uint32 size = length(self); + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + + uint32 last = size - 1; + + // get root location (in the data array) and value + Uint256HeapNode storage rootNode = _unsafeNodeAccess(self, 0); + uint32 rootIdx = rootNode.index; + Uint256HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); + Uint256HeapNode storage lastNode = _unsafeNodeAccess(self, last); + uint256 rootDataValue = rootData.value; + + // if root is not the last element of the data array (that will get pop-ed), reorder the data array. + if (rootIdx != last) { + // get details about the value stored in the last element of the array (that will get pop-ed) + uint32 lastDataIdx = lastNode.lookup; + uint256 lastDataValue = lastNode.value; + // copy these values to the location of the root (that is safe, and that we no longer use) + rootData.value = lastDataValue; + rootData.lookup = lastDataIdx; + // update the tree node that used to point to that last element (value now located where the root was) + _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; + } - // get last leaf location (in the data array) and value - uint32 lastIdx = lastNode.index; - uint256 lastValue = _unsafeNodeAccess(self, lastIdx).value; + // get last leaf location (in the data array) and value + uint32 lastIdx = lastNode.index; + uint256 lastValue = _unsafeNodeAccess(self, lastIdx).value; - // move the last leaf to the root, pop last leaf ... - rootNode.index = lastIdx; - _unsafeNodeAccess(self, lastIdx).lookup = 0; - self.data.pop(); + // move the last leaf to the root, pop last leaf ... + rootNode.index = lastIdx; + _unsafeNodeAccess(self, lastIdx).lookup = 0; + self.data.pop(); - // ... and heapify - _siftDown(self, last, 0, lastValue, comp); + // ... and heapify + _siftDown(self, last, 0, lastValue, comp); - // return root value - return rootDataValue; + // return root value + return rootDataValue; + } } /** @@ -132,9 +134,8 @@ library Heap { function(uint256, uint256) view returns (bool) comp ) internal { uint32 size = length(self); - if (size == type(uint32).max) { - Panic.panic(Panic.RESOURCE_ERROR); - } + if (size == type(uint32).max) Panic.panic(Panic.RESOURCE_ERROR); + self.data.push(Uint256HeapNode({index: size, lookup: size, value: value})); _siftUp(self, size, value, comp); } @@ -327,6 +328,7 @@ library Heap { * @dev Lookup the root element of the heap. */ function peek(Uint208Heap storage self) internal view returns (uint208) { + // self.data[0] will `ARRAY_ACCESS_OUT_OF_BOUNDS` panic if heap is empty. return _unsafeNodeAccess(self, self.data[0].index).value; } @@ -350,45 +352,46 @@ library Heap { Uint208Heap storage self, function(uint256, uint256) view returns (bool) comp ) internal returns (uint208) { - uint24 size = length(self); - - if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - - uint24 last = size - 1; // could be unchecked (check above) - - // get root location (in the data array) and value - Uint208HeapNode storage rootNode = _unsafeNodeAccess(self, 0); - uint24 rootIdx = rootNode.index; - Uint208HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); - Uint208HeapNode storage lastNode = _unsafeNodeAccess(self, last); - uint208 rootDataValue = rootData.value; - - // if root is not the last element of the data array (that will get pop-ed), reorder the data array. - if (rootIdx != last) { - // get details about the value stored in the last element of the array (that will get pop-ed) - uint24 lastDataIdx = lastNode.lookup; - uint208 lastDataValue = lastNode.value; - // copy these values to the location of the root (that is safe, and that we no longer use) - rootData.value = lastDataValue; - rootData.lookup = lastDataIdx; - // update the tree node that used to point to that last element (value now located where the root was) - _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; - } + unchecked { + uint24 size = length(self); + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + + uint24 last = size - 1; + + // get root location (in the data array) and value + Uint208HeapNode storage rootNode = _unsafeNodeAccess(self, 0); + uint24 rootIdx = rootNode.index; + Uint208HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); + Uint208HeapNode storage lastNode = _unsafeNodeAccess(self, last); + uint208 rootDataValue = rootData.value; + + // if root is not the last element of the data array (that will get pop-ed), reorder the data array. + if (rootIdx != last) { + // get details about the value stored in the last element of the array (that will get pop-ed) + uint24 lastDataIdx = lastNode.lookup; + uint208 lastDataValue = lastNode.value; + // copy these values to the location of the root (that is safe, and that we no longer use) + rootData.value = lastDataValue; + rootData.lookup = lastDataIdx; + // update the tree node that used to point to that last element (value now located where the root was) + _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; + } - // get last leaf location (in the data array) and value - uint24 lastIdx = lastNode.index; - uint208 lastValue = _unsafeNodeAccess(self, lastIdx).value; + // get last leaf location (in the data array) and value + uint24 lastIdx = lastNode.index; + uint208 lastValue = _unsafeNodeAccess(self, lastIdx).value; - // move the last leaf to the root, pop last leaf ... - rootNode.index = lastIdx; - _unsafeNodeAccess(self, lastIdx).lookup = 0; - self.data.pop(); + // move the last leaf to the root, pop last leaf ... + rootNode.index = lastIdx; + _unsafeNodeAccess(self, lastIdx).lookup = 0; + self.data.pop(); - // ... and heapify - _siftDown(self, last, 0, lastValue, comp); + // ... and heapify + _siftDown(self, last, 0, lastValue, comp); - // return root value - return rootDataValue; + // return root value + return rootDataValue; + } } /** @@ -413,9 +416,8 @@ library Heap { function(uint256, uint256) view returns (bool) comp ) internal { uint24 size = length(self); - if (size == type(uint24).max) { - Panic.panic(Panic.RESOURCE_ERROR); - } + if (size == type(uint24).max) Panic.panic(Panic.RESOURCE_ERROR); + self.data.push(Uint208HeapNode({index: size, lookup: size, value: value})); _siftUp(self, size, value, comp); } diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index b513e27bf88..80b150a84d5 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -48,6 +48,7 @@ struct ${node} { * @dev Lookup the root element of the heap. */ function peek(${struct} storage self) internal view returns (${valueType}) { + // self.data[0] will \`ARRAY_ACCESS_OUT_OF_BOUNDS\` panic if heap is empty. return _unsafeNodeAccess(self, self.data[0].index).value; } @@ -71,45 +72,46 @@ function pop( ${struct} storage self, function(uint256, uint256) view returns (bool) comp ) internal returns (${valueType}) { - ${indexType} size = length(self); - - if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + unchecked { + ${indexType} size = length(self); + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - ${indexType} last = size - 1; // could be unchecked (check above) + ${indexType} last = size - 1; - // get root location (in the data array) and value - ${node} storage rootNode = _unsafeNodeAccess(self, 0); - ${indexType} rootIdx = rootNode.index; - ${node} storage rootData = _unsafeNodeAccess(self, rootIdx); - ${node} storage lastNode = _unsafeNodeAccess(self, last); - ${valueType} rootDataValue = rootData.value; + // get root location (in the data array) and value + ${node} storage rootNode = _unsafeNodeAccess(self, 0); + ${indexType} rootIdx = rootNode.index; + ${node} storage rootData = _unsafeNodeAccess(self, rootIdx); + ${node} storage lastNode = _unsafeNodeAccess(self, last); + ${valueType} rootDataValue = rootData.value; - // if root is not the last element of the data array (that will get pop-ed), reorder the data array. - if (rootIdx != last) { - // get details about the value stored in the last element of the array (that will get pop-ed) - ${indexType} lastDataIdx = lastNode.lookup; - ${valueType} lastDataValue = lastNode.value; - // copy these values to the location of the root (that is safe, and that we no longer use) - rootData.value = lastDataValue; - rootData.lookup = lastDataIdx; - // update the tree node that used to point to that last element (value now located where the root was) - _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; - } + // if root is not the last element of the data array (that will get pop-ed), reorder the data array. + if (rootIdx != last) { + // get details about the value stored in the last element of the array (that will get pop-ed) + ${indexType} lastDataIdx = lastNode.lookup; + ${valueType} lastDataValue = lastNode.value; + // copy these values to the location of the root (that is safe, and that we no longer use) + rootData.value = lastDataValue; + rootData.lookup = lastDataIdx; + // update the tree node that used to point to that last element (value now located where the root was) + _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; + } - // get last leaf location (in the data array) and value - ${indexType} lastIdx = lastNode.index; - ${valueType} lastValue = _unsafeNodeAccess(self, lastIdx).value; + // get last leaf location (in the data array) and value + ${indexType} lastIdx = lastNode.index; + ${valueType} lastValue = _unsafeNodeAccess(self, lastIdx).value; - // move the last leaf to the root, pop last leaf ... - rootNode.index = lastIdx; - _unsafeNodeAccess(self, lastIdx).lookup = 0; - self.data.pop(); + // move the last leaf to the root, pop last leaf ... + rootNode.index = lastIdx; + _unsafeNodeAccess(self, lastIdx).lookup = 0; + self.data.pop(); - // ... and heapify - _siftDown(self, last, 0, lastValue, comp); + // ... and heapify + _siftDown(self, last, 0, lastValue, comp); - // return root value - return rootDataValue; + // return root value + return rootDataValue; + } } /** @@ -134,9 +136,8 @@ function insert( function(uint256, uint256) view returns (bool) comp ) internal { ${indexType} size = length(self); - if (size == type(${indexType}).max) { - Panic.panic(Panic.RESOURCE_ERROR); - } + if (size == type(${indexType}).max) Panic.panic(Panic.RESOURCE_ERROR); + self.data.push(${struct}Node({index: size, lookup: size, value: value})); _siftUp(self, size, value, comp); } From 516f1cab1b5b0f56f2758a9ea1227442474fceb9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 11:58:50 +0200 Subject: [PATCH 19/36] tests --- test/utils/structs/Heap.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index c4e9c28ae84..bd5d7c06cea 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -32,12 +32,13 @@ describe('Heap', function () { }); it('starts empty', async function () { - await expect(this.helper.peek()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); expect(await this.helper.length()).to.equal(0n); }); - it('pop from empty', async function () { + it('peek, pop and replace from empty', async function () { + await expect(this.helper.peek()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + await expect(this.helper.replace(0n)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); }); it('clear', async function () { From cf1278e3a0865669078e406a589a1ef801422fab Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 13:31:30 +0200 Subject: [PATCH 20/36] Update test/utils/structs/Heap.test.js --- test/utils/structs/Heap.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index bd5d7c06cea..30244dfa66a 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -74,7 +74,7 @@ describe('Heap', function () { await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); }); - it('insert, pop ansd replace', async function () { + it('insert, pop and replace', async function () { const heap = []; for (const { op, value } of [ { op: 'insert', value: 712 }, // [712] From 5f15d1cf5e176359067d2392db7893553e69ac68 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Jun 2024 13:32:13 +0200 Subject: [PATCH 21/36] Update test/utils/structs/Heap.test.js --- test/utils/structs/Heap.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index 30244dfa66a..7e95a0e7adb 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -86,7 +86,7 @@ describe('Heap', function () { { op: 'insert', value: 155 }, // [155, 165, 712, 1559, 4437] { op: 'insert', value: 7702 }, // [155, 165, 712, 1559, 4437, 7702] { op: 'pop' }, // 155, [165, 712, 1559, 4437, 7702] - { op: 'replace', value: 721 }, // [712, 721, 1559, 4437, 7702] + { op: 'replace', value: 721 }, // 165, [712, 721, 1559, 4437, 7702] { op: 'pop' }, // 712, [721, 1559, 4437, 7702] { op: 'pop' }, // 721, [1559, 4437, 7702] { op: 'pop' }, // 1559, [4437, 7702] From 32e9b494dd73626e01d957ed93ab98bff5a28325 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 27 Jun 2024 14:17:31 +0200 Subject: [PATCH 22/36] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto GarcĂ­a --- .changeset/great-pianos-work.md | 2 +- contracts/utils/README.adoc | 2 +- scripts/generate/templates/Heap.js | 15 ++++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.changeset/great-pianos-work.md b/.changeset/great-pianos-work.md index 8dfd83c4329..da54483e47e 100644 --- a/.changeset/great-pianos-work.md +++ b/.changeset/great-pianos-work.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Heap`: A data structure that implements a heap based priority-queue. +`Heap`: A data structure that implements a heap-based priority queue. diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index dc0978589eb..87ddb128077 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -23,7 +23,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be removed added or remove from both sides. Useful for FIFO and LIFO structures. * {CircularBuffer}: A data structure to store the last N values pushed to it. * {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time. - * {Heap}: A library that implement https://en.wikipedia.org/wiki/Binary_heap[binary heap] in storage. + * {Heap}: A library that implements a https://en.wikipedia.org/wiki/Binary_heap[binary heap] in storage. * {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions. * {Create2}: Wrapper around the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode] for safe use without having to deal with low-level assembly. * {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type. diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index 80b150a84d5..4ddac712cff 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -19,20 +19,21 @@ const generate = ({ struct, node, valueType, indexType, blockSize }) => `\ * - An array of values (payload). At each index we store a ${valueType} \`value\` and \`lookup\`, the index of the node * that points to this value. * - * Some invariant: + * Some invariants: * \`\`\` * i == heap.data[heap[data].index].lookup // for all index i * i == heap.data[heap[data].lookup].index // for all index i * \`\`\` * - * The structure is order so that each node is bigger then its parent. An immediate consequence is that the + * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the * smallest value is the one at the root. It can be retrieved in O(1) at \`heap.data[heap.data[0].index].value\` * - * This structure is designed for the following complexities: - * - peek (get the smallest value in set): O(1) - * - insert (insert a value in the set): 0(log(n)) - * - pop (remove the smallest value in set): O(log(n)) - * - replace (replace the smallest value in set with a new value): O(log(n)) + * The structure is designed to perform the following operations with the corresponding complexities: + * + * * peek (get the smallest value in set): O(1) + * * insert (insert a value in the set): 0(log(n)) + * * pop (remove the smallest value in set): O(log(n)) + * * replace (replace the smallest value in set with a new value): O(log(n)) */ struct ${struct} { ${node}[] data; From c083d7944d8f951989604aab3def234d8186f549 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 27 Jun 2024 14:18:06 +0200 Subject: [PATCH 23/36] regenrate --- contracts/utils/structs/Heap.sol | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 8601c7853c7..f4e578b4090 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -17,20 +17,21 @@ library Heap { * - An array of values (payload). At each index we store a uint256 `value` and `lookup`, the index of the node * that points to this value. * - * Some invariant: + * Some invariants: * ``` * i == heap.data[heap[data].index].lookup // for all index i * i == heap.data[heap[data].lookup].index // for all index i * ``` * - * The structure is order so that each node is bigger then its parent. An immediate consequence is that the + * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` * - * This structure is designed for the following complexities: - * - peek (get the smallest value in set): O(1) - * - insert (insert a value in the set): 0(log(n)) - * - pop (remove the smallest value in set): O(log(n)) - * - replace (replace the smallest value in set with a new value): O(log(n)) + * The structure is designed to perform the following operations with the corresponding complexities: + * + * * peek (get the smallest value in set): O(1) + * * insert (insert a value in the set): 0(log(n)) + * * pop (remove the smallest value in set): O(log(n)) + * * replace (replace the smallest value in set with a new value): O(log(n)) */ struct Uint256Heap { Uint256HeapNode[] data; @@ -299,20 +300,21 @@ library Heap { * - An array of values (payload). At each index we store a uint208 `value` and `lookup`, the index of the node * that points to this value. * - * Some invariant: + * Some invariants: * ``` * i == heap.data[heap[data].index].lookup // for all index i * i == heap.data[heap[data].lookup].index // for all index i * ``` * - * The structure is order so that each node is bigger then its parent. An immediate consequence is that the + * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` * - * This structure is designed for the following complexities: - * - peek (get the smallest value in set): O(1) - * - insert (insert a value in the set): 0(log(n)) - * - pop (remove the smallest value in set): O(log(n)) - * - replace (replace the smallest value in set with a new value): O(log(n)) + * The structure is designed to perform the following operations with the corresponding complexities: + * + * * peek (get the smallest value in set): O(1) + * * insert (insert a value in the set): 0(log(n)) + * * pop (remove the smallest value in set): O(log(n)) + * * replace (replace the smallest value in set with a new value): O(log(n)) */ struct Uint208Heap { Uint208HeapNode[] data; From 7c981028b4235ea4c296e2a4d4f268cc19a3de2f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 15 Jul 2024 17:06:58 +0200 Subject: [PATCH 24/36] update inline comments --- contracts/utils/structs/Heap.sol | 71 +++++++++++++----------------- scripts/generate/templates/Heap.js | 28 ++++++++---- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index f4e578b4090..e7e2c1d3d8b 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -7,31 +7,40 @@ import {SafeCast} from "../math/SafeCast.sol"; import {Comparators} from "../Comparators.sol"; import {Panic} from "../Panic.sol"; +/** + * @dev Library for managing https://en.wikipedia.org/wiki/Binary_heap[binary heap] that can be used as + * https://en.wikipedia.org/wiki/Priority_queue[priority queue]. + * + * Heaps are represented as an array of Node objects. In this array we store two overlapping structures: + * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. + * For each index in this tree we have the `index` pointer that gives the position of the corresponding value. + * - An array of values (payload). At each index we store a `value` and `lookup`. Type of `value` depends on the + * variant you chose. `lookup` is the index of the node (in the tree) that points to this value. + * + * Some invariants: + * ``` + * i == heap.data[heap.data[i].index].lookup // for all indices i + * i == heap.data[heap.data[i].lookup].index // for all indices i + * ``` + * + * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the + * smallest value is the one at the root. This value can be lookup up in constant time (O(1)) at + * `heap.data[heap.data[0].index].value` + * + * The structure is designed to perform the following operations with the corresponding complexities: + * + * * peek (get the smallest value in set): O(1) + * * insert (insert a value in the set): 0(log(n)) + * * pop (remove the smallest value in set): O(log(n)) + * * replace (replace the smallest value in set with a new value): O(log(n)) + */ library Heap { using SafeCast for *; /** - * A Heap is represented as an array of Node objects. In this array we store two overlapping structures: - * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. - * For each index in this tree we have the `index` pointer that gives the position of the corresponding value. - * - An array of values (payload). At each index we store a uint256 `value` and `lookup`, the index of the node - * that points to this value. + * @dev Binary heap that support values of type uint256. * - * Some invariants: - * ``` - * i == heap.data[heap[data].index].lookup // for all index i - * i == heap.data[heap[data].lookup].index // for all index i - * ``` - * - * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the - * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` - * - * The structure is designed to perform the following operations with the corresponding complexities: - * - * * peek (get the smallest value in set): O(1) - * * insert (insert a value in the set): 0(log(n)) - * * pop (remove the smallest value in set): O(log(n)) - * * replace (replace the smallest value in set with a new value): O(log(n)) + * Each element of that structures uses 2 storage slots. */ struct Uint256Heap { Uint256HeapNode[] data; @@ -294,27 +303,9 @@ library Heap { } /** - * A Heap is represented as an array of Node objects. In this array we store two overlapping structures: - * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. - * For each index in this tree we have the `index` pointer that gives the position of the corresponding value. - * - An array of values (payload). At each index we store a uint208 `value` and `lookup`, the index of the node - * that points to this value. - * - * Some invariants: - * ``` - * i == heap.data[heap[data].index].lookup // for all index i - * i == heap.data[heap[data].lookup].index // for all index i - * ``` - * - * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the - * smallest value is the one at the root. It can be retrieved in O(1) at `heap.data[heap.data[0].index].value` - * - * The structure is designed to perform the following operations with the corresponding complexities: + * @dev Binary heap that support values of type uint208. * - * * peek (get the smallest value in set): O(1) - * * insert (insert a value in the set): 0(log(n)) - * * pop (remove the smallest value in set): O(log(n)) - * * replace (replace the smallest value in set with a new value): O(log(n)) + * Each element of that structures uses 1 storage slots. */ struct Uint208Heap { Uint208HeapNode[] data; diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index 4ddac712cff..fdeae8330ad 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -9,24 +9,26 @@ pragma solidity ^0.8.20; import {SafeCast} from "../math/SafeCast.sol"; import {Comparators} from "../Comparators.sol"; import {Panic} from "../Panic.sol"; -`; -const generate = ({ struct, node, valueType, indexType, blockSize }) => `\ /** - * A Heap is represented as an array of Node objects. In this array we store two overlapping structures: + * @dev Library for managing https://en.wikipedia.org/wiki/Binary_heap[binary heap] that can be used as + * https://en.wikipedia.org/wiki/Priority_queue[priority queue]. + * + * Heaps are represented as an array of Node objects. In this array we store two overlapping structures: * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. * For each index in this tree we have the \`index\` pointer that gives the position of the corresponding value. - * - An array of values (payload). At each index we store a ${valueType} \`value\` and \`lookup\`, the index of the node - * that points to this value. + * - An array of values (payload). At each index we store a \`value\` and \`lookup\`. Type of \`value\` depends on the + * variant you chose. \`lookup\` is the index of the node (in the tree) that points to this value. * * Some invariants: * \`\`\` - * i == heap.data[heap[data].index].lookup // for all index i - * i == heap.data[heap[data].lookup].index // for all index i + * i == heap.data[heap.data[i].index].lookup // for all indices i + * i == heap.data[heap.data[i].lookup].index // for all indices i * \`\`\` * * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the - * smallest value is the one at the root. It can be retrieved in O(1) at \`heap.data[heap.data[0].index].value\` + * smallest value is the one at the root. This value can be lookup up in constant time (O(1)) at + * \`heap.data[heap.data[0].index].value\` * * The structure is designed to perform the following operations with the corresponding complexities: * @@ -35,6 +37,14 @@ const generate = ({ struct, node, valueType, indexType, blockSize }) => `\ * * pop (remove the smallest value in set): O(log(n)) * * replace (replace the smallest value in set with a new value): O(log(n)) */ +`; + +const generate = ({ struct, node, valueType, indexType, blockSize }) => `\ +/** + * @dev Binary heap that support values of type ${valueType}. + * + * Each element of that structures uses ${blockSize} storage slots. + */ struct ${struct} { ${node}[] data; } @@ -298,7 +308,7 @@ function _unsafeNodeAccess( // GENERATE module.exports = format( - header, + header.trimEnd(), 'library Heap {', format( [].concat( From a1767d4420fedb21d2cec9e71d9f7a17bc89d636 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 15 Jul 2024 17:13:42 +0200 Subject: [PATCH 25/36] update --- contracts/utils/structs/Heap.sol | 11 ++++++----- scripts/generate/templates/Heap.js | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index e7e2c1d3d8b..a7975c233c2 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -11,11 +11,12 @@ import {Panic} from "../Panic.sol"; * @dev Library for managing https://en.wikipedia.org/wiki/Binary_heap[binary heap] that can be used as * https://en.wikipedia.org/wiki/Priority_queue[priority queue]. * - * Heaps are represented as an array of Node objects. In this array we store two overlapping structures: - * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. - * For each index in this tree we have the `index` pointer that gives the position of the corresponding value. - * - An array of values (payload). At each index we store a `value` and `lookup`. Type of `value` depends on the - * variant you chose. `lookup` is the index of the node (in the tree) that points to this value. + * Heaps are represented as an array of Node objects. This array stores two overlapping structures: + * * A tree structure where the first element (index 0) is the root, and where the node at index i is the child of the + * node at index (i-1)/2 and the father of nodes at index 2*i+1 and 2*i+2. Each node stores the index (in the array) + * where the corresponding value is stored. + * * A list of payloads values where each index contains a value and a lookup index. The type of the value depends on + * the variant being used. The lookup is the index of the node (in the tree) that points to this value. * * Some invariants: * ``` diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index fdeae8330ad..1336ff65b8e 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -14,11 +14,12 @@ import {Panic} from "../Panic.sol"; * @dev Library for managing https://en.wikipedia.org/wiki/Binary_heap[binary heap] that can be used as * https://en.wikipedia.org/wiki/Priority_queue[priority queue]. * - * Heaps are represented as an array of Node objects. In this array we store two overlapping structures: - * - A tree structure, where index 0 is the root, and for each index i, the children are 2*i+1 and 2*i+2. - * For each index in this tree we have the \`index\` pointer that gives the position of the corresponding value. - * - An array of values (payload). At each index we store a \`value\` and \`lookup\`. Type of \`value\` depends on the - * variant you chose. \`lookup\` is the index of the node (in the tree) that points to this value. + * Heaps are represented as an array of Node objects. This array stores two overlapping structures: + * * A tree structure where the first element (index 0) is the root, and where the node at index i is the child of the + * node at index (i-1)/2 and the father of nodes at index 2*i+1 and 2*i+2. Each node stores the index (in the array) + * where the corresponding value is stored. + * * A list of payloads values where each index contains a value and a lookup index. The type of the value depends on + * the variant being used. The lookup is the index of the node (in the tree) that points to this value. * * Some invariants: * \`\`\` From 1c1e84bf8139f1ed97374c9c4d7332c45bd987f9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 16 Jul 2024 23:47:32 +0200 Subject: [PATCH 26/36] Address comment for the PR --- contracts/utils/structs/Heap.sol | 36 ++++++++++++++++-------------- scripts/generate/templates/Heap.js | 20 +++++++++-------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index a7975c233c2..d1e31541bc0 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -34,6 +34,8 @@ import {Panic} from "../Panic.sol"; * * insert (insert a value in the set): 0(log(n)) * * pop (remove the smallest value in set): O(log(n)) * * replace (replace the smallest value in set with a new value): O(log(n)) + * * length (get the number of elements in the set): O(1) + * * clear (remove all elements in the set): O(1) */ library Heap { using SafeCast for *; @@ -64,7 +66,7 @@ library Heap { /** * @dev Remove (and return) the root element for the heap using the default comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function pop(Uint256Heap storage self) internal returns (uint256) { @@ -74,7 +76,7 @@ library Heap { /** * @dev Remove (and return) the root element for the heap using the provided comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function pop( @@ -126,7 +128,7 @@ library Heap { /** * @dev Insert a new element in the heap using the default comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function insert(Uint256Heap storage self, uint256 value) internal { @@ -136,7 +138,7 @@ library Heap { /** * @dev Insert a new element in the heap using the provided comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function insert( @@ -155,7 +157,7 @@ library Heap { * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function replace(Uint256Heap storage self, uint256 newValue) internal returns (uint256) { @@ -166,7 +168,7 @@ library Heap { * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function replace( @@ -228,7 +230,7 @@ library Heap { * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a * comparator, and moving toward the leafs of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. `length` + * NOTE: This is a private function that is called in a trusted context with already cached parameters. `length` * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ @@ -272,7 +274,7 @@ library Heap { * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a * comparator, and moving toward the root of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. `value` + * NOTE: This is a private function that is called in a trusted context with already cached parameters. `value` * could be extracted from `self` and `pos`, but that would require redundant storage read. This parameters is not * verified. It is the caller role to make sure the parameters are correct. */ @@ -329,7 +331,7 @@ library Heap { /** * @dev Remove (and return) the root element for the heap using the default comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function pop(Uint208Heap storage self) internal returns (uint208) { @@ -339,7 +341,7 @@ library Heap { /** * @dev Remove (and return) the root element for the heap using the provided comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function pop( @@ -391,7 +393,7 @@ library Heap { /** * @dev Insert a new element in the heap using the default comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function insert(Uint208Heap storage self, uint208 value) internal { @@ -401,7 +403,7 @@ library Heap { /** * @dev Insert a new element in the heap using the provided comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function insert( @@ -420,7 +422,7 @@ library Heap { * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function replace(Uint208Heap storage self, uint208 newValue) internal returns (uint208) { @@ -431,7 +433,7 @@ library Heap { * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function replace( @@ -493,7 +495,7 @@ library Heap { * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a * comparator, and moving toward the leafs of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. `length` + * NOTE: This is a private function that is called in a trusted context with already cached parameters. `length` * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ @@ -537,7 +539,7 @@ library Heap { * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a * comparator, and moving toward the root of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. `value` + * NOTE: This is a private function that is called in a trusted context with already cached parameters. `value` * could be extracted from `self` and `pos`, but that would require redundant storage read. This parameters is not * verified. It is the caller role to make sure the parameters are correct. */ @@ -564,7 +566,7 @@ library Heap { ) private pure returns (Uint208HeapNode storage result) { assembly ("memory-safe") { mstore(0x00, self.slot) - result.slot := add(keccak256(0x00, 0x20), mul(pos, 1)) + result.slot := add(keccak256(0x00, 0x20), pos) } } } diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index 1336ff65b8e..2f15ce0390d 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -37,6 +37,8 @@ import {Panic} from "../Panic.sol"; * * insert (insert a value in the set): 0(log(n)) * * pop (remove the smallest value in set): O(log(n)) * * replace (replace the smallest value in set with a new value): O(log(n)) + * * length (get the number of elements in the set): O(1) + * * clear (remove all elements in the set): O(1) */ `; @@ -67,7 +69,7 @@ function peek(${struct} storage self) internal view returns (${valueType}) { /** * @dev Remove (and return) the root element for the heap using the default comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function pop(${struct} storage self) internal returns (${valueType}) { @@ -77,7 +79,7 @@ function pop(${struct} storage self) internal returns (${valueType}) { /** * @dev Remove (and return) the root element for the heap using the provided comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function pop( @@ -129,7 +131,7 @@ function pop( /** * @dev Insert a new element in the heap using the default comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function insert(${struct} storage self, ${valueType} value) internal { @@ -139,7 +141,7 @@ function insert(${struct} storage self, ${valueType} value) internal { /** * @dev Insert a new element in the heap using the provided comparator. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function insert( @@ -158,7 +160,7 @@ function insert( * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function replace(${struct} storage self, ${valueType} newValue) internal returns (${valueType}) { @@ -169,7 +171,7 @@ function replace(${struct} storage self, ${valueType} newValue) internal returns * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. * - * Note: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator + * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator * during the lifecycle of a heap will result in undefined behavior. */ function replace( @@ -231,7 +233,7 @@ function _swap(${struct} storage self, ${indexType} i, ${indexType} j) private { * @dev Perform heap maintenance on \`self\`, starting at position \`pos\` (with the \`value\`), using \`comp\` as a * comparator, and moving toward the leafs of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. \`length\` + * NOTE: This is a private function that is called in a trusted context with already cached parameters. \`length\` * and \`value\` could be extracted from \`self\` and \`pos\`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ @@ -275,7 +277,7 @@ function _siftDown( * @dev Perform heap maintenance on \`self\`, starting at position \`pos\` (with the \`value\`), using \`comp\` as a * comparator, and moving toward the root of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. \`value\` + * NOTE: This is a private function that is called in a trusted context with already cached parameters. \`value\` * could be extracted from \`self\` and \`pos\`, but that would require redundant storage read. This parameters is not * verified. It is the caller role to make sure the parameters are correct. */ @@ -302,7 +304,7 @@ function _unsafeNodeAccess( ) private pure returns (${node} storage result) { assembly ("memory-safe") { mstore(0x00, self.slot) - result.slot := add(keccak256(0x00, 0x20), mul(pos, ${blockSize})) + result.slot := add(keccak256(0x00, 0x20), ${blockSize == 1 ? 'pos' : `mul(pos, ${blockSize})`}) } } `; From 0e7fe7a96a7c9ef2dc3b32e41e64c330307329dd Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 17 Jul 2024 18:33:02 +0200 Subject: [PATCH 27/36] rewrite Arrays.sol to use uint256[] as the default, and use Comparator.lt as default --- contracts/utils/Arrays.sol | 72 +++++++++++++--------------- scripts/generate/templates/Arrays.js | 43 +++++++---------- 2 files changed, 52 insertions(+), 63 deletions(-) diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index d67ae90ba2b..fe54bafee7c 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; +import {Comparators} from "./Comparators.sol"; import {SlotDerivation} from "./SlotDerivation.sol"; import {StorageSlot} from "./StorageSlot.sol"; import {Math} from "./math/Math.sol"; @@ -16,7 +17,7 @@ library Arrays { using StorageSlot for bytes32; /** - * @dev Sort an array of bytes32 (in memory) following the provided comparator function. + * @dev Sort an array of uint256 (in memory) following the provided comparator function. * * This function does the sorting "in place", meaning that it overrides the input. The object is returned for * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array. @@ -27,18 +28,18 @@ library Arrays { * consume more gas than is available in a block, leading to potential DoS. */ function sort( - bytes32[] memory array, - function(bytes32, bytes32) pure returns (bool) comp - ) internal pure returns (bytes32[] memory) { + uint256[] memory array, + function(uint256, uint256) pure returns (bool) comp + ) internal pure returns (uint256[] memory) { _quickSort(_begin(array), _end(array), comp); return array; } /** - * @dev Variant of {sort} that sorts an array of bytes32 in increasing order. + * @dev Variant of {sort} that sorts an array of uint256 in increasing order. */ - function sort(bytes32[] memory array) internal pure returns (bytes32[] memory) { - sort(array, _defaultComp); + function sort(uint256[] memory array) internal pure returns (uint256[] memory) { + sort(array, Comparators.lt); return array; } @@ -57,7 +58,7 @@ library Arrays { address[] memory array, function(address, address) pure returns (bool) comp ) internal pure returns (address[] memory) { - sort(_castToBytes32Array(array), _castToBytes32Comp(comp)); + sort(_castToUint256Array(array), _castToUint256Comp(comp)); return array; } @@ -65,12 +66,12 @@ library Arrays { * @dev Variant of {sort} that sorts an array of address in increasing order. */ function sort(address[] memory array) internal pure returns (address[] memory) { - sort(_castToBytes32Array(array), _defaultComp); + sort(_castToUint256Array(array), Comparators.lt); return array; } /** - * @dev Sort an array of uint256 (in memory) following the provided comparator function. + * @dev Sort an array of bytes32 (in memory) following the provided comparator function. * * This function does the sorting "in place", meaning that it overrides the input. The object is returned for * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array. @@ -81,18 +82,18 @@ library Arrays { * consume more gas than is available in a block, leading to potential DoS. */ function sort( - uint256[] memory array, - function(uint256, uint256) pure returns (bool) comp - ) internal pure returns (uint256[] memory) { - sort(_castToBytes32Array(array), _castToBytes32Comp(comp)); + bytes32[] memory array, + function(bytes32, bytes32) pure returns (bool) comp + ) internal pure returns (bytes32[] memory) { + sort(_castToUint256Array(array), _castToUint256Comp(comp)); return array; } /** - * @dev Variant of {sort} that sorts an array of uint256 in increasing order. + * @dev Variant of {sort} that sorts an array of bytes32 in increasing order. */ - function sort(uint256[] memory array) internal pure returns (uint256[] memory) { - sort(_castToBytes32Array(array), _defaultComp); + function sort(bytes32[] memory array) internal pure returns (bytes32[] memory) { + sort(_castToUint256Array(array), Comparators.lt); return array; } @@ -105,12 +106,12 @@ library Arrays { * IMPORTANT: Memory locations between `begin` and `end` are not validated/zeroed. This function should * be used only if the limits are within a memory array. */ - function _quickSort(uint256 begin, uint256 end, function(bytes32, bytes32) pure returns (bool) comp) private pure { + function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure returns (bool) comp) private pure { unchecked { if (end - begin < 0x40) return; // Use first element as pivot - bytes32 pivot = _mload(begin); + uint256 pivot = _mload(begin); // Position where the pivot should be at the end of the loop uint256 pos = begin; @@ -132,7 +133,7 @@ library Arrays { /** * @dev Pointer to the memory location of the first element of `array`. */ - function _begin(bytes32[] memory array) private pure returns (uint256 ptr) { + function _begin(uint256[] memory array) private pure returns (uint256 ptr) { /// @solidity memory-safe-assembly assembly { ptr := add(array, 0x20) @@ -143,16 +144,16 @@ library Arrays { * @dev Pointer to the memory location of the first memory word (32bytes) after `array`. This is the memory word * that comes just after the last element of the array. */ - function _end(bytes32[] memory array) private pure returns (uint256 ptr) { + function _end(uint256[] memory array) private pure returns (uint256 ptr) { unchecked { return _begin(array) + array.length * 0x20; } } /** - * @dev Load memory word (as a bytes32) at location `ptr`. + * @dev Load memory word (as a uint256) at location `ptr`. */ - function _mload(uint256 ptr) private pure returns (bytes32 value) { + function _mload(uint256 ptr) private pure returns (uint256 value) { assembly { value := mload(ptr) } @@ -170,38 +171,33 @@ library Arrays { } } - /// @dev Comparator for sorting arrays in increasing order. - function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) { - return a < b; - } - /// @dev Helper: low level cast address memory array to uint256 memory array - function _castToBytes32Array(address[] memory input) private pure returns (bytes32[] memory output) { + function _castToUint256Array(address[] memory input) private pure returns (uint256[] memory output) { assembly { output := input } } - /// @dev Helper: low level cast uint256 memory array to uint256 memory array - function _castToBytes32Array(uint256[] memory input) private pure returns (bytes32[] memory output) { + /// @dev Helper: low level cast bytes32 memory array to uint256 memory array + function _castToUint256Array(bytes32[] memory input) private pure returns (uint256[] memory output) { assembly { output := input } } - /// @dev Helper: low level cast address comp function to bytes32 comp function - function _castToBytes32Comp( + /// @dev Helper: low level cast address comp function to uint256 comp function + function _castToUint256Comp( function(address, address) pure returns (bool) input - ) private pure returns (function(bytes32, bytes32) pure returns (bool) output) { + ) private pure returns (function(uint256, uint256) pure returns (bool) output) { assembly { output := input } } - /// @dev Helper: low level cast uint256 comp function to bytes32 comp function - function _castToBytes32Comp( - function(uint256, uint256) pure returns (bool) input - ) private pure returns (function(bytes32, bytes32) pure returns (bool) output) { + /// @dev Helper: low level cast bytes32 comp function to uint256 comp function + function _castToUint256Comp( + function(bytes32, bytes32) pure returns (bool) input + ) private pure returns (function(uint256, uint256) pure returns (bool) output) { assembly { output := input } diff --git a/scripts/generate/templates/Arrays.js b/scripts/generate/templates/Arrays.js index 30a6e069aa6..9823e4e5d7b 100644 --- a/scripts/generate/templates/Arrays.js +++ b/scripts/generate/templates/Arrays.js @@ -5,6 +5,7 @@ const { TYPES } = require('./Arrays.opts'); const header = `\ pragma solidity ^0.8.20; +import {Comparators} from "./Comparators.sol"; import {SlotDerivation} from "./SlotDerivation.sol"; import {StorageSlot} from "./StorageSlot.sol"; import {Math} from "./math/Math.sol"; @@ -31,9 +32,9 @@ function sort( function(${type}, ${type}) pure returns (bool) comp ) internal pure returns (${type}[] memory) { ${ - type === 'bytes32' + type === 'uint256' ? '_quickSort(_begin(array), _end(array), comp);' - : 'sort(_castToBytes32Array(array), _castToBytes32Comp(comp));' + : 'sort(_castToUint256Array(array), _castToUint256Comp(comp));' } return array; } @@ -42,7 +43,7 @@ function sort( * @dev Variant of {sort} that sorts an array of ${type} in increasing order. */ function sort(${type}[] memory array) internal pure returns (${type}[] memory) { - ${type === 'bytes32' ? 'sort(array, _defaultComp);' : 'sort(_castToBytes32Array(array), _defaultComp);'} + ${type === 'uint256' ? 'sort(array, Comparators.lt);' : 'sort(_castToUint256Array(array), Comparators.lt);'} return array; } `; @@ -57,12 +58,12 @@ const quickSort = `\ * IMPORTANT: Memory locations between \`begin\` and \`end\` are not validated/zeroed. This function should * be used only if the limits are within a memory array. */ -function _quickSort(uint256 begin, uint256 end, function(bytes32, bytes32) pure returns (bool) comp) private pure { +function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure returns (bool) comp) private pure { unchecked { if (end - begin < 0x40) return; // Use first element as pivot - bytes32 pivot = _mload(begin); + uint256 pivot = _mload(begin); // Position where the pivot should be at the end of the loop uint256 pos = begin; @@ -84,7 +85,7 @@ function _quickSort(uint256 begin, uint256 end, function(bytes32, bytes32) pure /** * @dev Pointer to the memory location of the first element of \`array\`. */ -function _begin(bytes32[] memory array) private pure returns (uint256 ptr) { +function _begin(uint256[] memory array) private pure returns (uint256 ptr) { /// @solidity memory-safe-assembly assembly { ptr := add(array, 0x20) @@ -95,16 +96,16 @@ function _begin(bytes32[] memory array) private pure returns (uint256 ptr) { * @dev Pointer to the memory location of the first memory word (32bytes) after \`array\`. This is the memory word * that comes just after the last element of the array. */ -function _end(bytes32[] memory array) private pure returns (uint256 ptr) { +function _end(uint256[] memory array) private pure returns (uint256 ptr) { unchecked { return _begin(array) + array.length * 0x20; } } /** - * @dev Load memory word (as a bytes32) at location \`ptr\`. + * @dev Load memory word (as a uint256) at location \`ptr\`. */ -function _mload(uint256 ptr) private pure returns (bytes32 value) { +function _mload(uint256 ptr) private pure returns (uint256 value) { assembly { value := mload(ptr) } @@ -123,16 +124,9 @@ function _swap(uint256 ptr1, uint256 ptr2) private pure { } `; -const defaultComparator = `\ -/// @dev Comparator for sorting arrays in increasing order. -function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) { - return a < b; -} -`; - const castArray = type => `\ /// @dev Helper: low level cast ${type} memory array to uint256 memory array -function _castToBytes32Array(${type}[] memory input) private pure returns (bytes32[] memory output) { +function _castToUint256Array(${type}[] memory input) private pure returns (uint256[] memory output) { assembly { output := input } @@ -140,10 +134,10 @@ function _castToBytes32Array(${type}[] memory input) private pure returns (bytes `; const castComparator = type => `\ -/// @dev Helper: low level cast ${type} comp function to bytes32 comp function -function _castToBytes32Comp( +/// @dev Helper: low level cast ${type} comp function to uint256 comp function +function _castToUint256Comp( function(${type}, ${type}) pure returns (bool) input -) private pure returns (function(bytes32, bytes32) pure returns (bool) output) { +) private pure returns (function(uint256, uint256) pure returns (bool) output) { assembly { output := input } @@ -374,12 +368,11 @@ module.exports = format( 'using StorageSlot for bytes32;', '', // sorting, comparator, helpers and internal - sort('bytes32'), - TYPES.filter(type => type !== 'bytes32').map(sort), + sort('uint256'), + TYPES.filter(type => type !== 'uint256').map(sort), quickSort, - defaultComparator, - TYPES.filter(type => type !== 'bytes32').map(castArray), - TYPES.filter(type => type !== 'bytes32').map(castComparator), + TYPES.filter(type => type !== 'uint256').map(castArray), + TYPES.filter(type => type !== 'uint256').map(castComparator), // lookup search, // unsafe (direct) storage and memory access From d4958593f1a0f6b2fa91011f8f00f78930e11ed7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 18 Jul 2024 17:30:27 +0200 Subject: [PATCH 28/36] Update scripts/generate/templates/Heap.js Co-authored-by: cairo --- scripts/generate/templates/Heap.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index 2f15ce0390d..abb7a6b2759 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -205,6 +205,9 @@ function length(${struct} storage self) internal view returns (${indexType}) { return self.data.length.to${capitalize(indexType)}(); } +/** + * @dev Removes all elements in the heap. + */ function clear(${struct} storage self) internal { ${struct}Node[] storage data = self.data; /// @solidity memory-safe-assembly From fe8e9029421882f7a5e812e2ef204bc7492f8357 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 18 Jul 2024 17:31:02 +0200 Subject: [PATCH 29/36] regenerate --- contracts/utils/structs/Heap.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index d1e31541bc0..eff9ed579c1 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -202,6 +202,9 @@ library Heap { return self.data.length.toUint32(); } + /** + * @dev Removes all elements in the heap. + */ function clear(Uint256Heap storage self) internal { Uint256HeapNode[] storage data = self.data; /// @solidity memory-safe-assembly @@ -467,6 +470,9 @@ library Heap { return self.data.length.toUint24(); } + /** + * @dev Removes all elements in the heap. + */ function clear(Uint208Heap storage self) internal { Uint208HeapNode[] storage data = self.data; /// @solidity memory-safe-assembly From 3abeb848fc1dba19504c531124c56309377cd0d6 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 22 Jul 2024 23:56:41 -0600 Subject: [PATCH 30/36] Add docs --- docs/modules/ROOT/pages/utilities.adoc | 29 +++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 31d4d5e33ed..ecb950caf91 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -189,6 +189,7 @@ Some use cases require more powerful data structures than arrays and mappings of - xref:api:utils.adoc#EnumerableSet[`EnumerableSet`]: A https://en.wikipedia.org/wiki/Set_(abstract_data_type)[set] with enumeration capabilities. - xref:api:utils.adoc#EnumerableMap[`EnumerableMap`]: A `mapping` variant with enumeration capabilities. - xref:api:utils.adoc#MerkleTree[`MerkleTree`]: An on-chain https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] with helper functions. +- xref:api:utils.adoc#Heap.sol[`Heap`]: A The `Enumerable*` structures are similar to mappings in that they store and remove elements in constant time and don't allow for repeated entries, but they also support _enumeration_, which means you can easily query all stored entries both on and off-chain. @@ -240,6 +241,32 @@ function _hashFn(bytes32 a, bytes32 b) internal view returns(bytes32) { } ---- +=== Using a Heap + +A https://en.wikipedia.org/wiki/Binary_heap[binary heap] is a data structure that always store the most important element at its peak and it can be used as a priority queue. + +To define what is most important in a heap, these frequently take comparator functions that tell the binary heap whether a value has more relevance than another. + +OpenZeppelin Contracts implements a Heap data structure with the properties of a binary heap. The heap uses the xref:api:utils.adoc#Comparators-lt-uint256-uint256-[`lt`] function by default but allows to customize its comparator. + +When using a custom comparator, it's recommended to wrap your function to avoid the possibility of mistakenly using a different comparator function: + +[source,solidity] +---- +function pop(Uint256Heap storage self) internal returns (uint256) { + return pop(self, Comparators.gt); +} + +function insert(Uint256Heap storage self, uint256 value) internal { + insert(self, value, Comparators.gt); +} + +function replace(Uint256Heap storage self, uint256 newValue) internal returns (uint256) { + return replace(self, newValue, Comparators.gt); +} +---- + + [[misc]] == Misc @@ -292,7 +319,7 @@ function _setImplementation(address newImplementation) internal { } ---- -The xref:api:utils.adoc#StorageSlot[`StorageSlot`] library also supports transient storage through user defined value types (UDVTs[https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types]), which enables the same value types as in Solidity. +The xref:api:utils.adoc#StorageSlot[`StorageSlot`] library also supports transient storage through user defined value types (https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types[UDVTs]), which enables the same value types as in Solidity. [source,solidity] ---- From 8801d98ad85a58cf3b30861b40172fe6c75d3e5e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 Jul 2024 09:21:17 +0200 Subject: [PATCH 31/36] Update scripts/generate/templates/Heap.js --- scripts/generate/templates/Heap.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index abb7a6b2759..a5793abc36f 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -52,6 +52,9 @@ struct ${struct} { ${node}[] data; } +/** + * @dev Internal node type for ${struct}. Stores a value of type ${valueType}. + */ struct ${node} { ${valueType} value; ${indexType} index; // position -> value From f78df0c7f029cbf5d82754eff1825c28cce9e45d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 Jul 2024 09:31:42 +0200 Subject: [PATCH 32/36] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto GarcĂ­a --- scripts/generate/templates/Heap.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index a5793abc36f..a72a15ae8de 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -260,13 +260,9 @@ function _siftDown( ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; ${valueType} rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; if (comp(lValue, value) || comp(rValue, value)) { - if (comp(lValue, rValue)) { - _swap(self, pos, lIndex); - _siftDown(self, size, lIndex, value, comp); - } else { - _swap(self, pos, rIndex); - _siftDown(self, size, rIndex, value, comp); - } + index = ternary(comp(lValue, rValue), lIndex, rIndex); + _swap(self, pos, index); + _siftDown(self, size, index, value, comp); } } else if (left < size) { // the check guarantees that \`left\` is a valid uint32 From bb37dfb5e232803ac7a999d45738dfe9ac142bbc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 Jul 2024 09:38:14 +0200 Subject: [PATCH 33/36] fix generation + change key type --- contracts/utils/structs/Heap.sol | 80 ++++++++++++------------- scripts/generate/templates/Heap.js | 4 +- scripts/generate/templates/Heap.opts.js | 2 +- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index eff9ed579c1..0c52a6ecc8c 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; +import {Math} from "../math/Math.sol"; import {SafeCast} from "../math/SafeCast.sol"; import {Comparators} from "../Comparators.sol"; import {Panic} from "../Panic.sol"; @@ -38,6 +39,7 @@ import {Panic} from "../Panic.sol"; * * clear (remove all elements in the set): O(1) */ library Heap { + using Math for *; using SafeCast for *; /** @@ -49,10 +51,13 @@ library Heap { Uint256HeapNode[] data; } + /** + * @dev Internal node type for Uint256Heap. Stores a value of type uint256. + */ struct Uint256HeapNode { uint256 value; - uint32 index; // position -> value - uint32 lookup; // value -> position + uint64 index; // position -> value + uint64 lookup; // value -> position } /** @@ -84,14 +89,14 @@ library Heap { function(uint256, uint256) view returns (bool) comp ) internal returns (uint256) { unchecked { - uint32 size = length(self); + uint64 size = length(self); if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - uint32 last = size - 1; + uint64 last = size - 1; // get root location (in the data array) and value Uint256HeapNode storage rootNode = _unsafeNodeAccess(self, 0); - uint32 rootIdx = rootNode.index; + uint64 rootIdx = rootNode.index; Uint256HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); Uint256HeapNode storage lastNode = _unsafeNodeAccess(self, last); uint256 rootDataValue = rootData.value; @@ -99,7 +104,7 @@ library Heap { // if root is not the last element of the data array (that will get pop-ed), reorder the data array. if (rootIdx != last) { // get details about the value stored in the last element of the array (that will get pop-ed) - uint32 lastDataIdx = lastNode.lookup; + uint64 lastDataIdx = lastNode.lookup; uint256 lastDataValue = lastNode.value; // copy these values to the location of the root (that is safe, and that we no longer use) rootData.value = lastDataValue; @@ -109,7 +114,7 @@ library Heap { } // get last leaf location (in the data array) and value - uint32 lastIdx = lastNode.index; + uint64 lastIdx = lastNode.index; uint256 lastValue = _unsafeNodeAccess(self, lastIdx).value; // move the last leaf to the root, pop last leaf ... @@ -146,8 +151,8 @@ library Heap { uint256 value, function(uint256, uint256) view returns (bool) comp ) internal { - uint32 size = length(self); - if (size == type(uint32).max) Panic.panic(Panic.RESOURCE_ERROR); + uint64 size = length(self); + if (size == type(uint64).max) Panic.panic(Panic.RESOURCE_ERROR); self.data.push(Uint256HeapNode({index: size, lookup: size, value: value})); _siftUp(self, size, value, comp); @@ -176,11 +181,11 @@ library Heap { uint256 newValue, function(uint256, uint256) view returns (bool) comp ) internal returns (uint256) { - uint32 size = length(self); + uint64 size = length(self); if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); // position of the node that holds the data for the root - uint32 rootIdx = _unsafeNodeAccess(self, 0).index; + uint64 rootIdx = _unsafeNodeAccess(self, 0).index; // storage pointer to the node that holds the data for the root Uint256HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); @@ -198,8 +203,8 @@ library Heap { /** * @dev Returns the number of elements in the heap. */ - function length(Uint256Heap storage self) internal view returns (uint32) { - return self.data.length.toUint32(); + function length(Uint256Heap storage self) internal view returns (uint64) { + return self.data.length.toUint64(); } /** @@ -216,11 +221,11 @@ library Heap { /* * @dev Swap node `i` and `j` in the tree. */ - function _swap(Uint256Heap storage self, uint32 i, uint32 j) private { + function _swap(Uint256Heap storage self, uint64 i, uint64 j) private { Uint256HeapNode storage ni = _unsafeNodeAccess(self, i); Uint256HeapNode storage nj = _unsafeNodeAccess(self, j); - uint32 ii = ni.index; - uint32 jj = nj.index; + uint64 ii = ni.index; + uint64 jj = nj.index; // update pointers to the data (swap the value) ni.index = jj; nj.index = ii; @@ -239,32 +244,28 @@ library Heap { */ function _siftDown( Uint256Heap storage self, - uint32 size, - uint32 pos, + uint64 size, + uint64 pos, uint256 value, function(uint256, uint256) view returns (bool) comp ) private { - uint256 left = 2 * pos + 1; // this could overflow uint32 - uint256 right = 2 * pos + 2; // this could overflow uint32 + uint256 left = 2 * pos + 1; // this could overflow uint64 + uint256 right = 2 * pos + 2; // this could overflow uint64 if (right < size) { // the check guarantees that `left` and `right` are both valid uint32 - uint32 lIndex = uint32(left); - uint32 rIndex = uint32(right); + uint64 lIndex = uint64(left); + uint64 rIndex = uint64(right); uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; uint256 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; if (comp(lValue, value) || comp(rValue, value)) { - if (comp(lValue, rValue)) { - _swap(self, pos, lIndex); - _siftDown(self, size, lIndex, value, comp); - } else { - _swap(self, pos, rIndex); - _siftDown(self, size, rIndex, value, comp); - } + uint64 index = uint64(comp(lValue, rValue).ternary(lIndex, rIndex)); + _swap(self, pos, index); + _siftDown(self, size, index, value, comp); } } else if (left < size) { // the check guarantees that `left` is a valid uint32 - uint32 lIndex = uint32(left); + uint64 lIndex = uint64(left); uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; if (comp(lValue, value)) { _swap(self, pos, lIndex); @@ -283,13 +284,13 @@ library Heap { */ function _siftUp( Uint256Heap storage self, - uint32 pos, + uint64 pos, uint256 value, function(uint256, uint256) view returns (bool) comp ) private { unchecked { while (pos > 0) { - uint32 parent = (pos - 1) / 2; + uint64 parent = (pos - 1) / 2; uint256 parentValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, parent).index).value; if (comp(parentValue, value)) break; _swap(self, pos, parent); @@ -300,7 +301,7 @@ library Heap { function _unsafeNodeAccess( Uint256Heap storage self, - uint32 pos + uint64 pos ) private pure returns (Uint256HeapNode storage result) { assembly ("memory-safe") { mstore(0x00, self.slot) @@ -317,6 +318,9 @@ library Heap { Uint208HeapNode[] data; } + /** + * @dev Internal node type for Uint208Heap. Stores a value of type uint208. + */ struct Uint208HeapNode { uint208 value; uint24 index; // position -> value @@ -522,13 +526,9 @@ library Heap { uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; uint208 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; if (comp(lValue, value) || comp(rValue, value)) { - if (comp(lValue, rValue)) { - _swap(self, pos, lIndex); - _siftDown(self, size, lIndex, value, comp); - } else { - _swap(self, pos, rIndex); - _siftDown(self, size, rIndex, value, comp); - } + uint24 index = uint24(comp(lValue, rValue).ternary(lIndex, rIndex)); + _swap(self, pos, index); + _siftDown(self, size, index, value, comp); } } else if (left < size) { // the check guarantees that `left` is a valid uint32 diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index a72a15ae8de..59328b66736 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -6,6 +6,7 @@ const { capitalize } = require('../../helpers'); const header = `\ pragma solidity ^0.8.20; +import {Math} from "../math/Math.sol"; import {SafeCast} from "../math/SafeCast.sol"; import {Comparators} from "../Comparators.sol"; import {Panic} from "../Panic.sol"; @@ -260,7 +261,7 @@ function _siftDown( ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; ${valueType} rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; if (comp(lValue, value) || comp(rValue, value)) { - index = ternary(comp(lValue, rValue), lIndex, rIndex); + ${indexType} index = ${indexType}(comp(lValue, rValue).ternary(lIndex, rIndex)); _swap(self, pos, index); _siftDown(self, size, index, value, comp); } @@ -317,6 +318,7 @@ module.exports = format( 'library Heap {', format( [].concat( + 'using Math for *;', 'using SafeCast for *;', '', TYPES.map(type => generate(type)), diff --git a/scripts/generate/templates/Heap.opts.js b/scripts/generate/templates/Heap.opts.js index 32bcce1c560..8b8be0afdfa 100644 --- a/scripts/generate/templates/Heap.opts.js +++ b/scripts/generate/templates/Heap.opts.js @@ -9,5 +9,5 @@ const makeType = (valueSize, indexSize) => ({ }); module.exports = { - TYPES: [makeType(256, 32), makeType(208, 24)], + TYPES: [makeType(256, 64), makeType(208, 24)], }; From 1fb4b812c3db7a1f9b3c10ab451ca0f9cffe5d22 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 Jul 2024 09:41:59 +0200 Subject: [PATCH 34/36] more invariant check --- scripts/generate/templates/Heap.t.js | 1 + test/utils/structs/Heap.t.sol | 2 ++ 2 files changed, 3 insertions(+) diff --git a/scripts/generate/templates/Heap.t.js b/scripts/generate/templates/Heap.t.js index 37e1774da53..04b3152ba3a 100644 --- a/scripts/generate/templates/Heap.t.js +++ b/scripts/generate/templates/Heap.t.js @@ -21,6 +21,7 @@ contract ${struct}Test is Test { for (uint32 i = 0; i < heap.length(); ++i) { // lookups assertEq(i, heap.data[heap.data[i].index].lookup); + assertEq(i, heap.data[heap.data[i].lookup].index); // ordering: each node has a value bigger then its parent if (i > 0) diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index 873f4c16a64..b9d0b98787c 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -17,6 +17,7 @@ contract Uint256HeapTest is Test { for (uint32 i = 0; i < heap.length(); ++i) { // lookups assertEq(i, heap.data[heap.data[i].index].lookup); + assertEq(i, heap.data[heap.data[i].lookup].index); // ordering: each node has a value bigger then its parent if (i > 0) @@ -88,6 +89,7 @@ contract Uint208HeapTest is Test { for (uint32 i = 0; i < heap.length(); ++i) { // lookups assertEq(i, heap.data[heap.data[i].index].lookup); + assertEq(i, heap.data[heap.data[i].lookup].index); // ordering: each node has a value bigger then its parent if (i > 0) From d3308c469ac1c4f34433e06c1deb4c4dcb36a86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 23 Jul 2024 08:58:45 -0600 Subject: [PATCH 35/36] Update scripts/generate/templates/Heap.js --- scripts/generate/templates/Heap.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js index 59328b66736..7ed99939bb5 100644 --- a/scripts/generate/templates/Heap.js +++ b/scripts/generate/templates/Heap.js @@ -29,15 +29,15 @@ import {Panic} from "../Panic.sol"; * \`\`\` * * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the - * smallest value is the one at the root. This value can be lookup up in constant time (O(1)) at + * highest priority value is the one at the root. This value can be lookup up in constant time (O(1)) at * \`heap.data[heap.data[0].index].value\` * * The structure is designed to perform the following operations with the corresponding complexities: * - * * peek (get the smallest value in set): O(1) + * * peek (get the highest priority in set): O(1) * * insert (insert a value in the set): 0(log(n)) - * * pop (remove the smallest value in set): O(log(n)) - * * replace (replace the smallest value in set with a new value): O(log(n)) + * * pop (remove the highest priority value in set): O(log(n)) + * * replace (replace the highest priority value in set with a new value): O(log(n)) * * length (get the number of elements in the set): O(1) * * clear (remove all elements in the set): O(1) */ From 5b07512a825c02993aaeda409de87d46e4e410c2 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 23 Jul 2024 08:59:10 -0600 Subject: [PATCH 36/36] Generate --- contracts/utils/structs/Heap.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 0c52a6ecc8c..ad684d40bdb 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -26,15 +26,15 @@ import {Panic} from "../Panic.sol"; * ``` * * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the - * smallest value is the one at the root. This value can be lookup up in constant time (O(1)) at + * highest priority value is the one at the root. This value can be lookup up in constant time (O(1)) at * `heap.data[heap.data[0].index].value` * * The structure is designed to perform the following operations with the corresponding complexities: * - * * peek (get the smallest value in set): O(1) + * * peek (get the highest priority in set): O(1) * * insert (insert a value in the set): 0(log(n)) - * * pop (remove the smallest value in set): O(log(n)) - * * replace (replace the smallest value in set with a new value): O(log(n)) + * * pop (remove the highest priority value in set): O(log(n)) + * * replace (replace the highest priority value in set with a new value): O(log(n)) * * length (get the number of elements in the set): O(1) * * clear (remove all elements in the set): O(1) */