-
Notifications
You must be signed in to change notification settings - Fork 11.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CircularBuffer data structure (#4913)
Co-authored-by: ernestognw <ernestognw@gmail.com>
- Loading branch information
1 parent
60697cb
commit c80b675
Showing
7 changed files
with
223 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'openzeppelin-solidity': minor | ||
--- | ||
|
||
`CircularBuffer`: Add a data structure that stores the last `N` values pushed to it. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.20; | ||
|
||
import {Math} from "../math/Math.sol"; | ||
import {Arrays} from "../Arrays.sol"; | ||
import {Panic} from "../Panic.sol"; | ||
|
||
/** | ||
* @dev A fixed-size buffer for keeping `bytes32` items in storage. | ||
* | ||
* This data structure allows for pushing elements to it, and when its length exceeds the specified fixed size, | ||
* new items take the place of the oldest element in the buffer, keeping at most `N` elements in the | ||
* structure. | ||
* | ||
* Elements can't be removed but the data structure can be cleared. See {clear}. | ||
* | ||
* Complexity: | ||
* - insertion ({push}): O(1) | ||
* - lookup ({last}): O(1) | ||
* - inclusion ({includes}): O(N) (worst case) | ||
* - reset ({clear}): O(1) | ||
* | ||
* * The struct is called `Bytes32CircularBuffer`. Other types can be cast to and from `bytes32`. This data structure | ||
* can only be used in storage, and not in memory. | ||
* | ||
* Example usage: | ||
* | ||
* ```solidity | ||
* contract Example { | ||
* // Add the library methods | ||
* using CircularBuffer for CircularBuffer.Bytes32CircularBuffer; | ||
* | ||
* // Declare a buffer storage variable | ||
* CircularBuffer.Bytes32CircularBuffer private myBuffer; | ||
* } | ||
* ``` | ||
*/ | ||
library CircularBuffer { | ||
/** | ||
* @dev Counts the number of items that have been pushed to the buffer. The residuo modulo _data.length indicates | ||
* where the next value should be stored. | ||
* | ||
* Struct members have an underscore prefix indicating that they are "private" and should not be read or written to | ||
* directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and | ||
* lead to unexpected behavior. | ||
* | ||
* The last item is at data[(index - 1) % data.length] and the last item is at data[index % data.length]. This | ||
* range can wrap around. | ||
*/ | ||
struct Bytes32CircularBuffer { | ||
uint256 _count; | ||
bytes32[] _data; | ||
} | ||
|
||
/** | ||
* @dev Initialize a new CircularBuffer of given size. | ||
* | ||
* If the CircularBuffer was already setup and used, calling that function again will reset it to a blank state. | ||
* | ||
* NOTE: The size of the buffer will affect the execution of {includes} function, as it has a complexity of O(N). | ||
* Consider a large buffer size may render the function unusable. | ||
*/ | ||
function setup(Bytes32CircularBuffer storage self, uint256 size) internal { | ||
clear(self); | ||
Arrays.unsafeSetLength(self._data, size); | ||
} | ||
|
||
/** | ||
* @dev Clear all data in the buffer without resetting memory, keeping the existing size. | ||
*/ | ||
function clear(Bytes32CircularBuffer storage self) internal { | ||
self._count = 0; | ||
} | ||
|
||
/** | ||
* @dev Push a new value to the buffer. If the buffer is already full, the new value replaces the oldest value in | ||
* the buffer. | ||
*/ | ||
function push(Bytes32CircularBuffer storage self, bytes32 value) internal { | ||
uint256 index = self._count++; | ||
uint256 modulus = self._data.length; | ||
Arrays.unsafeAccess(self._data, index % modulus).value = value; | ||
} | ||
|
||
/** | ||
* @dev Number of values currently in the buffer. This value is 0 for an empty buffer, and cannot exceed the size of | ||
* the buffer. | ||
*/ | ||
function count(Bytes32CircularBuffer storage self) internal view returns (uint256) { | ||
return Math.min(self._count, self._data.length); | ||
} | ||
|
||
/** | ||
* @dev Length of the buffer. This is the maximum number of elements kepts in the buffer. | ||
*/ | ||
function length(Bytes32CircularBuffer storage self) internal view returns (uint256) { | ||
return self._data.length; | ||
} | ||
|
||
/** | ||
* @dev Getter for the i-th value in the buffer, from the end. | ||
* | ||
* Reverts with {Panic-ARRAY_OUT_OF_BOUNDS} if trying to access an element that was not pushed, or that was | ||
* dropped to make room for newer elements. | ||
*/ | ||
function last(Bytes32CircularBuffer storage self, uint256 i) internal view returns (bytes32) { | ||
uint256 index = self._count; | ||
uint256 modulus = self._data.length; | ||
uint256 total = Math.min(index, modulus); // count(self) | ||
if (i >= total) { | ||
Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); | ||
} | ||
return Arrays.unsafeAccess(self._data, (index - i - 1) % modulus).value; | ||
} | ||
|
||
/** | ||
* @dev Check if a given value is in the buffer. | ||
*/ | ||
function includes(Bytes32CircularBuffer storage self, bytes32 value) internal view returns (bool) { | ||
uint256 index = self._count; | ||
uint256 modulus = self._data.length; | ||
uint256 total = Math.min(index, modulus); // count(self) | ||
for (uint256 i = 0; i < total; ++i) { | ||
if (Arrays.unsafeAccess(self._data, (index - i - 1) % modulus).value == value) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
const { ethers } = require('hardhat'); | ||
const { expect } = require('chai'); | ||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); | ||
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); | ||
|
||
const { generators } = require('../../helpers/random'); | ||
|
||
const LENGTH = 4; | ||
|
||
async function fixture() { | ||
const mock = await ethers.deployContract('$CircularBuffer'); | ||
await mock.$setup(0, LENGTH); | ||
return { mock }; | ||
} | ||
|
||
describe('CircularBuffer', function () { | ||
beforeEach(async function () { | ||
Object.assign(this, await loadFixture(fixture)); | ||
}); | ||
|
||
it('starts empty', async function () { | ||
expect(await this.mock.$count(0)).to.equal(0n); | ||
expect(await this.mock.$length(0)).to.equal(LENGTH); | ||
expect(await this.mock.$includes(0, ethers.ZeroHash)).to.be.false; | ||
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); | ||
}); | ||
|
||
it('push', async function () { | ||
const values = Array.from({ length: LENGTH + 3 }, generators.bytes32); | ||
|
||
for (const [i, value] of values.map((v, i) => [i, v])) { | ||
// push value | ||
await this.mock.$push(0, value); | ||
|
||
// view of the values | ||
const pushed = values.slice(0, i + 1); | ||
const stored = pushed.slice(-LENGTH); | ||
const dropped = pushed.slice(0, -LENGTH); | ||
|
||
// check count | ||
expect(await this.mock.$length(0)).to.equal(LENGTH); | ||
expect(await this.mock.$count(0)).to.equal(stored.length); | ||
|
||
// check last | ||
for (const j in stored) { | ||
expect(await this.mock.$last(0, j)).to.equal(stored.at(-j - 1)); | ||
} | ||
await expect(this.mock.$last(0, stored.length + 1)).to.be.revertedWithPanic( | ||
PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS, | ||
); | ||
|
||
// check included and non-included values | ||
for (const v of stored) { | ||
expect(await this.mock.$includes(0, v)).to.be.true; | ||
} | ||
for (const v of dropped) { | ||
expect(await this.mock.$includes(0, v)).to.be.false; | ||
} | ||
expect(await this.mock.$includes(0, ethers.ZeroHash)).to.be.false; | ||
} | ||
}); | ||
|
||
it('clear', async function () { | ||
const value = generators.bytes32(); | ||
await this.mock.$push(0, value); | ||
|
||
expect(await this.mock.$count(0)).to.equal(1n); | ||
expect(await this.mock.$length(0)).to.equal(LENGTH); | ||
expect(await this.mock.$includes(0, value)).to.be.true; | ||
await this.mock.$last(0, 0); // not revert | ||
|
||
await this.mock.$clear(0); | ||
|
||
expect(await this.mock.$count(0)).to.equal(0n); | ||
expect(await this.mock.$length(0)).to.equal(LENGTH); | ||
expect(await this.mock.$includes(0, value)).to.be.false; | ||
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); | ||
}); | ||
}); |