Part 2: Technical Overview
The Building Blocks
- Shuffling is used to randomly assign validators to committees and choose block proposers.
- Ethereum 2 uses a "swap-or-not" shuffle.
- Swap-or-not is an oblivious shuffle: it can be applied to single list elements and subsets.
- This makes it ideal for supporting light clients.
Shuffling is used to randomly assign validators to committees, both attestation committees and sync committees. It is also used to select the block proposer at each slot.
Although there are pitfalls to be aware of, shuffling is a well understood problem in computer science. The gold standard is probably the Fisher–Yates shuffle. So why aren't we using that for Eth2? In short: light clients.
Other shuffles rely on processing the entire list of elements to find the final ordering. We wish to spare light clients this burden. Ideally, they should deal with only the subsets of lists that they are interested in. Therefore, rather than Fisher–Yates, we are using a construction called a "swap-or-not" shuffle. The swap-or-not shuffle can tell you the destination index (or, conversely, the origin index) of a single list element, so is ideal when dealing with subsets of the whole validator set.
For example, formally committees are assigned by shuffling the full validator list and then taking contiguous slices of the resulting permutation. If I only need to know the members of committee , then this is very inefficient. Instead, I can run the swap-or-not shuffle backwards for only the indices in slice to find out which of the whole set of validators would be shuffled into . This is much more efficient.
The algorithm for shuffling in the specification deals with only a single index at a time.
def compute_shuffled_index(index: uint64, index_count: uint64, seed: Bytes32) -> uint64: """ Return the shuffled index corresponding to ``seed`` (and ``index_count``). """ assert index < index_count # Swap or not (https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf) # See the 'generalized domain' algorithm on page 3 for current_round in range(SHUFFLE_ROUND_COUNT): pivot = bytes_to_uint64(hash(seed + uint_to_bytes(uint8(current_round)))[0:8]) % index_count flip = (pivot + index_count - index) % index_count position = max(index, flip) source = hash( seed + uint_to_bytes(uint8(current_round)) + uint_to_bytes(uint32(position // 256)) ) byte = uint8(source[(position % 256) // 8]) bit = (byte >> (position % 8)) % 2 index = flip if bit else index return index
An index position in the list to be shuffled,
index, is provided, along with the total number of indices,
index_count, and a
seed value. The output is the index that the initial index gets shuffled to.
The hash functions used to calculate
source are deterministic, and are used to generate pseudo-random output from the inputs: given the same input, they will generate the same output. So we can see that, for given values of
seed, the routine will always return the same output.
The shuffling proceeds in rounds. In each round, a
pivot index is pseudo-randomly chosen somewhere in the list, based only on the
seed value and the round number.
Next, an index
flip is found, which is
pivot - index, after accounting for wrap-around due to the modulo function. The important points are that, given
index maps to a unique
flip, and that the calculation is symmetrical, so that
flip maps to
index_count = 100,
pivot = 70,
index = 45, we get
flip = 25.
index_count = 100,
pivot = 70,
index = 82, we get
flip = 88.
Finally in the round, a decision is made as to whether to keep the index as-is, or to update it to
flip. This decision is pseudo-randomly made based on the values of
seed, the round number, and the higher of
Note that basing the swap-or-not decision on the higher of
flip brings a symmetry to the algorithm. Whether we are considering the element at
index or the element at
flip, the decision as to whether to swap the elements or not will be the same. This is the key to seeing the that full algorithm delivers a shuffling (permutation) of the original set.
The algorithm proceeds with the next iteration based on the updated index.
It may not be immediately obvious, but since we are deterministically calculating
flip based only on the round number, the shuffle can be run in reverse simply by running from
SHUFFLE_ROUND_COUNT - 1 to
0. The same swap-or-not decisions will be made in reverse. As described above, this reverse shuffle is perfect for finding which validators ended up in a particular committee.
To get an intuition for how this single-index shuffle can deliver a full shuffling of a list of indices, we can consider how the algorithm is typically implemented in clients when shuffling an entire list at once.
As an optimisation, the loop over the indices to be shuffled is brought inside the loop over rounds. This hugely reduces the amount of hashing required since the pivot is fixed for the round (it does not depend on the index) and the bits of
source can be reused for 256 consecutive indices, since the hash has a 256-bit output.
For each round, we do the following.
First, we pick a pivot index . This is pseudo-randomly chosen, based on the round number and some other seed data. The pivot is fixed for the rest of the round.
With this pivot, we then pick the mirror index halfway between and . That is, . (We will simplify by ignoring off-by-one rounding issues for the purposes of this explanation.)
For each index between the mirror index and the pivot index , we decide whether we are going to swap the element or not.
Consider the element at index . If we choose not to swap it, we just move on to consider the next index.
If we do decide to swap, then we exchange the list element at with that at , its image in the mirror index. That is, is swapped with , so that and are equidistant from . In practice we don't exchange the elements at this point, we just update the indices , and .
We make the same swap-or-not decision for each index between and .
The decision as to whether to swap or not is based on hashing together the random seed, the round number, and some position data. A single bit is extracted from this hash for each index, and the swap is made or not according to whether this bit is one or zero.
After considering all the indices from to , mirroring in , we now find a second mirror index at , which is the point equidistant between and the end of the list: .
Finally, we repeat the swap-or-not process, considering all the points from the pivot, to the second mirror . If we choose not to swap, we just move on. If we choose to swap then we exchange the element at with its image at in the mirror index . Here, .
At the end of the round, we have considered all the indices between and , which, by construction, is half of the total indices. For each index considered, we have either left the element in place, or swapped the element at a distinct index in the other half. Thus, all of the indices have been considered exactly once for swapping.
The next round begins by incrementing (or decrementing for a reverse shuffle) the round number, which gives us a new pivot index, and off we go again.
When deciding whether to swap or not for each index, the algorithm cleverly bases its decision on the higher of the candidate index or its image in the mirror. That is, rather than (when below the pivot), and rather than (when above the pivot). This means that we have flexibility when running through the indices of the list: we could do to and to as two separate loops, or do it with a single loop from to as I outlined above. The result will be the same: it doesn't matter if we are considering or its image ; the decision as to whether to swap or not has the same outcome.
In Ethereum 2.0 we do 90 rounds of the algorithm per shuffle, set by the constant
SHUFFLE_ROUND_COUNT [TODO - link]. The original paper on which this technique is based suggests that rounds is required "to start to see a good bound on CCA-security", where is the list length. In his annotated spec Vitalik says "Expert cryptographer advice told us ~ is sufficient for safety". The absolute maximum number of validators in Eth2, thus the maximum size of the list we would ever need to shuffle, is about (4.2 million). On Vitalik's estimate that gives us 88 rounds required, on the paper's estimate, 92 rounds (assuming that is the natural logarithm). So we are in the right ballpark, especially as we are very, very unlikely to end up with that many active validators.
It might be interesting to make the number of rounds adaptive based on list length. But we don't do that; it's probably an optimisation too far.
Fun fact: when Least Authority audited the beacon chain specification, they initially found bias in the shuffling used for selecting block proposers (see Issue F in their report). This turned out to be due to mistakenly using a configuration that had only 10 rounds of shuffling. When they increased it to the 90 we use for mainnet, the bias no longer appeared.
The algorithm requires that we select a pivot point randomly in each round, and randomly choose whether to swap each element or not in each round.
In Eth2, we deterministically generate the "randomness" from a seed value, such that the same seed will always generate the same shuffling.
The pivot index is generated from eight bytes of a SHA256 hash of the seed concatenated with the round number, so it usually changes each round.
The decision bits used to determine whether or not to swap elements are bits drawn from SHA256 hashes of the seed, the round number, and the index of the element within the list.
This shuffling algorithm is much slower than Fisher–Yates. That algorithm requires swaps. Our algorithm will require swaps on average to shuffle elements.
We should also consider the generation of pseudo-randomness, which is the most expensive part of the algorithm. Fisher–Yates needs something like bits of randomness, and we need bits, which, for the range of we need in Eth2, is many more bits (about twice as many when is a million).
Why would we use such an inefficient implementation?
The brilliance is that, if we are interested in only a few indices, we do not need to compute the shuffling of the whole list. In fact, we can apply the algorithm to a single index to find out which index it will be swapped with.
So, if we want to know where the element with index 217 gets shuffled to, we can run the algorithm with only that index; we do not need to shuffle the whole list. Moreover, if we want to know the converse, which element gets shuffled into index 217, we can just run the algorithm backwards for element 217 (backwards means running the round number from high to low rather than low to high).
In summary, we can compute the destination of element in operations, and the source of element (the inverse operation) also in , not dependent on the length of the list. Shuffles like the Fisher–Yates shuffle do not have this property and cannot work with single indices, they always need to iterate the whole list. The technical term for a shuffle having this property is that it is oblivious (to all the other elements in the list).
This property is important for light clients. Light clients are observers of the Eth2 beacon and shard chains that do not store the entire state, but do wish to be able to securely access data on the chains. As part of verifying that they have the correct data – that no-one has lied to them – it is necessary to compute the committees that attested to that data. This means shuffling, and we don't want light clients to have to hold and shuffle the entire list of validators. By using the swap-or-not shuffle, light clients need only to consider the small subset of validators that they are interested in, which is vastly more efficient overall.
- The initial discussion about the search for a good shuffling algorithm is Issue 323 on the specs repo.
- The winning algorithm was announced in Issue 563.
- The original paper describing the swap-or-not shuffle is Hoang, Morris, and Rogaway, 2012, "An Enciphering Scheme Based on a Card Shuffle". See the "generalized domain" algorithm on page 3.