diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 8f407c1bbc4..d2a15d03b8c 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -17,12 +17,13 @@ import {Hash} from "./libraries/Hash.sol"; import {Errors} from "./libraries/Errors.sol"; import {Constants} from "./libraries/ConstantsGen.sol"; import {MerkleLib} from "./libraries/MerkleLib.sol"; -import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; +import {SignatureLib} from "./sequencer_selection/SignatureLib.sol"; // Contracts import {MockVerifier} from "../mock/MockVerifier.sol"; import {Inbox} from "./messagebridge/Inbox.sol"; import {Outbox} from "./messagebridge/Outbox.sol"; +import {Leonidas} from "./sequencer_selection/Leonidas.sol"; /** * @title Rollup @@ -30,8 +31,7 @@ import {Outbox} from "./messagebridge/Outbox.sol"; * @notice Rollup contract that is concerned about readability and velocity of development * not giving a damn about gas costs. */ -contract Rollup is IRollup { - IVerifier public verifier; +contract Rollup is Leonidas, IRollup { IRegistry public immutable REGISTRY; IAvailabilityOracle public immutable AVAILABILITY_ORACLE; IInbox public immutable INBOX; @@ -39,6 +39,7 @@ contract Rollup is IRollup { uint256 public immutable VERSION; IERC20 public immutable GAS_TOKEN; + IVerifier public verifier; bytes32 public archive; // Root of the archive tree uint256 public lastBlockTs; // Tracks the last time time was warped on L2 ("warp" is the testing cheatcode). @@ -47,16 +48,12 @@ contract Rollup is IRollup { bytes32 public vkTreeRoot; - using EnumerableSet for EnumerableSet.AddressSet; - - EnumerableSet.AddressSet private sequencers; - constructor( IRegistry _registry, IAvailabilityOracle _availabilityOracle, IERC20 _gasToken, bytes32 _vkTreeRoot - ) { + ) Leonidas(msg.sender) { verifier = new MockVerifier(); REGISTRY = _registry; AVAILABILITY_ORACLE = _availabilityOracle; @@ -67,27 +64,6 @@ contract Rollup is IRollup { VERSION = 1; } - // HACK: Add a sequencer to set of potential sequencers - function addSequencer(address sequencer) external { - sequencers.add(sequencer); - } - - // HACK: Remove a sequencer from the set of potential sequencers - function removeSequencer(address sequencer) external { - sequencers.remove(sequencer); - } - - // HACK: Return whose turn it is to submit a block - function whoseTurnIsIt(uint256 blockNumber) public view returns (address) { - return - sequencers.length() == 0 ? address(0x0) : sequencers.at(blockNumber % sequencers.length()); - } - - // HACK: Return all the registered sequencers - function getSequencers() external view returns (address[] memory) { - return sequencers.values(); - } - function setVerifier(address _verifier) external override(IRollup) { // TODO remove, only needed for testing verifier = IVerifier(_verifier); @@ -101,8 +77,15 @@ contract Rollup is IRollup { * @notice Process an incoming L2 block and progress the state * @param _header - The L2 block header * @param _archive - A root of the archive tree after the L2 block is applied + * @param _signatures - Signatures from the validators */ - function process(bytes calldata _header, bytes32 _archive) external override(IRollup) { + function process( + bytes calldata _header, + bytes32 _archive, + SignatureLib.Signature[] memory _signatures + ) public { + _processPendingBlock(_signatures, _archive); + // Decode and validate header HeaderLib.Header memory header = HeaderLib.decode(_header); HeaderLib.validate(header, VERSION, lastBlockTs, archive); @@ -112,12 +95,6 @@ contract Rollup is IRollup { revert Errors.Rollup__UnavailableTxs(header.contentCommitment.txsEffectsHash); } - // Check that this is the current sequencer's turn - address sequencer = whoseTurnIsIt(header.globalVariables.blockNumber); - if (sequencer != address(0x0) && sequencer != msg.sender) { - revert Errors.Rollup__InvalidSequencer(msg.sender); - } - archive = _archive; lastBlockTs = block.timestamp; @@ -142,6 +119,11 @@ contract Rollup is IRollup { emit L2BlockProcessed(header.globalVariables.blockNumber); } + function process(bytes calldata _header, bytes32 _archive) external override(IRollup) { + SignatureLib.Signature[] memory emptySignatures = new SignatureLib.Signature[](0); + process(_header, _archive, emptySignatures); + } + function submitProof( bytes calldata _header, bytes32 _archive, diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index e2c82cf5496..8dd9e9ee114 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -46,7 +46,6 @@ library Errors { error Rollup__TimestampInFuture(); // 0xbc1ce916 error Rollup__TimestampTooOld(); // 0x72ed9c81 error Rollup__UnavailableTxs(bytes32 txsHash); // 0x414906c3 - error Rollup__InvalidSequencer(address sequencer); // 0xa127a106 // Registry error Registry__RollupNotRegistered(address rollup); // 0xa1fee4cf @@ -61,4 +60,14 @@ library Errors { // MerkleLib error MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual, bytes32 leaf, uint256 leafIndex); // 0x5f216bf1 + + // SignatureLib + error SignatureLib__CannotVerifyEmpty(); // 0xc7690a37 + error SignatureLib__InvalidSignature(address expected, address recovered); // 0xd9cbae6c + + // Sequencer Selection (Leonidas) + error Leonidas__NotGod(); // 0xabc2f815 + error Leonidas__EpochNotSetup(); // 0xcf4e597e + error Leonidas__InvalidProposer(address expected, address actual); // 0xd02d278e + error Leonidas__InsufficientAttestations(uint256 expected, uint256 actual); // 0xbf1ca4cb } diff --git a/l1-contracts/src/core/sequencer_selection/ILeonidas.sol b/l1-contracts/src/core/sequencer_selection/ILeonidas.sol new file mode 100644 index 00000000000..bf06ac2f5fd --- /dev/null +++ b/l1-contracts/src/core/sequencer_selection/ILeonidas.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +interface ILeonidas { + // Changing depending on sybil mechanism and slashing enforcement + function addValidator(address _validator) external; + function removeValidator(address _validator) external; + + // Likely changing to optimize in Pleistarchus + function setupEpoch() external; + function getCurrentProposer() external view returns (address); + + // Stable + function getCurrentEpoch() external view returns (uint256); + function getCurrentSlot() external view returns (uint256); + + // Consider removing below this point + // Likely removal of these to replace with a size and indiviual getter + function getEpochCommittee(uint256 _epoch) external view returns (address[] memory); + function getValidators() external view returns (address[] memory); +} diff --git a/l1-contracts/src/core/sequencer_selection/Leonidas.sol b/l1-contracts/src/core/sequencer_selection/Leonidas.sol new file mode 100644 index 00000000000..85718db8235 --- /dev/null +++ b/l1-contracts/src/core/sequencer_selection/Leonidas.sol @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +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 {ILeonidas} from "./ILeonidas.sol"; + +/** + * @title Leonidas + * @author Anaxandridas II + * @notice Leonidas is the spartan king, it is his job to select the warriors progressing the state of the kingdom. + * 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 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. + * It will be the duty of his successor (Pleistarchus) to optimize the costs with same functionality. + * + */ +contract Leonidas is Ownable, ILeonidas { + using EnumerableSet for EnumerableSet.AddressSet; + using SignatureLib for SignatureLib.Signature; + + /** + * @notice The structure of 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 + */ + struct Epoch { + address[] committee; + uint256 sampleSeed; + uint256 nextSeed; + } + + // The size/duration of a slot in seconds, multiple of 12 to align with Ethereum blocks + uint256 public constant SLOT_SIZE = 12 * 5; + + // The size/duration of an epoch in slots + uint256 public constant EPOCH_SIZE = 32; + + // The target number of validators in a committee + uint256 public constant TARGET_COMMITTEE_SIZE = EPOCH_SIZE; + + // The time that the contract was deployed + uint256 public immutable GENESIS_TIME; + + // An enumerable set of validators that are up to date + EnumerableSet.AddressSet private validatorSet; + + // A mapping to snapshots of the validator set + mapping(uint256 epochNumber => Epoch epoch) public epochs; + + // The last stored randao value, same value as `seed` in the last inserted epoch + uint256 internal lastSeed; + + constructor(address _ares) Ownable(_ares) { + GENESIS_TIME = block.timestamp; + + // We will setup the initial epoch value + uint256 seed = _computeNextSeed(0); + epochs[0] = Epoch({committee: new address[](0), sampleSeed: type(uint256).max, nextSeed: seed}); + lastSeed = seed; + } + + /** + * @notice Adds a validator to the validator set + * + * @dev Only ARES can add validators + * + * @dev Will setup the epoch if needed BEFORE adding the validator. + * This means that the validator will effectively be added to the NEXT epoch. + * + * @param _validator - The validator to add + */ + function addValidator(address _validator) external override(ILeonidas) onlyOwner { + setupEpoch(); + validatorSet.add(_validator); + } + + /** + * @notice Removes a validator from the validator set + * + * @dev Only ARES can add validators + * + * @dev Will setup the epoch if needed BEFORE removing the validator. + * This means that the validator will effectively be removed from the NEXT epoch. + * + * @param _validator - The validator to remove + */ + function removeValidator(address _validator) external override(ILeonidas) onlyOwner { + setupEpoch(); + validatorSet.remove(_validator); + } + + /** + * @notice Get the validator set for a given epoch + * + * @dev Consider removing this to replace with a `size` and individual getter. + * + * @param _epoch The epoch number to get the validator set for + * + * @return The validator set for the given epoch + */ + function getEpochCommittee(uint256 _epoch) + external + view + override(ILeonidas) + returns (address[] memory) + { + return epochs[_epoch].committee; + } + + /** + * @notice Get the validator set + * + * @dev Consider removing this to replace with a `size` and individual getter. + * + * @return The validator set + */ + function getValidators() external view override(ILeonidas) returns (address[] memory) { + return validatorSet.values(); + } + + /** + * @notice Performs a setup of an epoch if needed. The setup will + * - Sample the validator set for the epoch + * - Set the seed for the epoch + * - Update the last seed + * + * @dev Since this is a reference optimising for simplicity, we store the actual validator set in the epoch structure. + * This is very heavy on gas, so start crying because the gas here will melt the poles + * https://i.giphy.com/U1aN4HTfJ2SmgB2BBK.webp + */ + function setupEpoch() public override(ILeonidas) { + uint256 epochNumber = getCurrentEpoch(); + Epoch storage epoch = epochs[epochNumber]; + + // For epoch 0 the sampleSeed == type(uint256).max, so we will never enter this + if (epoch.sampleSeed == 0) { + epoch.sampleSeed = _getSampleSeed(epochNumber); + epoch.nextSeed = lastSeed = _computeNextSeed(epochNumber); + + epoch.committee = _sampleValidators(epochNumber, epoch.sampleSeed); + } + } + + /** + * @notice Get the current epoch number + * + * @return The current epoch number + */ + function getCurrentEpoch() public view override(ILeonidas) returns (uint256) { + return (block.timestamp - GENESIS_TIME) / (EPOCH_SIZE * SLOT_SIZE); + } + + /** + * @notice Get the current slot number + * + * @return The current slot number + */ + function getCurrentSlot() public view override(ILeonidas) returns (uint256) { + return (block.timestamp - GENESIS_TIME) / SLOT_SIZE; + } + + /** + * @notice Get the proposer for the current slot + * + * @dev The proposer is selected from the validator set of the current epoch. + * + * @dev Should only be access on-chain if epoch is setup, otherwise very expensive. + * + * @dev A return value of address(0) means that the proposer is "open" and can be anyone. + * + * @dev If the current epoch is the first epoch, returns address(0) + * If the current epoch is setup, we will return the proposer for the current slot + * If the current epoch is not setup, we will perform a sample as if it was (gas heavy) + * + * @return The address of the proposer + */ + function getCurrentProposer() public view override(ILeonidas) returns (address) { + uint256 epochNumber = getCurrentEpoch(); + if (epochNumber == 0) { + return address(0); + } + uint256 slot = getCurrentSlot(); + + Epoch storage epoch = epochs[epochNumber]; + + // If the epoch is setup, we can just return the proposer. Otherwise we have to emulate sampling + if (epoch.sampleSeed != 0) { + uint256 committeeSize = epoch.committee.length; + if (committeeSize == 0) { + return address(0); + } + + return + epoch.committee[_computeProposerIndex(epochNumber, slot, epoch.sampleSeed, committeeSize)]; + } + + // Allow anyone if there is no validator set + if (validatorSet.length() == 0) { + return address(0); + } + + // Emulate a sampling of the validators + uint256 sampleSeed = _getSampleSeed(epochNumber); + address[] memory committee = _sampleValidators(epochNumber, sampleSeed); + return committee[_computeProposerIndex(epochNumber, slot, sampleSeed, committee.length)]; + } + + /** + * @notice Process a pending block from the point-of-view of sequencer selection. Will: + * - Setup the epoch if needed (if epoch committee is empty skips the rest) + * - Validate that the proposer is the current proposer + * - Validate that the signatures for attestations are indeed from the validatorset + * - Validate that the number of valid attestations is sufficient + * + * @dev Cases where errors are thrown: + * - If the epoch is not setup + * - If the proposer is not the current proposer + * - If the number of valid attestations is insufficient + */ + function _processPendingBlock(SignatureLib.Signature[] memory _signatures, bytes32 _digest) + internal + { + setupEpoch(); + + Epoch storage epoch = epochs[getCurrentEpoch()]; + + // We should never enter this case because of `setupEpoch` + if (epoch.sampleSeed == 0) { + revert Errors.Leonidas__EpochNotSetup(); + } + + address proposer = getCurrentProposer(); + + // If the proposer is open, we allow anyone to propose without needing any signatures + if (proposer == address(0)) { + return; + } + + // @todo We should allow to provide a signature instead of needing the proposer to broadcast. + if (proposer != msg.sender) { + revert Errors.Leonidas__InvalidProposer(proposer, msg.sender); + } + + // Validate the attestations + uint256 validAttestations = 0; + for (uint256 i = 0; i < _signatures.length; i++) { + SignatureLib.Signature memory signature = _signatures[i]; + if (signature.isEmpty) { + continue; + } + + // The verification will throw if invalid + signature.verify(epoch.committee[i], _digest); + validAttestations++; + } + uint256 needed = epoch.committee.length * 2 / 3 + 1; + if (validAttestations < needed) { + revert Errors.Leonidas__InsufficientAttestations(needed, validAttestations); + } + } + + /** + * @notice Samples a validator set for a specific epoch + * + * @dev Only used internally, should never be called for anything but the "next" epoch + * Allowing us to always use `lastSeed`. + * + * @dev The first epoch will always return an empty list + * If the validator set is empty, we return an empty list + * If the validator set is smaller than the target committee size, we return the full set + * If the validator set is larger than the target committee size, we sample the validators + * by using the seed of the previous epoch to compute an offset for the validator set and then + * we take the next `TARGET_COMMITTEE_SIZE` validators from that offset (wrapping around). + * + * @param _epoch - The epoch to sample the validators for + * + * @return The validators for the given epoch + */ + function _sampleValidators(uint256 _epoch, uint256 _seed) private view returns (address[] memory) { + // If we are in the first epoch, we just return an empty list + if (_epoch == 0) { + return new address[](0); + } + + uint256 validatorSetSize = validatorSet.length(); + if (validatorSetSize == 0) { + return new address[](0); + } + + // If we have less validators than the target committee size, we just return the full set + if (validatorSet.length() <= TARGET_COMMITTEE_SIZE) { + return validatorSet.values(); + } + + // @todo Issue(#7603): The sampling should be improved + + uint256 offset = _seed % validatorSetSize; + address[] memory validators = new address[](TARGET_COMMITTEE_SIZE); + for (uint256 i = 0; i < TARGET_COMMITTEE_SIZE; i++) { + validators[i] = validatorSet.at((offset + i) % validatorSetSize); + } + return validators; + } + + /** + * @notice Get the sample seed for an epoch + * + * @dev The `_epoch` will never be 0 nor in the future + * + * @dev The return value will be equal to keccak256(n, block.prevrandao) for n being the last epoch + * setup. + * + * @return The sample seed for the epoch + */ + function _getSampleSeed(uint256 _epoch) private view returns (uint256) { + uint256 sampleSeed = epochs[_epoch].sampleSeed; + if (sampleSeed != 0) { + return sampleSeed; + } + + sampleSeed = epochs[_epoch - 1].nextSeed; + if (sampleSeed != 0) { + return sampleSeed; + } + + return lastSeed; + } + + /** + * @notice Computes the nextSeed for an epoch + * + * @dev We include the `_epoch` instead of using the randao directly to avoid issues with foundry testing + * where randao == 0. + * + * @param _epoch - The epoch to compute the seed for + * + * @return The computed seed + */ + function _computeNextSeed(uint256 _epoch) private view returns (uint256) { + return uint256(keccak256(abi.encode(_epoch, block.prevrandao))); + } + + /** + * @notice Computes the index of the committee member that acts as proposer for a given slot + * + * @param _epoch - The epoch to compute the proposer index for + * @param _slot - The slot to compute the proposer index for + * @param _seed - The seed to use for the computation + * @param _size - The size of the committee + * + * @return The index of the proposer + */ + function _computeProposerIndex(uint256 _epoch, uint256 _slot, uint256 _seed, uint256 _size) + private + pure + returns (uint256) + { + return uint256(keccak256(abi.encode(_epoch, _slot, _seed))) % _size; + } +} diff --git a/l1-contracts/src/core/sequencer_selection/SignatureLib.sol b/l1-contracts/src/core/sequencer_selection/SignatureLib.sol new file mode 100644 index 00000000000..434e6945c3f --- /dev/null +++ b/l1-contracts/src/core/sequencer_selection/SignatureLib.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.8.13; + +import {Errors} from "../libraries/Errors.sol"; + +library SignatureLib { + struct Signature { + bool isEmpty; + uint8 v; + bytes32 r; + bytes32 s; + } + + /** + * @notice Verified a signature, throws if the signature is invalid or empty + * + * @param _signature - The signature to verify + * @param _signer - The expected signer of the signature + * @param _digest - The digest that was signed + */ + function verify(Signature memory _signature, address _signer, bytes32 _digest) internal pure { + if (_signature.isEmpty) { + revert Errors.SignatureLib__CannotVerifyEmpty(); + } + address recovered = ecrecover(_digest, _signature.v, _signature.r, _signature.s); + if (_signer != recovered) { + revert Errors.SignatureLib__InvalidSignature(_signer, recovered); + } + } +} diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index a9af9338165..a78e5da1a12 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -127,8 +127,9 @@ contract RollupTest is DecoderBase { bytes32 archive = data.archive; bytes memory body = data.body; - // Overwrite in the rollup contract - vm.store(address(rollup), bytes32(uint256(2)), bytes32(uint256(block.timestamp))); + // Beware of the store slot below, if the test is failing might be because of the slot + // We overwrite `lastBlockTs` in the rollup + vm.store(address(rollup), bytes32(uint256(7)), bytes32(uint256(block.timestamp))); availabilityOracle.publish(body); @@ -143,8 +144,8 @@ contract RollupTest is DecoderBase { bytes memory body = full.block.body; uint32 numTxs = full.block.numTxs; - // We jump to the time of the block. - vm.warp(full.block.decodedHeader.globalVariables.timestamp); + // We jump to the time of the block. (unless it is in the past) + vm.warp(max(block.timestamp, full.block.decodedHeader.globalVariables.timestamp)); _populateInbox(full.populate.sender, full.populate.recipient, full.populate.l1ToL2Content); @@ -200,4 +201,8 @@ contract RollupTest is DecoderBase { ); } } + + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } } diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol new file mode 100644 index 00000000000..f3d9dc7662b --- /dev/null +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Aztec Labs. +pragma solidity >=0.8.18; + +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + +import {DecoderBase} from "../decoders/Base.sol"; + +import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; +import {Constants} from "../../src/core/libraries/ConstantsGen.sol"; +import {SignatureLib} from "../../src/core/sequencer_selection/SignatureLib.sol"; + +import {Registry} from "../../src/core/messagebridge/Registry.sol"; +import {Inbox} from "../../src/core/messagebridge/Inbox.sol"; +import {Outbox} from "../../src/core/messagebridge/Outbox.sol"; +import {Errors} from "../../src/core/libraries/Errors.sol"; +import {Rollup} from "../../src/core/Rollup.sol"; +import {AvailabilityOracle} from "../../src/core/availability_oracle/AvailabilityOracle.sol"; +import {NaiveMerkle} from "../merkle/Naive.sol"; +import {MerkleTestUtil} from "../merkle/TestUtil.sol"; +import {PortalERC20} from "../portals/PortalERC20.sol"; +import {TxsDecoderHelper} from "../decoders/helpers/TxsDecoderHelper.sol"; + +/** + * We are using the same blocks as from Rollup.t.sol. + * The tests in this file is testing the sequencer selection + */ +contract SpartaTest is DecoderBase { + Registry internal registry; + Inbox internal inbox; + Outbox internal outbox; + Rollup internal rollup; + MerkleTestUtil internal merkleTestUtil; + TxsDecoderHelper internal txsHelper; + PortalERC20 internal portalERC20; + + AvailabilityOracle internal availabilityOracle; + + mapping(address validator => uint256 privateKey) internal privateKeys; + + SignatureLib.Signature internal emptySignature; + + function setUp() public virtual { + registry = new Registry(); + availabilityOracle = new AvailabilityOracle(); + portalERC20 = new PortalERC20(); + rollup = new Rollup(registry, availabilityOracle, IERC20(address(portalERC20)), bytes32(0)); + inbox = Inbox(address(rollup.INBOX())); + outbox = Outbox(address(rollup.OUTBOX())); + + registry.upgrade(address(rollup), address(inbox), address(outbox)); + + // mint some tokens to the rollup + portalERC20.mint(address(rollup), 1000000); + + merkleTestUtil = new MerkleTestUtil(); + txsHelper = new TxsDecoderHelper(); + + for (uint256 i = 1; i < 5; i++) { + uint256 privateKey = uint256(keccak256(abi.encode("validator", i))); + address validator = vm.addr(privateKey); + privateKeys[validator] = privateKey; + rollup.addValidator(validator); + } + } + + function _testProposerForFutureEpoch() public { + // @todo Implement + } + + function _testValidatorSetLargerThanCommittee() public { + // @todo Implement + } + + function testHappyPath() public { + _testBlock("mixed_block_0", 0, false); // We run a block before the epoch with validators + _testBlock("mixed_block_1", 3, false); // We need signatures! + } + + function testInvalidProposer() public { + _testBlock("mixed_block_0", 0, false); // We run a block before the epoch with validators + _testBlock("mixed_block_1", 3, true); // We need signatures! + } + + function testInsufficientSigs() public { + _testBlock("mixed_block_0", 0, false); // We run a block before the epoch with validators + _testBlock("mixed_block_1", 2, false); // We need signatures! + } + + struct StructToAvoidDeepStacks { + uint256 needed; + address proposer; + bool shouldRevert; + } + + function _testBlock(string memory _name, uint256 _signatureCount, bool _invalidaProposer) public { + DecoderBase.Full memory full = load(_name); + bytes memory header = full.block.header; + bytes32 archive = full.block.archive; + bytes memory body = full.block.body; + + StructToAvoidDeepStacks memory ree; + + // We jump to the time of the block. (unless it is in the past) + vm.warp(max(block.timestamp, full.block.decodedHeader.globalVariables.timestamp)); + + _populateInbox(full.populate.sender, full.populate.recipient, full.populate.l1ToL2Content); + + availabilityOracle.publish(body); + + uint256 toConsume = inbox.toConsume(); + ree.proposer = rollup.getCurrentProposer(); + ree.shouldRevert = false; + + rollup.setupEpoch(); + + if (_signatureCount > 0 && ree.proposer != address(0)) { + address[] memory validators = rollup.getEpochCommittee(rollup.getCurrentEpoch()); + ree.needed = validators.length * 2 / 3 + 1; + + SignatureLib.Signature[] memory signatures = new SignatureLib.Signature[](_signatureCount); + + for (uint256 i = 0; i < _signatureCount; i++) { + signatures[i] = createSignature(validators[i], archive); + } + + if (_signatureCount < ree.needed) { + vm.expectRevert( + abi.encodeWithSelector( + Errors.Leonidas__InsufficientAttestations.selector, ree.needed, _signatureCount + ) + ); + ree.shouldRevert = true; + } + + if (_invalidaProposer) { + address realProposer = ree.proposer; + ree.proposer = address(uint160(uint256(keccak256(abi.encode("invalid", ree.proposer))))); + vm.expectRevert( + abi.encodeWithSelector( + Errors.Leonidas__InvalidProposer.selector, realProposer, ree.proposer + ) + ); + ree.shouldRevert = true; + } + + vm.prank(ree.proposer); + rollup.process(header, archive, signatures); + + if (ree.shouldRevert) { + return; + } + } else { + rollup.process(header, archive); + } + + assertEq(inbox.toConsume(), toConsume + 1, "Message subtree not consumed"); + + bytes32 l2ToL1MessageTreeRoot; + { + uint32 numTxs = full.block.numTxs; + // NB: The below works with full blocks because we require the largest possible subtrees + // for L2 to L1 messages - usually we make variable height subtrees, the roots of which + // form a balanced tree + + // The below is a little janky - we know that this test deals with full txs with equal numbers + // of msgs or txs with no messages, so the division works + // TODO edit full.messages to include information about msgs per tx? + uint256 subTreeHeight = merkleTestUtil.calculateTreeHeightFromSize( + full.messages.l2ToL1Messages.length == 0 ? 0 : full.messages.l2ToL1Messages.length / numTxs + ); + uint256 outHashTreeHeight = merkleTestUtil.calculateTreeHeightFromSize(numTxs); + uint256 numMessagesWithPadding = numTxs * Constants.MAX_L2_TO_L1_MSGS_PER_TX; + + uint256 treeHeight = subTreeHeight + outHashTreeHeight; + NaiveMerkle tree = new NaiveMerkle(treeHeight); + for (uint256 i = 0; i < numMessagesWithPadding; i++) { + if (i < full.messages.l2ToL1Messages.length) { + tree.insertLeaf(full.messages.l2ToL1Messages[i]); + } else { + tree.insertLeaf(bytes32(0)); + } + } + + l2ToL1MessageTreeRoot = tree.computeRoot(); + } + + (bytes32 root,) = outbox.roots(full.block.decodedHeader.globalVariables.blockNumber); + + assertEq(l2ToL1MessageTreeRoot, root, "Invalid l2 to l1 message tree root"); + + assertEq(rollup.archive(), archive, "Invalid archive"); + } + + function _populateInbox(address _sender, bytes32 _recipient, bytes32[] memory _contents) internal { + for (uint256 i = 0; i < _contents.length; i++) { + vm.prank(_sender); + inbox.sendL2Message( + DataStructures.L2Actor({actor: _recipient, version: 1}), _contents[i], bytes32(0) + ); + } + } + + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + function createSignature(address _signer, bytes32 _digest) + internal + view + returns (SignatureLib.Signature memory) + { + uint256 privateKey = privateKeys[_signer]; + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _digest); + + return SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}); + } +} diff --git a/yarn-project/aztec.js/src/utils/cheat_codes.ts b/yarn-project/aztec.js/src/utils/cheat_codes.ts index d10bbcef91d..08055d90130 100644 --- a/yarn-project/aztec.js/src/utils/cheat_codes.ts +++ b/yarn-project/aztec.js/src/utils/cheat_codes.ts @@ -267,9 +267,9 @@ export class AztecCheatCodes { await this.eth.setNextBlockTimestamp(to); // also store this time on the rollup contract (slot 2 tracks `lastBlockTs`). // This is because when the sequencer executes public functions, it uses the timestamp stored in the rollup contract. - await this.eth.store(rollupContract, 2n, BigInt(to)); - // also store this on slot 3 of the rollup contract (`lastWarpedBlockTs`) which tracks the last time warp was used. - await this.eth.store(rollupContract, 3n, BigInt(to)); + await this.eth.store(rollupContract, 7n, BigInt(to)); + // also store this on slot of the rollup contract (`lastWarpedBlockTs`) which tracks the last time warp was used. + await this.eth.store(rollupContract, 8n, BigInt(to)); } /** diff --git a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts index 68566180953..e908c1c6e79 100644 --- a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts +++ b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts @@ -18,17 +18,7 @@ export async function sequencers(opts: { log: LogFn; debugLogger: DebugLogger; }) { - const { - blockNumber: maybeBlockNumber, - command, - who: maybeWho, - mnemonic, - rpcUrl, - l1RpcUrl, - chainId, - log, - debugLogger, - } = opts; + const { command, who: maybeWho, mnemonic, rpcUrl, l1RpcUrl, chainId, log, debugLogger } = opts; const client = await createCompatibleClient(rpcUrl, debugLogger); const { l1ContractAddresses } = await client.getNodeInfo(); @@ -60,7 +50,7 @@ export async function sequencers(opts: { const who = (maybeWho as `0x{string}`) ?? walletClient?.account.address.toString(); if (command === 'list') { - const sequencers = await rollup.read.getSequencers(); + const sequencers = await rollup.read.getValidators(); if (sequencers.length === 0) { log(`No sequencers registered on rollup`); } else { @@ -74,7 +64,7 @@ export async function sequencers(opts: { throw new Error(`Missing sequencer address`); } log(`Adding ${who} as sequencer`); - const hash = await writeableRollup.write.addSequencer([who]); + const hash = await writeableRollup.write.addValidator([who]); await publicClient.waitForTransactionReceipt({ hash }); log(`Added in tx ${hash}`); } else if (command === 'remove') { @@ -82,13 +72,12 @@ export async function sequencers(opts: { throw new Error(`Missing sequencer address`); } log(`Removing ${who} as sequencer`); - const hash = await writeableRollup.write.removeSequencer([who]); + const hash = await writeableRollup.write.removeValidator([who]); await publicClient.waitForTransactionReceipt({ hash }); log(`Removed in tx ${hash}`); } else if (command === 'who-next') { - const blockNumber = maybeBlockNumber ?? (await client.getBlockNumber()) + 1; - const next = await rollup.read.whoseTurnIsIt([BigInt(blockNumber)]); - log(`Next sequencer expected to build ${blockNumber} is ${next}`); + const next = await rollup.read.getCurrentProposer(); + log(`Sequencer expected to build is ${next}`); } else { throw new Error(`Unknown command ${command}`); } diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 313f5345e0f..a4bc9fde02d 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -46,8 +46,8 @@ export interface L1PublisherTxSender { /** Returns the EOA used for sending txs to L1. */ getSenderAddress(): Promise; - /** Returns the address elected for submitting a given block number or zero if anyone can submit. */ - getSubmitterAddressForBlock(blockNumber: number): Promise; + /** Returns the address of the current proposer or zero if anyone can submit. */ + getSubmitterAddressForBlock(): Promise; /** * Publishes tx effects to Availability Oracle. @@ -137,8 +137,8 @@ export class L1Publisher implements L2BlockReceiver { this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000; } - public async isItMyTurnToSubmit(blockNumber: number): Promise { - const submitter = await this.txSender.getSubmitterAddressForBlock(blockNumber); + public async isItMyTurnToSubmit(): Promise { + const submitter = await this.txSender.getSubmitterAddressForBlock(); const sender = await this.txSender.getSenderAddress(); return submitter.isZero() || submitter.equals(sender); } diff --git a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts index 241fad9ef51..a9456301037 100644 --- a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts +++ b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts @@ -77,12 +77,12 @@ export class ViemTxSender implements L1PublisherTxSender { return Promise.resolve(EthAddress.fromString(this.account.address)); } - async getSubmitterAddressForBlock(blockNumber: number): Promise { + async getSubmitterAddressForBlock(): Promise { try { - const submitter = await this.rollupContract.read.whoseTurnIsIt([BigInt(blockNumber)]); + const submitter = await this.rollupContract.read.getCurrentProposer(); return EthAddress.fromString(submitter); } catch (err) { - this.log.warn(`Failed to get submitter for block ${blockNumber}: ${err}`); + this.log.warn(`Failed to get submitter: ${err}`); return EthAddress.ZERO; } } diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 45e7c7437dc..a1241e9ee50 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -179,7 +179,7 @@ export class Sequencer { : Number(historicalHeader.globalVariables.blockNumber.toBigInt())) + 1; // Do not go forward with new block if not my turn - if (!(await this.publisher.isItMyTurnToSubmit(newBlockNumber))) { + if (!(await this.publisher.isItMyTurnToSubmit())) { this.log.debug('Not my turn to submit block'); return; } @@ -250,7 +250,8 @@ export class Sequencer { if (currentBlockNumber + 1 !== newGlobalVariables.blockNumber.toNumber()) { throw new Error('New block was emitted while building block'); } - if (!(await this.publisher.isItMyTurnToSubmit(newGlobalVariables.blockNumber.toNumber()))) { + + if (!(await this.publisher.isItMyTurnToSubmit())) { throw new Error(`Not this sequencer turn to submit block`); } };