diff --git a/.changeset/long-flowers-itch.md b/.changeset/long-flowers-itch.md new file mode 100644 index 00000000000..f89ea4e2471 --- /dev/null +++ b/.changeset/long-flowers-itch.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`BitMaps`: Add new map types `PairMap`, `NibbleMap`, `Uint8Map`, `Uint16Map`, `Uint32Map`, `Uint64Map`, and `Uint128Map` for gas-efficient storage of packed values with different bit widths. diff --git a/contracts/utils/structs/BitMaps.sol b/contracts/utils/structs/BitMaps.sol index 40cceb90bd4..18ac3ab0b24 100644 --- a/contracts/utils/structs/BitMaps.sol +++ b/contracts/utils/structs/BitMaps.sol @@ -1,19 +1,28 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/BitMaps.sol) +// This file was procedurally generated from scripts/generate/templates/BitMaps.js. + pragma solidity ^0.8.20; /** - * @dev Library for managing uint256 to bool mapping in a compact and efficient way, provided the keys are sequential. + * @dev Library for managing bytes-based mappings in a compact and efficient way, provided the keys are sequential. * Largely inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor]. * - * BitMaps pack 256 booleans across each bit of a single 256-bit slot of `uint256` type. - * Hence booleans corresponding to 256 _sequential_ indices would only consume a single slot, - * unlike the regular `bool` which would consume an entire slot for a single value. + * The library provides several map types that pack multiple values into single 256-bit storage slots: + * + * * `BitMap`: 256 booleans per slot (1 bit each) + * * `PairMap`: 128 2-bit values per slot + * * `NibbleMap`: 64 4-bit values per slot + * * `Uint8Map`: 32 8-bit values per slot + * * `Uint16Map`: 16 16-bit values per slot + * * `Uint32Map`: 8 32-bit values per slot + * * `Uint64Map`: 4 64-bit values per slot + * * `Uint128Map`: 2 128-bit values per slot * - * This results in gas savings in two ways: + * This approach provides significant gas savings compared to using individual storage slots for each value: * - * - Setting a zero value to non-zero only once every 256 times - * - Accessing the same warm slot for every 256 _sequential_ indices + * * Setting a zero value to non-zero only once every N times (where N is the packing density) + * * Accessing the same warm slot for every N _sequential_ indices */ library BitMaps { struct BitMap { @@ -57,4 +66,179 @@ library BitMaps { uint256 mask = 1 << (index & 0xff); bitmap._data[bucket] &= ~mask; } + + struct PairMap { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 2-bit value at `index` in `map`. + */ + function get(PairMap storage map, uint256 index) internal view returns (uint8) { + uint256 bucket = index >> 7; // 128 values per bucket (256/2) + uint256 shift = (index & 0x7f) << 1; // i.e. (index % 128) * 2 = position * 2 + return uint8((map._data[bucket] >> shift) & 0x03); + } + + /** + * @dev Sets the 2-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 2 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(PairMap storage map, uint256 index, uint8 value) internal { + uint256 bucket = index >> 7; // 128 values per bucket (256/2) + uint256 shift = (index & 0x7f) << 1; // i.e. (index % 128) * 2 = position * 2 + uint256 mask = 0x03 << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 2 bits + } + + struct NibbleMap { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 4-bit value at `index` in `map`. + */ + function get(NibbleMap storage map, uint256 index) internal view returns (uint8) { + uint256 bucket = index >> 6; // 64 values per bucket (256/4) + uint256 shift = (index & 0x3f) << 2; // i.e. (index % 64) * 4 = position * 4 + return uint8((map._data[bucket] >> shift) & 0x0f); + } + + /** + * @dev Sets the 4-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 4 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(NibbleMap storage map, uint256 index, uint8 value) internal { + uint256 bucket = index >> 6; // 64 values per bucket (256/4) + uint256 shift = (index & 0x3f) << 2; // i.e. (index % 64) * 4 = position * 4 + uint256 mask = 0x0f << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 4 bits + } + + struct Uint8Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 8-bit value at `index` in `map`. + */ + function get(Uint8Map storage map, uint256 index) internal view returns (uint8) { + uint256 bucket = index >> 5; // 32 values per bucket (256/8) + uint256 shift = (index & 0x1f) << 3; // i.e. (index % 32) * 8 = position * 8 + return uint8(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 8-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 8 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint8Map storage map, uint256 index, uint8 value) internal { + uint256 bucket = index >> 5; // 32 values per bucket (256/8) + uint256 shift = (index & 0x1f) << 3; // i.e. (index % 32) * 8 = position * 8 + uint256 mask = 0xff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 8 bits + } + + struct Uint16Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 16-bit value at `index` in `map`. + */ + function get(Uint16Map storage map, uint256 index) internal view returns (uint16) { + uint256 bucket = index >> 4; // 16 values per bucket (256/16) + uint256 shift = (index & 0x0f) << 4; // i.e. (index % 16) * 16 = position * 16 + return uint16(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 16-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 16 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint16Map storage map, uint256 index, uint16 value) internal { + uint256 bucket = index >> 4; // 16 values per bucket (256/16) + uint256 shift = (index & 0x0f) << 4; // i.e. (index % 16) * 16 = position * 16 + uint256 mask = 0xffff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 16 bits + } + + struct Uint32Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 32-bit value at `index` in `map`. + */ + function get(Uint32Map storage map, uint256 index) internal view returns (uint32) { + uint256 bucket = index >> 3; // 8 values per bucket (256/32) + uint256 shift = (index & 0x07) << 5; // i.e. (index % 8) * 32 = position * 32 + return uint32(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 32-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 32 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint32Map storage map, uint256 index, uint32 value) internal { + uint256 bucket = index >> 3; // 8 values per bucket (256/32) + uint256 shift = (index & 0x07) << 5; // i.e. (index % 8) * 32 = position * 32 + uint256 mask = 0xffffffff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 32 bits + } + + struct Uint64Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 64-bit value at `index` in `map`. + */ + function get(Uint64Map storage map, uint256 index) internal view returns (uint64) { + uint256 bucket = index >> 2; // 4 values per bucket (256/64) + uint256 shift = (index & 0x03) << 6; // i.e. (index % 4) * 64 = position * 64 + return uint64(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 64-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 64 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint64Map storage map, uint256 index, uint64 value) internal { + uint256 bucket = index >> 2; // 4 values per bucket (256/64) + uint256 shift = (index & 0x03) << 6; // i.e. (index % 4) * 64 = position * 64 + uint256 mask = 0xffffffffffffffff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 64 bits + } + + struct Uint128Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 128-bit value at `index` in `map`. + */ + function get(Uint128Map storage map, uint256 index) internal view returns (uint128) { + uint256 bucket = index >> 1; // 2 values per bucket (256/128) + uint256 shift = (index & 0x01) << 7; // i.e. (index % 2) * 128 = position * 128 + return uint128(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 128-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 128 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint128Map storage map, uint256 index, uint128 value) internal { + uint256 bucket = index >> 1; // 2 values per bucket (256/128) + uint256 shift = (index & 0x01) << 7; // i.e. (index % 2) * 128 = position * 128 + uint256 mask = 0xffffffffffffffffffffffffffffffff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 128 bits + } } diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 6fe2de63b9d..67e17325b43 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -193,6 +193,81 @@ Some use cases require more powerful data structures than arrays and mappings of 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. +=== Efficient Storage with BitMaps + +The xref:api:utils.adoc#BitMaps[`BitMaps`] library provides a collection of gas-efficient data structures that pack multiple values into single storage slots. This approach can significantly reduce gas costs by minimizing storage operations, especially when dealing with sequential indices. + +==== Using BitMaps for Boolean Flags + +The most common use case is `BitMap` for storing boolean flags efficiently: + +[source,solidity] +---- +using BitMaps for BitMaps.BitMap; + +contract VotingContract { + BitMaps.BitMap private _hasVoted; + + function vote(uint256 proposalId) external { + require(!_hasVoted.get(proposalId), "Already voted"); + _hasVoted.set(proposalId); + // ... voting logic + } + + function hasVoted(uint256 proposalId) external view returns (bool) { + return _hasVoted.get(proposalId); + } +} +---- + +==== Working with Small Integer Values + +For scenarios requiring small integer values, use the appropriate map type. For example, storing user roles that fit in 2 bits: + +[source,solidity] +---- +using BitMaps for BitMaps.PairMap; + +contract AccessControl { + // 0: no access, 1: read, 2: write, 3: admin + BitMaps.PairMap private _userRoles; + + function setUserRole(uint256 userId, uint8 role) external { + require(role <= 3, "Invalid role"); + _userRoles.set(userId, role); + } + + function getUserRole(uint256 userId) external view returns (uint8) { + return _userRoles.get(userId); + } +} +---- + +==== Handling Larger Values + +For larger values, use the appropriate sized map. Here's an example with time delays that fit in 32 bits: + +[source,solidity] +---- +using BitMaps for BitMaps.Uint32Map; + +contract DelaysTracker { + BitMaps.Uint32Map private _delays; + + function recordDelay(uint256 id) external { + _delays.set(id, 12 hours); + } + + function getDelay(uint256 id) external view returns (uint32) { + return _delays.get(id); + } +} +---- + +IMPORTANT: Values larger than the map's bit size are truncated. For example, setting a value of 16 in a `NibbleMap` (4-bit) will result in 0. Always ensure your values fit within the allocated bits. + +TIP: BitMaps are most efficient when indices are sequential or clustered, as they share storage slots. Sparse indices may not provide significant gas savings over regular mappings. + === Building a Merkle Tree Building an on-chain Merkle Tree allows developers to keep track of the history of roots in a decentralized manner. For these cases, the xref:api:utils.adoc#MerkleTree[`MerkleTree`] includes a predefined structure with functions to manipulate the tree (e.g. pushing values or resetting the tree). @@ -482,4 +557,4 @@ function _setTargetAdminDelay(address target, uint32 newDelay) internal virtual emit TargetAdminDelayUpdated(target, newDelay, effect); } ----- \ No newline at end of file +---- diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 6779c93f44b..1e0e23ca83e 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -37,6 +37,7 @@ for (const [file, template] of Object.entries({ 'utils/structs/Checkpoints.sol': './templates/Checkpoints.js', 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', + 'utils/structs/BitMaps.sol': './templates/BitMaps.js', 'utils/SlotDerivation.sol': './templates/SlotDerivation.js', 'utils/StorageSlot.sol': './templates/StorageSlot.js', 'utils/TransientSlot.sol': './templates/TransientSlot.js', @@ -51,6 +52,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/BitMaps.t.sol': './templates/BitMaps.t.js', 'utils/Packing.t.sol': './templates/Packing.t.js', 'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js', })) { diff --git a/scripts/generate/templates/BitMaps.js b/scripts/generate/templates/BitMaps.js new file mode 100644 index 00000000000..9a046f79572 --- /dev/null +++ b/scripts/generate/templates/BitMaps.js @@ -0,0 +1,100 @@ +const format = require('../format-lines'); +const { TYPES } = require('./BitMaps.opts'); + +const header = `\ +pragma solidity ^0.8.20; + +/** + * @dev Library for managing bytes-based mappings in a compact and efficient way, provided the keys are sequential. + * Largely inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor]. + * + * The library provides several map types that pack multiple values into single 256-bit storage slots: + * + * * \`BitMap\`: 256 booleans per slot (1 bit each) +${TYPES.map(({ bits, name }) => ` * * \`${name}\`: ${256n / bits} ${bits}-bit values per slot`).join('\n')} + * + * This approach provides significant gas savings compared to using individual storage slots for each value: + * + * * Setting a zero value to non-zero only once every N times (where N is the packing density) + * * Accessing the same warm slot for every N _sequential_ indices + */ +`; + +const bitmapTemplate = `\ +struct BitMap { + mapping(uint256 bucket => uint256) _data; +} + +/** + * @dev Returns whether the bit at \`index\` is set. + */ +function get(BitMap storage bitmap, uint256 index) internal view returns (bool) { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + return bitmap._data[bucket] & mask != 0; +} + +/** + * @dev Sets the bit at \`index\` to the boolean \`value\`. + */ +function setTo(BitMap storage bitmap, uint256 index, bool value) internal { + if (value) { + set(bitmap, index); + } else { + unset(bitmap, index); + } +} + +/** + * @dev Sets the bit at \`index\`. + */ +function set(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] |= mask; +} + +/** + * @dev Unsets the bit at \`index\`. + */ +function unset(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] &= ~mask; +} +`; + +const byteTemplates = opts => `\ +struct ${opts.name} { + mapping(uint256 bucket => uint256) _data; +} + +/** + * @dev Returns the ${opts.bits}-bit value at \`index\` in \`map\`. + */ +function get(${opts.name} storage map, uint256 index) internal view returns (${opts.type}) { + uint256 bucket = index >> ${8 - opts.width}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) + uint256 shift = (index & ${opts.mask}) << ${opts.width}; // i.e. (index % ${Number(opts.mask) + 1}) * ${opts.bits} = position * ${opts.bits} + return ${opts.type}(${opts.bits < 8 ? `(map._data[bucket] >> shift) & ${opts.max}` : `map._data[bucket] >> shift`}); +} + +/** + * @dev Sets the ${opts.bits}-bit value at \`index\` in \`map\`. + * + * NOTE: Assumes \`value\` fits in ${opts.bits} bits. Assembly-manipulated values may corrupt adjacent data. + */ +function set(${opts.name} storage map, uint256 index, ${opts.type} value) internal { + uint256 bucket = index >> ${8 - opts.width}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) + uint256 shift = (index & ${opts.mask}) << ${opts.width}; // i.e. (index % ${Number(opts.mask) + 1}) * ${opts.bits} = position * ${opts.bits} + uint256 mask = ${opts.max} << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the ${opts.bits} bits +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + `library BitMaps {`, + format([].concat(bitmapTemplate, TYPES.map(byteTemplates))).trimEnd(), + '}', +); diff --git a/scripts/generate/templates/BitMaps.opts.js b/scripts/generate/templates/BitMaps.opts.js new file mode 100644 index 00000000000..66f558e3957 --- /dev/null +++ b/scripts/generate/templates/BitMaps.opts.js @@ -0,0 +1,20 @@ +const { ethers } = require('ethers'); + +const TYPES = [ + { bits: 2n, name: 'PairMap' }, + { bits: 4n, name: 'NibbleMap' }, + { bits: 8n }, + { bits: 16n }, + { bits: 32n }, + { bits: 64n }, + { bits: 128n }, +].map(({ bits, name = `Uint${bits}Map` }) => ({ + bits, + name, + max: ethers.toBeHex((1n << bits) - 1n), + mask: ethers.toBeHex(256n / bits - 1n), + type: `uint${Math.max(Number(bits), 8)}`, + width: Math.log2(Number(bits)), +})); + +module.exports = { TYPES }; diff --git a/scripts/generate/templates/BitMaps.t.js b/scripts/generate/templates/BitMaps.t.js new file mode 100644 index 00000000000..23dc2fcb954 --- /dev/null +++ b/scripts/generate/templates/BitMaps.t.js @@ -0,0 +1,124 @@ +const format = require('../format-lines'); +const { TYPES } = require('./BitMaps.opts'); + +const header = `\ +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +`; + +const bitmapTests = `\ +// ========== BitMap Tests ========== + +BitMaps.BitMap[2] private _bitmaps; + +function testSymbolicBitMapSetAndGet(uint256 value) public { + assertFalse(_bitmaps[0].get(value)); // initial state + _bitmaps[0].set(value); + assertTrue(_bitmaps[0].get(value)); // after set +} + +function testSymbolicBitMapSetToAndGet(uint256 value) public { + _bitmaps[0].setTo(value, true); + assertTrue(_bitmaps[0].get(value)); // after setTo true + _bitmaps[0].setTo(value, false); + assertFalse(_bitmaps[0].get(value)); // after setTo false +} + +function testSymbolicBitMapIsolation(uint256 value1, uint256 value2) public { + vm.assume(value1 != value2); + + _bitmaps[0].set(value1); + _bitmaps[1].set(value2); + + assertTrue(_bitmaps[0].get(value1)); + assertTrue(_bitmaps[1].get(value2)); + assertFalse(_bitmaps[0].get(value2)); + assertFalse(_bitmaps[1].get(value1)); +} +`; + +const testSymbolicSetAndGet = opts => `\ +function testSymbolic${opts.name}SetAndGet(uint256 index, ${opts.type} value) public { + value = ${opts.type}(bound(value, 0, ${opts.max})); // ${opts.name} only supports ${opts.bits}-bit values (0-${opts.max}) + + assertEq(${opts.store}[0].get(index), 0); // initial state + ${opts.store}[0].set(index, value); + assertEq(${opts.store}[0].get(index), value); // after set +} +`; + +const testSymbolicTruncation = opts => `\ +function testSymbolic${opts.name}Truncation(uint256 index, ${opts.type} value) public { + value = ${opts.type}(bound(value, ${1n << opts.bits}, type(${opts.type}).max)); // Test values that need truncation + + ${opts.store}[0].set(index, value); + assertEq(${opts.store}[0].get(index), value & ${opts.max}); // Should be truncated to ${opts.bits} bits +} +`; + +const testSymbolicAssemblyCorruption = opts => `\ +function testSymbolic${opts.name}AssemblyCorruption(uint256 index, ${opts.type} value) public { + uint256 corrupted = _corruptValue(value); + ${opts.type} corruptedValue; + assembly { + corruptedValue := corrupted + } + + ${opts.store}[0].set(index, corruptedValue); + assertEq(${opts.store}[0].get(index), ${opts.type}(corrupted)); // Should match truncated value +} +`; + +const testSymbolicIsolation = opts => `\ +function testSymbolic${opts.name}Isolation(uint256 index1, uint256 index2, ${opts.type} value1, ${opts.type} value2) public { + vm.assume(index1 != index2); + value1 = ${opts.type}(bound(value1, 0, ${opts.max})); + value2 = ${opts.type}(bound(value2, 0, ${opts.max})); + + ${opts.store}[0].set(index1, value1); + ${opts.store}[1].set(index2, value2); + + assertEq(${opts.store}[0].get(index1), value1); + assertEq(${opts.store}[1].get(index2), value2); + assertEq(${opts.store}[0].get(index2), 0); + assertEq(${opts.store}[1].get(index1), 0); +} +`; + +const footer = `\ +function _corruptValue(uint256 value) private pure returns (uint256 corrupted) { + // Simulate assembly corruption by adding high bits + assembly { + corrupted := or(value, shl(200, 0xffffffff)) + } +} +`; + +// GENERATE +module.exports = format( + header, + `contract BitMapsTest is Test {`, + format( + [].concat( + `using BitMaps for *;`, + ``, + bitmapTests, + TYPES.map(opts => Object.assign(opts, { store: `_${opts.name.toLowerCase()}s` })) + .flatMap(opts => [ + `// ========== ${opts.name} Tests ==========`, + ``, + `BitMaps.${opts.name}[2] private ${opts.store};`, + ``, + testSymbolicSetAndGet(opts), + opts.bits < 8 && testSymbolicTruncation(opts), + opts.bits >= 8 && testSymbolicAssemblyCorruption(opts), + testSymbolicIsolation(opts), + ]) + .filter(e => typeof e == 'string'), + footer, + ), + ).trimEnd(), + '}', +); diff --git a/test/utils/structs/BitMap.test.js b/test/utils/structs/BitMap.test.js deleted file mode 100644 index 5662ab13f88..00000000000 --- a/test/utils/structs/BitMap.test.js +++ /dev/null @@ -1,149 +0,0 @@ -const { ethers } = require('hardhat'); -const { expect } = require('chai'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); - -async function fixture() { - const bitmap = await ethers.deployContract('$BitMaps'); - return { bitmap }; -} - -describe('BitMap', function () { - const keyA = 7891n; - const keyB = 451n; - const keyC = 9592328n; - - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - it('starts empty', async function () { - expect(await this.bitmap.$get(0, keyA)).to.be.false; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); - - describe('setTo', function () { - it('set a key to true', async function () { - await this.bitmap.$setTo(0, keyA, true); - expect(await this.bitmap.$get(0, keyA)).to.be.true; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); - - it('set a key to false', async function () { - await this.bitmap.$setTo(0, keyA, true); - await this.bitmap.$setTo(0, keyA, false); - expect(await this.bitmap.$get(0, keyA)).to.be.false; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); - - it('set several consecutive keys', async function () { - await this.bitmap.$setTo(0, keyA + 0n, true); - await this.bitmap.$setTo(0, keyA + 1n, true); - await this.bitmap.$setTo(0, keyA + 2n, true); - await this.bitmap.$setTo(0, keyA + 3n, true); - await this.bitmap.$setTo(0, keyA + 4n, true); - await this.bitmap.$setTo(0, keyA + 2n, false); - await this.bitmap.$setTo(0, keyA + 4n, false); - expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 1n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false; - }); - }); - - describe('set', function () { - it('adds a key', async function () { - await this.bitmap.$set(0, keyA); - expect(await this.bitmap.$get(0, keyA)).to.be.true; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); - - it('adds several keys', async function () { - await this.bitmap.$set(0, keyA); - await this.bitmap.$set(0, keyB); - expect(await this.bitmap.$get(0, keyA)).to.be.true; - expect(await this.bitmap.$get(0, keyB)).to.be.true; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); - - it('adds several consecutive keys', async function () { - await this.bitmap.$set(0, keyA + 0n); - await this.bitmap.$set(0, keyA + 1n); - await this.bitmap.$set(0, keyA + 3n); - expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 1n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false; - }); - }); - - describe('unset', function () { - it('removes added keys', async function () { - await this.bitmap.$set(0, keyA); - await this.bitmap.$set(0, keyB); - await this.bitmap.$unset(0, keyA); - expect(await this.bitmap.$get(0, keyA)).to.be.false; - expect(await this.bitmap.$get(0, keyB)).to.be.true; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); - - it('removes consecutive added keys', async function () { - await this.bitmap.$set(0, keyA + 0n); - await this.bitmap.$set(0, keyA + 1n); - await this.bitmap.$set(0, keyA + 3n); - await this.bitmap.$unset(0, keyA + 1n); - expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 1n)).to.be.false; - expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false; - }); - - it('adds and removes multiple keys', async function () { - // [] - - await this.bitmap.$set(0, keyA); - await this.bitmap.$set(0, keyC); - - // [A, C] - - await this.bitmap.$unset(0, keyA); - await this.bitmap.$unset(0, keyB); - - // [C] - - await this.bitmap.$set(0, keyB); - - // [C, B] - - await this.bitmap.$set(0, keyA); - await this.bitmap.$unset(0, keyC); - - // [A, B] - - await this.bitmap.$set(0, keyA); - await this.bitmap.$set(0, keyB); - - // [A, B] - - await this.bitmap.$set(0, keyC); - await this.bitmap.$unset(0, keyA); - - // [B, C] - - await this.bitmap.$set(0, keyA); - await this.bitmap.$unset(0, keyB); - - // [A, C] - - expect(await this.bitmap.$get(0, keyA)).to.be.true; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.true; - }); - }); -}); diff --git a/test/utils/structs/BitMaps.t.sol b/test/utils/structs/BitMaps.t.sol new file mode 100644 index 00000000000..95053661be5 --- /dev/null +++ b/test/utils/structs/BitMaps.t.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/BitMaps.t.js. + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; + +contract BitMapsTest is Test { + using BitMaps for *; + + // ========== BitMap Tests ========== + + BitMaps.BitMap[2] private _bitmaps; + + function testSymbolicBitMapSetAndGet(uint256 value) public { + assertFalse(_bitmaps[0].get(value)); // initial state + _bitmaps[0].set(value); + assertTrue(_bitmaps[0].get(value)); // after set + } + + function testSymbolicBitMapSetToAndGet(uint256 value) public { + _bitmaps[0].setTo(value, true); + assertTrue(_bitmaps[0].get(value)); // after setTo true + _bitmaps[0].setTo(value, false); + assertFalse(_bitmaps[0].get(value)); // after setTo false + } + + function testSymbolicBitMapIsolation(uint256 value1, uint256 value2) public { + vm.assume(value1 != value2); + + _bitmaps[0].set(value1); + _bitmaps[1].set(value2); + + assertTrue(_bitmaps[0].get(value1)); + assertTrue(_bitmaps[1].get(value2)); + assertFalse(_bitmaps[0].get(value2)); + assertFalse(_bitmaps[1].get(value1)); + } + + // ========== PairMap Tests ========== + + BitMaps.PairMap[2] private _pairmaps; + + function testSymbolicPairMapSetAndGet(uint256 index, uint8 value) public { + value = uint8(bound(value, 0, 0x03)); // PairMap only supports 2-bit values (0-0x03) + + assertEq(_pairmaps[0].get(index), 0); // initial state + _pairmaps[0].set(index, value); + assertEq(_pairmaps[0].get(index), value); // after set + } + + function testSymbolicPairMapTruncation(uint256 index, uint8 value) public { + value = uint8(bound(value, 4, type(uint8).max)); // Test values that need truncation + + _pairmaps[0].set(index, value); + assertEq(_pairmaps[0].get(index), value & 0x03); // Should be truncated to 2 bits + } + + function testSymbolicPairMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + vm.assume(index1 != index2); + value1 = uint8(bound(value1, 0, 0x03)); + value2 = uint8(bound(value2, 0, 0x03)); + + _pairmaps[0].set(index1, value1); + _pairmaps[1].set(index2, value2); + + assertEq(_pairmaps[0].get(index1), value1); + assertEq(_pairmaps[1].get(index2), value2); + assertEq(_pairmaps[0].get(index2), 0); + assertEq(_pairmaps[1].get(index1), 0); + } + + // ========== NibbleMap Tests ========== + + BitMaps.NibbleMap[2] private _nibblemaps; + + function testSymbolicNibbleMapSetAndGet(uint256 index, uint8 value) public { + value = uint8(bound(value, 0, 0x0f)); // NibbleMap only supports 4-bit values (0-0x0f) + + assertEq(_nibblemaps[0].get(index), 0); // initial state + _nibblemaps[0].set(index, value); + assertEq(_nibblemaps[0].get(index), value); // after set + } + + function testSymbolicNibbleMapTruncation(uint256 index, uint8 value) public { + value = uint8(bound(value, 16, type(uint8).max)); // Test values that need truncation + + _nibblemaps[0].set(index, value); + assertEq(_nibblemaps[0].get(index), value & 0x0f); // Should be truncated to 4 bits + } + + function testSymbolicNibbleMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + vm.assume(index1 != index2); + value1 = uint8(bound(value1, 0, 0x0f)); + value2 = uint8(bound(value2, 0, 0x0f)); + + _nibblemaps[0].set(index1, value1); + _nibblemaps[1].set(index2, value2); + + assertEq(_nibblemaps[0].get(index1), value1); + assertEq(_nibblemaps[1].get(index2), value2); + assertEq(_nibblemaps[0].get(index2), 0); + assertEq(_nibblemaps[1].get(index1), 0); + } + + // ========== Uint8Map Tests ========== + + BitMaps.Uint8Map[2] private _uint8maps; + + function testSymbolicUint8MapSetAndGet(uint256 index, uint8 value) public { + value = uint8(bound(value, 0, 0xff)); // Uint8Map only supports 8-bit values (0-0xff) + + assertEq(_uint8maps[0].get(index), 0); // initial state + _uint8maps[0].set(index, value); + assertEq(_uint8maps[0].get(index), value); // after set + } + + function testSymbolicUint8MapAssemblyCorruption(uint256 index, uint8 value) public { + uint256 corrupted = _corruptValue(value); + uint8 corruptedValue; + assembly { + corruptedValue := corrupted + } + + _uint8maps[0].set(index, corruptedValue); + assertEq(_uint8maps[0].get(index), uint8(corrupted)); // Should match truncated value + } + + function testSymbolicUint8MapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + vm.assume(index1 != index2); + value1 = uint8(bound(value1, 0, 0xff)); + value2 = uint8(bound(value2, 0, 0xff)); + + _uint8maps[0].set(index1, value1); + _uint8maps[1].set(index2, value2); + + assertEq(_uint8maps[0].get(index1), value1); + assertEq(_uint8maps[1].get(index2), value2); + assertEq(_uint8maps[0].get(index2), 0); + assertEq(_uint8maps[1].get(index1), 0); + } + + // ========== Uint16Map Tests ========== + + BitMaps.Uint16Map[2] private _uint16maps; + + function testSymbolicUint16MapSetAndGet(uint256 index, uint16 value) public { + value = uint16(bound(value, 0, 0xffff)); // Uint16Map only supports 16-bit values (0-0xffff) + + assertEq(_uint16maps[0].get(index), 0); // initial state + _uint16maps[0].set(index, value); + assertEq(_uint16maps[0].get(index), value); // after set + } + + function testSymbolicUint16MapAssemblyCorruption(uint256 index, uint16 value) public { + uint256 corrupted = _corruptValue(value); + uint16 corruptedValue; + assembly { + corruptedValue := corrupted + } + + _uint16maps[0].set(index, corruptedValue); + assertEq(_uint16maps[0].get(index), uint16(corrupted)); // Should match truncated value + } + + function testSymbolicUint16MapIsolation(uint256 index1, uint256 index2, uint16 value1, uint16 value2) public { + vm.assume(index1 != index2); + value1 = uint16(bound(value1, 0, 0xffff)); + value2 = uint16(bound(value2, 0, 0xffff)); + + _uint16maps[0].set(index1, value1); + _uint16maps[1].set(index2, value2); + + assertEq(_uint16maps[0].get(index1), value1); + assertEq(_uint16maps[1].get(index2), value2); + assertEq(_uint16maps[0].get(index2), 0); + assertEq(_uint16maps[1].get(index1), 0); + } + + // ========== Uint32Map Tests ========== + + BitMaps.Uint32Map[2] private _uint32maps; + + function testSymbolicUint32MapSetAndGet(uint256 index, uint32 value) public { + value = uint32(bound(value, 0, 0xffffffff)); // Uint32Map only supports 32-bit values (0-0xffffffff) + + assertEq(_uint32maps[0].get(index), 0); // initial state + _uint32maps[0].set(index, value); + assertEq(_uint32maps[0].get(index), value); // after set + } + + function testSymbolicUint32MapAssemblyCorruption(uint256 index, uint32 value) public { + uint256 corrupted = _corruptValue(value); + uint32 corruptedValue; + assembly { + corruptedValue := corrupted + } + + _uint32maps[0].set(index, corruptedValue); + assertEq(_uint32maps[0].get(index), uint32(corrupted)); // Should match truncated value + } + + function testSymbolicUint32MapIsolation(uint256 index1, uint256 index2, uint32 value1, uint32 value2) public { + vm.assume(index1 != index2); + value1 = uint32(bound(value1, 0, 0xffffffff)); + value2 = uint32(bound(value2, 0, 0xffffffff)); + + _uint32maps[0].set(index1, value1); + _uint32maps[1].set(index2, value2); + + assertEq(_uint32maps[0].get(index1), value1); + assertEq(_uint32maps[1].get(index2), value2); + assertEq(_uint32maps[0].get(index2), 0); + assertEq(_uint32maps[1].get(index1), 0); + } + + // ========== Uint64Map Tests ========== + + BitMaps.Uint64Map[2] private _uint64maps; + + function testSymbolicUint64MapSetAndGet(uint256 index, uint64 value) public { + value = uint64(bound(value, 0, 0xffffffffffffffff)); // Uint64Map only supports 64-bit values (0-0xffffffffffffffff) + + assertEq(_uint64maps[0].get(index), 0); // initial state + _uint64maps[0].set(index, value); + assertEq(_uint64maps[0].get(index), value); // after set + } + + function testSymbolicUint64MapAssemblyCorruption(uint256 index, uint64 value) public { + uint256 corrupted = _corruptValue(value); + uint64 corruptedValue; + assembly { + corruptedValue := corrupted + } + + _uint64maps[0].set(index, corruptedValue); + assertEq(_uint64maps[0].get(index), uint64(corrupted)); // Should match truncated value + } + + function testSymbolicUint64MapIsolation(uint256 index1, uint256 index2, uint64 value1, uint64 value2) public { + vm.assume(index1 != index2); + value1 = uint64(bound(value1, 0, 0xffffffffffffffff)); + value2 = uint64(bound(value2, 0, 0xffffffffffffffff)); + + _uint64maps[0].set(index1, value1); + _uint64maps[1].set(index2, value2); + + assertEq(_uint64maps[0].get(index1), value1); + assertEq(_uint64maps[1].get(index2), value2); + assertEq(_uint64maps[0].get(index2), 0); + assertEq(_uint64maps[1].get(index1), 0); + } + + // ========== Uint128Map Tests ========== + + BitMaps.Uint128Map[2] private _uint128maps; + + function testSymbolicUint128MapSetAndGet(uint256 index, uint128 value) public { + value = uint128(bound(value, 0, 0xffffffffffffffffffffffffffffffff)); // Uint128Map only supports 128-bit values (0-0xffffffffffffffffffffffffffffffff) + + assertEq(_uint128maps[0].get(index), 0); // initial state + _uint128maps[0].set(index, value); + assertEq(_uint128maps[0].get(index), value); // after set + } + + function testSymbolicUint128MapAssemblyCorruption(uint256 index, uint128 value) public { + uint256 corrupted = _corruptValue(value); + uint128 corruptedValue; + assembly { + corruptedValue := corrupted + } + + _uint128maps[0].set(index, corruptedValue); + assertEq(_uint128maps[0].get(index), uint128(corrupted)); // Should match truncated value + } + + function testSymbolicUint128MapIsolation(uint256 index1, uint256 index2, uint128 value1, uint128 value2) public { + vm.assume(index1 != index2); + value1 = uint128(bound(value1, 0, 0xffffffffffffffffffffffffffffffff)); + value2 = uint128(bound(value2, 0, 0xffffffffffffffffffffffffffffffff)); + + _uint128maps[0].set(index1, value1); + _uint128maps[1].set(index2, value2); + + assertEq(_uint128maps[0].get(index1), value1); + assertEq(_uint128maps[1].get(index2), value2); + assertEq(_uint128maps[0].get(index2), 0); + assertEq(_uint128maps[1].get(index1), 0); + } + + function _corruptValue(uint256 value) private pure returns (uint256 corrupted) { + // Simulate assembly corruption by adding high bits + assembly { + corrupted := or(value, shl(200, 0xffffffff)) + } + } +} diff --git a/test/utils/structs/BitMaps.test.js b/test/utils/structs/BitMaps.test.js new file mode 100644 index 00000000000..88fdf35aa77 --- /dev/null +++ b/test/utils/structs/BitMaps.test.js @@ -0,0 +1,313 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const bitmap = await ethers.deployContract('$BitMaps'); + return { bitmap }; +} + +describe('BitMaps', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('BitMap', function () { + const keyA = 7891n; + const keyB = 451n; + const keyC = 9592328n; + + it('starts empty', async function () { + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; + }); + + describe('setTo', function () { + it('set a key to true', async function () { + await this.bitmap.$setTo(0, keyA, true); + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; + }); + + it('set a key to false', async function () { + await this.bitmap.$setTo(0, keyA, true); + await this.bitmap.$setTo(0, keyA, false); + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; + }); + + it('set several consecutive keys', async function () { + await this.bitmap.$setTo(0, keyA + 0n, true); + await this.bitmap.$setTo(0, keyA + 1n, true); + await this.bitmap.$setTo(0, keyA + 2n, true); + await this.bitmap.$setTo(0, keyA + 3n, true); + await this.bitmap.$setTo(0, keyA + 4n, true); + await this.bitmap.$setTo(0, keyA + 2n, false); + await this.bitmap.$setTo(0, keyA + 4n, false); + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.eventually.be.false; + }); + }); + + describe('set', function () { + it('adds a key', async function () { + await this.bitmap.$set(0, keyA); + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; + }); + + it('adds several keys', async function () { + await this.bitmap.$set(0, keyA); + await this.bitmap.$set(0, keyB); + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; + }); + + it('adds several consecutive keys', async function () { + await this.bitmap.$set(0, keyA + 0n); + await this.bitmap.$set(0, keyA + 1n); + await this.bitmap.$set(0, keyA + 3n); + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.eventually.be.false; + }); + }); + + describe('unset', function () { + it('removes added keys', async function () { + await this.bitmap.$set(0, keyA); + await this.bitmap.$set(0, keyB); + await this.bitmap.$unset(0, keyA); + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; + }); + + it('removes consecutive added keys', async function () { + await this.bitmap.$set(0, keyA + 0n); + await this.bitmap.$set(0, keyA + 1n); + await this.bitmap.$set(0, keyA + 3n); + await this.bitmap.$unset(0, keyA + 1n); + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.eventually.be.false; + }); + + it('adds and removes multiple keys', async function () { + // [] + + await this.bitmap.$set(0, keyA); + await this.bitmap.$set(0, keyC); + + // [A, C] + + await this.bitmap.$unset(0, keyA); + await this.bitmap.$unset(0, keyB); + + // [C] + + await this.bitmap.$set(0, keyB); + + // [C, B] + + await this.bitmap.$set(0, keyA); + await this.bitmap.$unset(0, keyC); + + // [A, B] + + await this.bitmap.$set(0, keyA); + await this.bitmap.$set(0, keyB); + + // [A, B] + + await this.bitmap.$set(0, keyC); + await this.bitmap.$unset(0, keyA); + + // [B, C] + + await this.bitmap.$set(0, keyA); + await this.bitmap.$unset(0, keyB); + + // [A, C] + + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.true; + }); + }); + }); + + describe('PairMap', function () { + it('stores and retrieves 2-bit values', async function () { + await this.bitmap.$set_BitMaps_PairMap(1, 0n, 0); + await this.bitmap.$set_BitMaps_PairMap(1, 1n, 1); + await this.bitmap.$set_BitMaps_PairMap(1, 2n, 2); + await this.bitmap.$set_BitMaps_PairMap(1, 3n, 3); + + await expect(this.bitmap.$get_BitMaps_PairMap(1, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 1n)).to.eventually.equal(1); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 2n)).to.eventually.equal(2); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 3n)).to.eventually.equal(3); + }); + + it('truncates values larger than 3', async function () { + await this.bitmap.$set_BitMaps_PairMap(1, 0n, 4); // Should become 0 + await this.bitmap.$set_BitMaps_PairMap(1, 1n, 5); // Should become 1 + await this.bitmap.$set_BitMaps_PairMap(1, 2n, 6); // Should become 2 + await this.bitmap.$set_BitMaps_PairMap(1, 3n, 7); // Should become 3 + + await expect(this.bitmap.$get_BitMaps_PairMap(1, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 1n)).to.eventually.equal(1); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 2n)).to.eventually.equal(2); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 3n)).to.eventually.equal(3); + }); + + it('handles multiple buckets', async function () { + // Test across bucket boundary (128 values per bucket) + await this.bitmap.$set_BitMaps_PairMap(1, 127n, 2); + await this.bitmap.$set_BitMaps_PairMap(1, 128n, 3); + + await expect(this.bitmap.$get_BitMaps_PairMap(1, 127n)).to.eventually.equal(2); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 128n)).to.eventually.equal(3); + }); + }); + + describe('NibbleMap', function () { + it('stores and retrieves 4-bit values', async function () { + await this.bitmap.$set_BitMaps_NibbleMap(2, 0n, 0); + await this.bitmap.$set_BitMaps_NibbleMap(2, 1n, 5); + await this.bitmap.$set_BitMaps_NibbleMap(2, 2n, 10); + await this.bitmap.$set_BitMaps_NibbleMap(2, 3n, 15); + + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 1n)).to.eventually.equal(5); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 2n)).to.eventually.equal(10); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 3n)).to.eventually.equal(15); + }); + + it('truncates values larger than 15', async function () { + await this.bitmap.$set_BitMaps_NibbleMap(2, 0n, 16); // Should become 0 + await this.bitmap.$set_BitMaps_NibbleMap(2, 1n, 17); // Should become 1 + await this.bitmap.$set_BitMaps_NibbleMap(2, 2n, 30); // Should become 14 + await this.bitmap.$set_BitMaps_NibbleMap(2, 3n, 31); // Should become 15 + + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 1n)).to.eventually.equal(1); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 2n)).to.eventually.equal(14); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 3n)).to.eventually.equal(15); + }); + }); + + describe('Uint8Map', function () { + it('stores and retrieves uint8 values', async function () { + await this.bitmap.$set_BitMaps_Uint8Map(3, 0n, ethers.Typed.uint8(0)); + await this.bitmap.$set_BitMaps_Uint8Map(3, 1n, ethers.Typed.uint8(42)); + await this.bitmap.$set_BitMaps_Uint8Map(3, 2n, ethers.Typed.uint8(255)); + + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 1n)).to.eventually.equal(42); + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 2n)).to.eventually.equal(255); + }); + + it('handles bucket boundaries', async function () { + // 32 values per bucket for Uint8Map + await this.bitmap.$set_BitMaps_Uint8Map(3, 31n, ethers.Typed.uint8(100)); + await this.bitmap.$set_BitMaps_Uint8Map(3, 32n, ethers.Typed.uint8(200)); + + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 31n)).to.eventually.equal(100); + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 32n)).to.eventually.equal(200); + }); + }); + + describe('Uint16Map', function () { + it('stores and retrieves uint16 values', async function () { + await this.bitmap.$set(4, 0n, ethers.Typed.uint16(0)); + await this.bitmap.$set(4, 1n, ethers.Typed.uint16(1000)); + await this.bitmap.$set(4, 2n, ethers.Typed.uint16(65535)); + + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 1n)).to.eventually.equal(1000); + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 2n)).to.eventually.equal(65535); + }); + + it('handles bucket boundaries', async function () { + // 16 values per bucket for Uint16Map + await this.bitmap.$set(4, 15n, ethers.Typed.uint16(100)); + await this.bitmap.$set(4, 16n, ethers.Typed.uint16(200)); + + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 15n)).to.eventually.equal(100); + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 16n)).to.eventually.equal(200); + }); + }); + + describe('Uint32Map', function () { + it('stores and retrieves uint32 values', async function () { + await this.bitmap.$set(5, 0n, ethers.Typed.uint32(0)); + await this.bitmap.$set(5, 1n, ethers.Typed.uint32(1000000)); + await this.bitmap.$set(5, 2n, ethers.Typed.uint32(4294967295)); // 2^32 - 1 + + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 1n)).to.eventually.equal(1000000); + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 2n)).to.eventually.equal(4294967295); + }); + + it('handles bucket boundaries', async function () { + // 32 values per bucket for Uint32Map + await this.bitmap.$set(5, 31n, ethers.Typed.uint32(100)); + await this.bitmap.$set(5, 32n, ethers.Typed.uint32(200)); + + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 31n)).to.eventually.equal(100); + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 32n)).to.eventually.equal(200); + }); + }); + + describe('Uint64Map', function () { + it('stores and retrieves uint64 values', async function () { + const maxUint64 = (1n << 64n) - 1n; + + await this.bitmap.$set(6, 0n, ethers.Typed.uint64(0)); + await this.bitmap.$set(6, 1n, ethers.Typed.uint64(1000000000)); + await this.bitmap.$set(6, 2n, ethers.Typed.uint64(maxUint64)); + + await expect(this.bitmap.$get_BitMaps_Uint64Map(6, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint64Map(6, 1n)).to.eventually.equal(1000000000); + await expect(this.bitmap.$get_BitMaps_Uint64Map(6, 2n)).to.eventually.equal(maxUint64); + }); + }); + + describe('Uint128Map', function () { + it('stores and retrieves uint128 values', async function () { + const maxUint128 = (1n << 128n) - 1n; + const largeValue = 1n << 100n; + + await this.bitmap.$set(7, 0n, ethers.Typed.uint128(0)); + await this.bitmap.$set(7, 1n, ethers.Typed.uint128(largeValue)); + await this.bitmap.$set(7, 2n, ethers.Typed.uint128(maxUint128)); + + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 1n)).to.eventually.equal(largeValue); + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 2n)).to.eventually.equal(maxUint128); + }); + + it('handles bucket boundaries', async function () { + // Uint128Map has 2 values per bucket + await this.bitmap.$set(7, 31n, ethers.Typed.uint128(100)); + await this.bitmap.$set(7, 32n, ethers.Typed.uint128(200)); + + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 31n)).to.eventually.equal(100); + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 32n)).to.eventually.equal(200); + }); + }); +});