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

Linked list queue #304

Merged
merged 6 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions contracts/contract/util/LinkedListStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
pragma solidity 0.8.18;
pragma abicoder v2;

// SPDX-License-Identifier: GPL-3.0-only

import "../RocketBase.sol";
import "../../interface/util/LinkedListStorageInterface.sol";

/// @notice A linked list storage helper for the deposit requests queue data
contract LinkedListStorage is RocketBase, LinkedListStorageInterface {

// Constants for packing queue metadata into a single uint256
uint256 constant internal startOffset = 256 - 64;
uint256 constant internal endOffset = 256 - 128;
uint256 constant internal lengthOffset = 256 - 192;

// Constants for packing a deposit item (struct) into a single uint256
uint256 constant internal receiverOffset = 256 - 160;
uint256 constant internal indexOffset = 256 - 160 - 32;
uint256 constant internal suppliedOffset = 256 - 160 - 32 - 32;

uint64 constant internal ones64Bits = 0xFFFFFFFFFFFFFFFF;

// Construct
constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) {
version = 1;
}

/// @notice The number of items in the queue
/// @param _namespace defines the queue to be used
function getLength(bytes32 _namespace) override public view returns (uint256) {
return uint64(getUint(keccak256(abi.encodePacked(_namespace, ".data"))) >> lengthOffset);
}

/// @notice The item in a queue by index
/// @param _namespace defines the queue to be used
/// @param _index the item index
function getItem(bytes32 _namespace, uint256 _index) override external view returns (DepositQueueValue memory) {
uint256 packedValue = getUint(keccak256(abi.encodePacked(_namespace, ".item", _index)));
return _unpackItem(packedValue);
}

/// @notice The index of an item in a queue. Returns 0 if the value is not found
/// @param _namespace defines the queue to be used
/// @param _value the deposit queue value
function getIndexOf(bytes32 _namespace, DepositQueueValue memory _value) override external view returns (uint256) {
return getUint(keccak256(abi.encodePacked(_namespace, ".index", _value.receiver, _value.validatorId)));
}

/// @notice Finds an item index in a queue and returns the previous item
/// @param _namespace defines the queue to be used
/// @param _value the deposit queue value
function getPreviousItem(bytes32 _namespace, DepositQueueValue memory _value) external view returns (DepositQueueValue memory previousItem) {
uint256 index = getUint(keccak256(abi.encodePacked(_namespace, ".index", _value.receiver, _value.validatorId)));
if (index > 0) {
uint256 previousIndex = getUint(keccak256(abi.encodePacked(_namespace, ".prev", index)));
previousItem = _unpackItem(getUint(keccak256(abi.encodePacked(_namespace, ".item", previousIndex))));
}
}

/// @notice Finds an item index in a queue and returns the next item
/// @param _namespace defines the queue to be used
/// @param _value the deposit queue value
function getNextItem(bytes32 _namespace, DepositQueueValue memory _value) external view returns (DepositQueueValue memory nextItem) {
uint256 index = getUint(keccak256(abi.encodePacked(_namespace, ".index", _value.receiver, _value.validatorId)));
if (index > 0) {
uint256 nextIndex = getUint(keccak256(abi.encodePacked(_namespace, ".next", index)));
nextItem = _unpackItem(getUint(keccak256(abi.encodePacked(_namespace, ".item", nextIndex))));
}
}

/// @notice Add an item to the end of the list. Requires that the item does not exist in the list
/// @param _namespace defines the queue to be used
/// @param _item the deposit queue item to be added
function enqueueItem(bytes32 _namespace, DepositQueueValue memory _item) virtual override external onlyLatestContract("addressLinkedListStorage", address(this)) onlyLatestNetworkContract {
_enqueueItem(_namespace, _item);
}

