diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 1ff731499b87..4675969dbd9d 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -102,23 +102,25 @@ 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, bytes32 _digest, bytes32 _blobsHash, BlockHeaderValidationFlags memory _flags ) external override(IRollup) { Timestamp currentTime = Timestamp.wrap(block.timestamp); - ExtRollupLib.validateHeader( + ExtRollupLib.validateHeaderWithAttestations( ValidateHeaderArgs({ header: _header, - attestations: _attestations, digest: _digest, manaBaseFee: getManaBaseFeeAt(currentTime, true), blobsHashesCommitment: _blobsHash, flags: _flags - }) + }), + _attestations, + _signers ); } @@ -181,34 +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) { - 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, _who); } function getTargetCommitteeSize() external view override(IValidatorSelection) returns (uint256) { @@ -365,6 +350,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..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); @@ -223,16 +224,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) @@ -245,9 +245,27 @@ 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( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee, + uint256 _invalidIndex + ) external override(IRollupCore) { + ExtRollupLib2.invalidateBadAttestation(_blockNumber, _attestations, _committee, _invalidIndex); + } + + function invalidateInsufficientAttestations( + uint256 _blockNumber, + CommitteeAttestations memory _attestations, + address[] memory _committee + ) external override(IRollupCore) { + ExtRollupLib2.invalidateInsufficientAttestations(_blockNumber, _attestations, _committee); } function setupEpoch() public override(IValidatorSelectionCore) { diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 672c4106402a..8ed0cbb9b755 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); @@ -119,11 +119,25 @@ interface IRollupCore { function propose( ProposeArgs calldata _args, CommitteeAttestations memory _attestations, + address[] memory _signers, bytes calldata _blobInput ) external; 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; @@ -132,15 +146,18 @@ interface IRollupCore { } interface IRollup is IRollupCore, IHaveVersion { - function validateHeader( + function validateHeaderWithAttestations( ProposedHeader calldata _header, CommitteeAttestations memory _attestations, + address[] memory _signers, bytes32 _digest, bytes32 _blobsHash, 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 e2682319e25e..f9f40bc776b8 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 @@ -98,10 +102,12 @@ 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 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..7a1ac7b20d51 100644 --- a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol +++ b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol @@ -8,6 +8,7 @@ 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 {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; @@ -15,7 +16,10 @@ 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 {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"; @@ -26,6 +30,8 @@ library EpochProofLib { using FeeHeaderLib for CompressedFeeHeader; 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 { @@ -65,6 +71,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(); @@ -207,6 +216,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 + CompressedTempBlockLog storage blockLog = STFLib.getStorageTempBlockLog(_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.decompress(); + Epoch epoch = STFLib.getEpochForBlock(_endBlockNumber); + + ValidatorSelectionLib.verifyAttestations(slot, epoch, _attestations, blockLog.payloadDigest); + } + function assertAcceptable(uint256 _start, uint256 _end) private view returns (Epoch) { RollupStore storage rollupStore = STFLib.getStorage(); diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index e686e0f4ed8f..b325fd71849e 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -3,34 +3,61 @@ // 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 {Timestamp, TimeLib} from "@aztec/core/libraries/TimeLib.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 {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 validateHeaderWithAttestations( + ValidateHeaderArgs calldata _args, + CommitteeAttestations calldata _attestations, + address[] calldata _signers + ) external { ProposeLib.validateHeader(_args); + if (_attestations.isEmpty()) { + return; // No attestations to validate + } + + Slot slot = _args.header.slotNumber; + Epoch epoch = slot.epochFromSlot(); + ValidatorSelectionLib.verifyAttestations(slot, epoch, _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 { + require(STFLib.canPruneAtTime(Timestamp.wrap(block.timestamp)), Errors.Rollup__NothingToPrune()); + STFLib.prune(); } function getEpochProofPublicInputs( diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol index 34625eab3bab..07e6bc8aa4d5 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol @@ -6,40 +6,13 @@ 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 { - RewardBooster, - RewardBoostConfig, - IBoosterCore, - IValidatorSelection -} from "@aztec/core/reward-boost/RewardBooster.sol"; -import {Slasher, ISlasher} from "@aztec/core/slashing/Slasher.sol"; +import {CommitteeAttestations} from "@aztec/shared/libraries/SignatureLib.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); } @@ -60,6 +33,10 @@ library ExtRollupLib2 { return StakingLib.initiateWithdraw(_attester, _recipient); } + function finaliseWithdraw(address _attester) external { + StakingLib.finaliseWithdraw(_attester); + } + function initializeValidatorSelection(uint256 _targetCommitteeSize) external { ValidatorSelectionLib.initialize(_targetCommitteeSize); } @@ -78,12 +55,40 @@ 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 slash(address _attester, uint256 _amount) external returns (bool) { + return StakingLib.trySlash(_attester, _amount); + } + + 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) { 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/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/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol new file mode 100644 index 000000000000..4fbafdbec9b0 --- /dev/null +++ b/l1-contracts/src/core/libraries/rollup/InvalidateLib.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +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"; + +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; + using CompressedTimeMath for CompressedSlot; + + /** + * @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 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); + + // 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(); + } + + _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; // committeeSize * 2 / 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 + CompressedTempBlockLog storage blockLog = STFLib.getStorageTempBlockLog(_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.decompress().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..37dbb44ab95f 100644 --- a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol @@ -44,11 +44,13 @@ struct InterimProposeValues { bytes[] blobCommitments; bytes32 inHash; bytes32 headerHash; + bytes32 attestationsHash; + bytes32 payloadDigest; + Epoch currentEpoch; } /** * @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 +58,6 @@ struct InterimProposeValues { */ struct ValidateHeaderArgs { ProposedHeader header; - CommitteeAttestations attestations; bytes32 digest; uint256 manaBaseFee; bytes32 blobsHashesCommitment; @@ -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,36 +106,41 @@ 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); + 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, 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 ); @@ -145,6 +153,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 +163,8 @@ library ProposeLib { TempBlockLog({ headerHash: v.headerHash, blobCommitmentsHash: blobCommitmentsHash, + attestationsHash: v.attestationsHash, + payloadDigest: v.payloadDigest, slotNumber: header.slotNumber, feeHeader: feeHeader }) @@ -169,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()); @@ -211,10 +223,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/STFLib.sol b/l1-contracts/src/core/libraries/rollup/STFLib.sol index 0d45f03f364c..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(); } } @@ -121,6 +129,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; 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 f718ce26c0b7..a716dafb26c7 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -2,11 +2,12 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {BlockHeaderValidationFlags} from "@aztec/core/interfaces/IRollup.sol"; +import {RollupStore} 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 @@ -39,7 +40,6 @@ library ValidatorSelectionLib { uint256 needed; uint256 signaturesRecovered; address[] reconstructedCommittee; - bool proposerVerified; } bytes32 private constant VALIDATOR_SELECTION_STORAGE_POSITION = @@ -82,27 +82,85 @@ 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 is the proposer of the slot - * - Validate that the signatures for attestations are indeed from the validatorset - * - Validate that the number of valid attestations is sufficient + * 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. * - * @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 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, + Epoch _epochNumber, + CommitteeAttestations memory _attestations, + address[] memory _signers, + bytes32 _digest + ) internal { + // Try load the proposer from cache + (address proposer, uint256 proposerIndex) = getCachedProposer(_slot); + + // 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); + + // 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); + proposerIndex = computeProposerIndex(_epochNumber, _slot, sampleSeed, committeeSize); + proposer = committee[proposerIndex]; + + setCachedProposer(_slot, proposer, proposerIndex); + } + + // Check if the proposer has signed, if not, fail + bool hasProposerSignature = _attestations.isSignature(proposerIndex); + if (!hasProposerSignature) { + revert Errors.ValidatorSelection__MissingProposerSignature(proposer, proposerIndex); + } + + // Check if the signature is correct + bytes32 digest = _digest.toEthSignedMessageHash(); + Signature memory signature = _attestations.getSignature(proposerIndex); + SignatureLib.verify(signature, proposer, digest); + } + + /** + * Verifies the committee attestations for a given slot and epoch. Throws on validation failure. + * + * - 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 verify( + function verifyAttestations( Slot _slot, Epoch _epochNumber, CommitteeAttestations memory _attestations, - bytes32 _digest, - BlockHeaderValidationFlags memory _flags + bytes32 _digest ) internal { (bytes32 committeeCommitment, uint256 targetCommitteeSize) = getCommitteeCommitmentAt(_epochNumber); @@ -114,10 +172,6 @@ library ValidatorSelectionLib { return; } - if (_flags.ignoreSignatures) { - return; - } - VerifyStack memory stack = VerifyStack({ proposerIndex: computeProposerIndex( _epochNumber, _slot, getSampleSeed(_epochNumber), targetCommitteeSize @@ -125,8 +179,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(); @@ -157,10 +210,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 { @@ -174,11 +223,6 @@ library ValidatorSelectionLib { address proposer = stack.reconstructedCommittee[stack.proposerIndex]; - require( - stack.proposerVerified || proposer == msg.sender, - Errors.ValidatorSelection__InvalidProposer(proposer, msg.sender) - ); - require( stack.signaturesRecovered >= stack.needed, Errors.ValidatorSelection__InsufficientAttestations(stack.needed, stack.signaturesRecovered) @@ -192,32 +236,37 @@ 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); + 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 cachedProposerIndex) = getCachedProposer(_slot); if (cachedProposer != address(0)) { - return cachedProposer; + 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) { - return address(0); + (uint32 ts, uint256[] memory indices) = sampleValidatorsIndices(epochNumber, sampleSeed); + uint256 committeeSize = indices.length; + if (committeeSize == 0) { + return (address(0), 0); } - - return committee[computeProposerIndex(epochNumber, _slot, sampleSeed, committee.length)]; + uint256 proposerIndex = computeProposerIndex(epochNumber, _slot, sampleSeed, committeeSize); + return ( + StakingLib.getAttesterFromIndexAtTime(indices[proposerIndex], Timestamp.wrap(ts)), + proposerIndex + ); } /** @@ -229,24 +278,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); } @@ -321,8 +353,40 @@ library ValidatorSelectionLib { } } - function getCachedProposer(Slot _slot) internal view returns (address) { - return PROPOSER_NAMESPACE.erc7201Slot().deriveMapping(Slot.unwrap(_slot)).asAddress().tload(); + function canProposeAtTime(Timestamp _ts, bytes32 _archive, address _who) + 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 == _who, Errors.ValidatorSelection__InvalidProposer(proposer, _who)); + + return (slot, pendingBlockNumber + 1); + } + + 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) { @@ -358,6 +422,53 @@ 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. + * + * @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) + private + 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 * @@ -383,22 +494,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) - private - pure - returns (uint256) - { - return uint256(keccak256(abi.encode(_epoch, _slot, _seed))) % _size; - } } 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; diff --git a/l1-contracts/src/shared/libraries/SignatureLib.sol b/l1-contracts/src/shared/libraries/SignatureLib.sol index 096a4c109afd..16d71d623363 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,98 @@ 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}); + } + + /** + * 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; + bytes memory signatureIndices = _attestations.signatureIndices; + address[] memory addresses = new address[](_length); + + uint256 signersIndex; + uint256 dataPtr; + uint256 currentByte; + uint256 bitMask; + + assembly { + // Skip length + dataPtr := add(signaturesOrAddresses, 0x20) + } + + for (uint256 i = 0; i < _length; ++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++; + } 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 * * @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..2dcb712645f7 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 /** @@ -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"); } @@ -643,6 +645,7 @@ contract RollupTest is RollupBase { end: 2, args: args, fees: fees, + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), blobInputs: data.batchedBlobInputs, proof: proof }) @@ -728,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") { @@ -751,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") { @@ -861,6 +864,7 @@ contract RollupTest is RollupBase { end: _end, args: args, fees: fees, + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), blobInputs: _blobInputs, proof: "" }) diff --git a/l1-contracts/test/RollupGetters.t.sol b/l1-contracts/test/RollupGetters.t.sol index 3131b83e5105..5c431b9ffb96 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. } @@ -124,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/base/RollupBase.sol b/l1-contracts/test/base/RollupBase.sol index 1e87627c83b3..dc7a0504114d 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"; @@ -35,6 +36,7 @@ contract RollupBase is DecoderBase { MerkleTestUtil internal merkleTestUtil = new MerkleTestUtil(); CommitteeAttestation[] internal attestations; + address[] internal signers; mapping(uint256 => uint256) internal blockFees; @@ -100,6 +102,7 @@ contract RollupBase is DecoderBase { end: endBlockNumber, args: args, fees: fees, + attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}), blobInputs: endFull.block.batchedBlobInputs, proof: "" }) @@ -202,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 ef7cefa4195f..4b07502514b2 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 @@ -116,6 +117,7 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { ProposeArgs proposeArgs; bytes blobInputs; CommitteeAttestation[] attestations; + address[] signers; } DecoderBase.Full full = load("single_tx_block_1"); @@ -133,6 +135,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(); SlashingProposer internal slashingProposer; @@ -245,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); @@ -269,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]); } @@ -287,7 +298,8 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { return Block({ proposeArgs: proposeArgs, blobInputs: full.block.blobCommitments, - attestations: attestations + attestations: attestations, + signers: signers }); } @@ -368,6 +380,10 @@ contract BenchmarkRollupTest 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); + if (_slashing) { Signature memory sig = createSignalSignature(proposer, slashPayload, round); Multicall3.Call3[] memory calls = new Multicall3.Call3[](2); @@ -375,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 }); @@ -387,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); @@ -432,6 +450,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..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"); @@ -135,6 +136,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,8 +229,14 @@ 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); + rollup.propose( + b.proposeArgs, SignatureLib.packAttestations(b.attestations), b.signers, b.blobInputs + ); nextSlot = nextSlot + Slot.wrap(1); } @@ -269,6 +279,7 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { end: start + epochSize - 1, args: args, fees: fees, + attestations: blockAttestations[start + epochSize - 1], blobInputs: full.block.batchedBlobInputs, proof: "" }) @@ -328,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); @@ -355,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]); } @@ -370,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 141e0704c688..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 ); @@ -392,6 +397,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 diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 941294ad227f..e627d415f425 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -3,9 +3,13 @@ // 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, 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"; @@ -34,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 /** @@ -45,19 +53,36 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { // Test Block Flags struct TestFlags { - bool provideEmptyAttestations; - bool invalidProposer; - bool proposerNotProvided; - bool invalidCommitteeCommitment; + bool senderIsNotProposer; + bool proposerAttestationNotProvided; + bool invalidAttestation; + bool invalidSigners; } TestFlags NO_FLAGS = TestFlags({ - provideEmptyAttestations: true, - invalidProposer: false, - proposerNotProvided: false, - invalidCommitteeCommitment: false + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: false, + invalidSigners: false + }); + + TestFlags INVALID_ATTESTATION = TestFlags({ + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: true, + invalidSigners: false }); + TestFlags INVALID_SIGNERS = TestFlags({ + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: false, + invalidSigners: true + }); + + bytes4 NO_REVERT = bytes4(0); + bytes4 ANY_REVERT = bytes4(0xFFFFFFFF); + function testInitialCommitteeMatch() public setup(4, 4) progressEpochs(2) { address[] memory attesters = rollup.getAttesters(); address[] memory committee = rollup.getCurrentEpochCommittee(); @@ -169,31 +194,62 @@ 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( - "mixed_block_1", - _insufficientSigs, - committeeSize, - TestFlags({ - provideEmptyAttestations: true, - invalidProposer: false, - proposerNotProvided: false, - invalidCommitteeCommitment: false - }) - ); + ProposeTestData memory ree = + _testBlock("mixed_block_1", NO_REVERT, signatureCount, committeeSize, NO_FLAGS); - 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 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); + + 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 +258,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 +289,99 @@ 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, + invalidSigners: false }) ); } - function testProposerNotProvided() public setup(4, 4) progressEpochs(2) { + function testProposerAttestationNotProvided() public setup(4, 4) progressEpochs(2) { _testBlock( "mixed_block_1", - true, + Errors.ValidatorSelection__MissingProposerSignature.selector, 3, + 4, TestFlags({ - invalidProposer: false, - provideEmptyAttestations: true, - proposerNotProvided: true, - invalidCommitteeCommitment: false + senderIsNotProposer: true, + proposerAttestationNotProvided: true, + invalidAttestation: false, + invalidSigners: false }) ); } - function testInvalidCommitteeCommitment() public setup(4, 4) progressEpochs(2) { + function testInvalidSigners() public setup(4, 4) progressEpochs(2) { _testBlock( "mixed_block_1", - true, + Errors.ValidatorSelection__InvalidCommitteeCommitment.selector, 3, + 4, TestFlags({ - invalidProposer: false, - provideEmptyAttestations: true, - proposerNotProvided: false, - invalidCommitteeCommitment: true + senderIsNotProposer: true, + proposerAttestationNotProvided: false, + invalidAttestation: false, + invalidSigners: true }) ); } - function testInsufficientSigsMove() public setup(4, 4) progressEpochs(2) { + 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 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 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 + GSE gse = rollup.getGSE(); address caller = gse.owner(); vm.prank(caller); @@ -284,28 +389,69 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { assertEq(rollup.getCurrentEpochCommittee().length, 4); _testBlock( "mixed_block_1", - true, + ANY_REVERT, + 0, 0, TestFlags({ - provideEmptyAttestations: false, - invalidProposer: false, - proposerNotProvided: false, - invalidCommitteeCommitment: false + senderIsNotProposer: false, + proposerAttestationNotProvided: false, + invalidAttestation: false, + invalidSigners: 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 +460,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 +471,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 +480,84 @@ 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); + ree.signers = new address[](_signatureCount); + 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 signersIndex = 0; + uint256 signaturesCollected = _flags.proposerAttestationNotProvided ? 0 : 1; + for (uint256 i = 0; i < ree.attestationsCount; i++) { + 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[signersIndex] = ree.committee[i]; + signaturesCollected++; + signersIndex++; } - 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)); + if (_flags.senderIsNotProposer) { + ree.sender = address(uint160(uint256(keccak256(abi.encode("invalid", ree.proposer))))); + } - vm.expectRevert( - abi.encodeWithSelector( - Errors.ValidatorSelection__InvalidCommitteeCommitment.selector, - incorrectCommitteeCommitment, - correctCommitteeCommitment - ) - ); - } + 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); + } - emit log("Time to propose"); - vm.prank(ree.proposer); - rollup.propose(args, SignatureLib.packAttestations(attestations), full.block.blobCommitments); + 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; + } - if (ree.shouldRevert) { - return; + emit log("Time to propose"); + if (_revertData != NO_REVERT) { + if (_revertData == ANY_REVERT) { + vm.expectRevert(); + } else { + vm.expectPartialRevert(_revertData); } - } 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); } - assertEq(_expectRevert, ree.shouldRevert, "Does not match revert expectation"); + vm.prank(ree.sender); + rollup.propose( + ree.proposeArgs, + SignatureLib.packAttestations(ree.attestations), + ree.signers, + full.block.blobCommitments + ); - if (ree.shouldRevert) { - return; + if (_revertData != NO_REVERT) { + return ree; } bytes32 l2ToL1MessageTreeRoot; @@ -495,7 +598,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 +611,52 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { } } - function createAttestation(address _signer, bytes32 _digest) + 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 returns (CommitteeAttestation memory) @@ -522,7 +670,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..6c812f5fa66c 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,16 @@ 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; + address[] signers; ProposePayload proposePayload; + ProposeArgs proposeArgs; } SlashFactory internal slashFactory; 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/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/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/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..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 @@ -480,6 +480,7 @@ describe('L1Publisher integration', () => { }, }, RollupContract.packAttestations([]), + [], Blob.getPrefixedEthBlobCommitments(blockBlobs), ], }); @@ -574,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 () => { diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index 08179c3eb32f..7e63130b52b3 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}`, { @@ -404,7 +405,7 @@ export class RollupContract { await this.client.simulateContract({ address: this.address, abi: RollupAbi, - functionName: 'validateHeader', + functionName: 'validateHeaderWithAttestations', args, account, }); @@ -503,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 { @@ -511,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, }); diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 098eee87efd9..38626605820a 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, @@ -91,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'; @@ -132,6 +103,10 @@ export interface Libraries { * Contract artifacts */ export interface ContractArtifacts { + /** + * The contract name. + */ + name: string; /** * The contract abi. */ @@ -146,115 +121,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; @@ -293,18 +159,18 @@ 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[] = []; - 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 +179,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 +187,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 +203,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 +219,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 +233,7 @@ export const deploySharedContracts = async ( { to: gseAddress.toString(), data: encodeFunctionData({ - abi: gseArtifact.contractAbi, + abi: GSEArtifact.contractAbi, functionName: 'setGovernance', args: [governanceAddress.toString()], }), @@ -379,7 +245,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 +254,7 @@ export const deploySharedContracts = async ( const feeAsset = getContract({ address: feeAssetAddress.toString(), - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, client: l1Client, }); @@ -400,7 +266,7 @@ export const deploySharedContracts = async ( { to: feeAssetAddress.toString(), data: encodeFunctionData({ - abi: l1Artifacts.feeAsset.contractAbi, + abi: FeeAssetArtifact.contractAbi, functionName: 'addMinter', args: [coinIssuerAddress.toString()], }), @@ -423,7 +289,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 +299,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 +332,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 +374,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 +456,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 +465,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 +546,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 +561,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 +572,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 +592,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 +610,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 +618,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 +665,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 +686,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 +703,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 +763,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 +775,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 +788,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 +820,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 +830,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()], }), @@ -1138,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/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..dfad41f889c6 --- /dev/null +++ b/yarn-project/ethereum/src/l1_artifacts.ts @@ -0,0 +1,208 @@ +import { + CoinIssuerAbi, + CoinIssuerBytecode, + ExtRollupLib2Abi, + ExtRollupLib2Bytecode, + ExtRollupLib3Abi, + ExtRollupLib3Bytecode, + 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 = { + 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, + }, + ExtRollupLib3: { + name: 'ExtRollupLib3', + contractAbi: ExtRollupLib3Abi, + contractBytecode: ExtRollupLib3Bytecode as Hex, + }, + }, + }, +}; + +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, +}; + +// 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/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" 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..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 @@ -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,9 +22,12 @@ describe('EpochProvingJobData', () => { 3: [Fr.random()], }, previousBlockHeader: await L2Block.random(0).then(b => b.header), + attestations: times(3, CommitteeAttestation.random), }; 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-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..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 @@ -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: attestations.map(a => a.toViem()) }), ); }); 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.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 }, 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/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..b04acb754b7a 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, @@ -343,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, }, { @@ -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, 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);