Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a double ended queue #3153

Merged
merged 21 commits into from Feb 16, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 58 additions & 0 deletions contracts/mocks/VectorMock.sol
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../utils/structs/Vector.sol";

// Bytes32Vector
contract Bytes32VectorMock {
using Vector for Vector.Bytes32Vector;

event OperationResult(bytes32 value);

Vector.Bytes32Vector private _vector;

function pushBack(bytes32 value) public {
_vector.pushBack(value);
}

function pushFront(bytes32 value) public {
_vector.pushFront(value);
}

function popFront() public returns (bytes32) {
bytes32 value = _vector.popFront();
emit OperationResult(value);
return value;
}

function popBack() public returns (bytes32) {
bytes32 value = _vector.popBack();
emit OperationResult(value);
return value;
}

function front() public view returns (bytes32) {
return _vector.front();
}

function back() public view returns (bytes32) {
return _vector.back();
}

function at(uint256 i) public view returns (bytes32) {
return _vector.at(i);
}

function clear() public {
_vector.clear();
}

function length() public view returns (uint256) {
return _vector.length();
}

function empty() public view returns (bool) {
return _vector.empty();
}
}
79 changes: 79 additions & 0 deletions contracts/utils/structs/Vector.sol
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

library Vector {
error Empty();
error OutOfBound();
Amxx marked this conversation as resolved.
Show resolved Hide resolved

struct Bytes32Vector {
int128 begin;
int128 end;
mapping(int128 => bytes32) data;
}

function pushBack(Bytes32Vector storage vector, bytes32 value) internal {
unchecked {
vector.data[vector.end++] = value;
frangio marked this conversation as resolved.
Show resolved Hide resolved
}
}

function pushFront(Bytes32Vector storage vector, bytes32 value) internal {
unchecked {
vector.data[--vector.begin] = value;
}
}

function popFront(Bytes32Vector storage vector) internal returns (bytes32 value) {
if (empty(vector)) revert Empty();
unchecked {
int128 idx = vector.begin++;
value = vector.data[idx];
delete vector.data[idx];
}
}

function popBack(Bytes32Vector storage vector) internal returns (bytes32 value) {
if (empty(vector)) revert Empty();
unchecked {
int128 idx = --vector.end;
value = vector.data[idx];
delete vector.data[idx];
}
}

function front(Bytes32Vector storage vector) internal view returns (bytes32 value) {
if (empty(vector)) revert Empty();
unchecked {
return vector.data[vector.begin];
}
}

function back(Bytes32Vector storage vector) internal view returns (bytes32 value) {
if (empty(vector)) revert Empty();
unchecked {
return vector.data[vector.end - 1];
}
}

function at(Bytes32Vector storage vector, uint256 i) internal view returns (bytes32 value) {
// leave check here: overflow could happen
int128 idx = vector.begin + int128(int256(i));
if (idx >= vector.end) revert OutOfBound();
return vector.data[idx];
}

function clear(Bytes32Vector storage vector) internal {
vector.begin = 0;
vector.end = 0;
}

function length(Bytes32Vector storage vector) internal view returns (uint256) {
unchecked {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Why don't we do just use checked arithmetic and even SafeCast?

Copy link
Collaborator Author

@Amxx Amxx Feb 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

begin and end are only updated by ++ or --. They are not expected to overflow (it would take 2**127 calls for it to overflow).

a consequence is that any checks (like the safemath that solidity includes by default and safecast) would just be wasted gas.

For example, on a pushX/popX operation, unchecked saves ~100 gas per operation. Not sure what the safecast impact would be here, but it would be wasted gas

return uint256(int256(vector.end - vector.begin));
}
}

function empty(Bytes32Vector storage vector) internal view returns (bool) {
return length(vector) == 0;
}
}
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -12,7 +12,7 @@
},
"scripts": {
"compile": "hardhat compile",
"coverage": "env COVERAGE=true hardhat coverage",
"coverage": "env COVERAGE=true NODE_OPTIONS=--max-old-space-size=4096 hardhat coverage",
"docs": "oz-docs",
"docs:watch": "npm run docs watch contracts 'docs/*.hbs' docs/helpers.js",
"prepare-docs": "scripts/prepare-docs.sh",
Expand Down
91 changes: 91 additions & 0 deletions test/utils/structs/Vector.test.js
@@ -0,0 +1,91 @@
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');

const Bytes32VectorMock = artifacts.require('Bytes32VectorMock');

async function getContent (vector) {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
const length = await vector.length().then(bn => bn.toNumber());
const values = await Promise.all(Array(length).fill().map((_, i) => vector.at(i)));
return values;
}

contract('Vector', function (accounts) {
// Bytes32Vector
describe('EnumerableVector', function () {
const bytesA = '0xdeadbeef'.padEnd(66, '0');
const bytesB = '0x0123456789'.padEnd(66, '0');
const bytesC = '0x42424242'.padEnd(66, '0');
const bytesD = '0x171717'.padEnd(66, '0');

beforeEach(async function () {
this.vector = await Bytes32VectorMock.new();
});

it('empty vector', async function () {
expect(await this.vector.empty()).to.be.equal(true);
expect(await getContent(this.vector)).to.have.all.members([]);
});

describe('with content', function () {
beforeEach(async function () {
await this.vector.pushBack(bytesB);
await this.vector.pushFront(bytesA);
await this.vector.pushBack(bytesC);
});

it('content is valid', async function () {
expect(await this.vector.empty()).to.be.equal(false);
expect(await this.vector.front()).to.be.equal(bytesA);
expect(await this.vector.back()).to.be.equal(bytesC);
expect(await getContent(this.vector)).to.have.all.members([ bytesA, bytesB, bytesC ]);
Amxx marked this conversation as resolved.
Show resolved Hide resolved
});

it('out of bound access', async function () {
await expectRevert.unspecified(this.vector.at(10));
frangio marked this conversation as resolved.
Show resolved Hide resolved
});

describe('push', function () {
it('front', async function () {
await this.vector.pushFront(bytesD);

expect(await this.vector.front()).to.be.equal(bytesD);
expect(await this.vector.back()).to.be.equal(bytesC);
expect(await getContent(this.vector)).to.have.all.members([ bytesD, bytesA, bytesB, bytesC ]);
Amxx marked this conversation as resolved.
Show resolved Hide resolved
});

it('back', async function () {
await this.vector.pushBack(bytesD);

expect(await this.vector.front()).to.be.equal(bytesA);
expect(await this.vector.back()).to.be.equal(bytesD);
expect(await getContent(this.vector)).to.have.all.members([ bytesA, bytesB, bytesC, bytesD ]);
});
});

describe('pop', function () {
it('front', async function () {
expectEvent(await this.vector.popFront(), 'OperationResult', { value: bytesA });

expect(await this.vector.front()).to.be.equal(bytesB);
expect(await this.vector.back()).to.be.equal(bytesC);
expect(await getContent(this.vector)).to.have.all.members([ bytesB, bytesC ]);
});

it('back', async function () {
expectEvent(await this.vector.popBack(), 'OperationResult', { value: bytesC });

expect(await this.vector.front()).to.be.equal(bytesA);
expect(await this.vector.back()).to.be.equal(bytesB);
expect(await getContent(this.vector)).to.have.all.members([ bytesA, bytesB ]);
});
});

it('reset', async function () {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
await this.vector.clear();

expect(await this.vector.empty()).to.be.equal(true);
expect(await getContent(this.vector)).to.have.all.members([]);
});
});
});
});