/// @notice Internal function created to allow testing enqueueItem
/// @param _namespace defines the queue to be used
/// @param _item the deposit queue value
function _enqueueItem(bytes32 _namespace, DepositQueueValue memory _item) internal {
require(getUint(keccak256(abi.encodePacked(_namespace, ".index", _item.receiver, _item.validatorId))) == 0, "Item already exists in queue");
uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data")));
uint256 endIndex = uint64(data >> endOffset);
uint256 newIndex = endIndex + 1;

if (endIndex > 0) {
setUint(keccak256(abi.encodePacked(_namespace, ".next", endIndex)), newIndex);
setUint(keccak256(abi.encodePacked(_namespace, ".prev", newIndex)), endIndex);
} else {
// clear the 64 bits used to stored the 'start' pointer
data &= ~(uint256(ones64Bits) << startOffset);
data |= newIndex << startOffset;
}

setUint(keccak256(abi.encodePacked(_namespace, ".item", newIndex)), _packItem(_item));
setUint(keccak256(abi.encodePacked(_namespace, ".index", _item.receiver, _item.validatorId)), newIndex);
// clear the 64 bits used to stored the 'end' pointer
data &= ~(uint256(ones64Bits) << endOffset);
data |= newIndex << endOffset;

// Update the length of the queue
uint256 currentLength = uint64(data >> lengthOffset);
// clear the 64 bits used to stored the 'length' information
data &= ~(uint256(ones64Bits) << lengthOffset);
data |= (currentLength + 1) << lengthOffset;
setUint(keccak256(abi.encodePacked(_namespace, ".data")), data);
}

/// @notice Remove an item from the start of a queue and return it. Requires that the queue is not empty
/// @param _namespace defines the queue to be used
function dequeueItem(bytes32 _namespace) public virtual override onlyLatestContract("addressLinkedListStorage", address(this)) onlyLatestNetworkContract returns (DepositQueueValue memory item) {
return _dequeueItem(_namespace);
}

/// @notice Remove an item from the start of a queue and return it. Requires that the queue is not empty
/// @param _namespace defines the queue to be used
function _dequeueItem(bytes32 _namespace) internal returns (DepositQueueValue memory item) {
uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data")));
uint256 length = uint64(data >> lengthOffset);
require(length > 0, "Queue can't be empty");
uint256 start = uint64(data >> startOffset);
uint256 packedItem = getUint(keccak256(abi.encodePacked(_namespace, ".item", start)));
item = _unpackItem(packedItem);

uint256 nextItem = getUint(keccak256(abi.encodePacked(_namespace, ".next", start)));
// clear the 64 bits used to stored the 'start' pointer
data &= ~(uint256(ones64Bits) << startOffset);
data |= nextItem << startOffset;
setUint(keccak256(abi.encodePacked(_namespace, ".index", item.receiver, item.validatorId)), 0);

if (nextItem > 0) {
setUint(keccak256(abi.encodePacked(_namespace, ".prev", nextItem)), 0);
} else {
// zero the 64 bits storing the 'end' pointer
data &= ~(uint256(ones64Bits) << endOffset);
}

// Update the length of the queue
// clear the 64 bits used to stored the 'length' information
data &= ~(uint256(ones64Bits) << lengthOffset);
data |= (length - 1) << lengthOffset;
setUint(keccak256(abi.encodePacked(_namespace, ".data")), data);

return item;
}

/// @notice Removes an item from a queue. Requires that the item exists in the queue
/// @param _namespace defines the queue to be used
/// @param _item to be removed from the queue
function removeItem(bytes32 _namespace, DepositQueueValue memory _item) public virtual override onlyLatestContract("addressLinkedListStorage", address(this)) onlyLatestNetworkContract {
_removeItem(_namespace, _item);
}

