Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: swap-or-not shuffle #7646

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
27 changes: 14 additions & 13 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 All @@ -40,13 +41,13 @@ contract Leonidas is Ownable, ILeonidas {
}

// The size/duration of a slot in seconds, multiple of 12 to align with Ethereum blocks
uint256 public constant SLOT_SIZE = 12 * 5;
uint256 public constant SLOT_DURATION = 12 * 5;

// The size/duration of an epoch in slots
uint256 public constant EPOCH_SIZE = 32;
uint256 public constant EPOCH_DURATION = 32;

// The target number of validators in a committee
uint256 public constant TARGET_COMMITTEE_SIZE = EPOCH_SIZE;
uint256 public constant TARGET_COMMITTEE_SIZE = EPOCH_DURATION;

// The time that the contract was deployed
uint256 public immutable GENESIS_TIME;
Expand Down Expand Up @@ -157,7 +158,7 @@ contract Leonidas is Ownable, ILeonidas {
* @return The current epoch number
*/
function getCurrentEpoch() public view override(ILeonidas) returns (uint256) {
return (block.timestamp - GENESIS_TIME) / (EPOCH_SIZE * SLOT_SIZE);
return (block.timestamp - GENESIS_TIME) / (EPOCH_DURATION * SLOT_DURATION);
}

/**
Expand All @@ -166,7 +167,7 @@ contract Leonidas is Ownable, ILeonidas {
* @return The current slot number
*/
function getCurrentSlot() public view override(ILeonidas) returns (uint256) {
return (block.timestamp - GENESIS_TIME) / SLOT_SIZE;
return (block.timestamp - GENESIS_TIME) / SLOT_DURATION;
}

/**
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 SampleLib
* @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)))
}
}
}
Loading
Loading