diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 4dbe4cec8d8f..ce8c5723b654 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -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 diff --git a/l1-contracts/src/core/sequencer_selection/Leonidas.sol b/l1-contracts/src/core/sequencer_selection/Leonidas.sol index 85718db8235e..6b3d69b4a97e 100644 --- a/l1-contracts/src/core/sequencer_selection/Leonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/Leonidas.sol @@ -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"; @@ -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. @@ -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 @@ -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; } /** diff --git a/l1-contracts/src/core/sequencer_selection/SampleLib.sol b/l1-contracts/src/core/sequencer_selection/SampleLib.sol new file mode 100644 index 000000000000..8ccdde0ad3e9 --- /dev/null +++ b/l1-contracts/src/core/sequencer_selection/SampleLib.sol @@ -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))) + } + } +} diff --git a/l1-contracts/test/sparta/Sampling.t.sol b/l1-contracts/test/sparta/Sampling.t.sol new file mode 100644 index 000000000000..35c349ad7464 --- /dev/null +++ b/l1-contracts/test/sparta/Sampling.t.sol @@ -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"); + } + } +} diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index b57b3546af15..5e03e8756e9a 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -64,27 +64,58 @@ contract SpartaTest is DecoderBase { } } - function _testProposerForFutureEpoch() public { - // @todo Implement + function testProposerForNonSetupEpoch(uint8 _epochsToJump) public { + uint256 pre = rollup.getCurrentEpoch(); + vm.warp(block.timestamp + uint256(_epochsToJump) * rollup.EPOCH_SIZE() * rollup.SLOT_SIZE()); + uint256 post = rollup.getCurrentEpoch(); + assertEq(pre + _epochsToJump, post, "Invalid epoch"); + + address expectedProposer = rollup.getCurrentProposer(); + + // Add a validator which will also setup the epoch + rollup.addValidator(address(0xdead)); + + address actualProposer = rollup.getCurrentProposer(); + assertEq(expectedProposer, actualProposer, "Invalid proposer"); } - function _testValidatorSetLargerThanCommittee() public { - // @todo Implement + function testValidatorSetLargerThanCommittee(bool _insufficientSigs) public { + _testBlock("mixed_block_1", false, 0, false); // We run a block before the epoch with validators + + // Adding more validators! + for (uint256 i = 5; i < 100; i++) { + uint256 privateKey = uint256(keccak256(abi.encode("validator", i))); + address validator = vm.addr(privateKey); + privateKeys[validator] = privateKey; + rollup.addValidator(validator); + } + + assertGt(rollup.getValidators().length, rollup.TARGET_COMMITTEE_SIZE(), "Not enough validators"); + + // We should not be passing here, something should be breaking! Why is the committe small? + uint256 committeSize = rollup.TARGET_COMMITTEE_SIZE() * 2 / 3 + (_insufficientSigs ? 0 : 1); + _testBlock("mixed_block_2", _insufficientSigs, committeSize, false); // We need signatures! + + assertEq( + rollup.getEpochCommittee(rollup.getCurrentEpoch()).length, + rollup.TARGET_COMMITTEE_SIZE(), + "Invalid committee size" + ); } function testHappyPath() public { - _testBlock("mixed_block_1", 0, false); // We run a block before the epoch with validators - _testBlock("mixed_block_2", 3, false); // We need signatures! + _testBlock("mixed_block_1", false, 0, false); // We run a block before the epoch with validators + _testBlock("mixed_block_2", false, 3, false); // We need signatures! } function testInvalidProposer() public { - _testBlock("mixed_block_1", 0, false); // We run a block before the epoch with validators - _testBlock("mixed_block_2", 3, true); // We need signatures! + _testBlock("mixed_block_1", false, 0, false); // We run a block before the epoch with validators + _testBlock("mixed_block_2", true, 3, true); // We need signatures! } function testInsufficientSigs() public { - _testBlock("mixed_block_1", 0, false); // We run a block before the epoch with validators - _testBlock("mixed_block_2", 2, false); // We need signatures! + _testBlock("mixed_block_1", false, 0, false); // We run a block before the epoch with validators + _testBlock("mixed_block_2", true, 2, false); // We need signatures! } struct StructToAvoidDeepStacks { @@ -93,7 +124,12 @@ contract SpartaTest is DecoderBase { bool shouldRevert; } - function _testBlock(string memory _name, uint256 _signatureCount, bool _invalidaProposer) public { + function _testBlock( + string memory _name, + bool _expectRevert, + uint256 _signatureCount, + bool _invalidaProposer + ) public { DecoderBase.Full memory full = load(_name); bytes memory header = full.block.header; bytes32 archive = full.block.archive; @@ -124,7 +160,7 @@ contract SpartaTest is DecoderBase { signatures[i] = createSignature(validators[i], archive); } - if (_signatureCount < ree.needed) { + if (_expectRevert && _signatureCount < ree.needed) { vm.expectRevert( abi.encodeWithSelector( Errors.Leonidas__InsufficientAttestations.selector, ree.needed, _signatureCount @@ -133,7 +169,7 @@ contract SpartaTest is DecoderBase { ree.shouldRevert = true; } - if (_invalidaProposer) { + if (_expectRevert && _invalidaProposer) { address realProposer = ree.proposer; ree.proposer = address(uint160(uint256(keccak256(abi.encode("invalid", ree.proposer))))); vm.expectRevert(