/// @notice Internal funciton to remove an item from a queue. Requires that the item exists in the queue
/// @param _namespace defines the queue to be used
/// @param _item to be removed from the queue
function _removeItem(bytes32 _namespace, DepositQueueValue memory _item) internal {
uint256 index = getUint(keccak256(abi.encodePacked(_namespace, ".index", _item.receiver, _item.validatorId)));
uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data")));
require(index > 0, "Item does not exist in queue");

uint256 prevIndex = getUint(keccak256(abi.encodePacked(_namespace, ".prev", index)));
uint256 nextIndex = getUint(keccak256(abi.encodePacked(_namespace, ".next", index)));
if (prevIndex > 0) {
// Not the first item
setUint(keccak256(abi.encodePacked(_namespace, ".next", prevIndex)), nextIndex);
} else {
// First item
// clear the 64 bits used to stored the 'start' pointer
data &= ~(uint256(ones64Bits) << startOffset);
data |= nextIndex << startOffset;
setUint(keccak256(abi.encodePacked(_namespace, ".prev", nextIndex)), 0);
}

if (nextIndex > 0) {
// Not the last item
setUint(keccak256(abi.encodePacked(_namespace, ".prev", nextIndex)), prevIndex);
} else {
// Last item
// clear the 64 bits used to stored the 'end' pointer
data &= ~(uint256(ones64Bits) << endOffset);
data |= prevIndex << endOffset;
}

setUint(keccak256(abi.encodePacked(_namespace, ".index", _item.receiver, _item.validatorId)), 0);
setUint(keccak256(abi.encodePacked(_namespace, ".next", index)), 0);
setUint(keccak256(abi.encodePacked(_namespace, ".prev", index)), 0);

// Update the length of the queue
uint256 currentLength = uint64(data >> lengthOffset);
// clear the 64 bits used to stored the 'length' information
data &= ~(uint256(ones64Bits) << lengthOffset);
data |= (currentLength - 1) << lengthOffset;
setUint(keccak256(abi.encodePacked(_namespace, ".data")), data);
}

/// @notice packs a deposit queue value into a single uint256
/// @param _item the deposit queue item to be packed
function _packItem(DepositQueueValue memory _item) internal pure returns (uint256 packed) {
packed |= uint256(uint160(_item.receiver)) << receiverOffset;
packed |= uint256(_item.validatorId) << indexOffset;
packed |= uint256(_item.suppliedValue) << suppliedOffset;
packed |= uint256(_item.requestedValue);
}

/// @notice unpacks an uint256 value into a deposit queue struct
/// @param _packedValue the packed deposit queue value
function _unpackItem(uint256 _packedValue) internal pure returns (DepositQueueValue memory item) {
item.receiver = address(uint160(_packedValue >> receiverOffset));
item.validatorId = uint32(_packedValue >> indexOffset);
item.suppliedValue = uint32(_packedValue >> suppliedOffset);
item.requestedValue = uint32(_packedValue);
}

}
43 changes: 43 additions & 0 deletions contracts/contract/util/LinkedListStorageHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
pragma solidity 0.8.18;
pragma abicoder v2;

// SPDX-License-Identifier: GPL-3.0-only

import "./LinkedListStorage.sol";

/// @notice A linked list storage helper to test internal functions
contract LinkedListStorageHelper is LinkedListStorage {

// Construct
constructor(RocketStorageInterface _rocketStorageAddress) LinkedListStorage(_rocketStorageAddress) {
version = 1;
}

/// @notice Add an item to the end of the list. Requires that the item does not exist in the list
/// @param _namespace defines the queue to be used
/// @param _item the deposit queue item
function enqueueItem(bytes32 _namespace, DepositQueueValue memory _item) public override {
_enqueueItem(_namespace, _item);
}

/// @notice Remove an item from the start of a queue and return it. Requires that the queue is not empty
/// @param _namespace defines the queue to be used
function dequeueItem(bytes32 _namespace) public virtual override returns (DepositQueueValue memory item) {
return _dequeueItem(_namespace);
}

/// @notice Removes an item from a queue. Requires that the item exists in the queue
/// @param _namespace to be used
/// @param _item to be removed from the queue
function removeItem(bytes32 _namespace, DepositQueueValue memory _item) public virtual override {
return _removeItem(_namespace, _item);
}

function packItem(DepositQueueValue memory _item) public pure returns (uint256 packed) {
return _packItem(_item);
}

function unpackItem(uint256 _item) public pure returns (DepositQueueValue memory item) {
return _unpackItem(_item);
}
}
20 changes: 20 additions & 0 deletions contracts/interface/util/LinkedListStorageInterface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pragma solidity >0.5.0 <0.9.0;
pragma abicoder v2;

