Skip to content

Commit

Permalink
feat: swap-or-not shuffle
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Jul 29, 2024
1 parent d3429c5 commit e61dfd7
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 21 deletions.
3 changes: 3 additions & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ library Errors {
error SignatureLib__CannotVerifyEmpty(); // 0xc7690a37
error SignatureLib__InvalidSignature(address expected, address recovered); // 0xd9cbae6c

// SampleLib
error SampleLib__IndexOutOfBounds(uint256 requested, uint256 bound); // 0xa12fc559

// Sequencer Selection (Leonidas)
error Leonidas__NotGod(); // 0xabc2f815
error Leonidas__EpochNotSetup(); // 0xcf4e597e
Expand Down
17 changes: 9 additions & 8 deletions l1-contracts/src/core/sequencer_selection/Leonidas.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {Errors} from "../libraries/Errors.sol";
import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol";
import {Ownable} from "@oz/access/Ownable.sol";
import {SignatureLib} from "./SignatureLib.sol";
import {SampleLib} from "./SampleLib.sol";

import {ILeonidas} from "./ILeonidas.sol";

Expand All @@ -16,7 +17,7 @@ import {ILeonidas} from "./ILeonidas.sol";
* He define the structure needed for committee and leader selection and provides logic for validating that
* the block and its "evidence" follows his rules.
*
* @dev Leonidas is depending on Ares to select warriors competently.
* @dev Leonidas is depending on Ares to add/remove warriors to/from his army competently.
*
* @dev Leonidas have one thing in mind, he provide a reference of the LOGIC going on for the spartan selection.
* He is not concerned about gas costs, he is a king, he just throw gas in the air like no-one cares.
Expand All @@ -28,7 +29,7 @@ contract Leonidas is Ownable, ILeonidas {
using SignatureLib for SignatureLib.Signature;

/**
* @notice The structure of an epoch
* @notice The data structure for an epoch
* @param committee - The validator set for the epoch
* @param sampleSeed - The seed used to sample the validator set of the epoch
* @param nextSeed - The seed used to influence the NEXT epoch
Expand Down Expand Up @@ -298,18 +299,18 @@ contract Leonidas is Ownable, ILeonidas {
}

// If we have less validators than the target committee size, we just return the full set
if (validatorSet.length() <= TARGET_COMMITTEE_SIZE) {
if (validatorSetSize <= TARGET_COMMITTEE_SIZE) {
return validatorSet.values();
}

// @todo Issue(#7603): The sampling should be improved
uint256[] memory indicies =
SampleLib.computeCommitteeClever(TARGET_COMMITTEE_SIZE, validatorSetSize, _seed);

uint256 offset = _seed % validatorSetSize;
address[] memory validators = new address[](TARGET_COMMITTEE_SIZE);
address[] memory committee = new address[](TARGET_COMMITTEE_SIZE);
for (uint256 i = 0; i < TARGET_COMMITTEE_SIZE; i++) {
validators[i] = validatorSet.at((offset + i) % validatorSetSize);
committee[i] = validatorSet.at(indicies[i]);
}
return validators;
return committee;
}

/**
Expand Down
236 changes: 236 additions & 0 deletions l1-contracts/src/core/sequencer_selection/SampleLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.18;

import {Errors} from "../libraries/Errors.sol";

/**
* @title SimpleLib
* @author Anaxandridas II
* @notice A tiny library to shuffle indices using the swap-or-not algorithm and then
* draw a committee from the shuffled indices.
*
* @dev Using the `swap-or-not` alogirthm that is used by Ethereum consensus client.
* We are using this algorithm, since it can compute a shuffle of individual indices,
* which will be very useful for EVENTUALLY reducing the cost of committee selection.
*
* Currently the library is maximally simple, and will simply do "dumb" sampling to select
* a committee, but re-use parts of computation to improve efficiency.
*
* https://eth2book.info/capella/part2/building_blocks/shuffling/
* https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf
*/
library SampleLib {
/**
* @notice Computes the shuffled index
*
* @param _index - The index to shuffle
* @param _indexCount - The total number of indices
* @param _seed - The seed to use for shuffling
*/
function computeShuffledIndex(uint256 _index, uint256 _indexCount, uint256 _seed)
internal
pure
returns (uint256)
{
if (_index >= _indexCount) {
revert Errors.SampleLib__IndexOutOfBounds(_index, _indexCount);
}
uint256 rounds = computeShuffleRounds(_indexCount);

uint256 index = _index;

for (uint256 currentRound = 0; currentRound < rounds; currentRound++) {
uint256 pivot = computePivot(_seed, currentRound, _indexCount);
index = computeInner(_seed, pivot, index, currentRound, _indexCount);
}

return index;
}

/**
* @notice Computes the original index from a shuffled index
*
* @dev Notice, that we are using same logic as `computeShuffledIndex` just looping in reverse.
*
* @param _shuffledIndex - The shuffled index
* @param _indexCount - The total number of indices
* @param _seed - The seed to use for shuffling
*
* @return index - The original index
*/
function computeOriginalIndex(uint256 _shuffledIndex, uint256 _indexCount, uint256 _seed)
internal
pure
returns (uint256)
{
if (_shuffledIndex >= _indexCount) {
revert Errors.SampleLib__IndexOutOfBounds(_shuffledIndex, _indexCount);
}

uint256 rounds = computeShuffleRounds(_indexCount);

uint256 index = _shuffledIndex;

for (uint256 currentRound = rounds; currentRound > 0; currentRound--) {
uint256 pivot = computePivot(_seed, currentRound - 1, _indexCount);
index = computeInner(_seed, pivot, index, currentRound - 1, _indexCount);
}

return index;
}

/**
* @notice Computing a committee the most direct way.
* This is horribly inefficient as we are throwing plenty of things away, but it is useful
* for testing and just showcasing the simplest case.
*
* @param _committeeSize - The size of the committee
* @param _indexCount - The total number of indices
* @param _seed - The seed to use for shuffling
*
* @return indices - The indices of the committee
*/
function computeCommitteeStupid(uint256 _committeeSize, uint256 _indexCount, uint256 _seed)
internal
pure
returns (uint256[] memory)
{
uint256[] memory indices = new uint256[](_committeeSize);

for (uint256 index = 0; index < _indexCount; index++) {
uint256 sampledIndex = computeShuffledIndex(index, _indexCount, _seed);
if (sampledIndex < _committeeSize) {
indices[sampledIndex] = index;
}
}

return indices;
}

/**
* @notice Computing a committee slightly more cleverly.
* Only computes for the committee size, and does not sample the full set.
* This is more efficient than the stupid way, but still not optimal.
* To be more clever, we can compute the `shuffeRounds` and `pivots` separately
* such that they get shared accross multiple indices.
*
* @param _committeeSize - The size of the committee
* @param _indexCount - The total number of indices
* @param _seed - The seed to use for shuffling
*
* @return indices - The indices of the committee
*/
function computeCommitteeClever(uint256 _committeeSize, uint256 _indexCount, uint256 _seed)
internal
pure
returns (uint256[] memory)
{
uint256[] memory indices = new uint256[](_committeeSize);

for (uint256 index = 0; index < _committeeSize; index++) {
uint256 originalIndex = computeOriginalIndex(index, _indexCount, _seed);
indices[index] = originalIndex;
}

return indices;
}

/**
* @notice Compute the number of shuffle rounds
*
* @dev A safe number of rounds should be 4 * log_2 N where N is the number of indices
*
* @param _count - The number of indices
*
* @return rounds - The number of rounds to shuffle
*/
function computeShuffleRounds(uint256 _count) private pure returns (uint256) {
return log2(_count) * 4;
}

/**
* @notice Computes the pivot for a given round
*
* @param _seed - The seed to use for shuffling
* @param _currentRound - The current round of shuffling
* @param _indexCount - The total number of indices
*
* @return pivot - The pivot for the round
*/
function computePivot(uint256 _seed, uint256 _currentRound, uint256 _indexCount)
private
pure
returns (uint256)
{
return uint256(keccak256(abi.encodePacked(_seed, uint8(_currentRound)))) % _indexCount;
}

/**
* @notice Computes the inner loop (one round) of a shuffle
*
* @param _seed - The seed to use for shuffling
* @param _pivot - The pivot to use for shuffling
* @param _index - The index to shuffle
* @param _currentRound - The current round of shuffling
* @param _indexCount - The total number of indices
*
* @return index - The shuffled index
*/
function computeInner(
uint256 _seed,
uint256 _pivot,
uint256 _index,
uint256 _currentRound,
uint256 _indexCount
) private pure returns (uint256) {
uint256 flip = (_pivot + _indexCount - _index) % _indexCount;
uint256 position = _index > flip ? _index : flip;
bytes32 source =
keccak256(abi.encodePacked(_seed, uint8(_currentRound), uint32(position / 256)));
uint8 byte_ = uint8(source[(position % 256) / 8]);
uint8 bit_ = (byte_ >> (position % 8)) % 2;

return bit_ == 1 ? flip : _index;
}

/**
* @notice Computes the log2 of a uint256 number
*
* @param x - The number to compute the log2 of
*
* @return y - The log2 of the number
*/
function log2(uint256 x) private pure returns (uint256 y) {
// https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogDeBruijn
assembly {
let arg := x
x := sub(x, 1)
x := or(x, div(x, 0x02))
x := or(x, div(x, 0x04))
x := or(x, div(x, 0x10))
x := or(x, div(x, 0x100))
x := or(x, div(x, 0x10000))
x := or(x, div(x, 0x100000000))
x := or(x, div(x, 0x10000000000000000))
x := or(x, div(x, 0x100000000000000000000000000000000))
x := add(x, 1)
let m := mload(0x40)
mstore(m, 0xf8f9cbfae6cc78fbefe7cdc3a1793dfcf4f0e8bbd8cec470b6a28a7a5a3e1efd)
mstore(add(m, 0x20), 0xf5ecf1b3e9debc68e1d9cfabc5997135bfb7a7a3938b7b606b5b4b3f2f1f0ffe)
mstore(add(m, 0x40), 0xf6e4ed9ff2d6b458eadcdf97bd91692de2d4da8fd2d0ac50c6ae9a8272523616)
mstore(add(m, 0x60), 0xc8c0b887b0a8a4489c948c7f847c6125746c645c544c444038302820181008ff)
mstore(add(m, 0x80), 0xf7cae577eec2a03cf3bad76fb589591debb2dd67e0aa9834bea6925f6a4a2e0e)
mstore(add(m, 0xa0), 0xe39ed557db96902cd38ed14fad815115c786af479b7e83247363534337271707)
mstore(add(m, 0xc0), 0xc976c13bb96e881cb166a933a55e490d9d56952b8d4e801485467d2362422606)
mstore(add(m, 0xe0), 0x753a6d1b65325d0c552a4d1345224105391a310b29122104190a110309020100)
mstore(0x40, add(m, 0x100))
let magic := 0x818283848586878898a8b8c8d8e8f929395969799a9b9d9e9faaeb6bedeeff
let shift := 0x100000000000000000000000000000000000000000000000000000000000000
let a := div(mul(x, magic), shift)
y := div(mload(add(m, sub(255, a))), shift)
y :=
add(y, mul(256, gt(arg, 0x8000000000000000000000000000000000000000000000000000000000000000)))
}
}
}
78 changes: 78 additions & 0 deletions l1-contracts/test/sparta/Sampling.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Aztec Labs.
pragma solidity >=0.8.18;

import {Test} from "forge-std/Test.sol";

import {SampleLib} from "../../src/core/sequencer_selection/SampleLib.sol";

// Adding a contract to get some gas-numbers out.
contract Sampler {
function computeShuffledIndex(uint256 _index, uint256 _indexCount, uint256 _seed)
public
pure
returns (uint256)
{
return SampleLib.computeShuffledIndex(_index, _indexCount, _seed);
}

function computeOriginalIndex(uint256 _index, uint256 _indexCount, uint256 _seed)
public
pure
returns (uint256)
{
return SampleLib.computeOriginalIndex(_index, _indexCount, _seed);
}

function computeCommitteeStupid(uint256 _committeeSize, uint256 _indexCount, uint256 _seed)
public
pure
returns (uint256[] memory)
{
return SampleLib.computeCommitteeStupid(_committeeSize, _indexCount, _seed);
}

function computeCommitteeClever(uint256 _committeeSize, uint256 _indexCount, uint256 _seed)
public
pure
returns (uint256[] memory)
{
return SampleLib.computeCommitteeClever(_committeeSize, _indexCount, _seed);
}
}

contract SamplingTest is Test {
Sampler sampler = new Sampler();

function testShuffle() public {
// Sizes pulled out of thin air
uint256 setSize = 1024;
uint256 commiteeSize = 32;

uint256[] memory indices = new uint256[](setSize);
for (uint256 i = 0; i < setSize; i++) {
indices[i] = i;
}

uint256[] memory shuffledIndices = new uint256[](setSize);
uint256 seed = uint256(keccak256(abi.encodePacked("seed1")));

for (uint256 i = 0; i < setSize; i++) {
shuffledIndices[i] = sampler.computeShuffledIndex(indices[i], setSize, seed);
uint256 recoveredIndex = sampler.computeOriginalIndex(shuffledIndices[i], setSize, seed);
assertEq(recoveredIndex, indices[i], "Invalid index");
}

uint256[] memory committee = sampler.computeCommitteeStupid(commiteeSize, setSize, seed);
uint256[] memory committeeClever = sampler.computeCommitteeClever(commiteeSize, setSize, seed);

for (uint256 i = 0; i < commiteeSize; i++) {
assertEq(committee[i], committeeClever[i], "Invalid index");

uint256 recoveredIndex = sampler.computeOriginalIndex(committee[i], setSize, seed);
uint256 shuffledIndex = sampler.computeShuffledIndex(recoveredIndex, setSize, seed);

assertEq(committee[i], shuffledIndex, "Invalid shuffled index");
}
}
}
Loading

0 comments on commit e61dfd7

Please sign in to comment.