Skip to content

Commit

Permalink
Merge branch 'main' into leaf-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasz-zimnoch authored Apr 29, 2022
2 parents a6d39b0 + effa14d commit 1175c08
Show file tree
Hide file tree
Showing 14 changed files with 55,881 additions and 957 deletions.
77 changes: 0 additions & 77 deletions README.adoc

This file was deleted.

63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Sortition Pools

Sortition pool is a logarithmic data structure used to store the pool of
eligible operators weighted by their stakes. In the Keep network the stake
consists of staked KEEP tokens. It allows to select a group of operators based
on the provided pseudo-random seed.

Each privileged application has its own sortition pool and is responsible for
maintaining operator weights up-to-date.

## In-Depth Reading

To familiarize yourself with the sortition pool and it's design, we provide

+ [Building Intuition](docs/building-intuition.md)
+ [Implementation Details](docs/implementation-details.md)

[Building Intuition](docs/building-intuition.md) starts the reader from the
problem description and an easy-to-understand naive solution, and then works
its way up to the current design of the sortition pool through a series of
optimizations.

[Implementation Details](docs/implementation-details.md) builds off of the
knowledge in [Building Intuition](docs/building-intuition.md), and gets into
the finer points about the data structure, data (de)serialization, how
operators join/leave the pool, and how it all comes together to select a full
group.

## Important Facts

