├── .gas-snapshot ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── foundry.toml ├── slither.config.json ├── src └── LibDDRV.sol └── test ├── LibDDRV.feature ├── LibDDRV.invariant.t.sol ├── LibDDRV.montecarlo.t.sol ├── LibDDRV.unit.t.sol └── utils ├── ContractUnderTest.sol ├── Handler.sol ├── LibDDRVUtils.sol └── Math.sol /.gas-snapshot: -------------------------------------------------------------------------------- 1 | TestContract:testBar() (gas: 401) 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | 11 | jobs: 12 | run-ci: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install Foundry 18 | uses: foundry-rs/foundry-toolchain@v1 19 | with: 20 | version: nightly 21 | 22 | - name: Install deps 23 | run: forge install 24 | 25 | - name: Check gas snapshots 26 | run: forge snapshot --check 27 | 28 | - name: Run tests 29 | run: forge test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | broadcast/ 3 | cache/ 4 | node_modules/ 5 | .venv/ 6 | package-lock.json 7 | .env 8 | .idea 9 | .vscode/ 10 | lcov.info 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solmate"] 5 | path = lib/solmate 6 | url = https://github.com/transmissions11/solmate 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2023 Valorem Labs Inc. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibDDRV 2 | 3 | ![Github Actions](https://github.com/valorem-labs-inc/LibDDRV/workflows/CI/badge.svg) 4 | 5 | Library for generating discrete random variates from a set of dynamically weighted elements in Solidity. 6 | 7 | Based on [this paper](https://kuscholarworks.ku.edu/bitstream/handle/1808/7224/MVN03.dynamic_rv_gen.pdf). 8 | 9 | The algorithm preprocesses a list of weighted elements into a forest of trees data structure, and then 10 | traverses that forest to generate a random variate from the discrete distribution in sublinear time. 11 | Critically, the library supports inserting, updating, and deleting elements from the forest. 12 | 13 | _more to come_ 14 | 15 | ## Getting Started 16 | 17 | ## Contributing 18 | 19 | ## Security 20 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | remappings = ['forge-std/=lib/forge-std/src/', 'solmate/=lib/solmate/src/'] 3 | 4 | [profile.ci.fuzz] 5 | runs = 10_000 6 | 7 | [fuzz] 8 | runs = 10 9 | 10 | [invariant] 11 | runs = 10 12 | depth = 15 13 | fail_on_revert = true 14 | dictionary_weight = 80 15 | include_storage = true 16 | include_push_bytes = true 17 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "solc_args": "optimize", 3 | "solc_remaps": [ 4 | "forge-std/=lib/forge-std/src/" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/LibDDRV.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Library for generating discrete random variates from a set of dynamically weighted elements. 5 | * 6 | * Copyright © 2023 Valorem Labs Inc. 7 | * 8 | * @author 0xAlcibiades 9 | * @author neodaoist 10 | * @author Flip-Liquid 11 | */ 12 | 13 | pragma solidity >=0.8.8; 14 | 15 | import "forge-std/console.sol"; 16 | 17 | // Associated storage structures 18 | 19 | // Represents an edge in the forest of trees 20 | struct Edge { 21 | uint256 level; 22 | uint256 index; 23 | } 24 | 25 | // Represents a node in the forest of trees 26 | struct Node { 27 | uint256 index; 28 | uint256 weight; 29 | Edge[] children; 30 | } 31 | 32 | // Represents an enqueued update operation for the given index 33 | struct NodeUpdate { 34 | uint256 index; 35 | int256 delta; 36 | } 37 | 38 | // A level of the canopy in the forest of trees 39 | struct Level { 40 | uint256 weight; 41 | uint256 roots; 42 | mapping(uint256 => Node) ranges; // QUESTION should we rename this to nodes, bc they are Elements on Level 0? 43 | } 44 | 45 | // A struct representing the whole forest 46 | struct Forest { 47 | uint256 weight; 48 | mapping(uint256 => Level) levels; 49 | } 50 | 51 | // A struct representing a FILO queue of ranges to be processed 52 | struct Queue { 53 | // points to the reserved word at the head of the queue to indicate 54 | // which ranges have been enqueued; enqueue_range is skipped if the 55 | // range is already enqueued 56 | bytes32 ptr; 57 | // points to the last element in the FILO queue 58 | bytes32 head; 59 | // points to the first element in the FILO queue; items will be enqueued 60 | // and poppoed off at this location 61 | bytes32 tail; 62 | } 63 | 64 | // A library which takes in weights and uniform random variates and uses them 65 | // to generate dynamically weighted discrete probability mass function random 66 | // variates. 67 | library LibDDRV { 68 | uint256 private constant fp = 0x40; 69 | uint256 private constant word = 0x20; 70 | // The maximum number of ranges that can be enqueued at any given time for a level update 71 | // In the worst case, a single update to an element can cause 2^L updates to parent ranges, 72 | // where L is the maximum number of levels in the forest. This max queue size 73 | uint256 private constant MAX_QUEUE_SIZE = 32; 74 | 75 | // Preprocess an array of elements and their weights into a forest of trees. 76 | // TODO(This presently supports natural number weights, could easily support posits) 77 | function preprocess(Forest storage forest, uint256[] calldata weights) external { 78 | (bytes32 ptr, bytes32 head, bytes32 tail) = new_queue(); 79 | 80 | uint256 n = weights.length; 81 | uint256 weight; 82 | uint256 i; 83 | uint256 j; 84 | for (i = 0; i < n; i++) { 85 | weight = weights[i]; 86 | j = floor_ilog(weight) + 1; 87 | Node storage element = forest.levels[0].ranges[i]; 88 | 89 | // Add this index to table level zero. 90 | element.weight = weight; 91 | element.index = i; 92 | forest.levels[0].weight += weight; 93 | 94 | // insert this element into the 1st-level range at index j 95 | Node storage destRange = insert_range(forest, element, 0, 1, j); 96 | 97 | // Update the forest weight overall TODO: duplicated in level[0].weight 98 | forest.weight += weight; 99 | 100 | // TODO: verify skip if range already enqueued 101 | tail = enqueue_range(ptr, head, tail, j, int256(weight)); 102 | } 103 | 104 | assembly { 105 | // Cap off the level queue by incrementing the free memory pointer 106 | mstore(fp, add(tail, word)) 107 | } 108 | 109 | console.log("ptr: %s", uint256(ptr)); 110 | console.log("head: %s", uint256(head)); 111 | console.log("tail: %s", uint256(tail)); 112 | // Construct the forest of trees from the bottom up 113 | update_levels(forest, ptr, head, tail); 114 | } 115 | 116 | // Propogate upwards any changes in the the element or range weights 117 | function update_levels(Forest storage forest, bytes32 ptr, bytes32 head, bytes32 tail) internal { 118 | uint256 l = 1; 119 | 120 | while (head != tail) { 121 | // Set Qₗ₊₁ 122 | (ptr, head, tail) = process_level_queue(forest, l, ptr, head, tail); 123 | // Increment level 124 | l += 1; 125 | } 126 | } 127 | 128 | // Construct a level in the forest of trees 129 | function process_level_queue(Forest storage forest, uint256 level, bytes32 ptr, bytes32 head, bytes32 tail) 130 | internal 131 | returns (bytes32 nextPtr, bytes32 nextHead, bytes32 nextTail) 132 | { 133 | // construct the queue for the next level 134 | (nextPtr, nextHead, nextTail) = new_queue(); 135 | console.log("process ptr: %s", uint256(ptr)); 136 | console.log("process head: %s", uint256(head)); 137 | console.log("process tail: %s", uint256(tail)); 138 | console.log("process nextptr: %s", uint256(nextPtr)); 139 | console.log("process nexthead: %s", uint256(nextHead)); 140 | console.log("process nexttail: %s", uint256(nextTail)); 141 | 142 | console.log("construct level"); 143 | // While Qₗ ≠ ∅ 144 | while (head != tail) { 145 | // Dequeue update 146 | NodeUpdate memory update; 147 | Node storage range; 148 | assembly { 149 | mstore(update, mload(head)) 150 | mstore(add(update, word), mload(add(head, word))) 151 | head := add(mul(word, 2), head) 152 | } 153 | // Get weight and range number 154 | int256 delta = update.delta; 155 | uint256 index = update.index; 156 | 157 | // Get range 158 | range = forest.levels[level].ranges[index]; 159 | 160 | console.log("level: %s", level); 161 | console.log("weight delta: %s", uint256(delta)); 162 | console.log("head: %s", uint256(head)); 163 | console.log("tail: %s", uint256(tail)); 164 | 165 | // TODO(Support expanded degree bound) 166 | if (range.children.length == 0) { 167 | console.log("root"); 168 | // this is a root range with no parent 169 | // add range weight to level; add index to level table (roots) 170 | set_root_range(range, forest.levels[level]); 171 | forest.levels[level].weight = uint256(int256(forest.levels[level].weight) + delta); 172 | } 173 | nextTail = _update_range(forest, range, delta, level + 1, nextHead, nextPtr, nextTail); 174 | } 175 | assembly { 176 | // Cap off the level queue by incrementing the free memory pointer 177 | mstore(fp, add(nextTail, word)) 178 | } 179 | } 180 | 181 | // Insert the range into the forest at level, index, updating edges 182 | function insert_range( 183 | Forest storage forest, 184 | Node storage range, 185 | uint256 srcLevel, 186 | uint256 destLevel, 187 | uint256 destIndex 188 | ) internal returns (Node storage) { 189 | Node storage destRange = forest.levels[destLevel].ranges[destIndex]; 190 | // TODO: Range.index is duplicated here 191 | destRange.index = destIndex; 192 | return insert_range(forest, range, srcLevel, destRange); 193 | } 194 | 195 | // Insert the range into the forest at level, index, edges 196 | function insert_range(Forest storage forest, Node storage range, uint256 srcLevel, Node storage destRange) 197 | internal 198 | returns (Node storage) 199 | { 200 | // Adds an edge from the destination range to the source 201 | Edge memory edge = Edge({level: srcLevel, index: range.index}); 202 | destRange.children.push(edge); 203 | // Range weight is updated in update_range 204 | //destRange.weight += range.weight; 205 | return destRange; 206 | } 207 | 208 | // move the specified range from currentParent to newParent 209 | function move_range( 210 | Forest storage forest, 211 | Node storage range, 212 | uint256 parentLevel, 213 | uint256 j, 214 | uint256 k 215 | ) internal returns (Node storage _newParent) { 216 | uint256 srcLevel = parentLevel - 1; 217 | Node storage currentParent = forest.levels[parentLevel].ranges[j]; 218 | Node storage newParent = forest.levels[parentLevel].ranges[k]; 219 | // if the current parent range is a zero range, then the supplied range 220 | // is being added for the first time, and no operations on the current parent 221 | // range are appropriate 222 | Level storage nextLevel = forest.levels[srcLevel]; 223 | if (currentParent.weight != 0) { 224 | // find and remove the edge to the supplied range in currentParent 225 | for (uint256 i = 0; i < currentParent.children.length; i++) { 226 | if (currentParent.children[i].level == srcLevel && currentParent.children[i].index == range.index) { 227 | currentParent.children[i] = currentParent.children[currentParent.children.length - 1]; 228 | currentParent.children.pop(); 229 | break; 230 | } 231 | } 232 | 233 | // check if current parent is now a root range, updating roots and level weights 234 | if (is_root_range(currentParent, nextLevel)) { 235 | unset_root_range(currentParent, nextLevel); 236 | } 237 | } 238 | 239 | // unset new parent range as a root if it is one currently 240 | if (is_root_range(newParent, nextLevel)) { 241 | unset_root_range(newParent, nextLevel); 242 | nextLevel.weight -= newParent.weight; 243 | } 244 | 245 | // insert range into newParent 246 | _newParent = insert_range(forest, range, srcLevel, newParent); 247 | 248 | // set new parent range as a root if it is one now 249 | if (is_root_range(_newParent, nextLevel)) { 250 | set_root_range(_newParent, nextLevel); 251 | nextLevel.weight += _newParent.weight; 252 | } 253 | } 254 | 255 | function new_queue() internal returns (bytes32 ptr, bytes32 head, bytes32 tail) { 256 | // Set up an in memory queue object 257 | // Qₗ = ∅ OR Qₗ₊₁ = ∅ 258 | assembly { 259 | // Set the queue to the free pointer 260 | ptr := mload(fp) 261 | // Note that Solidity generated IR code reserves memory offset ``0x60`` as well, but a pure Yul object is free to use memory as it chooses. 262 | if iszero(ptr) { ptr := 0x60 } 263 | mstore(fp, add(ptr, fp)) 264 | // One word is reserved here to act as a header for the queue, 265 | // to check if a range is already in the queue. 266 | head := add(ptr, word) 267 | tail := head 268 | } 269 | } 270 | 271 | // Adds a NodeUpdate to the level queue. If an update for a particular range is already in the queue, 272 | // the next update's delta is added to the existing update. i.e. two separate updates for +3 and +5 will 273 | // consolidate to +8. 274 | // TODO: consolidate updates in the queue in a more time efficient way than linear search 275 | function enqueue_range(bytes32 ptr, bytes32 head, bytes32 tail, uint256 j, int256 weightDelta) 276 | internal 277 | returns (bytes32 nextTail) 278 | { 279 | uint256 flags; 280 | assembly { 281 | flags := mload(ptr) 282 | } 283 | console.log(flags); 284 | uint256 existingUpdateIndex = 0; 285 | int256 existingUpdateDelta = 0; 286 | assembly { 287 | // Check if the bit j is set in the header 288 | // The smallest current value of j is 1, corresponding to the half open range 289 | // [1, 2). The reserved word at the front of the queue correspondingly does not 290 | // make use of the 0th bit. 291 | // if the bitwise AND of the the header and 1 shifted to the jth bit is zero, the 292 | // range is already in the queue. 293 | if eq(and(mload(ptr), shl(j, 1)), 0) { 294 | // If it's not, add the range to the queue 295 | // Set the bit j 296 | mstore(ptr, or(shl(j, 1), mload(ptr))) 297 | // Store the range in the queue 298 | mstore(tail, j) 299 | mstore(add(tail, word), weightDelta) 300 | // Update the tail of the queue 301 | nextTail := add(tail, mul(2, word)) 302 | } 303 | // If the range is already contained in the queue, find and update it 304 | if eq(and(mload(ptr), shl(j, 1)), 1) { 305 | // Find the range in the queue 306 | let i := head 307 | let found := 0 308 | for {} and(lt(i, tail), eq(found, 0)) { i := add(i, mul(2, word)) } { 309 | // If the range is found, add the delta to the existing update 310 | existingUpdateIndex := mload(i) 311 | if eq(existingUpdateIndex, j) { 312 | existingUpdateDelta := mload(add(i, word)) 313 | mstore(add(i, word), add(existingUpdateDelta, weightDelta)) 314 | found := 1 315 | } 316 | } 317 | } 318 | if eq(nextTail, 0) { nextTail := tail } 319 | } 320 | } 321 | 322 | // TODO: restrict new weight to int256.max, if the delta is being percolated up in the level queues 323 | // as a signed value 324 | function insert_element(Forest storage forest, uint256 index, uint256 newWeight) external { 325 | // TODO revert if element already exists 326 | update_element(forest, index, newWeight); 327 | } 328 | 329 | // TODO(can this take a list of elements?) 330 | // TODO b-factor 331 | // TODO: can one enqueue 2 levels above or does this mess up the ordering 332 | // TODO: revert if delta > 2 ^128 (for typing on enqueue) 333 | // Update an element's weight in the forest of trees 334 | function update_element(Forest storage forest, uint256 index, uint256 newWeight) public { 335 | // Set up an in memory queue object 336 | (bytes32 ptr, bytes32 head, bytes32 tail) = new_queue(); 337 | _update_element(forest, index, newWeight, ptr, head, tail); 338 | update_levels(forest, ptr, head, tail); 339 | } 340 | 341 | function _update_element( 342 | Forest storage forest, 343 | uint256 index, 344 | uint256 newWeight, 345 | bytes32 ptr, 346 | bytes32 head, 347 | bytes32 tail 348 | ) internal { 349 | // TODO revert if weight is same 350 | Node storage elt = forest.levels[0].ranges[index]; 351 | 352 | uint256 oldWeight = elt.weight; 353 | 354 | // update the forest weight 355 | forest.weight -= oldWeight; 356 | forest.weight += newWeight; 357 | 358 | // update l0 weight 359 | forest.levels[0].weight -= oldWeight; 360 | forest.levels[0].weight += newWeight; 361 | 362 | _update_range(forest, elt, int256(newWeight) - int256(oldWeight), 1, ptr, head, tail); 363 | } 364 | 365 | // Checks the current range to see if it needs to move parents 366 | function _update_range( 367 | Forest storage forest, 368 | Node storage range, 369 | int256 weightDelta, 370 | uint256 parentLevel, 371 | bytes32 ptr, 372 | bytes32 head, 373 | bytes32 tail 374 | ) internal returns (bytes32 nextTail) { 375 | // TODO revert if weight is same 376 | uint256 oldWeight = range.weight; 377 | 378 | // update leaf/element weight 379 | uint256 newWeight = uint256(int256(range.weight) + weightDelta); 380 | range.weight = newWeight; 381 | 382 | // get the current range index 383 | uint256 j = 0; 384 | if (oldWeight > 0) { 385 | j = floor_ilog(oldWeight) + 1; 386 | } 387 | 388 | nextTail = tail; 389 | 390 | if (j == 0 || newWeight < 2 ** (j - 1) || (2 ** j) <= newWeight) { 391 | // MOVE TO NEW PARENT 392 | // parent range is changing if 393 | // the current parent is a zero range 394 | // the updated weight does not belong to the current parent 395 | uint256 k = floor_ilog(newWeight) + 1; 396 | move_range(forest, range, parentLevel, j, k); 397 | nextTail = enqueue_range(ptr, head, tail, j, -1 * int256(oldWeight)); 398 | nextTail = enqueue_range(ptr, head, nextTail, k, weightDelta); 399 | } else if (j != 0) { 400 | // UPDATE CURRENT PARENT 401 | // enqueue the current parent range for update if it's not a zero range 402 | nextTail = enqueue_range(ptr, head, tail, j, weightDelta); 403 | } 404 | } 405 | 406 | function generate(Forest storage forest, uint256 seed) external view returns (uint256) { 407 | // level search with the URV by finding the minimum integer l such that 408 | // U * W < ∑ₖ weight(Tₖ), where 1 ≤ k ≤ l, U == URV ∈ [0, 1), W is the total weight 409 | // seed ∈ [0, 255), so the arithmetic included is to put it into a fractional value 410 | uint256 l = 1; 411 | uint256 w = 0; 412 | 413 | // scale the seed down to 128b, to ensure muldiv doesn't underflow when dividing 414 | // by intmax 415 | seed >>= 128; 416 | uint256 threshold = (forest.weight * seed) / type(uint128).max; 417 | uint256 j; 418 | uint256 lj; 419 | 420 | Level storage chosenLevel; 421 | 422 | // TODO: level has no root ranges 423 | while (w <= threshold) { 424 | w += forest.levels[l].weight; 425 | l++; 426 | } 427 | w = 0; 428 | chosenLevel = forest.levels[l]; 429 | 430 | mapping(uint256 => Node) storage ranges = chosenLevel.ranges; 431 | 432 | threshold = chosenLevel.weight; 433 | lj = chosenLevel.roots; 434 | 435 | // select root range within level 436 | while (w < threshold) { 437 | j = floor_ilog(lj) + 1; 438 | lj -= 2 ** j; 439 | w += ranges[j].weight; 440 | } 441 | 442 | return bucket_rejection(forest, l, j, seed); 443 | } 444 | 445 | /** 446 | * preprocess 447 | * 448 | * update weight of the element and forest 449 | * update weight of parent range 450 | * enqueue update to parent range, 451 | * if changing parent, remove edge from parent 452 | * if changing parent, update weight of new parent range 453 | * if changing parent, add edge from new parent range 454 | * if changing parent, enqueue update to new parent range 455 | * 456 | * 457 | * insert element 458 | * revert if element is already nonzero 459 | * 460 | * update element 461 | * 462 | * 463 | * TODO 464 | * 465 | * unify insert elt and update elt 466 | * 467 | * preprocess externalL 468 | * update element internal 469 | * process levels 470 | * 471 | * update element external: 472 | * update element internal 473 | * process levels 474 | * 475 | * process levels 476 | * agnostic to preprocess or update 477 | * we just process the queue 478 | */ 479 | 480 | function bucket_rejection(Forest storage forest, uint256 level, uint256 range, uint256 urv) 481 | internal 482 | view 483 | returns (uint256) 484 | { 485 | // We want to choose a child to descend to from the range. 486 | // To do this, we use the bucket rejection method as described by 487 | // Knuth. 488 | 489 | // Here we expand numbers from the URV 490 | uint256 i = 1; 491 | bool repeat = true; 492 | Node storage range_s = forest.levels[level].ranges[range]; 493 | uint256 n = range_s.children.length; 494 | uint256 index; 495 | uint256 ilog_n = floor_ilog(n); 496 | uint256 scale_down_bits = 255 - ilog_n; 497 | while (repeat) { 498 | uint256 expanded_urv = uint256(keccak256(abi.encode(urv, i++))) >> scale_down_bits; 499 | uint256 un = expanded_urv * n; 500 | // = ⌊un⌋ 501 | index = ((expanded_urv * n) >> ilog_n) << ilog_n; 502 | if ( 503 | (un - index) 504 | > (forest.levels[range_s.children[index].level].ranges[range_s.children[index].index].weight << ilog_n) 505 | ) { 506 | repeat = false; 507 | } 508 | } 509 | return ((index >> ilog_n) + 1); 510 | } 511 | 512 | // TODO: these should likely all be inlined 513 | function is_root_range(Node storage range, Level storage level) internal view returns (bool) { 514 | return (level.roots & (2 ** range.index)) != 0; 515 | } 516 | 517 | function set_root_range(Node storage range, Level storage level) internal { 518 | level.roots |= (2 ** range.index); 519 | } 520 | 521 | function unset_root_range(Node storage range, Level storage level) internal { 522 | level.roots &= ~(2 ** range.index); 523 | } 524 | 525 | /*=================== MATH ===================*/ 526 | 527 | // TODO: There may be a slightly more optimal implementation of this in Hackers delight. 528 | // https://github.com/hcs0/Hackers-Delight/blob/master/nlz.c.txt 529 | // @return the number of leading zeros in the binary representation of x 530 | function nlz(uint256 x) public pure returns (uint256) { 531 | if (x == 0) { 532 | return 256; 533 | } 534 | 535 | uint256 n = 1; 536 | 537 | if (x >> 128 == 0) { 538 | n += 128; 539 | x <<= 128; 540 | } 541 | if (x >> 192 == 0) { 542 | n += 64; 543 | x <<= 64; 544 | } 545 | if (x >> 224 == 0) { 546 | n += 32; 547 | x <<= 32; 548 | } 549 | if (x >> 240 == 0) { 550 | n += 16; 551 | x <<= 16; 552 | } 553 | if (x >> 248 == 0) { 554 | n += 8; 555 | x <<= 8; 556 | } 557 | if (x >> 252 == 0) { 558 | n += 4; 559 | x <<= 4; 560 | } 561 | if (x >> 254 == 0) { 562 | n += 2; 563 | x <<= 2; 564 | } 565 | 566 | n -= x >> 255; 567 | 568 | return n; 569 | } 570 | 571 | // @retrun integer ⌊lg n⌋ of x. 572 | function floor_ilog(uint256 x) public pure returns (uint256) { 573 | return (255 - nlz(x)); 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /test/LibDDRV.feature: -------------------------------------------------------------------------------- 1 | Feature: Dynamic Discrete Random Variates 2 | 3 | As a Solidity Developer, 4 | I want to generate random variates from a discrete distribution of dynamically weighted elements in sublinear time, 5 | so that I can make probabilistic selections in my smart contracts. 6 | 7 | # Preprocess 8 | 9 | Scenario: Preprocess 0 Elements 10 | Given An empty and uninstantiated Forest 11 | When I preprocess the Forest 12 | Then The Forest should be instantiated and empty 13 | 14 | Scenario: Preprocess 1 Element 15 | Given An empty and uninstantiated Forest 16 | When I preprocess the Forest with the following 1 Element: 17 | | Element | Weight | 18 | | 1 | 10 | 19 | Then The Forest should have the following structure: 20 | | Element | E Weight | Parent | P Weight | 21 | | 1 | 10 | R₄⁽¹⁾ | 10 | 22 | 23 | Scenario: Preprocess 10 Elements 24 | Given An empty and uninstantiated Forest 25 | When I preprocess the Forest with the following 10 Elements: 26 | | Element | Weight | 27 | | 1 | 5 | 28 | | 2 | 4 | 29 | | 3 | 5 | 30 | | 4 | 7 | 31 | | 5 | 6 | 32 | | 6 | 4 | 33 | | 7 | 4 | 34 | | 8 | 5 | 35 | | 9 | 6 | 36 | | 10 | 7 | 37 | Then The Forest should have the following structure: TODO 38 | | Element | E Weight | Parent | P Weight | 39 | | 1 | 5 | R₄⁽¹⁾ | 53 | 40 | | 2 | 4 | R₄⁽¹⁾ | 53 | 41 | | 3 | 5 | R₄⁽¹⁾ | 53 | 42 | | 4 | 7 | R₄⁽¹⁾ | 53 | 43 | | 5 | 6 | R₄⁽¹⁾ | 53 | 44 | | 6 | 4 | R₄⁽¹⁾ | 53 | 45 | | 7 | 4 | R₄⁽¹⁾ | 53 | 46 | | 8 | 5 | R₄⁽¹⁾ | 53 | 47 | | 9 | 6 | R₄⁽¹⁾ | 53 | 48 | | 10 | 7 | R₄⁽¹⁾ | 53 | 49 | 50 | # Insert (basic -- see Update scenarios for more expressive Insert scenarios) 51 | 52 | Scenario: Insert 1 Element 53 | Given A Forest with the following 1 Element: 54 | | Element | Weight | 55 | | 1 | 7 | 56 | When I insert the following 1 Element: 57 | | Element | Weight | 58 | | 2 | 5 | 59 | Then The Forest should have the following structure: 60 | | Element | E Weight | Parent | P Weight | 61 | | 1 | 7 | R₄⁽¹⁾ | 12 | 62 | | 2 | 5 | R₄⁽¹⁾ | 12 | 63 | 64 | Scenario: TODO Insert 1 Element 10 times 65 | 66 | # Update 67 | 68 | #*////////////////////////////////////////////////////////////////////////// 69 | # 70 | # Visual Representation of Forest with 11 Elements 71 | # 72 | # R₆⁽²⁾ R₅⁽²⁾ <--- Level 2 73 | # | | 74 | # | | 75 | # | | 76 | # R₅⁽¹⁾ R₄⁽¹⁾ R₃⁽¹⁾ <--- Level 1 77 | # | / / | \ \ / / | \ \ 78 | # | / | | | \ / | | | \ 79 | # | / | | | \ / | | | \ 80 | # 4 11 10 9 3 1 8 7 6 5 2 <--- Elements 81 | # 82 | #/////////////////////////////////////////////////////////////////////////*/ 83 | 84 | Background: Forest with 11 Elements, 3 Root Ranges, 2 Levels, and total weight of 100 85 | # NOTE this background applies only to Update scenarios 86 | Given The Forest contains the following 11 Elements: 87 | | Element | Weight | 88 | | 1 | 10 | 89 | | 2 | 5 | 90 | | 3 | 15 | 91 | | 4 | 20 | 92 | | 5 | 5 | 93 | | 6 | 5 | 94 | | 7 | 5 | 95 | | 8 | 5 | 96 | | 9 | 10 | 97 | | 10 | 10 | 98 | | 11 | 10 | 99 | 100 | And The total weight of the Forest is 100 101 | 102 | And There are 2 Levels in the Forest 103 | And The weight of Level 1 is 20 104 | And The weight of Level 2 is 80 105 | 106 | And The weight of Range R₃⁽¹⁾ is 25 107 | And The weight of Range R₄⁽¹⁾ is 55 108 | And The weight of Range R₅⁽¹⁾ is 20 109 | And The weight of Range R₅⁽²⁾ is 25 110 | And The weight of Range R₆⁽²⁾ is 55 111 | 112 | And The Forest has the following structure: 113 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 114 | | 2 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 115 | | 5 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 116 | | 6 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 117 | | 7 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 118 | | 8 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 119 | | 1 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 120 | | 3 | 15 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 121 | | 9 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 122 | | 10 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 123 | | 11 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 124 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | 125 | 126 | @Revert 127 | Scenario: A -- Update 1 Element, no change in weight, no change in parent 128 | When I update Element 8 from weight 5 to weight 5 129 | Then The transaction should revert with a NewWeightMustBeDifferent error 130 | 131 | Scenario: B -- Update 1 Element, decrease weight, no change in parent 132 | When I update Element 8 from weight 5 to weight 4 133 | 134 | Then The parent of Element 8 should still be Range R₃⁽¹⁾ 135 | 136 | And The total weight of the Forest should be 99 137 | 138 | And There should still be 2 Levels in the Forest 139 | And The weight of Level 1 should still be 20 140 | And The weight of Level 2 should be 79 141 | 142 | And The weight of Range R₃⁽¹⁾ should be 24 143 | And The weight of Range R₄⁽¹⁾ should still be 55 144 | And The weight of Range R₅⁽¹⁾ should still be 20 145 | And The weight of Range R₅⁽²⁾ should be 24 146 | And The weight of Range R₆⁽²⁾ should still be 55 147 | 148 | And The Forest should not change its structure 149 | 150 | Scenario: C -- Update 1 Element, decrease weight, moves to lower range numbered-parent 151 | When I update Element 3 from weight 15 to weight 6 152 | 153 | Then The parent of Element 3 should now be Range R₃⁽¹⁾ 154 | 155 | And The total weight of the Forest should be 91 156 | 157 | And There should still be 2 Levels in the Forest 158 | And The weight of Level 1 should still be 20 159 | And The weight of Level 2 should be 71 160 | 161 | And The weight of Range R₃⁽¹⁾ should be 31 162 | And The weight of Range R₄⁽¹⁾ should be 40 163 | And The weight of Range R₅⁽¹⁾ should still be 20 164 | And The weight of Range R₅⁽²⁾ should be 31 165 | And The weight of Range R₆⁽²⁾ should be 40 166 | 167 | And The Forest should have the following structure: 168 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 169 | | 2 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 170 | | 3 | 6 * | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 171 | | 5 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 172 | | 6 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 173 | | 7 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 174 | | 8 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 175 | | 1 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | 40 | 176 | | 9 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | 40 | 177 | | 10 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | 40 | 178 | | 11 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | 40 | 179 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | 180 | 181 | 182 | 183 | Scenario: D -- Update 1 Element, increase weight, no change in parent 184 | When I update Element 8 from weight 5 to weight 7 185 | 186 | Then The parent of Element 8 should still be Range R₃⁽¹⁾ 187 | 188 | And The total weight of the forest should be 102 189 | 190 | And There should be 2 Levels in the Forest 191 | And The weight of Level 1 should be 20 192 | And The weight of Level 2 should be 82 193 | 194 | And The weight of Range R₃⁽¹⁾ should still be 20 195 | And The weight of Range R₄⁽¹⁾ should be 27 196 | And The weight of Range R₅⁽¹⁾ should still be 55 197 | And The weight of Range R₅⁽²⁾ should be 27 198 | And The weight of Range R₆⁽²⁾ should be 55 199 | 200 | And The Forest should not change its structure 201 | 202 | Scenario: E -- Update 1 Element, increase weight, moves to higher range numbered-parent 203 | When I update Element 8 from weight 5 to weight 8 204 | 205 | Then The parent of Element 8 should now be Range R₄⁽¹⁾ 206 | 207 | And The total weight of the forest should be 103 208 | 209 | And There should be 2 Levels in the Forest 210 | And The weight of Level 1 should be 20 211 | And The weight of Level 2 should be 83 212 | 213 | And The weight of Range R₃⁽¹⁾ should be 20 214 | And The weight of Range R₄⁽¹⁾ should be 63 215 | And The weight of Range R₅⁽¹⁾ should still be 20 216 | And The weight of Range R₅⁽²⁾ should be 20 217 | And The weight of Range R₆⁽²⁾ should be 63 218 | 219 | And The Forest should have the following structure: 220 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 221 | | 2 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 222 | | 5 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 223 | | 6 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 224 | | 7 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 225 | | 1 | 10 | R₄⁽¹⁾ | (63) | R₆⁽²⁾ | 63 | 226 | | 3 | 15 | R₄⁽¹⁾ | (63) | R₆⁽²⁾ | 63 | 227 | | 8 | 8 * | R₄⁽¹⁾ | (63) | R₆⁽²⁾ | 63 | 228 | | 9 | 10 | R₄⁽¹⁾ | (63) | R₆⁽²⁾ | 63 | 229 | | 10 | 10 | R₄⁽¹⁾ | (63) | R₆⁽²⁾ | 63 | 230 | | 11 | 10 | R₄⁽¹⁾ | (63) | R₆⁽²⁾ | 63 | 231 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | 232 | 233 | Scenario: F -- Update 1 Element, decrease weight, moves to lower range numbered-parent, 1 grandparent dies, 1 Level is added 234 | When I update Element 3 from weight 15 to weight 7 235 | 236 | Then The parent of Element 3 should now be Range R₃⁽¹⁾ 237 | 238 | And The parent of Range R₃⁽¹⁾ should now be Range R₆⁽²⁾ 239 | 240 | And The total weight of the forest should be 92 241 | 242 | And There should be 3 Levels in the Forest 243 | And The weight of Level 1 should be 20 244 | And The weight of Level 2 should be 0 245 | And The weight of Level 3 should be 72 246 | 247 | And The weight of Range R₃⁽¹⁾ should be 32 248 | And The weight of Range R₄⁽¹⁾ should be 40 249 | And The weight of Range R₅⁽¹⁾ should still be 20 250 | And The weight of Range R₅⁽²⁾ should be 0 251 | And The weight of Range R₆⁽²⁾ should be 72 252 | And The weight of Range R₇⁽³⁾ should be 72 253 | 254 | And The Forest should have the following structure: 255 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | Greatgrandparent | GGP Weight | 256 | | 2 | 5 | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 257 | | 3 | 7 * | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 258 | | 5 | 5 | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 259 | | 6 | 5 | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 260 | | 7 | 5 | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 261 | | 8 | 5 | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 262 | | 1 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 263 | | 9 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 264 | | 10 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 265 | | 11 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | (72) | R₇⁽³⁾ | 72 | 266 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | | | 267 | 268 | Scenario: G -- Update 1 Element, increase weight, moves to higher range numbered-parent and -grandparent 269 | When I update Element 8 from weight 5 to weight 9 270 | 271 | Then The parent of Element 3 should now be Range R₄⁽¹⁾ 272 | 273 | And The parent of Range R₄⁽¹⁾ should now be Range R₇⁽²⁾ 274 | 275 | And The total weight of the forest should be 104 276 | 277 | And There should be 2 Levels in the Forest 278 | And The weight of Level 1 should be 20 279 | And The weight of Level 2 should be 84 280 | 281 | And The weight of Range R₃⁽¹⁾ should be 20 282 | And The weight of Range R₄⁽¹⁾ should be 64 283 | And The weight of Range R₅⁽¹⁾ should still be 20 284 | And The weight of Range R₅⁽²⁾ should be 20 285 | And The weight of Range R₆⁽²⁾ should be 0 286 | And The weight of Range R₇⁽²⁾ should be 64 287 | 288 | And The Forest should have the following structure: 289 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 290 | | 2 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 291 | | 5 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 292 | | 6 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 293 | | 7 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 294 | | 1 | 10 | R₄⁽¹⁾ | (64) | R₇⁽²⁾ | 64 | 295 | | 3 | 15 | R₄⁽¹⁾ | (64) | R₇⁽²⁾ | 64 | 296 | | 8 | 9 * | R₄⁽¹⁾ | (64) | R₇⁽²⁾ | 64 | 297 | | 9 | 10 | R₄⁽¹⁾ | (64) | R₇⁽²⁾ | 64 | 298 | | 10 | 10 | R₄⁽¹⁾ | (64) | R₇⁽²⁾ | 64 | 299 | | 11 | 10 | R₄⁽¹⁾ | (64) | R₇⁽²⁾ | 64 | 300 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | 301 | 302 | Scenario: H -- Update 5 Elements, decrease weight, moves to lower range numbered-parent and -grandparent 303 | When I update Element 8 from weight 5 to weight 3 304 | And I update Element 7 from weight 5 to weight 3 305 | And I update Element 6 from weight 5 to weight 3 306 | And I update Element 5 from weight 5 to weight 3 307 | And I update Element 2 from weight 5 to weight 3 308 | 309 | Then The parent of Element 8 should now be Range R₂⁽¹⁾ 310 | And The parent of Element 7 should now be Range R₂⁽¹⁾ 311 | And The parent of Element 6 should now be Range R₂⁽¹⁾ 312 | And The parent of Element 5 should now be Range R₂⁽¹⁾ 313 | And The parent of Element 2 should now be Range R₂⁽¹⁾ 314 | And The parent of Range R₂⁽¹⁾ should now be Range R₄⁽²⁾ 315 | 316 | And The total weight of the forest should be 90 317 | 318 | And There should be 2 Levels in the Forest 319 | And The weight of Level 1 should be 20 320 | And The weight of Level 2 should be 70 321 | 322 | And The weight of Range R₂⁽¹⁾ should be 15 323 | And The weight of Range R₃⁽¹⁾ should be 0 324 | And The weight of Range R₄⁽¹⁾ should still be 55 325 | And The weight of Range R₅⁽¹⁾ should still be 20 326 | And The weight of Range R₄⁽²⁾ should be 15 327 | And The weight of Range R₅⁽²⁾ should be 0 328 | And The weight of Range R₆⁽²⁾ should still be 55 329 | 330 | And The Forest should have the following structure: 331 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 332 | | 2 | 3 * | R₂⁽¹⁾ | (15) | R₄⁽²⁾ | 15 | 333 | | 5 | 3 * | R₂⁽¹⁾ | (15) | R₄⁽²⁾ | 15 | 334 | | 6 | 3 * | R₂⁽¹⁾ | (15) | R₄⁽²⁾ | 15 | 335 | | 7 | 3 * | R₂⁽¹⁾ | (15) | R₄⁽²⁾ | 15 | 336 | | 8 | 3 * | R₂⁽¹⁾ | (15) | R₄⁽²⁾ | 15 | 337 | | 1 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 338 | | 3 | 15 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 339 | | 9 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 340 | | 10 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 341 | | 11 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 342 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | 343 | 344 | Scenario: I -- Update 2 Elements, increase weight, moves to higher range numbered-grandparent, no change in parent 345 | When I update Element 9 from weight 10 to weight 15 346 | And I update Element 11 from weight 10 to weight 15 347 | 348 | Then The parent of Element 9 should still be Range R₄⁽¹⁾ 349 | And The parent of Element 11 should still be Range R₄⁽¹⁾ 350 | And The parent of Range R₄⁽¹⁾ should now be Range R₇⁽²⁾ 351 | 352 | And The total weight of the forest should be 110 353 | 354 | And There should be 2 Levels in the Forest 355 | And The weight of Level 1 should be 20 356 | And The weight of Level 2 should be 90 357 | 358 | And The weight of Range R₃⁽¹⁾ should still be 25 359 | And The weight of Range R₄⁽¹⁾ should be 65 360 | And The weight of Range R₅⁽¹⁾ should still be 20 361 | And The weight of Range R₅⁽²⁾ should still be 25 362 | And The weight of Range R₆⁽²⁾ should be 0 363 | And The weight of Range R₇⁽²⁾ should be 65 364 | 365 | And The Forest has the following structure: 366 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 367 | | 2 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 368 | | 5 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 369 | | 6 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 370 | | 7 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 371 | | 8 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 372 | | 1 | 10 | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 373 | | 3 | 15 | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 374 | | 9 | 15 * | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 375 | | 10 | 10 | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 376 | | 11 | 15 * | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 377 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | 378 | 379 | Scenario: J -- Update 1 Element, increase weight, moves to much higher range numbered-parent 380 | When I update Element 4 from weight 20 to weight 127 381 | 382 | Then The parent of Element 4 should now be Range R₇⁽¹⁾ 383 | 384 | And The total weight of the forest should be 207 385 | 386 | And There should be 2 Levels in the Forest 387 | And The weight of Level 1 should be 127 388 | And The weight of Level 2 should be 80 389 | 390 | And The weight of Range R₃⁽¹⁾ should still be 25 391 | And The weight of Range R₄⁽¹⁾ should still be 55 392 | And The weight of Range R₅⁽¹⁾ should be 0 393 | And The weight of Range R₇⁽¹⁾ should be 127 394 | And The weight of Range R₅⁽²⁾ should still be 25 395 | And The weight of Range R₆⁽²⁾ should still be 55 396 | 397 | And The Forest should have the following structure: 398 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 399 | | 2 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 400 | | 5 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 401 | | 6 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 402 | | 7 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 403 | | 8 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 404 | | 1 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 405 | | 3 | 15 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 406 | | 9 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 407 | | 10 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 408 | | 11 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 409 | | 4 | 127 * | R₇⁽¹⁾ | 127 | | | 410 | 411 | Scenario: K -- Update 1 Element, increase weight, moves to much, much higher range numbered-parent 412 | When I update Element 4 from weight 20 to weight 128 413 | 414 | Then The parent of Element 4 should now be Range R₈⁽¹⁾ 415 | 416 | And The total weight of the forest should be 208 417 | 418 | And There should be 2 Levels in the Forest 419 | And The weight of Level 1 should be 128 420 | And The weight of Level 2 should be 80 421 | 422 | And The weight of Range R₃⁽¹⁾ should still be 25 423 | And The weight of Range R₄⁽¹⁾ should still be 55 424 | And The weight of Range R₅⁽¹⁾ should be 0 425 | And The weight of Range R₈⁽¹⁾ should be 128 426 | And The weight of Range R₅⁽²⁾ should still be 25 427 | And The weight of Range R₆⁽²⁾ should still be 55 428 | 429 | And The Forest should have the following structure: 430 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 431 | | 2 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 432 | | 5 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 433 | | 6 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 434 | | 7 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 435 | | 8 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 436 | | 1 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 437 | | 3 | 15 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 438 | | 9 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 439 | | 10 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 440 | | 11 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 441 | | 4 | 128 * | R₈⁽¹⁾ | 128 | | | 442 | 443 | Scenario: L -- Update 4 Elements, increase weight, 1 grandparent dies, no change in parent 444 | When I update Element 2 from weight 5 to weight 8 445 | And I update Element 6 from weight 5 to weight 6 446 | And I update Element 7 from weight 5 to weight 7 447 | And I update Element 8 from weight 5 to weight 6 448 | 449 | Then The parent of Element 2 should still be Range R₃⁽¹⁾ 450 | And The parent of Element 6 should still be Range R₃⁽¹⁾ 451 | And The parent of Element 7 should still be Range R₃⁽¹⁾ 452 | And The parent of Element 8 should still be Range R₃⁽¹⁾ 453 | And The parent of Range R₃⁽¹⁾ should now be Range R₆⁽²⁾ 454 | 455 | And The total weight of the forest should be 107 456 | 457 | And There should be 2 Levels in the Forest 458 | And The weight of Level 1 should be 20 459 | And The weight of Level 2 should be 87 460 | 461 | And The weight of Range R₃⁽¹⁾ should be 32 462 | And The weight of Range R₄⁽¹⁾ should still be 55 463 | And The weight of Range R₅⁽¹⁾ should still be 20 464 | And The weight of Range R₅⁽²⁾ should be 0 465 | And The weight of Range R₆⁽²⁾ should be 87 466 | 467 | And The Forest should have the following structure: 468 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 469 | | 2 | 8 * | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | 87 | 470 | | 5 | 5 | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | 87 | 471 | | 6 | 6 * | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | 87 | 472 | | 7 | 7 * | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | 87 | 473 | | 8 | 6 * | R₃⁽¹⁾ | (32) | R₆⁽²⁾ | 87 | 474 | | 1 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 87 | 475 | | 3 | 15 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 87 | 476 | | 9 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 87 | 477 | | 10 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 87 | 478 | | 11 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 87 | 479 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | 480 | 481 | 482 | 483 | Scenario: M -- Update 1 Element, increase weight, jump across parents, 1 Level is added 484 | When I update Element 5 from weight 5 to weight 30 485 | 486 | Then The parent of Element 5 should be Range R₅⁽¹⁾ 487 | 488 | And The total weight of the Forest should be 125 489 | 490 | And There should still be 2 Levels in the Forest 491 | And The weight of Level 1 should be 0 492 | And The weight of Level 2 should be 20 493 | And The weight of Level 3 should be 105 494 | 495 | And The weight of Range R₃⁽¹⁾ should be 20 496 | And The weight of Range R₄⁽¹⁾ should be 55 497 | And The weight of Range R₅⁽¹⁾ should be 50 498 | And The weight of Range R₅⁽²⁾ should be 20 499 | And The weight of Range R₆⁽²⁾ should be 105 500 | And The weight of Range R₇⁽³⁾ should be 105 501 | 502 | And The Forest should have the following structure: 503 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | Greatgrandparent | GGP Weight | 504 | | 2 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | | | 505 | | 6 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | | | 506 | | 7 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | | | 507 | | 8 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | | | 508 | | 1 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | (105) | R₇⁽³⁾ | 105 | 509 | | 3 | 15 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | (105) | R₇⁽³⁾ | 105 | 510 | | 9 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | (105) | R₇⁽³⁾ | 105 | 511 | | 10 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | (105) | R₇⁽³⁾ | 105 | 512 | | 11 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | (105) | R₇⁽³⁾ | 105 | 513 | | 4 | 20 | R₅⁽¹⁾ | (50) | R₆⁽²⁾ | (105) | R₇⁽³⁾ | 105 | 514 | | 5 | 30 * | R₅⁽¹⁾ | (50) | R₆⁽²⁾ | (105) | R₇⁽³⁾ | 105 | 515 | 516 | Scenario: N -- Update 2 Elements, increase weight, jump across parents, 1 grandparent dies, 1 switches, 1 is born 517 | When I update Element 5 from weight 5 to weight 30 518 | And I update Element 7 from weight 5 to weight 10 519 | 520 | Then The parent of Element 5 should now be Range R₅⁽¹⁾ 521 | And The parent of Element 7 should now be Range R₄⁽¹⁾ 522 | 523 | And The total weight of the Forest should be 130 524 | 525 | And There should still be 2 Levels in the Forest 526 | And The weight of Level 1 should be 0 527 | And The weight of Level 2 should be 130 528 | 529 | And The weight of Range R₃⁽¹⁾ should be 15 530 | And The weight of Range R₄⁽¹⁾ should be 65 531 | And The weight of Range R₅⁽¹⁾ should be 50 532 | And The weight of Range R₄⁽²⁾ should be 15 533 | And The weight of Range R₅⁽²⁾ should be 0 534 | And The weight of Range R₆⁽²⁾ should be 50 535 | And The weight of Range R₇⁽²⁾ should be 65 536 | 537 | And The Forest should have the following structure: 538 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 539 | | 2 | 5 | R₃⁽¹⁾ | (15) | R₄⁽²⁾ | 15 | 540 | | 6 | 5 | R₃⁽¹⁾ | (15) | R₄⁽²⁾ | 15 | 541 | | 8 | 5 | R₃⁽¹⁾ | (15) | R₄⁽²⁾ | 15 | 542 | | 1 | 10 | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 543 | | 3 | 15 | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 544 | | 7 | 10 * | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 545 | | 9 | 10 | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 546 | | 10 | 10 | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 547 | | 11 | 10 | R₄⁽¹⁾ | (65) | R₇⁽²⁾ | 65 | 548 | | 4 | 20 | R₅⁽¹⁾ | (50) | R₆⁽²⁾ | 50 | 549 | | 5 | 30 * | R₅⁽¹⁾ | (50) | R₆⁽²⁾ | 50 | 550 | 551 | Scenario: O -- TODO 4 Levels 552 | - Insert Element 12 with weight 30 553 | - Parent should R₅⁽¹⁾ with weight 50 554 | - Grandparent should be R₆⁽²⁾ with weight XYZ 555 | - Great Grandparent should be R₇⁽³⁾ with weight XYZ 556 | 557 | Scenario: P -- "Delete" 1 Element, 1 parent dies, 1 grandparent dies 558 | When I update Element 5 weight from 5 to weight 0 559 | 560 | Then The parent of Element 5 should be the null Range R₀⁽¹⁾ 561 | 562 | And The total weight of the forest should be 95 563 | 564 | And There should be 2 Levels in the Forest 565 | And The weight of Level 1 should still be 20 566 | And The weight of Level 2 should be 75 567 | 568 | And The weight of Range R₃⁽¹⁾ should be 20 569 | And The weight of Range R₄⁽¹⁾ should still be 55 570 | And The weight of Range R₅⁽¹⁾ should still be 20 571 | And The weight of Range R₅⁽²⁾ should be 20 572 | And The weight of Range R₆⁽²⁾ should still be 55 573 | 574 | And The Forest should have the following structure: 575 | | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 576 | | 5 | 0 * | R₀⁽¹⁾ | 0 | | | 577 | | 2 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 578 | | 6 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 579 | | 7 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 580 | | 8 | 5 | R₃⁽¹⁾ | (20) | R₅⁽²⁾ | 20 | 581 | | 1 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 582 | | 3 | 15 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 583 | | 9 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 584 | | 10 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 585 | | 11 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 586 | | 4 | 20 | R₅⁽¹⁾ | 20 | | | 587 | 588 | Scenario: Q -- TODO "Delete" n Elements, decrease parent 589 | 590 | Scenario: R -- TODO "Delete" n Elements, increase parent 591 | 592 | # Generate 593 | 594 | Scenario: Generate discrete random variate from set of 100 Elements 595 | Given The Forest contains the following 5 Elements: 596 | | Element | Weight | 597 | | 1 | 38 | 598 | | 2 | 4 | 599 | | 3 | 5 | 600 | | 4 | 12 | 601 | | 5 | 41 | 602 | When I generate 10,000 discrete random variates 603 | Then The instances of each Element being selected should be the following, within 10% tolerance: 604 | | Element | Instances | 605 | | 1 | 3800 | 606 | | 2 | 400 | 607 | | 3 | 500 | 608 | | 4 | 1200 | 609 | | 5 | 4100 | 610 | -------------------------------------------------------------------------------- /test/LibDDRV.invariant.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {Math} from "./utils/Math.sol"; 7 | import {LibDDRVUtils} from "./utils/LibDDRVUtils.sol"; 8 | 9 | import {ContractUnderTest} from "./utils/ContractUnderTest.sol"; 10 | import {Handler} from "./utils/Handler.sol"; 11 | 12 | // TEMP Unicode symbols for reference 13 | // Rⱼ⁽ℓ⁾ 14 | // Rⱼʹ⁽ℓ⁾ 15 | // Rⱼʹ⁽ℓ⁺¹⁾ 16 | // 2ʲʹ 17 | // 2ʲʹ⁻¹ 18 | // reg: jʹ 19 | // sub: ⱼʹ 20 | // super: ʲʹ 21 | // ᵐ⁻¹⁺ᶜ 22 | // ℓ 23 | 24 | /// @notice Invariant tests for LibDDRV 25 | /// For more info, see https://kuscholarworks.ku.edu/bitstream/handle/1808/7224/MVN03.dynamic_rv_gen.pdf 26 | contract LibDDRVInvariantTest is Test { 27 | ContractUnderTest internal c; 28 | Handler internal handler; 29 | 30 | function setUp() public { 31 | c = new ContractUnderTest(); 32 | handler = new Handler(address(c)); 33 | 34 | excludeContract(address(c)); 35 | } 36 | 37 | /** 38 | * @dev Invariant A -- "Number of levels" 39 | * 40 | * The total number of levels L in the forest of trees is <= lg* N + 1 (where lg* denotes the 41 | * base-2 iterated logarithm; for more info, see https://en.wikipedia.org/wiki/Iterated_logarithm). 42 | * 43 | * (from Theorem 4) 44 | */ 45 | function invariantA_numberOfLevels() public { 46 | assertLe(c.numberOfLevels(), Math.logStar2(c.numberOfLevels()) + 1, "Number of levels"); 47 | } 48 | 49 | /** 50 | * @dev Invariant B -- "Number of non-empty ranges" 51 | * 52 | * The number of non-empty ranges is O(N), i.e., it is of the same order as the number of elements N. 53 | * 54 | * (from Lemma 4) 55 | */ 56 | function invariantB_numberOfNonEmptyRanges() public { 57 | assertEq( 58 | Math.floor(Math.log10(c.numberOfRanges())), 59 | Math.floor(Math.log10(c.numberOfElements())), 60 | "Number of non-empty ranges" 61 | ); 62 | } 63 | 64 | /** 65 | * @dev Invariant C -- "Parent range of non-root ranges" 66 | * 67 | * For any non-root range Rⱼ⁽ℓ⁾ (defined as having degree m >= 2, where 2 is the degree bound constant), 68 | * its parent range is Rⱼʹ⁽ℓ⁺¹⁾ and its weight is within [2ʲʹ⁻¹, 2ʲʹ), where jʹ is the range number of 69 | * its weight. 70 | * 71 | * (from Lemma 1) 72 | */ 73 | function invariantC_parentRangeOfNonRootRanges() public { 74 | // Loop through all non-root ranges. 75 | 76 | // Check that its parent range is Rⱼʹ⁽ℓ⁺¹⁾. 77 | 78 | // Check that its weight is within [2ʲʹ⁻¹, 2ʲʹ). 79 | 80 | assertTrue(false, "Parent range of non-root ranges"); // TEMP 81 | } 82 | 83 | /** 84 | * @dev Invariant D -- "Difference between range number of children and of non-root range itself" 85 | * 86 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= 2, the difference between the range number of its 87 | * children jʹ and its own range number j satisfies the inequality lg m - 1 < jʹ - j < lg m + 1. 88 | * 89 | * (from Lemma 1) 90 | */ 91 | function invariantD_differenceBetweenRangeNumberOfChildrenAndNonRootRangeItself() public { 92 | // Loop through all non-root ranges. 93 | 94 | // Calculate the difference between the range number of its children jʹ and its own range number j. 95 | 96 | // Check that it satisfies the inequality lg m - 1 < jʹ - j < lg m + 1. 97 | 98 | assertTrue(false, "Difference between range number of children and of non-root range itself"); // TEMP 99 | } 100 | 101 | /** 102 | * @dev Invariant E -- "Degree of one child of non-root ranges on level 2+" 103 | * 104 | * // QUESTION clarify if is this one child and one child only, or at least one child? 105 | * 106 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= 2 on level 2 or higher, one of its children has 107 | * degree >= 2ᵐ⁻¹ + 1. 108 | * 109 | * (from Lemma 2) 110 | */ 111 | function invariantE_degreeOfOneChildOfNonRootRangesOnLevel2AndUp() public { 112 | // Loop through all levels 2 and up. 113 | 114 | // Loop through all non-root ranges on this level. 115 | 116 | // Check that one of its children has degree >= 2ᵐ⁻¹ + 1. 117 | 118 | assertTrue(false, "Degree of one child of non-root ranges on level 2+"); // TEMP 119 | } 120 | 121 | /** 122 | * @dev Invariant F -- "Number of grandchildren of non-root ranges on level 2+" 123 | * 124 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= 2 on level 2 or higher, the number of its grandchildren 125 | * is >= 2ᵐ + m - 1. 126 | * 127 | * (from Lemma 2) 128 | */ 129 | function invariantF_numberOfGrandchildrenOfNonRootRangesOnLevel2AndUp() public { 130 | // Loop through all levels 2 and up. 131 | 132 | // Loop through all non-root ranges on this level. 133 | 134 | // Check that the number of its grandchildren is >= 2ᵐ + m - 1. 135 | 136 | assertTrue(false, "Number of grandchildren of non-root ranges on level 2+"); // TEMP 137 | } 138 | 139 | /** 140 | * @dev Invariant G -- "Difference between range numbers of smallest-numbered descendents of 141 | * non-root ranges on level 3+" 142 | * 143 | * // QUESTION what precisely is smallest-numbered range? —- think smallest index but could be smallest weight / range number 144 | * 145 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= 2 where level ℓ >= k >= 3, the range number of 146 | * the smallest-numbered descendent range on level ℓ-k minus the range number of the smallest-numbered 147 | * descendent range on level ℓ-k+1 is greater than the base-2 power tower of order k and hat m (e.g., 148 | * the base-2 power tower of order 3 and hat 7 is 2^2^2^7, for order 4 it is 2^2^2^2^7, and so on; 149 | * for more info, see https://mathworld.wolfram.com/PowerTower.html). 150 | * 151 | * (from Lemma 3) 152 | */ 153 | function invariantG_differenceBetweenRangeNumbersOfSmallestNumberedDescendentsOfNonRootRangesOnLevel3AndUp() 154 | public 155 | { 156 | // Loop through all levels 3 and up. 157 | 158 | // Loop through this level and any levels above it. 159 | 160 | // Get the smallest-numbered descendent range on level ℓ-k. 161 | 162 | // Get the smallest-numbered descendent range on level ℓ-k+1. 163 | 164 | // Calculate the base-2 power tower of order k and hat m. 165 | 166 | // Check that the difference between them is greater than this value. 167 | 168 | assertTrue( 169 | false, 170 | "Difference between range numbers of smallest-numbered descendents of non-root ranges on level 3+" // TEMP 171 | ); 172 | } 173 | 174 | /** 175 | * @dev Invariant H -- "Number of descendents of non-root ranges on level 3+" 176 | * 177 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= 2 where level ℓ >= k >= 3, the number of descendents 178 | * on level ℓ-k > the base-2 power tower of order k and hat m (see Invariant E for power tower examples). 179 | * 180 | * (from Lemma 3) 181 | */ 182 | function invariantH_numberOfDescendentsOfNonRootRangesOnLevel3AndUp() public { 183 | // Loop through all levels 3 and up. 184 | 185 | // Loop through this level and any levels above it. 186 | 187 | // Get the number of descendents on level ℓ-k. 188 | 189 | // Calculate the base-2 power tower of order k and hat m. 190 | 191 | // Check that the number of descendents on level ℓ-k is greater than this value. 192 | 193 | assertTrue(false, "Number of descendents of non-root ranges on level 3+"); // TEMP 194 | } 195 | 196 | /*////////////////////////////////////////////////////////////// 197 | // Modified Data Structure and Algorithm 198 | //////////////////////////////////////////////////////////////*/ 199 | 200 | /** 201 | * @dev Invariant A -- "Number of levels" 202 | * 203 | * The total number of levels L in the forest of trees is <= lg* N - 1 (where lg* denotes the 204 | * base-2 iterated logarithm; for more info, see https://en.wikipedia.org/wiki/Iterated_logarithm). 205 | * 206 | * (from Theorem 4ʹ) 207 | */ 208 | function MODIFIED_invariantA_numberOfLevels() public { 209 | assertLe(c.numberOfLevels(), Math.logStar2(c.numberOfLevels()) - 1, "Number of levels"); 210 | } 211 | 212 | // NOTE There would be no change in MODIFIED data structure for invariant B 213 | 214 | /** 215 | * @dev Invariant C -- "Parent range of non-root ranges" 216 | * 217 | * For any non-root range Rⱼ⁽ℓ⁾ (defined as having degree m >= d, where d is the degree bound 218 | * constant), its parent range is Rⱼʹ⁽ℓ⁺¹⁾ and its weight is within [2ʲʹ⁻¹, 2ʲʹ), 219 | * where jʹ is the range number of its weight. 220 | * 221 | * (from Lemma 1ʹ) 222 | */ 223 | function MODIFIED_invariantC_parentRangeOfNonRootRanges() public { 224 | // Loop through all non-root ranges. 225 | 226 | // Check that its parent range is Rⱼʹ⁽ℓ⁺¹⁾. 227 | 228 | // Check that its weight is within [2ʲʹ⁻¹, 2ʲʹ). 229 | 230 | assertTrue(false, "Parent range of non-root ranges"); // TEMP 231 | } 232 | 233 | /** 234 | * @dev Invariant D -- "Difference between range number of children and of non-root range itself" 235 | * 236 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= d, the difference between the range number of its children jʹ and its own range number j 237 | * satisfies the inequality lg m - lg (2+b)/(1-b) < jʹ - j < lg m + lg (2+b)/(1-b). 238 | * 239 | * (from Lemma 1ʹ) 240 | */ 241 | function MODIFIED_invariantD_differenceBetweenRangeNumberOfChildrenAndNonRootRangeItself() public { 242 | // Loop through all non-root ranges. 243 | 244 | // Calculate the difference between the range number of its children jʹ and its own range number j. 245 | 246 | // Check that it satisfies the inequality lg m - lg (2+b)/(1-b) < jʹ - j < lg m + lg (2+b)/(1-b). 247 | 248 | assertTrue(false, "Difference between range number of children and of non-root range itself"); 249 | } 250 | 251 | /** 252 | * @dev Invariant E -- "Degree of one child of non-root ranges on level 2+" 253 | * 254 | * // QUESTION clarify if is this one child and one child only, or at least one child? 255 | * 256 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= d on level 2 or higher, one of its children has degree >= 2ᵐ⁻¹⁺ᶜ, where c is a 257 | * non-negative integer constant >= 1 used to calculate the degree bound constant. 258 | * 259 | * (from Lemma 2ʹ) 260 | */ 261 | function MODIFIED_invariantE_degreeOfOneChildOfNonRootRangesOnLevel2AndUp() public { 262 | // Loop through all levels 2 and up. 263 | 264 | // Loop through all non-root ranges on this level. 265 | 266 | // Check that one of its children has degree >= 2ᵐ⁻¹⁺ᶜ. 267 | 268 | assertTrue(false, "Degree of one child of non-root ranges on level 2+"); // TEMP 269 | } 270 | 271 | /** 272 | * @dev Invariant F -- "Number of grandchildren of non-root ranges on level 2+" 273 | * 274 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= d on level 2 or higher, the number of its grandchildren is >= 2ᵐ⁺ᶜ + m - 2ᶜ, where 275 | * c is a non-negative integer constant >= 1 used to calculate the degree bound constant. 276 | * 277 | * (from Lemma 2ʹ) 278 | */ 279 | function MODIFIED_invariantF_numberOfGrandchildrenOfNonRootRangesOnLevel2AndUp() public { 280 | // Loop through all levels 2 and up. 281 | 282 | // Loop through all non-root ranges on this level. 283 | 284 | // Check that the number of its grandchildren is >= 2ᵐ⁺ᶜ + m - 2ᶜ. 285 | 286 | assertTrue(false, "Number of grandchildren of non-root ranges on level 2+"); // TEMP 287 | } 288 | 289 | /** 290 | * @dev Invariant G -- "Difference between range numbers of smallest-numbered descendents of non-root ranges on level 3+" 291 | * 292 | * // QUESTION what precisely is smallest-numbered range? —- think smallest index but could be smallest weight / range number 293 | * 294 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= d where level ℓ >= k >= 3, the range number of the smallest-numbered descendent range 295 | * on level ℓ-k minus the range number of the smallest-numbered descendent range on level ℓ-k+1 296 | * is greater than or equal to the base-2 power tower of order k and hat m (e.g., the base-2 power tower of 297 | * order 3 and hat 7 is 2^2^2^7, for order 4 it is 2^2^2^2^7, and so on; for more info, see 298 | * https://mathworld.wolfram.com/PowerTower.html) plus lg (2+b)/(1-b) + 1. 299 | * 300 | * (from Lemma 3ʹ) 301 | */ 302 | function MODIFIED_invariantG_differenceBetweenRangeNumbersOfSmallestNumberedDescendentsOfNonRootRangesOnLevel3AndUp( 303 | ) public { 304 | // Loop through all levels 3 and up. 305 | 306 | // Loop through this level and any levels above it. 307 | 308 | // Get the smallest-numbered descendent range on level ℓ-k. 309 | 310 | // Get the smallest-numbered descendent range on level ℓ-k+1. 311 | 312 | // Calculate the base-2 power tower of order k and hat m + lg (2+b)/(1-b) + 1. 313 | 314 | // Check that the difference between them is greater than this value + lg (2+b)/(1-b) + 1. 315 | 316 | assertTrue( 317 | false, 318 | "Difference between range numbers of smallest-numbered descendents of non-root ranges on level 3+" // TEMP 319 | ); 320 | } 321 | 322 | /** 323 | * @dev Invariant H -- "Number of descendents of non-root ranges on level 3+" 324 | * 325 | * For any non-root range Rⱼ⁽ℓ⁾ with degree m >= d where level ℓ >= k >= 3, the number of descendents on level ℓ-k >= the base-2 power 326 | * tower of order k and hat m (see Invariant E for power tower examples). 327 | * 328 | * (from Lemma 3ʹ) 329 | */ 330 | function MODIFIED_invariantH_numberOfDescendentsOfNonRootRangesOnLevel3AndUp() public { 331 | // Loop through all levels 3 and up. 332 | 333 | // Loop through this level and any levels above it. 334 | 335 | // Get the number of descendents on level ℓ-k. 336 | 337 | // Calculate the base-2 power tower of order k and hat m. 338 | 339 | // Check that the number of descendents on level ℓ-k is greater than or equal to this value. 340 | 341 | assertTrue(false, "Number of descendents of non-root ranges on level 3+"); // TEMP 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /test/LibDDRV.montecarlo.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; 7 | 8 | import "../src/LibDDRV.sol"; 9 | 10 | /// @notice Monte Carlo simulations for LibDDRV 11 | contract LibDDRVMonteCarloTest is Test { 12 | Forest internal forest; 13 | uint256[] internal seeds; 14 | mapping(uint256 => uint256) internal countElementGenerated; 15 | 16 | uint256 internal constant SEED_COUNT = 1000; 17 | 18 | function setUp() public { 19 | seeds.push(uint256(keccak256(abi.encode(0)))); 20 | for (uint256 i = 1; i < SEED_COUNT; i++) { 21 | seeds.push(uint256(keccak256(abi.encode(seeds[i - 1] + i)))); 22 | } 23 | } 24 | 25 | function testMonteCarloSim() public { 26 | // Initialize test parameters and helper data structures. 27 | uint256 numElements = 100; 28 | uint256 numRuns = 10_000; 29 | uint256 totalWeight = 0; 30 | uint256[] memory elements = new uint256[](numElements); 31 | uint256[] memory expectedProbabilities = new uint256[](numElements); 32 | 33 | // Preprocess with zero elements. 34 | // LibDDRV.preprocess(forest); 35 | 36 | // Insert 100 elements. 37 | for (uint256 i = 0; i < numElements; i++) { 38 | uint256 element = uint256(keccak256(abi.encodePacked(block.number, i))) % 200; // PRN ∈ [0, 199] 39 | totalWeight += element; 40 | elements[i] = element; 41 | 42 | LibDDRV.insert_element(forest, i, element); 43 | } 44 | 45 | // Calculate approximate expected probabilities. 46 | for (uint256 i = 0; i < numElements; i++) { 47 | uint256 expectedProbability = FixedPointMathLib.mulDivDown(elements[i], 1e4, totalWeight); // normalize P(x) to 10,000 48 | expectedProbabilities[i] = expectedProbability; 49 | 50 | emit log_named_uint("Element", i); 51 | emit log_named_uint("Weight", elements[i]); 52 | emit log_named_uint("Total Weight", totalWeight); 53 | emit log_named_uint("Expected P(x)", expectedProbability); 54 | emit log_string("---"); 55 | } 56 | 57 | // Generate 10,000 random variates. 58 | for (uint256 i = 0; i < numRuns; i++) { 59 | uint256 elementIndex = 0; // LibDDRV.generate(forest, seeds[i]); 60 | countElementGenerated[elementIndex]++; 61 | } 62 | 63 | // Compare actual count of times generated vs. expected probabilities, with 10% tolerance. 64 | for (uint256 i = 0; i < numElements; i++) { 65 | assertApproxEqRel(countElementGenerated[i], expectedProbabilities[i], 0.1e18); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/LibDDRV.unit.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | import "src/LibDDRV.sol"; 7 | 8 | /// @notice Unit tests for LibDDRV 9 | contract LibDDRVUnitTest is Test { 10 | Forest internal forest; 11 | uint256[] internal seeds; 12 | uint256 SEED_COUNT = 1000; 13 | 14 | /// @dev Convenience empty array for element Nodes without children 15 | Edge[] internal none; 16 | 17 | function setUp() public { 18 | seeds.push(uint256(keccak256(abi.encode(0)))); 19 | for (uint256 i = 1; i < SEED_COUNT; i++) { 20 | seeds.push(uint256(keccak256(abi.encode(seeds[i - 1] + i)))); 21 | } 22 | } 23 | 24 | /*////////////////////////////////////////////////////////////// 25 | // Utilities 26 | //////////////////////////////////////////////////////////////*/ 27 | 28 | /// @dev Logs forest information to the console 29 | function logForest(Forest storage f) internal view { 30 | console.log("forest weight: %s", f.weight); 31 | for (uint256 level = 0; level < 10; level++) { 32 | console.log("\t level %s weight: %s", level, f.levels[level].weight); 33 | console.log("\t level %s roots: %s", level, f.levels[level].roots); 34 | for (uint256 index = 0; index < 10; index++) { 35 | if (f.levels[level].ranges[index].weight == 0) { 36 | continue; 37 | } 38 | console.log("\t\t level %s index %s weight: %s", level, index, f.levels[level].ranges[index].weight); 39 | console.log( 40 | "\t\t level %s index %s children: %s", level, index, f.levels[level].ranges[index].children.length 41 | ); 42 | } 43 | } 44 | } 45 | 46 | /// @dev Writes the range of values [start, end) to the weights array, starting at index 47 | function addRange(uint256 index, uint256 start, uint256 end, uint256[] memory weights) 48 | internal 49 | pure 50 | returns (uint256[] memory) 51 | { 52 | for (uint256 i = start; i < end; i++) { 53 | weights[index++] = i; 54 | } 55 | return weights; 56 | } 57 | 58 | /// @dev Assertion helper for Node struct. Note this only checks children.length, not the actual children. 59 | function assertEq(Node memory a, Node memory b, string memory message) internal { 60 | assertEq(a.index, b.index, message); 61 | assertEq(a.weight, b.weight, message); 62 | assertEq(a.children.length, b.children.length, message); 63 | } 64 | 65 | /// @dev Assertion helper for Edge struct. TODO TBD if needed. 66 | function assertEq(Edge memory a, Edge memory b, string memory message) internal { 67 | assertEq(a.level, b.level, message); 68 | assertEq(a.index, b.index, message); 69 | } 70 | 71 | /// @dev Finds the Node to which a given Edge connects. 72 | function nodeFor(Edge memory edge) internal view returns (Node memory) { 73 | return forest.levels[edge.level].ranges[edge.index]; 74 | } 75 | 76 | /*////////////////////////////////////////////////////////////// 77 | // Preprocess 78 | //////////////////////////////////////////////////////////////*/ 79 | 80 | function testPreprocess_simple() public { 81 | uint256[] memory weights = new uint256[](2); 82 | weights[0] = 50; 83 | weights[1] = 50; 84 | 85 | LibDDRV.preprocess(forest, weights); 86 | 87 | // total weight should be the sum 88 | assertEq(forest.weight, 100); 89 | 90 | // The two weights should exist as leaves on level 0 91 | assertEq(forest.levels[0].ranges[0].weight, 50); 92 | assertEq(forest.levels[0].ranges[1].weight, 50); 93 | 94 | logForest(forest); 95 | 96 | // two elements should be in the only range on level 1 97 | assertEq(forest.levels[0].weight, 100); 98 | } 99 | 100 | /* 101 | R2,5 Step 3: pop enqueued range R2,5 from queue and check if it has >1 child 102 | / it does not, so add R2,5 to the roots on level 2 103 | / Done! 104 | / 105 | / 106 | / 107 | R1,3 Step 2: pop enqueued range R1,3 from queue, and check if it has >1 child 108 | / | \ \ it does, so calculate L2 range that R1,3 falls into 109 | / | \ \ ilg2(4+5+6+7) = 5 110 | / | \ \ add, enqueue R2,5 111 | / | | \ 112 | 4 5 6 7 Step 1: 4,5,6,7 e [2^2, 2^3] => 2^j-1 = 2^2 => j = 3; 113 | Create range R1,3 and augment weight; 114 | enqueue R1,3 115 | */ 116 | // Test that the forest is built correctly when there are more 4 elements 117 | function testPreprocess_oneTree() public { 118 | uint256[] memory weights = new uint256[](4); 119 | uint256 expectedWeight = 22; //E i [4,7] 120 | addRange(0, 4, 8, weights); 121 | 122 | LibDDRV.preprocess(forest, weights); 123 | 124 | logForest(forest); 125 | 126 | // total weight should be the sum 127 | assertEq(forest.weight, expectedWeight, "assert forest weight"); 128 | assertEq(forest.levels[0].weight, expectedWeight, "assert l0 weight"); 129 | // Should be zero, since we only add weight for root ranges 130 | assertEq(forest.levels[1].weight, 0, "assert l1 weight"); 131 | assertEq(forest.levels[2].weight, expectedWeight, "assert l2 weight"); 132 | 133 | uint256 l1RangeIndex = LibDDRV.floor_ilog(7) + 1; 134 | uint256 l2RangeIndex = LibDDRV.floor_ilog(expectedWeight) + 1; 135 | 136 | console.log("l1 index %s", l1RangeIndex); 137 | console.log("l2 index %s", l2RangeIndex); 138 | 139 | // range weighs 22, and is not a root range 140 | assertEq(forest.levels[1].ranges[l1RangeIndex].weight, 22, "assert l1 index range weight"); 141 | assertEq(forest.levels[1].roots, 0, "assert no roots in l1"); 142 | // assert children for l1 range 143 | assertEq(forest.levels[1].ranges[l1RangeIndex].children.length, 4, "assert l1 index range children length"); 144 | 145 | // range weighs 22, and is the only root range 146 | assertEq(forest.levels[2].ranges[l2RangeIndex].weight, expectedWeight, "assert l2 index range weight"); 147 | assertEq(forest.levels[2].roots, l2RangeIndex, "assert roots in l2"); 148 | // assert children for l2 range 149 | assertEq(forest.levels[2].ranges[l2RangeIndex].children.length, 1, "assert l2 index range children length"); 150 | } 151 | 152 | function testPreprocess_twoTrees() public { 153 | // A test which adds weights 4-7, and 64-127 to the forest 154 | uint256 expectedWeight = 6134; // E(64-127) == 6112, + 22 = 6134 155 | uint256[] memory weights = new uint256[](68); 156 | addRange(0, 4, 8, weights); // -> elts into R1,3; R1,3 into R2,5 (E(4-7) == 22, ilg2(22) = 5) 157 | addRange(4, 64, 128, weights); // -> elts into R1,7; R1,7 into R2,13 (ilg2(6112) = 13) 158 | 159 | LibDDRV.preprocess(forest, weights); 160 | assertEq(forest.weight, expectedWeight); 161 | assertEq(forest.levels[0].weight, expectedWeight); 162 | // Should be zero, since we only add weight for root ranges 163 | assertEq(forest.levels[1].weight, 0); 164 | assertEq(forest.levels[1].roots, 0); 165 | 166 | // roots field is the sum of the root range indices 167 | assertEq(forest.levels[2].roots, 5 + 13); 168 | assertEq(forest.levels[2].weight, expectedWeight); 169 | 170 | // R1,3 should have weight 22, and 4 children 171 | assertEq(forest.levels[1].ranges[3].weight, 22); 172 | assertEq(forest.levels[1].ranges[3].children.length, 4); 173 | 174 | // R1,7 should have weight 6112, and 64 children 175 | assertEq(forest.levels[1].ranges[7].weight, 6112); 176 | assertEq(forest.levels[1].ranges[7].children.length, 64); 177 | 178 | // R2,5 should have weight 22, and 1 child 179 | assertEq(forest.levels[2].ranges[5].weight, 22); 180 | assertEq(forest.levels[2].ranges[5].children.length, 1); 181 | 182 | // R2,13 should have weight 6112, and 1 child 183 | assertEq(forest.levels[2].ranges[13].weight, 6112); 184 | assertEq(forest.levels[2].ranges[13].children.length, 1); 185 | } 186 | 187 | /*////////////////////////////////////////////////////////////// 188 | // Insert 189 | //////////////////////////////////////////////////////////////*/ 190 | 191 | function test_insert() public { 192 | // LibDDRV.insert_element(0, 5, forest); 193 | 194 | assertEq(forest.weight, 5, "forest weight"); 195 | assertEq(forest.levels[0].weight, 5, "level 0 weight"); 196 | assertEq(forest.levels[1].weight, 5, "level 1 weight"); 197 | } 198 | 199 | function testFuzz_insert(uint256 weight) public { 200 | // LibDDRV.insert_element(0, weight, forest); 201 | 202 | assertEq(forest.weight, weight, "forest weight"); 203 | assertEq(forest.levels[0].weight, weight, "level 0 weight"); 204 | assertEq(forest.levels[1].weight, weight, "level 1 weight"); 205 | } 206 | 207 | /*////////////////////////////////////////////////////////////// 208 | // Update 209 | //////////////////////////////////////////////////////////////*/ 210 | 211 | function testUpdate_simple() public { 212 | uint256[] memory weights = new uint256[](2); 213 | weights[0] = 50; 214 | weights[1] = 50; 215 | 216 | LibDDRV.preprocess(forest, weights); 217 | // Log2(50) = 6, so the range is R1,6 218 | assertEq(forest.levels[1].ranges[6].weight, 100); 219 | // R1,6 should have two children 220 | assertEq(forest.levels[1].ranges[6].children.length, 2); 221 | // R2,7 should have weight 100, and 1 child 222 | assertEq(forest.levels[2].ranges[7].weight, 100); 223 | assertEq(forest.levels[2].ranges[7].children.length, 1); 224 | 225 | console.log("updating element"); 226 | 227 | LibDDRV.update_element(forest, 0, 30); 228 | assertEq(forest.levels[0].weight, 80, "l0 weight is 80"); 229 | assertEq(forest.levels[0].ranges[0].weight, 30, "first elt weight is 30"); 230 | assertEq(forest.levels[0].ranges[1].weight, 50, "second elt weight is 50"); 231 | assertEq(forest.weight, 80, "forest weight is 80"); 232 | assertEq(forest.levels[1].ranges[6].weight, 80, "R1,6 is 80"); 233 | } 234 | 235 | modifier withUpdateBackground() { 236 | // Given The Forest contains the following 11 Elements: 237 | // | Element | Weight | 238 | // | 1 | 10 | 239 | // | 2 | 5 | 240 | // | 3 | 15 | 241 | // | 4 | 20 | 242 | // | 5 | 5 | 243 | // | 6 | 5 | 244 | // | 7 | 5 | 245 | // | 8 | 5 | 246 | // | 9 | 10 | 247 | // | 10 | 10 | 248 | // | 11 | 10 | 249 | uint256[] memory weights = new uint256[](11); 250 | weights[0] = 10; 251 | weights[1] = 5; 252 | weights[2] = 15; 253 | weights[3] = 20; 254 | weights[4] = 5; 255 | weights[5] = 5; 256 | weights[6] = 5; 257 | weights[7] = 5; 258 | weights[8] = 10; 259 | weights[9] = 10; 260 | weights[10] = 10; 261 | LibDDRV.preprocess(weights, forest); 262 | 263 | // And The total weight of the Forest is 100 264 | assertEq(forest.weight, 100, "Forest total weight"); 265 | 266 | // And There are 2 Levels in the Forest 267 | // assertEq(forest.levels.length, 2, "Levels count"); 268 | 269 | // And The weight of Level 1 is 20 270 | assertEq(forest.levels[1].weight, 20, "Level 1 weight"); 271 | 272 | // And The weight of Level 2 is 80 273 | assertEq(forest.levels[2].weight, 80, "Level 2 weight"); 274 | 275 | // And The weight of Range R₃⁽¹⁾ should be 25 276 | assertEq(forest.levels[1].ranges[3].index, 3, unicode"Range R₃⁽¹⁾ index"); 277 | assertEq(forest.levels[1].ranges[3].weight, 25, unicode"Range R₃⁽¹⁾ weight"); 278 | 279 | // And The weight of Range R₄⁽¹⁾ should be 55 280 | assertEq(forest.levels[1].ranges[4].index, 4, unicode"Range R₄⁽¹⁾ index"); 281 | assertEq(forest.levels[1].ranges[4].weight, 55, unicode"Range R₄⁽¹⁾ weight"); 282 | 283 | // And The weight of Range R₅⁽¹⁾ should be 20 284 | assertEq(forest.levels[1].ranges[5].index, 5, unicode"Range R₅⁽¹⁾ index"); 285 | assertEq(forest.levels[1].ranges[5].weight, 20, unicode"Range R₅⁽¹⁾ weight"); 286 | 287 | // And The weight of Range R₅⁽²⁾ should be 25 288 | assertEq(forest.levels[2].ranges[5].index, 5, unicode"Range R₅⁽²⁾ index"); 289 | assertEq(forest.levels[2].ranges[5].weight, 25, unicode"Range R₅⁽²⁾ weight"); 290 | 291 | // And The weight of Range R₆⁽²⁾ should be 55 292 | assertEq(forest.levels[2].ranges[6].index, 6, unicode"Range R₆⁽²⁾ index"); 293 | assertEq(forest.levels[2].ranges[6].weight, 55, unicode"Range R₆⁽²⁾ weight"); 294 | 295 | // And The Forest has the following structure: 296 | // | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 297 | // | 2 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 298 | // | 5 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 299 | // | 6 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 300 | // | 7 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 301 | // | 8 | 5 | R₃⁽¹⁾ | (25) | R₅⁽²⁾ | 25 | 302 | // | 1 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 303 | // | 3 | 15 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 304 | // | 9 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 305 | // | 10 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 306 | // | 11 | 10 | R₄⁽¹⁾ | (55) | R₆⁽²⁾ | 55 | 307 | // | 4 | 20 | R₅⁽¹⁾ | 20 | | | 308 | 309 | // Check the children of parent Range R₃⁽¹⁾ 310 | assertEq(forest.levels[1].ranges[3].children.length, 5, unicode"Range R₃⁽¹⁾ children count"); 311 | assertEq( 312 | nodeFor(forest.levels[1].ranges[3].children[0]), 313 | Node({index: 1, weight: 5, children: none}), 314 | unicode"Range R₃⁽¹⁾ child 1" 315 | ); 316 | assertEq( 317 | nodeFor(forest.levels[1].ranges[3].children[1]), 318 | Node({index: 4, weight: 5, children: none}), 319 | unicode"Range R₃⁽¹⁾ child 2" 320 | ); 321 | assertEq( 322 | nodeFor(forest.levels[1].ranges[3].children[2]), 323 | Node({index: 5, weight: 5, children: none}), 324 | unicode"Range R₃⁽¹⁾ child 3" 325 | ); 326 | assertEq( 327 | nodeFor(forest.levels[1].ranges[3].children[3]), 328 | Node({index: 6, weight: 5, children: none}), 329 | unicode"Range R₃⁽¹⁾ child 4" 330 | ); 331 | assertEq( 332 | nodeFor(forest.levels[1].ranges[3].children[4]), 333 | Node({index: 7, weight: 5, children: none}), 334 | unicode"Range R₃⁽¹⁾ child 5" 335 | ); 336 | 337 | // Check the children of parent Range R₄⁽¹⁾ 338 | assertEq(forest.levels[1].ranges[4].children.length, 5, unicode"Range R₄⁽¹⁾ children count"); 339 | assertEq( 340 | nodeFor(forest.levels[1].ranges[3].children[0]), 341 | Node({index: 0, weight: 10, children: none}), 342 | unicode"Range R₄⁽¹⁾ child 1" 343 | ); 344 | assertEq( 345 | nodeFor(forest.levels[1].ranges[3].children[1]), 346 | Node({index: 2, weight: 15, children: none}), 347 | unicode"Range R₄⁽¹⁾ child 2" 348 | ); 349 | assertEq( 350 | nodeFor(forest.levels[1].ranges[3].children[2]), 351 | Node({index: 8, weight: 10, children: none}), 352 | unicode"Range R₄⁽¹⁾ child 3" 353 | ); 354 | assertEq( 355 | nodeFor(forest.levels[1].ranges[3].children[3]), 356 | Node({index: 9, weight: 10, children: none}), 357 | unicode"Range R₄⁽¹⁾ child 4" 358 | ); 359 | assertEq( 360 | nodeFor(forest.levels[1].ranges[3].children[4]), 361 | Node({index: 10, weight: 10, children: none}), 362 | unicode"Range R₄⁽¹⁾ child 5" 363 | ); 364 | 365 | // Check the children of parent Range R₅⁽¹⁾ 366 | assertEq(forest.levels[1].ranges[5].children.length, 1, unicode"Range R₅⁽¹⁾ children count"); 367 | assertEq( 368 | nodeFor(forest.levels[1].ranges[5].children[0]), 369 | Node({index: 3, weight: 20, children: none}), 370 | unicode"Range R₅⁽¹⁾ child 1" 371 | ); 372 | 373 | // Check the children of grandparent Range R₅⁽²⁾ 374 | assertEq(forest.levels[2].ranges[5].children.length, 1, unicode"Range R₅⁽²⁾ children count"); 375 | assertEq( 376 | nodeFor(forest.levels[2].ranges[5].children[0]), 377 | Node({index: 3, weight: 25, children: new Edge[](5)}), 378 | unicode"Range R₅⁽²⁾ child 1" 379 | ); 380 | 381 | // Check the children of grandparent Range R₆⁽²⁾ 382 | assertEq(forest.levels[2].ranges[6].children.length, 1, unicode"Range R₆⁽²⁾ children count"); 383 | assertEq( 384 | nodeFor(forest.levels[2].ranges[6].children[0]), 385 | Node({index: 4, weight: 55, children: new Edge[](5)}), 386 | unicode"Range R₆⁽²⁾ child 1" 387 | ); 388 | 389 | _; 390 | } 391 | 392 | // Scenario: C -- Update 1 Element, decrease weight, moves to lower range numbered-parent 393 | function test_update_scenarioC() public withUpdateBackground { 394 | // When I update Element 3 from weight 15 to weight 6 395 | LibDDRV.update_element(2, 6, forest); 396 | 397 | // Then The parent of Element 3 should now be Range R₃⁽¹⁾ 398 | // TODO 399 | 400 | // And The total weight of the Forest should be 91 401 | assertEq(forest.weight, 91, "Forest total weight"); 402 | 403 | // And There should still be 2 Levels in the Forest 404 | // 405 | 406 | // And The weight of Level 1 should still be 20 407 | assertEq(forest.levels[0].weight, 20, "Level 1 weight"); 408 | 409 | // And The weight of Level 2 should be 71 410 | assertEq(forest.levels[1].weight, 71, "Level 2 weight"); 411 | 412 | // And The weight of Range R₃⁽¹⁾ should be 31 413 | assertEq(forest.levels[1].ranges[3].index, 3, unicode"Range R₃⁽¹⁾ index"); 414 | assertEq(forest.levels[1].ranges[3].weight, 31, unicode"Range R₃⁽¹⁾ weight"); 415 | 416 | // And The weight of Range R₄⁽¹⁾ should be 40 417 | assertEq(forest.levels[1].ranges[4].index, 4, unicode"Range R₄⁽¹⁾ index"); 418 | assertEq(forest.levels[1].ranges[4].weight, 40, unicode"Range R₄⁽¹⁾ weight"); 419 | 420 | // And The weight of Range R₅⁽¹⁾ should still be 20 421 | assertEq(forest.levels[1].ranges[5].index, 5, unicode"Range R₅⁽¹⁾ index"); 422 | assertEq(forest.levels[1].ranges[5].weight, 20, unicode"Range R₅⁽¹⁾ weight"); 423 | 424 | // And The weight of Range R₅⁽²⁾ should be 31 425 | assertEq(forest.levels[2].ranges[5].index, 5, unicode"Range R₅⁽²⁾ index"); 426 | assertEq(forest.levels[2].ranges[5].weight, 31, unicode"Range R₅⁽²⁾ weight"); 427 | 428 | // And The weight of Range R₆⁽²⁾ should be 40 429 | assertEq(forest.levels[2].ranges[6].index, 6, unicode"Range R₆⁽²⁾ index"); 430 | assertEq(forest.levels[2].ranges[6].weight, 40, unicode"Range R₆⁽²⁾ weight"); 431 | 432 | // And The Forest should have the following structure: 433 | // | Element | E Weight | Parent | P Weight | Grandparent | GP Weight | 434 | // | 2 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 435 | // | 3 | 6 * | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 436 | // | 5 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 437 | // | 6 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 438 | // | 7 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 439 | // | 8 | 5 | R₃⁽¹⁾ | (31) | R₅⁽²⁾ | 31 | 440 | // | 1 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | 40 | 441 | // | 9 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | 40 | 442 | // | 10 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | 40 | 443 | // | 11 | 10 | R₄⁽¹⁾ | (40) | R₆⁽²⁾ | 40 | 444 | // | 4 | 20 | R₅⁽¹⁾ | 20 | | | 445 | 446 | // Check the children of parent Range R₃⁽¹⁾ 447 | assertEq(forest.levels[1].ranges[3].children.length, 6, unicode"Range R₃⁽¹⁾ children count"); 448 | assertEq( 449 | nodeFor(forest.levels[1].ranges[3].children[0]), 450 | Node({index: 1, weight: 5, children: none}), 451 | unicode"Range R₃⁽¹⁾ child 1" 452 | ); 453 | assertEq( 454 | nodeFor(forest.levels[1].ranges[3].children[1]), 455 | Node({index: 4, weight: 5, children: none}), 456 | unicode"Range R₃⁽¹⁾ child 2" 457 | ); 458 | assertEq( 459 | nodeFor(forest.levels[1].ranges[3].children[2]), 460 | Node({index: 5, weight: 5, children: none}), 461 | unicode"Range R₃⁽¹⁾ child 3" 462 | ); 463 | assertEq( 464 | nodeFor(forest.levels[1].ranges[3].children[3]), 465 | Node({index: 6, weight: 5, children: none}), 466 | unicode"Range R₃⁽¹⁾ child 4" 467 | ); 468 | assertEq( 469 | nodeFor(forest.levels[1].ranges[3].children[4]), 470 | Node({index: 7, weight: 5, children: none}), 471 | unicode"Range R₃⁽¹⁾ child 5" 472 | ); 473 | assertEq( 474 | nodeFor(forest.levels[1].ranges[3].children[5]), 475 | Node({index: 2, weight: 6, children: none}), 476 | unicode"Range R₃⁽¹⁾ child 6" 477 | ); 478 | 479 | // Check the children of parent Range R₄⁽¹⁾ 480 | assertEq(forest.levels[1].ranges[4].children.length, 4, unicode"Range R₄⁽¹⁾ children count"); 481 | assertEq( 482 | nodeFor(forest.levels[1].ranges[3].children[0]), 483 | Node({index: 0, weight: 10, children: none}), 484 | unicode"Range R₄⁽¹⁾ child 1" 485 | ); 486 | assertEq( 487 | nodeFor(forest.levels[1].ranges[3].children[1]), 488 | Node({index: 8, weight: 10, children: none}), 489 | unicode"Range R₄⁽¹⁾ child 2" 490 | ); 491 | assertEq( 492 | nodeFor(forest.levels[1].ranges[3].children[2]), 493 | Node({index: 9, weight: 10, children: none}), 494 | unicode"Range R₄⁽¹⁾ child 3" 495 | ); 496 | assertEq( 497 | nodeFor(forest.levels[1].ranges[3].children[3]), 498 | Node({index: 10, weight: 10, children: none}), 499 | unicode"Range R₄⁽¹⁾ child 4" 500 | ); 501 | 502 | // Check the children of parent Range R₅⁽¹⁾ 503 | assertEq(forest.levels[1].ranges[5].children.length, 1, unicode"Range R₅⁽¹⁾ children count"); 504 | assertEq( 505 | nodeFor(forest.levels[1].ranges[5].children[0]), 506 | Node({index: 3, weight: 20, children: none}), 507 | unicode"Range R₅⁽¹⁾ child 1" 508 | ); 509 | 510 | // Check the children of grandparent Range R₅⁽²⁾ 511 | assertEq(forest.levels[2].ranges[5].children.length, 1, unicode"Range R₅⁽²⁾ children count"); 512 | assertEq( 513 | nodeFor(forest.levels[2].ranges[5].children[0]), 514 | Node({index: 3, weight: 25, children: new Edge[](6)}), 515 | unicode"Range R₅⁽²⁾ child 1" 516 | ); 517 | 518 | // Check the children of grandparent Range R₆⁽²⁾ 519 | assertEq(forest.levels[2].ranges[6].children.length, 1, unicode"Range R₆⁽²⁾ children count"); 520 | assertEq( 521 | nodeFor(forest.levels[2].ranges[6].children[0]), 522 | Node({index: 4, weight: 55, children: new Edge[](4)}), 523 | unicode"Range R₆⁽²⁾ child 1" 524 | ); 525 | } 526 | 527 | // TODO convert remaining 17 Cucumber scenarios to Forge tests 528 | 529 | /*////////////////////////////////////////////////////////////// 530 | // Generate 531 | //////////////////////////////////////////////////////////////*/ 532 | 533 | function testGenerate_simple() public { 534 | uint256[] memory weights = new uint256[](2); 535 | weights[0] = 50; 536 | weights[1] = 50; 537 | 538 | LibDDRV.preprocess(forest, weights); 539 | uint256 element = LibDDRV.generate(forest, 0); 540 | assertTrue(element == 0 || element == 1); 541 | } 542 | 543 | function testGenerate() public { 544 | uint256 countHeads = 0; 545 | uint256 countTails = 0; 546 | uint256[] memory weights = new uint256[](2); 547 | weights[0] = 50; 548 | weights[1] = 50; 549 | 550 | LibDDRV.preprocess(forest, weights); 551 | 552 | // flip 1000 coins 553 | for (uint256 i = 0; i < SEED_COUNT; i++) { 554 | uint256 seed = seeds[i]; 555 | uint256 element = 0; 556 | // TODO(What is causing forge to hang on generate?) 557 | element = LibDDRV.generate(forest, seed); 558 | 559 | if (element == 0) { 560 | countTails++; 561 | } else if (element == 1) { 562 | countHeads++; 563 | } else { 564 | revert("unexpected element index returned from generate"); 565 | } 566 | } 567 | 568 | // assert these after 569 | emit log_named_uint("heads count:", countHeads); 570 | emit log_named_uint("tails count:", countTails); 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /test/utils/ContractUnderTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../../src/LibDDRV.sol"; 5 | 6 | contract ContractUnderTest { 7 | uint256 public numberOfElements; 8 | uint256 public numberOfLevels; 9 | uint256 public numberOfRanges; 10 | 11 | Forest internal forest; 12 | 13 | function preprocess(uint256[] memory weights) public { 14 | // LibDDRV.preprocess(forest, weights); 15 | } 16 | 17 | function insert_element(uint256 index, uint256 weight) public { 18 | // LibDDRV.insert_element(forest, index, weight); 19 | } 20 | 21 | function update_element(uint256 index, uint256 weight) public { 22 | // LibDDRV.update_element(forest, index, weight); 23 | } 24 | 25 | function generate(uint256 seed) public returns (uint256) { 26 | // return LibDDRV.generate(forest, seed); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/utils/Handler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {StdUtils} from "../../lib/forge-std/src/StdUtils.sol"; 5 | import {Vm} from "../../lib/forge-std/src/Vm.sol"; 6 | 7 | import {ContractUnderTest} from "./ContractUnderTest.sol"; 8 | 9 | contract Handler is StdUtils { 10 | mapping(bytes32 => uint256) public numCalls; 11 | 12 | ContractUnderTest public c; 13 | 14 | address[] internal accounts; 15 | address internal currentAccount; 16 | 17 | Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); 18 | 19 | modifier useRandomAccount(uint256 accountIndex) { 20 | currentAccount = accounts[bound(accountIndex, 0, accounts.length - 1)]; 21 | 22 | vm.startPrank(currentAccount); 23 | _; 24 | vm.stopPrank(); 25 | } 26 | 27 | constructor(address _contractUsingLib) { 28 | // Setup random accounts 29 | accounts.push(address(0xA1)); 30 | accounts.push(address(0xA2)); 31 | accounts.push(address(0xA3)); 32 | accounts.push(address(0xA4)); 33 | accounts.push(address(0xA5)); 34 | accounts.push(address(0xA6)); 35 | accounts.push(address(0xA7)); 36 | accounts.push(address(0xA8)); 37 | accounts.push(address(0xA9)); 38 | accounts.push(address(0xA10)); 39 | 40 | // Setup contract using LibDDRV 41 | c = ContractUnderTest(_contractUsingLib); 42 | } 43 | 44 | function preprocess(uint256[] memory weights, uint256 accountIndex) public useRandomAccount(accountIndex) { 45 | numCalls["preprocess"]++; 46 | c.preprocess(weights); 47 | } 48 | 49 | function insert_element(uint256 index, uint256 weight, uint256 accountIndex) 50 | public 51 | useRandomAccount(accountIndex) 52 | { 53 | numCalls["insert_element"]++; 54 | c.insert_element(index, weight); 55 | } 56 | 57 | function update_element(uint256 index, uint256 weight, uint256 accountIndex) 58 | public 59 | useRandomAccount(accountIndex) 60 | { 61 | numCalls["update_element"]++; 62 | // TODO add element insertions to ensure element exists 63 | c.update_element(index, weight); 64 | } 65 | 66 | function generate(uint256 index, uint256 seed, uint256 accountIndex) 67 | public 68 | useRandomAccount(accountIndex) 69 | returns (uint256) 70 | { 71 | numCalls["generate"]++; 72 | return c.generate(seed); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/utils/LibDDRVUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Math} from "./Math.sol"; 5 | 6 | /// TODO 7 | library LibDDRVUtils { 8 | /// TODO 9 | function calculateRangeNumber(uint256 weight) public pure returns (uint256 rangeNumber) { 10 | rangeNumber = Math.floor(Math.log2(weight)) + 1; 11 | } 12 | 13 | /// TODO 14 | function calculateToleranceBounds(uint256 b, uint256 j) 15 | public 16 | pure 17 | returns (uint256 toleranceLowerBound, uint256 toleranceUpperBound) 18 | { 19 | toleranceLowerBound = (1 - b) * (2 ** (j - 1)); 20 | toleranceUpperBound = (2 + b) * (2 ** (j - 1)); 21 | } 22 | 23 | /// TODO 24 | function calculateDegreeBound(uint256 b, uint256 c) public pure returns (uint256 d) { 25 | d = (((1 - b) / (2 + b)) ** 2 * 2 ** c) / 2; 26 | } 27 | 28 | /// TODO 29 | function isElementOfHalfOpenRange(uint256 x, uint256 lowerBound, uint256 upperBound) 30 | public 31 | pure 32 | returns (bool within) 33 | { 34 | within = (x >= lowerBound && x < upperBound); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/utils/Math.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | /// TODO 5 | library Math { 6 | /*////////////////////////////////////////////////////////////// 7 | // Arithmetic 8 | //////////////////////////////////////////////////////////////*/ 9 | 10 | /// TODO 11 | function add(uint256 x, uint256 y) public pure returns (uint256 result) { 12 | // TODO 13 | } 14 | 15 | /// TODO 16 | function sub(uint256 x, uint256 y) public pure returns (uint256 result) { 17 | // TODO 18 | } 19 | 20 | /// TODO 21 | function mul(uint256 x, uint256 y) public pure returns (uint256 result) { 22 | // TODO 23 | } 24 | 25 | /// TODO 26 | function div(uint256 x, uint256 y) public pure returns (uint256 result) { 27 | // TODO 28 | } 29 | 30 | /*////////////////////////////////////////////////////////////// 31 | // Exponents 32 | //////////////////////////////////////////////////////////////*/ 33 | 34 | /** 35 | * @notice Exponentiate 2 to the power of x. 36 | * @dev tbd 37 | * @param x The input value. 38 | * @return result 2 to the power of x. 39 | */ 40 | function pow2(uint256 x) public pure returns (uint256 result) { 41 | // TODO not sure we need 42 | } 43 | 44 | /** 45 | * @notice Calculate the base-2 "power tower" of order x, with hat m. 46 | * @dev A power tower of order 3 with hat 5 is equivalent to 2^2^2^5. A power tower 47 | * of order 5 with hat 6 is equivalent to 2^2^2^2^2^6. Supports up to TODO order. 48 | * @param x The number of times to iteratively exponentiate 2 to the power of 2. 49 | * @param m The final exponent in the power tower. 50 | * @return result The base-2 power tower of order x with hat m. 51 | */ 52 | function powerTower2(uint256 x, uint256 m) public pure returns (uint256 result) { 53 | // TODO 54 | } 55 | 56 | /*////////////////////////////////////////////////////////////// 57 | // Logarithms 58 | //////////////////////////////////////////////////////////////*/ 59 | 60 | /** 61 | * @notice Calculate the base-2 logarithm. 62 | * @dev tbd 63 | * @param x The input value. 64 | * @return result The base-2 logarithm of x. 65 | */ 66 | function log2(uint256 x) public pure returns (uint256 result) { 67 | // TODO 68 | } 69 | 70 | /** 71 | * @notice Calculate the base-2 iterated logarithm. 72 | * @dev tbd 73 | * @param x The input value. 74 | * @return result The iterative base-2 logarithm of x. 75 | */ 76 | function logStar2(uint256 x) public pure returns (uint256 result) { 77 | // TODO 78 | } 79 | 80 | /// TODO 81 | function log10(uint256 x) public pure returns (uint256 result) { 82 | // TODO 83 | } 84 | 85 | /*////////////////////////////////////////////////////////////// 86 | // Rounding 87 | //////////////////////////////////////////////////////////////*/ 88 | 89 | /** 90 | * @notice Round down to the nearest integer. 91 | * @dev tbd 92 | * @param x The input value. 93 | * @return result The value of x rounded down to the nearest integer. 94 | */ 95 | function floor(uint256 x) public pure returns (uint256 result) { 96 | // TODO 97 | } 98 | 99 | /** 100 | * @notice Round up to the nearest integer. 101 | * @dev tbd 102 | * @param x The input value. 103 | * @return result The value of x rounded up to the nearest integer. 104 | */ 105 | function ceiling(uint256 x) public pure returns (uint256 result) { 106 | // TODO 107 | } 108 | } 109 | --------------------------------------------------------------------------------