From 42a3daa4a7a792f748be09681ddfa5d2d94b994f Mon Sep 17 00:00:00 2001 From: Nick Barry Date: Sun, 16 Mar 2025 21:19:13 -0600 Subject: [PATCH 1/2] Revert "remove BinaryHeap" This reverts commit 3062ca7c7420faf726654be3e555a3d30b1bc328. --- contracts/data/BinaryHeap.sol | 300 +++++++++++ test/data/BinaryHeap.ts | 931 ++++++++++++++++++++++++++++++++++ test/inheritance.ts | 1 + 3 files changed, 1232 insertions(+) create mode 100644 contracts/data/BinaryHeap.sol create mode 100644 test/data/BinaryHeap.ts diff --git a/contracts/data/BinaryHeap.sol b/contracts/data/BinaryHeap.sol new file mode 100644 index 000000000..133c1a5bb --- /dev/null +++ b/contracts/data/BinaryHeap.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @title Binary Heap implementation + * @dev The data structure is configured as a max-heap + */ +library BinaryHeap { + struct Heap { + bytes32[] _values; + // 1-indexed to allow 0 to signify nonexistence + mapping(bytes32 => uint256) _indexes; + } + + struct Bytes32Heap { + Heap _inner; + } + + struct AddressHeap { + Heap _inner; + } + + struct UintHeap { + Heap _inner; + } + + function at( + Bytes32Heap storage heap, + uint256 index + ) internal view returns (bytes32) { + return _at(heap._inner, index); + } + + function at( + AddressHeap storage heap, + uint256 index + ) internal view returns (address) { + return address(uint160(uint256(_at(heap._inner, index)))); + } + + function at( + UintHeap storage heap, + uint256 index + ) internal view returns (uint256) { + return uint256(_at(heap._inner, index)); + } + + function contains( + Bytes32Heap storage heap, + bytes32 value + ) internal view returns (bool) { + return _contains(heap._inner, value); + } + + function contains( + AddressHeap storage heap, + address value + ) internal view returns (bool) { + return _contains(heap._inner, bytes32(uint256(uint160(value)))); + } + + function contains( + UintHeap storage heap, + uint256 value + ) internal view returns (bool) { + return _contains(heap._inner, bytes32(value)); + } + + function indexOf( + Bytes32Heap storage heap, + bytes32 value + ) internal view returns (uint256) { + return _indexOf(heap._inner, value); + } + + function indexOf( + AddressHeap storage heap, + address value + ) internal view returns (uint256) { + return _indexOf(heap._inner, bytes32(uint256(uint160(value)))); + } + + function indexOf( + UintHeap storage heap, + uint256 value + ) internal view returns (uint256) { + return _indexOf(heap._inner, bytes32(value)); + } + + function length(Bytes32Heap storage heap) internal view returns (uint256) { + return _length(heap._inner); + } + + function length(AddressHeap storage heap) internal view returns (uint256) { + return _length(heap._inner); + } + + function length(UintHeap storage heap) internal view returns (uint256) { + return _length(heap._inner); + } + + function root(Bytes32Heap storage heap) internal view returns (bytes32) { + return _root(heap._inner); + } + + function root(AddressHeap storage heap) internal view returns (address) { + return address(uint160(uint256(_root(heap._inner)))); + } + + function root(UintHeap storage heap) internal view returns (uint256) { + return uint256(_root(heap._inner)); + } + + function add( + Bytes32Heap storage heap, + bytes32 value + ) internal returns (bool) { + return _add(heap._inner, value); + } + + function add( + AddressHeap storage heap, + address value + ) internal returns (bool) { + return _add(heap._inner, bytes32(uint256(uint160(value)))); + } + + function add(UintHeap storage heap, uint256 value) internal returns (bool) { + return _add(heap._inner, bytes32(value)); + } + + function remove( + Bytes32Heap storage heap, + bytes32 value + ) internal returns (bool) { + return _remove(heap._inner, value); + } + + function remove( + AddressHeap storage heap, + address value + ) internal returns (bool) { + return _remove(heap._inner, bytes32(uint256(uint160(value)))); + } + + function remove( + UintHeap storage heap, + uint256 value + ) internal returns (bool) { + return _remove(heap._inner, bytes32(value)); + } + + function toArray( + Bytes32Heap storage heap + ) internal view returns (bytes32[] memory) { + return heap._inner._values; + } + + function toArray( + AddressHeap storage heap + ) internal view returns (address[] memory) { + bytes32[] storage values = heap._inner._values; + address[] storage array; + + assembly { + array.slot := values.slot + } + + return array; + } + + function toArray( + UintHeap storage heap + ) internal view returns (uint256[] memory) { + bytes32[] storage values = heap._inner._values; + uint256[] storage array; + + assembly { + array.slot := values.slot + } + + return array; + } + + function _at( + Heap storage heap, + uint256 index + ) private view returns (bytes32) { + return heap._values[index]; + } + + function _contains( + Heap storage heap, + bytes32 value + ) private view returns (bool) { + return heap._indexes[value] != 0; + } + + function _indexOf( + Heap storage heap, + bytes32 value + ) private view returns (uint256) { + unchecked { + return heap._indexes[value] - 1; + } + } + + function _length(Heap storage heap) private view returns (uint256) { + return heap._values.length; + } + + function _root(Heap storage heap) private view returns (bytes32) { + return _at(heap, 0); + } + + function _add( + Heap storage heap, + bytes32 value + ) private returns (bool update) { + if (!_contains(heap, value)) { + heap._values.push(value); + heap._indexes[value] = _length(heap); + _heapify(heap); + + update = true; + } + } + + function _remove( + Heap storage heap, + bytes32 value + ) private returns (bool update) { + if (_contains(heap, value)) { + uint256 index = _indexOf(heap, value); + + unchecked { + // move node with last element in the tree, then remove it + _swap(heap, index, _length(heap) - 1); + } + + heap._values.pop(); + delete heap._indexes[value]; + + _heapify(heap); + + update = true; + } + } + + function _heapify(Heap storage heap) private { + uint256 len = _length(heap); + unchecked { + uint256 index = len / 2; + while (index > 0) { + _maxHeapify(heap, len, --index); + } + } + } + + function _maxHeapify( + Heap storage heap, + uint256 len, + uint256 index + ) private { + uint256 largest = index; + bytes32[] storage values = heap._values; + + unchecked { + uint256 left = (index << 1) | 1; + uint256 right = left + 1; + + if (left < len && values[largest] < values[left]) { + largest = left; + } + + if (right < len && values[largest] < values[right]) { + largest = right; + } + } + + if (largest != index) { + // swap until the largest node is the root node + _swap(heap, index, largest); + _maxHeapify(heap, len, largest); + } + } + + function _swap(Heap storage heap, uint256 a, uint256 b) private { + bytes32[] storage values = heap._values; + + bytes32 aValue = values[a]; + bytes32 bValue = values[b]; + + (values[a], values[b]) = (bValue, aValue); + + mapping(bytes32 => uint256) storage indexes = heap._indexes; + (indexes[aValue], indexes[bValue]) = (indexes[bValue], indexes[aValue]); + } +} diff --git a/test/data/BinaryHeap.ts b/test/data/BinaryHeap.ts new file mode 100644 index 000000000..9ba6f64d6 --- /dev/null +++ b/test/data/BinaryHeap.ts @@ -0,0 +1,931 @@ +import { PANIC_CODES } from '@nomicfoundation/hardhat-chai-matchers/panic'; +import { bigintToBytes32, bigintToAddress } from '@solidstate/library'; +import { $BinaryHeap, $BinaryHeap__factory } from '@solidstate/typechain-types'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +// data structures can be defined at any storage slot +// it doesn't matter which slot is used as long as it's consistent +const STORAGE_SLOT = 0n; + +const numbers = [0, 1, 2].map((n) => n); + +const constants = { + bytes32: numbers.map((n) => bigintToBytes32(n)), + address: numbers.map((n) => bigintToAddress(n)), + uint256: numbers, +}; + +const randomBytes32 = () => ethers.hexlify(ethers.randomBytes(32)); + +const randomAddress = () => + ethers.getAddress(ethers.zeroPadValue(ethers.randomBytes(20), 20)); + +const randomUint256 = () => BigInt(ethers.toQuantity(ethers.randomBytes(32))); + +// checks that the parent node is greater than or equal to the children nodes +function checkNodes(nodes: any[]) { + nodes.forEach((node, index) => { + const left = 2 * index + 1; + const right = 2 * index + 2; + + if (left < nodes.length && nodes[left] != null) { + expect(BigInt(node)).to.be.gte(BigInt(nodes[left])); + } + + if (right < nodes.length && nodes[right] != null) { + expect(BigInt(node)).to.be.gte(BigInt(nodes[right])); + } + }); +} + +describe('BinaryHeap', async () => { + describe('Bytes32Heap', async () => { + let instance: $BinaryHeap; + + beforeEach(async () => { + const [deployer] = await ethers.getSigners(); + instance = await new $BinaryHeap__factory(deployer).deploy(); + }); + + describe('__internal', () => { + describe('#at(bytes32)', () => { + it('returns the value corresponding to index provided', async () => { + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + + const array = + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = await instance.$at_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + key, + ); + expect(value).to.equal(array[key]); + } + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$at_BinaryHeap_Bytes32Heap(STORAGE_SLOT, 0n), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#contains(bytes32)', () => { + it('returns true if the value has been added', async () => { + const value = randomBytes32(); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + + expect( + await instance['$contains(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if the value has not been added', async () => { + expect( + await instance['$contains(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ), + ).to.be.false; + }); + }); + + describe('#indexOf(bytes32)', () => { + it('returns index of the value', async () => { + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + + const array = + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = array[key]; + const index = await instance[ + '$indexOf(uint256,bytes32)' + ].staticCall(STORAGE_SLOT, value); + expect(index).to.equal(key); + } + }); + + it('returns max uint256 if value does not exist', async () => { + expect( + await instance['$indexOf(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + randomBytes32(), + ), + ).to.equal(ethers.MaxUint256); + }); + }); + + describe('#length()', () => { + it('returns length of binary heap', async () => { + const values = [randomBytes32(), randomBytes32(), randomBytes32()]; + + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(0); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(1); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(2); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(3); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(2); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(1); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(0); + }); + }); + + describe('#root()', () => { + it('returns the highest value in the heap', async () => { + const [min, mid, max] = constants.bytes32; + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, min); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, mid); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(max); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(mid); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, mid); + + expect( + await instance.$root_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(min); + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$root_BinaryHeap_Bytes32Heap.staticCall(STORAGE_SLOT), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#add(bytes32)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is added', async () => { + for (let index = 0; index < 10; index++) { + const value = randomBytes32(); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + const nodes = + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ); + checkNodes(Array.from(nodes)); + } + }); + + it('returns true if value is added', async () => { + expect( + await instance['$add(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + randomBytes32(), + ), + ).to.be.true; + }); + + it('returns false if value has already been added', async () => { + const value = randomBytes32(); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + + expect( + await instance['$add(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.false; + }); + }); + + describe('#remove(bytes32)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is removed', async () => { + const values: string[] = []; + + for (let index = 0; index < 10; index++) { + const value = randomBytes32(); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + values.push(value); + } + + for (const value of values) { + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, value); + checkNodes( + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ); + } + }); + + it('returns true if value is removed', async () => { + const value = randomBytes32(); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + + expect( + await instance['$remove(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if value does not exist', async () => { + expect( + await instance['$remove(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + randomBytes32(), + ), + ).to.be.false; + }); + }); + + describe('#toArray()', () => { + it('returns the max heap as an array', async () => { + const [min, mid, max] = constants.bytes32; + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, min); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, mid); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, max); + + const array = + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ); + + expect(array.length).to.equal(3); + expect(array).to.deep.equal([max, min, mid]); + }); + }); + }); + }); + + describe('AddressHeap', async () => { + let instance: $BinaryHeap; + + beforeEach(async () => { + const [deployer] = await ethers.getSigners(); + instance = await new $BinaryHeap__factory(deployer).deploy(); + }); + + describe('__internal', () => { + describe('#at(address)', () => { + it('returns the value corresponding to index provided', async () => { + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + + const array = + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = await instance.$at_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + key, + ); + expect(value).to.equal(array[key]); + } + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$at_BinaryHeap_AddressHeap.staticCall(STORAGE_SLOT, 0n), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#contains(address)', () => { + it('returns true if the value has been added', async () => { + const value = randomAddress(); + + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + + expect( + await instance['$contains(uint256,address)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if the value has not been added', async () => { + expect( + await instance['$contains(uint256,address)'].staticCall( + STORAGE_SLOT, + randomAddress(), + ), + ).to.be.false; + }); + }); + + describe('#indexOf(address)', () => { + it('returns index of the value', async () => { + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + + const array = + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = array[key]; + const index = await instance[ + '$indexOf(uint256,address)' + ].staticCall(STORAGE_SLOT, value); + expect(index).to.equal(key); + } + }); + + it('returns max uint256 if value does not exist', async () => { + expect( + await instance['$indexOf(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ), + ).to.equal(ethers.MaxUint256); + }); + }); + + describe('#length()', () => { + it('returns length of binary heap', async () => { + const values = [randomAddress(), randomAddress(), randomAddress()]; + + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(0); + + await instance['$add(uint256,address)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(1); + + await instance['$add(uint256,address)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(2); + + await instance['$add(uint256,address)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(3); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(2); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(1); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(0); + }); + }); + + describe('#root()', () => { + it('returns the highest value in the heap', async () => { + const [min, mid, max] = constants.address; + + await instance['$add(uint256,address)'](STORAGE_SLOT, min); + await instance['$add(uint256,address)'](STORAGE_SLOT, mid); + await instance['$add(uint256,address)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(max); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(mid); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, mid); + + expect( + await instance.$root_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(min); + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$root_BinaryHeap_AddressHeap.staticCall(STORAGE_SLOT), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#add(address)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is added', async () => { + for (let index = 0; index < 10; index++) { + const value = randomAddress(); + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + const nodes = + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ); + checkNodes(Array.from(nodes)); + } + }); + + it('returns true if value is added', async () => { + expect( + await instance['$add(uint256,address)'].staticCall( + STORAGE_SLOT, + randomAddress(), + ), + ).to.be.true; + }); + + it('returns false if value has already been added', async () => { + const value = randomAddress(); + + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + + expect( + await instance['$add(uint256,address)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.false; + }); + }); + + describe('#remove(address)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is removed', async () => { + const values: string[] = []; + + for (let index = 0; index < 10; index++) { + const value = randomAddress(); + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + values.push(value); + } + + for (const value of values) { + await instance['$remove(uint256,address)'](STORAGE_SLOT, value); + checkNodes( + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ); + } + }); + + it('returns true if value is removed', async () => { + const value = randomAddress(); + + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + + expect( + await instance['$remove(uint256,address)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if value does not exist', async () => { + expect( + await instance['$remove(uint256,address)'].staticCall( + STORAGE_SLOT, + randomAddress(), + ), + ).to.be.false; + }); + }); + + describe('#toArray()', () => { + it('returns the max heap as an array', async () => { + const [min, mid, max] = constants.address; + + await instance['$add(uint256,address)'](STORAGE_SLOT, min); + await instance['$add(uint256,address)'](STORAGE_SLOT, mid); + await instance['$add(uint256,address)'](STORAGE_SLOT, max); + + const array = + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ); + + expect(array.length).to.equal(3); + expect(array).to.deep.equal([max, min, mid]); + }); + }); + }); + }); + + describe('UintHeap', async () => { + let instance: $BinaryHeap; + + beforeEach(async () => { + const [deployer] = await ethers.getSigners(); + instance = await new $BinaryHeap__factory(deployer).deploy(); + }); + + describe('__internal', () => { + describe('#at(uint256)', () => { + it('returns the value corresponding to index provided', async () => { + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + + const array = + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = await instance.$at_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + key, + ); + expect(value).to.equal(array[key]); + } + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$at_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT, 0n), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#contains(uint256)', () => { + it('returns true if the value has been added', async () => { + const value = randomUint256(); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + + expect( + await instance['$contains(uint256,uint256)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if the value has not been added', async () => { + expect( + await instance['$contains(uint256,uint256)'].staticCall( + STORAGE_SLOT, + randomUint256(), + ), + ).to.be.false; + }); + }); + + describe('#indexOf(uint256)', () => { + it('returns index of the value', async () => { + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + + const array = + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = array[key]; + const index = await instance[ + '$indexOf(uint256,uint256)' + ].staticCall(STORAGE_SLOT, value); + expect(index).to.equal(key); + } + }); + + it('returns max uint256 if value does not exist', async () => { + expect( + await instance['$indexOf(uint256,uint256)'].staticCall( + STORAGE_SLOT, + randomUint256(), + ), + ).to.equal(ethers.MaxUint256); + }); + }); + + describe('#length()', () => { + it('returns length of binary heap', async () => { + const values = [randomUint256(), randomUint256(), randomUint256()]; + + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(0); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(1); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(2); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(3); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(2); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(1); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(0); + }); + }); + + describe('#root()', () => { + it('returns the highest value in the heap', async () => { + const [min, mid, max] = constants.uint256; + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, min); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, mid); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(max); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(mid); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, mid); + + expect( + await instance.$root_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(min); + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$root_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#add(uint256)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is added', async () => { + for (let index = 0; index < 10; index++) { + const value = randomUint256(); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + const nodes = + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ); + checkNodes(Array.from(nodes)); + } + }); + + it('returns true if value is added', async () => { + expect( + await instance['$add(uint256,uint256)'].staticCall( + STORAGE_SLOT, + randomUint256(), + ), + ).to.be.true; + }); + + it('returns false if value has already been added', async () => { + const value = randomUint256(); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + + expect( + await instance['$add(uint256,uint256)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.false; + }); + }); + + describe('#remove(uint256)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is removed', async () => { + const values: bigint[] = []; + + for (let index = 0; index < 10; index++) { + const value = randomUint256(); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + values.push(value); + } + + for (const value of values) { + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, value); + checkNodes( + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ), + ); + } + }); + + it('returns true if value is removed', async () => { + const value = randomUint256(); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + + expect( + await instance['$remove(uint256,uint256)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if value does not exist', async () => { + expect( + await instance['$remove(uint256,uint256)'].staticCall( + STORAGE_SLOT, + randomUint256(), + ), + ).to.be.false; + }); + }); + + describe('#toArray()', () => { + it('returns the max heap as an array', async () => { + const [min, mid, max] = constants.uint256; + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, min); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, mid); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, max); + + const array = + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ); + + expect(array.length).to.equal(3); + expect(array).to.deep.equal([max, min, mid]); + }); + }); + }); + }); +}); diff --git a/test/inheritance.ts b/test/inheritance.ts index 049de9593..157925c6e 100644 --- a/test/inheritance.ts +++ b/test/inheritance.ts @@ -323,6 +323,7 @@ describe('Inheritance Graph', () => { 'ECDSA', 'EIP712', 'MerkleProof', + 'BinaryHeap', 'DoublyLinkedList', 'PackedDoublyLinkedList', 'EnumerableMap', From 517c2488e77c4a4ffe825c60386492975a2d6589 Mon Sep 17 00:00:00 2001 From: Nick Barry Date: Sun, 16 Mar 2025 21:33:19 -0600 Subject: [PATCH 2/2] use storage array casting utils in BinaryHeap --- contracts/data/BinaryHeap.sol | 36 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/contracts/data/BinaryHeap.sol b/contracts/data/BinaryHeap.sol index 133c1a5bb..1859b1a2b 100644 --- a/contracts/data/BinaryHeap.sol +++ b/contracts/data/BinaryHeap.sol @@ -2,11 +2,15 @@ pragma solidity ^0.8.20; +import { ArrayUtils } from '../utils/ArrayUtils.sol'; + /** * @title Binary Heap implementation * @dev The data structure is configured as a max-heap */ library BinaryHeap { + using ArrayUtils for bytes32[]; + struct Heap { bytes32[] _values; // 1-indexed to allow 0 to signify nonexistence @@ -153,34 +157,20 @@ library BinaryHeap { function toArray( Bytes32Heap storage heap - ) internal view returns (bytes32[] memory) { - return heap._inner._values; + ) internal view returns (bytes32[] memory array) { + array = _toArray(heap._inner); } function toArray( AddressHeap storage heap - ) internal view returns (address[] memory) { - bytes32[] storage values = heap._inner._values; - address[] storage array; - - assembly { - array.slot := values.slot - } - - return array; + ) internal view returns (address[] memory array) { + array = _toArray(heap._inner).toAddressArray(); } function toArray( UintHeap storage heap - ) internal view returns (uint256[] memory) { - bytes32[] storage values = heap._inner._values; - uint256[] storage array; - - assembly { - array.slot := values.slot - } - - return array; + ) internal view returns (uint256[] memory array) { + array = _toArray(heap._inner).toUint256Array(); } function _at( @@ -248,6 +238,12 @@ library BinaryHeap { } } + function _toArray( + Heap storage heap + ) private view returns (bytes32[] storage array) { + array = heap._values; + } + function _heapify(Heap storage heap) private { uint256 len = _length(heap); unchecked {