diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index db322ece51f..f7460dd321f 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -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; @@ -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; @@ -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); } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 62d2cdd80b3..60d2c090dfd 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -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); diff --git a/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol b/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol index 2139bd78483..5a9be37abae 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol @@ -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 } diff --git a/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol b/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol index 6eba97fca43..74469a1e9ee 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol @@ -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); } diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 6382b7ac06f..78505c56d46 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -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 @@ -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 diff --git a/l1-contracts/src/core/messagebridge/Inbox.sol b/l1-contracts/src/core/messagebridge/Inbox.sol index aaf5476a163..c5513a44f9e 100644 --- a/l1-contracts/src/core/messagebridge/Inbox.sol +++ b/l1-contracts/src/core/messagebridge/Inbox.sol @@ -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; @@ -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; @@ -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( @@ -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; } } diff --git a/l1-contracts/src/core/messagebridge/Outbox.sol b/l1-contracts/src/core/messagebridge/Outbox.sol index db1ccb76447..d4131930d8c 100644 --- a/l1-contracts/src/core/messagebridge/Outbox.sol +++ b/l1-contracts/src/core/messagebridge/Outbox.sol @@ -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 @@ -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 @@ -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(); } @@ -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 @@ -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); } @@ -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 @@ -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); + } } diff --git a/l1-contracts/src/core/messagebridge/frontier_tree/Frontier.sol b/l1-contracts/src/core/messagebridge/frontier_tree/Frontier.sol index 650dfbd30a1..e80099fe0d9 100644 --- a/l1-contracts/src/core/messagebridge/frontier_tree/Frontier.sol +++ b/l1-contracts/src/core/messagebridge/frontier_tree/Frontier.sol @@ -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; @@ -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; @@ -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; diff --git a/l1-contracts/src/core/sequencer_selection/Leonidas.sol b/l1-contracts/src/core/sequencer_selection/Leonidas.sol index 7f3745d06a8..4427115fe57 100644 --- a/l1-contracts/src/core/sequencer_selection/Leonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/Leonidas.sol @@ -43,7 +43,7 @@ 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 @@ -51,14 +51,16 @@ contract Leonidas is Ownable, ILeonidas { // 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 diff --git a/l1-contracts/test/Inbox.t.sol b/l1-contracts/test/Inbox.t.sol index 092aa99b87c..94e460d709b 100644 --- a/l1-contracts/test/Inbox.t.sol +++ b/l1-contracts/test/Inbox.t.sol @@ -18,6 +18,7 @@ contract InboxTest is Test { InboxHarness internal inbox; uint256 internal version = 0; + uint256 internal blockNumber = Constants.INITIAL_L2_BLOCK_NUM; bytes32 internal emptyTreeRoot; function setUp() public { @@ -66,13 +67,13 @@ contract InboxTest is Test { // be violated modifier checkInvariant() { _; - assertLt(inbox.toConsume(), inbox.inProgress()); + assertLt(blockNumber, inbox.inProgress()); } function testRevertIfNotConsumingFromRollup() public { vm.prank(address(0x1)); vm.expectRevert(Errors.Inbox__Unauthorized.selector); - inbox.consume(); + inbox.consume(blockNumber); } function testFuzzInsert(DataStructures.L1ToL2Msg memory _message) public checkInvariant { @@ -148,7 +149,7 @@ contract InboxTest is Test { } function _send(DataStructures.L1ToL2Msg[] memory _messages) internal checkInvariant { - bytes32 toConsumeRoot = inbox.getToConsumeRoot(); + bytes32 toConsumeRoot = inbox.getToConsumeRoot(blockNumber); // We send the messages and then check that toConsume root did not change. for (uint256 i = 0; i < _messages.length; i++) { @@ -166,7 +167,7 @@ contract InboxTest is Test { // Root of a tree waiting to be consumed should not change because we introduced a 1 block lag to prevent sequencer // DOS attacks assertEq( - inbox.getToConsumeRoot(), + inbox.getToConsumeRoot(blockNumber), toConsumeRoot, "Root of a tree waiting to be consumed should not change" ); @@ -181,9 +182,8 @@ contract InboxTest is Test { // Now we consume the trees for (uint256 i = 0; i < numTreesToConsume; i++) { uint256 numTrees = inbox.getNumTrees(); - uint256 expectedNumTrees = - (inbox.toConsume() + 1 == inbox.inProgress()) ? numTrees + 1 : numTrees; - bytes32 root = inbox.consume(); + uint256 expectedNumTrees = (blockNumber + 1 == inbox.inProgress()) ? numTrees + 1 : numTrees; + bytes32 root = inbox.consume(blockNumber); // We check whether a new tree is correctly initialized when the one which was in progress was set as to consume assertEq(inbox.getNumTrees(), expectedNumTrees, "Unexpected number of trees"); @@ -192,6 +192,7 @@ contract InboxTest is Test { if (i > initialNumTrees) { assertEq(root, emptyTreeRoot, "Root of a newly initialized tree not empty"); } + blockNumber += 1; } } } diff --git a/l1-contracts/test/Outbox.t.sol b/l1-contracts/test/Outbox.t.sol index af78cc15018..420e16625c7 100644 --- a/l1-contracts/test/Outbox.t.sol +++ b/l1-contracts/test/Outbox.t.sol @@ -11,19 +11,30 @@ import {Hash} from "../src/core/libraries/Hash.sol"; import {NaiveMerkle} from "./merkle/Naive.sol"; import {MerkleTestUtil} from "./merkle/TestUtil.sol"; +contract FakeRollup { + uint256 public provenBlockCount = 1; + + function setProvenBlockCount(uint256 _provenBlockCount) public { + provenBlockCount = _provenBlockCount; + } +} + contract OutboxTest is Test { using Hash for DataStructures.L2ToL1Msg; - address internal constant ROLLUP_CONTRACT = address(0x42069123); address internal constant NOT_RECIPIENT = address(0x420); uint256 internal constant DEFAULT_TREE_HEIGHT = 2; uint256 internal constant AZTEC_VERSION = 1; + address internal ROLLUP_CONTRACT; Outbox internal outbox; NaiveMerkle internal zeroedTree; MerkleTestUtil internal merkleTestUtil; function setUp() public { + ROLLUP_CONTRACT = address(new FakeRollup()); + FakeRollup(ROLLUP_CONTRACT).setProvenBlockCount(2); + outbox = new Outbox(ROLLUP_CONTRACT); zeroedTree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); merkleTestUtil = new MerkleTestUtil(); @@ -54,9 +65,13 @@ contract OutboxTest is Test { vm.prank(ROLLUP_CONTRACT); outbox.insert(1, root, DEFAULT_TREE_HEIGHT); + } + + function testRevertIfInsertingEmptyRoot() public { + bytes32 root = bytes32(0); vm.prank(ROLLUP_CONTRACT); - vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__RootAlreadySetAtBlock.selector, 1)); + vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__InsertingInvalidRoot.selector)); outbox.insert(1, root, DEFAULT_TREE_HEIGHT); } @@ -80,7 +95,7 @@ contract OutboxTest is Test { vm.prank(ROLLUP_CONTRACT); outbox.insert(1, root, treeHeight); - (bytes32 actualRoot, uint256 actualHeight) = outbox.roots(1); + (bytes32 actualRoot, uint256 actualHeight) = outbox.getRootData(1); assertEq(root, actualRoot); assertEq(treeHeight, actualHeight); } @@ -187,6 +202,28 @@ contract OutboxTest is Test { outbox.consume(fakeMessage, 1, 0, path); } + function testRevertIfConsumingFromTreeNotProven() public { + FakeRollup(ROLLUP_CONTRACT).setProvenBlockCount(1); + + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); + bytes32 leaf = fakeMessage.sha256ToField(); + + NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); + tree.insertLeaf(leaf); + bytes32 root = tree.computeRoot(); + + vm.prank(ROLLUP_CONTRACT); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); + + (bytes32[] memory path,) = tree.computeSiblingPath(0); + + bool statusBeforeConsumption = outbox.hasMessageBeenConsumedAtBlockAndIndex(1, 0); + assertEq(abi.encode(0), abi.encode(statusBeforeConsumption)); + + vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__BlockNotProven.selector, 1)); + outbox.consume(fakeMessage, 1, 0, path); + } + function testValidInsertAndConsume() public { DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); bytes32 leaf = fakeMessage.sha256ToField(); @@ -218,6 +255,8 @@ contract OutboxTest is Test { uint256 _blockNumber, uint8 _size ) public { + uint256 blockNumber = bound(_blockNumber, 1, 256); + FakeRollup(ROLLUP_CONTRACT).setProvenBlockCount(blockNumber + 1); uint256 numberOfMessages = bound(_size, 1, _recipients.length); DataStructures.L2ToL1Msg[] memory messages = new DataStructures.L2ToL1Msg[](numberOfMessages); @@ -235,22 +274,43 @@ contract OutboxTest is Test { bytes32 root = tree.computeRoot(); vm.expectEmit(true, true, true, true, address(outbox)); - emit IOutbox.RootAdded(_blockNumber, root, treeHeight); + emit IOutbox.RootAdded(blockNumber, root, treeHeight); vm.prank(ROLLUP_CONTRACT); - outbox.insert(_blockNumber, root, treeHeight); + outbox.insert(blockNumber, root, treeHeight); for (uint256 i = 0; i < numberOfMessages; i++) { (bytes32[] memory path, bytes32 leaf) = tree.computeSiblingPath(i); vm.expectEmit(true, true, true, true, address(outbox)); - emit IOutbox.MessageConsumed(_blockNumber, root, leaf, i); + emit IOutbox.MessageConsumed(blockNumber, root, leaf, i); vm.prank(_recipients[i]); - outbox.consume(messages[i], _blockNumber, i, path); + outbox.consume(messages[i], blockNumber, i, path); } } - function testCheckOutOfBoundsStatus(uint256 _blockNumber, uint256 _leafIndex) external { + function testCheckOutOfBoundsStatus(uint256 _blockNumber, uint256 _leafIndex) public { bool outOfBounds = outbox.hasMessageBeenConsumedAtBlockAndIndex(_blockNumber, _leafIndex); - assertEq(abi.encode(0), abi.encode(outOfBounds)); + assertFalse(outOfBounds); + } + + function testGetRootData() public { + bytes32 root = zeroedTree.computeRoot(); + + vm.startPrank(ROLLUP_CONTRACT); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); + outbox.insert(2, root, DEFAULT_TREE_HEIGHT); + vm.stopPrank(); + + { + (bytes32 actualRoot, uint256 actualHeight) = outbox.getRootData(1); + assertEq(root, actualRoot); + assertEq(DEFAULT_TREE_HEIGHT, actualHeight); + } + + { + (bytes32 actualRoot, uint256 actualHeight) = outbox.getRootData(2); + assertEq(bytes32(0), actualRoot); + assertEq(0, actualHeight); + } } } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 4943e67a02a..3591e67cfb2 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -16,6 +16,7 @@ import {Errors} from "../src/core/libraries/Errors.sol"; import {Rollup} from "../src/core/Rollup.sol"; import {Leonidas} from "../src/core/sequencer_selection/Leonidas.sol"; import {AvailabilityOracle} from "../src/core/availability_oracle/AvailabilityOracle.sol"; +import {FrontierMerkle} from "../src/core/messagebridge/frontier_tree/Frontier.sol"; import {NaiveMerkle} from "./merkle/Naive.sol"; import {MerkleTestUtil} from "./merkle/TestUtil.sol"; import {PortalERC20} from "./portals/PortalERC20.sol"; @@ -67,6 +68,79 @@ contract RollupTest is DecoderBase { _; } + function testRevertPrune() public setUpFor("mixed_block_1") { + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NothingToPrune.selector)); + rollup.prune(); + + _testBlock("mixed_block_1", false); + + uint256 currentSlot = rollup.getCurrentSlot(); + (, uint128 slot,) = rollup.blocks(1); + uint256 prunableAt = uint256(slot) + rollup.TIMELINESS_PROVING_IN_SLOTS(); + + vm.expectRevert( + abi.encodeWithSelector(Errors.Rollup__NotReadyToPrune.selector, currentSlot, prunableAt) + ); + rollup.prune(); + } + + function testPrune() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false); + + assertEq(inbox.inProgress(), 3, "Invalid in progress"); + + // @note Fetch the inbox root of block 2. This should be frozen when block 1 is proposed. + // Even if we end up reverting block 1, we should still see the same root in the inbox. + bytes32 inboxRoot2 = inbox.trees(2).root(); + + (, uint128 slot,) = rollup.blocks(1); + uint256 prunableAt = uint256(slot) + rollup.TIMELINESS_PROVING_IN_SLOTS(); + + uint256 timeOfPrune = rollup.getTimestampForSlot(prunableAt); + vm.warp(timeOfPrune); + + assertEq(rollup.pendingBlockCount(), 2, "Invalid pending block count"); + assertEq(rollup.provenBlockCount(), 1, "Invalid proven block count"); + + // @note Get the root and min height that we have in the outbox. + // We read it directly in storage because it is not yet proven, so the getter will give (0, 0). + // The values are stored such that we can check that after pruning, and inserting a new block, + // we will override it. + bytes32 rootMixed = vm.load(address(outbox), keccak256(abi.encode(1, 0))); + uint256 minHeightMixed = + uint256(vm.load(address(outbox), bytes32(uint256(keccak256(abi.encode(1, 0))) + 1))); + + assertNotEq(rootMixed, bytes32(0), "Invalid root"); + assertNotEq(minHeightMixed, 0, "Invalid min height"); + + rollup.prune(); + assertEq(inbox.inProgress(), 3, "Invalid in progress"); + assertEq(rollup.pendingBlockCount(), 1, "Invalid pending block count"); + assertEq(rollup.provenBlockCount(), 1, "Invalid proven block count"); + + // @note We alter what slot is specified in the empty block! + // This means that we keep the `empty_block_1` mostly as is, but replace the slot number + // and timestamp as if it was created at a different point in time. This allow us to insert it + // as if it was the first block, even after we had originally inserted the mixed block. + // An example where this could happen would be if no-one could proof the mixed block. + _testBlock("empty_block_1", false, prunableAt); + + assertEq(inbox.inProgress(), 3, "Invalid in progress"); + assertEq(inbox.trees(2).root(), inboxRoot2, "Invalid inbox root"); + assertEq(rollup.pendingBlockCount(), 2, "Invalid pending block count"); + assertEq(rollup.provenBlockCount(), 1, "Invalid proven block count"); + + // We check that the roots in the outbox have correctly been updated. + bytes32 rootEmpty = vm.load(address(outbox), keccak256(abi.encode(1, 0))); + uint256 minHeightEmpty = + uint256(vm.load(address(outbox), bytes32(uint256(keccak256(abi.encode(1, 0))) + 1))); + + assertNotEq(rootEmpty, bytes32(0), "Invalid root"); + assertNotEq(minHeightEmpty, 0, "Invalid min height"); + assertNotEq(rootEmpty, rootMixed, "Invalid root"); + assertNotEq(minHeightEmpty, minHeightMixed, "Invalid min height"); + } + function testMixedBlock(bool _toProve) public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", _toProve); @@ -244,12 +318,28 @@ contract RollupTest is DecoderBase { } function _testBlock(string memory name, bool _submitProof) public { + _testBlock(name, _submitProof, 0); + } + + function _testBlock(string memory name, bool _submitProof, uint256 _slotNumber) public { DecoderBase.Full memory full = load(name); bytes memory header = full.block.header; bytes32 archive = full.block.archive; bytes memory body = full.block.body; uint32 numTxs = full.block.numTxs; + // Overwrite some timestamps if needed + if (_slotNumber != 0) { + uint256 ts = rollup.getTimestampForSlot(_slotNumber); + + full.block.decodedHeader.globalVariables.timestamp = ts; + full.block.decodedHeader.globalVariables.slotNumber = _slotNumber; + assembly { + mstore(add(header, add(0x20, 0x0194)), _slotNumber) + mstore(add(header, add(0x20, 0x01b4)), ts) + } + } + // We jump to the time of the block. (unless it is in the past) vm.warp(max(block.timestamp, full.block.decodedHeader.globalVariables.timestamp)); @@ -257,8 +347,6 @@ contract RollupTest is DecoderBase { availabilityOracle.publish(body); - uint256 toConsume = inbox.toConsume(); - rollup.process(header, archive); if (_submitProof) { @@ -270,8 +358,6 @@ contract RollupTest is DecoderBase { ); } - assertEq(inbox.toConsume(), toConsume + 1, "Message subtree not consumed"); - bytes32 l2ToL1MessageTreeRoot; { // NB: The below works with full blocks because we require the largest possible subtrees @@ -300,9 +386,14 @@ contract RollupTest is DecoderBase { l2ToL1MessageTreeRoot = tree.computeRoot(); } - (bytes32 root,) = outbox.roots(full.block.decodedHeader.globalVariables.blockNumber); + (bytes32 root,) = outbox.getRootData(full.block.decodedHeader.globalVariables.blockNumber); - assertEq(l2ToL1MessageTreeRoot, root, "Invalid l2 to l1 message tree root"); + // If we are trying to read a block beyond the proven chain, we should see "nothing". + if (rollup.provenBlockCount() > full.block.decodedHeader.globalVariables.blockNumber) { + assertEq(l2ToL1MessageTreeRoot, root, "Invalid l2 to l1 message tree root"); + } else { + assertEq(root, bytes32(0), "Invalid outbox root"); + } assertEq(rollup.archive(), archive, "Invalid archive"); } @@ -315,8 +406,4 @@ 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/decoders/Base.sol b/l1-contracts/test/decoders/Base.sol index 3d18fd1e6a1..45beed502c0 100644 --- a/l1-contracts/test/decoders/Base.sol +++ b/l1-contracts/test/decoders/Base.sol @@ -85,12 +85,16 @@ contract DecoderBase is Test { AppendOnlyTreeSnapshot publicDataTree; } - function load(string memory name) public view returns (Full memory) { + function load(string memory name) internal view returns (Full memory) { string memory root = vm.projectRoot(); string memory path = string.concat(root, "/test/fixtures/", name, ".json"); string memory json = vm.readFile(path); - bytes memory json_bytes = vm.parseJson(json); - Full memory full = abi.decode(json_bytes, (Full)); + bytes memory jsonBytes = vm.parseJson(json); + Full memory full = abi.decode(jsonBytes, (Full)); return full; } + + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } } diff --git a/l1-contracts/test/harnesses/InboxHarness.sol b/l1-contracts/test/harnesses/InboxHarness.sol index 0a2e787021d..8d51447906d 100644 --- a/l1-contracts/test/harnesses/InboxHarness.sol +++ b/l1-contracts/test/harnesses/InboxHarness.sol @@ -22,10 +22,10 @@ contract InboxHarness is Inbox { return trees[inProgress].isFull(); } - function getToConsumeRoot() external view returns (bytes32) { + function getToConsumeRoot(uint256 _toConsume) external view returns (bytes32) { 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(); } return root; } diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index cff3b21f940..d8f2b504c1f 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -56,6 +56,8 @@ contract TokenPortalTest is Test { address internal recipient = address(0xdead); uint256 internal withdrawAmount = 654; + uint256 internal l2BlockNumber = 69; + function setUp() public { registry = new Registry(); portalERC20 = new PortalERC20(); @@ -71,6 +73,10 @@ contract TokenPortalTest is Test { tokenPortal.initialize(address(registry), address(portalERC20), l2TokenAddress); + // Modify the proven block count + vm.store(address(rollup), bytes32(uint256(7)), bytes32(l2BlockNumber + 1)); + assertEq(rollup.provenBlockCount(), l2BlockNumber + 1); + vm.deal(address(this), 100 ether); } @@ -203,7 +209,6 @@ contract TokenPortalTest is Test { function testAnyoneCanCallWithdrawIfNoDesignatedCaller(address _caller) public { vm.assume(_caller != address(0)); - uint256 l2BlockNumber = 69; // add message with caller as this address (bytes32 l2ToL1Message, bytes32[] memory siblingPath, bytes32 treeRoot) = _addWithdrawMessageInOutbox(address(0), l2BlockNumber); @@ -227,7 +232,6 @@ contract TokenPortalTest is Test { function testWithdrawWithDesignatedCallerFailsForOtherCallers(address _caller) public { vm.assume(_caller != address(this)); - uint256 l2BlockNumber = 69; // add message with caller as this address (, bytes32[] memory siblingPath, bytes32 treeRoot) = _addWithdrawMessageInOutbox(address(this), l2BlockNumber); @@ -252,7 +256,6 @@ contract TokenPortalTest is Test { } function testWithdrawWithDesignatedCallerSucceedsForDesignatedCaller() public { - uint256 l2BlockNumber = 69; // add message with caller as this address (bytes32 l2ToL1Message, bytes32[] memory siblingPath, bytes32 treeRoot) = _addWithdrawMessageInOutbox(address(this), l2BlockNumber); diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index 0e57a485586..61d3b8fede1 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -47,6 +47,8 @@ contract UniswapPortalTest is Test { bytes32 internal aztecRecipient = bytes32(uint256(0x3)); bytes32 internal secretHashForRedeemingMintedNotes = bytes32(uint256(0x4)); + uint256 internal l2BlockNumber = 69; + function setUp() public { // fork mainnet uint256 forkId = vm.createFork(vm.rpcUrl("mainnet_fork")); @@ -68,6 +70,10 @@ contract UniswapPortalTest is Test { uniswapPortal = new UniswapPortal(); uniswapPortal.initialize(address(registry), l2UniswapAddress); + // Modify the proven block count + vm.store(address(rollup), bytes32(uint256(7)), bytes32(l2BlockNumber + 1)); + assertEq(rollup.provenBlockCount(), l2BlockNumber + 1); + // have DAI locked in portal that can be moved when funds are withdrawn deal(address(DAI), address(daiTokenPortal), amount); @@ -182,7 +188,6 @@ contract UniswapPortalTest is Test { // Creates a withdraw transaction without a designated caller. // Should fail when uniswap portal tries to consume it since it tries using a designated caller. function testRevertIfWithdrawMessageHasNoDesignatedCaller() public { - uint256 l2BlockNumber = 69; bytes32 l2ToL1MessageToInsert = _createDaiWithdrawMessage(address(uniswapPortal), address(0)); (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = _addMessagesToOutbox(l2ToL1MessageToInsert, bytes32(uint256(0x1)), l2BlockNumber); @@ -237,7 +242,6 @@ contract UniswapPortalTest is Test { vm.assume(_recipient != address(uniswapPortal)); // malformed withdraw message (wrong recipient) - uint256 l2BlockNumber = 69; bytes32 l2ToL1MessageToInsert = _createDaiWithdrawMessage(_recipient, address(uniswapPortal)); (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = @@ -290,8 +294,6 @@ contract UniswapPortalTest is Test { } function testRevertIfSwapParamsDifferentToOutboxMessage() public { - uint256 l2BlockNumber = 69; - bytes32 daiWithdrawMessageHash = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); bytes32 swapMessageHash = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); @@ -355,8 +357,6 @@ contract UniswapPortalTest is Test { } function testSwapWithDesignatedCaller() public { - uint256 l2BlockNumber = 69; - bytes32 daiWithdrawMessageHash = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); bytes32 swapMessageHash = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); @@ -400,7 +400,6 @@ contract UniswapPortalTest is Test { function testSwapCalledByAnyoneIfDesignatedCallerNotSet(address _caller) public { vm.assume(_caller != address(uniswapPortal)); - uint256 l2BlockNumber = 69; bytes32 daiWithdrawMessageHash = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); @@ -447,7 +446,6 @@ contract UniswapPortalTest is Test { function testRevertIfSwapWithDesignatedCallerCalledByWrongCaller(address _caller) public { vm.assume(_caller != address(this)); - uint256 l2BlockNumber = 69; bytes32 daiWithdrawMessageHash = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); @@ -549,8 +547,6 @@ contract UniswapPortalTest is Test { } function testRevertIfSwapMessageWasForDifferentPublicOrPrivateFlow() public { - uint256 l2BlockNumber = 69; - bytes32 daiWithdrawMessageHash = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); diff --git a/l1-contracts/test/sparta/DevNet.t.sol b/l1-contracts/test/sparta/DevNet.t.sol index 5f17191dc62..12e64ec9614 100644 --- a/l1-contracts/test/sparta/DevNet.t.sol +++ b/l1-contracts/test/sparta/DevNet.t.sol @@ -156,7 +156,6 @@ contract DevNetTest is DecoderBase { availabilityOracle.publish(body); - uint256 toConsume = inbox.toConsume(); ree.proposer = rollup.getCurrentProposer(); ree.shouldRevert = false; @@ -180,8 +179,6 @@ contract DevNetTest is DecoderBase { return; } - assertEq(inbox.toConsume(), toConsume + 1, "Message subtree not consumed"); - bytes32 l2ToL1MessageTreeRoot; { uint32 numTxs = full.block.numTxs; @@ -211,9 +208,13 @@ contract DevNetTest is DecoderBase { l2ToL1MessageTreeRoot = tree.computeRoot(); } - (bytes32 root,) = outbox.roots(full.block.decodedHeader.globalVariables.blockNumber); + (bytes32 root,) = outbox.getRootData(full.block.decodedHeader.globalVariables.blockNumber); - assertEq(l2ToL1MessageTreeRoot, root, "Invalid l2 to l1 message tree root"); + if (rollup.provenBlockCount() > full.block.decodedHeader.globalVariables.blockNumber) { + assertEq(l2ToL1MessageTreeRoot, root, "Invalid l2 to l1 message tree root"); + } else { + assertEq(root, bytes32(0), "Invalid outbox root"); + } assertEq(rollup.archive(), archive, "Invalid archive"); } @@ -226,8 +227,4 @@ contract DevNetTest 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 index 20f8b60d8cb..ea3146b6446 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -199,7 +199,6 @@ contract SpartaTest is DecoderBase { availabilityOracle.publish(body); - uint256 toConsume = inbox.toConsume(); ree.proposer = rollup.getCurrentProposer(); ree.shouldRevert = false; @@ -253,8 +252,6 @@ contract SpartaTest is DecoderBase { assertEq(_expectRevert, ree.shouldRevert, "Invalid revert expectation"); - assertEq(inbox.toConsume(), toConsume + 1, "Message subtree not consumed"); - bytes32 l2ToL1MessageTreeRoot; { uint32 numTxs = full.block.numTxs; @@ -284,9 +281,14 @@ contract SpartaTest is DecoderBase { l2ToL1MessageTreeRoot = tree.computeRoot(); } - (bytes32 root,) = outbox.roots(full.block.decodedHeader.globalVariables.blockNumber); + (bytes32 root,) = outbox.getRootData(full.block.decodedHeader.globalVariables.blockNumber); - assertEq(l2ToL1MessageTreeRoot, root, "Invalid l2 to l1 message tree root"); + // If we are trying to read a block beyond the proven chain, we should see "nothing". + if (rollup.provenBlockCount() > full.block.decodedHeader.globalVariables.blockNumber) { + assertEq(l2ToL1MessageTreeRoot, root, "Invalid l2 to l1 message tree root"); + } else { + assertEq(root, bytes32(0), "Invalid outbox root"); + } assertEq(rollup.archive(), archive, "Invalid archive"); } @@ -300,10 +302,6 @@ contract SpartaTest is DecoderBase { } } - function max(uint256 a, uint256 b) internal pure returns (uint256) { - return a > b ? a : b; - } - function createSignature(address _signer, bytes32 _digest) internal view diff --git a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts index 5f7eeb85fa7..4497c9e2341 100644 --- a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts @@ -37,7 +37,7 @@ import { fr, makeScopedL2ToL1Message } from '@aztec/circuits.js/testing'; import { type L1ContractAddresses, createEthereumChain } from '@aztec/ethereum'; import { makeTuple, range } from '@aztec/foundation/array'; import { openTmpStore } from '@aztec/kv-store/utils'; -import { AvailabilityOracleAbi, InboxAbi, OutboxAbi, RollupAbi } from '@aztec/l1-artifacts'; +import { AvailabilityOracleAbi, OutboxAbi, RollupAbi } from '@aztec/l1-artifacts'; import { SHA256Trunc, StandardTree } from '@aztec/merkle-tree'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types'; import { TxProver } from '@aztec/prover-client'; @@ -84,11 +84,9 @@ describe('L1Publisher integration', () => { let deployerAccount: PrivateKeyAccount; let rollupAddress: Address; - let inboxAddress: Address; let outboxAddress: Address; let rollup: GetContractReturnType>; - let inbox: GetContractReturnType>; let outbox: GetContractReturnType>; let publisher: L1Publisher; @@ -135,7 +133,6 @@ describe('L1Publisher integration', () => { ethCheatCodes = new EthCheatCodes(config.l1RpcUrl); rollupAddress = getAddress(l1ContractAddresses.rollupAddress.toString()); - inboxAddress = getAddress(l1ContractAddresses.inboxAddress.toString()); outboxAddress = getAddress(l1ContractAddresses.outboxAddress.toString()); // Set up contract instances @@ -144,11 +141,6 @@ describe('L1Publisher integration', () => { abi: RollupAbi, client: publicClient, }); - inbox = getContract({ - address: inboxAddress, - abi: InboxAbi, - client: walletClient, - }); outbox = getContract({ address: outboxAddress, abi: OutboxAbi, @@ -364,9 +356,6 @@ describe('L1Publisher integration', () => { let currentL1ToL2Messages: Fr[] = []; let nextL1ToL2Messages: Fr[] = []; - // We store which tree is about to be consumed so that we can later check the value advanced - let toConsume = await inbox.read.toConsume(); - for (let i = 0; i < numberOfConsecutiveBlocks; i++) { // @note Make sure that the state is up to date before we start building. await worldStateSynchronizer.syncImmediate(); @@ -409,7 +398,7 @@ describe('L1Publisher integration', () => { const l2ToL1MsgsArray = block.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); - const [emptyRoot] = await outbox.read.roots([block.header.globalVariables.blockNumber.toBigInt()]); + const [emptyRoot] = await outbox.read.getRootData([block.header.globalVariables.blockNumber.toBigInt()]); // Check that we have not yet written a root to this blocknumber expect(BigInt(emptyRoot)).toStrictEqual(0n); @@ -444,11 +433,6 @@ describe('L1Publisher integration', () => { }); expect(ethTx.input).toEqual(expectedData); - // Check a tree have been consumed from the inbox - const newToConsume = await inbox.read.toConsume(); - expect(newToConsume).toEqual(toConsume + 1n); - toConsume = newToConsume; - const treeHeight = Math.ceil(Math.log2(l2ToL1MsgsArray.length)); const tree = new StandardTree( @@ -462,10 +446,16 @@ describe('L1Publisher integration', () => { await tree.appendLeaves(l2ToL1MsgsArray); const expectedRoot = tree.getRoot(true); - const [actualRoot] = await outbox.read.roots([block.header.globalVariables.blockNumber.toBigInt()]); + const [returnedRoot] = await outbox.read.getRootData([block.header.globalVariables.blockNumber.toBigInt()]); // check that values are inserted into the outbox - expect(`0x${expectedRoot.toString('hex')}`).toEqual(actualRoot); + expect(Fr.ZERO.toString()).toEqual(returnedRoot); + + const actualRoot = await ethCheatCodes.load( + EthAddress.fromString(outbox.address), + ethCheatCodes.keccak256(0n, 1n + BigInt(i)), + ); + expect(`0x${expectedRoot.toString('hex')}`).toEqual(new Fr(actualRoot).toString()); // There is a 1 block lag between before messages get consumed from the inbox currentL1ToL2Messages = nextL1ToL2Messages; diff --git a/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts index a404825c510..6d0a39b965c 100644 --- a/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts @@ -29,7 +29,7 @@ const testSetup = async (): Promise => { teardown = teardown_; - return { aztecNode, pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet }; + return { aztecNode, pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet, deployL1ContractsValues }; }; // docs:end:uniswap_setup diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index f65ba985a72..8ff1dca5857 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -11,8 +11,10 @@ import { computeAuthWitMessageHash, } from '@aztec/aztec.js'; import { sha256ToField } from '@aztec/foundation/crypto'; +import { RollupAbi } from '@aztec/l1-artifacts'; import { type TokenBridgeContract, type TokenContract } from '@aztec/noir-contracts.js'; +import { getContract } from 'viem'; import { toFunctionSelector } from 'viem/utils'; import { NO_L1_TO_L2_MSG_ERROR } from './fixtures/fixtures.js'; @@ -32,6 +34,7 @@ describe('e2e_cross_chain_messaging', () => { let crossChainTestHarness: CrossChainTestHarness; let l2Token: TokenContract; let l2Bridge: TokenBridgeContract; + let rollup: any; beforeEach(async () => { const { @@ -52,6 +55,12 @@ describe('e2e_cross_chain_messaging', () => { logger_, ); + rollup = getContract({ + address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: deployL1ContractsValues.walletClient, + }); + l2Token = crossChainTestHarness.l2Token; l2Bridge = crossChainTestHarness.l2Bridge; ethAccount = crossChainTestHarness.ethAccount; @@ -124,6 +133,9 @@ describe('e2e_cross_chain_messaging', () => { l2ToL1Message, ); + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await rollup.write.setAssumeProvenUntilBlockNumber([await rollup.read.pendingBlockCount()]); + // Check balance before and after exit. expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); await crossChainTestHarness.withdrawFundsFromBridgeOnL1( diff --git a/yarn-project/end-to-end/src/e2e_outbox.test.ts b/yarn-project/end-to-end/src/e2e_outbox.test.ts index 1ef39376dfb..2317447e9a9 100644 --- a/yarn-project/end-to-end/src/e2e_outbox.test.ts +++ b/yarn-project/end-to-end/src/e2e_outbox.test.ts @@ -9,7 +9,7 @@ import { } from '@aztec/aztec.js'; import { sha256ToField } from '@aztec/foundation/crypto'; import { truncateAndPad } from '@aztec/foundation/serialize'; -import { OutboxAbi } from '@aztec/l1-artifacts'; +import { OutboxAbi, RollupAbi } from '@aztec/l1-artifacts'; import { SHA256 } from '@aztec/merkle-tree'; import { TestContract } from '@aztec/noir-contracts.js'; @@ -26,6 +26,7 @@ describe('E2E Outbox Tests', () => { let wallets: AccountWalletWithSecretKey[]; let deployL1ContractsValues: DeployL1Contracts; let outbox: any; + let rollup: any; beforeEach(async () => { ({ teardown, aztecNode, wallets, deployL1ContractsValues } = await setup(1)); @@ -37,6 +38,12 @@ describe('E2E Outbox Tests', () => { const receipt = await TestContract.deploy(wallets[0]).send({ contractAddressSalt: Fr.ZERO }).wait(); contract = receipt.contract; + + rollup = getContract({ + address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: deployL1ContractsValues.walletClient, + }); }); afterAll(() => teardown()); @@ -92,8 +99,11 @@ describe('E2E Outbox Tests', () => { // Outbox L1 tests + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await rollup.write.setAssumeProvenUntilBlockNumber([1 + (txReceipt.blockNumber ?? 0)]); + // Check L1 has expected message tree - const [l1Root, l1MinHeight] = await outbox.read.roots([txReceipt.blockNumber]); + const [l1Root, l1MinHeight] = await outbox.read.getRootData([txReceipt.blockNumber]); expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); // The path for the message should have the shortest possible height, since we only have 2 msgs expect(l1MinHeight).toEqual(BigInt(siblingPath.pathSize)); @@ -207,9 +217,11 @@ describe('E2E Outbox Tests', () => { expect(siblingPath2.pathSize).toBe(3); // Outbox L1 tests + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await rollup.write.setAssumeProvenUntilBlockNumber([1 + (l2TxReceipt0.blockNumber ?? 0)]); // Check L1 has expected message tree - const [l1Root, l1MinHeight] = await outbox.read.roots([l2TxReceipt0.blockNumber]); + const [l1Root, l1MinHeight] = await outbox.read.getRootData([l2TxReceipt0.blockNumber]); expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); // The path for the single message should have the shortest possible height @@ -323,9 +335,11 @@ describe('E2E Outbox Tests', () => { expect(index2).toBe(2n); // Outbox L1 tests + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await rollup.write.setAssumeProvenUntilBlockNumber([1 + (l2TxReceipt0.blockNumber ?? 0)]); // Check L1 has expected message tree - const [l1Root, l1MinHeight] = await outbox.read.roots([l2TxReceipt0.blockNumber]); + const [l1Root, l1MinHeight] = await outbox.read.getRootData([l2TxReceipt0.blockNumber]); expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); // The path for the message should have the shortest possible height, since we only have one msg per tx expect(l1MinHeight).toEqual(BigInt(siblingPath.pathSize)); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts index 60e6edc6c3f..c3ca23206f4 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts @@ -99,6 +99,8 @@ describe('e2e_public_cross_chain_messaging deposits', () => { l2ToL1Message, ); + await t.assumeProven(); + await crossChainTestHarness.withdrawFundsFromBridgeOnL1( withdrawAmount, l2TxReceipt.blockNumber!, diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts index 0be2acfddbf..73d8b435ac3 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts @@ -74,6 +74,8 @@ describe('e2e_public_cross_chain_messaging l2_to_l1', () => { leaf, ); + await t.assumeProven(); + const txHash = await outbox.write.consume( [ l2ToL1Message, diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts index b571600ffea..9771c8403cd 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts @@ -11,7 +11,7 @@ import { createDebugLogger, } from '@aztec/aztec.js'; import { createL1Clients } from '@aztec/ethereum'; -import { InboxAbi, OutboxAbi, PortalERC20Abi, TokenPortalAbi } from '@aztec/l1-artifacts'; +import { InboxAbi, OutboxAbi, PortalERC20Abi, RollupAbi, TokenPortalAbi } from '@aztec/l1-artifacts'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts.js'; import { type Chain, type HttpTransport, type PublicClient, getContract } from 'viem'; @@ -47,6 +47,7 @@ export class PublicCrossChainMessagingContractTest { l2Token!: TokenContract; l2Bridge!: TokenBridgeContract; + rollup!: any; // GetContractReturnType | undefined; inbox!: any; // GetContractReturnType | undefined; outbox!: any; // GetContractReturnType | undefined; @@ -55,6 +56,10 @@ export class PublicCrossChainMessagingContractTest { this.snapshotManager = createSnapshotManager(`e2e_public_cross_chain_messaging/${testName}`, dataPath); } + async assumeProven() { + await this.rollup.write.setAssumeProvenUntilBlockNumber([await this.rollup.read.pendingBlockCount()]); + } + async setup() { const { aztecNode, pxe, aztecNodeConfig } = await this.snapshotManager.setup(); this.aztecNode = aztecNode; @@ -79,12 +84,18 @@ export class PublicCrossChainMessagingContractTest { await this.snapshotManager.snapshot( '3_accounts', addAccounts(3, this.logger), - async ({ accountKeys }, { pxe, aztecNodeConfig, aztecNode }) => { + async ({ accountKeys }, { pxe, aztecNodeConfig, aztecNode, deployL1ContractsValues }) => { const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1)); this.wallets = await Promise.all(accountManagers.map(a => a.getWallet())); this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`)); this.accounts = await pxe.getRegisteredAccounts(); + this.rollup = getContract({ + address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: deployL1ContractsValues.walletClient, + }); + this.user1Wallet = this.wallets[0]; this.user2Wallet = this.wallets[1]; diff --git a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts index f2baacdb8e0..8fe97ad2065 100644 --- a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts +++ b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts @@ -8,9 +8,9 @@ import { type PXE, computeAuthWitMessageHash, } from '@aztec/aztec.js'; -import { deployL1Contract } from '@aztec/ethereum'; +import { type DeployL1Contracts, deployL1Contract } from '@aztec/ethereum'; import { sha256ToField } from '@aztec/foundation/crypto'; -import { InboxAbi, UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; +import { InboxAbi, RollupAbi, UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; import { UniswapContract } from '@aztec/noir-contracts.js/Uniswap'; import { jest } from '@jest/globals'; @@ -56,6 +56,8 @@ export type UniswapSetupContext = { ownerWallet: AccountWallet; /** The sponsor wallet. */ sponsorWallet: AccountWallet; + /** */ + deployL1ContractsValues: DeployL1Contracts; }; // docs:end:uniswap_l1_l2_test_setup_const @@ -88,6 +90,8 @@ export const uniswapL1L2TestSuite = ( let daiCrossChainHarness: CrossChainTestHarness; let wethCrossChainHarness: CrossChainTestHarness; + let deployL1ContractsValues: DeployL1Contracts; + let rollup: any; let uniswapPortal: GetContractReturnType>; let uniswapPortalAddress: EthAddress; let uniswapL2Contract: UniswapContract; @@ -97,12 +101,19 @@ export const uniswapL1L2TestSuite = ( const minimumOutputAmount = 0n; beforeAll(async () => { - ({ aztecNode, pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet } = await setup()); + ({ aztecNode, pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet, deployL1ContractsValues } = + await setup()); if (Number(await publicClient.getBlockNumber()) < expectedForkBlockNumber) { throw new Error('This test must be run on a fork of mainnet with the expected fork block'); } + rollup = getContract({ + address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: deployL1ContractsValues.walletClient, + }); + ownerAddress = ownerWallet.getAddress(); // sponsorAddress = sponsorWallet.getAddress(); ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); @@ -285,6 +296,9 @@ export const uniswapL1L2TestSuite = ( // ensure that uniswap contract didn't eat the funds. await wethCrossChainHarness.expectPublicBalanceOnL2(uniswapL2Contract.address, 0n); + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await rollup.write.setAssumeProvenUntilBlockNumber([await rollup.read.pendingBlockCount()]); + // 5. Consume L2 to L1 message by calling uniswapPortal.swap_private() logger.info('Execute withdraw and swap on the uniswapPortal!'); const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( @@ -917,6 +931,9 @@ export const uniswapL1L2TestSuite = ( // ensure that user's funds were burnt await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await rollup.write.setAssumeProvenUntilBlockNumber([await rollup.read.pendingBlockCount()]); + // On L1 call swap_public! logger.info('call swap_public on L1'); const swapArgs = [ @@ -1048,6 +1065,9 @@ export const uniswapL1L2TestSuite = ( // check weth balance of owner on L2 (we first bridged `wethAmountToBridge` into L2 and now withdrew it!) await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, 0n); + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await rollup.write.setAssumeProvenUntilBlockNumber([await rollup.read.pendingBlockCount()]); + // Call swap_private on L1 const secretHashForRedeemingDai = Fr.random(); // creating my own secret hash logger.info('Execute withdraw and swap on the uniswapPortal!');