Skip to content

Commit

Permalink
feat: THIS IS SPARTA
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Jul 24, 2024
1 parent 840486e commit 1826c3c
Show file tree
Hide file tree
Showing 10 changed files with 451 additions and 60 deletions.
52 changes: 22 additions & 30 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,21 @@ 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
* @author Aztec Labs
* @notice Rollup contract that is concerned about readability and velocity of development
* not giving a damn about gas costs.
*/
contract Rollup is IRollup {
contract Rollup is Leonidas, IRollup {
IVerifier public verifier;
IRegistry public immutable REGISTRY;
IAvailabilityOracle public immutable AVAILABILITY_ORACLE;
Expand All @@ -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() {
verifier = new MockVerifier();
REGISTRY = _registry;
AVAILABILITY_ORACLE = _availabilityOracle;
Expand All @@ -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);
Expand All @@ -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 {
setupEpoch();

// Decode and validate header
HeaderLib.Header memory header = HeaderLib.decode(_header);
HeaderLib.validate(header, VERSION, lastBlockTs, archive);
Expand All @@ -113,11 +96,15 @@ contract Rollup is IRollup {
}

// Check that this is the current sequencer's turn
address sequencer = whoseTurnIsIt(header.globalVariables.blockNumber);
// @todo @LHerskind We should just have signature from the proposer, we don't need him as msg.sender
address sequencer = getCurrentProposer();
if (sequencer != address(0x0) && sequencer != msg.sender) {
revert Errors.Rollup__InvalidSequencer(msg.sender);
}

// Check that consensus agrees as well!
require(validateConsensus(_signatures, _archive), "Invalid consensus");

archive = _archive;
lastBlockTs = block.timestamp;

Expand All @@ -142,6 +129,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,
Expand Down
33 changes: 33 additions & 0 deletions l1-contracts/src/core/sequencer_selection/ConsensusValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
pragma solidity ^0.8.13;

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

library ConsensusValidator {
using SignatureLib for SignatureLib.Signature;

struct ConsensusContent {
SignatureLib.Signature[] signatures;
address[] validators;
}

function validateConsensus(ConsensusContent memory _content, bytes32 _digest)
internal
pure
returns (bool)
{
uint256 validAttestations = 0;
for (uint256 i = 0; i < _content.signatures.length; i++) {
SignatureLib.Signature memory signature = _content.signatures[i];
if (signature.isEmpty) {
continue;
}

require(signature.validate(_content.validators[i], _digest), "ECDSA: invalid signature");
validAttestations++;
}
uint256 needed = _content.validators.length * 2 / 3;

require(validAttestations > needed, "Insufficient signatures");
return true;
}
}
155 changes: 155 additions & 0 deletions l1-contracts/src/core/sequencer_selection/Leonidas.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.18;

import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol";
import {ConsensusValidator} from "./ConsensusValidator.sol";
import {SignatureLib} from "./SignatureLib.sol";

// @todo @LHerskind, This is REALLY bad code, we just throw gas in the air like no-one cares!
// It was planned to use validatorSetCommitments for a snapshot, but the way the full ts
// is made it seems pain to do that, so instead we just go spend all the gas in the world

contract Leonidas {
using EnumerableSet for EnumerableSet.AddressSet;
using ConsensusValidator for ConsensusValidator.ConsensusContent;

// For gas reasons we want to just use a validatorSetCommitment
// But the current sequencer setup seems to require the full set for easy integration
// @todo Replace the `validatorSet` with a commitment to the set currently storing the entire set to very easily
// have an accessible stable set that external actors can fetch WITHOUT keeping any storage on their own.
// This is only temporary as we are experimenting because it required fewer changes to get going
struct Epoch {
address[] validatorSet;
bool exists;
}

// 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 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;

constructor() {
GENESIS_TIME = block.timestamp;
}

/**
* @notice Get the validator set for a given epoch
*
* @param _epoch The epoch number to get the validator set for
* @return The validator set for the given epoch
*/
function getEpochValidatorSet(uint256 _epoch) public view returns (address[] memory) {
return epochs[_epoch].validatorSet;
}

/**
* @notice Get the current epoch number
*
* @return The current epoch number
*/
function getCurrentEpoch() public view 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 returns (uint256) {
return (block.timestamp - GENESIS_TIME) / SLOT_SIZE;
}

function getCurrentProposer() public view returns (address) {
uint256 epochNumber = getCurrentEpoch();
Epoch storage epoch = epochs[epochNumber];

if (epoch.exists) {
if (epoch.validatorSet.length == 0) {
return address(0);
}

return epoch.validatorSet[getCurrentSlot() % epoch.validatorSet.length];
}

// Allow anyone if there is no validator set
if (validatorSet.length() == 0) {
return address(0);
}

// If not setup, we "emulate" the setup
address[] memory validators = sampleValidators(epochNumber);
return validators[getCurrentSlot() % validators.length];
}

/**
* @dev Start crying because the gas here will melt the poles
* https://i.giphy.com/U1aN4HTfJ2SmgB2BBK.webp
*/
function setupEpoch() public {
uint256 epochNumber = getCurrentEpoch();
Epoch storage epoch = epochs[epochNumber];
if (!epoch.exists) {
epoch.exists = true;
address[] memory validators = sampleValidators(epochNumber);
/*for (uint256 i = 0; i < validators.length; i++) {
epoch.validatorSet.push(validators[i]);
}*/
epoch.validatorSet = validators;
}
}

function addValidator(address _validator) public {
setupEpoch();
validatorSet.add(_validator);
}

/**
* @dev Remove a validator from the validator set AFTER setting up the epoch if not already
*
* @param _validator The validator to remove
*/
function removeValidator(address _validator) public {
setupEpoch();
validatorSet.remove(_validator);
}

// @todo @LHerskind, Short term, we just keep the validators stable across epochs
function sampleValidators(uint256 _epoch) public view returns (address[] memory) {
return validatorSet.values();
}

// HACK: Return all the registered sequencers
function getValidators() external view returns (address[] memory) {
return validatorSet.values();
}

function validateConsensus(SignatureLib.Signature[] memory _signatures, bytes32 _digest)
public
view
returns (bool)
{
Epoch storage epoch = epochs[getCurrentEpoch()];
require(epoch.exists, "Epoch not setup");

// Pass if no validators
if (epoch.validatorSet.length == 0) {
return true;
}

return ConsensusValidator.ConsensusContent({
signatures: _signatures,
validators: epoch.validatorSet
}).validateConsensus(_digest);
}
}
23 changes: 23 additions & 0 deletions l1-contracts/src/core/sequencer_selection/SignatureLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pragma solidity ^0.8.13;

library SignatureLib {
struct Signature {
bool isEmpty;
uint8 v;
bytes32 r;
bytes32 s;
}

function validate(Signature memory _signature, address _signer, bytes32 _digest)
internal
pure
returns (bool)
{
require(!_signature.isEmpty, "Signature is empty");
require(
_signer == ecrecover(_digest, _signature.v, _signature.r, _signature.s),
"ECDSA: invalid signature"
);
return true;
}
}
12 changes: 8 additions & 4 deletions l1-contracts/test/Rollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ 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
vm.store(address(rollup), bytes32(uint256(5)), bytes32(uint256(block.timestamp)));

availabilityOracle.publish(body);

Expand All @@ -143,8 +143,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);

Expand Down Expand Up @@ -200,4 +200,8 @@ contract RollupTest is DecoderBase {
);
}
}

function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}
}
Loading

0 comments on commit 1826c3c

Please sign in to comment.