diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0ca505 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +__pycache__ +.history +.hypothesis/ +build/ +reports/ +logdo.md +_niftsy_s/ +.env +abi/ +node_modules/ +hardhat* +artifacts/ +cache/ +.pytest_cache/ + diff --git a/README.md b/README.md index 31759c3..6c0cea7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ ## Hitchens Order Statistics Tree -[https://github.com/rob-Hitchens/OrderStatisticsTree](https://github.com/rob-Hitchens/OrderStatisticsTree) +[https://github.com/rob-Hitchens/OrderStatisticsTree](https://github.com/rob-Hitchens/OrderStatisticsTree) + +For use **Hitchens Order Statistics Tree, Solidity v0.8.17** please follow [README2.md](./README2.md) Solidity Library that implements a self-balancing binary search tree (BST) with [Order Statistics Tree](https://en.wikipedia.org/wiki/Order_statistic_tree) extensions. The Library implements [Bokky Poobah's Red Black Tree](https://github.com/bokkypoobah/BokkyPooBahsRedBlackTreeLibrary) with additional properties. diff --git a/README2.md b/README2.md new file mode 100644 index 0000000..e0a178d --- /dev/null +++ b/README2.md @@ -0,0 +1,47 @@ +## Hitchens Order Statistics Tree, Solidity v0.8.17 + +### Tests +We use Brownie framework for developing and unit test. For run tests +first please [install it](https://eth-brownie.readthedocs.io/en/stable/install.html) +To run long tests you must rename test files in tests folder before running (delete "long_"). + +```bash +#brownie pm install OpenZeppelin/openzeppelin-contracts@4.7.3 +brownie test +``` + +Now tests are running in very verbose mode. For off this mode please edit `pyproject.toml` +in the root folder. + +At commit moment (2022-10-31) test coverage was 83% +```bash + contract: MockHOSTV1 - 83.8% + HitchensOrderStatisticsTreeLibV1.exists - 100.0% + HitchensOrderStatisticsTreeLibV1.getNode2 - 100.0% + HitchensOrderStatisticsTreeLibV1.insertFixup - 100.0% + HitchensOrderStatisticsTreeLibV1.keyExists - 100.0% + HitchensOrderStatisticsTreeLibV1.prev - 100.0% + HitchensOrderStatisticsTreeLibV1.rotateLeft - 100.0% + HitchensOrderStatisticsTreeLibV1.rotateRight - 100.0% + HitchensOrderStatisticsTreeLibV1.valueKeyAtIndex - 100.0% + HitchensOrderStatisticsTreeLibV1.atRank - 96.4% + HitchensOrderStatisticsTreeLibV1.insert - 95.0% + HitchensOrderStatisticsTreeLibV1.rank - 88.2% + HitchensOrderStatisticsTreeLibV1.below - 87.5% + HitchensOrderStatisticsTreeLibV1.next - 80.0% + HitchensOrderStatisticsTreeLibV1.above - 75.0% + HitchensOrderStatisticsTreeLibV1.getNode - 75.0% + HitchensOrderStatisticsTreeLibV1.removeFixup - 73.7% + HitchensOrderStatisticsTreeLibV1.remove - 63.3% + HitchensOrderStatisticsTreeLibV1.replaceParent - 0.0% + +``` + + +### Gas +```bash +MockHOSTV1 + ├─ constructor - avg: 1945401 avg (confirmed): 1945401 low: 1945401 high: 1945401 + ├─ insertKeyValue - avg: 136848 avg (confirmed): 141383 low: 23985 high: 260610 + └─ removeKeyValue - avg: 75151 avg (confirmed): 79721 low: 23941 high: 174703 +``` diff --git a/contracts/HOSTLib.sol b/contracts/HOSTLib.sol new file mode 100644 index 0000000..d1da383 --- /dev/null +++ b/contracts/HOSTLib.sol @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +/* +Order Statistics Tree by Envelop +Based on +Hitchens Order Statistics Tree v0.99 + +A Solidity Red-Black Tree library to store and maintain a sorted data +structure in a Red-Black binary search tree, with O(log 2n) insert, remove +and search time (and gas, approximately) + +https://github.com/rob-Hitchens/OrderStatisticsTree + +Copyright (c) Rob Hitchens. the MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Significant portions from BokkyPooBahsRedBlackTreeLibrary, +https://github.com/bokkypoobah/BokkyPooBahsRedBlackTreeLibrary + +THIS SOFTWARE IS NOT TESTED OR AUDITED. DO NOT USE FOR PRODUCTION. +*/ + +library HitchensOrderStatisticsTreeLibV1 { + uint private constant EMPTY = 0; + struct Node { + uint parent; + uint left; + uint right; + bool red; + bytes32[] keys; + mapping(bytes32 => uint) keyMap; + uint count; + } + struct Tree { + uint root; + mapping(uint => Node) nodes; + } + function first(Tree storage self) internal view returns (uint _value) { + _value = self.root; + if(_value == EMPTY) return 0; + while (self.nodes[_value].left != EMPTY) { + _value = self.nodes[_value].left; + } + } + function last(Tree storage self) internal view returns (uint _value) { + _value = self.root; + if(_value == EMPTY) return 0; + while (self.nodes[_value].right != EMPTY) { + _value = self.nodes[_value].right; + } + } + function next(Tree storage self, uint value) internal view returns (uint _cursor) { + require(value != EMPTY, "OrderStatisticsTree(401) - Starting value cannot be zero"); + if (self.nodes[value].right != EMPTY) { + _cursor = treeMinimum(self, self.nodes[value].right); + } else { + _cursor = self.nodes[value].parent; + while (_cursor != EMPTY && value == self.nodes[_cursor].right) { + value = _cursor; + _cursor = self.nodes[_cursor].parent; + } + } + } + function prev(Tree storage self, uint value) internal view returns (uint _cursor) { + require(value != EMPTY, "OrderStatisticsTree(402) - Starting value cannot be zero"); + if (self.nodes[value].left != EMPTY) { + _cursor = treeMaximum(self, self.nodes[value].left); + } else { + _cursor = self.nodes[value].parent; + while (_cursor != EMPTY && value == self.nodes[_cursor].left) { + value = _cursor; + _cursor = self.nodes[_cursor].parent; + } + } + } + function exists(Tree storage self, uint value) internal view returns (bool _exists) { + if(value == EMPTY) return false; + if(value == self.root) return true; + if(self.nodes[value].parent != EMPTY) return true; + return false; + } + function keyExists(Tree storage self, bytes32 key, uint value) internal view returns (bool _exists) { + if(!exists(self, value)) return false; + return self.nodes[value].keys[self.nodes[value].keyMap[key]] == key; + } + + function getNode(Tree storage self, uint value) + internal + view + returns ( + uint _parent, + uint _left, + uint _right, + bool _red, + uint keyCount, + uint _count + ) + { + require(exists(self,value), "OrderStatisticsTree(403) - Value does not exist."); + Node storage gn = self.nodes[value]; + return(gn.parent, gn.left, gn.right, gn.red, gn.keys.length, gn.keys.length+gn.count); + } + + function getNode2(Tree storage self, uint value) + internal + view + returns (Node storage node) + + { + require(exists(self,value), "OrderStatisticsTree(403) - Value does not exist."); + node = self.nodes[value]; + } + + + function getNodeCount(Tree storage self, uint value) internal view returns(uint _count) { + Node storage gn = self.nodes[value]; + return gn.keys.length + gn.count; + } + + function getNodeKeysLength(Tree storage self, uint value) internal view returns(uint _count) { + self.nodes[value]; + return self.nodes[value].keys.length; + } + + function valueKeyAtIndex(Tree storage self, uint value, uint index) internal view returns(bytes32 _key) { + require(exists(self,value), "OrderStatisticsTree(404) - Value does not exist."); + return self.nodes[value].keys[index]; + } + function count(Tree storage self) internal view returns(uint _count) { + return getNodeCount(self,self.root); + } + function percentile(Tree storage self, uint value) internal view returns(uint _percentile) { + uint denominator = count(self); + uint numerator = rank(self, value); + _percentile = ((uint(1000) * numerator)/denominator+(uint(5)))/uint(10); + } + function permil(Tree storage self, uint value) internal view returns(uint _permil) { + uint denominator = count(self); + uint numerator = rank(self, value); + _permil = ((uint(10000) * numerator)/denominator+(uint(5)))/uint(10); + } + function atPercentile(Tree storage self, uint _percentile) internal view returns(uint _value) { + uint findRank = (((_percentile * count(self))/uint(10)) + uint(5)) / uint(10); + return atRank(self,findRank); + } + function atPermil(Tree storage self, uint _permil) internal view returns(uint _value) { + uint findRank = (((_permil * count(self))/uint(100)) + uint(5)) / uint(10); + return atRank(self,findRank); + } + function median(Tree storage self) internal view returns(uint value) { + return atPercentile(self,50); + } + function below(Tree storage self, uint value) internal view returns(uint _below) { + if(count(self) > 0 && value > 0) _below = rank(self,value)-uint(1); + } + function above(Tree storage self, uint value) internal view returns(uint _above) { + if(count(self) > 0) _above = count(self)-rank(self,value); + } + function rank(Tree storage self, uint value) internal view returns(uint _rank) { + if(count(self) > 0) { + bool finished; + uint cursor = self.root; + Node storage c = self.nodes[cursor]; + uint smaller = getNodeCount(self,c.left); + while (!finished) { + uint keyCount = c.keys.length; + if(cursor == value) { + finished = true; + } else { + if(cursor < value) { + cursor = c.right; + c = self.nodes[cursor]; + smaller += keyCount + getNodeCount(self,c.left); + } else { + cursor = c.left; + c = self.nodes[cursor]; + smaller -= (keyCount + getNodeCount(self,c.right)); + } + } + if (!exists(self,cursor)) { + finished = true; + } + } + return smaller + 1; + } + } + function atRank(Tree storage self, uint _rank) internal view returns(uint _value) { + bool finished; + uint cursor = self.root; + Node storage c = self.nodes[cursor]; + // Case when only one node exist + if (c.parent == 0 && c.left == 0 && c.right == 0) { + _value = cursor; + return _value; + } + uint smaller = getNodeCount(self,c.left); + while (!finished) { + _value = cursor; + c = self.nodes[cursor]; + uint keyCount = c.keys.length; + if(smaller + 1 >= _rank && smaller + keyCount <= _rank) { + _value = cursor; + finished = true; + } else { + if(smaller + keyCount <= _rank) { + cursor = c.right; + c = self.nodes[cursor]; + smaller += keyCount + getNodeCount(self,c.left); + } else { + cursor = c.left; + c = self.nodes[cursor]; + smaller -= (keyCount + getNodeCount(self,c.right)); + } + } + if (!exists(self,cursor)) { + finished = true; + } + } + } + function insert(Tree storage self, bytes32 key, uint value) internal { + require(value != EMPTY, "OrderStatisticsTree(405) - Value to insert cannot be zero"); + require(! keyExists(self,key,value), "OrderStatisticsTree(406) - Value and Key pair exists. Cannot be inserted again."); + uint cursor; + uint probe = self.root; + while (probe != EMPTY) { + cursor = probe; + if (value < probe) { + probe = self.nodes[probe].left; + } else if (value > probe) { + probe = self.nodes[probe].right; + } else if (value == probe) { + self.nodes[probe].keys.push(key); + self.nodes[probe].keyMap[key] = self.nodes[probe].keys.length-uint256(1); + return; + } + self.nodes[cursor].count++; + } + Node storage nValue = self.nodes[value]; + nValue.parent = cursor; + nValue.left = EMPTY; + nValue.right = EMPTY; + nValue.red = true; + nValue.keys.push(key); + nValue.keyMap[key] = nValue.keys.length - uint256(1); + if (cursor == EMPTY) { + self.root = value; + } else if (value < cursor) { + self.nodes[cursor].left = value; + } else { + self.nodes[cursor].right = value; + } + insertFixup(self, value); + } + function remove(Tree storage self, bytes32 key, uint value) internal { + require(value != EMPTY, "OrderStatisticsTree(407) - Value to delete cannot be zero"); + require(keyExists(self,key,value), "OrderStatisticsTree(408) - Value to delete does not exist."); + Node storage nValue = self.nodes[value]; + uint rowToDelete = nValue.keyMap[key]; + + // Remove key from array. In Solidity only last array member + // can be delete. So we need some replace logic. + // But if there is only one array member we dont need any replacing + // and can safe some gas + if (nValue.keys.length > 1) { + // 1. First just replace key at delete index with last array key + nValue.keys[rowToDelete] = nValue.keys[nValue.keys.length -uint256(1)]; + // 2. Save new array index for just replaced key in mapping + //nValue.keyMap[nValue.keys[nValue.keys.length - uint256(1)]]=rowToDelete; + nValue.keyMap[nValue.keys[rowToDelete]]=rowToDelete; + } + // 3. Remove last array key + nValue.keys.pop(); + // 4. Clean mapping for deleted key + delete nValue.keyMap[key]; + + uint probe; + uint cursor; + if(nValue.keys.length == 0) { + if (self.nodes[value].left == EMPTY || self.nodes[value].right == EMPTY) { + cursor = value; + } else { + cursor = self.nodes[value].right; + while (self.nodes[cursor].left != EMPTY) { + cursor = self.nodes[cursor].left; + } + } + if (self.nodes[cursor].left != EMPTY) { + probe = self.nodes[cursor].left; + } else { + probe = self.nodes[cursor].right; + } + uint cursorParent = self.nodes[cursor].parent; + self.nodes[probe].parent = cursorParent; + if (cursorParent != EMPTY) { + if (cursor == self.nodes[cursorParent].left) { + self.nodes[cursorParent].left = probe; + } else { + self.nodes[cursorParent].right = probe; + } + } else { + self.root = probe; + } + bool doFixup = !self.nodes[cursor].red; + if (cursor != value) { + replaceParent(self, cursor, value); + self.nodes[cursor].left = self.nodes[value].left; + self.nodes[self.nodes[cursor].left].parent = cursor; + self.nodes[cursor].right = self.nodes[value].right; + self.nodes[self.nodes[cursor].right].parent = cursor; + self.nodes[cursor].red = self.nodes[value].red; + (cursor, value) = (value, cursor); + fixCountRecurse(self, value); + } + if (doFixup) { + removeFixup(self, probe); + } + fixCountRecurse(self, cursorParent); + delete self.nodes[cursor]; + } + } + function fixCountRecurse(Tree storage self, uint value) private { + while (value != EMPTY) { + self.nodes[value].count = getNodeCount(self,self.nodes[value].left) + getNodeCount(self,self.nodes[value].right); + value = self.nodes[value].parent; + } + } + function treeMinimum(Tree storage self, uint value) private view returns (uint) { + while (self.nodes[value].left != EMPTY) { + value = self.nodes[value].left; + } + return value; + } + function treeMaximum(Tree storage self, uint value) private view returns (uint) { + while (self.nodes[value].right != EMPTY) { + value = self.nodes[value].right; + } + return value; + } + function rotateLeft(Tree storage self, uint value) private { + uint cursor = self.nodes[value].right; + uint parent = self.nodes[value].parent; + uint cursorLeft = self.nodes[cursor].left; + self.nodes[value].right = cursorLeft; + if (cursorLeft != EMPTY) { + self.nodes[cursorLeft].parent = value; + } + self.nodes[cursor].parent = parent; + if (parent == EMPTY) { + self.root = cursor; + } else if (value == self.nodes[parent].left) { + self.nodes[parent].left = cursor; + } else { + self.nodes[parent].right = cursor; + } + self.nodes[cursor].left = value; + self.nodes[value].parent = cursor; + self.nodes[value].count = getNodeCount(self,self.nodes[value].left) + getNodeCount(self,self.nodes[value].right); + self.nodes[cursor].count = getNodeCount(self,self.nodes[cursor].left) + getNodeCount(self,self.nodes[cursor].right); + } + function rotateRight(Tree storage self, uint value) private { + uint cursor = self.nodes[value].left; + uint parent = self.nodes[value].parent; + uint cursorRight = self.nodes[cursor].right; + self.nodes[value].left = cursorRight; + if (cursorRight != EMPTY) { + self.nodes[cursorRight].parent = value; + } + self.nodes[cursor].parent = parent; + if (parent == EMPTY) { + self.root = cursor; + } else if (value == self.nodes[parent].right) { + self.nodes[parent].right = cursor; + } else { + self.nodes[parent].left = cursor; + } + self.nodes[cursor].right = value; + self.nodes[value].parent = cursor; + self.nodes[value].count = getNodeCount(self,self.nodes[value].left) + getNodeCount(self,self.nodes[value].right); + self.nodes[cursor].count = getNodeCount(self,self.nodes[cursor].left) + getNodeCount(self,self.nodes[cursor].right); + } + function insertFixup(Tree storage self, uint value) private { + uint cursor; + while (value != self.root && self.nodes[self.nodes[value].parent].red) { + uint valueParent = self.nodes[value].parent; + if (valueParent == self.nodes[self.nodes[valueParent].parent].left) { + cursor = self.nodes[self.nodes[valueParent].parent].right; + if (self.nodes[cursor].red) { + self.nodes[valueParent].red = false; + self.nodes[cursor].red = false; + self.nodes[self.nodes[valueParent].parent].red = true; + value = self.nodes[valueParent].parent; + } else { + if (value == self.nodes[valueParent].right) { + value = valueParent; + rotateLeft(self, value); + } + valueParent = self.nodes[value].parent; + self.nodes[valueParent].red = false; + self.nodes[self.nodes[valueParent].parent].red = true; + rotateRight(self, self.nodes[valueParent].parent); + } + } else { + cursor = self.nodes[self.nodes[valueParent].parent].left; + if (self.nodes[cursor].red) { + self.nodes[valueParent].red = false; + self.nodes[cursor].red = false; + self.nodes[self.nodes[valueParent].parent].red = true; + value = self.nodes[valueParent].parent; + } else { + if (value == self.nodes[valueParent].left) { + value = valueParent; + rotateRight(self, value); + } + valueParent = self.nodes[value].parent; + self.nodes[valueParent].red = false; + self.nodes[self.nodes[valueParent].parent].red = true; + rotateLeft(self, self.nodes[valueParent].parent); + } + } + } + self.nodes[self.root].red = false; + } + function replaceParent(Tree storage self, uint a, uint b) private { + uint bParent = self.nodes[b].parent; + self.nodes[a].parent = bParent; + if (bParent == EMPTY) { + self.root = a; + } else { + if (b == self.nodes[bParent].left) { + self.nodes[bParent].left = a; + } else { + self.nodes[bParent].right = a; + } + } + } + function removeFixup(Tree storage self, uint value) private { + uint cursor; + while (value != self.root && !self.nodes[value].red) { + uint valueParent = self.nodes[value].parent; + if (value == self.nodes[valueParent].left) { + cursor = self.nodes[valueParent].right; + if (self.nodes[cursor].red) { + self.nodes[cursor].red = false; + self.nodes[valueParent].red = true; + rotateLeft(self, valueParent); + cursor = self.nodes[valueParent].right; + } + if (!self.nodes[self.nodes[cursor].left].red && !self.nodes[self.nodes[cursor].right].red) { + self.nodes[cursor].red = true; + value = valueParent; + } else { + if (!self.nodes[self.nodes[cursor].right].red) { + self.nodes[self.nodes[cursor].left].red = false; + self.nodes[cursor].red = true; + rotateRight(self, cursor); + cursor = self.nodes[valueParent].right; + } + self.nodes[cursor].red = self.nodes[valueParent].red; + self.nodes[valueParent].red = false; + self.nodes[self.nodes[cursor].right].red = false; + rotateLeft(self, valueParent); + value = self.root; + } + } else { + cursor = self.nodes[valueParent].left; + if (self.nodes[cursor].red) { + self.nodes[cursor].red = false; + self.nodes[valueParent].red = true; + rotateRight(self, valueParent); + cursor = self.nodes[valueParent].left; + } + if (!self.nodes[self.nodes[cursor].right].red && !self.nodes[self.nodes[cursor].left].red) { + self.nodes[cursor].red = true; + value = valueParent; + } else { + if (!self.nodes[self.nodes[cursor].left].red) { + self.nodes[self.nodes[cursor].right].red = false; + self.nodes[cursor].red = true; + rotateLeft(self, cursor); + cursor = self.nodes[valueParent].left; + } + self.nodes[cursor].red = self.nodes[valueParent].red; + self.nodes[valueParent].red = false; + self.nodes[self.nodes[cursor].left].red = false; + rotateRight(self, valueParent); + value = self.root; + } + } + } + self.nodes[value].red = false; + } +} \ No newline at end of file diff --git a/contracts/mock/MockHOSTV1.sol b/contracts/mock/MockHOSTV1.sol new file mode 100644 index 0000000..b5cf254 --- /dev/null +++ b/contracts/mock/MockHOSTV1.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "../HOSTLib.sol"; + +contract MockHOSTV1 { + using HitchensOrderStatisticsTreeLibV1 for HitchensOrderStatisticsTreeLibV1.Tree; + + HitchensOrderStatisticsTreeLibV1.Tree tree; + + event Log(string action, bytes32 key, uint value); + + constructor() { + } + function treeRootNode() public view returns (uint _value) { + _value = tree.root; + } + function firstValue() public view returns (uint _value) { + _value = tree.first(); + } + function lastValue() public view returns (uint _value) { + _value = tree.last(); + } + function nextValue(uint value) public view returns (uint _value) { + _value = tree.next(value); + } + function prevValue(uint value) public view returns (uint _value) { + _value = tree.prev(value); + } + function valueExists(uint value) public view returns (bool _exists) { + _exists = tree.exists(value); + } + function keyValueExists(bytes32 key, uint value) public view returns(bool _exists) { + _exists = tree.keyExists(key, value); + } + function getNode(uint value) public view returns (uint _parent, uint _left, uint _right, bool _red, uint _keyCount, uint _count) { + (_parent, _left, _right, _red, _keyCount, _count) = tree.getNode(value); + } + + function getNode2(uint value) public view returns (uint _parent, uint _left, uint _right, bool _red, uint _keyCount, uint _count) { + HitchensOrderStatisticsTreeLibV1.Node storage node = tree.getNode2(value); + _parent = node.parent; + _left = node.left; + _right = node.right; + _red = node.red; + _keyCount = node.keys.length; + _count = node.count; + } + + function getNodeKeysCount(uint256 value) public view returns(uint256 keysCount) { + return tree.getNodeKeysLength(value); + } + function getValueKey(uint value, uint row) public view returns(bytes32 _key) { + _key = tree.valueKeyAtIndex(value,row); + } + function valueKeyCount() public view returns(uint _count) { + _count = tree.count(); + } + function valuePercentile(uint value) public view returns(uint _percentile) { + _percentile = tree.percentile(value); + } + function valuePermil(uint value) public view returns(uint _permil) { + _permil = tree.permil(value); + } + function valueAtPercentile(uint _percentile) public view returns(uint _value) { + _value = tree.atPercentile(_percentile); + } + function valueAtPermil(uint value) public view returns(uint _value) { + _value = tree.atPermil(value); + } + function medianValue() public view returns(uint _value) { + return tree.median(); + } + function valueRank(uint value) public view returns(uint _rank) { + _rank = tree.rank(value); + } + function valuesBelow(uint value) public view returns(uint _below) { + _below = tree.below(value); + } + function valuesAbove(uint value) public view returns(uint _above) { + _above = tree.above(value); + } + function valueAtRank(uint _rank) public view returns(uint _value) { + _value = tree.atRank(_rank); + } + function valueAtRankReverse(uint _rank) public view returns(uint _value) { + _value = tree.atRank(tree.count() - (_rank - 1)); + } + function valueRankReverse(uint value) public view returns(uint _rank) { + _rank = tree.count() - (tree.rank(value) - 1); + } + function insertKeyValue(bytes32 key, uint value) public { + emit Log("insert", key, value); + tree.insert(key, value); + } + function removeKeyValue(bytes32 key, uint value) public { + emit Log("delete", key, value); + tree.remove(key, value); + } + + // Mock functions + function mock1(uint256 p1) public view returns(uint256) { + return (((p1 * tree.getNodeCount(tree.root))/uint(100)) + uint(5)) / uint(10); + } + + // Mock functions + function mock2(uint256 p1, uint256 p2) public view returns(uint256) { + return (((p1 * p2)/uint(100)) + uint(5)) / uint(10); + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ec33f44 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.pytest.ini_options] +log_cli = true +log_cli_level = "INFO" +log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" +log_cli_date_format = "%Y-%m-%d %H:%M:%S" \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1033e09 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from brownie._config import CONFIG + + +pytest_plugins = [ + #"fixtures.conftest", + "fixtures.accounts", + "fixtures.deploy_env" + ] + +@pytest.fixture(scope="session") +def is_forked(): + yield "fork" in CONFIG.active_network['id'] diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/accounts.py b/tests/fixtures/accounts.py new file mode 100644 index 0000000..cec2edb --- /dev/null +++ b/tests/fixtures/accounts.py @@ -0,0 +1,16 @@ +import pytest + + +@pytest.fixture(scope="session") +def alice(accounts): + yield accounts[0] + + +@pytest.fixture(scope="session") +def bob(accounts): + yield accounts[1] + + +@pytest.fixture(scope="session") +def charlie(accounts): + yield accounts[2] diff --git a/tests/fixtures/deploy_env.py b/tests/fixtures/deploy_env.py new file mode 100644 index 0000000..0711e59 --- /dev/null +++ b/tests/fixtures/deploy_env.py @@ -0,0 +1,10 @@ +import pytest +#from brownie import chain + +############ Mocks ######################## +@pytest.fixture(scope="module") +def treeMock(accounts, MockHOSTV1, HitchensOrderStatisticsTreeLibV1): + #lib = accounts[0].deploy(HitchensOrderStatisticsTreeLibV1) + mock = accounts[0].deploy(MockHOSTV1) + yield mock + diff --git a/tests/test_mock_tree_01.py b/tests/test_mock_tree_01.py new file mode 100644 index 0000000..28103ac --- /dev/null +++ b/tests/test_mock_tree_01.py @@ -0,0 +1,112 @@ +import pytest +import logging +from brownie import chain, Wei, reverts +LOGGER = logging.getLogger(__name__) +from web3 import Web3 + +PRICES_0 = [10000,11111,22222, 33333, 44444] +PRICES_1 = [10, 100, 110, 200, 300] +KEYS_0 = [ + 0x0000000000000000000000000000000000000000000000000000000000000001, + 0x0000000000000000000000000000000000000000000000000000000000000002, + 0x0000000000000000000000000000000000000000000000000000000000000003, + 0x0000000000000000000000000000000000000000000000000000000000000004, + 0x0000000000000000000000000000000000000000000000000000000000000005 +] + + +def test_insert(accounts, treeMock): + with reverts("OrderStatisticsTree(401) - Starting value cannot be zero"): + treeMock.nextValue(0) + with reverts("OrderStatisticsTree(405) - Value to insert cannot be zero"): + treeMock.insertKeyValue(KEYS_0[0],0, {"from": accounts[0]}) + with reverts("OrderStatisticsTree(404) - Value does not exist."): + treeMock.getValueKey(77777,0) + assert treeMock.valueRank( KEYS_0[0]) == 0 + assert treeMock.valuesBelow(KEYS_0[0]) == 0 + treeMock.insertKeyValue(KEYS_0[0], PRICES_0[0], {"from": accounts[0]}) + with reverts("OrderStatisticsTree(406) - Value and Key pair exists. Cannot be inserted again."): + treeMock.insertKeyValue(KEYS_0[0], PRICES_0[0], {"from": accounts[0]}) + assert treeMock.valueKeyCount() == 1 + assert treeMock.medianValue() == PRICES_0[0] + logging.info('Median after 1 insert:{}'.format(treeMock.medianValue())) + logging.info( + 'Node({}):parent = {}, left = {}, right = {}, isRed = {}, keyCount = {}, count = {}'.format( + PRICES_0[0], + treeMock.getNode2(PRICES_0[0])[0], + treeMock.getNode2(PRICES_0[0])[1], + treeMock.getNode2(PRICES_0[0])[2], + treeMock.getNode2(PRICES_0[0])[3], + treeMock.getNode2(PRICES_0[0])[4], + treeMock.getNode2(PRICES_0[0])[5], + )) + +def test_feed_one_key_seven_prices(accounts, treeMock): + [treeMock.insertKeyValue(KEYS_0[0], x, {"from": accounts[0]}) for x in PRICES_1] + assert treeMock.valueKeyCount() == 6 + assert treeMock.medianValue() == PRICES_1[2] + logging.info('Median after PRICES_1 insert:{}'.format(treeMock.medianValue())) + for i in PRICES_1: + logging.info( + 'Node({:5d}):parent = {:6d}, left = {:6d}, right = {:6d}, isRed = {:<}, keyCount = {:3d}, count = {:3d}'.format( + i, + treeMock.getNode2(i)[0], + treeMock.getNode2(i)[1], + treeMock.getNode2(i)[2], + treeMock.getNode2(i)[3], + treeMock.getNode2(i)[4], + treeMock.getNode2(i)[5], + )) + with reverts("OrderStatisticsTree(403) - Value does not exist."): + treeMock.getNode2(777777777) + +def test_feed_five_keys_one_price(accounts, treeMock): + [treeMock.insertKeyValue(x, PRICES_0[4], {"from": accounts[0]}) for x in KEYS_0] + feed_count = treeMock.valueKeyCount() + assert feed_count == 11 + + logging.info('Median after same price insert:{}'.format(treeMock.medianValue())) + list_from_tree = [] + #list_from_tree.append(treeMock.firstValue()) + #logging.info('First value:{}'.format(list_from_tree[0])) + for x in range(feed_count): + val_at_rank = treeMock.valueAtRank(x+1) + list_from_tree.append(val_at_rank) + logging.info('Tree:value at rank( {:2g}): {:5d}, keys count {}, Above {}, Below {}'.format( + #list_from_tree[x], + #treeMock.valueRank(list_from_tree[x]), + x+1, + val_at_rank, + treeMock.getNodeKeysCount(val_at_rank), + treeMock.valuesBelow(val_at_rank), + treeMock.valuesAbove(val_at_rank) + )) + assert treeMock.getNode2(val_at_rank)[5] + treeMock.getNode2(val_at_rank)[4] == treeMock.getNode(treeMock.valueAtRank(x+1))[5] + if treeMock.getNodeKeysCount(val_at_rank) == 1: + assert treeMock.valueRank(val_at_rank) == x + 1 + logging.info('Price list from tree:{}'.format(list_from_tree)) + assert treeMock.medianValue() == list_from_tree[5] + assert treeMock.valueExists(777777777777) == False + assert treeMock.firstValue() == list_from_tree[0] + assert treeMock.lastValue() == list_from_tree[-1] + for x in range(len(list_from_tree)): + logging.info('list_from_tree[{}] = {}'.format(x,list_from_tree[x])) + logging.info( + 'Node({:5d}):parent = {:6d}, left = {:6d}, right = {:6d}, isRed = {:<}, keyCount = {:3d}, count = {:3d}'.format( + x, + treeMock.getNode2(list_from_tree[x])[0], + treeMock.getNode2(list_from_tree[x])[1], + treeMock.getNode2(list_from_tree[x])[2], + treeMock.getNode2(list_from_tree[x])[3], + treeMock.getNode2(list_from_tree[x])[4], + treeMock.getNode2(list_from_tree[x])[5], + )) + logging.info(treeMock.getValueKey(list_from_tree[x],0)) + + if x > 0: + if treeMock.getNodeKeysCount(list_from_tree[x]) == 1: + assert treeMock.prevValue(list_from_tree[x]) == list_from_tree[x - 1] + assert treeMock.valueRank(list_from_tree[x]) == x + 1 + assert treeMock.valuesBelow(list_from_tree[x]) == x + assert treeMock.nextValue(list_from_tree[x]) == list_from_tree[x + 1] + diff --git a/tests/test_mock_tree_02.py b/tests/test_mock_tree_02.py new file mode 100644 index 0000000..2a3278e --- /dev/null +++ b/tests/test_mock_tree_02.py @@ -0,0 +1,38 @@ +import pytest +import logging +from brownie import chain, Wei, reverts +LOGGER = logging.getLogger(__name__) +from web3 import Web3 + +PRICES_0 = [10000,10000,10000] +KEYS_0 = [ + 0x0000000000000000000000000000000000000000000000000000000000000001, + 0x0000000000000000000000000000000000000000000000000000000000000002, + 0x0000000000000000000000000000000000000000000000000000000000000003, +] + + +def test_feed_one_price(accounts, treeMock): + [treeMock.insertKeyValue(x, PRICES_0[0], {"from": accounts[0]}) for x in KEYS_0] + feed_count = treeMock.valueKeyCount() + assert feed_count == 3 + #logging.info('Median after same price insert:{}'.format(treeMock.medianValue())) + list_from_tree = [] + list_from_tree.append(treeMock.firstValue()) + logging.info('Rank of {} is {}'.format(PRICES_0[0], treeMock.valueRank(PRICES_0[0]))) + logging.info('median Rank after insert 3 same values:{}'.format(treeMock.mock1(50))) + logging.info( + 'Node({}):parent = {}, left = {}, right = {}, isRed = {}, keyCount = {}, count = {}'.format( + PRICES_0[0], + treeMock.getNode2(PRICES_0[0])[0], + treeMock.getNode2(PRICES_0[0])[1], + treeMock.getNode2(PRICES_0[0])[2], + treeMock.getNode2(PRICES_0[0])[3], + treeMock.getNode2(PRICES_0[0])[4], + treeMock.getNode2(PRICES_0[0])[5], + )) + for x in range (1,100): + logging.info('median Rank from {} values:{}'.format(x,treeMock.mock2(50,x))) + + logging.info('Price list from tree:{}'.format(list_from_tree)) + diff --git a/tests/test_mock_tree_03.py b/tests/test_mock_tree_03.py new file mode 100644 index 0000000..125ff40 --- /dev/null +++ b/tests/test_mock_tree_03.py @@ -0,0 +1,66 @@ +import pytest +import logging +from brownie import chain, Wei, reverts +LOGGER = logging.getLogger(__name__) +from web3 import Web3 + +PRICES_0 = [10000,11111,22222, 33333, 44444] +PRICES_1 = [10, 100, 110, 200, 300] +KEYS_0 = [ + 0x0000000000000000000000000000000000000000000000000000000000000001, + 0x0000000000000000000000000000000000000000000000000000000000000002, + 0x0000000000000000000000000000000000000000000000000000000000000003, + 0x0000000000000000000000000000000000000000000000000000000000000004, + 0x0000000000000000000000000000000000000000000000000000000000000005, + 0x0000000000000000000000000000000000000000000000000000000000000006, + 0x0000000000000000000000000000000000000000000000000000000000000007, + 0x0000000000000000000000000000000000000000000000000000000000000008, + 0x0000000000000000000000000000000000000000000000000000000000000009, + 0x000000000000000000000000000000000000000000000000000000000000000a, + 0x000000000000000000000000000000000000000000000000000000000000000b, + 0x000000000000000000000000000000000000000000000000000000000000000c, + 0x000000000000000000000000000000000000000000000000000000000000000d, +] + + +def test_feed_one_price(accounts, treeMock): + for x in KEYS_0: + treeMock.insertKeyValue(x, PRICES_0[4], {"from": accounts[0]}) + logging.info('Tree root: {}'.format(treeMock.treeRootNode())) + logging.info( + 'Node({}):parent = {}, left = {}, right = {}, isRed = {}, keyCount = {}, count = {}'.format( + PRICES_0[4], + treeMock.getNode2(PRICES_0[4])[0], + treeMock.getNode2(PRICES_0[4])[1], + treeMock.getNode2(PRICES_0[4])[2], + treeMock.getNode2(PRICES_0[4])[3], + treeMock.getNode2(PRICES_0[4])[4], + treeMock.getNode2(PRICES_0[4])[5], + )) + logging.info('Median after same price insert:{}'.format(treeMock.medianValue())) + logging.info('median Rank from {} values:{}'.format(treeMock.getNode2(PRICES_0[4])[4],treeMock.mock1(50))) + + x = 1 + logging.info('Next value:, value at rank({}):{}, keys count {}'.format( + x, + treeMock.valueAtRank(x), + treeMock.getNodeKeysCount( treeMock.valueAtRank(x)) + )) + #logging.info('Median after same price insert:{}'.format(treeMock.medianValue())) + # feed_count = treeMock.valueKeyCount() + # assert feed_count == 11 + # logging.info('Median after same price insert:{}'.format(treeMock.medianValue())) + # list_from_tree = [] + # list_from_tree.append(treeMock.firstValue()) + # #logging.info('First value:{}'.format(list_from_tree[0])) + # for x in range(feed_count): + # #st_from_tree.append(treeMock.nextValue(list_from_tree[x])) + # logging.info('Next value:, value at rank({}):{}, keys count {}'.format( + # #list_from_tree[x], + # #treeMock.valueRank(list_from_tree[x]), + # x+1, + # treeMock.valueAtRank(x+1), + # treeMock.getNodeKeysCount( treeMock.valueAtRank(x+1)) + # )) + # logging.info('Price list from tree:{}'.format(list_from_tree)) + diff --git a/tests/test_mock_tree_04.py b/tests/test_mock_tree_04.py new file mode 100644 index 0000000..870a37c --- /dev/null +++ b/tests/test_mock_tree_04.py @@ -0,0 +1,94 @@ +import pytest +import logging +from brownie import chain, Wei, reverts +LOGGER = logging.getLogger(__name__) +from web3 import Web3 +from random import randrange + +PRICES_0 = [randrange(1e6, 3e6) for x in range(21)] +KEYS_0 = [ + 0x0000000000000000000000000000000000000000000000000000000000000001, + 0x0000000000000000000000000000000000000000000000000000000000000002 +] + + +def test_insert_random_feed(accounts, treeMock): + for x in PRICES_0: + treeMock.insertKeyValue(KEYS_0[0], x, {"from": accounts[0]}) + logging.info('NodeCount {:3d}, root: {:7d}, median ={:7d}, after insert {:7d}'.format( + treeMock.valueKeyCount(), + treeMock.treeRootNode(), + treeMock.medianValue(), + x + )) + #insert second key to one of node + treeMock.insertKeyValue(KEYS_0[1], PRICES_0[1], {"from": accounts[0]}) + + # Lets sort original array + PRICES_0.sort() + median_rank = len(PRICES_0)//2 + len(PRICES_0)%2 + + logging.info('Original sorted array with len={} has median:{}'.format( + len(PRICES_0), + PRICES_0[median_rank - 1] + )) + assert treeMock.valueKeyCount() == len(PRICES_0) +1 + + +def test_remove_from_top(accounts, treeMock): + with reverts("OrderStatisticsTree(407) - Value to delete cannot be zero"): + treeMock.removeKeyValue(KEYS_0[0], 0, {"from": accounts[0]}) + with reverts("OrderStatisticsTree(408) - Value to delete does not exist."): + treeMock.removeKeyValue(KEYS_0[0], PRICES_0[0]+1, {"from": accounts[0]}) + + tx = treeMock.removeKeyValue(KEYS_0[0], PRICES_0[0], {"from": accounts[0]}) + assert treeMock.valueExists(PRICES_0[0]) == False + + logging.info('NodeCount {:3d}, root: {:7d}, median ={:7d}, after remove {:7d}'.format( + treeMock.valueKeyCount(), + treeMock.treeRootNode(), + treeMock.medianValue(), + PRICES_0[0] + )) + PRICES_0.remove(PRICES_0[0]) + + PRICES_0.sort() + median_rank = len(PRICES_0)//2 + len(PRICES_0)%2 + assert treeMock.valueKeyCount() == len(PRICES_0) +1 + +def test_remove_from_end(accounts, treeMock): + tx = treeMock.removeKeyValue(KEYS_0[0], PRICES_0[-1], {"from": accounts[0]}) + assert treeMock.valueExists(PRICES_0[-1]) == False + + logging.info('NodeCount {:3d}, root: {:7d}, median ={:7d}, after remove {:7d}'.format( + treeMock.valueKeyCount(), + treeMock.treeRootNode(), + treeMock.medianValue(), + PRICES_0[-1] + )) + PRICES_0.remove(PRICES_0[-1]) + PRICES_0.sort() + #median_rank = len(PRICES_0)//2 + len(PRICES_0)%2 + assert treeMock.valueKeyCount() == len(PRICES_0) +1 + +def test_remove_from_center(accounts, treeMock): + median_rank = len(PRICES_0)//2 + len(PRICES_0)%2 + tx = treeMock.removeKeyValue(KEYS_0[0], PRICES_0[median_rank], {"from": accounts[0]}) + assert treeMock.valueExists(PRICES_0[median_rank]) == False + + logging.info('NodeCount {:3d}, root: {:7d}, median ={:7d}, after remove {:7d}'.format( + treeMock.valueKeyCount(), + treeMock.treeRootNode(), + treeMock.medianValue(), + PRICES_0[median_rank] + )) + PRICES_0.remove(PRICES_0[median_rank]) + PRICES_0.sort() + assert treeMock.valueKeyCount() == len(PRICES_0)+1 + +def test_remove_from_center(accounts, treeMock): + for x in PRICES_0: + tx = treeMock.removeKeyValue(KEYS_0[0], x, {"from": accounts[0]}) + if treeMock.valueExists(x): + treeMock.removeKeyValue(KEYS_0[1], x, {"from": accounts[0]}) +