From 11bc28517fcbb4d4246702b541adcec3f15e58fe Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 17 Jul 2025 18:44:12 -0300 Subject: [PATCH 01/22] feat: Delayed signature verification during block proposals Implements https://github.com/AztecProtocol/engineering-designs/pull/69 --- l1-contracts/src/core/Rollup.sol | 6 +- l1-contracts/src/core/RollupCore.sol | 17 ++ l1-contracts/src/core/interfaces/IRollup.sol | 17 +- l1-contracts/src/core/libraries/Errors.sol | 5 + .../libraries/compressed-data/BlockLog.sol | 12 ++ .../core/libraries/rollup/EpochProofLib.sol | 38 +++- .../core/libraries/rollup/ExtRollupLib.sol | 27 ++- .../core/libraries/rollup/ExtRollupLib2.sol | 23 ++- .../core/libraries/rollup/InvalidateLib.sol | 167 ++++++++++++++++++ .../src/core/libraries/rollup/ProposeLib.sol | 36 ++-- .../rollup/ValidatorSelectionLib.sol | 77 ++++++-- .../src/shared/libraries/SignatureLib.sol | 50 +++++- l1-contracts/test/Rollup.t.sol | 4 +- l1-contracts/test/base/RollupBase.sol | 2 + l1-contracts/test/benchmark/happy.t.sol | 9 + .../test/compression/PreHeating.t.sol | 8 + l1-contracts/test/fees/FeeRollup.t.sol | 1 + l1-contracts/test/fees/MinimalFeeModel.sol | 2 + 18 files changed, 454 insertions(+), 47 deletions(-) create mode 100644 l1-contracts/src/core/libraries/rollup/InvalidateLib.sol diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 1ff731499b87..25a5fcfda0e7 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -113,12 +113,12 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { ExtRollupLib.validateHeader( ValidateHeaderArgs({ header: _header, - attestations: _attestations, digest: _digest, manaBaseFee: getManaBaseFeeAt(currentTime, true), blobsHashesCommitment: _blobsHash, flags: _flags - }) + }), + _attestations ); } @@ -365,6 +365,8 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { archive: rollupStore.archives[_blockNumber], headerHash: tempBlockLog.headerHash, blobCommitmentsHash: tempBlockLog.blobCommitmentsHash, + attestationsHash: tempBlockLog.attestationsHash, + payloadDigest: tempBlockLog.payloadDigest, slotNumber: tempBlockLog.slotNumber, feeHeader: tempBlockLog.feeHeader }); diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index bc9e6598eb48..7053c2fc5e44 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -250,6 +250,23 @@ contract RollupCore is ExtRollupLib.propose(_args, _attestations, _blobInput, checkBlob); } + function invalidateBadAttestation( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee, + uint256 _invalidIndex + ) external { + ExtRollupLib2.invalidateBadAttestation(_blockNumber, _attestations, _committee, _invalidIndex); + } + + function invalidateInsufficientAttestations( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee + ) external { + ExtRollupLib2.invalidateInsufficientAttestations(_blockNumber, _attestations, _committee); + } + function setupEpoch() public override(IValidatorSelectionCore) { ExtRollupLib2.setupEpoch(); } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 672c4106402a..813ef3cca352 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -36,6 +36,7 @@ struct SubmitEpochRootProofArgs { uint256 end; // inclusive PublicInputArgs args; bytes32[] fees; + CommitteeAttestations attestations; // attestations for the last block in epoch bytes blobInputs; bytes proof; } @@ -43,11 +44,9 @@ struct SubmitEpochRootProofArgs { /** * @notice Struct for storing flags for block header validation * @param ignoreDA - True will ignore DA check, otherwise checks - * @param ignoreSignature - True will ignore the signatures, otherwise checks */ struct BlockHeaderValidationFlags { bool ignoreDA; - bool ignoreSignatures; } struct GenesisState { @@ -98,6 +97,7 @@ interface IRollupCore { uint256 indexed blockNumber, bytes32 indexed archive, bytes32[] versionedBlobHashes ); event L2ProofVerified(uint256 indexed blockNumber, address indexed proverId); + event BlockInvalidated(uint256 indexed blockNumber); event RewardConfigUpdated(RewardConfig rewardConfig); event ManaTargetUpdated(uint256 indexed manaTarget); event PrunedPending(uint256 provenBlockNumber, uint256 pendingBlockNumber); @@ -124,6 +124,19 @@ interface IRollupCore { function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) external; + function invalidateBadAttestation( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee, + uint256 _invalidIndex + ) external; + + function invalidateInsufficientAttestations( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee + ) external; + function setRewardConfig(RewardConfig memory _config) external; function updateManaTarget(uint256 _manaTarget) external; diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index e2682319e25e..7909ae43da39 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -56,6 +56,10 @@ library Errors { error Rollup__InvalidProof(); // 0xa5b2ba17 error Rollup__InvalidProposedArchive(bytes32 expected, bytes32 actual); // 0x32532e73 error Rollup__InvalidTimestamp(Timestamp expected, Timestamp actual); // 0x3132e895 + error Rollup__InvalidAttestations(); + error Rollup__AttestationsAreValid(); + error Rollup__BlockAlreadyProven(); + error Rollup__BlockNotInPendingChain(); error Rollup__InvalidBlobHash(bytes32 expected, bytes32 actual); // 0x13031e6a error Rollup__InvalidBlobProof(bytes32 blobHash); // 0x5ca17bef error Rollup__NoEpochToProve(); // 0xcbaa3951 @@ -102,6 +106,7 @@ library Errors { error ValidatorSelection__InsufficientAttestations(uint256 minimumNeeded, uint256 provided); // 0xaf47297f error ValidatorSelection__InvalidCommitteeCommitment(bytes32 reconstructed, bytes32 expected); // 0xca8d5954 error ValidatorSelection__InsufficientCommitteeSize(uint256 actual, uint256 expected); // 0x98673597 + error ValidatorSelection__ProposerIndexTooLarge(uint256 index); // Staking error Staking__AlreadyQueued(address _attester); diff --git a/l1-contracts/src/core/libraries/compressed-data/BlockLog.sol b/l1-contracts/src/core/libraries/compressed-data/BlockLog.sol index d8d00912c92d..96479484ed17 100644 --- a/l1-contracts/src/core/libraries/compressed-data/BlockLog.sol +++ b/l1-contracts/src/core/libraries/compressed-data/BlockLog.sol @@ -15,12 +15,16 @@ import {Slot} from "@aztec/shared/libraries/TimeMath.sol"; * @param archive - Archive tree root of the block * @param headerHash - Hash of the proposed block header * @param blobCommitmentsHash - H(...H(H(commitment_0), commitment_1).... commitment_n) - used to validate we are using the same blob commitments on L1 and in the rollup circuit + * @param attestationsHash - Hash of the attestations for this block + * @param payloadDigest - Digest of the proposal payload that was attested to * @param slotNumber - This block's slot */ struct BlockLog { bytes32 archive; bytes32 headerHash; bytes32 blobCommitmentsHash; + bytes32 attestationsHash; + bytes32 payloadDigest; Slot slotNumber; FeeHeader feeHeader; } @@ -28,6 +32,8 @@ struct BlockLog { struct TempBlockLog { bytes32 headerHash; bytes32 blobCommitmentsHash; + bytes32 attestationsHash; + bytes32 payloadDigest; Slot slotNumber; FeeHeader feeHeader; } @@ -35,6 +41,8 @@ struct TempBlockLog { struct CompressedTempBlockLog { bytes32 headerHash; bytes32 blobCommitmentsHash; + bytes32 attestationsHash; + bytes32 payloadDigest; CompressedSlot slotNumber; CompressedFeeHeader feeHeader; } @@ -53,6 +61,8 @@ library CompressedTempBlockLogLib { return CompressedTempBlockLog({ headerHash: _blockLog.headerHash, blobCommitmentsHash: _blockLog.blobCommitmentsHash, + attestationsHash: _blockLog.attestationsHash, + payloadDigest: _blockLog.payloadDigest, slotNumber: _blockLog.slotNumber.compress(), feeHeader: _blockLog.feeHeader.compress() }); @@ -66,6 +76,8 @@ library CompressedTempBlockLogLib { return TempBlockLog({ headerHash: _compressedBlockLog.headerHash, blobCommitmentsHash: _compressedBlockLog.blobCommitmentsHash, + attestationsHash: _compressedBlockLog.attestationsHash, + payloadDigest: _compressedBlockLog.payloadDigest, slotNumber: _compressedBlockLog.slotNumber.decompress(), feeHeader: _compressedBlockLog.feeHeader.decompress() }); diff --git a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol index 218f20f8bed0..fafd748a2a37 100644 --- a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol +++ b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol @@ -6,16 +6,20 @@ import { SubmitEpochRootProofArgs, PublicInputArgs, IRollupCore, - RollupStore + RollupStore, + BlockHeaderValidationFlags } from "@aztec/core/interfaces/IRollup.sol"; import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol"; +import {TempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {BlobLib} from "@aztec/core/libraries/rollup/BlobLib.sol"; import {CompressedFeeHeader, FeeHeaderLib} from "@aztec/core/libraries/rollup/FeeLib.sol"; import {RewardLib} from "@aztec/core/libraries/rollup/RewardLib.sol"; import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; +import {ValidatorSelectionLib} from "@aztec/core/libraries/rollup/ValidatorSelectionLib.sol"; import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +import {CommitteeAttestations, SignatureLib} from "@aztec/shared/libraries/SignatureLib.sol"; import {Math} from "@oz/utils/math/Math.sol"; import {SafeCast} from "@oz/utils/math/SafeCast.sol"; @@ -26,6 +30,7 @@ library EpochProofLib { using FeeHeaderLib for CompressedFeeHeader; using SafeCast for uint256; using ChainTipsLib for CompressedChainTips; + using SignatureLib for CommitteeAttestations; // This is a temporary struct to avoid stack too deep errors struct BlobVarsTemp { @@ -65,6 +70,9 @@ library EpochProofLib { Epoch endEpoch = assertAcceptable(_args.start, _args.end); + // Verify attestations for the last block in the epoch + verifyLastBlockAttestations(_args.end, _args.attestations); + require(verifyEpochRootProof(_args), Errors.Rollup__InvalidProof()); RollupStore storage rollupStore = STFLib.getStorage(); @@ -286,4 +294,32 @@ library EpochProofLib { function addressToField(address _a) private pure returns (bytes32) { return bytes32(uint256(uint160(_a))); } + + /** + * @notice Verifies the attestations for the last block in the epoch + * @param _endBlockNumber The last block number in the epoch + * @param _attestations The attestations to verify + */ + function verifyLastBlockAttestations( + uint256 _endBlockNumber, + CommitteeAttestations memory _attestations + ) private { + // Get the stored attestation hash and payload digest for the last block + TempBlockLog memory blockLog = STFLib.getTempBlockLog(_endBlockNumber); + + // Verify that the provided attestations match the stored hash + bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations)); + require( + providedAttestationsHash == blockLog.attestationsHash, Errors.Rollup__InvalidAttestations() + ); + + // Get the slot and epoch for the last block + Slot slot = blockLog.slotNumber; + Epoch epoch = STFLib.getEpochForBlock(_endBlockNumber); + + // Only verify attestations if they are not empty (for testing compatibility) + if (!_attestations.isEmpty()) { + ValidatorSelectionLib.verify(slot, epoch, _attestations, blockLog.payloadDigest); + } + } } diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index e686e0f4ed8f..c6e28abab384 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -4,24 +4,45 @@ pragma solidity >=0.8.27; import {SubmitEpochRootProofArgs, PublicInputArgs} from "@aztec/core/interfaces/IRollup.sol"; -import {Timestamp, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +import {TempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; +import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; +import {Timestamp, TimeLib, Slot, Epoch} from "@aztec/core/libraries/TimeLib.sol"; import {BlobLib} from "./BlobLib.sol"; import {EpochProofLib} from "./EpochProofLib.sol"; +import {InvalidateLib} from "./InvalidateLib.sol"; +import {SignatureLib} from "@aztec/shared/libraries/SignatureLib.sol"; import { - ProposeLib, ProposeArgs, CommitteeAttestations, ValidateHeaderArgs + ProposeLib, + ProposeArgs, + CommitteeAttestations, + ValidateHeaderArgs, + ValidatorSelectionLib } from "./ProposeLib.sol"; // We are using this library such that we can more easily "link" just a larger external library // instead of a few smaller ones. library ExtRollupLib { using TimeLib for Timestamp; + using TimeLib for Slot; + using SignatureLib for CommitteeAttestations; function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) external { EpochProofLib.submitEpochRootProof(_args); } - function validateHeader(ValidateHeaderArgs calldata _args) external { + function validateHeader( + ValidateHeaderArgs calldata _args, + CommitteeAttestations calldata _attestations + ) external { ProposeLib.validateHeader(_args); + if (_attestations.isEmpty()) { + return; // No attestations to validate + } + + Slot slot = _args.header.slotNumber; + Epoch epoch = slot.epochFromSlot(); + ValidatorSelectionLib.verify(slot, epoch, _attestations, _args.digest); + ValidatorSelectionLib.verifyProposer(slot, _attestations, _args.digest); } function propose( diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol index 34625eab3bab..f211ee371fe2 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol @@ -6,7 +6,9 @@ pragma solidity >=0.8.27; import {Epoch, Slot, Timestamp, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; import {StakingQueueConfig} from "@aztec/core/libraries/compressed-data/StakingQueueConfig.sol"; import {StakingLib} from "./StakingLib.sol"; +import {InvalidateLib} from "./InvalidateLib.sol"; import {ValidatorSelectionLib} from "./ValidatorSelectionLib.sol"; +import {CommitteeAttestations} from "@aztec/shared/libraries/SignatureLib.sol"; import { RewardBooster, RewardBoostConfig, @@ -78,12 +80,29 @@ library ExtRollupLib2 { StakingLib.updateStakingQueueConfig(_config); } + function invalidateBadAttestation( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee, + uint256 _invalidIndex + ) external { + InvalidateLib.invalidateBadAttestation(_blockNumber, _attestations, _committee, _invalidIndex); + } + + function invalidateInsufficientAttestations( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee + ) external { + InvalidateLib.invalidateInsufficientAttestations(_blockNumber, _attestations, _committee); + } + function getCommitteeAt(Epoch _epoch) external returns (address[] memory) { return ValidatorSelectionLib.getCommitteeAt(_epoch); } - function getProposerAt(Slot _slot) external returns (address) { - return ValidatorSelectionLib.getProposerAt(_slot); + function getProposerAt(Slot _slot) external returns (address proposer) { + (proposer,) = ValidatorSelectionLib.getProposerAt(_slot); } function getCommitteeCommitmentAt(Epoch _epoch) external returns (bytes32, uint256) { diff --git a/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol new file mode 100644 index 000000000000..7162e90232a5 --- /dev/null +++ b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import { + IRollupCore, RollupStore, BlockHeaderValidationFlags +} from "@aztec/core/interfaces/IRollup.sol"; +import {TempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; +import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; +import {ValidatorSelectionLib} from "@aztec/core/libraries/rollup/ValidatorSelectionLib.sol"; +import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +import { + CommitteeAttestations, SignatureLib, Signature +} from "@aztec/shared/libraries/SignatureLib.sol"; +import {ECDSA} from "@oz/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; + +library InvalidateLib { + using TimeLib for Timestamp; + using TimeLib for Slot; + using TimeLib for Epoch; + using ChainTipsLib for CompressedChainTips; + using SignatureLib for CommitteeAttestations; + using MessageHashUtils for bytes32; + + /** + * @notice Invalidates a block with a bad attestation signature + * @dev Anyone can call this function to remove blocks with invalid attestations + * @dev No rebate is provided for calling this function + * @param _blockNumber The block number to invalidate + * @param _attestations The attestations that are claimed to be invalid + * @param _committee The committee members for the epoch + * @param _invalidIndex The index of the invalid attestation in the committee + */ + function invalidateBadAttestation( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee, + uint256 _invalidIndex + ) internal { + (bytes32 digest,) = _validateInvalidationInputs(_blockNumber, _attestations, _committee); + + // Verify that the attestation at invalidIndex is actually invalid + // Check if there's a signature at the invalid index + if (_attestations.isSignature(_invalidIndex)) { + // Extract and verify the signature + Signature memory signature = _attestations.getSignature(_invalidIndex); + address recovered = ECDSA.recover(digest, signature.v, signature.r, signature.s); + + // The signature is invalid if the recovered address doesn't match the committee member + require(recovered != _committee[_invalidIndex], Errors.Rollup__AttestationsAreValid()); + } else { + // If it's an address attestation, we need to ensure it's marked as invalid + // This would typically mean the address doesn't match what's expected + // For now, we'll revert as address attestations are assumed valid + revert Errors.Rollup__AttestationsAreValid(); + } + + // Reset the pending block number to remove this block and all subsequent blocks + RollupStore storage rollupStore = STFLib.getStorage(); + rollupStore.tips = rollupStore.tips.updatePendingBlockNumber(_blockNumber - 1); + + _invalidateBlock(_blockNumber); + } + + /** + * @notice Invalidates a block with insufficient attestations + * @dev Anyone can call this function to remove blocks with insufficient attestations + * @dev No rebate is provided for calling this function + * @param _blockNumber The block number to invalidate + * @param _attestations The attestations that are claimed to be insufficient + * @param _committee The committee members for the epoch + */ + function invalidateInsufficientAttestations( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee + ) internal { + (, uint256 committeeSize) = _validateInvalidationInputs(_blockNumber, _attestations, _committee); + + uint256 signatureCount = 0; + for (uint256 i = 0; i < committeeSize; ++i) { + if (_attestations.isSignature(i)) { + signatureCount++; + } + } + + // Calculate required threshold (2/3 + 1) + uint256 requiredSignatures = (committeeSize << 1) / 3 + 1; + + // Ensure the number of valid signatures is actually insufficient + require( + signatureCount < requiredSignatures, + Errors.ValidatorSelection__InsufficientAttestations(requiredSignatures, signatureCount) + ); + + _invalidateBlock(_blockNumber); + } + + /** + * @notice Common validation logic for invalidation functions. Verifies that the block is in the pending chain, + * that the attestations match the stored hash, and that the committee commitment is valid. + * @param _blockNumber The block number to validate + * @param _attestations The attestations to validate + * @param _committee The committee members for the epoch + * @return digest Digest of the payload that was signed by the committee + * @return committeeSize The size of the committee + */ + function _validateInvalidationInputs( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee + ) private returns (bytes32, uint256) { + RollupStore storage rollupStore = STFLib.getStorage(); + + // Block must be in the pending chain + require( + _blockNumber <= rollupStore.tips.getPendingBlockNumber(), + Errors.Rollup__BlockNotInPendingChain() + ); + + // But not yet proven + require( + _blockNumber > rollupStore.tips.getProvenBlockNumber(), Errors.Rollup__BlockAlreadyProven() + ); + + // Get the stored block data + TempBlockLog memory blockLog = STFLib.getTempBlockLog(_blockNumber); + + // Verify that the provided attestations match the stored hash + bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations)); + require( + providedAttestationsHash == blockLog.attestationsHash, Errors.Rollup__InvalidAttestations() + ); + + // Get the epoch for the block's slot to verify committee + Epoch epoch = blockLog.slotNumber.epochFromSlot(); + + // Get and verify the committee commitment + (bytes32 committeeCommitment, uint256 committeeSize) = + ValidatorSelectionLib.getCommitteeCommitmentAt(epoch); + bytes32 providedCommitteeCommitment = keccak256(abi.encode(_committee)); + require( + committeeCommitment == providedCommitteeCommitment, + Errors.ValidatorSelection__InvalidCommitteeCommitment( + providedCommitteeCommitment, committeeCommitment + ) + ); + + // Get the digest of the payload that was signed by the committee + bytes32 digest = blockLog.payloadDigest.toEthSignedMessageHash(); + + return (digest, committeeSize); + } + + /** + * @notice Invalidates a block by resetting the pending block number to the one immediately before it. + * @param _blockNumber The block number to invalidate + */ + function _invalidateBlock(uint256 _blockNumber) private { + RollupStore storage rollupStore = STFLib.getStorage(); + rollupStore.tips = rollupStore.tips.updatePendingBlockNumber(_blockNumber - 1); + emit IRollupCore.BlockInvalidated(_blockNumber); + } +} diff --git a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol index ffc51660112c..9bf573aec6b4 100644 --- a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol @@ -44,11 +44,12 @@ struct InterimProposeValues { bytes[] blobCommitments; bytes32 inHash; bytes32 headerHash; + bytes32 attestationsHash; + bytes32 payloadDigest; } /** * @param header - The proposed block header - * @param attestations - The signatures for the attestations * @param digest - The digest that signatures signed * @param currentTime - The time of execution * @param blobsHashesCommitment - The blobs hash for this block, provided for simpler future simulation @@ -56,7 +57,6 @@ struct InterimProposeValues { */ struct ValidateHeaderArgs { ProposedHeader header; - CommitteeAttestations attestations; bytes32 digest; uint256 manaBaseFee; bytes32 blobsHashesCommitment; @@ -109,24 +109,27 @@ library ProposeLib { ManaBaseFeeComponents memory components = getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true); + v.payloadDigest = digest( + ProposePayload({ + archive: _args.archive, + stateReference: _args.stateReference, + oracleInput: _args.oracleInput, + headerHash: v.headerHash + }) + ); + validateHeader( ValidateHeaderArgs({ header: header, - attestations: _attestations, - digest: digest( - ProposePayload({ - archive: _args.archive, - stateReference: _args.stateReference, - oracleInput: _args.oracleInput, - headerHash: v.headerHash - }) - ), + digest: v.payloadDigest, manaBaseFee: FeeLib.summedBaseFee(components), blobsHashesCommitment: v.blobsHashesCommitment, - flags: BlockHeaderValidationFlags({ignoreDA: false, ignoreSignatures: false}) + flags: BlockHeaderValidationFlags({ignoreDA: false}) }) ); + ValidatorSelectionLib.verifyProposer(header.slotNumber, _attestations, v.payloadDigest); + RollupStore storage rollupStore = STFLib.getStorage(); uint256 blockNumber = rollupStore.tips.getPendingBlockNumber() + 1; @@ -145,6 +148,9 @@ library ProposeLib { components.proverCost ); + // Compute attestationsHash from the attestations + v.attestationsHash = keccak256(abi.encode(_attestations)); + rollupStore.tips = rollupStore.tips.updatePendingBlockNumber(blockNumber); rollupStore.archives[blockNumber] = _args.archive; STFLib.setTempBlockLog( @@ -152,6 +158,8 @@ library ProposeLib { TempBlockLog({ headerHash: v.headerHash, blobCommitmentsHash: blobCommitmentsHash, + attestationsHash: v.attestationsHash, + payloadDigest: v.payloadDigest, slotNumber: header.slotNumber, feeHeader: feeHeader }) @@ -211,10 +219,6 @@ library ProposeLib { _args.header.gasFees.feePerL2Gas == _args.manaBaseFee, Errors.Rollup__InvalidManaBaseFee(_args.manaBaseFee, _args.header.gasFees.feePerL2Gas) ); - - ValidatorSelectionLib.verify( - slot, slot.epochFromSlot(), _args.attestations, _args.digest, _args.flags - ); } /** diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index f718ce26c0b7..c9b886c42317 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -81,10 +81,39 @@ library ValidatorSelectionLib { } } + /** + * Verifies the proposer for a given slot and epoch for a block proposal. + * Checks if the proposer has either signed their attestation or is the sender of the transaction. + * + * @param _slot - The slot of the block being proposed + * @param _attestations - The committee attestations for the block proposal + * @param _digest - The digest of the block being proposed + */ + function verifyProposer(Slot _slot, CommitteeAttestations memory _attestations, bytes32 _digest) + internal + { + // If there is no committee for the epoch, or the proposer is who sent the tx, we're good + (address proposer, uint256 proposerIndex) = getProposerAt(_slot); + if (proposer == address(0) || proposer == msg.sender) { + return; + } + + // Check if the proposer has signed, if not, fail + bool hasProposerSignature = _attestations.isSignature(proposerIndex); + if (!hasProposerSignature) { + revert Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender); + } + + // Check if the signature is correct + bytes32 digest = _digest.toEthSignedMessageHash(); + Signature memory signature = _attestations.getSignature(proposerIndex); + SignatureLib.verify(signature, proposer, digest); + } + /** * @notice Propose a pending block from the point-of-view of sequencer selection. Will: * - Setup the epoch if needed (if epoch committee is empty skips the rest) - * - Validate that the proposer is the proposer of the slot + * - Validate that the proposer has signed with its own key * - Validate that the signatures for attestations are indeed from the validatorset * - Validate that the number of valid attestations is sufficient * @@ -101,8 +130,7 @@ library ValidatorSelectionLib { Slot _slot, Epoch _epochNumber, CommitteeAttestations memory _attestations, - bytes32 _digest, - BlockHeaderValidationFlags memory _flags + bytes32 _digest ) internal { (bytes32 committeeCommitment, uint256 targetCommitteeSize) = getCommitteeCommitmentAt(_epochNumber); @@ -114,10 +142,6 @@ library ValidatorSelectionLib { return; } - if (_flags.ignoreSignatures) { - return; - } - VerifyStack memory stack = VerifyStack({ proposerIndex: computeProposerIndex( _epochNumber, _slot, getSampleSeed(_epochNumber), targetCommitteeSize @@ -175,8 +199,7 @@ library ValidatorSelectionLib { address proposer = stack.reconstructedCommittee[stack.proposerIndex]; require( - stack.proposerVerified || proposer == msg.sender, - Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender) + stack.proposerVerified, Errors.ValidatorSelection__InvalidProposer(proposer, address(0)) ); require( @@ -192,17 +215,23 @@ library ValidatorSelectionLib { ); } - setCachedProposer(_slot, proposer); + setCachedProposer(_slot, proposer, stack.proposerIndex); } - function setCachedProposer(Slot _slot, address _proposer) internal { - PROPOSER_NAMESPACE.erc7201Slot().deriveMapping(Slot.unwrap(_slot)).asAddress().tstore(_proposer); + // Q: Do we still need to cache the proposer? + function setCachedProposer(Slot _slot, address _proposer, uint256 _proposerIndex) internal { + require( + _proposerIndex <= type(uint96).max, + Errors.ValidatorSelection__ProposerIndexTooLarge(_proposerIndex) + ); + bytes32 packed = bytes32(uint256(uint160(_proposer))) | (bytes32(_proposerIndex) << 160); + PROPOSER_NAMESPACE.erc7201Slot().deriveMapping(Slot.unwrap(_slot)).asBytes32().tstore(packed); } - function getProposerAt(Slot _slot) internal returns (address) { - address cachedProposer = getCachedProposer(_slot); + function getProposerAt(Slot _slot) internal returns (address, uint256) { + (address cachedProposer, uint256 proposerIndex) = getCachedProposer(_slot); if (cachedProposer != address(0)) { - return cachedProposer; + return (cachedProposer, proposerIndex); } // @note this is deliberately "bad" for the simple reason of code reduction. @@ -214,10 +243,11 @@ library ValidatorSelectionLib { uint224 sampleSeed = getSampleSeed(epochNumber); address[] memory committee = sampleValidators(epochNumber, sampleSeed); if (committee.length == 0) { - return address(0); + return (address(0), 0); } - return committee[computeProposerIndex(epochNumber, _slot, sampleSeed, committee.length)]; + uint256 index = computeProposerIndex(epochNumber, _slot, sampleSeed, committee.length); + return (committee[index], index); } /** @@ -321,8 +351,17 @@ library ValidatorSelectionLib { } } - function getCachedProposer(Slot _slot) internal view returns (address) { - return PROPOSER_NAMESPACE.erc7201Slot().deriveMapping(Slot.unwrap(_slot)).asAddress().tload(); + function getCachedProposer(Slot _slot) + internal + view + returns (address proposer, uint256 proposerIndex) + { + bytes32 packed = + PROPOSER_NAMESPACE.erc7201Slot().deriveMapping(Slot.unwrap(_slot)).asBytes32().tload(); + // Extract address from lower 160 bits + proposer = address(uint160(uint256(packed))); + // Extract uint96 from upper 96 bits + proposerIndex = uint256(packed >> 160); } function epochToSampleTime(Epoch _epoch) internal view returns (uint32) { diff --git a/l1-contracts/src/shared/libraries/SignatureLib.sol b/l1-contracts/src/shared/libraries/SignatureLib.sol index 096a4c109afd..9ca63018d6dc 100644 --- a/l1-contracts/src/shared/libraries/SignatureLib.sol +++ b/l1-contracts/src/shared/libraries/SignatureLib.sol @@ -37,6 +37,16 @@ struct CommitteeAttestations { error SignatureLib__InvalidSignature(address, address); library SignatureLib { + /** + * @notice Checks if the given CommitteeAttestations is empty + * @param _attestations - The committee attestations + * @return True if the committee attestations are empty, false otherwise + */ + function isEmpty(CommitteeAttestations memory _attestations) internal pure returns (bool) { + return + _attestations.signatureIndices.length == 0 && _attestations.signaturesOrAddresses.length == 0; + } + /** * @notice Checks if the given index in the CommitteeAttestations is a signature * @param _attestations - The committee attestations @@ -60,7 +70,45 @@ library SignatureLib { } /** - * @notice Verified a signature, throws if the signature is invalid or empty + * @notice Gets the signature at the given index + * @param _attestations - The committee attestations + * @param _index - The index of the signature to get + */ + function getSignature(CommitteeAttestations memory _attestations, uint256 _index) + internal + pure + returns (Signature memory) + { + bytes memory signaturesOrAddresses = _attestations.signaturesOrAddresses; + require(isSignature(_attestations, _index), "Not a signature at this index"); + + uint256 dataPtr; + assembly { + // Skip length + dataPtr := add(signaturesOrAddresses, 0x20) + } + + // Move to the start of the signature + for (uint256 i = 0; i < _index; ++i) { + dataPtr += isSignature(_attestations, i) ? 65 : 20; + } + + uint8 v; + bytes32 r; + bytes32 s; + + assembly { + v := byte(0, mload(dataPtr)) + dataPtr := add(dataPtr, 1) + r := mload(dataPtr) + dataPtr := add(dataPtr, 32) + s := mload(dataPtr) + } + return Signature({v: v, r: r, s: s}); + } + + /** + * @notice Verifies a signature, throws if the signature is invalid or empty * * @param _signature - The signature to verify * @param _signer - The expected signer of the signature diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 68711bbd8020..d4ea89ab66cf 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -37,7 +37,7 @@ import {RollupBase, IInstance} from "./base/RollupBase.sol"; import {stdStorage, StdStorage} from "forge-std/StdStorage.sol"; import {RollupBuilder} from "./builder/RollupBuilder.sol"; import {Ownable} from "@oz/access/Ownable.sol"; -import {SignatureLib} from "@aztec/shared/libraries/SignatureLib.sol"; +import {SignatureLib, CommitteeAttestations} from "@aztec/shared/libraries/SignatureLib.sol"; // solhint-disable comprehensive-interface /** @@ -643,6 +643,7 @@ contract RollupTest is RollupBase { end: 2, args: args, fees: fees, + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), blobInputs: data.batchedBlobInputs, proof: proof }) @@ -861,6 +862,7 @@ contract RollupTest is RollupBase { end: _end, args: args, fees: fees, + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), blobInputs: _blobInputs, proof: "" }) diff --git a/l1-contracts/test/base/RollupBase.sol b/l1-contracts/test/base/RollupBase.sol index 1e87627c83b3..3c206172af4f 100644 --- a/l1-contracts/test/base/RollupBase.sol +++ b/l1-contracts/test/base/RollupBase.sol @@ -11,6 +11,7 @@ import { PublicInputArgs } from "@aztec/core/interfaces/IRollup.sol"; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; +import {CommitteeAttestations} from "@aztec/shared/libraries/SignatureLib.sol"; import {Strings} from "@oz/utils/Strings.sol"; import {SafeCast} from "@oz/utils/math/SafeCast.sol"; @@ -100,6 +101,7 @@ contract RollupBase is DecoderBase { end: endBlockNumber, args: args, fees: fees, + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), blobInputs: endFull.block.batchedBlobInputs, proof: "" }) diff --git a/l1-contracts/test/benchmark/happy.t.sol b/l1-contracts/test/benchmark/happy.t.sol index ef7cefa4195f..34eb89ef207a 100644 --- a/l1-contracts/test/benchmark/happy.t.sol +++ b/l1-contracts/test/benchmark/happy.t.sol @@ -107,6 +107,7 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { using MessageHashUtils for bytes32; using stdStorage for StdStorage; using TimeLib for Slot; + using TimeLib for Timestamp; using FeeLib for uint256; using FeeLib for ManaBaseFeeComponents; // We need to build a block that we can submit. We will be using some values from @@ -132,6 +133,9 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { CommitteeAttestation internal emptyAttestation; mapping(address attester => uint256 privateKey) internal attesterPrivateKeys; + + // Track attestations by block number for proof submission + mapping(uint256 => CommitteeAttestations) internal blockAttestations; Multicall3 internal multicall = new Multicall3(); @@ -367,6 +371,10 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { address proposer = rollup.getCurrentProposer(); skipBlobCheck(address(rollup)); + + // Store the attestations for the current block number + uint256 currentBlockNumber = rollup.getPendingBlockNumber() + 1; + blockAttestations[currentBlockNumber] = SignatureLib.packAttestations(b.attestations); if (_slashing) { Signature memory sig = createSignalSignature(proposer, slashPayload, round); @@ -432,6 +440,7 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { end: start + epochSize - 1, args: args, fees: fees, + attestations: blockAttestations[start + epochSize - 1], blobInputs: full.block.batchedBlobInputs, proof: "" }) diff --git a/l1-contracts/test/compression/PreHeating.t.sol b/l1-contracts/test/compression/PreHeating.t.sol index 25afb022dfa7..72b8466867bc 100644 --- a/l1-contracts/test/compression/PreHeating.t.sol +++ b/l1-contracts/test/compression/PreHeating.t.sol @@ -134,6 +134,9 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { CommitteeAttestation internal emptyAttestation; mapping(address attester => uint256 privateKey) internal attesterPrivateKeys; + + // Track attestations by block number for proof submission + mapping(uint256 => CommitteeAttestations) internal blockAttestations; SlashingProposer internal slashingProposer; IPayload internal slashPayload; @@ -225,6 +228,10 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { skipBlobCheck(address(rollup)); + // Store the attestations for the current block number + uint256 currentBlockNumber = rollup.getPendingBlockNumber() + 1; + blockAttestations[currentBlockNumber] = SignatureLib.packAttestations(b.attestations); + vm.prank(proposer); rollup.propose(b.proposeArgs, SignatureLib.packAttestations(b.attestations), b.blobInputs); @@ -269,6 +276,7 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { end: start + epochSize - 1, args: args, fees: fees, + attestations: blockAttestations[start + epochSize - 1], blobInputs: full.block.batchedBlobInputs, proof: "" }) diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index 141e0704c688..4c0b6998a3b8 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -392,6 +392,7 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { end: start + epochSize - 1, args: args, fees: fees, + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), blobInputs: full.block.batchedBlobInputs, proof: "" }) diff --git a/l1-contracts/test/fees/MinimalFeeModel.sol b/l1-contracts/test/fees/MinimalFeeModel.sol index 13f69049a268..63a2701533e7 100644 --- a/l1-contracts/test/fees/MinimalFeeModel.sol +++ b/l1-contracts/test/fees/MinimalFeeModel.sol @@ -120,6 +120,8 @@ contract MinimalFeeModel { TempBlockLog({ headerHash: bytes32(0), blobCommitmentsHash: bytes32(0), + attestationsHash: bytes32(0), + payloadDigest: bytes32(0), slotNumber: Slot.wrap(0), feeHeader: FeeLib.computeFeeHeader( blockNumber, _oracleInput.feeAssetPriceModifier, _manaUsed, 0, 0 From 2030004bca55f1938d190dd10f7b85a699ee5a68 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 18 Jul 2025 09:52:24 -0300 Subject: [PATCH 02/22] Address comments from @lherskind --- .../src/core/libraries/rollup/EpochProofLib.sol | 5 +---- .../src/core/libraries/rollup/InvalidateLib.sol | 14 +++++++++----- l1-contracts/src/core/libraries/rollup/STFLib.sol | 9 +++++++++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol index fafd748a2a37..2953ca48a8de 100644 --- a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol +++ b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol @@ -317,9 +317,6 @@ library EpochProofLib { Slot slot = blockLog.slotNumber; Epoch epoch = STFLib.getEpochForBlock(_endBlockNumber); - // Only verify attestations if they are not empty (for testing compatibility) - if (!_attestations.isEmpty()) { - ValidatorSelectionLib.verify(slot, epoch, _attestations, blockLog.payloadDigest); - } + ValidatorSelectionLib.verify(slot, epoch, _attestations, blockLog.payloadDigest); } } diff --git a/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol index 7162e90232a5..64e7532d7751 100644 --- a/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol +++ b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol @@ -5,7 +5,9 @@ pragma solidity >=0.8.27; import { IRollupCore, RollupStore, BlockHeaderValidationFlags } from "@aztec/core/interfaces/IRollup.sol"; -import {TempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; +import { + TempBlockLog, CompressedTempBlockLog +} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; @@ -16,6 +18,7 @@ import { } from "@aztec/shared/libraries/SignatureLib.sol"; import {ECDSA} from "@oz/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; +import {CompressedSlot, CompressedTimeMath} from "@aztec/shared/libraries/CompressedTimeMath.sol"; library InvalidateLib { using TimeLib for Timestamp; @@ -24,6 +27,7 @@ library InvalidateLib { using ChainTipsLib for CompressedChainTips; using SignatureLib for CommitteeAttestations; using MessageHashUtils for bytes32; + using CompressedTimeMath for CompressedSlot; /** * @notice Invalidates a block with a bad attestation signature @@ -45,7 +49,7 @@ library InvalidateLib { // Verify that the attestation at invalidIndex is actually invalid // Check if there's a signature at the invalid index if (_attestations.isSignature(_invalidIndex)) { - // Extract and verify the signature + // Extract the signature and verify it does NOT match the expected committee member at the given index Signature memory signature = _attestations.getSignature(_invalidIndex); address recovered = ECDSA.recover(digest, signature.v, signature.r, signature.s); @@ -88,7 +92,7 @@ library InvalidateLib { } // Calculate required threshold (2/3 + 1) - uint256 requiredSignatures = (committeeSize << 1) / 3 + 1; + uint256 requiredSignatures = (committeeSize << 1) / 3 + 1; // committeeSize * 2 / 3 + 1 // Ensure the number of valid signatures is actually insufficient require( @@ -127,7 +131,7 @@ library InvalidateLib { ); // Get the stored block data - TempBlockLog memory blockLog = STFLib.getTempBlockLog(_blockNumber); + CompressedTempBlockLog storage blockLog = STFLib.getStorageTempBlockLog(_blockNumber); // Verify that the provided attestations match the stored hash bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations)); @@ -136,7 +140,7 @@ library InvalidateLib { ); // Get the epoch for the block's slot to verify committee - Epoch epoch = blockLog.slotNumber.epochFromSlot(); + Epoch epoch = blockLog.slotNumber.decompress().epochFromSlot(); // Get and verify the committee commitment (bytes32 committeeCommitment, uint256 committeeSize) = diff --git a/l1-contracts/src/core/libraries/rollup/STFLib.sol b/l1-contracts/src/core/libraries/rollup/STFLib.sol index 0d45f03f364c..974b586fc191 100644 --- a/l1-contracts/src/core/libraries/rollup/STFLib.sol +++ b/l1-contracts/src/core/libraries/rollup/STFLib.sol @@ -121,6 +121,15 @@ library STFLib { return getStorage().tempBlockLogs[_blockNumber % size].decompress(); } + function getStorageTempBlockLog(uint256 _blockNumber) + internal + view + returns (CompressedTempBlockLog storage) + { + (, uint256 size) = innerIsStale(_blockNumber, true); + return getStorage().tempBlockLogs[_blockNumber % size]; + } + function getHeaderHash(uint256 _blockNumber) internal view returns (bytes32) { (, uint256 size) = innerIsStale(_blockNumber, true); return getStorage().tempBlockLogs[_blockNumber % size].headerHash; From a08448541c23aa805bc41004a3cef08f590e298b Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 18 Jul 2025 16:59:38 -0300 Subject: [PATCH 03/22] Preheating and unit tests --- .../src/core/libraries/rollup/STFLib.sol | 8 + l1-contracts/test/Rollup.t.sol | 2 +- .../ValidatorSelection.t.sol | 378 ++++++++++-------- .../ValidatorSelectionBase.sol | 12 +- 4 files changed, 222 insertions(+), 178 deletions(-) diff --git a/l1-contracts/src/core/libraries/rollup/STFLib.sol b/l1-contracts/src/core/libraries/rollup/STFLib.sol index 974b586fc191..a76d501c899e 100644 --- a/l1-contracts/src/core/libraries/rollup/STFLib.sol +++ b/l1-contracts/src/core/libraries/rollup/STFLib.sol @@ -70,6 +70,14 @@ library STFLib { blockLog.blobCommitmentsHash = bytes32(uint256(0x1)); } + if (blockLog.attestationsHash == bytes32(0)) { + blockLog.attestationsHash = bytes32(uint256(0x1)); + } + + if (blockLog.payloadDigest == bytes32(0)) { + blockLog.payloadDigest = bytes32(uint256(0x1)); + } + store.tempBlockLogs[i] = blockLog.compress(); } } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index d4ea89ab66cf..0deb565bc7b2 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -862,7 +862,7 @@ contract RollupTest is RollupBase { end: _end, args: args, fees: fees, - attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), // TODO(palla): Add unit tests with non-empty attestations blobInputs: _blobInputs, proof: "" }) diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 941294ad227f..841fb55712d8 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -5,7 +5,10 @@ pragma solidity >=0.8.27; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; import { - Signature, CommitteeAttestation, SignatureLib + Signature, + CommitteeAttestation, + CommitteeAttestations, + SignatureLib } from "@aztec/shared/libraries/SignatureLib.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; @@ -45,19 +48,25 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { // Test Block Flags struct TestFlags { - bool provideEmptyAttestations; - bool invalidProposer; - bool proposerNotProvided; - bool invalidCommitteeCommitment; + bool senderIsNotProposer; + bool proposerAttestationNotProvided; + bool invalidAttestation; } TestFlags NO_FLAGS = TestFlags({ - provideEmptyAttestations: true, - invalidProposer: false, - proposerNotProvided: false, - invalidCommitteeCommitment: false + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: false }); + TestFlags INVALID_ATTESTATION = TestFlags({ + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: true + }); + + bytes4 NO_REVERT = bytes4(0); + function testInitialCommitteeMatch() public setup(4, 4) progressEpochs(2) { address[] memory attesters = rollup.getAttesters(); address[] memory committee = rollup.getCurrentEpochCommittee(); @@ -169,31 +178,43 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { setup(100, 48) progressEpochs(2) { - assertGt(rollup.getAttesters().length, rollup.getTargetCommitteeSize(), "Not enough validators"); - uint256 committeeSize = rollup.getTargetCommitteeSize() * 2 / 3 + (_insufficientSigs ? 0 : 1); + uint256 committeeSize = rollup.getTargetCommitteeSize(); + uint256 signatureCount = committeeSize * 2 / 3 + (_insufficientSigs ? 0 : 1); + assertGt(rollup.getAttesters().length, committeeSize, "Not enough validators"); - _testBlock( + ProposeTestData memory ree = _testBlock( "mixed_block_1", - _insufficientSigs, + NO_REVERT, + signatureCount, committeeSize, TestFlags({ - provideEmptyAttestations: true, - invalidProposer: false, - proposerNotProvided: false, - invalidCommitteeCommitment: false + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: false }) ); - assertEq( - rollup.getEpochCommittee(rollup.getCurrentEpoch()).length, - rollup.getTargetCommitteeSize(), - "Invalid committee size" + assertEq(ree.committee.length, rollup.getTargetCommitteeSize(), "Invalid committee size"); + + // Test we can invalidate the block by insufficient attestations if sigs were insufficient + _invalidateByAttestationCount( + ree, + _insufficientSigs ? NO_REVERT : Errors.ValidatorSelection__InsufficientAttestations.selector ); } function testHappyPath() public setup(4, 4) progressEpochs(2) { - _testBlock("mixed_block_1", false, 3, NO_FLAGS); - _testBlock("mixed_block_2", false, 3, NO_FLAGS); + _testBlock("mixed_block_1", NO_REVERT, 3, 4, NO_FLAGS); + _testBlock("mixed_block_2", NO_REVERT, 3, 4, NO_FLAGS); + } + + function testCannotInvalidateProperProposal() public setup(4, 4) progressEpochs(2) { + ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 3, 4, NO_FLAGS); + _invalidateByAttestationCount(ree, Errors.ValidatorSelection__InsufficientAttestations.selector); + + for (uint256 i = 0; i < ree.attestations.length; i++) { + _invalidateByAttestationSig(ree, i, Errors.Rollup__AttestationsAreValid.selector); + } } function testNukeFromOrbit() public setup(4, 4) progressEpochs(2) { @@ -202,8 +223,8 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { // got finalised. // This is LIKELY, not the action you really want to take, you want to slash // the people actually attesting, etc, but for simplicity we can do this as showcase. - _testBlock("mixed_block_1", false, 3, NO_FLAGS); - _testBlock("mixed_block_2", false, 3, NO_FLAGS); + _testBlock("mixed_block_1", NO_REVERT, 3, 4, NO_FLAGS); + _testBlock("mixed_block_2", NO_REVERT, 3, 4, NO_FLAGS); address[] memory attesters = rollup.getAttesters(); uint256[] memory stakes = new uint256[](attesters.length); @@ -233,50 +254,91 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { } } - function testRelayedForProposer() public setup(4, 4) progressEpochs(2) { + function testProposerAttested() public setup(4, 4) progressEpochs(2) { // Having someone that is not the proposer submit it, but with all signatures (so there is signature from proposer) _testBlock( "mixed_block_1", - false, + NO_REVERT, + 4, 4, TestFlags({ - invalidProposer: true, - provideEmptyAttestations: false, - proposerNotProvided: false, - invalidCommitteeCommitment: false + senderIsNotProposer: true, + proposerAttestationNotProvided: false, + invalidAttestation: false }) ); } - function testProposerNotProvided() public setup(4, 4) progressEpochs(2) { + function testProposerAttestationNotProvided() public setup(4, 4) progressEpochs(2) { _testBlock( "mixed_block_1", - true, + Errors.ValidatorSelection__InvalidProposer.selector, 3, + 4, TestFlags({ - invalidProposer: false, - provideEmptyAttestations: true, - proposerNotProvided: true, - invalidCommitteeCommitment: false + senderIsNotProposer: true, + proposerAttestationNotProvided: true, + invalidAttestation: false }) ); } - function testInvalidCommitteeCommitment() public setup(4, 4) progressEpochs(2) { - _testBlock( - "mixed_block_1", - true, - 3, - TestFlags({ - invalidProposer: false, - provideEmptyAttestations: true, - proposerNotProvided: false, - invalidCommitteeCommitment: true - }) + function testInvalidAttestation() public setup(4, 4) progressEpochs(2) { + ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 3, 4, INVALID_ATTESTATION); + + // the invalid attestation is the first one + _invalidateByAttestationSig(ree, 1, Errors.Rollup__AttestationsAreValid.selector); + _invalidateByAttestationSig(ree, 0, NO_REVERT); + } + + function testInsufficientAttestations() public setup(4, 4) progressEpochs(2) { + ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 2, 2, NO_FLAGS); + + _invalidateByAttestationCount(ree, NO_REVERT); + } + + function testInsufficientSignatures() public setup(4, 4) progressEpochs(2) { + ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 2, 4, NO_FLAGS); + + _invalidateByAttestationCount(ree, NO_REVERT); + } + + function testInvalidateMultipleBlocks() public setup(4, 4) progressEpochs(2) { + uint256 initialBlockNumber = rollup.getPendingBlockNumber(); + ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 3, 4, INVALID_ATTESTATION); + _testBlock("mixed_block_2", NO_REVERT, 3, 4, NO_FLAGS); + + _invalidateByAttestationSig(ree, 0, NO_REVERT, initialBlockNumber + 1); + } + + function testProposeBlockAfterInvalidate() public setup(4, 4) progressEpochs(2) { + uint256 initialBlockNumber = rollup.getPendingBlockNumber(); + ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 3, 4, INVALID_ATTESTATION); + _invalidateByAttestationSig(ree, 0, NO_REVERT); + + _testBlock("mixed_block_1", NO_REVERT, 3, 4, NO_FLAGS); + assertEq( + rollup.getPendingBlockNumber(), + initialBlockNumber + 1, + "Failed to propose block after invalidate" ); } - function testInsufficientSigsMove() public setup(4, 4) progressEpochs(2) { + function testCannotProposeIfAllValidatorsHaveMoved() public setup(4, 4) progressEpochs(2) { + // Buried in the RollupBuilder, we add initial validators using l1-contracts/src/mock/MultiAdder.sol + // In that, you see this inconspicuous true at the end of the call to deposit. This means that the + // validators are going into the "bonus" instance, are are thus not tied directly to the rollup from + // the perspective of the GSE. + // The "bonus" instance (which is tracked by the GSE) is only available to the latest rollup in the GSE. + // So when we add a new 0xdead rollup, all the validators we added get moved over to that one, + // and our original rollup has no validators. + // So this is showing that in that case, even if all your validators move over, you still cannot build + // a block if you submit one with no signatures. This was a change from prior behavior where we had had + // that if there were zero validators in a rollup, anyone could build a block + + // TODO(palla): What should we do in this scenario? Block the proposal, or allow invalidating later? + vm.skip(true); + GSE gse = rollup.getGSE(); address caller = gse.owner(); vm.prank(caller); @@ -284,28 +346,68 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { assertEq(rollup.getCurrentEpochCommittee().length, 4); _testBlock( "mixed_block_1", - true, + NO_REVERT, + 0, 0, TestFlags({ - provideEmptyAttestations: false, - invalidProposer: false, - proposerNotProvided: false, - invalidCommitteeCommitment: false + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: false }) ); } + function _invalidateByAttestationCount(ProposeTestData memory ree, bytes4 _revertData) internal { + uint256 blockNumber = rollup.getPendingBlockNumber(); + CommitteeAttestations memory attestations = SignatureLib.packAttestations(ree.attestations); + if (_revertData != NO_REVERT) { + vm.expectPartialRevert(_revertData); + } + rollup.invalidateInsufficientAttestations(blockNumber, attestations, ree.committee); + assertEq( + rollup.getPendingBlockNumber(), + _revertData == NO_REVERT ? blockNumber - 1 : blockNumber, + "Block was not invalidated" + ); + } + + function _invalidateByAttestationSig( + ProposeTestData memory ree, + uint256 _index, + bytes4 _revertData + ) internal { + _invalidateByAttestationSig(ree, _index, _revertData, rollup.getPendingBlockNumber()); + } + + function _invalidateByAttestationSig( + ProposeTestData memory ree, + uint256 _index, + bytes4 _revertData, + uint256 _blockToInvalidate + ) internal { + uint256 blockNumber = rollup.getPendingBlockNumber(); + CommitteeAttestations memory attestations = SignatureLib.packAttestations(ree.attestations); + if (_revertData != NO_REVERT) { + vm.expectPartialRevert(_revertData); + } + rollup.invalidateBadAttestation(_blockToInvalidate, attestations, ree.committee, _index); + assertEq( + rollup.getPendingBlockNumber(), + _revertData == NO_REVERT ? _blockToInvalidate - 1 : blockNumber, + "Block was not invalidated" + ); + } + function _testBlock( string memory _name, - bool _expectRevert, + bytes4 _revertData, uint256 _signatureCount, + uint256 _attestationCount, TestFlags memory _flags - ) internal { + ) internal returns (ProposeTestData memory ree) { DecoderBase.Full memory full = load(_name); ProposedHeader memory header = full.block.header; - StructToAvoidDeepStacks memory ree; - // We jump to the time of the block. (unless it is in the past) vm.warp(max(block.timestamp, Timestamp.unwrap(full.block.header.timestamp))); @@ -314,7 +416,8 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { rollup.setupEpoch(); ree.proposer = rollup.getCurrentProposer(); - ree.shouldRevert = false; + ree.committee = rollup.getEpochCommittee(rollup.getCurrentEpoch()); + ree.sender = ree.proposer; { uint128 manaBaseFee = @@ -324,7 +427,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { header.gasFees.feePerL2Gas = manaBaseFee; } - ProposeArgs memory args = ProposeArgs({ + ree.proposeArgs = ProposeArgs({ header: header, archive: full.block.archive, stateReference: EMPTY_STATE_REFERENCE, @@ -333,128 +436,59 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { skipBlobCheck(address(rollup)); - if (_signatureCount > 0 && ree.proposer != address(0)) { - address[] memory validators = rollup.getEpochCommittee(rollup.getCurrentEpoch()); - ree.needed = validators.length * 2 / 3 + 1; - - // Pad out with empty (missing signature) attestations to make the committee commitment match - ree.provideEmptyAttestations = _flags.provideEmptyAttestations || !_expectRevert; - ree.attestationsCount = ree.provideEmptyAttestations ? validators.length : _signatureCount; + { + ree.needed = ree.committee.length * 2 / 3 + 1; + ree.attestationsCount = _attestationCount; ree.proposePayload = ProposePayload({ - archive: args.archive, - stateReference: args.stateReference, - oracleInput: args.oracleInput, + archive: ree.proposeArgs.archive, + stateReference: ree.proposeArgs.stateReference, + oracleInput: ree.proposeArgs.oracleInput, headerHash: ProposedHeaderLib.hash(header) }); + } - CommitteeAttestation[] memory attestations = new CommitteeAttestation[](ree.attestationsCount); - - bytes32 digest = ProposeLib.digest(ree.proposePayload); - for (uint256 i = 0; i < _signatureCount; i++) { - attestations[i] = createAttestation(validators[i], digest); - } - - // We must include empty attestations to make the committee commitment match - if (ree.provideEmptyAttestations) { - for (uint256 i = _signatureCount; i < validators.length; i++) { - attestations[i] = createEmptyAttestation(validators[i]); - } - } - - if (_expectRevert) { - ree.shouldRevert = true; - if (_signatureCount < ree.needed) { - vm.expectRevert( - abi.encodeWithSelector( - Errors.ValidatorSelection__InsufficientAttestations.selector, - ree.needed, - _signatureCount - ) - ); - } - } - - if (_expectRevert && _flags.invalidProposer) { - address realProposer = ree.proposer; - ree.proposer = address(uint160(uint256(keccak256(abi.encode("invalid", ree.proposer))))); - vm.expectRevert( - abi.encodeWithSelector( - Errors.ValidatorSelection__InvalidProposer.selector, realProposer, ree.proposer - ) - ); - ree.shouldRevert = true; - } + ree.attestations = new CommitteeAttestation[](ree.attestationsCount); + bytes32 digest = ProposeLib.digest(ree.proposePayload); - // Set all attestations, including the propser's addr to 0 - if (_flags.proposerNotProvided) { - bytes32 correctCommitteeCommitment = keccak256(abi.encode(validators)); - address[] memory incorrectCommittee = new address[](validators.length); - uint256 invalidAttesterKey = uint256(keccak256(abi.encode("invalid", block.timestamp))); - address invalidAttester = vm.addr(invalidAttesterKey); - attesterPrivateKeys[invalidAttester] = invalidAttesterKey; - for (uint256 i = 0; i < attestations.length; ++i) { - attestations[i] = createAttestation(invalidAttester, digest); - incorrectCommittee[i] = attestations[i].addr; + { + uint256 signaturesCollected = 0; + for (uint256 i = 0; i < ree.attestationsCount; i++) { + if ( + (signaturesCollected >= _signatureCount) + || (ree.committee[i] == ree.proposer && _flags.proposerAttestationNotProvided) + ) { + ree.attestations[i] = _createEmptyAttestation(ree.proposer); + } else { + signaturesCollected++; + ree.attestations[i] = _createAttestation(ree.committee[i], digest); } - bytes32 incorrectCommitteeCommitment = keccak256(abi.encode(incorrectCommittee)); - - vm.expectRevert( - abi.encodeWithSelector( - Errors.ValidatorSelection__InvalidCommitteeCommitment.selector, - incorrectCommitteeCommitment, - correctCommitteeCommitment - ) - ); } + } - if (_flags.invalidCommitteeCommitment) { - bytes32 correctCommitteeCommitment = keccak256(abi.encode(validators)); - - // Change the last element in the committee to a random address - address[] memory incorrectCommittee = validators; - uint256 invalidAttesterKey = uint256(keccak256(abi.encode("invalid", block.timestamp))); - address invalidAttester = vm.addr(invalidAttesterKey); - attesterPrivateKeys[invalidAttester] = invalidAttesterKey; - - incorrectCommittee[validators.length - 2] = invalidAttester; - attestations[validators.length - 2] = createAttestation(invalidAttester, digest); - - bytes32 incorrectCommitteeCommitment = keccak256(abi.encode(incorrectCommittee)); - - vm.expectRevert( - abi.encodeWithSelector( - Errors.ValidatorSelection__InvalidCommitteeCommitment.selector, - incorrectCommitteeCommitment, - correctCommitteeCommitment - ) - ); - } + if (_flags.senderIsNotProposer) { + ree.sender = address(uint160(uint256(keccak256(abi.encode("invalid", ree.proposer))))); + } - emit log("Time to propose"); - vm.prank(ree.proposer); - rollup.propose(args, SignatureLib.packAttestations(attestations), full.block.blobCommitments); + if (_flags.invalidAttestation) { + // Change the fist element in the committee to a random address + uint256 invalidAttesterKey = uint256(keccak256(abi.encode("invalid", block.timestamp))); + address invalidAttester = vm.addr(invalidAttesterKey); + attesterPrivateKeys[invalidAttester] = invalidAttesterKey; + ree.attestations[0] = _createAttestation(invalidAttester, digest); + } - if (ree.shouldRevert) { - return; - } - } else { - CommitteeAttestation[] memory attestations = new CommitteeAttestation[](0); - if (_expectRevert) { - vm.expectRevert( - abi.encodeWithSelector( - 0x4e487b71, // Panic(uint256) selector - 0x32 // Array out-of-bounds access panic code - ) - ); - ree.shouldRevert = true; - } - rollup.propose(args, SignatureLib.packAttestations(attestations), full.block.blobCommitments); + emit log("Time to propose"); + if (_revertData != NO_REVERT) { + vm.expectPartialRevert(_revertData); } - assertEq(_expectRevert, ree.shouldRevert, "Does not match revert expectation"); + vm.prank(ree.sender); + rollup.propose( + ree.proposeArgs, SignatureLib.packAttestations(ree.attestations), full.block.blobCommitments + ); - if (ree.shouldRevert) { - return; + if (_revertData != NO_REVERT) { + return ree; } bytes32 l2ToL1MessageTreeRoot; @@ -495,7 +529,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { assertEq(root, bytes32(0), "Invalid outbox root"); } - assertEq(rollup.archive(), args.archive, "Invalid archive"); + assertEq(rollup.archive(), ree.proposeArgs.archive, "Invalid archive"); } function _populateInbox(address _sender, bytes32 _recipient, bytes32[] memory _contents) internal { @@ -508,7 +542,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { } } - function createAttestation(address _signer, bytes32 _digest) + function _createAttestation(address _signer, bytes32 _digest) internal view returns (CommitteeAttestation memory) @@ -522,7 +556,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { return CommitteeAttestation({addr: _signer, signature: signature}); } - function createEmptyAttestation(address _signer) + function _createEmptyAttestation(address _signer) internal pure returns (CommitteeAttestation memory) diff --git a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol index 201b7639d6b3..a4df85447d9f 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.27; import {DecoderBase} from "../base/DecoderBase.sol"; -import {Signature} from "@aztec/shared/libraries/SignatureLib.sol"; +import {Signature, CommitteeAttestation} from "@aztec/shared/libraries/SignatureLib.sol"; import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; @@ -20,7 +20,7 @@ import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; import {SlashFactory} from "@aztec/periphery/SlashFactory.sol"; import {Slasher} from "@aztec/core/slashing/Slasher.sol"; import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; -import {ProposePayload} from "@aztec/core/libraries/rollup/ProposeLib.sol"; +import {ProposePayload, ProposeArgs} from "@aztec/core/libraries/rollup/ProposeLib.sol"; import {MultiAdder, CheatDepositArgs} from "@aztec/mock/MultiAdder.sol"; import {RollupBuilder} from "../builder/RollupBuilder.sol"; import {Slot} from "@aztec/core/libraries/TimeLib.sol"; @@ -38,13 +38,15 @@ contract ValidatorSelectionTestBase is DecoderBase { using MessageHashUtils for bytes32; using stdStorage for StdStorage; - struct StructToAvoidDeepStacks { + struct ProposeTestData { uint256 needed; address proposer; - bool shouldRevert; - bool provideEmptyAttestations; + address sender; uint256 attestationsCount; + address[] committee; + CommitteeAttestation[] attestations; ProposePayload proposePayload; + ProposeArgs proposeArgs; } SlashFactory internal slashFactory; From 56567c396239867e2463e6786b753fee35e9d09a Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 18 Jul 2025 19:12:46 -0300 Subject: [PATCH 04/22] Fix ts-land --- .../end-to-end/src/e2e_p2p/p2p_network.ts | 8 +- .../src/fixtures/snapshot_manager.ts | 4 +- yarn-project/end-to-end/src/fixtures/utils.ts | 4 +- .../ethereum/src/deploy_l1_contracts.ts | 246 ++++-------------- yarn-project/ethereum/src/index.ts | 1 + yarn-project/ethereum/src/l1_artifacts.ts | 178 +++++++++++++ .../src/job/epoch-proving-job-data.test.ts | 3 +- .../src/job/epoch-proving-job-data.ts | 10 +- .../src/job/epoch-proving-job.test.ts | 9 +- .../prover-node/src/job/epoch-proving-job.ts | 7 + .../src/prover-node-publisher.test.ts | 3 + .../prover-node/src/prover-node-publisher.ts | 13 +- yarn-project/prover-node/src/prover-node.ts | 4 +- .../block/proposal/committee_attestation.ts | 2 +- 14 files changed, 281 insertions(+), 211 deletions(-) create mode 100644 yarn-project/ethereum/src/l1_artifacts.ts diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts index c426fab07585..50c6c8932bef 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts @@ -5,12 +5,12 @@ import { type AccountWalletWithSecretKey, EthAddress } from '@aztec/aztec.js'; import { type ExtendedViemWalletClient, L1TxUtils, + MultiAdderArtifact, type Operator, RollupContract, type ViemClient, deployL1Contract, getL1ContractsConfigEnvVars, - l1Artifacts, } from '@aztec/ethereum'; import { ChainMonitor } from '@aztec/ethereum/test'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -234,14 +234,14 @@ export class P2PNetworkTest { const { address: multiAdderAddress } = await deployL1Contract( deployL1ContractsValues.l1Client, - l1Artifacts.multiAdder.contractAbi, - l1Artifacts.multiAdder.contractBytecode, + MultiAdderArtifact.contractAbi, + MultiAdderArtifact.contractBytecode, [rollup.address, deployL1ContractsValues.l1Client.account.address], ); const multiAdder = getContract({ address: multiAdderAddress.toString(), - abi: l1Artifacts.multiAdder.contractAbi, + abi: MultiAdderArtifact.contractAbi, client: deployL1ContractsValues.l1Client, }); diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 8742336fa0ea..e253862fd78c 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -20,11 +20,11 @@ import { type BlobSinkServer, createBlobSinkServer } from '@aztec/blob-sink/serv import { type DeployL1ContractsArgs, type DeployL1ContractsReturnType, + FeeAssetArtifact, RollupContract, createExtendedL1Client, deployMulticall3, getL1ContractsConfigEnvVars, - l1Artifacts, } from '@aztec/ethereum'; import { EthCheatCodesWithState, startAnvil } from '@aztec/ethereum/test'; import { asyncMap } from '@aztec/foundation/async-map'; @@ -389,7 +389,7 @@ async function setupFromFresh( const feeJuice = getContract({ address: deployL1ContractsValues.l1ContractAddresses.feeJuiceAddress.toString(), - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, client: deployL1ContractsValues.l1Client, }); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 0492b3f443d0..13628b3c8e2e 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -33,6 +33,7 @@ import { GENESIS_ARCHIVE_ROOT, SPONSORED_FPC_SALT } from '@aztec/constants'; import { type DeployL1ContractsArgs, type DeployL1ContractsReturnType, + FeeAssetArtifact, NULL_KEY, type Operator, RollupContract, @@ -41,7 +42,6 @@ import { deployMulticall3, getL1ContractsConfigEnvVars, isAnvilTestChain, - l1Artifacts, } from '@aztec/ethereum'; import { DelayedTxUtils, EthCheatCodesWithState, startAnvil } from '@aztec/ethereum/test'; import { SecretValue } from '@aztec/foundation/config'; @@ -494,7 +494,7 @@ export async function setup( const feeJuice = getContract({ address: deployL1ContractsValues.l1ContractAddresses.feeJuiceAddress.toString(), - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, client: deployL1ContractsValues.l1Client, }); diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 098eee87efd9..2bfbc5dde6a2 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -3,53 +3,6 @@ import { EthAddress } from '@aztec/foundation/eth-address'; import type { Fr } from '@aztec/foundation/fields'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { DateProvider } from '@aztec/foundation/timer'; -import { - CoinIssuerAbi, - CoinIssuerBytecode, - ExtRollupLib2Abi, - ExtRollupLib2Bytecode, - ExtRollupLibAbi, - ExtRollupLibBytecode, - FeeAssetHandlerAbi, - FeeAssetHandlerBytecode, - FeeJuicePortalAbi, - FeeJuicePortalBytecode, - GSEAbi, - GSEBytecode, - GovernanceAbi, - GovernanceBytecode, - GovernanceProposerAbi, - GovernanceProposerBytecode, - HonkVerifierAbi, - HonkVerifierBytecode, - InboxAbi, - InboxBytecode, - MockVerifierAbi, - MockVerifierBytecode, - MockZKPassportVerifierAbi, - MockZKPassportVerifierBytecode, - MultiAdderAbi, - MultiAdderBytecode, - OutboxAbi, - OutboxBytecode, - RegisterNewRollupVersionPayloadAbi, - RegisterNewRollupVersionPayloadBytecode, - RegistryAbi, - RegistryBytecode, - RewardDistributorAbi, - RewardDistributorBytecode, - RollupAbi, - RollupBytecode, - RollupLinkReferences, - SlashFactoryAbi, - SlashFactoryBytecode, - StakingAssetHandlerAbi, - StakingAssetHandlerBytecode, - TestERC20Abi, - TestERC20Bytecode, - ValidatorSelectionLibAbi, - ValidatorSelectionLibBytecode, -} from '@aztec/l1-artifacts'; import type { Abi, Narrow } from 'abitype'; import { @@ -81,6 +34,23 @@ import { import { deployMulticall3 } from './contracts/multicall.js'; import { RegistryContract } from './contracts/registry.js'; import { RollupContract } from './contracts/rollup.js'; +import { + CoinIssuerArtifact, + FeeAssetArtifact, + FeeAssetHandlerArtifact, + GSEArtifact, + GovernanceArtifact, + GovernanceProposerArtifact, + MultiAdderArtifact, + RegisterNewRollupVersionPayloadArtifact, + RegistryArtifact, + RollupArtifact, + SlashFactoryArtifact, + StakingAssetArtifact, + StakingAssetHandlerArtifact, + l1ArtifactsVerifiers, + mockVerifiers, +} from './l1_artifacts.js'; import type { L1ContractAddresses } from './l1_contract_addresses.js'; import { type GasPrice, @@ -146,115 +116,6 @@ export interface ContractArtifacts { libraries?: Libraries; } -export const l1Artifacts = { - registry: { - contractAbi: RegistryAbi, - contractBytecode: RegistryBytecode as Hex, - }, - inbox: { - contractAbi: InboxAbi, - contractBytecode: InboxBytecode as Hex, - }, - outbox: { - contractAbi: OutboxAbi, - contractBytecode: OutboxBytecode as Hex, - }, - rollup: { - contractAbi: RollupAbi, - contractBytecode: RollupBytecode as Hex, - libraries: { - linkReferences: RollupLinkReferences, - libraryCode: { - ValidatorSelectionLib: { - contractAbi: ValidatorSelectionLibAbi, - contractBytecode: ValidatorSelectionLibBytecode as Hex, - }, - ExtRollupLib: { - contractAbi: ExtRollupLibAbi, - contractBytecode: ExtRollupLibBytecode as Hex, - }, - ExtRollupLib2: { - contractAbi: ExtRollupLib2Abi, - contractBytecode: ExtRollupLib2Bytecode as Hex, - }, - }, - }, - }, - stakingAsset: { - contractAbi: TestERC20Abi, - contractBytecode: TestERC20Bytecode as Hex, - }, - feeAsset: { - contractAbi: TestERC20Abi, - contractBytecode: TestERC20Bytecode as Hex, - }, - feeJuicePortal: { - contractAbi: FeeJuicePortalAbi, - contractBytecode: FeeJuicePortalBytecode as Hex, - }, - rewardDistributor: { - contractAbi: RewardDistributorAbi, - contractBytecode: RewardDistributorBytecode as Hex, - }, - coinIssuer: { - contractAbi: CoinIssuerAbi, - contractBytecode: CoinIssuerBytecode as Hex, - }, - governanceProposer: { - contractAbi: GovernanceProposerAbi, - contractBytecode: GovernanceProposerBytecode as Hex, - }, - governance: { - contractAbi: GovernanceAbi, - contractBytecode: GovernanceBytecode as Hex, - }, - slashFactory: { - contractAbi: SlashFactoryAbi, - contractBytecode: SlashFactoryBytecode as Hex, - }, - registerNewRollupVersionPayload: { - contractAbi: RegisterNewRollupVersionPayloadAbi, - contractBytecode: RegisterNewRollupVersionPayloadBytecode as Hex, - }, - feeAssetHandler: { - contractAbi: FeeAssetHandlerAbi, - contractBytecode: FeeAssetHandlerBytecode as Hex, - }, - stakingAssetHandler: { - contractAbi: StakingAssetHandlerAbi, - contractBytecode: StakingAssetHandlerBytecode as Hex, - }, - multiAdder: { - contractAbi: MultiAdderAbi, - contractBytecode: MultiAdderBytecode as Hex, - }, -}; - -// Moving this out to avoid "type too big" error. -// Palla has a proper fix for this. -export const gseArtifact = { - contractAbi: GSEAbi, - contractBytecode: GSEBytecode as Hex, -}; - -export const l1ArtifactsVerifiers = { - honkVerifier: { - contractAbi: HonkVerifierAbi, - contractBytecode: HonkVerifierBytecode as Hex, - }, -}; - -const mockVerifiers = { - mockVerifier: { - contractAbi: MockVerifierAbi, - contractBytecode: MockVerifierBytecode as Hex, - }, - mockZkPassportVerifier: { - contractAbi: MockZKPassportVerifierAbi, - contractBytecode: MockZKPassportVerifierBytecode as Hex, - }, -}; - export interface DeployL1ContractsArgs extends L1ContractsConfig { /** The vk tree root. */ vkTreeRoot: Fr; @@ -297,14 +158,14 @@ export const deploySharedContracts = async ( const txHashes: Hex[] = []; - const feeAssetAddress = await deployer.deploy(l1Artifacts.feeAsset, [ + const feeAssetAddress = await deployer.deploy(FeeAssetArtifact, [ 'FeeJuice', 'FEE', l1Client.account.address.toString(), ]); logger.verbose(`Deployed Fee Asset at ${feeAssetAddress}`); - const stakingAssetAddress = await deployer.deploy(l1Artifacts.stakingAsset, [ + const stakingAssetAddress = await deployer.deploy(StakingAssetArtifact, [ 'Staking', 'STK', l1Client.account.address.toString(), @@ -313,7 +174,7 @@ export const deploySharedContracts = async ( const gseConfiguration = getGSEConfiguration(networkName); - const gseAddress = await deployer.deploy(gseArtifact, [ + const gseAddress = await deployer.deploy(GSEArtifact, [ l1Client.account.address.toString(), stakingAssetAddress.toString(), gseConfiguration.depositAmount, @@ -321,13 +182,13 @@ export const deploySharedContracts = async ( ]); logger.verbose(`Deployed GSE at ${gseAddress}`); - const registryAddress = await deployer.deploy(l1Artifacts.registry, [ + const registryAddress = await deployer.deploy(RegistryArtifact, [ l1Client.account.address.toString(), feeAssetAddress.toString(), ]); logger.verbose(`Deployed Registry at ${registryAddress}`); - const governanceProposerAddress = await deployer.deploy(l1Artifacts.governanceProposer, [ + const governanceProposerAddress = await deployer.deploy(GovernanceProposerArtifact, [ registryAddress.toString(), gseAddress.toString(), args.governanceProposerQuorum, @@ -337,7 +198,7 @@ export const deploySharedContracts = async ( // @note @LHerskind the assets are expected to be the same at some point, but for better // configurability they are different for now. - const governanceAddress = await deployer.deploy(l1Artifacts.governance, [ + const governanceAddress = await deployer.deploy(GovernanceArtifact, [ stakingAssetAddress.toString(), governanceProposerAddress.toString(), gseAddress.toString(), @@ -353,7 +214,7 @@ export const deploySharedContracts = async ( } else { const gseContract = getContract({ address: getAddress(gseAddress.toString()), - abi: gseArtifact.contractAbi, + abi: GSEArtifact.contractAbi, client: l1Client, }); const existingGovernance = await gseContract.read.getGovernance(); @@ -367,7 +228,7 @@ export const deploySharedContracts = async ( { to: gseAddress.toString(), data: encodeFunctionData({ - abi: gseArtifact.contractAbi, + abi: GSEArtifact.contractAbi, functionName: 'setGovernance', args: [governanceAddress.toString()], }), @@ -379,7 +240,7 @@ export const deploySharedContracts = async ( txHashes.push(txHash); } - const coinIssuerAddress = await deployer.deploy(l1Artifacts.coinIssuer, [ + const coinIssuerAddress = await deployer.deploy(CoinIssuerArtifact, [ feeAssetAddress.toString(), 1n * 10n ** 18n, // @todo #8084 governanceAddress.toString(), @@ -388,7 +249,7 @@ export const deploySharedContracts = async ( const feeAsset = getContract({ address: feeAssetAddress.toString(), - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, client: l1Client, }); @@ -400,7 +261,7 @@ export const deploySharedContracts = async ( { to: feeAssetAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, functionName: 'addMinter', args: [coinIssuerAddress.toString()], }), @@ -423,7 +284,7 @@ export const deploySharedContracts = async ( /* CHEAT CODES START HERE */ /* -------------------------------------------------------------------------- */ - feeAssetHandlerAddress = await deployer.deploy(l1Artifacts.feeAssetHandler, [ + feeAssetHandlerAddress = await deployer.deploy(FeeAssetHandlerArtifact, [ l1Client.account.address, feeAssetAddress.toString(), BigInt(1e18), @@ -433,7 +294,7 @@ export const deploySharedContracts = async ( const { txHash } = await deployer.sendTransaction({ to: feeAssetAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, functionName: 'addMinter', args: [feeAssetHandlerAddress.toString()], }), @@ -466,15 +327,13 @@ export const deploySharedContracts = async ( skipMerkleCheck: true, // skip merkle check - needed for testing without generating proofs }; - stakingAssetHandlerAddress = await deployer.deploy(l1Artifacts.stakingAssetHandler, [ - stakingAssetHandlerDeployArgs, - ]); + stakingAssetHandlerAddress = await deployer.deploy(StakingAssetHandlerArtifact, [stakingAssetHandlerDeployArgs]); logger.verbose(`Deployed StakingAssetHandler at ${stakingAssetHandlerAddress}`); const { txHash: stakingMinterTxHash } = await deployer.sendTransaction({ to: stakingAssetAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.stakingAsset.contractAbi, + abi: StakingAssetArtifact.contractAbi, functionName: 'addMinter', args: [stakingAssetHandlerAddress.toString()], }), @@ -510,7 +369,7 @@ export const deploySharedContracts = async ( const { txHash: fundRewardDistributorTxHash } = await deployer.sendTransaction({ to: feeAssetAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, functionName: 'mint', args: [rewardDistributorAddress.toString(), funding], }), @@ -592,7 +451,7 @@ export const deployRollupForUpgrade = async ( }; export const deploySlashFactory = async (deployer: L1Deployer, rollupAddress: Hex, logger: Logger) => { - const slashFactoryAddress = await deployer.deploy(l1Artifacts.slashFactory, [rollupAddress]); + const slashFactoryAddress = await deployer.deploy(SlashFactoryArtifact, [rollupAddress]); logger.verbose(`Deployed SlashFactory at ${slashFactoryAddress}`); return slashFactoryAddress; }; @@ -601,7 +460,7 @@ export const deployUpgradePayload = async ( deployer: L1Deployer, addresses: Pick, ) => { - const payloadAddress = await deployer.deploy(l1Artifacts.registerNewRollupVersionPayload, [ + const payloadAddress = await deployer.deploy(RegisterNewRollupVersionPayloadArtifact, [ addresses.registryAddress.toString(), addresses.rollupAddress.toString(), ]); @@ -682,7 +541,7 @@ export const deployRollup = async ( rollupConfigArgs, ]; - const rollupAddress = await deployer.deploy(l1Artifacts.rollup, rollupArgs); + const rollupAddress = await deployer.deploy(RollupArtifact, rollupArgs); logger.verbose(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs); const rollupContract = new RollupContract(extendedClient, rollupAddress); @@ -697,7 +556,7 @@ export const deployRollup = async ( const { txHash: mintTxHash } = await deployer.sendTransaction({ to: addresses.feeJuiceAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, functionName: 'mint', args: [feeJuicePortalAddress.toString(), args.feeJuicePortalInitialBalance], }), @@ -708,13 +567,13 @@ export const deployRollup = async ( txHashes.push(mintTxHash); } - const slashFactoryAddress = await deployer.deploy(l1Artifacts.slashFactory, [rollupAddress.toString()]); + const slashFactoryAddress = await deployer.deploy(SlashFactoryArtifact, [rollupAddress.toString()]); logger.verbose(`Deployed SlashFactory at ${slashFactoryAddress}`); // We need to call a function on the registry to set the various contract addresses. const registryContract = getContract({ address: getAddress(addresses.registryAddress.toString()), - abi: l1Artifacts.registry.contractAbi, + abi: RegistryArtifact.contractAbi, client: extendedClient, }); @@ -728,7 +587,7 @@ export const deployRollup = async ( const { txHash: addRollupTxHash } = await deployer.sendTransaction({ to: addresses.registryAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.registry.contractAbi, + abi: RegistryArtifact.contractAbi, functionName: 'addRollup', args: [getAddress(rollupContract.address)], }), @@ -746,7 +605,7 @@ export const deployRollup = async ( // We need to call a function on the registry to set the various contract addresses. const gseContract = getContract({ address: getAddress(addresses.gseAddress.toString()), - abi: gseArtifact.contractAbi, + abi: GSEArtifact.contractAbi, client: extendedClient, }); if ((await gseContract.read.owner()) === getAddress(extendedClient.account.address)) { @@ -754,7 +613,7 @@ export const deployRollup = async ( const { txHash: addRollupTxHash } = await deployer.sendTransaction({ to: addresses.gseAddress.toString(), data: encodeFunctionData({ - abi: gseArtifact.contractAbi, + abi: GSEArtifact.contractAbi, functionName: 'addRollup', args: [getAddress(rollupContract.address)], }), @@ -801,13 +660,13 @@ export const handoverToGovernance = async ( // We need to call a function on the registry to set the various contract addresses. const registryContract = getContract({ address: getAddress(registryAddress.toString()), - abi: l1Artifacts.registry.contractAbi, + abi: RegistryArtifact.contractAbi, client: extendedClient, }); const gseContract = getContract({ address: getAddress(gseAddress.toString()), - abi: gseArtifact.contractAbi, + abi: GSEArtifact.contractAbi, client: extendedClient, }); @@ -822,7 +681,7 @@ export const handoverToGovernance = async ( const { txHash: transferOwnershipTxHash } = await deployer.sendTransaction({ to: registryAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.registry.contractAbi, + abi: RegistryArtifact.contractAbi, functionName: 'transferOwnership', args: [getAddress(governanceAddress.toString())], }), @@ -839,7 +698,7 @@ export const handoverToGovernance = async ( const { txHash: transferOwnershipTxHash } = await deployer.sendTransaction({ to: gseContract.address, data: encodeFunctionData({ - abi: gseArtifact.contractAbi, + abi: GSEArtifact.contractAbi, functionName: 'transferOwnership', args: [getAddress(governanceAddress.toString())], }), @@ -899,10 +758,7 @@ export const addMultipleValidators = async ( } if (validators.length > 0) { - const multiAdder = await deployer.deploy(l1Artifacts.multiAdder, [ - rollupAddress, - deployer.client.account.address, - ]); + const multiAdder = await deployer.deploy(MultiAdderArtifact, [rollupAddress, deployer.client.account.address]); const validatorsTuples = validators.map(v => ({ attester: getAddress(v.attester.toString()), @@ -914,7 +770,7 @@ export const addMultipleValidators = async ( const { txHash } = await deployer.sendTransaction({ to: stakingAssetAddress, data: encodeFunctionData({ - abi: l1Artifacts.stakingAsset.contractAbi, + abi: StakingAssetArtifact.contractAbi, functionName: 'mint', args: [multiAdder.toString(), stakeNeeded], }), @@ -927,7 +783,7 @@ export const addMultipleValidators = async ( const addValidatorsTxHash = await deployer.client.writeContract({ address: multiAdder.toString(), - abi: l1Artifacts.multiAdder.contractAbi, + abi: MultiAdderArtifact.contractAbi, functionName: 'addValidators', args: [validatorsTuples], }); @@ -959,7 +815,7 @@ export const cheat_initializeFeeAssetHandler = async ( feeAssetHandlerAddress: EthAddress; txHash: Hex; }> => { - const feeAssetHandlerAddress = await deployer.deploy(l1Artifacts.feeAssetHandler, [ + const feeAssetHandlerAddress = await deployer.deploy(FeeAssetHandlerArtifact, [ extendedClient.account.address, feeAssetAddress.toString(), BigInt(1e18), @@ -969,7 +825,7 @@ export const cheat_initializeFeeAssetHandler = async ( const { txHash } = await deployer.sendTransaction({ to: feeAssetAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, functionName: 'addMinter', args: [feeAssetHandlerAddress.toString()], }), diff --git a/yarn-project/ethereum/src/index.ts b/yarn-project/ethereum/src/index.ts index 8ae82a605b16..66afabd6d35f 100644 --- a/yarn-project/ethereum/src/index.ts +++ b/yarn-project/ethereum/src/index.ts @@ -12,3 +12,4 @@ export * from './queries.js'; export * from './client.js'; export * from './account.js'; export * from './l1_types.js'; +export * from './l1_artifacts.js'; diff --git a/yarn-project/ethereum/src/l1_artifacts.ts b/yarn-project/ethereum/src/l1_artifacts.ts new file mode 100644 index 000000000000..d812af9336a1 --- /dev/null +++ b/yarn-project/ethereum/src/l1_artifacts.ts @@ -0,0 +1,178 @@ +import { + CoinIssuerAbi, + CoinIssuerBytecode, + ExtRollupLib2Abi, + ExtRollupLib2Bytecode, + ExtRollupLibAbi, + ExtRollupLibBytecode, + FeeAssetHandlerAbi, + FeeAssetHandlerBytecode, + FeeJuicePortalAbi, + FeeJuicePortalBytecode, + GSEAbi, + GSEBytecode, + GovernanceAbi, + GovernanceBytecode, + GovernanceProposerAbi, + GovernanceProposerBytecode, + HonkVerifierAbi, + HonkVerifierBytecode, + InboxAbi, + InboxBytecode, + MockVerifierAbi, + MockVerifierBytecode, + MockZKPassportVerifierAbi, + MockZKPassportVerifierBytecode, + MultiAdderAbi, + MultiAdderBytecode, + OutboxAbi, + OutboxBytecode, + RegisterNewRollupVersionPayloadAbi, + RegisterNewRollupVersionPayloadBytecode, + RegistryAbi, + RegistryBytecode, + RewardDistributorAbi, + RewardDistributorBytecode, + RollupAbi, + RollupBytecode, + RollupLinkReferences, + SlashFactoryAbi, + SlashFactoryBytecode, + StakingAssetHandlerAbi, + StakingAssetHandlerBytecode, + TestERC20Abi, + TestERC20Bytecode, + ValidatorSelectionLibAbi, + ValidatorSelectionLibBytecode, +} from '@aztec/l1-artifacts'; + +import type { Hex } from 'viem'; + +export const RegistryArtifact = { + contractAbi: RegistryAbi, + contractBytecode: RegistryBytecode as Hex, +}; + +export const InboxArtifact = { + contractAbi: InboxAbi, + contractBytecode: InboxBytecode as Hex, +}; + +export const OutboxArtifact = { + contractAbi: OutboxAbi, + contractBytecode: OutboxBytecode as Hex, +}; + +export const RollupArtifact = { + contractAbi: RollupAbi, + contractBytecode: RollupBytecode as Hex, + libraries: { + linkReferences: RollupLinkReferences, + libraryCode: { + ValidatorSelectionLib: { + contractAbi: ValidatorSelectionLibAbi, + contractBytecode: ValidatorSelectionLibBytecode as Hex, + }, + ExtRollupLib: { + contractAbi: ExtRollupLibAbi, + contractBytecode: ExtRollupLibBytecode as Hex, + }, + ExtRollupLib2: { + contractAbi: ExtRollupLib2Abi, + contractBytecode: ExtRollupLib2Bytecode as Hex, + }, + }, + }, +}; + +export const StakingAssetArtifact = { + contractAbi: TestERC20Abi, + contractBytecode: TestERC20Bytecode as Hex, +}; + +export const FeeAssetArtifact = { + contractAbi: TestERC20Abi, + contractBytecode: TestERC20Bytecode as Hex, +}; + +export const FeeJuicePortalArtifact = { + contractAbi: FeeJuicePortalAbi, + contractBytecode: FeeJuicePortalBytecode as Hex, +}; + +export const RewardDistributorArtifact = { + contractAbi: RewardDistributorAbi, + contractBytecode: RewardDistributorBytecode as Hex, +}; + +export const CoinIssuerArtifact = { + contractAbi: CoinIssuerAbi, + contractBytecode: CoinIssuerBytecode as Hex, +}; + +export const GovernanceProposerArtifact = { + contractAbi: GovernanceProposerAbi, + contractBytecode: GovernanceProposerBytecode as Hex, +}; + +export const GovernanceArtifact = { + contractAbi: GovernanceAbi, + contractBytecode: GovernanceBytecode as Hex, +}; + +export const SlashFactoryArtifact = { + contractAbi: SlashFactoryAbi, + contractBytecode: SlashFactoryBytecode as Hex, +}; + +export const RegisterNewRollupVersionPayloadArtifact = { + contractAbi: RegisterNewRollupVersionPayloadAbi, + contractBytecode: RegisterNewRollupVersionPayloadBytecode as Hex, +}; + +export const FeeAssetHandlerArtifact = { + contractAbi: FeeAssetHandlerAbi, + contractBytecode: FeeAssetHandlerBytecode as Hex, +}; + +export const StakingAssetHandlerArtifact = { + contractAbi: StakingAssetHandlerAbi, + contractBytecode: StakingAssetHandlerBytecode as Hex, +}; + +export const MultiAdderArtifact = { + contractAbi: MultiAdderAbi, + contractBytecode: MultiAdderBytecode as Hex, +}; + +export const GSEArtifact = { + contractAbi: GSEAbi, + contractBytecode: GSEBytecode as Hex, +}; + +// Verifier artifacts +export const HonkVerifierArtifact = { + contractAbi: HonkVerifierAbi, + contractBytecode: HonkVerifierBytecode as Hex, +}; + +export const MockVerifierArtifact = { + contractAbi: MockVerifierAbi, + contractBytecode: MockVerifierBytecode as Hex, +}; + +export const MockZkPassportVerifierArtifact = { + contractAbi: MockZKPassportVerifierAbi, + contractBytecode: MockZKPassportVerifierBytecode as Hex, +}; + +// Re-export the verifier artifacts for backwards compatibility +export const l1ArtifactsVerifiers = { + honkVerifier: HonkVerifierArtifact, +}; + +// Re-export the mock verifiers for backwards compatibility +export const mockVerifiers = { + mockVerifier: MockVerifierArtifact, + mockZkPassportVerifier: MockZkPassportVerifierArtifact, +}; diff --git a/yarn-project/prover-node/src/job/epoch-proving-job-data.test.ts b/yarn-project/prover-node/src/job/epoch-proving-job-data.test.ts index c2f57d2e0d82..79d683a4c2e9 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job-data.test.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job-data.test.ts @@ -1,6 +1,6 @@ import { times, timesAsync } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/fields'; -import { L2Block } from '@aztec/stdlib/block'; +import { CommitteeAttestation, L2Block } from '@aztec/stdlib/block'; import { Tx } from '@aztec/stdlib/tx'; import { @@ -22,6 +22,7 @@ describe('EpochProvingJobData', () => { 3: [Fr.random()], }, previousBlockHeader: await L2Block.random(0).then(b => b.header), + attestations: times(3, CommitteeAttestation.random), }; const serialized = serializeEpochProvingJobData(jobData); diff --git a/yarn-project/prover-node/src/job/epoch-proving-job-data.ts b/yarn-project/prover-node/src/job/epoch-proving-job-data.ts index f5ff9e82e8f9..bf4746e87ef8 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job-data.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job-data.ts @@ -1,6 +1,6 @@ import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; -import { L2Block } from '@aztec/stdlib/block'; +import { CommitteeAttestation, L2Block } from '@aztec/stdlib/block'; import { BlockHeader, Tx } from '@aztec/stdlib/tx'; /** All data from an epoch used in proving. */ @@ -10,6 +10,7 @@ export type EpochProvingJobData = { txs: Tx[]; l1ToL2Messages: Record; previousBlockHeader: BlockHeader; + attestations: CommitteeAttestation[]; }; export function validateEpochProvingJobData(data: EpochProvingJobData) { @@ -36,6 +37,7 @@ export function serializeEpochProvingJobData(data: EpochProvingJobData): Buffer messages.length, ...messages, ]); + const attestations = data.attestations.map(attestation => attestation.toBuffer()); return serializeToBuffer( Number(data.epochNumber), @@ -46,6 +48,8 @@ export function serializeEpochProvingJobData(data: EpochProvingJobData): Buffer ...txs, l1ToL2Messages.length, ...l1ToL2Messages, + attestations.length, + ...attestations, ); } @@ -64,5 +68,7 @@ export function deserializeEpochProvingJobData(buf: Buffer): EpochProvingJobData l1ToL2Messages[blockNumber] = messages; } - return { epochNumber, previousBlockHeader, blocks, txs, l1ToL2Messages }; + const attestations = reader.readVector(CommitteeAttestation); + + return { epochNumber, previousBlockHeader, blocks, txs, l1ToL2Messages, attestations }; } diff --git a/yarn-project/prover-node/src/job/epoch-proving-job.test.ts b/yarn-project/prover-node/src/job/epoch-proving-job.test.ts index 603b238a82fd..e356b482af14 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job.test.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job.test.ts @@ -3,7 +3,7 @@ import { fromEntries, times, timesParallel } from '@aztec/foundation/collection' import { toArray } from '@aztec/foundation/iterable'; import { sleep } from '@aztec/foundation/sleep'; import type { PublicProcessor, PublicProcessorFactory } from '@aztec/simulator/server'; -import { L2Block, type L2BlockSource } from '@aztec/stdlib/block'; +import { CommitteeAttestation, L2Block, type L2BlockSource, PublishedL2Block } from '@aztec/stdlib/block'; import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import type { EpochProver, MerkleTreeWriteOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import { Proof } from '@aztec/stdlib/proofs'; @@ -40,6 +40,7 @@ describe('epoch-proving-job', () => { let txs: Tx[]; let initialHeader: BlockHeader; let epochNumber: number; + let attestations: CommitteeAttestation[]; // Constants const NUM_BLOCKS = 3; @@ -54,6 +55,7 @@ describe('epoch-proving-job', () => { epochNumber: BigInt(epochNumber), l1ToL2Messages: fromEntries(blocks.map(b => [b.number, []])), previousBlockHeader: initialHeader, + attestations, }; return new EpochProvingJob( data, @@ -93,6 +95,8 @@ describe('epoch-proving-job', () => { epochNumber = 1; initialHeader = BlockHeader.empty(); blocks = await timesParallel(NUM_BLOCKS, i => L2Block.random(i + 1, TXS_PER_BLOCK)); + attestations = times(3, CommitteeAttestation.random); + txs = times(NUM_TXS, i => mock({ getTxHash: () => blocks[i % NUM_BLOCKS].body.txEffects[i % TXS_PER_BLOCK].txHash, @@ -102,6 +106,7 @@ describe('epoch-proving-job', () => { l2BlockSource.getBlockHeader.mockResolvedValue(initialHeader); l2BlockSource.getL1Constants.mockResolvedValue({ ethereumSlotDuration: 0.1 } as L1RollupConstants); l2BlockSource.getBlockHeadersForEpoch.mockResolvedValue(blocks.map(b => b.header)); + l2BlockSource.getPublishedBlocks.mockResolvedValue([{ block: blocks.at(-1)!, attestations } as PublishedL2Block]); publicProcessorFactory.create.mockReturnValue(publicProcessor); db.getInitialHeader.mockReturnValue(initialHeader); worldState.fork.mockResolvedValue(db); @@ -123,7 +128,7 @@ describe('epoch-proving-job', () => { expect(db.close).toHaveBeenCalledTimes(NUM_BLOCKS); expect(publicProcessor.process).toHaveBeenCalledTimes(NUM_BLOCKS); expect(publisher.submitEpochProof).toHaveBeenCalledWith( - expect.objectContaining({ epochNumber, proof, publicInputs }), + expect.objectContaining({ epochNumber, proof, publicInputs, attestations: [] }), ); }); diff --git a/yarn-project/prover-node/src/job/epoch-proving-job.ts b/yarn-project/prover-node/src/job/epoch-proving-job.ts index acbe7951b15b..e272a6b50dff 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job.ts @@ -84,6 +84,10 @@ export class EpochProvingJob implements Traceable { return this.data.txs; } + private get attestations() { + return this.data.attestations; + } + /** * Proves the given epoch and submits the proof to L1. */ @@ -96,6 +100,7 @@ export class EpochProvingJob implements Traceable { await this.scheduleEpochCheck(); } + const attestations = this.attestations.map(attestation => attestation.toViem()); const epochNumber = Number(this.epochNumber); const epochSizeBlocks = this.blocks.length; const epochSizeTxs = this.blocks.reduce((total, current) => total + current.body.txEffects.length, 0); @@ -168,6 +173,7 @@ export class EpochProvingJob implements Traceable { this.log.info(`Finalised proof for epoch ${epochNumber}`, { epochNumber, uuid: this.uuid, duration: timer.ms() }); this.progressState('publishing-proof'); + const success = await this.publisher.submitEpochProof({ fromBlock, toBlock, @@ -175,6 +181,7 @@ export class EpochProvingJob implements Traceable { publicInputs, proof, batchedBlobInputs, + attestations, }); if (!success) { throw new Error('Failed to submit epoch proof to L1'); diff --git a/yarn-project/prover-node/src/prover-node-publisher.test.ts b/yarn-project/prover-node/src/prover-node-publisher.test.ts index cf91361b73f4..c76d7b0fb22e 100644 --- a/yarn-project/prover-node/src/prover-node-publisher.test.ts +++ b/yarn-project/prover-node/src/prover-node-publisher.test.ts @@ -138,6 +138,8 @@ describe('prover-node-publisher', () => { rollup.getBlock.mockImplementation((blockNumber: bigint) => Promise.resolve({ archive: blocks[Number(blockNumber) - 1].endArchiveRoot.toString(), + attestationsHash: '0x', // unused, + payloadDigest: '0x', // unused, headerHash: '0x', // unused, blobCommitmentsHash: '0x', // unused, slotNumber: 0n, // unused, @@ -177,6 +179,7 @@ describe('prover-node-publisher', () => { publicInputs: ourPublicInputs, proof: Proof.empty(), batchedBlobInputs: ourBatchedBlob, + attestations: [], }) .then(() => 'Success') .catch(error => error.message); diff --git a/yarn-project/prover-node/src/prover-node-publisher.ts b/yarn-project/prover-node/src/prover-node-publisher.ts index 6192a74d3b49..2f1f5c209f09 100644 --- a/yarn-project/prover-node/src/prover-node-publisher.ts +++ b/yarn-project/prover-node/src/prover-node-publisher.ts @@ -1,6 +1,11 @@ import { type BatchedBlob, FinalBlobAccumulatorPublicInputs } from '@aztec/blob-lib'; import { AZTEC_MAX_EPOCH_DURATION } from '@aztec/constants'; -import type { L1TxUtils, RollupContract } from '@aztec/ethereum'; +import { + type L1TxUtils, + type RollupContract, + RollupContract as RollupContractClass, + type ViemCommitteeAttestation, +} from '@aztec/ethereum'; import { makeTuple } from '@aztec/foundation/array'; import { areArraysEqual } from '@aztec/foundation/collection'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -97,6 +102,7 @@ export class ProverNodePublisher { publicInputs: RootRollupPublicInputs; proof: Proof; batchedBlobInputs: BatchedBlob; + attestations: ViemCommitteeAttestation[]; }): Promise { const { epochNumber, fromBlock, toBlock } = args; const ctx = { epochNumber, fromBlock, toBlock }; @@ -150,6 +156,7 @@ export class ProverNodePublisher { publicInputs: RootRollupPublicInputs; proof: Proof; batchedBlobInputs: BatchedBlob; + attestations: ViemCommitteeAttestation[]; }) { const { fromBlock, toBlock, publicInputs, batchedBlobInputs } = args; @@ -207,6 +214,7 @@ export class ProverNodePublisher { publicInputs: RootRollupPublicInputs; proof: Proof; batchedBlobInputs: BatchedBlob; + attestations: ViemCommitteeAttestation[]; }): Promise { const txArgs = [this.getSubmitEpochProofArgs(args)] as const; @@ -246,6 +254,7 @@ export class ProverNodePublisher { toBlock: number; publicInputs: RootRollupPublicInputs; batchedBlobInputs: BatchedBlob; + attestations: ViemCommitteeAttestation[]; }) { // Returns arguments for EpochProofLib.sol -> getEpochProofPublicInputs() return [ @@ -271,6 +280,7 @@ export class ProverNodePublisher { publicInputs: RootRollupPublicInputs; proof: Proof; batchedBlobInputs: BatchedBlob; + attestations: ViemCommitteeAttestation[]; }) { // Returns arguments for EpochProofLib.sol -> submitEpochRootProof() const proofHex: Hex = `0x${args.proof.withoutPublicInputs().toString('hex')}`; @@ -280,6 +290,7 @@ export class ProverNodePublisher { end: argsArray[1], args: argsArray[2], fees: argsArray[3], + attestations: RollupContractClass.packAttestations(args.attestations), blobInputs: argsArray[4], proof: proofHex, }; diff --git a/yarn-project/prover-node/src/prover-node.ts b/yarn-project/prover-node/src/prover-node.ts index ce174aa7d5a4..365959f067a2 100644 --- a/yarn-project/prover-node/src/prover-node.ts +++ b/yarn-project/prover-node/src/prover-node.ts @@ -306,8 +306,10 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable const txs = await this.gatherTxs(epochNumber, blocks); const l1ToL2Messages = await this.gatherMessages(epochNumber, blocks); const previousBlockHeader = await this.gatherPreviousBlockHeader(epochNumber, blocks[0]); + const [lastBlock] = await this.l2BlockSource.getPublishedBlocks(blocks.at(-1)!.number, 1); + const attestations = lastBlock?.attestations ?? []; - return { blocks, txs, l1ToL2Messages, epochNumber, previousBlockHeader }; + return { blocks, txs, l1ToL2Messages, epochNumber, previousBlockHeader, attestations }; } private async gatherBlocks(epochNumber: bigint) { diff --git a/yarn-project/stdlib/src/block/proposal/committee_attestation.ts b/yarn-project/stdlib/src/block/proposal/committee_attestation.ts index 4cd1601778ad..e91eeec5b6a7 100644 --- a/yarn-project/stdlib/src/block/proposal/committee_attestation.ts +++ b/yarn-project/stdlib/src/block/proposal/committee_attestation.ts @@ -36,7 +36,7 @@ export class CommitteeAttestation { return new CommitteeAttestation(EthAddress.fromString(viem.addr), Signature.fromViemSignature(viem.signature)); } - static fromBuffer(buffer: Buffer): CommitteeAttestation { + static fromBuffer(buffer: Buffer | BufferReader): CommitteeAttestation { const reader = BufferReader.asReader(buffer); const address = reader.readObject(EthAddress); const signature = reader.readObject(Signature); From c3edca62adda06e54c93ba781cef504e29ba1fdd Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 21 Jul 2025 19:06:39 -0300 Subject: [PATCH 05/22] Reduce Rollup contract size --- l1-contracts/src/core/Rollup.sol | 20 +-------------- l1-contracts/src/core/RollupCore.sol | 7 +++--- .../core/libraries/rollup/ExtRollupLib.sol | 6 +++++ .../core/libraries/rollup/ExtRollupLib2.sol | 12 +++++++++ .../rollup/ValidatorSelectionLib.sol | 25 +++++++++++++++++++ 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 25a5fcfda0e7..f5059e2fac94 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -190,25 +190,7 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { override(IRollup) returns (Slot, uint256) { - Slot slot = _ts.slotFromTimestamp(); - RollupStore storage rollupStore = STFLib.getStorage(); - - uint256 pendingBlockNumber = STFLib.getEffectivePendingBlockNumber(_ts); - - Slot lastSlot = STFLib.getSlotNumber(pendingBlockNumber); - - require(slot > lastSlot, Errors.Rollup__SlotAlreadyInChain(lastSlot, slot)); - - // Make sure that the proposer is up to date and on the right chain (ie no reorgs) - bytes32 tipArchive = rollupStore.archives[pendingBlockNumber]; - require(tipArchive == _archive, Errors.Rollup__InvalidArchive(tipArchive, _archive)); - - address proposer = ExtRollupLib2.getProposerAt(slot); - require( - proposer == msg.sender, Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender) - ); - - return (slot, pendingBlockNumber + 1); + return ExtRollupLib2.canProposeAtTime(_ts, _archive); } function getTargetCommitteeSize() external view override(IValidatorSelection) returns (uint256) { diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 7053c2fc5e44..58c56170115a 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -223,16 +223,15 @@ contract RollupCore is } function finaliseWithdraw(address _attester) external override(IStakingCore) { - StakingLib.finaliseWithdraw(_attester); + ExtRollupLib2.finaliseWithdraw(_attester); } function slash(address _attester, uint256 _amount) external override(IStakingCore) returns (bool) { - return StakingLib.trySlash(_attester, _amount); + return ExtRollupLib2.slash(_attester, _amount); } function prune() external override(IRollupCore) { - require(STFLib.canPruneAtTime(Timestamp.wrap(block.timestamp)), Errors.Rollup__NothingToPrune()); - STFLib.prune(); + ExtRollupLib.prune(); } function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index c6e28abab384..91aacc2902d1 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -3,6 +3,7 @@ // solhint-disable imports-order pragma solidity >=0.8.27; +import {Errors} from "@aztec/core/libraries/Errors.sol"; import {SubmitEpochRootProofArgs, PublicInputArgs} from "@aztec/core/interfaces/IRollup.sol"; import {TempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; @@ -79,4 +80,9 @@ library ExtRollupLib { function getBlobBaseFee() external view returns (uint256) { return BlobLib.getBlobBaseFee(); } + + function prune() external { + require(STFLib.canPruneAtTime(Timestamp.wrap(block.timestamp)), Errors.Rollup__NothingToPrune()); + STFLib.prune(); + } } diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol index f211ee371fe2..1deacf8e3517 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol @@ -62,6 +62,10 @@ library ExtRollupLib2 { return StakingLib.initiateWithdraw(_attester, _recipient); } + function finaliseWithdraw(address _attester) external { + StakingLib.finaliseWithdraw(_attester); + } + function initializeValidatorSelection(uint256 _targetCommitteeSize) external { ValidatorSelectionLib.initialize(_targetCommitteeSize); } @@ -97,6 +101,10 @@ library ExtRollupLib2 { InvalidateLib.invalidateInsufficientAttestations(_blockNumber, _attestations, _committee); } + function canProposeAtTime(Timestamp _ts, bytes32 _archive) external returns (Slot, uint256) { + return ValidatorSelectionLib.canProposeAtTime(_ts, _archive); + } + function getCommitteeAt(Epoch _epoch) external returns (address[] memory) { return ValidatorSelectionLib.getCommitteeAt(_epoch); } @@ -120,4 +128,8 @@ library ExtRollupLib2 { function getEntryQueueFlushSize() external view returns (uint256) { return StakingLib.getEntryQueueFlushSize(); } + + function slash(address _attester, uint256 _amount) external returns (bool) { + return StakingLib.trySlash(_attester, _amount); + } } diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index c9b886c42317..3d4899c78072 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -2,6 +2,9 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; +import {RollupStore} from "@aztec/core/interfaces/IRollup.sol"; +import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; + import {BlockHeaderValidationFlags} from "@aztec/core/interfaces/IRollup.sol"; import {ValidatorSelectionStorage} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {SampleLib} from "@aztec/core/libraries/crypto/SampleLib.sol"; @@ -397,6 +400,28 @@ library ValidatorSelectionLib { } } + function canProposeAtTime(Timestamp _ts, bytes32 _archive) internal returns (Slot, uint256) { + Slot slot = _ts.slotFromTimestamp(); + RollupStore storage rollupStore = STFLib.getStorage(); + + uint256 pendingBlockNumber = STFLib.getEffectivePendingBlockNumber(_ts); + + Slot lastSlot = STFLib.getSlotNumber(pendingBlockNumber); + + require(slot > lastSlot, Errors.Rollup__SlotAlreadyInChain(lastSlot, slot)); + + // Make sure that the proposer is up to date and on the right chain (ie no reorgs) + bytes32 tipArchive = rollupStore.archives[pendingBlockNumber]; + require(tipArchive == _archive, Errors.Rollup__InvalidArchive(tipArchive, _archive)); + + (address proposer,) = getProposerAt(slot); + require( + proposer == msg.sender, Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender) + ); + + return (slot, pendingBlockNumber + 1); + } + /** * @notice Computes the nextSeed for an epoch * From e183f9771a282b565bb42ff02f0b3bf8eeb09ee6 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 21 Jul 2025 19:09:44 -0300 Subject: [PATCH 06/22] Try/catch L1 contract deploy --- .../cli/src/cmds/devnet/bootstrap_network.ts | 2 + .../ethereum/src/deploy_l1_contracts.ts | 41 ++++++++++++------- yarn-project/ethereum/src/l1_artifacts.ts | 23 +++++++++++ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts index 7bf020460b05..4d5f7454b486 100644 --- a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts +++ b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts @@ -140,10 +140,12 @@ async function deployERC20(l1Client: ExtendedViemWalletClient) { const { TestERC20Abi, TestERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } = await import('@aztec/l1-artifacts'); const erc20: ContractArtifacts = { + name: 'TestERC20', contractAbi: TestERC20Abi, contractBytecode: TestERC20Bytecode, }; const portal: ContractArtifacts = { + name: 'TokenPortal', contractAbi: TokenPortalAbi, contractBytecode: TokenPortalBytecode, }; diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 2bfbc5dde6a2..38626605820a 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -61,6 +61,7 @@ import { getL1TxUtilsConfigEnvVars, } from './l1_tx_utils.js'; import type { ExtendedViemWalletClient } from './types.js'; +import { formatViemError } from './utils.js'; import { ZK_PASSPORT_DOMAIN, ZK_PASSPORT_SCOPE, ZK_PASSPORT_VERIFIER_ADDRESS } from './zkPassportVerifierAddress.js'; export const DEPLOYER_ADDRESS: Hex = '0x4e59b44847b379578588920cA78FbF26c0B4956C'; @@ -102,6 +103,10 @@ export interface Libraries { * Contract artifacts */ export interface ContractArtifacts { + /** + * The contract name. + */ + name: string; /** * The contract abi. */ @@ -154,7 +159,7 @@ export const deploySharedContracts = async ( args: DeployL1ContractsArgs, logger: Logger, ) => { - logger.info(`Deploying shared contracts. Network configration: ${networkName}`); + logger.info(`Deploying shared contracts for network configration: ${networkName}`); const txHashes: Hex[] = []; @@ -994,21 +999,27 @@ export class L1Deployer { } async deploy(params: ContractArtifacts, args: readonly unknown[] = []): Promise { - const { txHash, address } = await deployL1Contract( - this.client, - params.contractAbi, - params.contractBytecode, - args, - this.salt, - params.libraries, - this.logger, - this.l1TxUtils, - this.acceleratedTestDeployments, - ); - if (txHash) { - this.txHashes.push(txHash); + this.logger.debug(`Deploying ${params.name} contract`, { args }); + try { + const { txHash, address } = await deployL1Contract( + this.client, + params.contractAbi, + params.contractBytecode, + args, + this.salt, + params.libraries, + this.logger, + this.l1TxUtils, + this.acceleratedTestDeployments, + ); + if (txHash) { + this.txHashes.push(txHash); + } + this.logger.debug(`Deployed ${params.name} at ${address}`, { args }); + return address; + } catch (error) { + throw new Error(`Failed to deploy ${params.name}`, { cause: formatViemError(error) }); } - return address; } async waitForDeployments(): Promise { diff --git a/yarn-project/ethereum/src/l1_artifacts.ts b/yarn-project/ethereum/src/l1_artifacts.ts index d812af9336a1..cb4e51583f08 100644 --- a/yarn-project/ethereum/src/l1_artifacts.ts +++ b/yarn-project/ethereum/src/l1_artifacts.ts @@ -49,35 +49,42 @@ import { import type { Hex } from 'viem'; export const RegistryArtifact = { + name: 'Registry', contractAbi: RegistryAbi, contractBytecode: RegistryBytecode as Hex, }; export const InboxArtifact = { + name: 'Inbox', contractAbi: InboxAbi, contractBytecode: InboxBytecode as Hex, }; export const OutboxArtifact = { + name: 'Outbox', contractAbi: OutboxAbi, contractBytecode: OutboxBytecode as Hex, }; export const RollupArtifact = { + name: 'Rollup', contractAbi: RollupAbi, contractBytecode: RollupBytecode as Hex, libraries: { linkReferences: RollupLinkReferences, libraryCode: { ValidatorSelectionLib: { + name: 'ValidatorSelectionLib', contractAbi: ValidatorSelectionLibAbi, contractBytecode: ValidatorSelectionLibBytecode as Hex, }, ExtRollupLib: { + name: 'ExtRollupLib', contractAbi: ExtRollupLibAbi, contractBytecode: ExtRollupLibBytecode as Hex, }, ExtRollupLib2: { + name: 'ExtRollupLib2', contractAbi: ExtRollupLib2Abi, contractBytecode: ExtRollupLib2Bytecode as Hex, }, @@ -86,82 +93,98 @@ export const RollupArtifact = { }; export const StakingAssetArtifact = { + name: 'StakingAsset', contractAbi: TestERC20Abi, contractBytecode: TestERC20Bytecode as Hex, }; export const FeeAssetArtifact = { + name: 'FeeAsset', contractAbi: TestERC20Abi, contractBytecode: TestERC20Bytecode as Hex, }; export const FeeJuicePortalArtifact = { + name: 'FeeJuicePortal', contractAbi: FeeJuicePortalAbi, contractBytecode: FeeJuicePortalBytecode as Hex, }; export const RewardDistributorArtifact = { + name: 'RewardDistributor', contractAbi: RewardDistributorAbi, contractBytecode: RewardDistributorBytecode as Hex, }; export const CoinIssuerArtifact = { + name: 'CoinIssuer', contractAbi: CoinIssuerAbi, contractBytecode: CoinIssuerBytecode as Hex, }; export const GovernanceProposerArtifact = { + name: 'GovernanceProposer', contractAbi: GovernanceProposerAbi, contractBytecode: GovernanceProposerBytecode as Hex, }; export const GovernanceArtifact = { + name: 'Governance', contractAbi: GovernanceAbi, contractBytecode: GovernanceBytecode as Hex, }; export const SlashFactoryArtifact = { + name: 'SlashFactory', contractAbi: SlashFactoryAbi, contractBytecode: SlashFactoryBytecode as Hex, }; export const RegisterNewRollupVersionPayloadArtifact = { + name: 'RegisterNewRollupVersionPayload', contractAbi: RegisterNewRollupVersionPayloadAbi, contractBytecode: RegisterNewRollupVersionPayloadBytecode as Hex, }; export const FeeAssetHandlerArtifact = { + name: 'FeeAssetHandler', contractAbi: FeeAssetHandlerAbi, contractBytecode: FeeAssetHandlerBytecode as Hex, }; export const StakingAssetHandlerArtifact = { + name: 'StakingAssetHandler', contractAbi: StakingAssetHandlerAbi, contractBytecode: StakingAssetHandlerBytecode as Hex, }; export const MultiAdderArtifact = { + name: 'MultiAdder', contractAbi: MultiAdderAbi, contractBytecode: MultiAdderBytecode as Hex, }; export const GSEArtifact = { + name: 'GSE', contractAbi: GSEAbi, contractBytecode: GSEBytecode as Hex, }; // Verifier artifacts export const HonkVerifierArtifact = { + name: 'HonkVerifier', contractAbi: HonkVerifierAbi, contractBytecode: HonkVerifierBytecode as Hex, }; export const MockVerifierArtifact = { + name: 'MockVerifier', contractAbi: MockVerifierAbi, contractBytecode: MockVerifierBytecode as Hex, }; export const MockZkPassportVerifierArtifact = { + name: 'MockZkPassportVerifier', contractAbi: MockZKPassportVerifierAbi, contractBytecode: MockZKPassportVerifierBytecode as Hex, }; From f8612c51035307d96a89daf6da56327ee6f1ce22 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 21 Jul 2025 19:59:09 -0300 Subject: [PATCH 07/22] Fix tests --- .../prover-node/src/job/epoch-proving-job-data.test.ts | 4 +++- yarn-project/prover-node/src/job/epoch-proving-job.test.ts | 2 +- yarn-project/prover-node/src/prover-node.test.ts | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/yarn-project/prover-node/src/job/epoch-proving-job-data.test.ts b/yarn-project/prover-node/src/job/epoch-proving-job-data.test.ts index 79d683a4c2e9..e7e70ac8f87a 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job-data.test.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job-data.test.ts @@ -26,6 +26,8 @@ describe('EpochProvingJobData', () => { }; const serialized = serializeEpochProvingJobData(jobData); - expect(deserializeEpochProvingJobData(serialized)).toEqual(jobData); + const deserialized = deserializeEpochProvingJobData(serialized); + deserialized.attestations.forEach(a => a.signature.getSize()); + expect(deserialized).toEqual(jobData); }); }); diff --git a/yarn-project/prover-node/src/job/epoch-proving-job.test.ts b/yarn-project/prover-node/src/job/epoch-proving-job.test.ts index e356b482af14..a9ce13ff9ff1 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job.test.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job.test.ts @@ -128,7 +128,7 @@ describe('epoch-proving-job', () => { expect(db.close).toHaveBeenCalledTimes(NUM_BLOCKS); expect(publicProcessor.process).toHaveBeenCalledTimes(NUM_BLOCKS); expect(publisher.submitEpochProof).toHaveBeenCalledWith( - expect.objectContaining({ epochNumber, proof, publicInputs, attestations: [] }), + expect.objectContaining({ epochNumber, proof, publicInputs, attestations: attestations.map(a => a.toViem()) }), ); }); diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index df629e129d5c..73ae1c8c5f45 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -6,7 +6,7 @@ import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import type { P2PClient, TxProvider } from '@aztec/p2p'; import type { PublicProcessorFactory } from '@aztec/simulator/server'; -import { L2Block, type L2BlockSource } from '@aztec/stdlib/block'; +import { CommitteeAttestation, L2Block, type L2BlockSource, PublishedL2Block } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; import { EmptyL1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { @@ -49,6 +49,7 @@ describe('prover-node', () => { // Blocks returned by the archiver let blocks: L2Block[]; + let lastBlock: PublishedL2Block; let previousBlockHeader: BlockHeader; // Address of the publisher @@ -116,6 +117,7 @@ describe('prover-node', () => { // We create 3 fake blocks with 1 tx effect each blocks = await timesParallel(3, async i => await L2Block.random(i + 20, 1)); previousBlockHeader = await L2Block.random(19).then(b => b.header); + lastBlock = { block: blocks.at(-1)!, attestations: [CommitteeAttestation.random()] } as PublishedL2Block; // Archiver returns a bunch of fake blocks l2BlockSource.getBlocks.mockImplementation((from, limit) => { @@ -130,6 +132,7 @@ describe('prover-node', () => { l1GenesisTime = Math.floor(Date.now() / 1000) - 3600; l2BlockSource.getL1Constants.mockResolvedValue({ ...EmptyL1RollupConstants, l1GenesisTime: BigInt(l1GenesisTime) }); l2BlockSource.getBlocksForEpoch.mockResolvedValue(blocks); + l2BlockSource.getPublishedBlocks.mockResolvedValue([lastBlock]); l2BlockSource.getL2Tips.mockResolvedValue({ latest: { number: blocks.at(-1)!.number, hash: (await blocks.at(-1)!.hash()).toString() }, proven: { number: 0, hash: undefined }, From 72467eea9b691d5a416cc8fce95f4debc3f38fe2 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 22 Jul 2025 06:13:00 -0300 Subject: [PATCH 08/22] Format --- l1-contracts/test/benchmark/happy.t.sol | 4 ++-- l1-contracts/test/compression/PreHeating.t.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/l1-contracts/test/benchmark/happy.t.sol b/l1-contracts/test/benchmark/happy.t.sol index 34eb89ef207a..0884fc55fca1 100644 --- a/l1-contracts/test/benchmark/happy.t.sol +++ b/l1-contracts/test/benchmark/happy.t.sol @@ -133,7 +133,7 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { CommitteeAttestation internal emptyAttestation; mapping(address attester => uint256 privateKey) internal attesterPrivateKeys; - + // Track attestations by block number for proof submission mapping(uint256 => CommitteeAttestations) internal blockAttestations; @@ -371,7 +371,7 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { address proposer = rollup.getCurrentProposer(); skipBlobCheck(address(rollup)); - + // Store the attestations for the current block number uint256 currentBlockNumber = rollup.getPendingBlockNumber() + 1; blockAttestations[currentBlockNumber] = SignatureLib.packAttestations(b.attestations); diff --git a/l1-contracts/test/compression/PreHeating.t.sol b/l1-contracts/test/compression/PreHeating.t.sol index 72b8466867bc..e6d5006287a6 100644 --- a/l1-contracts/test/compression/PreHeating.t.sol +++ b/l1-contracts/test/compression/PreHeating.t.sol @@ -134,7 +134,7 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { CommitteeAttestation internal emptyAttestation; mapping(address attester => uint256 privateKey) internal attesterPrivateKeys; - + // Track attestations by block number for proof submission mapping(uint256 => CommitteeAttestations) internal blockAttestations; From 553058e541088fe81935008ca8418e6c892f8ba7 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 22 Jul 2025 06:59:17 -0300 Subject: [PATCH 09/22] Solhint --- l1-contracts/src/core/RollupCore.sol | 4 +- .../core/libraries/rollup/EpochProofLib.sol | 55 +++++++++---------- .../core/libraries/rollup/ExtRollupLib.sol | 12 ++-- .../core/libraries/rollup/ExtRollupLib2.sol | 8 +-- .../core/libraries/rollup/InvalidateLib.sol | 10 +--- .../rollup/ValidatorSelectionLib.sol | 48 ++++++++-------- l1-contracts/src/mock/StakingAssetHandler.sol | 2 +- 7 files changed, 65 insertions(+), 74 deletions(-) diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 58c56170115a..630b2634e127 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -254,7 +254,7 @@ contract RollupCore is CommitteeAttestations memory _attestations, address[] memory _committee, uint256 _invalidIndex - ) external { + ) external override(IRollupCore) { ExtRollupLib2.invalidateBadAttestation(_blockNumber, _attestations, _committee, _invalidIndex); } @@ -262,7 +262,7 @@ contract RollupCore is uint256 _blockNumber, CommitteeAttestations memory _attestations, address[] memory _committee - ) external { + ) external override(IRollupCore) { ExtRollupLib2.invalidateInsufficientAttestations(_blockNumber, _attestations, _committee); } diff --git a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol index 2953ca48a8de..6ff3b6e5e747 100644 --- a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol +++ b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol @@ -6,11 +6,10 @@ import { SubmitEpochRootProofArgs, PublicInputArgs, IRollupCore, - RollupStore, - BlockHeaderValidationFlags + RollupStore } from "@aztec/core/interfaces/IRollup.sol"; -import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol"; import {TempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; +import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol"; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {BlobLib} from "@aztec/core/libraries/rollup/BlobLib.sol"; @@ -215,6 +214,31 @@ library EpochProofLib { return publicInputs; } + /** + * @notice Verifies the attestations for the last block in the epoch + * @param _endBlockNumber The last block number in the epoch + * @param _attestations The attestations to verify + */ + function verifyLastBlockAttestations( + uint256 _endBlockNumber, + CommitteeAttestations memory _attestations + ) private { + // Get the stored attestation hash and payload digest for the last block + TempBlockLog memory blockLog = STFLib.getTempBlockLog(_endBlockNumber); + + // Verify that the provided attestations match the stored hash + bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations)); + require( + providedAttestationsHash == blockLog.attestationsHash, Errors.Rollup__InvalidAttestations() + ); + + // Get the slot and epoch for the last block + Slot slot = blockLog.slotNumber; + Epoch epoch = STFLib.getEpochForBlock(_endBlockNumber); + + ValidatorSelectionLib.verify(slot, epoch, _attestations, blockLog.payloadDigest); + } + function assertAcceptable(uint256 _start, uint256 _end) private view returns (Epoch) { RollupStore storage rollupStore = STFLib.getStorage(); @@ -294,29 +318,4 @@ library EpochProofLib { function addressToField(address _a) private pure returns (bytes32) { return bytes32(uint256(uint160(_a))); } - - /** - * @notice Verifies the attestations for the last block in the epoch - * @param _endBlockNumber The last block number in the epoch - * @param _attestations The attestations to verify - */ - function verifyLastBlockAttestations( - uint256 _endBlockNumber, - CommitteeAttestations memory _attestations - ) private { - // Get the stored attestation hash and payload digest for the last block - TempBlockLog memory blockLog = STFLib.getTempBlockLog(_endBlockNumber); - - // Verify that the provided attestations match the stored hash - bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations)); - require( - providedAttestationsHash == blockLog.attestationsHash, Errors.Rollup__InvalidAttestations() - ); - - // Get the slot and epoch for the last block - Slot slot = blockLog.slotNumber; - Epoch epoch = STFLib.getEpochForBlock(_endBlockNumber); - - ValidatorSelectionLib.verify(slot, epoch, _attestations, blockLog.payloadDigest); - } } diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index 91aacc2902d1..a9bc5bd106df 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -5,12 +5,10 @@ pragma solidity >=0.8.27; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {SubmitEpochRootProofArgs, PublicInputArgs} from "@aztec/core/interfaces/IRollup.sol"; -import {TempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; import {Timestamp, TimeLib, Slot, Epoch} from "@aztec/core/libraries/TimeLib.sol"; import {BlobLib} from "./BlobLib.sol"; import {EpochProofLib} from "./EpochProofLib.sol"; -import {InvalidateLib} from "./InvalidateLib.sol"; import {SignatureLib} from "@aztec/shared/libraries/SignatureLib.sol"; import { ProposeLib, @@ -55,6 +53,11 @@ library ExtRollupLib { ProposeLib.propose(_args, _attestations, _blobInput, _checkBlob); } + function prune() external { + require(STFLib.canPruneAtTime(Timestamp.wrap(block.timestamp)), Errors.Rollup__NothingToPrune()); + STFLib.prune(); + } + function getEpochProofPublicInputs( uint256 _start, uint256 _end, @@ -80,9 +83,4 @@ library ExtRollupLib { function getBlobBaseFee() external view returns (uint256) { return BlobLib.getBlobBaseFee(); } - - function prune() external { - require(STFLib.canPruneAtTime(Timestamp.wrap(block.timestamp)), Errors.Rollup__NothingToPrune()); - STFLib.prune(); - } } diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol index 1deacf8e3517..a50c3dfb8cae 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol @@ -101,6 +101,10 @@ library ExtRollupLib2 { InvalidateLib.invalidateInsufficientAttestations(_blockNumber, _attestations, _committee); } + function slash(address _attester, uint256 _amount) external returns (bool) { + return StakingLib.trySlash(_attester, _amount); + } + function canProposeAtTime(Timestamp _ts, bytes32 _archive) external returns (Slot, uint256) { return ValidatorSelectionLib.canProposeAtTime(_ts, _archive); } @@ -128,8 +132,4 @@ library ExtRollupLib2 { function getEntryQueueFlushSize() external view returns (uint256) { return StakingLib.getEntryQueueFlushSize(); } - - function slash(address _attester, uint256 _amount) external returns (bool) { - return StakingLib.trySlash(_attester, _amount); - } } diff --git a/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol index 64e7532d7751..a2ce9aea67f0 100644 --- a/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol +++ b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol @@ -2,23 +2,19 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import { - IRollupCore, RollupStore, BlockHeaderValidationFlags -} from "@aztec/core/interfaces/IRollup.sol"; -import { - TempBlockLog, CompressedTempBlockLog -} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; +import {IRollupCore, RollupStore} from "@aztec/core/interfaces/IRollup.sol"; +import {CompressedTempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; import {ValidatorSelectionLib} from "@aztec/core/libraries/rollup/ValidatorSelectionLib.sol"; import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +import {CompressedSlot, CompressedTimeMath} from "@aztec/shared/libraries/CompressedTimeMath.sol"; import { CommitteeAttestations, SignatureLib, Signature } from "@aztec/shared/libraries/SignatureLib.sol"; import {ECDSA} from "@oz/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; -import {CompressedSlot, CompressedTimeMath} from "@aztec/shared/libraries/CompressedTimeMath.sol"; library InvalidateLib { using TimeLib for Timestamp; diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index 3d4899c78072..f84dfa233293 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -3,13 +3,11 @@ pragma solidity >=0.8.27; import {RollupStore} from "@aztec/core/interfaces/IRollup.sol"; -import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; - -import {BlockHeaderValidationFlags} from "@aztec/core/interfaces/IRollup.sol"; import {ValidatorSelectionStorage} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {SampleLib} from "@aztec/core/libraries/crypto/SampleLib.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {StakingLib} from "@aztec/core/libraries/rollup/StakingLib.sol"; +import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; import { SignatureLib, Signature, CommitteeAttestations @@ -354,6 +352,28 @@ library ValidatorSelectionLib { } } + function canProposeAtTime(Timestamp _ts, bytes32 _archive) internal returns (Slot, uint256) { + Slot slot = _ts.slotFromTimestamp(); + RollupStore storage rollupStore = STFLib.getStorage(); + + uint256 pendingBlockNumber = STFLib.getEffectivePendingBlockNumber(_ts); + + Slot lastSlot = STFLib.getSlotNumber(pendingBlockNumber); + + require(slot > lastSlot, Errors.Rollup__SlotAlreadyInChain(lastSlot, slot)); + + // Make sure that the proposer is up to date and on the right chain (ie no reorgs) + bytes32 tipArchive = rollupStore.archives[pendingBlockNumber]; + require(tipArchive == _archive, Errors.Rollup__InvalidArchive(tipArchive, _archive)); + + (address proposer,) = getProposerAt(slot); + require( + proposer == msg.sender, Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender) + ); + + return (slot, pendingBlockNumber + 1); + } + function getCachedProposer(Slot _slot) internal view @@ -400,28 +420,6 @@ library ValidatorSelectionLib { } } - function canProposeAtTime(Timestamp _ts, bytes32 _archive) internal returns (Slot, uint256) { - Slot slot = _ts.slotFromTimestamp(); - RollupStore storage rollupStore = STFLib.getStorage(); - - uint256 pendingBlockNumber = STFLib.getEffectivePendingBlockNumber(_ts); - - Slot lastSlot = STFLib.getSlotNumber(pendingBlockNumber); - - require(slot > lastSlot, Errors.Rollup__SlotAlreadyInChain(lastSlot, slot)); - - // Make sure that the proposer is up to date and on the right chain (ie no reorgs) - bytes32 tipArchive = rollupStore.archives[pendingBlockNumber]; - require(tipArchive == _archive, Errors.Rollup__InvalidArchive(tipArchive, _archive)); - - (address proposer,) = getProposerAt(slot); - require( - proposer == msg.sender, Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender) - ); - - return (slot, pendingBlockNumber + 1); - } - /** * @notice Computes the nextSeed for an epoch * diff --git a/l1-contracts/src/mock/StakingAssetHandler.sol b/l1-contracts/src/mock/StakingAssetHandler.sol index 0daf0d3eccf6..4ee23cc53851 100644 --- a/l1-contracts/src/mock/StakingAssetHandler.sol +++ b/l1-contracts/src/mock/StakingAssetHandler.sol @@ -59,7 +59,7 @@ interface IStakingAssetHandler { // Add validator methods function addValidator( address _attester, - bytes32[] memory merkleProof, + bytes32[] memory _merkleProof, ProofVerificationParams memory _params ) external; function reenterExitedValidator(address _attester) external; From cad8ca4f47063cf5c34a6a5993b9e705c7dcabff Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 23 Jul 2025 18:50:40 -0300 Subject: [PATCH 10/22] Do not sample entire committee when checking proposer --- .../src/core/libraries/rollup/StakingLib.sol | 8 +++ .../rollup/ValidatorSelectionLib.sol | 72 +++++++++++-------- l1-contracts/test/RollupGetters.t.sol | 29 ++++++++ 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/l1-contracts/src/core/libraries/rollup/StakingLib.sol b/l1-contracts/src/core/libraries/rollup/StakingLib.sol index 9ace29493909..b612f572da84 100644 --- a/l1-contracts/src/core/libraries/rollup/StakingLib.sol +++ b/l1-contracts/src/core/libraries/rollup/StakingLib.sol @@ -353,6 +353,14 @@ library StakingLib { ); } + function getAttesterFromIndexAtTime(uint256 _index, Timestamp _timestamp) + internal + view + returns (address) + { + return getStorage().gse.getAttesterFromIndexAtTime(address(this), _index, _timestamp); + } + function getAttestersFromIndicesAtTime(Timestamp _timestamp, uint256[] memory _indices) internal view diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index f84dfa233293..4632cb631756 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -219,7 +219,6 @@ library ValidatorSelectionLib { setCachedProposer(_slot, proposer, stack.proposerIndex); } - // Q: Do we still need to cache the proposer? function setCachedProposer(Slot _slot, address _proposer, uint256 _proposerIndex) internal { require( _proposerIndex <= type(uint96).max, @@ -230,25 +229,24 @@ library ValidatorSelectionLib { } function getProposerAt(Slot _slot) internal returns (address, uint256) { - (address cachedProposer, uint256 proposerIndex) = getCachedProposer(_slot); + (address cachedProposer, uint256 cachedProposerIndex) = getCachedProposer(_slot); if (cachedProposer != address(0)) { - return (cachedProposer, proposerIndex); + return (cachedProposer, cachedProposerIndex); } - // @note this is deliberately "bad" for the simple reason of code reduction. - // it does not need to actually return the full committee and then draw from it - // it can just return the proposer directly, but then we duplicate the code - // which we just don't have room for right now... Epoch epochNumber = _slot.epochFromSlot(); uint224 sampleSeed = getSampleSeed(epochNumber); - address[] memory committee = sampleValidators(epochNumber, sampleSeed); - if (committee.length == 0) { + (uint32 ts, uint256[] memory indices) = sampleValidatorsIndices(epochNumber, sampleSeed); + uint256 committeeSize = indices.length; + if (committeeSize == 0) { return (address(0), 0); } - - uint256 index = computeProposerIndex(epochNumber, _slot, sampleSeed, committee.length); - return (committee[index], index); + uint256 proposerIndex = computeProposerIndex(epochNumber, _slot, sampleSeed, committeeSize); + return ( + StakingLib.getAttesterFromIndexAtTime(indices[proposerIndex], Timestamp.wrap(ts)), + proposerIndex + ); } /** @@ -260,24 +258,7 @@ library ValidatorSelectionLib { * @return The validators for the given epoch */ function sampleValidators(Epoch _epoch, uint224 _seed) internal returns (address[] memory) { - ValidatorSelectionStorage storage store = getStorage(); - uint32 ts = epochToSampleTime(_epoch); - uint256 validatorSetSize = StakingLib.getAttesterCountAtTime(Timestamp.wrap(ts)); - uint256 targetCommitteeSize = store.targetCommitteeSize; - - require( - validatorSetSize >= targetCommitteeSize, - Errors.ValidatorSelection__InsufficientCommitteeSize(validatorSetSize, targetCommitteeSize) - ); - - if (targetCommitteeSize == 0) { - return new address[](0); - } - - // Sample the larger committee - uint256[] memory indices = - SampleLib.computeCommittee(targetCommitteeSize, validatorSetSize, _seed); - + (uint32 ts, uint256[] memory indices) = sampleValidatorsIndices(_epoch, _seed); return StakingLib.getAttestersFromIndicesAtTime(Timestamp.wrap(ts), indices); } @@ -420,6 +401,35 @@ library ValidatorSelectionLib { } } + /** + * @notice Samples a validator set for a specific epoch and returns their indices within the set. + * + * @dev Only used internally, should never be called for anything but the "next" epoch + * Allowing us to always use `lastSeed`. + * + * @return The sample time and the indices of the validators for the given epoch + */ + function sampleValidatorsIndices(Epoch _epoch, uint224 _seed) + internal + returns (uint32, uint256[] memory) + { + ValidatorSelectionStorage storage store = getStorage(); + uint32 ts = epochToSampleTime(_epoch); + uint256 validatorSetSize = StakingLib.getAttesterCountAtTime(Timestamp.wrap(ts)); + uint256 targetCommitteeSize = store.targetCommitteeSize; + + require( + validatorSetSize >= targetCommitteeSize, + Errors.ValidatorSelection__InsufficientCommitteeSize(validatorSetSize, targetCommitteeSize) + ); + + if (targetCommitteeSize == 0) { + return (ts, new uint256[](0)); + } + + return (ts, SampleLib.computeCommittee(targetCommitteeSize, validatorSetSize, _seed)); + } + /** * @notice Computes the nextSeed for an epoch * @@ -457,7 +467,7 @@ library ValidatorSelectionLib { * @return The index of the proposer */ function computeProposerIndex(Epoch _epoch, Slot _slot, uint256 _seed, uint256 _size) - private + internal pure returns (uint256) { diff --git a/l1-contracts/test/RollupGetters.t.sol b/l1-contracts/test/RollupGetters.t.sol index 3131b83e5105..24f1c732be0e 100644 --- a/l1-contracts/test/RollupGetters.t.sol +++ b/l1-contracts/test/RollupGetters.t.sol @@ -14,6 +14,7 @@ import {StakingQueueConfig} from "@aztec/core/libraries/compressed-data/StakingQ import {ValidatorSelectionTestBase} from "./validator-selection/ValidatorSelectionBase.sol"; import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributor.sol"; import {IBoosterCore} from "@aztec/core/reward-boost/RewardBooster.sol"; +import {ValidatorSelectionLib} from "@aztec/core/libraries/rollup/ValidatorSelectionLib.sol"; /** * Testing the things that should be getters are not updating state! @@ -103,6 +104,34 @@ contract RollupShouldBeGetters is ValidatorSelectionTestBase { assertEq(writes.length, 0, "No writes should be done"); } + // Checks that getProposerAt yields the same result as sampling the entire committee + // and then fetching the proposer from it given the proposer index. + function test_getProposerFromCommittee(uint16 _slot, bool _setup) external setup(4, 4) { + timeCheater.cheat__jumpForwardEpochs(2); + Slot s = Slot.wrap(timeCheater.currentSlot()) + Slot.wrap(_slot); + Timestamp t = timeCheater.slotToTimestamp(s); + + vm.warp(Timestamp.unwrap(t)); + + if (_setup) { + rollup.setupEpoch(); + } + + vm.record(); + + address proposer = rollup.getProposerAt(t); + + address[] memory committee = rollup.getCommitteeAt(t); + uint256 seed = rollup.getSampleSeedAt(t); + Epoch epoch = rollup.getEpochAt(t); + uint256 proposerIndex = ValidatorSelectionLib.computeProposerIndex(epoch, s, seed, 4); + + assertEq(proposer, committee[proposerIndex], "proposer should be the same"); + + (, bytes32[] memory writes) = vm.accesses(address(rollup)); + assertEq(writes.length, 0, "No writes should be done"); + } + function test_validateHeader() external setup(4, 4) { // Todo this one is a bit annoying here really. We need a lot of header information. } From 6b9a58482039671ffad790b3c3a37ba5250fb216 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 24 Jul 2025 15:21:49 -0300 Subject: [PATCH 11/22] Provide signers in calldata during propose --- l1-contracts/src/core/Rollup.sol | 4 +- l1-contracts/src/core/RollupCore.sol | 3 +- l1-contracts/src/core/interfaces/IRollup.sol | 2 + .../core/libraries/rollup/ExtRollupLib.sol | 8 +- .../src/core/libraries/rollup/ProposeLib.sol | 13 +++- .../rollup/ValidatorSelectionLib.sol | 43 +++++++++-- .../src/shared/libraries/SignatureLib.sol | 40 ++++++++++ l1-contracts/test/Rollup.t.sol | 14 ++-- l1-contracts/test/base/RollupBase.sol | 3 +- l1-contracts/test/benchmark/happy.t.sol | 20 +++-- .../test/compression/PreHeating.t.sol | 16 +++- l1-contracts/test/fees/FeeRollup.t.sol | 7 +- .../ValidatorSelection.t.sol | 75 ++++++++++++------- .../ValidatorSelectionBase.sol | 1 + 14 files changed, 193 insertions(+), 56 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index f5059e2fac94..b183aa9b4809 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -105,6 +105,7 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { function validateHeader( ProposedHeader calldata _header, CommitteeAttestations memory _attestations, + address[] calldata _signers, bytes32 _digest, bytes32 _blobsHash, BlockHeaderValidationFlags memory _flags @@ -118,7 +119,8 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { blobsHashesCommitment: _blobsHash, flags: _flags }), - _attestations + _attestations, + _signers ); } diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 630b2634e127..24476d0a057c 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -244,9 +244,10 @@ contract RollupCore is function propose( ProposeArgs calldata _args, CommitteeAttestations memory _attestations, + address[] calldata _signers, bytes calldata _blobInput ) external override(IRollupCore) { - ExtRollupLib.propose(_args, _attestations, _blobInput, checkBlob); + ExtRollupLib.propose(_args, _attestations, _signers, _blobInput, checkBlob); } function invalidateBadAttestation( diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 813ef3cca352..ec32e79f9567 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -119,6 +119,7 @@ interface IRollupCore { function propose( ProposeArgs calldata _args, CommitteeAttestations memory _attestations, + address[] memory _signers, bytes calldata _blobInput ) external; @@ -148,6 +149,7 @@ interface IRollup is IRollupCore, IHaveVersion { function validateHeader( ProposedHeader calldata _header, CommitteeAttestations memory _attestations, + address[] memory _signers, bytes32 _digest, bytes32 _blobsHash, BlockHeaderValidationFlags memory _flags diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index a9bc5bd106df..d8a0be0bb4a2 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -31,7 +31,8 @@ library ExtRollupLib { function validateHeader( ValidateHeaderArgs calldata _args, - CommitteeAttestations calldata _attestations + CommitteeAttestations calldata _attestations, + address[] calldata _signers ) external { ProposeLib.validateHeader(_args); if (_attestations.isEmpty()) { @@ -41,16 +42,17 @@ library ExtRollupLib { Slot slot = _args.header.slotNumber; Epoch epoch = slot.epochFromSlot(); ValidatorSelectionLib.verify(slot, epoch, _attestations, _args.digest); - ValidatorSelectionLib.verifyProposer(slot, _attestations, _args.digest); + ValidatorSelectionLib.verifyProposer(slot, epoch, _attestations, _signers, _args.digest); } function propose( ProposeArgs calldata _args, CommitteeAttestations memory _attestations, + address[] calldata _signers, bytes calldata _blobInput, bool _checkBlob ) external { - ProposeLib.propose(_args, _attestations, _blobInput, _checkBlob); + ProposeLib.propose(_args, _attestations, _signers, _blobInput, _checkBlob); } function prune() external { diff --git a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol index 9bf573aec6b4..915539dcac75 100644 --- a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol @@ -46,6 +46,7 @@ struct InterimProposeValues { bytes32 headerHash; bytes32 attestationsHash; bytes32 payloadDigest; + Epoch currentEpoch; } /** @@ -76,6 +77,7 @@ library ProposeLib { * * @param _args - The arguments to propose the block * @param _attestations - Signatures (or empty) from the validators + * @param _signers - The addresses of the signers from the attestations * Input _blobsInput bytes: * input[:1] - num blobs in block * input[1:] - blob commitments (48 bytes * num blobs in block) @@ -85,6 +87,7 @@ library ProposeLib { function propose( ProposeArgs calldata _args, CommitteeAttestations memory _attestations, + address[] memory _signers, bytes calldata _blobsInput, bool _checkBlob ) internal { @@ -103,8 +106,8 @@ library ProposeLib { ProposedHeader memory header = _args.header; v.headerHash = ProposedHeaderLib.hash(_args.header); - Epoch currentEpoch = Timestamp.wrap(block.timestamp).epochFromTimestamp(); - ValidatorSelectionLib.setupEpoch(currentEpoch); + v.currentEpoch = Timestamp.wrap(block.timestamp).epochFromTimestamp(); + ValidatorSelectionLib.setupEpoch(v.currentEpoch); ManaBaseFeeComponents memory components = getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true); @@ -128,14 +131,16 @@ library ProposeLib { }) ); - ValidatorSelectionLib.verifyProposer(header.slotNumber, _attestations, v.payloadDigest); + ValidatorSelectionLib.verifyProposer( + header.slotNumber, v.currentEpoch, _attestations, _signers, v.payloadDigest + ); RollupStore storage rollupStore = STFLib.getStorage(); uint256 blockNumber = rollupStore.tips.getPendingBlockNumber() + 1; // Blob commitments are collected and proven per root rollup proof (=> per epoch), so we need to know whether we are at the epoch start: bool isFirstBlockOfEpoch = - currentEpoch > STFLib.getEpochForBlock(blockNumber - 1) || blockNumber == 1; + v.currentEpoch > STFLib.getEpochForBlock(blockNumber - 1) || blockNumber == 1; bytes32 blobCommitmentsHash = BlobLib.calculateBlobCommitmentsHash( STFLib.getBlobCommitmentsHash(blockNumber - 1), v.blobCommitments, isFirstBlockOfEpoch ); diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index 4632cb631756..b268a877b9f8 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -88,14 +88,45 @@ library ValidatorSelectionLib { * * @param _slot - The slot of the block being proposed * @param _attestations - The committee attestations for the block proposal + * @param _signers - The addresses of the signers from the attestations * @param _digest - The digest of the block being proposed */ - function verifyProposer(Slot _slot, CommitteeAttestations memory _attestations, bytes32 _digest) - internal - { - // If there is no committee for the epoch, or the proposer is who sent the tx, we're good - (address proposer, uint256 proposerIndex) = getProposerAt(_slot); - if (proposer == address(0) || proposer == msg.sender) { + function verifyProposer( + Slot _slot, + Epoch _epochNumber, + CommitteeAttestations memory _attestations, + address[] memory _signers, + bytes32 _digest + ) internal { + // Load the committee commitment for the epoch + (bytes32 committeeCommitment, uint256 committeeSize) = getCommitteeCommitmentAt(_epochNumber); + + // If the target committee size is 0, we skip the validation + if (committeeSize == 0) { + return; + } + + // Reconstruct the committee from the attestations and signers + address[] memory committee = + _attestations.reconstructCommitteeFromSigners(_signers, committeeSize); + + // Check it matches the expected one + bytes32 reconstructedCommitment = computeCommitteeCommitment(committee); + if (reconstructedCommitment != committeeCommitment) { + revert Errors.ValidatorSelection__InvalidCommitteeCommitment( + reconstructedCommitment, committeeCommitment + ); + } + + // Get the proposer from the committee based on the epoch, slot, and sample seed + uint224 sampleSeed = getSampleSeed(_epochNumber); + uint256 proposerIndex = computeProposerIndex(_epochNumber, _slot, sampleSeed, committeeSize); + address proposer = committee[proposerIndex]; + + setCachedProposer(_slot, proposer, proposerIndex); + + // If the proposer is who sent the tx, we're good + if (proposer == msg.sender) { return; } diff --git a/l1-contracts/src/shared/libraries/SignatureLib.sol b/l1-contracts/src/shared/libraries/SignatureLib.sol index 9ca63018d6dc..db7f5171c37d 100644 --- a/l1-contracts/src/shared/libraries/SignatureLib.sol +++ b/l1-contracts/src/shared/libraries/SignatureLib.sol @@ -107,6 +107,46 @@ library SignatureLib { return Signature({v: v, r: r, s: s}); } + /** + * Returns the addresses from the CommitteeAttestations, using the array of signers to populate where there are signatures. + * Indices with signatures will have a zero address. + * @param _attestations - The committee attestations + * @param _length - The number of addresses to return, should match the number of committee members + */ + function reconstructCommitteeFromSigners( + CommitteeAttestations memory _attestations, + address[] memory _signers, + uint256 _length + ) internal pure returns (address[] memory) { + bytes memory signaturesOrAddresses = _attestations.signaturesOrAddresses; + address[] memory addresses = new address[](_length); + + uint256 signersIndex; + uint256 dataPtr; + + assembly { + // Skip length + dataPtr := add(signaturesOrAddresses, 0x20) + } + + for (uint256 i = 0; i < _length; ++i) { + if (isSignature(_attestations, i)) { + dataPtr += 65; + addresses[i] = _signers[signersIndex]; + signersIndex++; + } else { + address addr; + assembly { + addr := shr(96, mload(dataPtr)) + dataPtr := add(dataPtr, 20) + } + addresses[i] = addr; + } + } + + return addresses; + } + /** * @notice Verifies a signature, throws if the signature is invalid or empty * diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 0deb565bc7b2..f4589b78f038 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -250,7 +250,7 @@ contract RollupTest is RollupBase { vm.expectRevert( abi.encodeWithSelector(Errors.Rollup__InvalidBlobHash.selector, blobHashes[0], realBlobHash) ); - rollup.propose(args, SignatureLib.packAttestations(attestations), data.blobCommitments); + rollup.propose(args, SignatureLib.packAttestations(attestations), signers, data.blobCommitments); } function testExtraBlobs() public setUpFor("mixed_block_1") { @@ -332,7 +332,7 @@ contract RollupTest is RollupBase { stateReference: EMPTY_STATE_REFERENCE, oracleInput: OracleInput(0) }); - rollup.propose(args, SignatureLib.packAttestations(attestations), data.blobCommitments); + rollup.propose(args, SignatureLib.packAttestations(attestations), signers, data.blobCommitments); } function testInvalidL2Fee() public setUpFor("mixed_block_1") { @@ -360,7 +360,7 @@ contract RollupTest is RollupBase { stateReference: EMPTY_STATE_REFERENCE, oracleInput: OracleInput(0) }); - rollup.propose(args, SignatureLib.packAttestations(attestations), data.blobCommitments); + rollup.propose(args, SignatureLib.packAttestations(attestations), signers, data.blobCommitments); } function testProvingFeeUpdates() public setUpFor("mixed_block_1") { @@ -471,7 +471,9 @@ contract RollupTest is RollupBase { stateReference: EMPTY_STATE_REFERENCE, oracleInput: OracleInput(0) }); - rollup.propose(args, SignatureLib.packAttestations(attestations), data.blobCommitments); + rollup.propose( + args, SignatureLib.packAttestations(attestations), signers, data.blobCommitments + ); assertEq(testERC20.balanceOf(header.coinbase), 0, "invalid coinbase balance"); } @@ -729,7 +731,7 @@ contract RollupTest is RollupBase { stateReference: EMPTY_STATE_REFERENCE, oracleInput: OracleInput(0) }); - rollup.propose(args, SignatureLib.packAttestations(attestations), new bytes(144)); + rollup.propose(args, SignatureLib.packAttestations(attestations), signers, new bytes(144)); } function testRevertInvalidCoinbase() public setUpFor("empty_block_1") { @@ -752,7 +754,7 @@ contract RollupTest is RollupBase { stateReference: EMPTY_STATE_REFERENCE, oracleInput: OracleInput(0) }); - rollup.propose(args, SignatureLib.packAttestations(attestations), new bytes(144)); + rollup.propose(args, SignatureLib.packAttestations(attestations), signers, new bytes(144)); } function testSubmitProofNonExistentBlock() public setUpFor("empty_block_1") { diff --git a/l1-contracts/test/base/RollupBase.sol b/l1-contracts/test/base/RollupBase.sol index 3c206172af4f..dc7a0504114d 100644 --- a/l1-contracts/test/base/RollupBase.sol +++ b/l1-contracts/test/base/RollupBase.sol @@ -36,6 +36,7 @@ contract RollupBase is DecoderBase { MerkleTestUtil internal merkleTestUtil = new MerkleTestUtil(); CommitteeAttestation[] internal attestations; + address[] internal signers; mapping(uint256 => uint256) internal blockFees; @@ -204,7 +205,7 @@ contract RollupBase is DecoderBase { if (_revertMsg.length > 0) { vm.expectRevert(_revertMsg); } - rollup.propose(args, SignatureLib.packAttestations(attestations), blobCommitments); + rollup.propose(args, SignatureLib.packAttestations(attestations), signers, blobCommitments); if (_revertMsg.length > 0) { return; diff --git a/l1-contracts/test/benchmark/happy.t.sol b/l1-contracts/test/benchmark/happy.t.sol index 0884fc55fca1..4b07502514b2 100644 --- a/l1-contracts/test/benchmark/happy.t.sol +++ b/l1-contracts/test/benchmark/happy.t.sol @@ -117,6 +117,7 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { ProposeArgs proposeArgs; bytes blobInputs; CommitteeAttestation[] attestations; + address[] signers; } DecoderBase.Full full = load("single_tx_block_1"); @@ -249,11 +250,13 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { }); CommitteeAttestation[] memory attestations; + address[] memory signers; { address[] memory validators = rollup.getEpochCommittee(rollup.getCurrentEpoch()); uint256 needed = validators.length * 2 / 3 + 1; attestations = new CommitteeAttestation[](validators.length); + signers = new address[](needed); bytes32 headerHash = ProposedHeaderLib.hash(proposeArgs.header); @@ -273,15 +276,19 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { } } - // loop through again to get to the required number of attestations. + // loop to get to the required number of attestations. // yes, inefficient, but it's simple, clear, and is a test. uint256 sigCount = 1; + uint256 signersIndex = 0; for (uint256 i = 0; i < validators.length; i++) { if (validators[i] == proposer) { - continue; + signers[signersIndex] = validators[i]; + signersIndex++; } else if (sigCount < needed) { attestations[i] = createAttestation(validators[i], digest); + signers[signersIndex] = validators[i]; sigCount++; + signersIndex++; } else { attestations[i] = createEmptyAttestation(validators[i]); } @@ -291,7 +298,8 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { return Block({ proposeArgs: proposeArgs, blobInputs: full.block.blobCommitments, - attestations: attestations + attestations: attestations, + signers: signers }); } @@ -383,7 +391,7 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { target: address(rollup), callData: abi.encodeCall( rollup.propose, - (b.proposeArgs, SignatureLib.packAttestations(b.attestations), b.blobInputs) + (b.proposeArgs, SignatureLib.packAttestations(b.attestations), b.signers, b.blobInputs) ), allowFailure: false }); @@ -395,7 +403,9 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { multicall.aggregate3(calls); } else { vm.prank(proposer); - rollup.propose(b.proposeArgs, SignatureLib.packAttestations(b.attestations), b.blobInputs); + rollup.propose( + b.proposeArgs, SignatureLib.packAttestations(b.attestations), b.signers, b.blobInputs + ); } nextSlot = nextSlot + Slot.wrap(1); diff --git a/l1-contracts/test/compression/PreHeating.t.sol b/l1-contracts/test/compression/PreHeating.t.sol index e6d5006287a6..1cd04b29322b 100644 --- a/l1-contracts/test/compression/PreHeating.t.sol +++ b/l1-contracts/test/compression/PreHeating.t.sol @@ -118,6 +118,7 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { ProposeArgs proposeArgs; bytes blobInputs; CommitteeAttestation[] attestations; + address[] signers; } DecoderBase.Full full = load("empty_block_1"); @@ -233,7 +234,9 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { blockAttestations[currentBlockNumber] = SignatureLib.packAttestations(b.attestations); vm.prank(proposer); - rollup.propose(b.proposeArgs, SignatureLib.packAttestations(b.attestations), b.blobInputs); + rollup.propose( + b.proposeArgs, SignatureLib.packAttestations(b.attestations), b.signers, b.blobInputs + ); nextSlot = nextSlot + Slot.wrap(1); } @@ -336,11 +339,13 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { }); CommitteeAttestation[] memory attestations; + address[] memory signers; { address[] memory validators = rollup.getEpochCommittee(rollup.getCurrentEpoch()); uint256 needed = validators.length * 2 / 3 + 1; attestations = new CommitteeAttestation[](validators.length); + signers = new address[](needed); bytes32 headerHash = ProposedHeaderLib.hash(proposeArgs.header); @@ -363,12 +368,16 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { // loop through again to get to the required number of attestations. // yes, inefficient, but it's simple, clear, and is a test. uint256 sigCount = 1; + uint256 signersIndex = 0; for (uint256 i = 0; i < validators.length; i++) { if (validators[i] == proposer) { - continue; + signers[signersIndex] = validators[i]; + signersIndex++; } else if (sigCount < needed) { attestations[i] = createAttestation(validators[i], digest); + signers[signersIndex] = validators[i]; sigCount++; + signersIndex++; } else { attestations[i] = createEmptyAttestation(validators[i]); } @@ -378,7 +387,8 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { return Block({ proposeArgs: proposeArgs, blobInputs: full.block.blobCommitments, - attestations: attestations + attestations: attestations, + signers: signers }); } diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index 4c0b6998a3b8..c0ba794edea1 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -81,6 +81,7 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { bytes body; bytes blobInputs; CommitteeAttestation[] attestations; + address[] signers; } DecoderBase.Full full = load("empty_block_1"); @@ -138,6 +139,7 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { bytes32 archiveRoot = bytes32(Constants.GENESIS_ARCHIVE_ROOT); CommitteeAttestation[] memory attestations = new CommitteeAttestation[](0); + address[] memory signers = new address[](0); bytes memory body = full.block.body; ProposedHeader memory header = full.block.header; @@ -178,7 +180,8 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { header: header, body: body, blobInputs: full.block.blobCommitments, - attestations: attestations + attestations: attestations, + signers: signers }); } @@ -203,6 +206,7 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { }) }), SignatureLib.packAttestations(b.attestations), + b.signers, b.blobInputs ); nextSlot = nextSlot + Slot.wrap(1); @@ -297,6 +301,7 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { }) }), SignatureLib.packAttestations(b.attestations), + b.signers, b.blobInputs ); diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 841fb55712d8..94906d7e2d1e 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -51,18 +51,28 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { bool senderIsNotProposer; bool proposerAttestationNotProvided; bool invalidAttestation; + bool invalidSigners; } TestFlags NO_FLAGS = TestFlags({ senderIsNotProposer: false, proposerAttestationNotProvided: false, - invalidAttestation: false + invalidAttestation: false, + invalidSigners: false }); TestFlags INVALID_ATTESTATION = TestFlags({ senderIsNotProposer: false, proposerAttestationNotProvided: false, - invalidAttestation: true + invalidAttestation: true, + invalidSigners: false + }); + + TestFlags INVALID_SIGNERS = TestFlags({ + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: false, + invalidSigners: true }); bytes4 NO_REVERT = bytes4(0); @@ -182,17 +192,8 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { uint256 signatureCount = committeeSize * 2 / 3 + (_insufficientSigs ? 0 : 1); assertGt(rollup.getAttesters().length, committeeSize, "Not enough validators"); - ProposeTestData memory ree = _testBlock( - "mixed_block_1", - NO_REVERT, - signatureCount, - committeeSize, - TestFlags({ - senderIsNotProposer: false, - proposerAttestationNotProvided: false, - invalidAttestation: false - }) - ); + ProposeTestData memory ree = + _testBlock("mixed_block_1", NO_REVERT, signatureCount, committeeSize, NO_FLAGS); assertEq(ree.committee.length, rollup.getTargetCommitteeSize(), "Invalid committee size"); @@ -264,7 +265,8 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { TestFlags({ senderIsNotProposer: true, proposerAttestationNotProvided: false, - invalidAttestation: false + invalidAttestation: false, + invalidSigners: false }) ); } @@ -278,7 +280,23 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { TestFlags({ senderIsNotProposer: true, proposerAttestationNotProvided: true, - invalidAttestation: false + invalidAttestation: false, + invalidSigners: false + }) + ); + } + + function testInvalidSigners() public setup(4, 4) progressEpochs(2) { + _testBlock( + "mixed_block_1", + Errors.ValidatorSelection__InvalidCommitteeCommitment.selector, + 3, + 4, + TestFlags({ + senderIsNotProposer: true, + proposerAttestationNotProvided: false, + invalidAttestation: false, + invalidSigners: true }) ); } @@ -291,12 +309,6 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { _invalidateByAttestationSig(ree, 0, NO_REVERT); } - function testInsufficientAttestations() public setup(4, 4) progressEpochs(2) { - ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 2, 2, NO_FLAGS); - - _invalidateByAttestationCount(ree, NO_REVERT); - } - function testInsufficientSignatures() public setup(4, 4) progressEpochs(2) { ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 2, 4, NO_FLAGS); @@ -352,7 +364,8 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { TestFlags({ senderIsNotProposer: false, proposerAttestationNotProvided: false, - invalidAttestation: false + invalidAttestation: false, + invalidSigners: false }) ); } @@ -448,6 +461,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { } ree.attestations = new CommitteeAttestation[](ree.attestationsCount); + ree.signers = new address[](_signatureCount); bytes32 digest = ProposeLib.digest(ree.proposePayload); { @@ -457,10 +471,11 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { (signaturesCollected >= _signatureCount) || (ree.committee[i] == ree.proposer && _flags.proposerAttestationNotProvided) ) { - ree.attestations[i] = _createEmptyAttestation(ree.proposer); + ree.attestations[i] = _createEmptyAttestation(ree.committee[i]); } else { - signaturesCollected++; ree.attestations[i] = _createAttestation(ree.committee[i], digest); + ree.signers[signaturesCollected] = ree.committee[i]; + signaturesCollected++; } } } @@ -477,6 +492,13 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { ree.attestations[0] = _createAttestation(invalidAttester, digest); } + if (_flags.invalidSigners) { + // Change the first element in the signers to a random address + uint256 invalidSignerKey = uint256(keccak256(abi.encode("invalid", block.timestamp))); + address invalidSigner = vm.addr(invalidSignerKey); + ree.signers[0] = invalidSigner; + } + emit log("Time to propose"); if (_revertData != NO_REVERT) { vm.expectPartialRevert(_revertData); @@ -484,7 +506,10 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { vm.prank(ree.sender); rollup.propose( - ree.proposeArgs, SignatureLib.packAttestations(ree.attestations), full.block.blobCommitments + ree.proposeArgs, + SignatureLib.packAttestations(ree.attestations), + ree.signers, + full.block.blobCommitments ); if (_revertData != NO_REVERT) { diff --git a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol index a4df85447d9f..6c812f5fa66c 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol @@ -45,6 +45,7 @@ contract ValidatorSelectionTestBase is DecoderBase { uint256 attestationsCount; address[] committee; CommitteeAttestation[] attestations; + address[] signers; ProposePayload proposePayload; ProposeArgs proposeArgs; } From 57d5d442c32a13ef1acff7a5402390fb1ce89fb5 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 24 Jul 2025 15:40:29 -0300 Subject: [PATCH 12/22] Optimize reconstructCommitteeFromSigners --- .../src/shared/libraries/SignatureLib.sol | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/l1-contracts/src/shared/libraries/SignatureLib.sol b/l1-contracts/src/shared/libraries/SignatureLib.sol index db7f5171c37d..16d71d623363 100644 --- a/l1-contracts/src/shared/libraries/SignatureLib.sol +++ b/l1-contracts/src/shared/libraries/SignatureLib.sol @@ -119,10 +119,13 @@ library SignatureLib { uint256 _length ) internal pure returns (address[] memory) { bytes memory signaturesOrAddresses = _attestations.signaturesOrAddresses; + bytes memory signatureIndices = _attestations.signatureIndices; address[] memory addresses = new address[](_length); uint256 signersIndex; uint256 dataPtr; + uint256 currentByte; + uint256 bitMask; assembly { // Skip length @@ -130,7 +133,17 @@ library SignatureLib { } for (uint256 i = 0; i < _length; ++i) { - if (isSignature(_attestations, i)) { + // Load new byte every 8 iterations + if (i % 8 == 0) { + uint256 byteIndex = i / 8; + currentByte = uint8(signatureIndices[byteIndex]); + bitMask = 128; // 0b10000000 + } + + bool isSignatureFlag = (currentByte & bitMask) != 0; + bitMask >>= 1; + + if (isSignatureFlag) { dataPtr += 65; addresses[i] = _signers[signersIndex]; signersIndex++; From c670d46565332a5c6937dc7d09a7f267d10aa1e5 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 24 Jul 2025 16:12:24 -0300 Subject: [PATCH 13/22] Add signers to propose in ts-land --- yarn-project/archiver/src/archiver/archiver.test.ts | 1 + yarn-project/archiver/src/archiver/data_retrieval.ts | 3 ++- .../src/integration/integration_l1_publisher.test.ts | 1 + yarn-project/ethereum/src/contracts/rollup.ts | 1 + .../src/publisher/sequencer-publisher.test.ts | 1 + .../src/publisher/sequencer-publisher.ts | 12 ++++++++++++ 6 files changed, 18 insertions(+), 1 deletion(-) diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index 4d7e3ffa9b60..7496b1302ee2 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -821,6 +821,7 @@ async function makeRollupTx(l2Block: L2Block) { oracleInput: { feeAssetPriceModifier: 0n }, }, RollupContract.packAttestations([]), + [], blobInput, ], }); diff --git a/yarn-project/archiver/src/archiver/data_retrieval.ts b/yarn-project/archiver/src/archiver/data_retrieval.ts index fa2aa668feac..cad3127aabd7 100644 --- a/yarn-project/archiver/src/archiver/data_retrieval.ts +++ b/yarn-project/archiver/src/archiver/data_retrieval.ts @@ -311,7 +311,7 @@ async function getBlockFromRollupTx( throw new Error(`Unexpected rollup method called ${rollupFunctionName}`); } - const [decodedArgs, attestations, _blobInput] = rollupArgs! as readonly [ + const [decodedArgs, attestations, _signers, _blobInput] = rollupArgs! as readonly [ { archive: Hex; stateReference: ViemStateReference; @@ -322,6 +322,7 @@ async function getBlockFromRollupTx( txHashes: readonly Hex[]; }, ViemCommitteeAttestations, + Hex[], Hex, ]; diff --git a/yarn-project/end-to-end/src/integration/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/integration/integration_l1_publisher.test.ts index 2b496310b6e9..5fa2857b3d70 100644 --- a/yarn-project/end-to-end/src/integration/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/integration/integration_l1_publisher.test.ts @@ -480,6 +480,7 @@ describe('L1Publisher integration', () => { }, }, RollupContract.packAttestations([]), + [], Blob.getPrefixedEthBlobCommitments(blockBlobs), ], }); diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index 08179c3eb32f..953f8c464d00 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -391,6 +391,7 @@ export class RollupContract { args: readonly [ ViemHeader, ViemCommitteeAttestations, + `0x${string}`[], `0x${string}`, `0x${string}`, { diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts index beab4f26c6e8..d3e7dd882cbd 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts @@ -260,6 +260,7 @@ describe('SequencerPublisher', () => { txHashes: [], }, RollupContract.packAttestations([]), + [], blobInput, ] as const; expect(forwardSpy).toHaveBeenCalledWith( diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index 097c673c4a48..645f2f88499e 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -331,6 +331,7 @@ export class SequencerPublisher { const args = [ header.toViem(), RollupContract.packAttestations([]), + [], // no signers `0x${'0'.repeat(64)}`, // 32 empty bytes header.contentCommitment.blobsHash.toString(), flags, @@ -394,6 +395,9 @@ export class SequencerPublisher { const blobInput = Blob.getPrefixedEthBlobCommitments(blobs); const formattedAttestations = attestationData.attestations.map(attest => attest.toViem()); + const signers = attestationData.attestations + .filter(attest => !attest.signature.isEmpty()) + .map(attest => attest.address.toString()); const args = [ { @@ -406,6 +410,7 @@ export class SequencerPublisher { }, }, RollupContract.packAttestations(formattedAttestations), + signers, blobInput, ] as const; @@ -645,6 +650,11 @@ export class SequencerPublisher { const attestations = encodedData.attestations ? encodedData.attestations.map(attest => attest.toViem()) : []; const txHashes = encodedData.txHashes ? encodedData.txHashes.map(txHash => txHash.toString()) : []; + + const signers = encodedData.attestations + ?.filter(attest => !attest.signature.isEmpty()) + .map(attest => attest.address.toString()); + const args = [ { header: encodedData.header.toViem(), @@ -657,6 +667,7 @@ export class SequencerPublisher { txHashes, }, RollupContract.packAttestations(attestations), + signers ?? [], blobInput, ] as const; @@ -683,6 +694,7 @@ export class SequencerPublisher { }; }, ViemCommitteeAttestations, + `0x${string}`[], `0x${string}`, ], timestamp: bigint, From 5b29e1264eabf1a8f57e3e5c857e4b5c4c279c54 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 24 Jul 2025 16:24:20 -0300 Subject: [PATCH 14/22] Enter ExtRollupLib3 --- l1-contracts/src/core/RollupCore.sol | 5 +-- .../core/libraries/rollup/ExtRollupLib2.sol | 29 --------------- .../core/libraries/rollup/ExtRollupLib3.sol | 36 +++++++++++++++++++ yarn-project/ethereum/src/l1_artifacts.ts | 7 ++++ .../scripts/generate-artifacts.sh | 1 + 5 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 l1-contracts/src/core/libraries/rollup/ExtRollupLib3.sol diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 24476d0a057c..fb706de4cadd 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -20,6 +20,7 @@ import {CommitteeAttestations} from "@aztec/shared/libraries/SignatureLib.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {ExtRollupLib} from "@aztec/core/libraries/rollup/ExtRollupLib.sol"; import {ExtRollupLib2} from "@aztec/core/libraries/rollup/ExtRollupLib2.sol"; +import {ExtRollupLib3} from "@aztec/core/libraries/rollup/ExtRollupLib3.sol"; import {EthValue, FeeLib} from "@aztec/core/libraries/rollup/FeeLib.sol"; import {ProposeArgs} from "@aztec/core/libraries/rollup/ProposeLib.sol"; import {STFLib, GenesisState} from "@aztec/core/libraries/rollup/STFLib.sol"; @@ -86,7 +87,7 @@ contract RollupCore is ); Timestamp exitDelay = Timestamp.wrap(_config.exitDelaySeconds); - ISlasher slasher = ExtRollupLib2.deploySlasher( + ISlasher slasher = ExtRollupLib3.deploySlasher( _config.slashingQuorum, _config.slashingRoundSize, _config.slashingLifetimeInRounds, @@ -101,7 +102,7 @@ contract RollupCore is // If no booster specifically provided deploy one. if (address(_config.rewardConfig.booster) == address(0)) { - _config.rewardConfig.booster = ExtRollupLib2.deployRewardBooster(_config.rewardBoostConfig); + _config.rewardConfig.booster = ExtRollupLib3.deployRewardBooster(_config.rewardBoostConfig); } RewardLib.setConfig(_config.rewardConfig); diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol index a50c3dfb8cae..13c91e505f72 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol @@ -9,39 +9,10 @@ import {StakingLib} from "./StakingLib.sol"; import {InvalidateLib} from "./InvalidateLib.sol"; import {ValidatorSelectionLib} from "./ValidatorSelectionLib.sol"; import {CommitteeAttestations} from "@aztec/shared/libraries/SignatureLib.sol"; -import { - RewardBooster, - RewardBoostConfig, - IBoosterCore, - IValidatorSelection -} from "@aztec/core/reward-boost/RewardBooster.sol"; -import {Slasher, ISlasher} from "@aztec/core/slashing/Slasher.sol"; library ExtRollupLib2 { using TimeLib for Timestamp; - function deployRewardBooster(RewardBoostConfig memory _config) external returns (IBoosterCore) { - RewardBooster booster = new RewardBooster(IValidatorSelection(address(this)), _config); - return IBoosterCore(address(booster)); - } - - function deploySlasher( - uint256 _slashingQuorum, - uint256 _slashingRoundSize, - uint256 _slashingLifetimeInRounds, - uint256 _slashingExecutionDelayInRounds, - address _slashingVetoer - ) external returns (ISlasher) { - Slasher slasher = new Slasher( - _slashingQuorum, - _slashingRoundSize, - _slashingLifetimeInRounds, - _slashingExecutionDelayInRounds, - _slashingVetoer - ); - return ISlasher(address(slasher)); - } - function setSlasher(address _slasher) external { StakingLib.setSlasher(_slasher); } diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib3.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib3.sol new file mode 100644 index 000000000000..f20ea4f8579b --- /dev/null +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib3.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +// solhint-disable imports-order +pragma solidity >=0.8.27; + +import { + RewardBooster, + RewardBoostConfig, + IBoosterCore, + IValidatorSelection +} from "@aztec/core/reward-boost/RewardBooster.sol"; +import {Slasher, ISlasher} from "@aztec/core/slashing/Slasher.sol"; + +library ExtRollupLib3 { + function deployRewardBooster(RewardBoostConfig memory _config) external returns (IBoosterCore) { + RewardBooster booster = new RewardBooster(IValidatorSelection(address(this)), _config); + return IBoosterCore(address(booster)); + } + + function deploySlasher( + uint256 _slashingQuorum, + uint256 _slashingRoundSize, + uint256 _slashingLifetimeInRounds, + uint256 _slashingExecutionDelayInRounds, + address _slashingVetoer + ) external returns (ISlasher) { + Slasher slasher = new Slasher( + _slashingQuorum, + _slashingRoundSize, + _slashingLifetimeInRounds, + _slashingExecutionDelayInRounds, + _slashingVetoer + ); + return ISlasher(address(slasher)); + } +} diff --git a/yarn-project/ethereum/src/l1_artifacts.ts b/yarn-project/ethereum/src/l1_artifacts.ts index cb4e51583f08..dfad41f889c6 100644 --- a/yarn-project/ethereum/src/l1_artifacts.ts +++ b/yarn-project/ethereum/src/l1_artifacts.ts @@ -3,6 +3,8 @@ import { CoinIssuerBytecode, ExtRollupLib2Abi, ExtRollupLib2Bytecode, + ExtRollupLib3Abi, + ExtRollupLib3Bytecode, ExtRollupLibAbi, ExtRollupLibBytecode, FeeAssetHandlerAbi, @@ -88,6 +90,11 @@ export const RollupArtifact = { contractAbi: ExtRollupLib2Abi, contractBytecode: ExtRollupLib2Bytecode as Hex, }, + ExtRollupLib3: { + name: 'ExtRollupLib3', + contractAbi: ExtRollupLib3Abi, + contractBytecode: ExtRollupLib3Bytecode as Hex, + }, }, }, }; diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index f81836ddb6e4..fe3ad20058d3 100755 --- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh +++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh @@ -15,6 +15,7 @@ contracts=( "EmpireBase" "ExtRollupLib" "ExtRollupLib2" + "ExtRollupLib3" "FeeJuicePortal" "Governance" "GovernanceProposer" From ca3894e889ac7c276faaa3321761dcdd8a4fea48 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 24 Jul 2025 16:30:15 -0300 Subject: [PATCH 15/22] Fix bad rebase --- .../end-to-end/src/integration/integration_l1_publisher.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/src/integration/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/integration/integration_l1_publisher.test.ts index 5fa2857b3d70..df9d37c3fda3 100644 --- a/yarn-project/end-to-end/src/integration/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/integration/integration_l1_publisher.test.ts @@ -575,7 +575,7 @@ describe('L1Publisher integration', () => { const result = await publisher.sendRequests(); expect(result!.successfulActions).toEqual(['propose']); - expect(result!.failedActions).toEqual(['slashing-vote']); + expect(result!.failedActions).toEqual(['slashing-signal']); }); it(`shows propose custom errors if tx simulation fails`, async () => { From c8066b600d57d00a7f3c782f2ca2c50e159b8c10 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 24 Jul 2025 17:09:39 -0300 Subject: [PATCH 16/22] Lint --- .../src/core/libraries/rollup/ValidatorSelectionLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index b268a877b9f8..7422932f1387 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -441,7 +441,7 @@ library ValidatorSelectionLib { * @return The sample time and the indices of the validators for the given epoch */ function sampleValidatorsIndices(Epoch _epoch, uint224 _seed) - internal + private returns (uint32, uint256[] memory) { ValidatorSelectionStorage storage store = getStorage(); From 6543447c0754b797f9f0b83c8eca079e6684ff29 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 24 Jul 2025 17:32:54 -0300 Subject: [PATCH 17/22] Solhint --- .../rollup/ValidatorSelectionLib.sol | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index 7422932f1387..ef56ae47fbc3 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -432,6 +432,24 @@ library ValidatorSelectionLib { } } + /** + * @notice Computes the index of the committee member that acts as proposer for a given slot + * + * @param _epoch - The epoch to compute the proposer index for + * @param _slot - The slot to compute the proposer index for + * @param _seed - The seed to use for the computation + * @param _size - The size of the committee + * + * @return The index of the proposer + */ + function computeProposerIndex(Epoch _epoch, Slot _slot, uint256 _seed, uint256 _size) + internal + pure + returns (uint256) + { + return uint256(keccak256(abi.encode(_epoch, _slot, _seed))) % _size; + } + /** * @notice Samples a validator set for a specific epoch and returns their indices within the set. * @@ -486,22 +504,4 @@ library ValidatorSelectionLib { function computeCommitteeCommitment(address[] memory _committee) private pure returns (bytes32) { return keccak256(abi.encode(_committee)); } - - /** - * @notice Computes the index of the committee member that acts as proposer for a given slot - * - * @param _epoch - The epoch to compute the proposer index for - * @param _slot - The slot to compute the proposer index for - * @param _seed - The seed to use for the computation - * @param _size - The size of the committee - * - * @return The index of the proposer - */ - function computeProposerIndex(Epoch _epoch, Slot _slot, uint256 _seed, uint256 _size) - internal - pure - returns (uint256) - { - return uint256(keccak256(abi.encode(_epoch, _slot, _seed))) % _size; - } } From 3ed72013b61468c0b032d77d65eb4fc9d0dc77f7 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 24 Jul 2025 17:43:35 -0300 Subject: [PATCH 18/22] Add test for submitEpochProof --- .../ValidatorSelection.t.sol | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 94906d7e2d1e..42b1e7b66e12 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -3,6 +3,7 @@ // solhint-disable imports-order pragma solidity >=0.8.27; +import {Strings} from "@oz/utils/Strings.sol"; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; import { Signature, @@ -37,6 +38,10 @@ import {ValidatorSelectionTestBase} from "./ValidatorSelectionBase.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; +import { + BlockLog, PublicInputArgs, SubmitEpochRootProofArgs +} from "@aztec/core/interfaces/IRollup.sol"; + // solhint-disable comprehensive-interface /** @@ -209,6 +214,34 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { _testBlock("mixed_block_2", NO_REVERT, 3, 4, NO_FLAGS); } + function testProveWithAttestations() public setup(4, 4) progressEpochs(2) { + _testBlock("mixed_block_1", NO_REVERT, 3, 4, NO_FLAGS); + ProposeTestData memory ree2 = _testBlock("mixed_block_2", NO_REVERT, 3, 4, NO_FLAGS); + uint256 blockNumber = rollup.getPendingBlockNumber(); + + _proveBlocks( + "mixed_block_", + blockNumber - 1, + blockNumber, + SignatureLib.packAttestations(ree2.attestations), + NO_REVERT + ); + } + + function testProveFailWithoutCorrectAttestations() public setup(4, 4) progressEpochs(2) { + ProposeTestData memory ree1 = _testBlock("mixed_block_1", NO_REVERT, 3, 4, NO_FLAGS); + _testBlock("mixed_block_2", NO_REVERT, 3, 4, NO_FLAGS); + uint256 blockNumber = rollup.getPendingBlockNumber(); + + _proveBlocks( + "mixed_block_", + blockNumber - 1, + blockNumber, + SignatureLib.packAttestations(ree1.attestations), + Errors.Rollup__InvalidAttestations.selector + ); + } + function testCannotInvalidateProperProposal() public setup(4, 4) progressEpochs(2) { ProposeTestData memory ree = _testBlock("mixed_block_1", NO_REVERT, 3, 4, NO_FLAGS); _invalidateByAttestationCount(ree, Errors.ValidatorSelection__InsufficientAttestations.selector); @@ -567,6 +600,51 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { } } + function _proveBlocks( + string memory _name, + uint256 _start, + uint256 _end, + CommitteeAttestations memory _attestations, + bytes4 _revertData + ) internal { + // Logic is mostly duplicated from RollupBase._proveBlocks + DecoderBase.Full memory startFull = load(string.concat(_name, Strings.toString(_start))); + DecoderBase.Full memory endFull = load(string.concat(_name, Strings.toString(_end))); + + uint256 startBlockNumber = uint256(startFull.block.blockNumber); + uint256 endBlockNumber = uint256(endFull.block.blockNumber); + + assertEq(startBlockNumber, _start, "Invalid start block number"); + assertEq(endBlockNumber, _end, "Invalid end block number"); + + BlockLog memory parentBlockLog = rollup.getBlock(startBlockNumber - 1); + address prover = address(0xcafe); + + PublicInputArgs memory args = PublicInputArgs({ + previousArchive: parentBlockLog.archive, + endArchive: endFull.block.archive, + proverId: prover + }); + + bytes32[] memory fees = new bytes32[](Constants.AZTEC_MAX_EPOCH_DURATION * 2); + + if (_revertData != NO_REVERT) { + vm.expectPartialRevert(_revertData); + } + + rollup.submitEpochRootProof( + SubmitEpochRootProofArgs({ + start: startBlockNumber, + end: endBlockNumber, + args: args, + fees: fees, + attestations: _attestations, + blobInputs: endFull.block.batchedBlobInputs, + proof: "" + }) + ); + } + function _createAttestation(address _signer, bytes32 _digest) internal view From 7ad42c0b3882e63af7628c3db97f9189048317e8 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 25 Jul 2025 11:41:10 -0300 Subject: [PATCH 19/22] Suggestions from code review --- l1-contracts/src/core/Rollup.sol | 4 +- l1-contracts/src/core/interfaces/IRollup.sol | 2 +- .../core/libraries/rollup/EpochProofLib.sol | 10 +-- .../core/libraries/rollup/ExtRollupLib.sol | 4 +- .../core/libraries/rollup/InvalidateLib.sol | 4 -- .../rollup/ValidatorSelectionLib.sol | 64 +++++++++---------- l1-contracts/test/Rollup.t.sol | 2 +- yarn-project/ethereum/src/contracts/rollup.ts | 2 +- .../src/publisher/sequencer-publisher.ts | 2 +- 9 files changed, 43 insertions(+), 51 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index b183aa9b4809..2c949ac24c05 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -102,7 +102,7 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { * @param _blobsHash - The blobs hash for this block * @param _flags - The flags to validate */ - function validateHeader( + function validateHeaderWithAttestations( ProposedHeader calldata _header, CommitteeAttestations memory _attestations, address[] calldata _signers, @@ -111,7 +111,7 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { BlockHeaderValidationFlags memory _flags ) external override(IRollup) { Timestamp currentTime = Timestamp.wrap(block.timestamp); - ExtRollupLib.validateHeader( + ExtRollupLib.validateHeaderWithAttestations( ValidateHeaderArgs({ header: _header, digest: _digest, diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index ec32e79f9567..3770f53b3906 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -146,7 +146,7 @@ interface IRollupCore { } interface IRollup is IRollupCore, IHaveVersion { - function validateHeader( + function validateHeaderWithAttestations( ProposedHeader calldata _header, CommitteeAttestations memory _attestations, address[] memory _signers, diff --git a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol index 6ff3b6e5e747..7a1ac7b20d51 100644 --- a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol +++ b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol @@ -8,7 +8,7 @@ import { IRollupCore, RollupStore } from "@aztec/core/interfaces/IRollup.sol"; -import {TempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; +import {CompressedTempBlockLog} from "@aztec/core/libraries/compressed-data/BlockLog.sol"; import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol"; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; @@ -18,6 +18,7 @@ import {RewardLib} from "@aztec/core/libraries/rollup/RewardLib.sol"; import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol"; import {ValidatorSelectionLib} from "@aztec/core/libraries/rollup/ValidatorSelectionLib.sol"; import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +import {CompressedSlot, CompressedTimeMath} from "@aztec/shared/libraries/CompressedTimeMath.sol"; import {CommitteeAttestations, SignatureLib} from "@aztec/shared/libraries/SignatureLib.sol"; import {Math} from "@oz/utils/math/Math.sol"; import {SafeCast} from "@oz/utils/math/SafeCast.sol"; @@ -30,6 +31,7 @@ library EpochProofLib { using SafeCast for uint256; using ChainTipsLib for CompressedChainTips; using SignatureLib for CommitteeAttestations; + using CompressedTimeMath for CompressedSlot; // This is a temporary struct to avoid stack too deep errors struct BlobVarsTemp { @@ -224,7 +226,7 @@ library EpochProofLib { CommitteeAttestations memory _attestations ) private { // Get the stored attestation hash and payload digest for the last block - TempBlockLog memory blockLog = STFLib.getTempBlockLog(_endBlockNumber); + CompressedTempBlockLog storage blockLog = STFLib.getStorageTempBlockLog(_endBlockNumber); // Verify that the provided attestations match the stored hash bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations)); @@ -233,10 +235,10 @@ library EpochProofLib { ); // Get the slot and epoch for the last block - Slot slot = blockLog.slotNumber; + Slot slot = blockLog.slotNumber.decompress(); Epoch epoch = STFLib.getEpochForBlock(_endBlockNumber); - ValidatorSelectionLib.verify(slot, epoch, _attestations, blockLog.payloadDigest); + ValidatorSelectionLib.verifyAttestations(slot, epoch, _attestations, blockLog.payloadDigest); } function assertAcceptable(uint256 _start, uint256 _end) private view returns (Epoch) { diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index d8a0be0bb4a2..b325fd71849e 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -29,7 +29,7 @@ library ExtRollupLib { EpochProofLib.submitEpochRootProof(_args); } - function validateHeader( + function validateHeaderWithAttestations( ValidateHeaderArgs calldata _args, CommitteeAttestations calldata _attestations, address[] calldata _signers @@ -41,7 +41,7 @@ library ExtRollupLib { Slot slot = _args.header.slotNumber; Epoch epoch = slot.epochFromSlot(); - ValidatorSelectionLib.verify(slot, epoch, _attestations, _args.digest); + ValidatorSelectionLib.verifyAttestations(slot, epoch, _attestations, _args.digest); ValidatorSelectionLib.verifyProposer(slot, epoch, _attestations, _signers, _args.digest); } diff --git a/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol index a2ce9aea67f0..4fbafdbec9b0 100644 --- a/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol +++ b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol @@ -58,10 +58,6 @@ library InvalidateLib { revert Errors.Rollup__AttestationsAreValid(); } - // Reset the pending block number to remove this block and all subsequent blocks - RollupStore storage rollupStore = STFLib.getStorage(); - rollupStore.tips = rollupStore.tips.updatePendingBlockNumber(_blockNumber - 1); - _invalidateBlock(_blockNumber); } diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index ef56ae47fbc3..d45fec2989b3 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -40,7 +40,6 @@ library ValidatorSelectionLib { uint256 needed; uint256 signaturesRecovered; address[] reconstructedCommittee; - bool proposerVerified; } bytes32 private constant VALIDATOR_SELECTION_STORAGE_POSITION = @@ -98,32 +97,38 @@ library ValidatorSelectionLib { address[] memory _signers, bytes32 _digest ) internal { - // Load the committee commitment for the epoch - (bytes32 committeeCommitment, uint256 committeeSize) = getCommitteeCommitmentAt(_epochNumber); + // Try load the proposer from cache + (address proposer, uint256 proposerIndex) = getCachedProposer(_slot); - // If the target committee size is 0, we skip the validation - if (committeeSize == 0) { - return; - } + // If not in cache, grab from the committee, reconstructed from the attestations and signers + if (proposer == address(0)) { + // Load the committee commitment for the epoch + (bytes32 committeeCommitment, uint256 committeeSize) = getCommitteeCommitmentAt(_epochNumber); - // Reconstruct the committee from the attestations and signers - address[] memory committee = - _attestations.reconstructCommitteeFromSigners(_signers, committeeSize); + // If the target committee size is 0, we skip the validation + if (committeeSize == 0) { + return; + } - // Check it matches the expected one - bytes32 reconstructedCommitment = computeCommitteeCommitment(committee); - if (reconstructedCommitment != committeeCommitment) { - revert Errors.ValidatorSelection__InvalidCommitteeCommitment( - reconstructedCommitment, committeeCommitment - ); - } + // Reconstruct the committee from the attestations and signers + address[] memory committee = + _attestations.reconstructCommitteeFromSigners(_signers, committeeSize); - // Get the proposer from the committee based on the epoch, slot, and sample seed - uint224 sampleSeed = getSampleSeed(_epochNumber); - uint256 proposerIndex = computeProposerIndex(_epochNumber, _slot, sampleSeed, committeeSize); - address proposer = committee[proposerIndex]; + // Check it matches the expected one + bytes32 reconstructedCommitment = computeCommitteeCommitment(committee); + if (reconstructedCommitment != committeeCommitment) { + revert Errors.ValidatorSelection__InvalidCommitteeCommitment( + reconstructedCommitment, committeeCommitment + ); + } - setCachedProposer(_slot, proposer, proposerIndex); + // Get the proposer from the committee based on the epoch, slot, and sample seed + uint224 sampleSeed = getSampleSeed(_epochNumber); + proposerIndex = computeProposerIndex(_epochNumber, _slot, sampleSeed, committeeSize); + proposer = committee[proposerIndex]; + + setCachedProposer(_slot, proposer, proposerIndex); + } // If the proposer is who sent the tx, we're good if (proposer == msg.sender) { @@ -145,20 +150,18 @@ library ValidatorSelectionLib { /** * @notice Propose a pending block from the point-of-view of sequencer selection. Will: * - Setup the epoch if needed (if epoch committee is empty skips the rest) - * - Validate that the proposer has signed with its own key * - Validate that the signatures for attestations are indeed from the validatorset * - Validate that the number of valid attestations is sufficient * * @dev Cases where errors are thrown: * - If the epoch is not setup - * - If the proposer is not the real proposer AND the proposer is not open * - If the number of valid attestations is insufficient * * @param _slot - The slot of the block * @param _attestations - The signatures (or empty; just address is provided) of the committee members * @param _digest - The digest of the block */ - function verify( + function verifyAttestations( Slot _slot, Epoch _epochNumber, CommitteeAttestations memory _attestations, @@ -181,8 +184,7 @@ library ValidatorSelectionLib { needed: (targetCommitteeSize << 1) / 3 + 1, // targetCommitteeSize * 2 / 3 + 1, but cheaper index: 0, signaturesRecovered: 0, - reconstructedCommittee: new address[](targetCommitteeSize), - proposerVerified: false + reconstructedCommittee: new address[](targetCommitteeSize) }); bytes32 digest = _digest.toEthSignedMessageHash(); @@ -213,10 +215,6 @@ library ValidatorSelectionLib { ++stack.signaturesRecovered; stack.reconstructedCommittee[i] = ECDSA.recover(digest, v, r, s); - - if (i == stack.proposerIndex) { - stack.proposerVerified = true; - } } else { address addr; assembly { @@ -230,10 +228,6 @@ library ValidatorSelectionLib { address proposer = stack.reconstructedCommittee[stack.proposerIndex]; - require( - stack.proposerVerified, Errors.ValidatorSelection__InvalidProposer(proposer, address(0)) - ); - require( stack.signaturesRecovered >= stack.needed, Errors.ValidatorSelection__InsufficientAttestations(stack.needed, stack.signaturesRecovered) diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index f4589b78f038..2dcb712645f7 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -864,7 +864,7 @@ contract RollupTest is RollupBase { end: _end, args: args, fees: fees, - attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), // TODO(palla): Add unit tests with non-empty attestations + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), blobInputs: _blobInputs, proof: "" }) diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index 953f8c464d00..b7e539ad21fa 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -405,7 +405,7 @@ export class RollupContract { await this.client.simulateContract({ address: this.address, abi: RollupAbi, - functionName: 'validateHeader', + functionName: 'validateHeaderWithAttestations', args, account, }); diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index 645f2f88499e..b04acb754b7a 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -344,7 +344,7 @@ export class SequencerPublisher { await this.l1TxUtils.simulate( { to: this.rollupContract.address, - data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeader', args }), + data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }), from: MULTI_CALL_3_ADDRESS, }, { From 69263f460644e5beec7ee12e44f9174df069bb94 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 25 Jul 2025 12:33:04 -0300 Subject: [PATCH 20/22] No msg.sender checks in validator selection --- l1-contracts/src/core/Rollup.sol | 5 ++-- l1-contracts/src/core/interfaces/IRollup.sol | 4 +++- l1-contracts/src/core/libraries/Errors.sol | 1 + .../core/libraries/rollup/ExtRollupLib2.sol | 7 ++++-- .../rollup/ValidatorSelectionLib.sol | 16 +++++-------- l1-contracts/test/RollupGetters.t.sol | 3 +-- .../ValidatorSelection.t.sol | 23 +++++++++++++------ yarn-project/ethereum/src/contracts/rollup.ts | 3 ++- 8 files changed, 37 insertions(+), 25 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 2c949ac24c05..4675969dbd9d 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -183,16 +183,17 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { * * @param _ts - The timestamp to check * @param _archive - The archive to check (should be the latest archive) + * @param _who - The address to check * * @return uint256 - The slot at the given timestamp * @return uint256 - The block number at the given timestamp */ - function canProposeAtTime(Timestamp _ts, bytes32 _archive) + function canProposeAtTime(Timestamp _ts, bytes32 _archive, address _who) external override(IRollup) returns (Slot, uint256) { - return ExtRollupLib2.canProposeAtTime(_ts, _archive); + return ExtRollupLib2.canProposeAtTime(_ts, _archive, _who); } function getTargetCommitteeSize() external view override(IValidatorSelection) returns (uint256) { diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 3770f53b3906..8ed0cbb9b755 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -155,7 +155,9 @@ interface IRollup is IRollupCore, IHaveVersion { BlockHeaderValidationFlags memory _flags ) external; - function canProposeAtTime(Timestamp _ts, bytes32 _archive) external returns (Slot, uint256); + function canProposeAtTime(Timestamp _ts, bytes32 _archive, address _who) + external + returns (Slot, uint256); function getTips() external view returns (ChainTips memory); diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 7909ae43da39..f9f40bc776b8 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -102,6 +102,7 @@ library Errors { // Sequencer Selection (ValidatorSelection) error ValidatorSelection__EpochNotSetup(); // 0x10816cae error ValidatorSelection__InvalidProposer(address expected, address actual); // 0xa8843a68 + error ValidatorSelection__MissingProposerSignature(address proposer, uint256 index); error ValidatorSelection__InvalidDeposit(address attester, address proposer); // 0x533169bd error ValidatorSelection__InsufficientAttestations(uint256 minimumNeeded, uint256 provided); // 0xaf47297f error ValidatorSelection__InvalidCommitteeCommitment(bytes32 reconstructed, bytes32 expected); // 0xca8d5954 diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol index 13c91e505f72..07e6bc8aa4d5 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol @@ -76,8 +76,11 @@ library ExtRollupLib2 { return StakingLib.trySlash(_attester, _amount); } - function canProposeAtTime(Timestamp _ts, bytes32 _archive) external returns (Slot, uint256) { - return ValidatorSelectionLib.canProposeAtTime(_ts, _archive); + function canProposeAtTime(Timestamp _ts, bytes32 _archive, address _who) + external + returns (Slot, uint256) + { + return ValidatorSelectionLib.canProposeAtTime(_ts, _archive, _who); } function getCommitteeAt(Epoch _epoch) external returns (address[] memory) { diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index d45fec2989b3..945a4e9b9294 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -130,15 +130,10 @@ library ValidatorSelectionLib { setCachedProposer(_slot, proposer, proposerIndex); } - // If the proposer is who sent the tx, we're good - if (proposer == msg.sender) { - return; - } - // Check if the proposer has signed, if not, fail bool hasProposerSignature = _attestations.isSignature(proposerIndex); if (!hasProposerSignature) { - revert Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender); + revert Errors.ValidatorSelection__MissingProposerSignature(proposer, proposerIndex); } // Check if the signature is correct @@ -358,7 +353,10 @@ library ValidatorSelectionLib { } } - function canProposeAtTime(Timestamp _ts, bytes32 _archive) internal returns (Slot, uint256) { + function canProposeAtTime(Timestamp _ts, bytes32 _archive, address _who) + internal + returns (Slot, uint256) + { Slot slot = _ts.slotFromTimestamp(); RollupStore storage rollupStore = STFLib.getStorage(); @@ -373,9 +371,7 @@ library ValidatorSelectionLib { require(tipArchive == _archive, Errors.Rollup__InvalidArchive(tipArchive, _archive)); (address proposer,) = getProposerAt(slot); - require( - proposer == msg.sender, Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender) - ); + require(proposer == _who, Errors.ValidatorSelection__InvalidProposer(proposer, _who)); return (slot, pendingBlockNumber + 1); } diff --git a/l1-contracts/test/RollupGetters.t.sol b/l1-contracts/test/RollupGetters.t.sol index 24f1c732be0e..5c431b9ffb96 100644 --- a/l1-contracts/test/RollupGetters.t.sol +++ b/l1-contracts/test/RollupGetters.t.sol @@ -153,8 +153,7 @@ contract RollupShouldBeGetters is ValidatorSelectionTestBase { vm.record(); - vm.prank(proposer); - rollup.canProposeAtTime(t, log.archive); + rollup.canProposeAtTime(t, log.archive, proposer); (, bytes32[] memory writes) = vm.accesses(address(rollup)); assertEq(writes.length, 0, "No writes should be done"); diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 42b1e7b66e12..390aaf4dc838 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -307,7 +307,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { function testProposerAttestationNotProvided() public setup(4, 4) progressEpochs(2) { _testBlock( "mixed_block_1", - Errors.ValidatorSelection__InvalidProposer.selector, + Errors.ValidatorSelection__MissingProposerSignature.selector, 3, 4, TestFlags({ @@ -498,17 +498,26 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { bytes32 digest = ProposeLib.digest(ree.proposePayload); { - uint256 signaturesCollected = 0; + uint256 signersIndex = 0; + uint256 signaturesCollected = _flags.proposerAttestationNotProvided ? 0 : 1; for (uint256 i = 0; i < ree.attestationsCount; i++) { - if ( - (signaturesCollected >= _signatureCount) - || (ree.committee[i] == ree.proposer && _flags.proposerAttestationNotProvided) - ) { + if ((ree.committee[i] == ree.proposer && _flags.proposerAttestationNotProvided)) { + // If the proposer is not providing an attestation, we skip it + ree.attestations[i] = _createEmptyAttestation(ree.committee[i]); + } else if ((ree.committee[i] == ree.proposer)) { + // If the proposer is providing an attestation, set it + ree.attestations[i] = _createAttestation(ree.committee[i], digest); + ree.signers[signersIndex] = ree.committee[i]; + signersIndex++; + } else if ((signaturesCollected >= _signatureCount)) { + // No need to create more signatures if we have collected enough ree.attestations[i] = _createEmptyAttestation(ree.committee[i]); } else { + // Create an attestation for the committee member and add them to the signers ree.attestations[i] = _createAttestation(ree.committee[i], digest); - ree.signers[signaturesCollected] = ree.committee[i]; + ree.signers[signersIndex] = ree.committee[i]; signaturesCollected++; + signersIndex++; } } } diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index b7e539ad21fa..7e63130b52b3 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -504,6 +504,7 @@ export class RollupContract { } const latestBlock = await this.client.getBlock(); const timeOfNextL1Slot = latestBlock.timestamp + slotDuration; + const who = typeof account === 'string' ? account : account.address; try { const { @@ -512,7 +513,7 @@ export class RollupContract { address: this.address, abi: RollupAbi, functionName: 'canProposeAtTime', - args: [timeOfNextL1Slot, `0x${archive.toString('hex')}`], + args: [timeOfNextL1Slot, `0x${archive.toString('hex')}`, who], account, }); From 9e591cb3c8a430c04fa111ce72455386d3943785 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 25 Jul 2025 18:30:56 -0300 Subject: [PATCH 21/22] Reenable test --- .../validator-selection/ValidatorSelection.t.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 390aaf4dc838..e627d415f425 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -81,6 +81,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { }); bytes4 NO_REVERT = bytes4(0); + bytes4 ANY_REVERT = bytes4(0xFFFFFFFF); function testInitialCommitteeMatch() public setup(4, 4) progressEpochs(2) { address[] memory attesters = rollup.getAttesters(); @@ -381,9 +382,6 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { // a block if you submit one with no signatures. This was a change from prior behavior where we had had // that if there were zero validators in a rollup, anyone could build a block - // TODO(palla): What should we do in this scenario? Block the proposal, or allow invalidating later? - vm.skip(true); - GSE gse = rollup.getGSE(); address caller = gse.owner(); vm.prank(caller); @@ -391,7 +389,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { assertEq(rollup.getCurrentEpochCommittee().length, 4); _testBlock( "mixed_block_1", - NO_REVERT, + ANY_REVERT, 0, 0, TestFlags({ @@ -543,7 +541,11 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { emit log("Time to propose"); if (_revertData != NO_REVERT) { - vm.expectPartialRevert(_revertData); + if (_revertData == ANY_REVERT) { + vm.expectRevert(); + } else { + vm.expectPartialRevert(_revertData); + } } vm.prank(ree.sender); From c35f7629d9f3f7ac1cc193d113b4c68a74d0ec38 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 28 Jul 2025 09:56:37 -0300 Subject: [PATCH 22/22] Address comments from @just-mitch --- .../src/core/libraries/rollup/ProposeLib.sol | 3 +-- .../libraries/rollup/ValidatorSelectionLib.sol | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol index 915539dcac75..37dbb44ab95f 100644 --- a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol @@ -182,8 +182,7 @@ library ProposeLib { emit IRollupCore.L2BlockProposed(blockNumber, _args.archive, v.blobHashes); } - // @note: not view as sampling validators uses tstore - function validateHeader(ValidateHeaderArgs memory _args) internal { + function validateHeader(ValidateHeaderArgs memory _args) internal view { require(_args.header.coinbase != address(0), Errors.Rollup__InvalidCoinbase()); require(_args.header.totalManaUsed <= FeeLib.getManaLimit(), Errors.Rollup__ManaLimitExceeded()); diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index 945a4e9b9294..a716dafb26c7 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -143,18 +143,18 @@ library ValidatorSelectionLib { } /** - * @notice Propose a pending block from the point-of-view of sequencer selection. Will: - * - Setup the epoch if needed (if epoch committee is empty skips the rest) - * - Validate that the signatures for attestations are indeed from the validatorset - * - Validate that the number of valid attestations is sufficient + * Verifies the committee attestations for a given slot and epoch. Throws on validation failure. * - * @dev Cases where errors are thrown: - * - If the epoch is not setup - * - If the number of valid attestations is insufficient + * - Computes the committee commitment for the epoch from storage as source of truth. + * - Recomputes the commitment from the signatures and compares with the stored one. + * - Sets the proposer in temporary storage. + * - Validates the signatures for the attestations. + * - Checks the number of valid attestations. * * @param _slot - The slot of the block + * @param _epochNumber - The epoch of the block * @param _attestations - The signatures (or empty; just address is provided) of the committee members - * @param _digest - The digest of the block + * @param _digest - The digest of the block that the attestations are signed over */ function verifyAttestations( Slot _slot,