diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 80ec25e9e18..4ab86edf4e0 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -10,7 +10,8 @@ import { FeeHeader, ManaBaseFeeComponents, BlockLog, - L1FeeData + L1FeeData, + SubmitEpochRootProofArgs } from "@aztec/core/interfaces/IRollup.sol"; import {IVerifier} from "@aztec/core/interfaces/IVerifier.sol"; import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; @@ -51,6 +52,13 @@ struct Config { uint256 aztecEpochProofClaimWindowInL2Slots; } +struct SubmitEpochRootProofInterimValues { + uint256 previousBlockNumber; + uint256 endBlockNumber; + Epoch epochToProve; + Epoch startEpoch; +} + /** * @title Rollup * @author Aztec Labs @@ -64,6 +72,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { using SafeERC20 for IERC20; using ProposeLib for ProposeArgs; using FeeMath for uint256; + using FeeMath for ManaBaseFeeComponents; struct L1GasOracleValues { L1FeeData pre; @@ -81,6 +90,10 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { // for justification of CLAIM_DURATION_IN_L2_SLOTS. uint256 public constant PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST = 1000; + // A Cuauhxicalli [kʷaːʍʃiˈkalːi] ("eagle gourd bowl") is a ceremonial Aztec vessel or altar used to hold offerings, + // such as sacrificial hearts, during rituals performed within temples. + address public constant CUAUHXICALLI = address(bytes20("CUAUHXICALLI")); + address public constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); bool public immutable IS_FOUNDRY_TEST; @@ -154,7 +167,8 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { excessMana: 0, feeAssetPriceNumerator: 0, manaUsed: 0, - provingCostPerManaNumerator: 0 + provingCostPerManaNumerator: 0, + congestionCost: 0 }), archive: bytes32(Constants.GENESIS_ARCHIVE_ROOT), blockHash: bytes32(0), // TODO(palla/prover): The first block does not have hash zero @@ -263,37 +277,45 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { * @dev We provide the `_archive` and `_blockHash` even if it could be read from storage itself because it allow for * better error messages. Without passing it, we would just have a proof verification failure. * - * @param _epochSize - The size of the epoch (to be promoted to a constant) - * @param _args - Array of public inputs to the proof (previousArchive, endArchive, previousBlockHash, endBlockHash, endTimestamp, outHash, proverId) - * @param _fees - Array of recipient-value pairs with fees to be distributed for the epoch - * @param _aggregationObject - The aggregation object for the proof - * @param _proof - The proof to verify + * @param _args - The arguments to submit the epoch root proof: + * _epochSize - The size of the epoch (to be promoted to a constant) + * _args - Array of public inputs to the proof (previousArchive, endArchive, previousBlockHash, endBlockHash, endTimestamp, outHash, proverId) + * _fees - Array of recipient-value pairs with fees to be distributed for the epoch + * _aggregationObject - The aggregation object for the proof + * _proof - The proof to verify */ - function submitEpochRootProof( - uint256 _epochSize, - bytes32[7] calldata _args, - bytes32[] calldata _fees, - bytes calldata _aggregationObject, - bytes calldata _proof - ) external override(IRollup) { + function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) external override(IRollup) { if (canPrune()) { _prune(); } - uint256 previousBlockNumber = tips.provenBlockNumber; - uint256 endBlockNumber = previousBlockNumber + _epochSize; + SubmitEpochRootProofInterimValues memory interimValues; + + interimValues.previousBlockNumber = tips.provenBlockNumber; + interimValues.endBlockNumber = interimValues.previousBlockNumber + _args.epochSize; // @note The getEpochForBlock is expected to revert if the block is beyond pending. // If this changes you are gonna get so rekt you won't believe it. // I mean proving blocks that have been pruned rekt. - Epoch epochToProve = getEpochForBlock(endBlockNumber); + interimValues.epochToProve = getEpochForBlock(interimValues.endBlockNumber); + interimValues.startEpoch = getEpochForBlock(interimValues.previousBlockNumber + 1); + + // Ensure that the proof is not across epochs + require( + interimValues.startEpoch == interimValues.epochToProve, + Errors.Rollup__InvalidEpoch(interimValues.startEpoch, interimValues.epochToProve) + ); bytes32[] memory publicInputs = - getEpochProofPublicInputs(_epochSize, _args, _fees, _aggregationObject); + getEpochProofPublicInputs(_args.epochSize, _args.args, _args.fees, _args.aggregationObject); + + require(epochProofVerifier.verify(_args.proof, publicInputs), Errors.Rollup__InvalidProof()); - require(epochProofVerifier.verify(_proof, publicInputs), Errors.Rollup__InvalidProof()); + if (proofClaim.epochToProve == interimValues.epochToProve) { + PROOF_COMMITMENT_ESCROW.unstakeBond(proofClaim.bondProvider, proofClaim.bondAmount); + } - tips.provenBlockNumber = endBlockNumber; + tips.provenBlockNumber = interimValues.endBlockNumber; // @note Only if the rollup is the canonical will it be able to meaningfully claim fees // Otherwise, the fees are unbacked #7938. @@ -301,17 +323,25 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { bool isRewardDistributorCanonical = address(this) == REWARD_DISTRIBUTOR.canonicalRollup(); uint256 totalProverReward = 0; + uint256 totalBurn = 0; if (isFeeCanonical || isRewardDistributorCanonical) { - for (uint256 i = 0; i < _epochSize; i++) { + for (uint256 i = 0; i < _args.epochSize; i++) { address coinbase = address(uint160(uint256(publicInputs[9 + i * 2]))); uint256 reward = 0; uint256 toProver = 0; + uint256 burn = 0; if (isFeeCanonical) { uint256 fees = uint256(publicInputs[10 + i * 2]); if (fees > 0) { - reward += fees; + // This is insanely expensive, and will be fixed as part of the general storage cost reduction. + // See #9826. + FeeHeader storage feeHeader = + blocks[interimValues.previousBlockNumber + 1 + i].feeHeader; + burn += feeHeader.congestionCost * feeHeader.manaUsed; + + reward += (fees - burn); FEE_JUICE_PORTAL.distributeFees(address(this), fees); } } @@ -335,6 +365,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { } totalProverReward += toProver; + totalBurn += burn; } if (totalProverReward > 0) { @@ -343,13 +374,13 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { proofClaim.bondProvider == address(0) ? msg.sender : proofClaim.bondProvider; ASSET.safeTransfer(proofRewardRecipient, totalProverReward); } - } - if (proofClaim.epochToProve == epochToProve) { - PROOF_COMMITMENT_ESCROW.unstakeBond(proofClaim.bondProvider, proofClaim.bondAmount); + if (totalBurn > 0) { + ASSET.safeTransfer(CUAUHXICALLI, totalBurn); + } } - emit L2ProofVerified(endBlockNumber, _args[6]); + emit L2ProofVerified(interimValues.endBlockNumber, _args.args[6]); } function status(uint256 _myHeaderBlockNumber) @@ -520,13 +551,13 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { // Decode and validate header HeaderLib.Header memory header = HeaderLib.decode(_args.header); - bytes32 digest = _args.digest(); setupEpoch(); - uint256 manaBaseFee = getManaBaseFee(true); + ManaBaseFeeComponents memory components = getManaBaseFeeComponents(true); + uint256 manaBaseFee = FeeMath.summedBaseFee(components); _validateHeader({ _header: header, _signatures: _signatures, - _digest: digest, + _digest: _args.digest(), _currentTime: Timestamp.wrap(block.timestamp), _manaBaseFee: manaBaseFee, _txEffectsHash: txsEffectsHash, @@ -553,17 +584,20 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { manaUsed: header.totalManaUsed, provingCostPerManaNumerator: parentFeeHeader.provingCostPerManaNumerator.clampedAdd( _args.oracleInput.provingCostModifier - ) + ), + congestionCost: components.congestionCost }) }); } // @note The block number here will always be >=1 as the genesis block is at 0 - bytes32 inHash = INBOX.consume(blockNumber); - require( - header.contentCommitment.inHash == inHash, - Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash) - ); + { + bytes32 inHash = INBOX.consume(blockNumber); + require( + header.contentCommitment.inHash == inHash, + Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash) + ); + } // TODO(#7218): Revert to fixed height tree for outbox, currently just providing min as interim // Min size = smallest path of the rollup tree + 1 @@ -644,9 +678,8 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { * @return The mana base fee */ function getManaBaseFee(bool _inFeeAsset) public view override(IRollup) returns (uint256) { - ManaBaseFeeComponents memory components = manaBaseFeeComponents(_inFeeAsset); - return - components.dataCost + components.gasCost + components.provingCost + components.congestionCost; + ManaBaseFeeComponents memory components = getManaBaseFeeComponents(_inFeeAsset); + return components.summedBaseFee(); } /** @@ -661,7 +694,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { * * @return The mana base fee components */ - function manaBaseFeeComponents(bool _inFeeAsset) + function getManaBaseFeeComponents(bool _inFeeAsset) public view override(ITestRollup) diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 00981c23eaf..f04333c4a22 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -7,14 +7,24 @@ import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {EpochProofQuoteLib} from "@aztec/core/libraries/EpochProofQuoteLib.sol"; +import {ManaBaseFeeComponents} from "@aztec/core/libraries/FeeMath.sol"; import {ProposeArgs} from "@aztec/core/libraries/ProposeLib.sol"; import {Timestamp, Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; +struct SubmitEpochRootProofArgs { + uint256 epochSize; + bytes32[7] args; + bytes32[] fees; + bytes aggregationObject; + bytes proof; +} + struct FeeHeader { uint256 excessMana; uint256 feeAssetPriceNumerator; uint256 manaUsed; uint256 provingCostPerManaNumerator; + uint256 congestionCost; } struct BlockLog { @@ -29,20 +39,12 @@ struct L1FeeData { uint256 blobFee; } -struct ManaBaseFeeComponents { - uint256 congestionCost; - uint256 congestionMultiplier; - uint256 dataCost; - uint256 gasCost; - uint256 provingCost; -} - interface ITestRollup { function setEpochVerifier(address _verifier) external; function setVkTreeRoot(bytes32 _vkTreeRoot) external; function setProtocolContractTreeRoot(bytes32 _protocolContractTreeRoot) external; function setAssumeProvenThroughBlockNumber(uint256 _blockNumber) external; - function manaBaseFeeComponents(bool _inFeeAsset) + function getManaBaseFeeComponents(bool _inFeeAsset) external view returns (ManaBaseFeeComponents memory); @@ -78,13 +80,7 @@ interface IRollup { EpochProofQuoteLib.SignedEpochProofQuote calldata _quote ) external; - function submitEpochRootProof( - uint256 _epochSize, - bytes32[7] calldata _args, - bytes32[] calldata _fees, - bytes calldata _aggregationObject, - bytes calldata _proof - ) external; + function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) external; function canProposeAtTime(Timestamp _ts, bytes32 _archive) external view returns (Slot, uint256); diff --git a/l1-contracts/src/core/libraries/FeeMath.sol b/l1-contracts/src/core/libraries/FeeMath.sol index 6c63cc0e08f..215c2e4739a 100644 --- a/l1-contracts/src/core/libraries/FeeMath.sol +++ b/l1-contracts/src/core/libraries/FeeMath.sol @@ -13,6 +13,14 @@ struct OracleInput { int256 feeAssetPriceModifier; } +struct ManaBaseFeeComponents { + uint256 congestionCost; + uint256 congestionMultiplier; + uint256 dataCost; + uint256 gasCost; + uint256 provingCost; +} + library FeeMath { using Math for uint256; using SafeCast for int256; @@ -81,6 +89,11 @@ library FeeMath { return fakeExponential(MINIMUM_CONGESTION_MULTIPLIER, _numerator, CONGESTION_UPDATE_FRACTION); } + function summedBaseFee(ManaBaseFeeComponents memory _components) internal pure returns (uint256) { + return _components.dataCost + _components.gasCost + _components.provingCost + + _components.congestionCost; + } + /** * @notice An approximation of the exponential function: factor * e ** (numerator / denominator) * diff --git a/l1-contracts/src/core/libraries/crypto/SampleLib.sol b/l1-contracts/src/core/libraries/crypto/SampleLib.sol index bdca8f12628..a790dc6e56f 100644 --- a/l1-contracts/src/core/libraries/crypto/SampleLib.sol +++ b/l1-contracts/src/core/libraries/crypto/SampleLib.sol @@ -21,6 +21,62 @@ import {Errors} from "@aztec/core/libraries/Errors.sol"; * https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf */ library SampleLib { + /** + * @notice Computing a committee the most direct way. + * This is horribly inefficient as we are throwing plenty of things away, but it is useful + * for testing and just showcasing the simplest case. + * + * @param _committeeSize - The size of the committee + * @param _indexCount - The total number of indices + * @param _seed - The seed to use for shuffling + * + * @return indices - The indices of the committee + */ + function computeCommitteeStupid(uint256 _committeeSize, uint256 _indexCount, uint256 _seed) + external + pure + returns (uint256[] memory) + { + uint256[] memory indices = new uint256[](_committeeSize); + + for (uint256 index = 0; index < _indexCount; index++) { + uint256 sampledIndex = computeShuffledIndex(index, _indexCount, _seed); + if (sampledIndex < _committeeSize) { + indices[sampledIndex] = index; + } + } + + return indices; + } + + /** + * @notice Computing a committee slightly more cleverly. + * Only computes for the committee size, and does not sample the full set. + * This is more efficient than the stupid way, but still not optimal. + * To be more clever, we can compute the `shuffeRounds` and `pivots` separately + * such that they get shared accross multiple indices. + * + * @param _committeeSize - The size of the committee + * @param _indexCount - The total number of indices + * @param _seed - The seed to use for shuffling + * + * @return indices - The indices of the committee + */ + function computeCommitteeClever(uint256 _committeeSize, uint256 _indexCount, uint256 _seed) + external + pure + returns (uint256[] memory) + { + uint256[] memory indices = new uint256[](_committeeSize); + + for (uint256 index = 0; index < _committeeSize; index++) { + uint256 originalIndex = computeOriginalIndex(index, _indexCount, _seed); + indices[index] = originalIndex; + } + + return indices; + } + /** * @notice Computes the shuffled index * @@ -78,62 +134,6 @@ library SampleLib { return index; } - /** - * @notice Computing a committee the most direct way. - * This is horribly inefficient as we are throwing plenty of things away, but it is useful - * for testing and just showcasing the simplest case. - * - * @param _committeeSize - The size of the committee - * @param _indexCount - The total number of indices - * @param _seed - The seed to use for shuffling - * - * @return indices - The indices of the committee - */ - function computeCommitteeStupid(uint256 _committeeSize, uint256 _indexCount, uint256 _seed) - internal - pure - returns (uint256[] memory) - { - uint256[] memory indices = new uint256[](_committeeSize); - - for (uint256 index = 0; index < _indexCount; index++) { - uint256 sampledIndex = computeShuffledIndex(index, _indexCount, _seed); - if (sampledIndex < _committeeSize) { - indices[sampledIndex] = index; - } - } - - return indices; - } - - /** - * @notice Computing a committee slightly more cleverly. - * Only computes for the committee size, and does not sample the full set. - * This is more efficient than the stupid way, but still not optimal. - * To be more clever, we can compute the `shuffeRounds` and `pivots` separately - * such that they get shared accross multiple indices. - * - * @param _committeeSize - The size of the committee - * @param _indexCount - The total number of indices - * @param _seed - The seed to use for shuffling - * - * @return indices - The indices of the committee - */ - function computeCommitteeClever(uint256 _committeeSize, uint256 _indexCount, uint256 _seed) - internal - pure - returns (uint256[] memory) - { - uint256[] memory indices = new uint256[](_committeeSize); - - for (uint256 index = 0; index < _committeeSize; index++) { - uint256 originalIndex = computeOriginalIndex(index, _indexCount, _seed); - indices[index] = originalIndex; - } - - return indices; - } - /** * @notice Compute the number of shuffle rounds * diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 3c7883aa54d..3fc32d369d8 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -14,9 +14,8 @@ import {Registry} from "@aztec/governance/Registry.sol"; import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {BlockLog} from "@aztec/core/interfaces/IRollup.sol"; import {Rollup} from "./harnesses/Rollup.sol"; -import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; +import {IRollup, BlockLog, SubmitEpochRootProofArgs} from "@aztec/core/interfaces/IRollup.sol"; import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; import {FeeJuicePortal} from "@aztec/core/FeeJuicePortal.sol"; import {Leonidas} from "@aztec/core/Leonidas.sol"; @@ -726,9 +725,55 @@ contract RollupTest is DecoderBase, TimeFns { assertEq(rollup.getProvenBlockNumber(), 0 + toProve, "Invalid proven block number"); } + function testRevertSubmittingProofForBlocksAcrossEpochs() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 1); + _testBlock("mixed_block_2", false, TestConstants.AZTEC_EPOCH_DURATION + 1); + + DecoderBase.Data memory data = load("mixed_block_2").block; + + assertEq(rollup.getProvenBlockNumber(), 0, "Invalid initial proven block number"); + + BlockLog memory blockLog = rollup.getBlock(0); + + bytes32[7] memory args = [ + blockLog.archive, + data.archive, + blockLog.blockHash, + data.blockHash, + bytes32(0), + bytes32(0), + bytes32(0) + ]; + + bytes32[] memory fees = new bytes32[](Constants.AZTEC_MAX_EPOCH_DURATION * 2); + + fees[0] = bytes32(uint256(uint160(address(0)))); + fees[1] = bytes32(0); + + bytes memory aggregationObject = ""; + bytes memory proof = ""; + + vm.expectRevert( + abi.encodeWithSelector(Errors.Rollup__InvalidEpoch.selector, Epoch.wrap(0), Epoch.wrap(1)) + ); + + rollup.submitEpochRootProof( + SubmitEpochRootProofArgs({ + epochSize: 2, + args: args, + fees: fees, + aggregationObject: aggregationObject, + proof: proof + }) + ); + + assertEq(rollup.getPendingBlockNumber(), 2, "Invalid pending block number"); + assertEq(rollup.getProvenBlockNumber(), 0, "Invalid proven block number"); + } + function testProveEpochWithTwoMixedBlocks() public setUpFor("mixed_block_1") { - _testBlock("mixed_block_1", false); - _testBlock("mixed_block_2", false); + _testBlock("mixed_block_1", false, 1); + _testBlock("mixed_block_2", false, 2); DecoderBase.Data memory data = load("mixed_block_2").block; @@ -1125,7 +1170,15 @@ contract RollupTest is DecoderBase, TimeFns { bytes memory aggregationObject = ""; bytes memory proof = ""; - _rollup.submitEpochRootProof(_epochSize, args, fees, aggregationObject, proof); + _rollup.submitEpochRootProof( + SubmitEpochRootProofArgs({ + epochSize: _epochSize, + args: args, + fees: fees, + aggregationObject: aggregationObject, + proof: proof + }) + ); } function _quoteToSignedQuote(EpochProofQuoteLib.EpochProofQuote memory _quote) diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index 2f149ed84b7..512d9fd2f8c 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -20,7 +20,8 @@ import { BlockLog, L1FeeData, FeeHeader, - ManaBaseFeeComponents + ManaBaseFeeComponents, + SubmitEpochRootProofArgs } from "@aztec/core/Rollup.sol"; import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; @@ -37,6 +38,7 @@ import {IFeeJuicePortal} from "@aztec/core/interfaces/IFeeJuicePortal.sol"; import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributor.sol"; import {OracleInput} from "@aztec/core/libraries/FeeMath.sol"; import {ProposeArgs, OracleInput, ProposeLib} from "@aztec/core/libraries/ProposeLib.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import { FeeHeader as FeeHeaderModel, @@ -52,17 +54,32 @@ import {MinimalFeeModel} from "./MinimalFeeModel.sol"; // solhint-disable comprehensive-interface contract FakeCanonical { - function canonicalRollup() external view returns (address) { - return msg.sender; + uint256 public constant BLOCK_REWARD = 50e18; + IERC20 public immutable UNDERLYING; + + address public canonicalRollup; + + constructor(IERC20 _asset) { + UNDERLYING = _asset; + } + + function setCanonicalRollup(address _rollup) external { + canonicalRollup = _rollup; + } + + function claim(address _recipient) external returns (uint256) { + TestERC20(address(UNDERLYING)).mint(_recipient, BLOCK_REWARD); + return BLOCK_REWARD; } - function UNDERLYING() external pure returns (address) { - return address(0); + function distributeFees(address _recipient, uint256 _amount) external { + TestERC20(address(UNDERLYING)).mint(_recipient, _amount); } } contract FeeRollupTest is FeeModelTestPoints, DecoderBase { using SlotLib for Slot; + using EpochLib for Epoch; // We need to build a block that we can submit. We will be using some values from // the empty blocks, but otherwise populate using the fee model test points. @@ -82,6 +99,10 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { Rollup internal rollup; + address internal coinbase = address(bytes20("MONEY MAKER")); + TestERC20 internal asset; + FakeCanonical internal fakeCanonical; + function setUp() public { // We deploy a the rollup and sets the time and all to @@ -89,7 +110,9 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { vm.fee(l1Metadata[0].base_fee); vm.blobBaseFee(l1Metadata[0].blob_fee); - FakeCanonical fakeCanonical = new FakeCanonical(); + asset = new TestERC20(); + + fakeCanonical = new FakeCanonical(IERC20(address(asset))); rollup = new Rollup( IFeeJuicePortal(address(fakeCanonical)), IRewardDistributor(address(fakeCanonical)), @@ -104,6 +127,13 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { aztecEpochProofClaimWindowInL2Slots: 16 }) ); + fakeCanonical.setCanonicalRollup(address(rollup)); + + vm.label(coinbase, "coinbase"); + vm.label(address(rollup), "ROLLUP"); + vm.label(address(fakeCanonical), "FAKE CANONICAL"); + vm.label(address(asset), "ASSET"); + vm.label(rollup.CUAUHXICALLI(), "CUAUHXICALLI"); } function _loadL1Metadata(uint256 index) internal { @@ -145,6 +175,9 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { uint256 manaSpent = point.block_header.mana_spent; + // Put coinbase onto the stack + address cb = coinbase; + // Updating the header with important information! assembly { let headerRef := add(header, 0x20) @@ -162,12 +195,11 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { mstore(add(headerRef, 0x0174), bn) mstore(add(headerRef, 0x0194), slotNumber) mstore(add(headerRef, 0x01b4), ts) - mstore(add(headerRef, 0x01d4), 0) - mstore(add(headerRef, 0x01e8), 0) - mstore(add(headerRef, 0x0208), 0) - mstore(add(headerRef, 0x0228), manaBaseFee) - - mstore(add(headerRef, 0x0268), manaSpent) + mstore(add(headerRef, 0x01d4), cb) // coinbase + mstore(add(headerRef, 0x01e8), 0) // fee recipient + mstore(add(headerRef, 0x0208), 0) // fee per da gas + mstore(add(headerRef, 0x0228), manaBaseFee) // fee per l2 gas + mstore(add(headerRef, 0x0268), manaSpent) // total mana used } return Block({ @@ -180,10 +212,9 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { }); } - function test_BigBrainTime() public { - rollup.setAssumeProvenThroughBlockNumber(10000); - + function test_FeeModelEquivalence() public { Slot nextSlot = Slot.wrap(1); + Epoch nextEpoch = Epoch.wrap(1); // Loop through all of the L1 metadata for (uint256 i = 0; i < l1Metadata.length; i++) { @@ -198,8 +229,8 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { L1FeeData memory fees = rollup.getCurrentL1Fees(); uint256 feeAssetPrice = rollup.getFeeAssetPrice(); - ManaBaseFeeComponents memory components = rollup.manaBaseFeeComponents(false); - ManaBaseFeeComponents memory componentsFeeAsset = rollup.manaBaseFeeComponents(true); + ManaBaseFeeComponents memory components = rollup.getManaBaseFeeComponents(false); + ManaBaseFeeComponents memory componentsFeeAsset = rollup.getManaBaseFeeComponents(true); BlockLog memory parentBlockLog = rollup.getBlock(nextSlot.unwrap() - 1); Block memory b = getBlock(); @@ -221,6 +252,11 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { BlockLog memory blockLog = rollup.getBlock(nextSlot.unwrap()); + assertEq( + componentsFeeAsset.congestionCost, + blockLog.feeHeader.congestionCost, + "congestion cost mismatch" + ); // Want to check the fee header to see if they are as we want them. assertEq(point.block_header.block_number, nextSlot, "invalid l2 block number"); @@ -243,6 +279,78 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { nextSlot = nextSlot + Slot.wrap(1); } + + // If we are entering a new epoch, we will post a proof + // Ensure that the fees are split correctly between sequencers and burns etc. + if (rollup.getCurrentEpoch() == nextEpoch) { + nextEpoch = nextEpoch + Epoch.wrap(1); + uint256 pendingBlockNumber = rollup.getPendingBlockNumber(); + uint256 start = rollup.getProvenBlockNumber() + 1; + uint256 epochSize = 0; + while ( + start + epochSize <= pendingBlockNumber + && rollup.getEpochForBlock(start) == rollup.getEpochForBlock(start + epochSize) + ) { + epochSize++; + } + + uint256 feeSum = 0; + uint256 burnSum = 0; + bytes32[] memory fees = new bytes32[](Constants.AZTEC_MAX_EPOCH_DURATION * 2); + + for (uint256 feeIndex = 0; feeIndex < epochSize; feeIndex++) { + TestPoint memory point = points[start + feeIndex - 1]; + + // We assume that everyone PERFECTLY pays their fees with 0 priority fees and no + // overpaying on teardown. + uint256 baseFee = point.outputs.mana_base_fee_components_in_fee_asset.data_cost + + point.outputs.mana_base_fee_components_in_fee_asset.gas_cost + + point.outputs.mana_base_fee_components_in_fee_asset.proving_cost + + point.outputs.mana_base_fee_components_in_fee_asset.congestion_cost; + + uint256 fee = rollup.getBlock(start + feeIndex).feeHeader.manaUsed * baseFee; + feeSum += fee; + burnSum += rollup.getBlock(start + feeIndex).feeHeader.manaUsed + * point.outputs.mana_base_fee_components_in_fee_asset.congestion_cost; + + fees[feeIndex * 2] = bytes32(uint256(uint160(coinbase))); + fees[feeIndex * 2 + 1] = bytes32(fee); + } + + bytes memory aggregationObject = ""; + bytes memory proof = ""; + + uint256 cuauhxicalliBalanceBefore = asset.balanceOf(rollup.CUAUHXICALLI()); + uint256 coinbaseBalanceBefore = asset.balanceOf(coinbase); + + bytes32[7] memory args = [ + rollup.getBlock(start).archive, + rollup.getBlock(start + epochSize - 1).archive, + rollup.getBlock(start).blockHash, + rollup.getBlock(start + epochSize - 1).blockHash, + bytes32(0), + bytes32(0), + bytes32(0) + ]; + rollup.submitEpochRootProof( + SubmitEpochRootProofArgs({ + epochSize: epochSize, + args: args, + fees: fees, + aggregationObject: aggregationObject, + proof: proof + }) + ); + + uint256 burned = asset.balanceOf(rollup.CUAUHXICALLI()) - cuauhxicalliBalanceBefore; + assertEq( + asset.balanceOf(coinbase) - coinbaseBalanceBefore + - fakeCanonical.BLOCK_REWARD() * epochSize + burned, + feeSum, + "Sum of fees does not match" + ); + assertEq(burnSum, burned, "Sum of burned does not match"); + } } } diff --git a/yarn-project/archiver/src/archiver/data_retrieval.ts b/yarn-project/archiver/src/archiver/data_retrieval.ts index d6ce1653c4c..3249a5fc541 100644 --- a/yarn-project/archiver/src/archiver/data_retrieval.ts +++ b/yarn-project/archiver/src/archiver/data_retrieval.ts @@ -291,11 +291,20 @@ export async function getProofFromSubmitProofTx( let proof: Proof; if (functionName === 'submitEpochRootProof') { - const [_epochSize, nestedArgs, _fees, aggregationObjectHex, proofHex] = args!; - aggregationObject = Buffer.from(hexToBytes(aggregationObjectHex)); - proverId = Fr.fromString(nestedArgs[6]); - archiveRoot = Fr.fromString(nestedArgs[1]); - proof = Proof.fromBuffer(Buffer.from(hexToBytes(proofHex))); + const [decodedArgs] = args as readonly [ + { + epochSize: bigint; + args: readonly [Hex, Hex, Hex, Hex, Hex, Hex, Hex]; + fees: readonly Hex[]; + aggregationObject: Hex; + proof: Hex; + }, + ]; + + aggregationObject = Buffer.from(hexToBytes(decodedArgs.aggregationObject)); + proverId = Fr.fromString(decodedArgs.args[6]); + archiveRoot = Fr.fromString(decodedArgs.args[1]); + proof = Proof.fromBuffer(Buffer.from(hexToBytes(decodedArgs.proof))); } else { throw new Error(`Unexpected proof method called ${functionName}`); } diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index d1b8dc91687..cb647ffda9c 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -22,6 +22,8 @@ import { RollupAbi, RollupBytecode, RollupLinkReferences, + SampleLibAbi, + SampleLibBytecode, TestERC20Abi, TestERC20Bytecode, TxsDecoderAbi, @@ -173,6 +175,10 @@ export const l1Artifacts: L1ContractArtifactsForDeployment = { contractAbi: TxsDecoderAbi, contractBytecode: TxsDecoderBytecode, }, + SampleLib: { + contractAbi: SampleLibAbi, + contractBytecode: SampleLibBytecode, + }, }, }, }, @@ -623,9 +629,15 @@ export async function deployL1Contract( ); for (const linkRef in libraries.linkReferences) { - for (const c in libraries.linkReferences[linkRef]) { - const start = 2 + 2 * libraries.linkReferences[linkRef][c][0].start; - const length = 2 * libraries.linkReferences[linkRef][c][0].length; + for (const contractName in libraries.linkReferences[linkRef]) { + // If the library name matches the one we just deployed, we replace it. + if (contractName !== libraryName) { + continue; + } + + // We read the first instance to figure out what we are to replace. + const start = 2 + 2 * libraries.linkReferences[linkRef][contractName][0].start; + const length = 2 * libraries.linkReferences[linkRef][contractName][0].length; const toReplace = bytecode.slice(start, start + length); replacements[toReplace] = address; diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index 4ddefda991c..ef0892de022 100755 --- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh +++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh @@ -29,6 +29,7 @@ CONTRACTS=( "l1-contracts:Governance" "l1-contracts:NewGovernanceProposerPayload" "l1-contracts:TxsDecoder" + "l1-contracts:SampleLib" ) diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 2bc6c67d9b8..b1e0aa5a50c 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -707,7 +707,18 @@ export class L1Publisher { }): Promise { try { const proofHex: Hex = `0x${args.proof.withoutPublicInputs().toString('hex')}`; - const txArgs = [...this.getSubmitEpochProofArgs(args), proofHex] as const; + const argsArray = this.getSubmitEpochProofArgs(args); + + const txArgs = [ + { + epochSize: argsArray[0], + args: argsArray[1], + fees: argsArray[2], + aggregationObject: argsArray[3], + proof: proofHex, + }, + ] as const; + this.log.info(`SubmitEpochProof proofSize=${args.proof.withoutPublicInputs().length} bytes`); await this.rollupContract.simulate.submitEpochRootProof(txArgs, { account: this.account }); return await this.rollupContract.write.submitEpochRootProof(txArgs, { account: this.account });