diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 73d18edea6f9..5879176ed96b 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -37,6 +37,7 @@ import {RewardLib, RewardConfig} from "@aztec/core/libraries/rollup/RewardLib.so import {StakingQueueConfig} from "@aztec/core/libraries/compressed-data/StakingQueueConfig.sol"; import {FeeConfigLib, CompressedFeeConfig} from "@aztec/core/libraries/compressed-data/fees/FeeConfig.sol"; import {G1Point, G2Point} from "@aztec/shared/libraries/BN254Lib.sol"; +import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol"; /** * @title RollupCore @@ -177,6 +178,7 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali using TimeLib for Slot; using TimeLib for Epoch; using FeeConfigLib for CompressedFeeConfig; + using ChainTipsLib for CompressedChainTips; /** * @notice The L1 block number when this rollup was deployed @@ -319,6 +321,13 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali uint256 currentManaTarget = FeeLib.getStorage().config.getManaTarget(); require(_manaTarget >= currentManaTarget, Errors.Rollup__InvalidManaTarget(currentManaTarget, _manaTarget)); FeeLib.updateManaTarget(_manaTarget); + + // If we are going from 0 to non-zero mana limits, we need to catch up the inbox + if (currentManaTarget == 0 && _manaTarget > 0) { + RollupStore storage rollupStore = STFLib.getStorage(); + rollupStore.config.inbox.catchUp(rollupStore.tips.getPendingBlockNumber()); + } + emit IRollupCore.ManaTargetUpdated(_manaTarget); } diff --git a/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol b/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol index db2f6982cab6..d5a23274f7e3 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol @@ -30,6 +30,8 @@ interface IInbox { */ event MessageSent(uint256 indexed l2BlockNumber, uint256 index, bytes32 indexed hash, bytes16 rollingHash); + event InboxSynchronized(uint256 indexed inProgress); + // docs:start:send_l1_to_l2_message /** * @notice Inserts a new message into the Inbox @@ -59,6 +61,8 @@ interface IInbox { function consume(uint256 _toConsume) external returns (bytes32); // docs:end:consume + function catchUp(uint256 _pendingBlockNumber) external; + function getFeeAssetPortal() external view returns (address); function getRoot(uint256 _blockNumber) external view returns (bytes32); diff --git a/l1-contracts/src/core/messagebridge/Inbox.sol b/l1-contracts/src/core/messagebridge/Inbox.sol index 922f1864d520..34b77688bb0f 100644 --- a/l1-contracts/src/core/messagebridge/Inbox.sol +++ b/l1-contracts/src/core/messagebridge/Inbox.sol @@ -11,6 +11,7 @@ import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {FeeJuicePortal} from "@aztec/core/messagebridge/FeeJuicePortal.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeCast} from "@oz/utils/math/SafeCast.sol"; /** * @title Inbox @@ -153,6 +154,21 @@ contract Inbox is IInbox { return root; } + /** + * @notice Catch up the inbox to the pending block number + * + * @dev Only callable by the rollup contract + * Will only be called WHEN a change is made from 0 to non-zero mana limits + * + * @param _pendingBlockNumber - The pending block number to catch up to + */ + function catchUp(uint256 _pendingBlockNumber) external override(IInbox) { + require(msg.sender == ROLLUP, Errors.Inbox__Unauthorized()); + // The next expected will be 1 ahead of the next block, e.g., + 2 from current. + state.inProgress = SafeCast.toUint64(_pendingBlockNumber + 2); + emit InboxSynchronized(state.inProgress); + } + function getFeeAssetPortal() external view override(IInbox) returns (address) { return FEE_ASSET_PORTAL; } diff --git a/l1-contracts/test/ignition.t.sol b/l1-contracts/test/ignition/ignition.t.sol similarity index 92% rename from l1-contracts/test/ignition.t.sol rename to l1-contracts/test/ignition/ignition.t.sol index 3db4e7a6f06e..c2e8c08f0e7b 100644 --- a/l1-contracts/test/ignition.t.sol +++ b/l1-contracts/test/ignition/ignition.t.sol @@ -2,12 +2,12 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {DecoderBase} from "./base/DecoderBase.sol"; +import {DecoderBase} from "../base/DecoderBase.sol"; import {Registry} from "@aztec/governance/Registry.sol"; import {FeeJuicePortal} from "@aztec/core/messagebridge/FeeJuicePortal.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; -import {TestConstants} from "./harnesses/TestConstants.sol"; +import {TestConstants} from "../harnesses/TestConstants.sol"; import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; import {ProposeArgs, ProposeLib} from "@aztec/core/libraries/rollup/ProposeLib.sol"; @@ -15,9 +15,9 @@ import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {RollupBase, IInstance} from "./base/RollupBase.sol"; -import {RollupBuilder} from "./builder/RollupBuilder.sol"; -import {TimeCheater} from "./staking/TimeCheater.sol"; +import {RollupBase, IInstance} from "../base/RollupBase.sol"; +import {RollupBuilder} from "../builder/RollupBuilder.sol"; +import {TimeCheater} from "../staking/TimeCheater.sol"; import {Bps, BpsLib} from "@aztec/core/libraries/rollup/RewardLib.sol"; // solhint-disable comprehensive-interface diff --git a/l1-contracts/test/ignition/tmnt223.t.sol b/l1-contracts/test/ignition/tmnt223.t.sol new file mode 100644 index 000000000000..6815d3af7465 --- /dev/null +++ b/l1-contracts/test/ignition/tmnt223.t.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {DecoderBase} from "../base/DecoderBase.sol"; + +import {Registry} from "@aztec/governance/Registry.sol"; +import {FeeJuicePortal} from "@aztec/core/messagebridge/FeeJuicePortal.sol"; +import {TestERC20} from "@aztec/mock/TestERC20.sol"; +import {TestConstants} from "../harnesses/TestConstants.sol"; +import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; +import {ProposeArgs, ProposeLib} from "@aztec/core/libraries/rollup/ProposeLib.sol"; + +import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; + +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {ProposeArgs, ProposePayload, OracleInput, ProposeLib} from "@aztec/core/libraries/rollup/ProposeLib.sol"; + +import {RollupBase, IInstance} from "../base/RollupBase.sol"; +import {RollupBuilder} from "../builder/RollupBuilder.sol"; +import {TimeCheater} from "../staking/TimeCheater.sol"; +import {Bps, BpsLib} from "@aztec/core/libraries/rollup/RewardLib.sol"; +import { + AttestationLib, + Signature, + CommitteeAttestation, + CommitteeAttestations +} from "@aztec/core/libraries/rollup/AttestationLib.sol"; +import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; +import {ProposedHeader} from "@aztec/core/libraries/rollup/ProposedHeaderLib.sol"; +import {SafeCast} from "@oz/utils/math/SafeCast.sol"; +import {AttestationLibHelper} from "@test/helper_libraries/AttestationLibHelper.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; +import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; +// solhint-disable comprehensive-interface + +struct Block { + ProposeArgs proposeArgs; + bytes blobInputs; + CommitteeAttestation[] attestations; + address[] signers; +} + +/** + * Blocks are generated using the `integration_l1_publisher.test.ts` tests. + * Main use of these test is shorter cycles when updating the decoder contract. + */ +contract Tmnt223Test is RollupBase { + using ProposeLib for ProposeArgs; + using TimeLib for Timestamp; + using TimeLib for Slot; + using TimeLib for Epoch; + + Registry internal registry; + TestERC20 internal testERC20; + FeeJuicePortal internal feeJuicePortal; + RewardDistributor internal rewardDistributor; + TimeCheater internal timeCheater; + + uint256 internal SLOT_DURATION; + uint256 internal EPOCH_DURATION; + uint256 internal PROOF_SUBMISSION_EPOCHS; + uint256 internal MANA_TARGET = 0; + + address internal sequencer = address(bytes20("sequencer")); + + DecoderBase.Full internal full; + + /** + * @notice Set up the contracts needed for the tests with time aligned to the provided block name + */ + modifier setUpFor(string memory _name) { + { + full = load(_name); + Slot slotNumber = full.block.header.slotNumber; + uint256 initialTime = Timestamp.unwrap(full.block.header.timestamp) - Slot.unwrap(slotNumber) * SLOT_DURATION; + vm.warp(initialTime); + } + + TimeLib.initialize( + block.timestamp, + TestConstants.AZTEC_SLOT_DURATION, + TestConstants.AZTEC_EPOCH_DURATION, + TestConstants.AZTEC_PROOF_SUBMISSION_EPOCHS + ); + SLOT_DURATION = TestConstants.AZTEC_SLOT_DURATION; + EPOCH_DURATION = TestConstants.AZTEC_EPOCH_DURATION; + PROOF_SUBMISSION_EPOCHS = TestConstants.AZTEC_PROOF_SUBMISSION_EPOCHS; + timeCheater = + new TimeCheater(address(this), block.timestamp, SLOT_DURATION, EPOCH_DURATION, PROOF_SUBMISSION_EPOCHS); + + RollupBuilder builder = new RollupBuilder(address(this)).setManaTarget(MANA_TARGET).setTargetCommitteeSize(0); + builder.deploy(); + + rollup = IInstance(address(builder.getConfig().rollup)); + testERC20 = builder.getConfig().testERC20; + registry = builder.getConfig().registry; + + feeJuicePortal = FeeJuicePortal(address(rollup.getFeeAssetPortal())); + rewardDistributor = RewardDistributor(address(registry.getRewardDistributor())); + + _; + } + + function test_deadlock() public setUpFor("empty_block_1") { + skipBlobCheck(address(rollup)); + timeCheater.cheat__progressSlot(); + + for (uint256 i = 0; i < 10; i++) { + Block memory l2Block = getBlock(); + rollup.propose( + l2Block.proposeArgs, + AttestationLibHelper.packAttestations(l2Block.attestations), + l2Block.signers, + l2Block.blobInputs + ); + timeCheater.cheat__progressSlot(); + } + + // Now say that we alter the mana limit! Ensure that we can still produce blocks! + MANA_TARGET = 1e6; + vm.expectEmit(true, true, true, true, address(rollup.getInbox())); + emit IInbox.InboxSynchronized(12); + vm.prank(Ownable(address(rollup)).owner()); + rollup.updateManaTarget(MANA_TARGET); + + Block memory nonEmptyBlock = getBlock(); + rollup.propose( + nonEmptyBlock.proposeArgs, + AttestationLibHelper.packAttestations(nonEmptyBlock.attestations), + nonEmptyBlock.signers, + nonEmptyBlock.blobInputs + ); + + assertEq(rollup.getPendingBlockNumber(), 11); + assertEq(rollup.getInbox().getInProgress(), 13); + } + + function getBlock() internal view returns (Block memory) { + // We will be using the genesis for both before and after. This will be impossible + // to prove, but we don't need to prove anything here. + bytes32 archiveRoot = bytes32(Constants.GENESIS_ARCHIVE_ROOT); + + ProposedHeader memory header = full.block.header; + + Slot slotNumber = rollup.getCurrentSlot(); + Timestamp ts = rollup.getTimestampForSlot(slotNumber); + + // Updating the header with important information! + header.lastArchiveRoot = archiveRoot; + header.slotNumber = slotNumber; + header.timestamp = ts; + header.coinbase = address(bytes20("coinbase")); + header.feeRecipient = bytes32(0); + header.gasFees.feePerL2Gas = SafeCast.toUint128(rollup.getManaBaseFeeAt(Timestamp.wrap(block.timestamp), true)); + if (MANA_TARGET > 0) { + header.totalManaUsed = MANA_TARGET; + } else { + header.totalManaUsed = 0; + } + + ProposeArgs memory proposeArgs = ProposeArgs({ + header: header, + archive: archiveRoot, + stateReference: EMPTY_STATE_REFERENCE, + oracleInput: OracleInput({feeAssetPriceModifier: 0}) + }); + + CommitteeAttestation[] memory attestations = new CommitteeAttestation[](0); + address[] memory signers = new address[](0); + + return Block({ + proposeArgs: proposeArgs, + blobInputs: full.block.blobCommitments, + attestations: attestations, + signers: signers + }); + } +} diff --git a/yarn-project/end-to-end/src/fixtures/l1_to_l2_messaging.ts b/yarn-project/end-to-end/src/fixtures/l1_to_l2_messaging.ts index ee5be2a99389..662fa37e87a5 100644 --- a/yarn-project/end-to-end/src/fixtures/l1_to_l2_messaging.ts +++ b/yarn-project/end-to-end/src/fixtures/l1_to_l2_messaging.ts @@ -48,20 +48,31 @@ export async function sendL1ToL2Message( throw new Error(`Receipt transaction hash mismatch: ${txReceipt.transactionHash} !== ${txHash}`); } - // Exactly 1 event should be emitted in the transaction - if (txReceipt.logs.length !== 1) { + // Filter for MessageSent events from the Inbox contract by trying to decode each log + const messageSentLogs = txReceipt.logs + .filter(log => log.address.toLowerCase() === ctx.l1ContractAddresses.inboxAddress.toString().toLowerCase()) + .map(log => { + try { + const decoded = decodeEventLog({ + abi: InboxAbi, + data: log.data, + topics: log.topics, + }); + return { log, decoded }; + } catch { + return null; // Not a decodable event from this ABI + } + }) + .filter((item): item is { log: any; decoded: any } => item !== null && item.decoded.eventName === 'MessageSent'); + + if (messageSentLogs.length !== 1) { throw new Error( - `Wrong number of logs found in ${txHash} transaction (got ${txReceipt.logs.length} expected 1)\n${tryJsonStringify(txReceipt.logs)}`, + `Wrong number of MessageSent logs found in ${txHash} transaction (got ${messageSentLogs.length} expected 1)\n${tryJsonStringify(messageSentLogs.map(item => item.log))}`, ); } - // We decode the event and get leaf out of it - const messageSentLog = txReceipt.logs[0]; - const topics = decodeEventLog({ - abi: InboxAbi, - data: messageSentLog.data, - topics: messageSentLog.topics, - }); + // We already have the decoded event + const topics = messageSentLogs[0].decoded; const receivedMsgHash = topics.args.hash; const receivedGlobalLeafIndex = topics.args.index;