Skip to content

Commit

Permalink
feat: Add pending reorg chain resistance in contracts (#7926)
Browse files Browse the repository at this point in the history
Fixes #7592 and #7616.
  • Loading branch information
LHerskind authored Aug 15, 2024
1 parent 523724d commit f769f84
Show file tree
Hide file tree
Showing 26 changed files with 420 additions and 122 deletions.
43 changes: 41 additions & 2 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ contract Rollup is Leonidas, IRollup, ITestRollup {
bool isProven;
}

// @note The number of slots within which a block must be proven
// This number is currently pulled out of thin air and should be replaced when we are not blind
// @todo #8018
uint256 public constant TIMELINESS_PROVING_IN_SLOTS = 100;

IRegistry public immutable REGISTRY;
IAvailabilityOracle public immutable AVAILABILITY_ORACLE;
IInbox public immutable INBOX;
Expand Down Expand Up @@ -87,11 +92,44 @@ contract Rollup is Leonidas, IRollup, ITestRollup {
provenBlockCount = 1;
}

/**
* @notice Prune the pending chain up to the last proven block
*
* @dev Will revert if there is nothing to prune or if the chain is not ready to be pruned
*/
function prune() external override(IRollup) {
if (pendingBlockCount == provenBlockCount) {
revert Errors.Rollup__NothingToPrune();
}

BlockLog storage firstPendingNotInProven = blocks[provenBlockCount];
uint256 prunableAtSlot =
uint256(firstPendingNotInProven.slotNumber) + TIMELINESS_PROVING_IN_SLOTS;
uint256 currentSlot = getCurrentSlot();

if (currentSlot < prunableAtSlot) {
revert Errors.Rollup__NotReadyToPrune(currentSlot, prunableAtSlot);
}

// @note We are not deleting the blocks, but we are "winding back" the pendingBlockCount
// to the last block that was proven.
// The reason we can do this, is that any new block proposed will overwrite a previous block
// so no values should "survive". It it is however slightly odd for people reading
// the chain separately from the contract without using pendingBlockCount as a boundary.
pendingBlockCount = provenBlockCount;

emit PrunedPending(provenBlockCount, pendingBlockCount);
}

/**
* Sets the assumeProvenUntilBlockNumber. Only the contract deployer can set it.
* @param blockNumber - New value.
*/
function setAssumeProvenUntilBlockNumber(uint256 blockNumber) external onlyOwner {
function setAssumeProvenUntilBlockNumber(uint256 blockNumber)
external
override(ITestRollup)
onlyOwner
{
if (blockNumber > provenBlockCount && blockNumber <= pendingBlockCount) {
for (uint256 i = provenBlockCount; i < blockNumber; i++) {
blocks[i].isProven = true;
Expand Down Expand Up @@ -308,7 +346,8 @@ contract Rollup is Leonidas, IRollup, ITestRollup {
isProven: false
});

bytes32 inHash = INBOX.consume();
// @note The block number here will always be >=1 as the genesis block is at 0
bytes32 inHash = INBOX.consume(header.globalVariables.blockNumber);
if (header.contentCommitment.inHash != inHash) {
revert Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash);
}
Expand Down
4 changes: 4 additions & 0 deletions l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ interface ITestRollup {
function setDevNet(bool _devNet) external;
function setVerifier(address _verifier) external;
function setVkTreeRoot(bytes32 _vkTreeRoot) external;
function setAssumeProvenUntilBlockNumber(uint256 blockNumber) external;
}

interface IRollup {
event L2BlockProcessed(uint256 indexed blockNumber);
event L2ProofVerified(uint256 indexed blockNumber, bytes32 indexed proverId);
event ProgressedState(uint256 provenBlockCount, uint256 pendingBlockCount);
event PrunedPending(uint256 provenBlockCount, uint256 pendingBlockCount);

function prune() external;

function INBOX() external view returns (IInbox);

Expand Down
5 changes: 4 additions & 1 deletion l1-contracts/src/core/interfaces/messagebridge/IInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ interface IInbox {
* @dev Only callable by the rollup contract
* @dev In the first iteration we return empty tree root because first block's messages tree is always
* empty because there has to be a 1 block lag to prevent sequencer DOS attacks
*
* @param _toConsume - The block number to consume
*
* @return The root of the consumed tree
*/
function consume() external returns (bytes32);
function consume(uint256 _toConsume) external returns (bytes32);
// docs:end:consume
}
14 changes: 14 additions & 0 deletions l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,18 @@ interface IOutbox {
view
returns (bool);
// docs:end:outbox_has_message_been_consumed_at_block_and_index

/**
* @notice Fetch the root data for a given block number
* Returns (0, 0) if the block is not proven
*
* @param _l2BlockNumber - The block number to fetch the root data for
*
* @return root - The root of the merkle tree containing the L2 to L1 messages
* @return minHeight - The min height for the the merkle tree that the root corresponds to
*/
function getRootData(uint256 _l2BlockNumber)
external
view
returns (bytes32 root, uint256 minHeight);
}
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 @@ -36,6 +36,7 @@ library Errors {
error Outbox__InvalidRecipient(address expected, address actual); // 0x57aad581
error Outbox__AlreadyNullified(uint256 l2BlockNumber, uint256 leafIndex); // 0xfd71c2d4
error Outbox__NothingToConsumeAtBlock(uint256 l2BlockNumber); // 0xa4508f22
error Outbox__BlockNotProven(uint256 l2BlockNumber); // 0x0e194a6d

// Rollup
error Rollup__InvalidArchive(bytes32 expected, bytes32 actual); // 0xb682a40e
Expand All @@ -53,6 +54,8 @@ library Errors {
error Rollup__TimestampInFuture(); // 0xbc1ce916
error Rollup__TimestampTooOld(); // 0x72ed9c81
error Rollup__UnavailableTxs(bytes32 txsHash); // 0x414906c3
error Rollup__NothingToPrune(); // 0x850defd3
error Rollup__NotReadyToPrune(uint256 currentSlot, uint256 prunableAt); // 0x9fdf1614

// Registry
error Registry__RollupNotRegistered(address rollup); // 0xa1fee4cf
Expand Down
23 changes: 14 additions & 9 deletions l1-contracts/src/core/messagebridge/Inbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {FrontierMerkle} from "./frontier_tree/Frontier.sol";
* @title Inbox
* @author Aztec Labs
* @notice Lives on L1 and is used to pass messages into the rollup, e.g., L1 -> L2 messages.
*
* @dev The current code is horribly gas inefficient, and should only be used as a reference implementation.
*/
contract Inbox is IInbox {
using Hash for DataStructures.L1ToL2Msg;
Expand All @@ -30,12 +32,10 @@ contract Inbox is IInbox {
uint256 internal immutable SIZE;
bytes32 internal immutable EMPTY_ROOT; // The root of an empty frontier tree

// Number of a tree which is ready to be consumed
uint256 public toConsume = Constants.INITIAL_L2_BLOCK_NUM;
// Number of a tree which is currently being filled
uint256 public inProgress = Constants.INITIAL_L2_BLOCK_NUM + 1;

mapping(uint256 blockNumber => IFrontier tree) internal trees;
mapping(uint256 blockNumber => IFrontier tree) public trees;

constructor(address _rollup, uint256 _height) {
ROLLUP = _rollup;
Expand All @@ -52,10 +52,13 @@ contract Inbox is IInbox {

/**
* @notice Inserts a new message into the Inbox
*
* @dev Emits `MessageSent` with data for easy access by the sequencer
*
* @param _recipient - The recipient of the message
* @param _content - The content of the message (application specific)
* @param _secretHash - The secret hash of the message (make it possible to hide when a specific message is consumed on L2)
*
* @return Hash of the sent message.
*/
function sendL2Message(
Expand Down Expand Up @@ -96,29 +99,31 @@ contract Inbox is IInbox {

/**
* @notice Consumes the current tree, and starts a new one if needed
*
* @dev Only callable by the rollup contract
* @dev In the first iteration we return empty tree root because first block's messages tree is always
* empty because there has to be a 1 block lag to prevent sequencer DOS attacks
*
* @param _toConsume - The block number to consume
*
* @return The root of the consumed tree
*/
function consume() external override(IInbox) returns (bytes32) {
function consume(uint256 _toConsume) external override(IInbox) returns (bytes32) {
if (msg.sender != ROLLUP) {
revert Errors.Inbox__Unauthorized();
}

bytes32 root = EMPTY_ROOT;
if (toConsume > Constants.INITIAL_L2_BLOCK_NUM) {
root = trees[toConsume].root();
if (_toConsume > Constants.INITIAL_L2_BLOCK_NUM) {
root = trees[_toConsume].root();
}

// If we are "catching up" we skip the tree creation as it is already there
if (toConsume + 1 == inProgress) {
if (_toConsume + 1 == inProgress) {
inProgress += 1;
trees[inProgress] = IFrontier(new FrontierMerkle(HEIGHT));
}

toConsume += 1;

return root;
}
}
52 changes: 41 additions & 11 deletions l1-contracts/src/core/messagebridge/Outbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ pragma solidity >=0.8.18;
// Libraries
import {DataStructures} from "../libraries/DataStructures.sol";
import {Errors} from "../libraries/Errors.sol";
import {Constants} from "../libraries/ConstantsGen.sol";
import {MerkleLib} from "../libraries/MerkleLib.sol";
import {Hash} from "../libraries/Hash.sol";
import {IOutbox} from "../interfaces/messagebridge/IOutbox.sol";

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

/**
* @title Outbox
* @author Aztec Labs
Expand All @@ -26,18 +27,19 @@ contract Outbox is IOutbox {
mapping(uint256 => bool) nullified;
}

address public immutable ROLLUP_CONTRACT;
mapping(uint256 l2BlockNumber => RootData) public roots;
Rollup public immutable ROLLUP;
mapping(uint256 l2BlockNumber => RootData) internal roots;

constructor(address _rollup) {
ROLLUP_CONTRACT = _rollup;
ROLLUP = Rollup(_rollup);
}

/**
* @notice Inserts the root of a merkle tree containing all of the L2 to L1 messages in
* a block specified by _l2BlockNumber.
* @notice Inserts the root of a merkle tree containing all of the L2 to L1 messages in a block
*
* @dev Only callable by the rollup contract
* @dev Emits `RootAdded` upon inserting the root successfully
*
* @param _l2BlockNumber - The L2 Block Number in which the L2 to L1 messages reside
* @param _root - The merkle root of the tree where all the L2 to L1 messages are leaves
* @param _minHeight - The min height of the merkle tree that the root corresponds to
Expand All @@ -46,14 +48,10 @@ contract Outbox is IOutbox {
external
override(IOutbox)
{
if (msg.sender != ROLLUP_CONTRACT) {
if (msg.sender != address(ROLLUP)) {
revert Errors.Outbox__Unauthorized();
}

if (roots[_l2BlockNumber].root != bytes32(0)) {
revert Errors.Outbox__RootAlreadySetAtBlock(_l2BlockNumber);
}

if (_root == bytes32(0)) {
revert Errors.Outbox__InsertingInvalidRoot();
}
Expand All @@ -66,8 +64,10 @@ contract Outbox is IOutbox {

/**
* @notice Consumes an entry from the Outbox
*
* @dev Only useable by portals / recipients of messages
* @dev Emits `MessageConsumed` when consuming messages
*
* @param _message - The L2 to L1 message
* @param _l2BlockNumber - The block number specifying the block that contains the message we want to consume
* @param _leafIndex - The index inside the merkle tree where the message is located
Expand All @@ -81,6 +81,10 @@ contract Outbox is IOutbox {
uint256 _leafIndex,
bytes32[] calldata _path
) external override(IOutbox) {
if (_l2BlockNumber >= ROLLUP.provenBlockCount()) {
revert Errors.Outbox__BlockNotProven(_l2BlockNumber);
}

if (msg.sender != _message.recipient.actor) {
revert Errors.Outbox__InvalidRecipient(_message.recipient.actor, msg.sender);
}
Expand Down Expand Up @@ -121,9 +125,13 @@ contract Outbox is IOutbox {

/**
* @notice Checks to see if an index of the L2 to L1 message tree for a specific block has been consumed
*
* @dev - This function does not throw. Out-of-bounds access is considered valid, but will always return false
*
* @param _l2BlockNumber - The block number specifying the block that contains the index of the message we want to check
* @param _leafIndex - The index of the message inside the merkle tree
*
* @return bool - True if the message has been consumed, false otherwise
*/
function hasMessageBeenConsumedAtBlockAndIndex(uint256 _l2BlockNumber, uint256 _leafIndex)
external
Expand All @@ -133,4 +141,26 @@ contract Outbox is IOutbox {
{
return roots[_l2BlockNumber].nullified[_leafIndex];
}

/**
* @notice Fetch the root data for a given block number
* Returns (0, 0) if the block is not proven
*
* @param _l2BlockNumber - The block number to fetch the root data for
*
* @return root - The root of the merkle tree containing the L2 to L1 messages
* @return minHeight - The min height for the the merkle tree that the root corresponds to
*/
function getRootData(uint256 _l2BlockNumber)
external
view
override(IOutbox)
returns (bytes32 root, uint256 minHeight)
{
if (_l2BlockNumber >= ROLLUP.provenBlockCount()) {
return (bytes32(0), 0);
}
RootData storage rootData = roots[_l2BlockNumber];
return (rootData.root, rootData.minHeight);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ pragma solidity >=0.8.18;

import {Hash} from "../../libraries/Hash.sol";
import {IFrontier} from "../../interfaces/messagebridge/IFrontier.sol";
import {Ownable} from "@oz/access/Ownable.sol";

// This truncates each hash and hash preimage to 31 bytes to follow Noir.
// It follows the logic in /noir-protocol-circuits/crates/parity-lib/src/utils/sha256_merkle_tree.nr
// TODO(Miranda): Possibly nuke this contract, and use a generic version which can either use
// regular sha256 or sha256ToField when emulating circuits
contract FrontierMerkle is IFrontier {
contract FrontierMerkle is IFrontier, Ownable {
uint256 public immutable HEIGHT;
uint256 public immutable SIZE;

Expand All @@ -21,7 +22,7 @@ contract FrontierMerkle is IFrontier {
// for the zeros at each level. This would save gas on computations
mapping(uint256 level => bytes32 zero) public zeros;

constructor(uint256 _height) {
constructor(uint256 _height) Ownable(msg.sender) {
HEIGHT = _height;
SIZE = 2 ** _height;

Expand All @@ -31,7 +32,7 @@ contract FrontierMerkle is IFrontier {
}
}

function insertLeaf(bytes32 _leaf) external override(IFrontier) returns (uint256) {
function insertLeaf(bytes32 _leaf) external override(IFrontier) onlyOwner returns (uint256) {
uint256 index = nextIndex;
uint256 level = _computeLevel(index);
bytes32 right = _leaf;
Expand Down
8 changes: 5 additions & 3 deletions l1-contracts/src/core/sequencer_selection/Leonidas.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,24 @@ contract Leonidas is Ownable, ILeonidas {
uint256 nextSeed;
}

// @note @LHerskind - The multiple cause pain and suffering in the E2E tests as we introduce
// @note @LHerskind The multiple cause pain and suffering in the E2E tests as we introduce
// a timeliness requirement into the publication that did not exists before,
// and at the same time have a setup that will impact the time at every tx
// because of auto-mine. By using just 1, we can make our test work
// but anything using an actual working chain would eat dung as simulating
// transactions is slower than an actual ethereum slot.
//
// The value should be a higher multiple for any actual chain
// @todo #8019
uint256 public constant SLOT_DURATION = Constants.ETHEREUM_SLOT_DURATION * 1;

// The duration of an epoch in slots
// @todo @LHerskind - This value should be updated when we are not blind.
// @todo @LHerskind - This value should be updated when we are not blind.
// @todo #8020
uint256 public constant EPOCH_DURATION = 32;

// The target number of validators in a committee
// @todo @LHerskind - This value should be updated when we are not blind.
// @todo #8021
uint256 public constant TARGET_COMMITTEE_SIZE = EPOCH_DURATION;

// The time that the contract was deployed
Expand Down
Loading

0 comments on commit f769f84

Please sign in to comment.