diff --git a/src/challenges/01_Merkle.sol b/src/challenges/01_Merkle.sol index e9691d7..79f112f 100644 --- a/src/challenges/01_Merkle.sol +++ b/src/challenges/01_Merkle.sol @@ -132,7 +132,6 @@ contract MerkleMystery { uint256 value, bytes calldata targetData ) internal view { - // Use address decoder to get addresses in call data. (bool success, bytes memory returndata) = decoderAndSanitizer.staticcall(targetData); if (!success) { assembly { diff --git a/src/challenges/03_WeightedRandomSample.md b/src/challenges/03_WeightedRandomSample.md new file mode 100644 index 0000000..e9a69ed --- /dev/null +++ b/src/challenges/03_WeightedRandomSample.md @@ -0,0 +1,73 @@ +# Weighted Random Sampling Interview Challenge + +This problem is split into multiple stages to discuss data structures and complexity. + +## Stage 1a: The Naive Array + +**Prompt:** +"Imagine we are building a lottery. Users deposit tokens (stake), and we need to pick a winner. The more tokens you have, the higher your chance of winning. How would you store this and pick a winner?" + +**Discussion:** +- Propose a data structure. +- Analyze the time complexity for: + - Insert + - Update + - Removal + - Draw (Picking a winner) + +**Problem:** +How can we improve the draw time complexity? + +## Stage 1b: The Naive Ticketing + +**Prompt:** +"What if we added an entry for every single token unit? For example, if Alice has 20 tokens, we add 20 entries. If Bob has 30, add 30 entries." + +**Discussion:** +- Analyze the complexity for Insert, Removal, and Update. +- Why is this approach generally not tenable for a smart contract? + +**transition:** +Can we combine these entries into a bucket? Think about "buckets of buckets" (Sum Tree). + +## Stage 2: The "Prefix Sum" Array + +**Prompt:** +"How can we make the drawing process faster? What if we pre-calculate the cumulative sums?" + +**Discussion:** +- Propose a data structure where `sums[i] = stake[0] + ... + stake[i]`. +- Analyze the time complexity for: + - Insert + - Removal + - Draw (Binary Search) + - Update + +**Problem:** +Updating one person's stake at the beginning of the array forces us to update every subsequent element in the prefix sum array. Update becomes O(n). How can we prevent ourselves from updating every subsequent element? + +## Stage 3: The Sum Tree + +**Prompt:** +"We need both the Draw and the Update to be fast. We've seen that arrays make one fast and the other slow. Is there a data structure that handles both logarithmic updates and logarithmic searches?" + +**Discussion:** +- Design a tree structure where internal nodes store the sum of their children. +- Analyze the time complexity for: + - Insert + - Update + - Removal + - Draw + +## Stage 4: Implementation & Optimization + +**Prompt:** +1. **The Array Representation:** + - Can we represent a K-ary tree using a flat array (where index `i` has children at `K*i + 1` ... `K*i + K`)? + +2. **Managing Vacancy:** + - How do we handle users leaving the tree without leaving "dead" empty leaves that make the tree unnecessarily deep? + +3. **The Branching Factor (K):** + - Discuss the trade-off of the `K` parameter. + - What happens with a higher `K`? (Tree depth vs. Draw cost per node) diff --git a/src/challenges/03_WeightedRandomSample.sol b/src/challenges/03_WeightedRandomSample.sol deleted file mode 100644 index 2053404..0000000 --- a/src/challenges/03_WeightedRandomSample.sol +++ /dev/null @@ -1,273 +0,0 @@ -// 1. Design a data structure that enables a stake-weighted random sampling of a user. -// - i.e. If User A has 20 ETH staked and user B has 80 ETH staked, there should be a 20% chance of selecting user A and an 80% chance of selecting user B. - -pragma solidity ^0.8.21; - -library SortitionSumTreeFactory { - /* Structs */ - - struct SortitionSumTree { - uint K; // The maximum number of childs per node. - // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. - uint[] stack; - uint[] nodes; - // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. - mapping(bytes32 => uint) IDsToNodeIndexes; - mapping(uint => bytes32) nodeIndexesToIDs; - } - - /* Storage */ - - struct SortitionSumTrees { - mapping(bytes32 => SortitionSumTree) sortitionSumTrees; - } - - /* internal */ - - /** - * @dev Create a sortition sum tree at the specified key. - * @param _key The key of the new tree. - * @param _K The number of children each node in the tree should have. - */ - function createTree(SortitionSumTrees storage self, bytes32 _key, uint _K) internal { - SortitionSumTree storage tree = self.sortitionSumTrees[_key]; - require(tree.K == 0, "Tree already exists."); - require(_K > 1, "K must be greater than one."); - tree.K = _K; - tree.stack = new uint[](0); - tree.nodes = new uint[](0); - tree.nodes.push(0); - } - - /** - * @dev Set a value of a tree. - * @param _key The key of the tree. - * @param _value The new value. - * @param _ID The ID of the value. - * `O(log_k(n))` where - * `k` is the maximum number of childs per node in the tree, - * and `n` is the maximum number of nodes ever appended. - */ - function set(SortitionSumTrees storage self, bytes32 _key, uint _value, bytes32 _ID) internal { - SortitionSumTree storage tree = self.sortitionSumTrees[_key]; - uint treeIndex = tree.IDsToNodeIndexes[_ID]; - - if (treeIndex == 0) { // No existing node. - if (_value != 0) { // Non zero value. - // Append. - // Add node. - if (tree.stack.length == 0) { // No vacant spots. - // Get the index and append the value. - treeIndex = tree.nodes.length; - tree.nodes.push(_value); - - // Potentially append a new node and make the parent a sum node. - if (treeIndex != 1 && (treeIndex - 1) % tree.K == 0) { // Is first child. - uint parentIndex = treeIndex / tree.K; - bytes32 parentID = tree.nodeIndexesToIDs[parentIndex]; - uint newIndex = treeIndex + 1; - tree.nodes.push(tree.nodes[parentIndex]); - delete tree.nodeIndexesToIDs[parentIndex]; - tree.IDsToNodeIndexes[parentID] = newIndex; - tree.nodeIndexesToIDs[newIndex] = parentID; - } - } else { // Some vacant spot. - // Pop the stack and append the value. - treeIndex = tree.stack[tree.stack.length - 1]; - tree.stack.pop(); - tree.nodes[treeIndex] = _value; - } - - // Add label. - tree.IDsToNodeIndexes[_ID] = treeIndex; - tree.nodeIndexesToIDs[treeIndex] = _ID; - - updateParents(self, _key, treeIndex, true, _value); - } - } else { // Existing node. - if (_value == 0) { // Zero value. - // Remove. - // Remember value and set to 0. - uint value = tree.nodes[treeIndex]; - tree.nodes[treeIndex] = 0; - - // Push to stack. - tree.stack.push(treeIndex); - - // Clear label. - delete tree.IDsToNodeIndexes[_ID]; - delete tree.nodeIndexesToIDs[treeIndex]; - - updateParents(self, _key, treeIndex, false, value); - } else if (_value != tree.nodes[treeIndex]) { // New, non zero value. - // Set. - bool plusOrMinus = tree.nodes[treeIndex] <= _value; - uint plusOrMinusValue = plusOrMinus ? _value - tree.nodes[treeIndex] : tree.nodes[treeIndex] - _value; - tree.nodes[treeIndex] = _value; - - updateParents(self, _key, treeIndex, plusOrMinus, plusOrMinusValue); - } - } - } - - /* internal Views */ - - /** - * @dev Query the leaves of a tree. Note that if `startIndex == 0`, the tree is empty and the root node will be returned. - * @param _key The key of the tree to get the leaves from. - * @param _cursor The pagination cursor. - * @param _count The number of items to return. - * @return startIndex The index at which leaves start - * @return values The values of the returned leaves - * @return hasMore Whether there are more for pagination. - * `O(n)` where - * `n` is the maximum number of nodes ever appended. - */ - function queryLeafs( - SortitionSumTrees storage self, - bytes32 _key, - uint _cursor, - uint _count - ) internal view returns(uint startIndex, uint[] memory values, bool hasMore) { - SortitionSumTree storage tree = self.sortitionSumTrees[_key]; - - // Find the start index. - for (uint i = 0; i < tree.nodes.length; i++) { - if ((tree.K * i) + 1 >= tree.nodes.length) { - startIndex = i; - break; - } - } - - // Get the values. - uint loopStartIndex = startIndex + _cursor; - values = new uint[](loopStartIndex + _count > tree.nodes.length ? tree.nodes.length - loopStartIndex : _count); - uint valuesIndex = 0; - for (uint j = loopStartIndex; j < tree.nodes.length; j++) { - if (valuesIndex < _count) { - values[valuesIndex] = tree.nodes[j]; - valuesIndex++; - } else { - hasMore = true; - break; - } - } - } - - /** - * @dev Draw an ID from a tree using a number. Note that this function reverts if the sum of all values in the tree is 0. - * @param _key The key of the tree. - * @param _drawnNumber The drawn number. - * @return ID The drawn ID. - * `O(k * log_k(n))` where - * `k` is the maximum number of childs per node in the tree, - * and `n` is the maximum number of nodes ever appended. - */ - function draw(SortitionSumTrees storage self, bytes32 _key, uint _drawnNumber) internal view returns(bytes32 ID) { - // TODO To Implement. - SortitionSumTree storage tree = self.sortitionSumTrees[_key]; - uint treeIndex = 0; - uint currentDrawnNumber = _drawnNumber % tree.nodes[0]; - - while ((tree.K * treeIndex) + 1 < tree.nodes.length) // While it still has children. - for (uint i = 1; i <= tree.K; i++) { // Loop over children. - uint nodeIndex = (tree.K * treeIndex) + i; - uint nodeValue = tree.nodes[nodeIndex]; - - if (currentDrawnNumber >= nodeValue) currentDrawnNumber -= nodeValue; // Go to the next child. - else { // Pick this child. - treeIndex = nodeIndex; - break; - } - } - - ID = tree.nodeIndexesToIDs[treeIndex]; - } - - /** @dev Gets a specified ID's associated value. - * @param _key The key of the tree. - * @param _ID The ID of the value. - * @return value The associated value. - */ - function stakeOf(SortitionSumTrees storage self, bytes32 _key, bytes32 _ID) internal view returns(uint value) { - SortitionSumTree storage tree = self.sortitionSumTrees[_key]; - uint treeIndex = tree.IDsToNodeIndexes[_ID]; - - if (treeIndex == 0) value = 0; - else value = tree.nodes[treeIndex]; - } - - function total(SortitionSumTrees storage self, bytes32 _key) internal view returns (uint) { - SortitionSumTree storage tree = self.sortitionSumTrees[_key]; - if (tree.nodes.length == 0) { - return 0; - } else { - return tree.nodes[0]; - } - } - - /* Private */ - - /** - * @dev Update all the parents of a node. - * @param _key The key of the tree to update. - * @param _treeIndex The index of the node to start from. - * @param _plusOrMinus Wether to add (true) or substract (false). - * @param _value The value to add or substract. - * `O(log_k(n))` where - * `k` is the maximum number of childs per node in the tree, - * and `n` is the maximum number of nodes ever appended. - */ - function updateParents(SortitionSumTrees storage self, bytes32 _key, uint _treeIndex, bool _plusOrMinus, uint _value) private { - SortitionSumTree storage tree = self.sortitionSumTrees[_key]; - - uint parentIndex = _treeIndex; - while (parentIndex != 0) { - parentIndex = (parentIndex - 1) / tree.K; - tree.nodes[parentIndex] = _plusOrMinus ? tree.nodes[parentIndex] + _value : tree.nodes[parentIndex] - _value; - } - } -} - -// Exposing contract to interact with the SortitionSumTreeFactory library in tests -contract ExposedSortitionSumTreeFactory { - using SortitionSumTreeFactory for SortitionSumTreeFactory.SortitionSumTrees; - - SortitionSumTreeFactory.SortitionSumTrees private trees; - - function _createTree(bytes32 key, uint256 K) external { - trees.createTree(key, K); - } - - function _set(bytes32 key, uint256 value, bytes32 id) external { - trees.set(key, value, id); - } - - function _draw(bytes32 key, uint256 drawnNumber) external view returns (bytes32) { - return trees.draw(key, drawnNumber); - } - - function _stakeOf(bytes32 key, bytes32 id) external view returns (uint256) { - return trees.stakeOf(key, id); - } - - function _total(bytes32 key) external view returns (uint256) { - return trees.total(key); - } - - // Debug: visualize the internal nodes and ID mapping for a given tree key - function _debugTree(bytes32 key) external view returns (uint256[] memory nodes, bytes32[] memory ids) { - // Copy nodes array - SortitionSumTreeFactory.SortitionSumTree storage tree = trees.sortitionSumTrees[key]; - uint256 len = tree.nodes.length; - nodes = new uint256[](len); - for (uint256 i; i < len; ++i) { - nodes[i] = tree.nodes[i]; - } - // Collect IDs per index (0 may be empty: root has no ID) - ids = new bytes32[](len); - for (uint256 i; i < len; ++i) { - ids[i] = tree.nodeIndexesToIDs[i]; - } - } -}