Skip to content

Commit c80b675

Browse files
Amxxernestognw
andauthored
Add CircularBuffer data structure (#4913)
Co-authored-by: ernestognw <ernestognw@gmail.com>
1 parent 60697cb commit c80b675

File tree

7 files changed

+223
-0
lines changed

7 files changed

+223
-0
lines changed

.changeset/cold-cheetahs-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`CircularBuffer`: Add a data structure that stores the last `N` values pushed to it.

contracts/mocks/Stateless.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {AuthorityUtils} from "../access/manager/AuthorityUtils.sol";
1010
import {Base64} from "../utils/Base64.sol";
1111
import {BitMaps} from "../utils/structs/BitMaps.sol";
1212
import {Checkpoints} from "../utils/structs/Checkpoints.sol";
13+
import {CircularBuffer} from "../utils/structs/CircularBuffer.sol";
1314
import {Clones} from "../proxy/Clones.sol";
1415
import {Create2} from "../utils/Create2.sol";
1516
import {DoubleEndedQueue} from "../utils/structs/DoubleEndedQueue.sol";
@@ -24,6 +25,7 @@ import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol";
2425
import {Math} from "../utils/math/Math.sol";
2526
import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
2627
import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol";
28+
import {Panic} from "../utils/Panic.sol";
2729
import {Packing} from "../utils/Packing.sol";
2830
import {SafeCast} from "../utils/math/SafeCast.sol";
2931
import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol";

contracts/utils/Arrays.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ library Arrays {
455455
* WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
456456
*/
457457
function unsafeSetLength(address[] storage array, uint256 len) internal {
458+
/// @solidity memory-safe-assembly
458459
assembly {
459460
sstore(array.slot, len)
460461
}
@@ -466,6 +467,7 @@ library Arrays {
466467
* WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
467468
*/
468469
function unsafeSetLength(bytes32[] storage array, uint256 len) internal {
470+
/// @solidity memory-safe-assembly
469471
assembly {
470472
sstore(array.slot, len)
471473
}
@@ -477,6 +479,7 @@ library Arrays {
477479
* WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
478480
*/
479481
function unsafeSetLength(uint256[] storage array, uint256 len) internal {
482+
/// @solidity memory-safe-assembly
480483
assembly {
481484
sstore(array.slot, len)
482485
}

contracts/utils/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
2121
* {EnumerableMap}: A type like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`], but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`).
2222
* {EnumerableSet}: Like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc.
2323
* {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be removed added or remove from both sides. Useful for FIFO and LIFO structures.
24+
* {CircularBuffer}: A data structure to store the last N values pushed to it.
2425
* {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time.
2526
* {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions.
2627
* {Create2}: Wrapper around the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode] for safe use without having to deal with low-level assembly.
@@ -95,6 +96,8 @@ Ethereum contracts have no native concept of an interface, so applications must
9596

9697
{{DoubleEndedQueue}}
9798

99+
{{CircularBuffer}}
100+
98101
{{Checkpoints}}
99102

100103
{{MerkleTree}}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Math} from "../math/Math.sol";
5+
import {Arrays} from "../Arrays.sol";
6+
import {Panic} from "../Panic.sol";
7+
8+
/**
9+
* @dev A fixed-size buffer for keeping `bytes32` items in storage.
10+
*
11+
* This data structure allows for pushing elements to it, and when its length exceeds the specified fixed size,
12+
* new items take the place of the oldest element in the buffer, keeping at most `N` elements in the
13+
* structure.
14+
*
15+
* Elements can't be removed but the data structure can be cleared. See {clear}.
16+
*
17+
* Complexity:
18+
* - insertion ({push}): O(1)
19+
* - lookup ({last}): O(1)
20+
* - inclusion ({includes}): O(N) (worst case)
21+
* - reset ({clear}): O(1)
22+
*
23+
* * The struct is called `Bytes32CircularBuffer`. Other types can be cast to and from `bytes32`. This data structure
24+
* can only be used in storage, and not in memory.
25+
*
26+
* Example usage:
27+
*
28+
* ```solidity
29+
* contract Example {
30+
* // Add the library methods
31+
* using CircularBuffer for CircularBuffer.Bytes32CircularBuffer;
32+
*
33+
* // Declare a buffer storage variable
34+
* CircularBuffer.Bytes32CircularBuffer private myBuffer;
35+
* }
36+
* ```
37+
*/
38+
library CircularBuffer {
39+
/**
40+
* @dev Counts the number of items that have been pushed to the buffer. The residuo modulo _data.length indicates
41+
* where the next value should be stored.
42+
*
43+
* Struct members have an underscore prefix indicating that they are "private" and should not be read or written to
44+
* directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and
45+
* lead to unexpected behavior.
46+
*
47+
* The last item is at data[(index - 1) % data.length] and the last item is at data[index % data.length]. This
48+
* range can wrap around.
49+
*/
50+
struct Bytes32CircularBuffer {
51+
uint256 _count;
52+
bytes32[] _data;
53+
}
54+
55+
/**
56+
* @dev Initialize a new CircularBuffer of given size.
57+
*
58+
* If the CircularBuffer was already setup and used, calling that function again will reset it to a blank state.
59+
*
60+
* NOTE: The size of the buffer will affect the execution of {includes} function, as it has a complexity of O(N).
61+
* Consider a large buffer size may render the function unusable.
62+
*/
63+
function setup(Bytes32CircularBuffer storage self, uint256 size) internal {
64+
clear(self);
65+
Arrays.unsafeSetLength(self._data, size);
66+
}
67+
68+
/**
69+
* @dev Clear all data in the buffer without resetting memory, keeping the existing size.
70+
*/
71+
function clear(Bytes32CircularBuffer storage self) internal {
72+
self._count = 0;
73+
}
74+
75+
/**
76+
* @dev Push a new value to the buffer. If the buffer is already full, the new value replaces the oldest value in
77+
* the buffer.
78+
*/
79+
function push(Bytes32CircularBuffer storage self, bytes32 value) internal {
80+
uint256 index = self._count++;
81+
uint256 modulus = self._data.length;
82+
Arrays.unsafeAccess(self._data, index % modulus).value = value;
83+
}
84+
85+
/**
86+
* @dev Number of values currently in the buffer. This value is 0 for an empty buffer, and cannot exceed the size of
87+
* the buffer.
88+
*/
89+
function count(Bytes32CircularBuffer storage self) internal view returns (uint256) {
90+
return Math.min(self._count, self._data.length);
91+
}
92+
93+
/**
94+
* @dev Length of the buffer. This is the maximum number of elements kepts in the buffer.
95+
*/
96+
function length(Bytes32CircularBuffer storage self) internal view returns (uint256) {
97+
return self._data.length;
98+
}
99+
100+
/**
101+
* @dev Getter for the i-th value in the buffer, from the end.
102+
*
103+
* Reverts with {Panic-ARRAY_OUT_OF_BOUNDS} if trying to access an element that was not pushed, or that was
104+
* dropped to make room for newer elements.
105+
*/
106+
function last(Bytes32CircularBuffer storage self, uint256 i) internal view returns (bytes32) {
107+
uint256 index = self._count;
108+
uint256 modulus = self._data.length;
109+
uint256 total = Math.min(index, modulus); // count(self)
110+
if (i >= total) {
111+
Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
112+
}
113+
return Arrays.unsafeAccess(self._data, (index - i - 1) % modulus).value;
114+
}
115+
116+
/**
117+
* @dev Check if a given value is in the buffer.
118+
*/
119+
function includes(Bytes32CircularBuffer storage self, bytes32 value) internal view returns (bool) {
120+
uint256 index = self._count;
121+
uint256 modulus = self._data.length;
122+
uint256 total = Math.min(index, modulus); // count(self)
123+
for (uint256 i = 0; i < total; ++i) {
124+
if (Arrays.unsafeAccess(self._data, (index - i - 1) % modulus).value == value) {
125+
return true;
126+
}
127+
}
128+
return false;
129+
}
130+
}

scripts/generate/templates/Arrays.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ const unsafeSetLength = type => `
356356
* WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
357357
*/
358358
function unsafeSetLength(${type}[] storage array, uint256 len) internal {
359+
/// @solidity memory-safe-assembly
359360
assembly {
360361
sstore(array.slot, len)
361362
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
5+
6+
const { generators } = require('../../helpers/random');
7+
8+
const LENGTH = 4;
9+
10+
async function fixture() {
11+
const mock = await ethers.deployContract('$CircularBuffer');
12+
await mock.$setup(0, LENGTH);
13+
return { mock };
14+
}
15+
16+
describe('CircularBuffer', function () {
17+
beforeEach(async function () {
18+
Object.assign(this, await loadFixture(fixture));
19+
});
20+
21+
it('starts empty', async function () {
22+
expect(await this.mock.$count(0)).to.equal(0n);
23+
expect(await this.mock.$length(0)).to.equal(LENGTH);
24+
expect(await this.mock.$includes(0, ethers.ZeroHash)).to.be.false;
25+
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
26+
});
27+
28+
it('push', async function () {
29+
const values = Array.from({ length: LENGTH + 3 }, generators.bytes32);
30+
31+
for (const [i, value] of values.map((v, i) => [i, v])) {
32+
// push value
33+
await this.mock.$push(0, value);
34+
35+
// view of the values
36+
const pushed = values.slice(0, i + 1);
37+
const stored = pushed.slice(-LENGTH);
38+
const dropped = pushed.slice(0, -LENGTH);
39+
40+
// check count
41+
expect(await this.mock.$length(0)).to.equal(LENGTH);
42+
expect(await this.mock.$count(0)).to.equal(stored.length);
43+
44+
// check last
45+
for (const j in stored) {
46+
expect(await this.mock.$last(0, j)).to.equal(stored.at(-j - 1));
47+
}
48+
await expect(this.mock.$last(0, stored.length + 1)).to.be.revertedWithPanic(
49+
PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS,
50+
);
51+
52+
// check included and non-included values
53+
for (const v of stored) {
54+
expect(await this.mock.$includes(0, v)).to.be.true;
55+
}
56+
for (const v of dropped) {
57+
expect(await this.mock.$includes(0, v)).to.be.false;
58+
}
59+
expect(await this.mock.$includes(0, ethers.ZeroHash)).to.be.false;
60+
}
61+
});
62+
63+
it('clear', async function () {
64+
const value = generators.bytes32();
65+
await this.mock.$push(0, value);
66+
67+
expect(await this.mock.$count(0)).to.equal(1n);
68+
expect(await this.mock.$length(0)).to.equal(LENGTH);
69+
expect(await this.mock.$includes(0, value)).to.be.true;
70+
await this.mock.$last(0, 0); // not revert
71+
72+
await this.mock.$clear(0);
73+
74+
expect(await this.mock.$count(0)).to.equal(0n);
75+
expect(await this.mock.$length(0)).to.equal(LENGTH);
76+
expect(await this.mock.$includes(0, value)).to.be.false;
77+
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
78+
});
79+
});

0 commit comments

Comments
 (0)