Skip to content

Commit

Permalink
ON-846: implemented linked list library
Browse files Browse the repository at this point in the history
  • Loading branch information
ernanirst committed May 2, 2024
1 parent 6368367 commit cefb282
Show file tree
Hide file tree
Showing 5 changed files with 412 additions and 0 deletions.
109 changes: 109 additions & 0 deletions contracts/libraries/Uint64SortedLinkedListLibrary.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.9;

/// @title Uint64SortedLinkedListLibrary
/// @dev Implementation of a linked list of uint64 keys. The list is ordered in descending order, and allows duplicates.
library Uint64SortedLinkedListLibrary {
uint64 private constant EMPTY = 0;

struct Item {
uint64 prev;
uint64 next;
uint8 count;
}

struct List {
uint64 head;
mapping(uint64 => Item) items;
}

/// @notice Inserts a new item into the list.
/// @dev It should maintain the descending order of the list.
/// @param _self The list to insert into.
/// @param _key The new item to be inserted.
function insert(List storage _self, uint64 _key) internal {
require(_key != EMPTY, 'Uint64SortedLinkedListLibrary: key cannot be zero');

// if _key already exists, only increase counter
if (_self.items[_key].count > 0) {
_self.items[_key].count++;
return;
}

// if _key is the highest in the list, insert as head
if (_key > _self.head) {
_self.items[_key] = Item(EMPTY, _self.head, 1);

// only update the previous head if list was not empty
if (_self.head != EMPTY) _self.items[_self.head].prev = _key;

_self.head = _key;
return;
}

// loop until position to insert is found
uint64 _itemKey = _self.head;
Item storage _item = _self.items[_itemKey];
while (_key < _item.next && _item.next != EMPTY) {
_itemKey = _item.next;
_item = _self.items[_itemKey];
}

// if found item is tail, next is EMPTY
if (_item.next == EMPTY) {
_self.items[_key] = Item(_itemKey, EMPTY, 1);
_item.next = _key;
return;
}

// if not tail, insert between two items
_self.items[_key] = Item(_itemKey, _item.next, 1);
_self.items[_item.next].prev = _key;
_item.next = _key;
}

/// @notice Removes an item from the list.
/// @dev It should maintain the descending order of the list.
/// @param _self The list to remove from.
/// @param _key The item to be removed.
function remove(List storage _self, uint64 _key) internal {
Item storage _itemToUpdate = _self.items[_key];

// if _key does not exist, return
if (_itemToUpdate.count == 0) {
return;
}

// if _key occurs more than once, just decrease counter
if (_itemToUpdate.count > 1) {
_itemToUpdate.count--;
return;
}

// updating list

// if _key is the head, update head to the next item
if (_itemToUpdate.prev == EMPTY) {
_self.head = _itemToUpdate.next;

// only update next item if it exists (if it's not head and tail simultaneously)
if (_itemToUpdate.next != EMPTY) _self.items[_itemToUpdate.next].prev = EMPTY;

delete _self.items[_key];
return;
}

// if _key is not head, but it is tail, update the previous item's next pointer to EMPTY
if (_itemToUpdate.next == EMPTY) {
_self.items[_itemToUpdate.prev].next = EMPTY;
delete _self.items[_key];
return;
}

// if not head nor tail, update both previous and next items
_self.items[_itemToUpdate.next].prev = _itemToUpdate.prev;
_self.items[_itemToUpdate.prev].next = _itemToUpdate.next;
delete _self.items[_key];
}
}
31 changes: 31 additions & 0 deletions contracts/mocks/MockSortedLinkedList.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.9;

import { Uint64SortedLinkedListLibrary } from '../libraries/Uint64SortedLinkedListLibrary.sol';