// SPDX-License-Identifier: GPL-3.0-only

struct DepositQueueValue {
address receiver; // the address that will receive the requested value
uint32 validatorId; // internal validator id
uint32 suppliedValue; // in milliether
uint32 requestedValue; // in milliether
}

interface LinkedListStorageInterface {
function getLength(bytes32 _namespace) external view returns (uint256);
function getItem(bytes32 _namespace, uint _index) external view returns (DepositQueueValue memory);
function getIndexOf(bytes32 _namespace, DepositQueueValue memory _value) external view returns (uint256);
function enqueueItem(bytes32 _namespace, DepositQueueValue memory _value) external;
function dequeueItem(bytes32 _namespace) external returns (DepositQueueValue memory);
function removeItem(bytes32 _namespace, DepositQueueValue memory _value) external;
}
9 changes: 7 additions & 2 deletions test/_helpers/deployment.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/*** Dependencies ********************/
import { RocketDAOProtocolSettingsNode, RocketStorage } from '../_utils/artifacts';
import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap';
import { RocketStorage } from '../_utils/artifacts';

const hre = require('hardhat');
const pako = require('pako');
Expand Down Expand Up @@ -107,6 +106,12 @@ const contracts = {
// Development helper contracts
const revertOnTransfer = artifacts.require('RevertOnTransfer.sol');
const rocketNodeDepositLEB4 = artifacts.require('RocketNodeDepositLEB4.sol');
if (network.name !== 'live' && network.name !== 'goerli') {
// the linked list storage helper needs to be added as a network contract
contracts.linkedListStorage = artifacts.require('LinkedListStorageHelper.sol');
} else {
contracts.linkedListStorage = artifacts.require('LinkedListStorage.sol');
}

// Instance contract ABIs
const abis = {
Expand Down
3 changes: 3 additions & 0 deletions test/_utils/artifacts.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { artifacts } from "hardhat";

export const RocketAuctionManager = artifacts.require('RocketAuctionManager.sol');
export const RocketClaimDAO = artifacts.require('RocketClaimDAO.sol');
export const RocketDAONodeTrusted = artifacts.require('RocketDAONodeTrusted.sol');
Expand Down Expand Up @@ -50,6 +52,7 @@ export const RocketMinipoolQueue = artifacts.require('RocketMinipoolQueue.sol');
export const RocketNodeDeposit = artifacts.require('RocketNodeDeposit.sol');
export const RocketMinipoolDelegate = artifacts.require('RocketMinipoolDelegate.sol');
export const RocketDAOProtocolSettingsMinipool = artifacts.require('RocketDAOProtocolSettingsMinipool.sol');
export const LinkedListStorage = artifacts.require('LinkedListStorageHelper.sol');
export const RocketDepositPool = artifacts.require('RocketDepositPool.sol');
export const RocketMinipoolBondReducer = artifacts.require('RocketMinipoolBondReducer.sol');
export const RocketNetworkSnapshots = artifacts.require('RocketNetworkSnapshots.sol');
Expand Down
2 changes: 1 addition & 1 deletion test/deposit/deposit-pool-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getDepositSetting } from '../_helpers/settings';
import { deposit } from './scenario-deposit';
import {
RocketDAONodeTrustedSettingsMembers,
RocketDAOProtocolSettingsDeposit, RocketDepositPool,
RocketDAOProtocolSettingsDeposit, RocketDepositPool, AddressLinkedQueueStorage
} from '../_utils/artifacts';
import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap';
import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'
Expand Down
2 changes: 2 additions & 0 deletions test/rocket-pool-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { injectBNHelpers } from './_helpers/bn';
import { checkInvariants } from './_helpers/invariants';
import networkSnapshotsTests from './network/network-snapshots-tests';
import networkVotingTests from './network/network-voting-tests';
import utilTests from './util/util-tests';

// Header
console.log('\n');
Expand Down Expand Up @@ -85,3 +86,4 @@ nodeDistributorTests();
rethTests();
rplTests();
rewardsPoolTests();
utilTests();
Loading
Loading