+ The max number of operators is `2,097,152`
+ The sortition pool is for general purpose group selection. Feel free to use
or fork it!
+ The sortition pool can be [optimistic](#optimisic-group-selection)! The
on-chain code then is only run in the case that the selection submission is
challenged.

## Safe Use

Miners and other actors that can predict the selection seed (due
to frontrunning the beacon or a public cached seed being used) may be able to
manipulate selection outcomes to some degree by selectively updating the pool.

To mitigate this, applications using sortition pool should lock sortition pool
state before seed used for the new selection is known and should unlock the
pool once the selection process is over, keeping in mind potential timeouts and
result challenges.

## Optimistic Group Selection

When an application (like the [Random
Beacon](https://github.com/keep-network/keep-core/tree/main/solidity/random-beacon#group-creation))
needs a new group, sortition is performed off-chain according to the same
algorithm that would be performed on-chain, and the results are submitted
on-chain.

Then, we enter a challenge period where anyone can claim that the submitted
results are inaccurate. If this happens, the on-chain sortition pool runs the
same group selection with the same seed and validates the results.

If the submission was invalid, the challenger is rewarded and the submitter is
punished, and we can accept another submission. If the submission was valid,
the challenger loses out on their gas, and the submitter is unaffected.
49 changes: 17 additions & 32 deletions contracts/Branch.sol
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
pragma solidity 0.8.9;

import "./Constants.sol";

/// @notice The implicit 8-ary trees of the sortition pool
/// rely on packing 8 "slots" of 32-bit values into each uint256.
/// The Branch library permits efficient calculations on these slots.
library Branch {
////////////////////////////////////////////////////////////////////////////
// Parameters for configuration

// How many bits a position uses per level of the tree;
// each branch of the tree contains 2**SLOT_BITS slots.
uint256 private constant SLOT_BITS = 3;
////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////
// Derived constants, do not touch
uint256 private constant SLOT_COUNT = 2**SLOT_BITS;
uint256 private constant SLOT_WIDTH = 256 / SLOT_COUNT;
uint256 private constant LAST_SLOT = SLOT_COUNT - 1;
uint256 private constant SLOT_MAX = (2**SLOT_WIDTH) - 1;

////////////////////////////////////////////////////////////////////////////

/// @notice Calculate the right shift required
/// to make the 32 least significant bits of an uint256
/// be the bits of the `position`th slot
Expand All @@ -31,7 +16,7 @@ library Branch {
/// I wish solidity had macros, even C macros.
function slotShift(uint256 position) internal pure returns (uint256) {
unchecked {
return position * SLOT_WIDTH;
return position * Constants.SLOT_WIDTH;
}
}

Expand All @@ -43,12 +28,12 @@ library Branch {
returns (uint256)
{
unchecked {
uint256 shiftBits = position * SLOT_WIDTH;
uint256 shiftBits = position * Constants.SLOT_WIDTH;
// Doing a bitwise AND with `SLOT_MAX`
// clears all but the 32 least significant bits.
// Because of the right shift by `slotShift(position)` bits,
// those 32 bits contain the 32 bits in the `position`th slot of `node`.
return (node >> shiftBits) & SLOT_MAX;
return (node >> shiftBits) & Constants.SLOT_MAX;
}
}

Expand All @@ -59,7 +44,7 @@ library Branch {
returns (uint256)
{
unchecked {
uint256 shiftBits = position * SLOT_WIDTH;
uint256 shiftBits = position * Constants.SLOT_WIDTH;
// Shifting `SLOT_MAX` left by `slotShift(position)` bits
// gives us a number where all bits of the `position`th slot are set,
// and all other bits are unset.
Expand All @@ -71,7 +56,7 @@ library Branch {
// Bitwise ANDing the original `node` with this number
// sets the bits of `position`th slot to zero,
// leaving all other bits unchanged.
return node & ~(SLOT_MAX << shiftBits);
return node & ~(Constants.SLOT_MAX << shiftBits);
}
}

Expand All @@ -86,17 +71,17 @@ library Branch {
uint256 weight
) internal pure returns (uint256) {
unchecked {
uint256 shiftBits = position * SLOT_WIDTH;
uint256 shiftBits = position * Constants.SLOT_WIDTH;
// Clear the `position`th slot like in `clearSlot()`.
uint256 clearedNode = node & ~(SLOT_MAX << shiftBits);
uint256 clearedNode = node & ~(Constants.SLOT_MAX << shiftBits);
// Bitwise AND `weight` with `SLOT_MAX`
// to clear all but the 32 least significant bits.
//
// Shift this left by `slotShift(position)` bits
// to obtain a uint256 with all bits unset
// except in the `position`th slot
// which contains the 32-bit value of `weight`.
uint256 shiftedWeight = (weight & SLOT_MAX) << shiftBits;
uint256 shiftedWeight = (weight & Constants.SLOT_MAX) << shiftBits;
// When we bitwise OR these together,
// all other slots except the `position`th one come from the left argument,
// and the `position`th gets filled with `weight` from the right argument.
Expand All @@ -107,14 +92,14 @@ library Branch {
/// @notice Calculate the summed weight of all slots in the `node`.
function sumWeight(uint256 node) internal pure returns (uint256 sum) {
unchecked {
sum = node & SLOT_MAX;
sum = node & Constants.SLOT_MAX;
// Iterate through each slot
// by shifting `node` right in increments of 32 bits,
// and adding the 32 least significant bits to the `sum`.
uint256 newNode = node >> SLOT_WIDTH;
uint256 newNode = node >> Constants.SLOT_WIDTH;
while (newNode > 0) {
sum += (newNode & SLOT_MAX);
newNode = newNode >> SLOT_WIDTH;
sum += (newNode & Constants.SLOT_MAX);
newNode = newNode >> Constants.SLOT_WIDTH;
}
return sum;
}
Expand Down Expand Up @@ -143,12 +128,12 @@ library Branch {
unchecked {
newIndex = index;
uint256 newNode = node;
uint256 currentSlotWeight = newNode & SLOT_MAX;
uint256 currentSlotWeight = newNode & Constants.SLOT_MAX;
while (newIndex >= currentSlotWeight) {
newIndex -= currentSlotWeight;
slot++;
newNode = newNode >> SLOT_WIDTH;
currentSlotWeight = newNode & SLOT_MAX;
newNode = newNode >> Constants.SLOT_WIDTH;
currentSlotWeight = newNode & Constants.SLOT_MAX;
}
return (slot, newIndex);
}
Expand Down
32 changes: 32 additions & 0 deletions contracts/Constants.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
pragma solidity 0.8.9;

library Constants {
////////////////////////////////////////////////////////////////////////////
// Parameters for configuration

// How many bits a position uses per level of the tree;
// each branch of the tree contains 2**SLOT_BITS slots.
uint256 constant SLOT_BITS = 3;
uint256 constant LEVELS = 7;
////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////
// Derived constants, do not touch
uint256 constant SLOT_COUNT = 2**SLOT_BITS;
uint256 constant SLOT_WIDTH = 256 / SLOT_COUNT;
uint256 constant LAST_SLOT = SLOT_COUNT - 1;
uint256 constant SLOT_MAX = (2**SLOT_WIDTH) - 1;
uint256 constant POOL_CAPACITY = SLOT_COUNT**LEVELS;

uint256 constant ID_WIDTH = SLOT_WIDTH;
uint256 constant ID_MAX = SLOT_MAX;

uint256 constant BLOCKHEIGHT_WIDTH = 96 - ID_WIDTH;
uint256 constant BLOCKHEIGHT_MAX = (2**BLOCKHEIGHT_WIDTH) - 1;

uint256 constant SLOT_POINTER_MAX = (2**SLOT_BITS) - 1;
uint256 constant LEAF_FLAG = 1 << 255;

uint256 constant WEIGHT_WIDTH = 256 / SLOT_COUNT;
////////////////////////////////////////////////////////////////////////////
}
33 changes: 7 additions & 26 deletions contracts/Leaf.sol
Original file line number Diff line number Diff line change
@@ -1,28 +1,8 @@
pragma solidity 0.8.9;

library Leaf {
////////////////////////////////////////////////////////////////////////////
// Parameters for configuration

// How many bits a position uses per level of the tree;
// each branch of the tree contains 2**SLOT_BITS slots.
uint256 private constant SLOT_BITS = 3;
////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////
// Derived constants, do not touch
uint256 private constant SLOT_COUNT = 2**SLOT_BITS;
uint256 private constant SLOT_WIDTH = 256 / SLOT_COUNT;
uint256 private constant SLOT_MAX = (2**SLOT_WIDTH) - 1;

uint256 private constant ID_WIDTH = SLOT_WIDTH;
uint256 private constant ID_MAX = SLOT_MAX;

uint256 private constant BLOCKHEIGHT_WIDTH = 96 - ID_WIDTH;
uint256 private constant BLOCKHEIGHT_MAX = (2**BLOCKHEIGHT_WIDTH) - 1;

////////////////////////////////////////////////////////////////////////////
import "./Constants.sol";

library Leaf {
function make(
address _operator,
uint256 _creationBlock,
Expand All @@ -35,10 +15,11 @@ library Leaf {
uint256 op = uint256(bytes32(bytes20(_operator)));
// Bitwise AND the id to erase
// all but the 32 least significant bits
uint256 uid = _id & ID_MAX;
uint256 uid = _id & Constants.ID_MAX;
// Erase all but the 64 least significant bits,
// then shift left by 32 bits to make room for the id
uint256 cb = (_creationBlock & BLOCKHEIGHT_MAX) << ID_WIDTH;
uint256 cb = (_creationBlock & Constants.BLOCKHEIGHT_MAX) <<
Constants.ID_WIDTH;
// Bitwise OR them all together to get
// [address operator || uint64 creationBlock || uint32 id]
return (op | cb | uid);
Expand All @@ -52,12 +33,12 @@ library Leaf {

/// @notice Return the block number the leaf was created in.
function creationBlock(uint256 leaf) internal pure returns (uint256) {
return ((leaf >> ID_WIDTH) & BLOCKHEIGHT_MAX);
return ((leaf >> Constants.ID_WIDTH) & Constants.BLOCKHEIGHT_MAX);
}

function id(uint256 leaf) internal pure returns (uint32) {
// Id is stored in the 32 least significant bits.
// Bitwise AND ensures that we only get the contents of those bits.
return uint32(leaf & ID_MAX);
return uint32(leaf & Constants.ID_MAX);
}
}
Loading

0 comments on commit 1175c08

Please sign in to comment.