contract MockSortedLinkedList {
using Uint64SortedLinkedListLibrary for Uint64SortedLinkedListLibrary.List;
using Uint64SortedLinkedListLibrary for Uint64SortedLinkedListLibrary.Item;

Uint64SortedLinkedListLibrary.List internal list;

function insert(uint64 _key) public {
list.insert(_key);
}

function remove(uint64 _key) public {
list.remove(_key);
}

function getHead() public view returns (uint256) {
return list.head;
}

function getItem(uint64 _key) public view returns (uint64 prev_, uint64 next_, uint8 count_) {
Uint64SortedLinkedListLibrary.Item storage item = list.items[_key];
prev_ = item.prev;
next_ = item.next;
count_ = item.count;
}
}
4 changes: 4 additions & 0 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ export function generateRandomInt() {
return Math.floor(Math.random() * 1000 * 1000) + 1
}

export function generateRandomIntBetween(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min
}

export function generateRoleId(role: string) {
return solidityKeccak256(['string'], [role])
}
Expand Down
197 changes: 197 additions & 0 deletions test/libraries/Uint64SortedLinkedListLibrary.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { beforeEach, it } from 'mocha'
import { expect } from 'chai'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
import { Contract } from 'ethers'
import { ethers } from 'hardhat'
import { checkList } from './helpers'
import { generateRandomIntBetween } from '../helpers'

describe('Uint64SortedLinkedListLibrary', async () => {
let MockedList: Contract

async function deployContracts() {
const MockedListFactory = await ethers.getContractFactory('MockSortedLinkedList')
MockedList = await MockedListFactory.deploy()
}

beforeEach(async () => {
await loadFixture(deployContracts)
})

describe('insert', async () => {
it('should revert when key is zero', async () => {
await expect(MockedList.insert(0)).to.be.revertedWith('Uint64SortedLinkedListLibrary: key cannot be zero')
})

it('should insert as head and tail when list is empty', async () => {
expect(await MockedList.insert(1)).to.not.be.reverted
await checkList(MockedList, [1], [1])
})

it('should insert as head when list is not empty', async () => {
expect(await MockedList.insert(10)).to.not.be.reverted
expect(await MockedList.insert(11)).to.not.be.reverted
await checkList(MockedList, [11, 10], [1, 1])
})

it('should insert as tail when list is not empty', async () => {
expect(await MockedList.insert(12)).to.not.be.reverted
expect(await MockedList.insert(10)).to.not.be.reverted
await checkList(MockedList, [12, 10], [1, 1])
})

it('should insert in the middle of the list', async () => {
expect(await MockedList.insert(20)).to.not.be.reverted
expect(await MockedList.insert(10)).to.not.be.reverted
expect(await MockedList.insert(15)).to.not.be.reverted
await checkList(MockedList, [20, 15, 10], [1, 1, 1])
})

it('should insert the same item', async () => {
await checkList(MockedList, [], [])
expect(await MockedList.insert(100)).to.not.be.reverted
await checkList(MockedList, [100], [1])
expect(await MockedList.insert(100)).to.not.be.reverted
await checkList(MockedList, [100], [2])
expect(await MockedList.insert(100)).to.not.be.reverted
await checkList(MockedList, [100], [3])
})

it('should insert as head multiple times', async () => {
expect(await MockedList.insert(10)).to.not.be.reverted
expect(await MockedList.insert(20)).to.not.be.reverted
expect(await MockedList.insert(30)).to.not.be.reverted
expect(await MockedList.insert(40)).to.not.be.reverted
expect(await MockedList.insert(50)).to.not.be.reverted
await checkList(MockedList, [50, 40, 30, 20, 10], [1, 1, 1, 1, 1])
})

it('should insert as tail multiple times', async () => {
expect(await MockedList.insert(50)).to.not.be.reverted
expect(await MockedList.insert(40)).to.not.be.reverted
expect(await MockedList.insert(30)).to.not.be.reverted
expect(await MockedList.insert(20)).to.not.be.reverted
expect(await MockedList.insert(10)).to.not.be.reverted
await checkList(MockedList, [50, 40, 30, 20, 10], [1, 1, 1, 1, 1])
})

it('should insert as middle item multiple times', async () => {
expect(await MockedList.insert(50)).to.not.be.reverted
expect(await MockedList.insert(10)).to.not.be.reverted

expect(await MockedList.insert(40)).to.not.be.reverted
expect(await MockedList.insert(30)).to.not.be.reverted
expect(await MockedList.insert(20)).to.not.be.reverted

await checkList(MockedList, [50, 40, 30, 20, 10], [1, 1, 1, 1, 1])
})
})

describe('remove', async () => {
it('should not do anything when list is empty', async () => {
expect(await MockedList.remove(1)).to.not.be.reverted
await checkList(MockedList, [], [])
})

it('should not do anything when list has only one different element', async () => {
expect(await MockedList.insert(1)).to.not.be.reverted
expect(await MockedList.remove(2)).to.not.be.reverted
await checkList(MockedList, [1], [1])
})

it('should not do anything when list has two different elements', async () => {
expect(await MockedList.insert(1)).to.not.be.reverted
expect(await MockedList.insert(2)).to.not.be.reverted
expect(await MockedList.remove(3)).to.not.be.reverted
await checkList(MockedList, [2, 1], [1, 1])
})

it('should decrease item count when item has more than one occurrence', async () => {
expect(await MockedList.insert(1)).to.not.be.reverted
expect(await MockedList.insert(1)).to.not.be.reverted
expect(await MockedList.insert(2)).to.not.be.reverted
expect(await MockedList.insert(2)).to.not.be.reverted
expect(await MockedList.insert(2)).to.not.be.reverted
expect(await MockedList.remove(1)).to.not.be.reverted
expect(await MockedList.remove(2)).to.not.be.reverted
await checkList(MockedList, [2, 1], [2, 1])
})

it('should remove head when list has only one element', async () => {
expect(await MockedList.insert(1)).to.not.be.reverted
expect(await MockedList.remove(1)).to.not.be.reverted
await checkList(MockedList, [], [])
})

it('should remove head when list has more than one element', async () => {
expect(await MockedList.insert(10)).to.not.be.reverted
expect(await MockedList.insert(20)).to.not.be.reverted
expect(await MockedList.remove(20)).to.not.be.reverted
await checkList(MockedList, [10], [1])
})

it('should remove tail when list has more than one element', async () => {
expect(await MockedList.insert(10)).to.not.be.reverted
expect(await MockedList.insert(20)).to.not.be.reverted
expect(await MockedList.remove(10)).to.not.be.reverted
await checkList(MockedList, [20], [1])
})

it('should remove middle element when list has more than two elements', async () => {
expect(await MockedList.insert(10)).to.not.be.reverted
expect(await MockedList.insert(20)).to.not.be.reverted
expect(await MockedList.insert(30)).to.not.be.reverted
expect(await MockedList.insert(40)).to.not.be.reverted
expect(await MockedList.remove(30)).to.not.be.reverted
await checkList(MockedList, [40, 20, 10], [1, 1, 1])
})
})

// takes up to 25s to run
describe('Insert & Remove multiple times', async () => {
it('should insert and remove 100 items', async () => {
// insert
const keyList = []
for (let i = 0; i < 100; i++) {
const key = generateRandomIntBetween(1, 100)
keyList.push(key)
const { keys, counters } = computeLinkedList(keyList)
expect(await MockedList.insert(key)).to.not.be.reverted
await checkList(MockedList, keys, counters)
}

// remove
for (let i = keyList.length; i > 0; i--) {
const key = keyList[i - 1]
keyList.pop()
const { keys, counters } = computeLinkedList(keyList)
expect(await MockedList.remove(key)).to.not.be.reverted
await checkList(MockedList, keys, counters)
}
})
})
})

function computeLinkedList(list: number[]) {
const keysMap = list.reduce((acc: { [key: string]: { key: number; count: number } }, key) => {
if (acc[key.toString()]) {
acc[key.toString()].count++
} else {
acc[key.toString()] = { key, count: 1 }
}
return acc
}, {})

const keysCounter = Object.keys(keysMap)
.map(key => keysMap[key])
.sort((a, b) => b.key - a.key)

const keys: number[] = []
const counters: number[] = []
keysCounter.forEach((item, i) => {
keys[i] = item.key
counters[i] = item.count
})

return { keys, counters }
}
Loading

0 comments on commit cefb282

Please sign in to comment.