diff --git a/l1-contracts/script/deploy/DeployAlpha.s.sol b/l1-contracts/script/deploy/DeployAlpha.s.sol new file mode 100644 index 000000000000..39ac7b4dd502 --- /dev/null +++ b/l1-contracts/script/deploy/DeployAlpha.s.sol @@ -0,0 +1,910 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Aztec Labs. +pragma solidity >=0.8.27; + +/** + * @title DeployAlpha + * @notice Mainnet-only deployment and validation script for the alpha upgrade. + * @dev This script is intentionally self-contained: + * - All deployment inputs are hardcoded constants in this file. + * - `run()` deploys verifier, rollup, escape hatch, and payload, then calls `validate(payload)`. + * - `validate(payload)` performs strict config checks and governance execution simulation. + * + * Runtime requirements: + * - `block.chainid == 1`. + * - `DEPLOYER` must be set and correspond to the broadcast key. + * + * Expected usage: + * - Preferred deployment entrypoint: `l1-contracts/scripts/run_alpha_upgrade.sh` + * (ensures `--verify` is always included for Etherscan verification). + * - Deployment path (direct): `run()`. + * - Post-deploy revalidation path: `validate(address payload)`. + */ +import {Script} from "forge-std/Script.sol"; +import {StdAssertions} from "forge-std/StdAssertions.sol"; +import {console} from "forge-std/console.sol"; + +import {Ownable} from "@oz/access/Ownable.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + +import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; +import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; +import {EscapeHatch} from "@aztec/core/EscapeHatch.sol"; +import {Rollup} from "@aztec/core/Rollup.sol"; +import {IEscapeHatch} from "@aztec/core/interfaces/IEscapeHatch.sol"; +import {IInstance} from "@aztec/core/interfaces/IInstance.sol"; +import {IRollup, IRollupCore, GenesisState, RollupConfigInput} from "@aztec/core/interfaces/IRollup.sol"; +import {SlasherFlavor} from "@aztec/core/interfaces/ISlasher.sol"; +import {IStaking} from "@aztec/core/interfaces/IStaking.sol"; +import {IValidatorSelectionCore} from "@aztec/core/interfaces/IValidatorSelection.sol"; +import {IVerifier} from "@aztec/core/interfaces/IVerifier.sol"; +import {StakingQueueConfig} from "@aztec/core/libraries/compressed-data/StakingQueueConfig.sol"; +import {EthPerFeeAssetE12, EthValue} from "@aztec/core/libraries/rollup/FeeLib.sol"; +import {Bps, RewardConfig} from "@aztec/core/libraries/rollup/RewardLib.sol"; +import {IBooster, IBoosterCore, RewardBoostConfig} from "@aztec/core/reward-boost/RewardBooster.sol"; +import {Slasher} from "@aztec/core/slashing/Slasher.sol"; +import {TallySlashingProposer} from "@aztec/core/slashing/TallySlashingProposer.sol"; + +import {GSE, IGSE, IGSECore} from "@aztec/governance/GSE.sol"; +import {Governance} from "@aztec/governance/Governance.sol"; +import {GSEPayload} from "@aztec/governance/GSEPayload.sol"; +import {Configuration, IGovernance, Proposal, ProposalState} from "@aztec/governance/interfaces/IGovernance.sol"; +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; +import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributor.sol"; +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; + +import {FlushRewarder} from "@aztec/periphery/FlushRewarder.sol"; + +import {Epoch, Timestamp} from "@aztec/shared/libraries/TimeMath.sol"; + +import {AlphaPayload} from "@aztec/alpha/AlphaPayload.sol"; +import {HonkVerifier as RollupVerifier} from "@aztec/alpha/AlphaVerifier.sol"; + +contract DeployAlpha is Script, StdAssertions { + error DeployAlpha__NotMainnet(uint256 chainId); + error DeployAlpha__SnapshotRevertFailed(uint256 snapshotId); + + uint256 internal constant MAINNET_CHAIN_ID = 1; + + address internal constant MAINNET_REGISTRY = 0x35b22e09Ee0390539439E24f06Da43D83f90e298; + address internal constant MAINNET_GOVERNANCE = 0x1102471Eb3378FEE427121c9EfcEa452E4B6B75e; + address internal constant MAINNET_CANONICAL_ROLLUP = 0x603bb2c05D474794ea97805e8De69bCcFb3bCA12; + address internal constant MAINNET_GSE = 0xa92ecFD0E70c9cd5E5cd76c50Af0F7Da93567a4f; + address internal constant MAINNET_FEE_ASSET = 0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2; + address internal constant MAINNET_STAKING_ASSET = 0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2; + address internal constant MAINNET_REWARD_DISTRIBUTOR = 0x3D6A1B00C830C5f278FC5dFb3f6Ff0b74Db6dfe0; + address internal constant MAINNET_OLD_FLUSH_REWARDER = 0x7C9a7130379F1B5dd6e7A53AF84fC0fE32267B65; + address internal constant MAINNET_REWARD_TOKEN = 0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2; + + bytes32 internal constant MAINNET_VK_TREE_ROOT = 0x2d0b15497929f5150c4c383993555456e60d27121f4ac2cb9ef880319f5f9a6f; + bytes32 internal constant MAINNET_PROTOCOL_CONTRACTS_HASH = + 0x2672340d9a0107a7b81e6d10d25b854debe613f3272e8738e8df0ca2ff297141; + bytes32 internal constant MAINNET_GENESIS_ARCHIVE_ROOT = + 0x15684c8c3d2106918d3860f777e50555b7166adff47df13cc652e2e5a50bf5c7; + + uint256 internal constant AZTEC_SLOT_DURATION = 72; + uint256 internal constant AZTEC_EPOCH_DURATION = 32; + uint256 internal constant AZTEC_TARGET_COMMITTEE_SIZE = 48; + uint256 internal constant AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET = 2; + uint256 internal constant AZTEC_LAG_IN_EPOCHS_FOR_RANDAO = 1; + uint256 internal constant AZTEC_INBOX_LAG = 1; + uint256 internal constant AZTEC_PROOF_SUBMISSION_EPOCHS = 1; + uint256 internal constant AZTEC_ACTIVATION_THRESHOLD = 200_000e18; + uint256 internal constant AZTEC_EJECTION_THRESHOLD = 100_000e18; + uint256 internal constant AZTEC_LOCAL_EJECTION_THRESHOLD = 190_000e18; + uint256 internal constant AZTEC_MANA_TARGET = 75_000_000; + uint256 internal constant AZTEC_EXIT_DELAY_SECONDS = 345_600; + // 10 * 30$ /(150e6 * 32) / 2500$ * 1e18 = 25_000_000 wei per mana + uint256 internal constant AZTEC_PROVING_COST_PER_MANA = 25_000_000; + uint256 internal constant AZTEC_INITIAL_ETH_PER_FEE_ASSET = 11_729_988; // 0.000011729988 eth per aztec token + + uint256 internal constant AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS = 4; + uint256 internal constant AZTEC_SLASHING_QUORUM = 65; + uint256 internal constant AZTEC_SLASHING_LIFETIME_IN_ROUNDS = 34; + uint256 internal constant AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS = 28; + uint256 internal constant AZTEC_SLASHING_OFFSET_IN_ROUNDS = 2; + address internal constant AZTEC_SLASHING_VETOER = 0xBbB4aF368d02827945748b28CD4b2D42e4A37480; + uint256 internal constant AZTEC_SLASHING_DISABLE_DURATION = 259_200; + uint256 internal constant AZTEC_SLASH_AMOUNT_SMALL = 2000e18; + uint256 internal constant AZTEC_SLASH_AMOUNT_MEDIUM = 2000e18; + uint256 internal constant AZTEC_SLASH_AMOUNT_LARGE = 2000e18; + + uint256 internal constant AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE = 500; + uint256 internal constant AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE = 500; + uint256 internal constant AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN = 1; + uint256 internal constant AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT = 400; + uint256 internal constant AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE = 4; + + uint16 internal constant REWARD_SEQUENCER_BPS = 7000; + uint96 internal constant REWARD_CHECKPOINT_REWARD = 500e18; + uint256 internal constant REWARDS_CLAIMABLE_TIMESTAMP = 0; + + // https://forum.aztec.network/t/last-resort-liveness-pricing-aztecs-escape-hatch/8427 + address internal constant ESCAPE_HATCH_BOND_TOKEN = MAINNET_STAKING_ASSET; + uint96 internal constant ESCAPE_HATCH_BOND_SIZE = 332_000_000e18; + uint96 internal constant ESCAPE_HATCH_WITHDRAWAL_TAX = 1_660_000e18; + uint96 internal constant ESCAPE_HATCH_FAILED_HATCH_PUNISHMENT = 9_600_000e18; + uint256 internal constant ESCAPE_HATCH_LAG_IN_EPOCHS_FOR_SET_SIZE = 2; + uint256 internal constant ESCAPE_HATCH_LAG_IN_EPOCHS_FOR_RANDAO = 1; + uint256 internal constant ESCAPE_HATCH_LAG_IN_HATCHES = 1; + uint256 internal constant ESCAPE_HATCH_FREQUENCY = 112; + uint256 internal constant ESCAPE_HATCH_ACTIVE_DURATION = 2; + uint256 internal constant ESCAPE_HATCH_PROPOSING_EXIT_DELAY = 2_592_000; // 30 days + + bytes32 internal constant EXPECTED_HONK_VERIFIER_CREATION_HASH = + 0x5bec8ab8249c56abdb5558db3a06d01fbd598d28872da479d4ec8a924428a7ee; + bytes32 internal constant EXPECTED_HONK_VERIFIER_RUNTIME_HASH = + 0x9a0aed515ad9e25d127fc25746b81a92701c2113f894f1122d87d32d98569e28; + + bytes32 internal constant STF_STORAGE_POSITION = keccak256("aztec.stf.storage"); + bytes32 internal constant STAKING_SLOT = keccak256("aztec.core.staking.storage"); + bytes32 internal constant VALIDATOR_SELECTION_STORAGE_POSITION = keccak256("aztec.validator_selection.storage"); + uint256 internal constant STF_VK_TREE_ROOT_SLOT_OFFSET = 3; + uint256 internal constant STF_PROTOCOL_CONTRACTS_HASH_SLOT_OFFSET = 4; + uint256 internal constant STF_EPOCH_PROOF_VERIFIER_SLOT_OFFSET = 7; + uint256 internal constant STAKING_QUEUE_CONFIG_SLOT_OFFSET = 4; + uint256 internal constant ESCAPE_HATCH_CHECKPOINTS_SLOT_OFFSET = 3; + uint256 internal constant MASK_32BIT = 0xFFFFFFFF; + + struct SimulationSnapshot { + address oldCanonicalRollup; + uint256 versions; + uint256 bonusCount; + uint256 oldEffectiveCount; + uint256 totalSupply; + uint256 bonusSupply; + uint256 oldEffectiveSupply; + Configuration governanceConfig; + uint256 fundsToMove; + uint256 oldRewarderBalance; + uint256 newRewarderBalance; + } + + function getExpectedRuntimeHash() external returns (bytes32) { + bytes32 val = _computeExpectedRuntimeHash(); + emit log_named_bytes32("Verifier code hash", val); + return val; + } + + function run() external { + _assertMainnet(); + _assertHardcodedAddresses(); + + GenesisState memory genesisState = _getGenesisState(); + RollupConfigInput memory config = _buildRollupConfiguration(IRewardDistributor(MAINNET_REWARD_DISTRIBUTOR)); + address deployer = vm.envAddress("DEPLOYER"); + + vm.startBroadcast(deployer); + + IVerifier verifier = IVerifier(address(new RollupVerifier())); + Rollup rollup = new Rollup( + IERC20(MAINNET_FEE_ASSET), + IERC20(MAINNET_STAKING_ASSET), + GSE(MAINNET_GSE), + verifier, + address(MAINNET_GOVERNANCE), + genesisState, + config + ); + IEscapeHatch escapeHatch = IEscapeHatch( + address( + new EscapeHatch( + address(rollup), + ESCAPE_HATCH_BOND_TOKEN, + ESCAPE_HATCH_BOND_SIZE, + ESCAPE_HATCH_WITHDRAWAL_TAX, + ESCAPE_HATCH_FAILED_HATCH_PUNISHMENT, + ESCAPE_HATCH_FREQUENCY, + ESCAPE_HATCH_ACTIVE_DURATION, + ESCAPE_HATCH_LAG_IN_HATCHES, + ESCAPE_HATCH_PROPOSING_EXIT_DELAY + ) + ) + ); + AlphaPayload payload = new AlphaPayload( + IRegistry(MAINNET_REGISTRY), IInstance(address(rollup)), FlushRewarder(MAINNET_OLD_FLUSH_REWARDER), escapeHatch + ); + + vm.stopBroadcast(); + + validate(payload); + _writeOutputJson(rollup, verifier, payload, escapeHatch); + + console.log("Payload address", address(payload)); + } + + function validate(AlphaPayload _payloadToValidate) public { + _assertMainnet(); + assertRollupConfiguration(_payloadToValidate); + simulateExecution(_payloadToValidate); + } + + function assertRollupConfiguration(AlphaPayload _payloadToValidate) public { + _assertMainnet(); + console.log("[assert] rollup configuration start"); + assertNotEq(address(_payloadToValidate), address(0), "payload is zero"); + + IInstance rollup = _payloadToValidate.ROLLUP(); + IRollup rollupCore = IRollup(address(rollup)); + _validateVerifierPinning(); + _validatePayloadImmutables(_payloadToValidate, rollup); + _validatePayloadActions(_payloadToValidate); + _validateEscapeHatchConfig(_payloadToValidate, rollup); + _validateRollupGetterConfig(rollup); + _validateRollupStorageConfig(rollupCore); + _validateStakingQueueConfig(rollupCore); + _validateSlasherStack(rollup); + console.log(unicode"[assert] rollup configuration ✓"); + } + + function simulateExecution(AlphaPayload _payloadToValidate) public { + _assertMainnet(); + console.log("[assert] simulate execution start"); + assertNotEq(address(_payloadToValidate), address(0), "payload is zero"); + + IRegistry registry = _payloadToValidate.REGISTRY(); + Governance governance = Governance(address(_payloadToValidate.GOVERNANCE())); + IInstance newRollup = _payloadToValidate.ROLLUP(); + FlushRewarder oldRewarder = _payloadToValidate.OLD_FLUSH_REWARDER(); + FlushRewarder newRewarder = _payloadToValidate.NEW_FLUSH_REWARDER(); + IERC20 rewardAsset = _payloadToValidate.REWARD_ASSET(); + IGSE gse = IGSE(address(newRollup.getGSE())); + Epoch currentEpoch = newRollup.getCurrentEpoch(); + + assertEq(address(newRollup.getEscapeHatch()), address(0), "sim pre escape hatch mismatch"); + assertEq(address(newRollup.getEscapeHatchForEpoch(currentEpoch)), address(0), "sim pre epoch escape hatch mismatch"); + assertEq(_getEscapeHatchCheckpointsLength(newRollup), 0, "sim pre escape hatch checkpoints mismatch"); + + SimulationSnapshot memory before = + _captureSimulationSnapshot(registry, governance, gse, oldRewarder, newRewarder, rewardAsset); + assertEq(before.oldCanonicalRollup, MAINNET_CANONICAL_ROLLUP, "sim pre canonical mismatch"); + assertEq(address(registry.getCanonicalRollup()), before.oldCanonicalRollup, "sim pre canonical drift"); + assertEq(gse.getLatestRollup(), before.oldCanonicalRollup, "sim pre latest mismatch"); + assertFalse(gse.isRollupRegistered(address(newRollup)), "sim pre rollup already in gse"); + + uint256 version = newRollup.getVersion(); + vm.expectRevert(); + registry.getRollup(version); + + uint256 snapshotId = vm.snapshotState(); + + _executePayloadThroughGovernance(_payloadToValidate, registry, governance, gse, before.oldCanonicalRollup); + + _assertSimulationPostState( + _payloadToValidate, registry, governance, gse, oldRewarder, newRewarder, rewardAsset, before + ); + + bool reverted = vm.revertToState(snapshotId); + if (!reverted) { + revert DeployAlpha__SnapshotRevertFailed(snapshotId); + } + console.log(unicode"[assert] simulate execution ✓"); + } + + function _assertMainnet() private view { + if (block.chainid != MAINNET_CHAIN_ID) { + revert DeployAlpha__NotMainnet(block.chainid); + } + } + + function _assertHardcodedAddresses() private view { + console.log("[assert] hardcoded addresses match start"); + IRegistry registry = IRegistry(MAINNET_REGISTRY); + IStaking canonicalRollup = IStaking(MAINNET_CANONICAL_ROLLUP); + IRollup canonicalRollupWithFees = IRollup(MAINNET_CANONICAL_ROLLUP); + + assertEq(registry.getGovernance(), MAINNET_GOVERNANCE, "governance mismatch"); + assertEq(address(registry.getCanonicalRollup()), MAINNET_CANONICAL_ROLLUP, "canonical rollup mismatch"); + assertEq(address(registry.getRewardDistributor()), MAINNET_REWARD_DISTRIBUTOR, "reward distributor mismatch"); + assertEq(address(canonicalRollup.getGSE()), MAINNET_GSE, "gse mismatch"); + assertEq(address(canonicalRollupWithFees.getFeeAsset()), MAINNET_FEE_ASSET, "fee asset mismatch"); + assertEq(address(canonicalRollup.getStakingAsset()), MAINNET_STAKING_ASSET, "staking asset mismatch"); + assertEq(address(FlushRewarder(MAINNET_OLD_FLUSH_REWARDER).REWARD_ASSET()), MAINNET_REWARD_TOKEN, "token mismatch"); + console.log(unicode"[assert] hardcoded addresses match ✓"); + } + + function _buildRollupConfiguration(IRewardDistributor rewardDistributor) + private + pure + returns (RollupConfigInput memory) + { + uint256 slashingRoundSize = AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS * AZTEC_EPOCH_DURATION; + + RollupConfigInput memory config = RollupConfigInput({ + aztecSlotDuration: AZTEC_SLOT_DURATION, + aztecEpochDuration: AZTEC_EPOCH_DURATION, + targetCommitteeSize: AZTEC_TARGET_COMMITTEE_SIZE, + lagInEpochsForValidatorSet: AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET, + lagInEpochsForRandao: AZTEC_LAG_IN_EPOCHS_FOR_RANDAO, + aztecProofSubmissionEpochs: AZTEC_PROOF_SUBMISSION_EPOCHS, + slashingQuorum: AZTEC_SLASHING_QUORUM, + slashingRoundSize: slashingRoundSize, + slashingLifetimeInRounds: AZTEC_SLASHING_LIFETIME_IN_ROUNDS, + slashingExecutionDelayInRounds: AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS, + slashAmounts: [AZTEC_SLASH_AMOUNT_SMALL, AZTEC_SLASH_AMOUNT_MEDIUM, AZTEC_SLASH_AMOUNT_LARGE], + slashingOffsetInRounds: AZTEC_SLASHING_OFFSET_IN_ROUNDS, + slasherFlavor: SlasherFlavor.TALLY, + slashingVetoer: AZTEC_SLASHING_VETOER, + slashingDisableDuration: AZTEC_SLASHING_DISABLE_DURATION, + manaTarget: AZTEC_MANA_TARGET, + exitDelaySeconds: AZTEC_EXIT_DELAY_SECONDS, + version: 0, + provingCostPerMana: EthValue.wrap(AZTEC_PROVING_COST_PER_MANA), + initialEthPerFeeAsset: EthPerFeeAssetE12.wrap(AZTEC_INITIAL_ETH_PER_FEE_ASSET), + rewardConfig: _getRewardConfiguration(rewardDistributor), + rewardBoostConfig: _getRewardBoostConfiguration(), + stakingQueueConfig: _getStakingQueueConfiguration(), + localEjectionThreshold: AZTEC_LOCAL_EJECTION_THRESHOLD, + earliestRewardsClaimableTimestamp: Timestamp.wrap(REWARDS_CLAIMABLE_TIMESTAMP), + inboxLag: AZTEC_INBOX_LAG + }); + + config.version = _computeConfigVersion(config, _getGenesisState()); + return config; + } + + function _getRewardConfiguration(IRewardDistributor rewardDistributor) private pure returns (RewardConfig memory) { + return RewardConfig({ + rewardDistributor: rewardDistributor, + sequencerBps: Bps.wrap(REWARD_SEQUENCER_BPS), + booster: IBoosterCore(address(0)), + checkpointReward: REWARD_CHECKPOINT_REWARD + }); + } + + function _getRewardBoostConfiguration() private pure returns (RewardBoostConfig memory) { + return RewardBoostConfig({increment: 125_000, maxScore: 15_000_000, a: 1000, minimum: 100_000, k: 1_000_000}); + } + + function _getStakingQueueConfiguration() private pure returns (StakingQueueConfig memory) { + return StakingQueueConfig({ + bootstrapValidatorSetSize: AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE, + bootstrapFlushSize: AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE, + normalFlushSizeMin: AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN, + normalFlushSizeQuotient: AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT, + maxQueueFlushSize: AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE + }); + } + + function _getGenesisState() private pure returns (GenesisState memory) { + return GenesisState({ + vkTreeRoot: MAINNET_VK_TREE_ROOT, + protocolContractsHash: MAINNET_PROTOCOL_CONTRACTS_HASH, + genesisArchiveRoot: MAINNET_GENESIS_ARCHIVE_ROOT + }); + } + + function _computeConfigVersion(RollupConfigInput memory config, GenesisState memory genesisState) + private + pure + returns (uint32) + { + bytes32 hash = keccak256(abi.encode(config, genesisState)); + return uint32(bytes4(hash)); + } + + function _computeExpectedRuntimeHash() private returns (bytes32) { + return address(new RollupVerifier()).codehash; + } + + function _validateVerifierPinning() private returns (bytes32 expectedRuntimeHash) { + console.log("[assert] verifier pinning start"); + assertEq( + keccak256(type(RollupVerifier).creationCode), + EXPECTED_HONK_VERIFIER_CREATION_HASH, + "bad verifier creation code hash" + ); + expectedRuntimeHash = _computeExpectedRuntimeHash(); + assertEq(expectedRuntimeHash, EXPECTED_HONK_VERIFIER_RUNTIME_HASH, "bad runtime hash constant"); + console.log(unicode"[assert] verifier pinning ✓"); + } + + function _validatePayloadImmutables(AlphaPayload _payloadToValidate, IInstance instance) private view { + console.log("[assert] payload immutables start"); + FlushRewarder oldRewarder = _payloadToValidate.OLD_FLUSH_REWARDER(); + FlushRewarder newRewarder = _payloadToValidate.NEW_FLUSH_REWARDER(); + IERC20 rewardAsset = _payloadToValidate.REWARD_ASSET(); + + assertEq(address(_payloadToValidate.REGISTRY()), MAINNET_REGISTRY, "payload registry mismatch"); + assertEq(address(_payloadToValidate.GOVERNANCE()), MAINNET_GOVERNANCE, "payload governance mismatch"); + assertEq(address(_payloadToValidate.ROLLUP()), address(instance), "payload rollup mismatch"); + assertNotEq(address(instance), MAINNET_CANONICAL_ROLLUP, "payload rollup already canonical"); + assertNotEq(address(_payloadToValidate.ESCAPE_HATCH()), address(0), "payload escape hatch missing"); + assertEq(address(oldRewarder), MAINNET_OLD_FLUSH_REWARDER, "payload old rewarder mismatch"); + assertEq(address(rewardAsset), MAINNET_REWARD_TOKEN, "payload reward asset mismatch"); + assertEq(address(oldRewarder.REWARD_ASSET()), MAINNET_REWARD_TOKEN, "old rewarder asset mismatch"); + assertEq(Ownable(address(oldRewarder)).owner(), MAINNET_GOVERNANCE, "old rewarder owner mismatch"); + assertEq(address(oldRewarder.ROLLUP()), MAINNET_CANONICAL_ROLLUP, "old rewarder rollup mismatch"); + + assertNotEq(address(newRewarder), address(oldRewarder), "rewarder alias mismatch"); + assertEq(Ownable(address(newRewarder)).owner(), MAINNET_GOVERNANCE, "new rewarder owner mismatch"); + assertEq(address(newRewarder.ROLLUP()), address(instance), "new rewarder rollup mismatch"); + assertEq(address(newRewarder.REWARD_ASSET()), address(oldRewarder.REWARD_ASSET()), "new rewarder asset mismatch"); + assertEq(newRewarder.rewardPerInsertion(), oldRewarder.rewardPerInsertion(), "new rewarder rate mismatch"); + assertEq(rewardAsset.balanceOf(address(newRewarder)), 0, "new rewarder unexpectedly funded"); + assertGt(bytes(_payloadToValidate.getURI()).length, 0, "payload uri empty"); + console.log(unicode"[assert] payload immutables ✓"); + } + + function _validatePayloadActions(AlphaPayload _payloadToValidate) private view { + console.log("[assert] payload actions start"); + IPayload.Action[] memory actions = _payloadToValidate.getActions(); + assertEq(actions.length, 6, "payload action count mismatch"); + + bytes memory expectedAddRegistry = + abi.encodeWithSelector(IRegistry.addRollup.selector, address(_payloadToValidate.ROLLUP())); + assertEq(actions[0].target, address(_payloadToValidate.REGISTRY()), "action0 target mismatch"); + assertEq(keccak256(actions[0].data), keccak256(expectedAddRegistry), "action0 data mismatch"); + + bytes memory expectedAddGse = + abi.encodeWithSelector(IGSECore.addRollup.selector, address(_payloadToValidate.ROLLUP())); + assertEq(actions[1].target, address(_payloadToValidate.ROLLUP().getGSE()), "action1 target mismatch"); + assertEq(keccak256(actions[1].data), keccak256(expectedAddGse), "action1 data mismatch"); + + Configuration memory config = _payloadToValidate.GOVERNANCE().getConfiguration(); + config.executionDelay = Timestamp.wrap(30 days); + bytes memory expectedConfig = abi.encodeWithSelector(IGovernance.updateConfiguration.selector, config); + bytes memory expectedSetRewardsClaimable = abi.encodeWithSelector(IRollupCore.setRewardsClaimable.selector, true); + assertEq(actions[2].target, address(_payloadToValidate.ROLLUP()), "action2 target mismatch"); + assertEq(keccak256(actions[2].data), keccak256(expectedSetRewardsClaimable), "action2 data mismatch"); + + bytes memory expectedUpdateEscapeHatch = abi.encodeWithSelector( + IValidatorSelectionCore.updateEscapeHatch.selector, address(_payloadToValidate.ESCAPE_HATCH()) + ); + assertEq(actions[3].target, address(_payloadToValidate.ROLLUP()), "action3 target mismatch"); + assertEq(keccak256(actions[3].data), keccak256(expectedUpdateEscapeHatch), "action3 data mismatch"); + + bytes memory expectedRecover = abi.encodeWithSelector( + FlushRewarder.recover.selector, + address(_payloadToValidate.REWARD_ASSET()), + address(_payloadToValidate.NEW_FLUSH_REWARDER()), + _payloadToValidate.OLD_FLUSH_REWARDER().rewardsAvailable() + ); + assertEq(actions[4].target, address(_payloadToValidate.OLD_FLUSH_REWARDER()), "action4 target mismatch"); + assertEq(keccak256(actions[4].data), keccak256(expectedRecover), "action4 data mismatch"); + + assertEq(actions[5].target, address(_payloadToValidate.GOVERNANCE()), "action5 target mismatch"); + assertEq(keccak256(actions[5].data), keccak256(expectedConfig), "action5 data mismatch"); + console.log(unicode"[assert] payload actions ✓"); + } + + function _validateEscapeHatchConfig(AlphaPayload _payloadToValidate, IInstance instance) private view { + console.log("[assert] escape hatch config start"); + EscapeHatch escapeHatch = EscapeHatch(address(_payloadToValidate.ESCAPE_HATCH())); + + assertEq(escapeHatch.getRollup(), address(instance), "escape hatch rollup mismatch"); + assertEq(escapeHatch.getBondToken(), ESCAPE_HATCH_BOND_TOKEN, "escape hatch bond token mismatch"); + assertEq(escapeHatch.getBondSize(), ESCAPE_HATCH_BOND_SIZE, "escape hatch bond size mismatch"); + assertEq(escapeHatch.getWithdrawalTax(), ESCAPE_HATCH_WITHDRAWAL_TAX, "escape hatch withdrawal tax mismatch"); + assertEq( + escapeHatch.getFailedHatchPunishment(), + ESCAPE_HATCH_FAILED_HATCH_PUNISHMENT, + "escape hatch failed punishment mismatch" + ); + assertEq(escapeHatch.getFrequency(), ESCAPE_HATCH_FREQUENCY, "escape hatch frequency mismatch"); + assertEq(escapeHatch.getActiveDuration(), ESCAPE_HATCH_ACTIVE_DURATION, "escape hatch active duration mismatch"); + assertEq(escapeHatch.getLagInHatches(), ESCAPE_HATCH_LAG_IN_HATCHES, "escape hatch lag in hatches mismatch"); + assertEq( + escapeHatch.getProposingExitDelay(), + ESCAPE_HATCH_PROPOSING_EXIT_DELAY, + "escape hatch proposing exit delay mismatch" + ); + assertEq( + escapeHatch.LAG_IN_EPOCHS_FOR_SET_SIZE(), + ESCAPE_HATCH_LAG_IN_EPOCHS_FOR_SET_SIZE, + "escape hatch lag in epochs for set size mismatch" + ); + assertEq( + escapeHatch.LAG_IN_EPOCHS_FOR_RANDAO(), + ESCAPE_HATCH_LAG_IN_EPOCHS_FOR_RANDAO, + "escape hatch lag in epochs for randao mismatch" + ); + assertEq(escapeHatch.getCandidateCount(), 0, "escape hatch candidate count mismatch"); + console.log(unicode"[assert] escape hatch config ✓"); + } + + function _validateRollupGetterConfig(IInstance rollup) private view { + console.log("[assert] rollup getter config start"); + IRollup rollupCore = IRollup(address(rollup)); + RollupConfigInput memory expectedConfig = _buildRollupConfiguration(IRewardDistributor(MAINNET_REWARD_DISTRIBUTOR)); + RewardConfig memory rewardConfig = rollupCore.getRewardConfig(); + assertNotEq(address(rewardConfig.booster), address(0), "booster missing"); + IBooster booster = IBooster(address(rewardConfig.booster)); + RewardBoostConfig memory boostConfig = booster.getConfig(); + + assertEq(Ownable(address(rollup)).owner(), MAINNET_GOVERNANCE, "rollup owner mismatch"); + assertEq(address(rollupCore.getFeeAsset()), MAINNET_FEE_ASSET, "rollup fee asset mismatch"); + assertEq(address(rollup.getStakingAsset()), MAINNET_STAKING_ASSET, "rollup staking asset mismatch"); + assertEq(rollup.getActivationThreshold(), AZTEC_ACTIVATION_THRESHOLD, "activation threshold mismatch"); + assertEq(rollup.getEjectionThreshold(), AZTEC_EJECTION_THRESHOLD, "ejection threshold mismatch"); + assertEq(address(rollup.getGSE()), MAINNET_GSE, "rollup gse mismatch"); + assertEq(address(rollupCore.getRewardDistributor()), MAINNET_REWARD_DISTRIBUTOR, "rollup distributor mismatch"); + assertEq(rollup.getSlotDuration(), AZTEC_SLOT_DURATION, "slot duration mismatch"); + assertEq(rollup.getEpochDuration(), AZTEC_EPOCH_DURATION, "epoch duration mismatch"); + assertEq(rollupCore.getProofSubmissionEpochs(), AZTEC_PROOF_SUBMISSION_EPOCHS, "proof epochs mismatch"); + assertEq(rollup.getTargetCommitteeSize(), AZTEC_TARGET_COMMITTEE_SIZE, "committee size mismatch"); + assertEq(rollup.getLagInEpochsForValidatorSet(), AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET, "validator lag mismatch"); + assertEq(rollup.getLagInEpochsForRandao(), AZTEC_LAG_IN_EPOCHS_FOR_RANDAO, "randao lag mismatch"); + assertEq(rollupCore.getManaTarget(), AZTEC_MANA_TARGET, "mana target mismatch"); + assertEq( + EthValue.unwrap(rollupCore.getProvingCostPerManaInEth()), AZTEC_PROVING_COST_PER_MANA, "proving cost mismatch" + ); + assertEq( + EthPerFeeAssetE12.unwrap(rollupCore.getEthPerFeeAsset()), AZTEC_INITIAL_ETH_PER_FEE_ASSET, "eth per fee mismatch" + ); + assertEq(Timestamp.unwrap(rollup.getExitDelay()), AZTEC_EXIT_DELAY_SECONDS, "exit delay mismatch"); + assertEq(rollup.getLocalEjectionThreshold(), AZTEC_LOCAL_EJECTION_THRESHOLD, "ejection threshold mismatch"); + assertEq(address(rewardConfig.rewardDistributor), MAINNET_REWARD_DISTRIBUTOR, "reward distributor config mismatch"); + assertEq(Bps.unwrap(rewardConfig.sequencerBps), REWARD_SEQUENCER_BPS, "sequencer bps mismatch"); + assertEq(rewardConfig.checkpointReward, REWARD_CHECKPOINT_REWARD, "checkpoint reward mismatch"); + assertEq(boostConfig.increment, 125_000, "boost increment mismatch"); + assertEq(boostConfig.maxScore, 15_000_000, "boost max score mismatch"); + assertEq(boostConfig.a, 1000, "boost a mismatch"); + assertEq(boostConfig.minimum, 100_000, "boost minimum mismatch"); + assertEq(boostConfig.k, 1_000_000, "boost k mismatch"); + assertEq( + Timestamp.unwrap(rollupCore.getEarliestRewardsClaimableTimestamp()), + REWARDS_CLAIMABLE_TIMESTAMP, + "earliest reward mismatch" + ); + assertFalse(rollupCore.isRewardsClaimable(), "rewards unexpectedly claimable before payload execution"); + assertEq(Inbox(address(rollupCore.getInbox())).LAG(), AZTEC_INBOX_LAG, "inbox lag mismatch"); + assertNotEq(address(rollupCore.getInbox()), address(0), "inbox missing"); + assertNotEq(address(rollupCore.getOutbox()), address(0), "outbox missing"); + assertEq(Inbox(address(rollupCore.getInbox())).ROLLUP(), address(rollup), "inbox rollup mismatch"); + assertEq(Inbox(address(rollupCore.getInbox())).VERSION(), expectedConfig.version, "inbox version mismatch"); + assertEq( + Inbox(address(rollupCore.getInbox())).FEE_ASSET_PORTAL(), + address(rollupCore.getFeeAssetPortal()), + "portal mismatch" + ); + assertEq(address(Outbox(address(rollupCore.getOutbox())).ROLLUP()), address(rollup), "outbox rollup mismatch"); + assertEq(Outbox(address(rollupCore.getOutbox())).VERSION(), expectedConfig.version, "outbox version mismatch"); + assertEq(rollupCore.getVersion(), expectedConfig.version, "rollup version mismatch"); + assertEq(rollupCore.archiveAt(0), MAINNET_GENESIS_ARCHIVE_ROOT, "genesis archive mismatch"); + assertEq(rollupCore.getBurnAddress(), address(bytes20("CUAUHXICALLI")), "burn address mismatch"); + assertFalse(rollup.getIsBootstrapped(), "rollup bootstrapped unexpectedly"); + console.log(unicode"[assert] rollup getter config ✓"); + } + + function _validateRollupStorageConfig(IRollup rollup) private view { + console.log("[assert] rollup storage config start"); + uint256 stfBase = uint256(STF_STORAGE_POSITION); + bytes32 vkTreeRootSlot = vm.load(address(rollup), bytes32(stfBase + STF_VK_TREE_ROOT_SLOT_OFFSET)); + bytes32 protocolContractsHashSlot = + vm.load(address(rollup), bytes32(stfBase + STF_PROTOCOL_CONTRACTS_HASH_SLOT_OFFSET)); + bytes32 verifierSlot = vm.load(address(rollup), bytes32(stfBase + STF_EPOCH_PROOF_VERIFIER_SLOT_OFFSET)); + address verifierAddress = address(uint160(uint256(verifierSlot))); + + assertEq(vkTreeRootSlot, MAINNET_VK_TREE_ROOT, "vk tree root mismatch"); + assertEq(protocolContractsHashSlot, MAINNET_PROTOCOL_CONTRACTS_HASH, "protocol contracts hash mismatch"); + assertNotEq(verifierAddress, address(0), "verifier address missing"); + assertEq(verifierAddress.codehash, EXPECTED_HONK_VERIFIER_RUNTIME_HASH, "verifier runtime mismatch"); + console.log(unicode"[assert] rollup storage config ✓"); + } + + function _validateStakingQueueConfig(IRollup rollup) private view { + console.log("[assert] staking queue config start"); + uint256 stakingBase = uint256(STAKING_SLOT); + uint256 packed = uint256(vm.load(address(rollup), bytes32(stakingBase + STAKING_QUEUE_CONFIG_SLOT_OFFSET))); + uint256 bootstrapValidatorSetSize = (packed >> 128) & MASK_32BIT; + uint256 bootstrapFlushSize = (packed >> 96) & MASK_32BIT; + uint256 normalFlushSizeMin = (packed >> 64) & MASK_32BIT; + uint256 normalFlushSizeQuotient = (packed >> 32) & MASK_32BIT; + uint256 maxQueueFlushSize = packed & MASK_32BIT; + + assertEq( + bootstrapValidatorSetSize, AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE, "queue bootstrap validator mismatch" + ); + assertEq(bootstrapFlushSize, AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE, "queue bootstrap flush mismatch"); + assertEq(normalFlushSizeMin, AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN, "queue normal min mismatch"); + assertEq(normalFlushSizeQuotient, AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT, "queue normal quotient mismatch"); + assertEq(maxQueueFlushSize, AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE, "queue max flush mismatch"); + console.log(unicode"[assert] staking queue config ✓"); + } + + function _getEscapeHatchCheckpointsLength(IInstance instance) private view returns (uint256) { + uint256 validatorSelectionBase = uint256(VALIDATOR_SELECTION_STORAGE_POSITION); + bytes32 len = vm.load(address(instance), bytes32(validatorSelectionBase + ESCAPE_HATCH_CHECKPOINTS_SLOT_OFFSET)); + return uint256(len); + } + + function _validateSlasherStack(IInstance instance) private view { + console.log("[assert] slasher stack start"); + address slasherAddress = instance.getSlasher(); + Slasher slasher = Slasher(slasherAddress); + address proposerAddress = slasher.PROPOSER(); + TallySlashingProposer proposer = TallySlashingProposer(proposerAddress); + + assertNotEq(slasherAddress, address(0), "slasher missing"); + assertEq(slasher.GOVERNANCE(), MAINNET_GOVERNANCE, "slasher governance mismatch"); + assertEq(slasher.VETOER(), AZTEC_SLASHING_VETOER, "slasher vetoer mismatch"); + assertEq(slasher.SLASHING_DISABLE_DURATION(), AZTEC_SLASHING_DISABLE_DURATION, "slasher disable mismatch"); + assertNotEq(proposerAddress, address(0), "slasher proposer missing"); + assertEq(uint256(proposer.SLASHING_PROPOSER_TYPE()), uint256(SlasherFlavor.TALLY), "slasher flavor mismatch"); + assertEq(proposer.INSTANCE(), address(instance), "proposer instance mismatch"); + assertEq(address(proposer.SLASHER()), slasherAddress, "proposer slasher mismatch"); + assertEq(proposer.QUORUM(), AZTEC_SLASHING_QUORUM, "proposer quorum mismatch"); + assertEq( + proposer.ROUND_SIZE(), AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS * AZTEC_EPOCH_DURATION, "proposer round mismatch" + ); + assertEq(proposer.ROUND_SIZE_IN_EPOCHS(), AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS, "proposer epoch round mismatch"); + assertEq(proposer.LIFETIME_IN_ROUNDS(), AZTEC_SLASHING_LIFETIME_IN_ROUNDS, "proposer lifetime mismatch"); + assertEq( + proposer.EXECUTION_DELAY_IN_ROUNDS(), + AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS, + "proposer execution delay mismatch" + ); + assertEq(proposer.SLASH_OFFSET_IN_ROUNDS(), AZTEC_SLASHING_OFFSET_IN_ROUNDS, "proposer slash offset mismatch"); + assertEq(proposer.SLASH_AMOUNT_SMALL(), AZTEC_SLASH_AMOUNT_SMALL, "proposer slash small mismatch"); + assertEq(proposer.SLASH_AMOUNT_MEDIUM(), AZTEC_SLASH_AMOUNT_MEDIUM, "proposer slash medium mismatch"); + assertEq(proposer.SLASH_AMOUNT_LARGE(), AZTEC_SLASH_AMOUNT_LARGE, "proposer slash large mismatch"); + assertEq(proposer.COMMITTEE_SIZE(), AZTEC_TARGET_COMMITTEE_SIZE, "proposer committee size mismatch"); + console.log(unicode"[assert] slasher stack ✓"); + } + + function _captureSimulationSnapshot( + IRegistry registry, + IGovernance governance, + IGSE gse, + FlushRewarder oldRewarder, + FlushRewarder newRewarder, + IERC20 rewardAsset + ) private view returns (SimulationSnapshot memory snap) { + Timestamp ts = Timestamp.wrap(block.timestamp); + address oldCanonicalRollup = address(registry.getCanonicalRollup()); + address bonusInstance = gse.getBonusInstanceAddress(); + + snap.oldCanonicalRollup = oldCanonicalRollup; + snap.versions = registry.numberOfVersions(); + snap.bonusCount = gse.getAttesterCountAtTime(bonusInstance, ts); + snap.oldEffectiveCount = gse.getAttesterCountAtTime(oldCanonicalRollup, ts); + snap.totalSupply = gse.totalSupply(); + snap.bonusSupply = gse.supplyOf(bonusInstance); + snap.oldEffectiveSupply = gse.supplyOf(oldCanonicalRollup) + snap.bonusSupply; + snap.governanceConfig = governance.getConfiguration(); + snap.fundsToMove = oldRewarder.rewardsAvailable(); + snap.oldRewarderBalance = rewardAsset.balanceOf(address(oldRewarder)); + snap.newRewarderBalance = rewardAsset.balanceOf(address(newRewarder)); + } + + function _validateGsePayloadWrapper(AlphaPayload payload, GSEPayload gsePayload) private view { + IPayload.Action[] memory payloadActions = payload.getActions(); + IPayload.Action[] memory wrappedActions = gsePayload.getActions(); + assertEq(wrappedActions.length, payloadActions.length + 1, "sim wrapped action count mismatch"); + + for (uint256 i = 0; i < payloadActions.length; i++) { + assertEq(wrappedActions[i].target, payloadActions[i].target, "sim wrapped target mismatch"); + assertEq(keccak256(wrappedActions[i].data), keccak256(payloadActions[i].data), "sim wrapped data mismatch"); + } + + uint256 terminalIndex = wrappedActions.length - 1; + assertEq(wrappedActions[terminalIndex].target, address(gsePayload), "sim wrapped terminal target mismatch"); + assertEq( + keccak256(wrappedActions[terminalIndex].data), + keccak256(abi.encodeWithSelector(GSEPayload.amIValid.selector)), + "sim wrapped terminal data mismatch" + ); + } + + function _executePayloadThroughGovernance( + AlphaPayload payload, + IRegistry registry, + Governance governance, + IGSE gse, + address oldCanonicalRollup + ) private { + GSEPayload gsePayload = new GSEPayload(IPayload(address(payload)), gse, registry); + _validateGsePayloadWrapper(payload, gsePayload); + + vm.prank(governance.governanceProposer()); + uint256 proposalId = governance.propose(IPayload(address(gsePayload))); + + Proposal memory proposal = governance.getProposal(proposalId); + assertEq( + uint256(governance.getProposalState(proposalId)), uint256(ProposalState.Pending), "sim proposal not pending" + ); + assertEq(proposal.proposer, governance.governanceProposer(), "sim proposal proposer mismatch"); + assertEq(address(proposal.payload), address(gsePayload), "sim proposal payload mismatch"); + assertEq( + address(GSEPayload(address(proposal.payload)).getOriginalPayload()), + address(payload), + "sim proposal original mismatch" + ); + + Timestamp pendingThrough = _pendingThrough(proposal); + Timestamp activeThrough = _activeThrough(proposal); + Timestamp queuedThrough = _queuedThrough(proposal); + + vm.warp(Timestamp.unwrap(pendingThrough) + 1); + assertEq(uint256(governance.getProposalState(proposalId)), uint256(ProposalState.Active), "sim proposal not active"); + + uint256 oldRollupVoteAmount = gse.getVotingPowerAt(oldCanonicalRollup, pendingThrough); + if (oldRollupVoteAmount > 0) { + vm.prank(oldCanonicalRollup); + gse.vote(proposalId, oldRollupVoteAmount, true); + } + + uint256 bonusVoteAmount = gse.getVotingPowerAt(gse.getBonusInstanceAddress(), pendingThrough); + if (bonusVoteAmount > 0) { + vm.prank(oldCanonicalRollup); + gse.voteWithBonus(proposalId, bonusVoteAmount, true); + } + + vm.warp(Timestamp.unwrap(activeThrough) + 1); + assertEq(uint256(governance.getProposalState(proposalId)), uint256(ProposalState.Queued), "sim proposal not queued"); + + vm.warp(Timestamp.unwrap(queuedThrough) + 1); + assertEq( + uint256(governance.getProposalState(proposalId)), uint256(ProposalState.Executable), "sim proposal not executable" + ); + + governance.execute(proposalId); + assertEq( + uint256(governance.getProposalState(proposalId)), uint256(ProposalState.Executed), "sim proposal not executed" + ); + } + + function _assertSimulationPostState( + AlphaPayload payload, + IRegistry registry, + IGovernance governance, + IGSE gse, + FlushRewarder oldRewarder, + FlushRewarder newRewarder, + IERC20 rewardAsset, + SimulationSnapshot memory before + ) private view { + console.log("[assert] post execution state start"); + assertNotEq(before.oldCanonicalRollup, address(payload.ROLLUP()), "sim old/new rollup alias"); + assertEq(address(registry.getCanonicalRollup()), address(payload.ROLLUP()), "sim canonical mismatch"); + assertNotEq(address(registry.getCanonicalRollup()), before.oldCanonicalRollup, "sim canonical unchanged"); + assertEq(gse.getLatestRollup(), address(registry.getCanonicalRollup()), "sim canonical/latest mismatch"); + + { + uint256 versionsAfter = registry.numberOfVersions(); + assertEq(versionsAfter, before.versions + 1, "sim versions mismatch"); + + uint256 newVersion = payload.ROLLUP().getVersion(); + assertEq(registry.getVersion(before.versions), newVersion, "sim latest version mismatch"); + assertEq(address(registry.getRollup(newVersion)), address(payload.ROLLUP()), "sim version mapping mismatch"); + } + + assertEq(gse.getLatestRollup(), address(payload.ROLLUP()), "sim gse latest mismatch"); + assertTrue(gse.isRollupRegistered(before.oldCanonicalRollup), "sim old gse rollup missing"); + assertTrue(gse.isRollupRegistered(address(payload.ROLLUP())), "sim new gse rollup missing"); + assertTrue(IRollup(address(payload.ROLLUP())).isRewardsClaimable(), "sim rewards not claimable"); + _assertEscapeHatchPostState(payload); + + _assertGseFollowerStateAfterSimulation(payload, gse, before); + + _assertGovernanceConfigurationAfterSimulation(before.governanceConfig, governance.getConfiguration()); + _assertRewardMigrationAfterSimulation(oldRewarder, newRewarder, rewardAsset, before); + console.log(unicode"[assert] post execution state ✓"); + } + + function _assertEscapeHatchPostState(AlphaPayload payload) private view { + assertEq(address(payload.ROLLUP().getEscapeHatch()), address(payload.ESCAPE_HATCH()), "sim escape hatch mismatch"); + + Epoch executionEpoch = payload.ROLLUP().getCurrentEpoch(); + assertEq( + address(payload.ROLLUP().getEscapeHatchForEpoch(executionEpoch)), + address(0), + "sim current epoch escape hatch mismatch" + ); + + Epoch nextEpoch = Epoch.wrap(Epoch.unwrap(executionEpoch) + 1); + assertEq( + address(payload.ROLLUP().getEscapeHatchForEpoch(nextEpoch)), + address(payload.ESCAPE_HATCH()), + "sim next epoch escape hatch mismatch" + ); + assertEq(_getEscapeHatchCheckpointsLength(payload.ROLLUP()), 1, "sim escape hatch checkpoints mismatch"); + } + + function _assertGseFollowerStateAfterSimulation(AlphaPayload payload, IGSE gse, SimulationSnapshot memory before) + private + view + { + address bonusInstance = gse.getBonusInstanceAddress(); + Timestamp postTs = Timestamp.wrap(block.timestamp); + uint256 bonusAfter = gse.getAttesterCountAtTime(bonusInstance, postTs); + uint256 oldEffectiveAfter = gse.getAttesterCountAtTime(before.oldCanonicalRollup, postTs); + uint256 newEffectiveAfter = gse.getAttesterCountAtTime(address(payload.ROLLUP()), postTs); + uint256 totalSupplyAfter = gse.totalSupply(); + uint256 bonusSupplyAfter = gse.supplyOf(bonusInstance); + uint256 oldSupplyAfter = gse.supplyOf(before.oldCanonicalRollup); + uint256 newSupplyAfter = gse.supplyOf(address(payload.ROLLUP())); + uint256 newEffectiveSupplyAfter = newSupplyAfter + bonusSupplyAfter; + + assertEq(newEffectiveAfter, bonusAfter, "sim new effective mismatch"); + assertEq(bonusAfter, before.bonusCount, "sim bonus count mismatch"); + assertEq(before.oldEffectiveCount, oldEffectiveAfter + before.bonusCount, "sim old effective mismatch"); + assertEq(totalSupplyAfter, before.totalSupply, "sim total supply mismatch"); + assertEq(bonusSupplyAfter, before.bonusSupply, "sim bonus supply mismatch"); + assertEq(before.oldEffectiveSupply, oldSupplyAfter + before.bonusSupply, "sim old effective supply mismatch"); + assertEq(newSupplyAfter, 0, "sim new direct supply mismatch"); + assertEq(newEffectiveSupplyAfter, before.bonusSupply, "sim new effective supply mismatch"); + assertGt(newEffectiveSupplyAfter, totalSupplyAfter * 2 / 3, "sim effective supply <= 2/3"); + } + + function _pendingThrough(Proposal memory proposal) private pure returns (Timestamp) { + return Timestamp.wrap(Timestamp.unwrap(proposal.creation) + Timestamp.unwrap(proposal.config.votingDelay)); + } + + function _activeThrough(Proposal memory proposal) private pure returns (Timestamp) { + return + Timestamp.wrap(Timestamp.unwrap(_pendingThrough(proposal)) + Timestamp.unwrap(proposal.config.votingDuration)); + } + + function _queuedThrough(Proposal memory proposal) private pure returns (Timestamp) { + return Timestamp.wrap(Timestamp.unwrap(_activeThrough(proposal)) + Timestamp.unwrap(proposal.config.executionDelay)); + } + + function _assertGovernanceConfigurationAfterSimulation( + Configuration memory beforeConfig, + Configuration memory afterConfig + ) private pure { + console.log("[assert] governance config after simulation start"); + assertEq(Timestamp.unwrap(afterConfig.executionDelay), 30 days, "sim execution delay mismatch"); + assertEq( + Timestamp.unwrap(beforeConfig.votingDelay), Timestamp.unwrap(afterConfig.votingDelay), "sim voting delay mismatch" + ); + assertEq( + Timestamp.unwrap(beforeConfig.votingDuration), + Timestamp.unwrap(afterConfig.votingDuration), + "sim voting duration mismatch" + ); + assertEq( + Timestamp.unwrap(beforeConfig.gracePeriod), Timestamp.unwrap(afterConfig.gracePeriod), "sim grace mismatch" + ); + assertEq(beforeConfig.quorum, afterConfig.quorum, "sim quorum mismatch"); + assertEq(beforeConfig.requiredYeaMargin, afterConfig.requiredYeaMargin, "sim yea margin mismatch"); + assertEq(beforeConfig.minimumVotes, afterConfig.minimumVotes, "sim minimum votes mismatch"); + assertEq( + Timestamp.unwrap(beforeConfig.proposeConfig.lockDelay), + Timestamp.unwrap(afterConfig.proposeConfig.lockDelay), + "sim lock delay mismatch" + ); + assertEq(beforeConfig.proposeConfig.lockAmount, afterConfig.proposeConfig.lockAmount, "sim lock amount mismatch"); + console.log(unicode"[assert] governance config after simulation ✓"); + } + + function _assertRewardMigrationAfterSimulation( + FlushRewarder oldRewarder, + FlushRewarder newRewarder, + IERC20 rewardAsset, + SimulationSnapshot memory before + ) private view { + console.log("[assert] reward migration after simulation start"); + uint256 oldRewarderBalanceAfter = rewardAsset.balanceOf(address(oldRewarder)); + uint256 newRewarderBalanceAfter = rewardAsset.balanceOf(address(newRewarder)); + + assertNotEq(address(oldRewarder), address(newRewarder), "sim rewarder alias mismatch"); + assertEq(Ownable(address(oldRewarder)).owner(), MAINNET_GOVERNANCE, "sim old rewarder owner mismatch"); + assertEq(Ownable(address(newRewarder)).owner(), MAINNET_GOVERNANCE, "sim new rewarder owner mismatch"); + assertEq( + oldRewarderBalanceAfter, before.oldRewarderBalance - before.fundsToMove, "sim old rewarder balance mismatch" + ); + assertEq( + newRewarderBalanceAfter, before.newRewarderBalance + before.fundsToMove, "sim new rewarder balance mismatch" + ); + assertEq(oldRewarder.rewardsAvailable(), 0, "sim old rewardsAvailable mismatch"); + assertEq(newRewarder.rewardsAvailable(), before.fundsToMove, "sim new rewardsAvailable mismatch"); + assertEq(newRewarder.rewardPerInsertion(), oldRewarder.rewardPerInsertion(), "sim reward rate mismatch"); + console.log(unicode"[assert] reward migration after simulation ✓"); + } + + function _writeOutputJson(Rollup rollup, IVerifier verifier, AlphaPayload payload, IEscapeHatch escapeHatch) private { + string memory key = "alpha"; + vm.serializeAddress(key, "rollupAddress", address(rollup)); + vm.serializeAddress(key, "verifierAddress", address(verifier)); + vm.serializeAddress(key, "payloadAddress", address(payload)); + vm.serializeAddress(key, "newFlushRewarderAddress", address(payload.NEW_FLUSH_REWARDER())); + vm.serializeAddress(key, "escapeHatchAddress", address(escapeHatch)); + vm.serializeAddress(key, "oldFlushRewarderAddress", MAINNET_OLD_FLUSH_REWARDER); + vm.serializeAddress(key, "registryAddress", MAINNET_REGISTRY); + string memory finalJson = vm.serializeAddress(key, "governanceAddress", MAINNET_GOVERNANCE); + console.log("JSON DEPLOY RESULT:", finalJson); + } +} diff --git a/l1-contracts/scripts/run_alpha_upgrade.sh b/l1-contracts/scripts/run_alpha_upgrade.sh new file mode 100755 index 000000000000..2a6f4947db28 --- /dev/null +++ b/l1-contracts/scripts/run_alpha_upgrade.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy the static mainnet alpha rollup + AlphaPayload bundle. +# +# Required environment variables: +# L1_RPC_URL +# ROLLUP_DEPLOYMENT_PRIVATE_KEY +# DEPLOYER +# ETHERSCAN_API_KEY + +cd "$(dirname "$0")/.." + +: "${L1_RPC_URL:?L1_RPC_URL is required}" +: "${ROLLUP_DEPLOYMENT_PRIVATE_KEY:?ROLLUP_DEPLOYMENT_PRIVATE_KEY is required}" +: "${DEPLOYER:?DEPLOYER is required}" +: "${ETHERSCAN_API_KEY:?ETHERSCAN_API_KEY is required for source verification}" + +echo "=== Deploying static mainnet alpha rollup + payload ===" + +forge script DeployAlpha \ + --rpc-url "$L1_RPC_URL" \ + --private-key "$ROLLUP_DEPLOYMENT_PRIVATE_KEY" \ + --etherscan-api-key "$ETHERSCAN_API_KEY" \ + --verify \ + --broadcast \ + --slow \ + -vvv diff --git a/l1-contracts/src/alpha/AlphaPayload.sol b/l1-contracts/src/alpha/AlphaPayload.sol new file mode 100644 index 000000000000..edb1c4651f05 --- /dev/null +++ b/l1-contracts/src/alpha/AlphaPayload.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IEscapeHatch} from "@aztec/core/interfaces/IEscapeHatch.sol"; +import {IInstance} from "@aztec/core/interfaces/IInstance.sol"; +import {IRollupCore} from "@aztec/core/interfaces/IRollup.sol"; +import {IValidatorSelectionCore} from "@aztec/core/interfaces/IValidatorSelection.sol"; +import {IGovernance, Configuration} from "@aztec/governance/Governance.sol"; +import {IGSECore} from "@aztec/governance/GSE.sol"; +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; +import {FlushRewarder} from "@aztec/periphery/FlushRewarder.sol"; +import {Timestamp} from "@aztec/shared/libraries/TimeMath.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + +/** + * @title AlphaPayload + * @notice Governance payload for the alpha rollup cutover. + * @dev The payload performs six actions in order: + * 1. Register the new rollup in the Registry (making it canonical). + * 2. Register the new rollup in the GSE so following stakers are moved. + * 3. Enable rewards claiming on the new rollup. + * 4. Update the rollup escape hatch to the newly deployed escape hatch contract. + * 5. Move currently claimable rewards from the old flush rewarder to the new flush rewarder. + * 6. Update Governance configuration so `executionDelay` is set to 30 days. + * + * The new flush rewarder is deployed in the constructor and mirrors the old rewarder's + * reward asset and reward-per-insertion settings. + */ +contract AlphaPayload is IPayload { + /// @notice Registry where the new rollup version is added. + IRegistry public immutable REGISTRY; + + /// @notice Governance contract controlling execution of this payload. + IGovernance public immutable GOVERNANCE; + + /// @notice New rollup instance to register and promote. + IInstance public immutable ROLLUP; + + /// @notice Reward token used by both old and new flush rewarders. + IERC20 public immutable REWARD_ASSET; + + /// @notice Existing flush rewarder holding rewards to migrate. + FlushRewarder public immutable OLD_FLUSH_REWARDER; + + /// @notice Newly deployed flush rewarder for the new rollup. + FlushRewarder public immutable NEW_FLUSH_REWARDER; + + /// @notice Newly deployed escape hatch contract for the new rollup. + IEscapeHatch public immutable ESCAPE_HATCH; + + /** + * @notice Constructs the alpha governance payload. + * @param _registry Registry containing canonical rollup and governance addresses. + * @param _rollup New rollup instance to register. + * @param _oldFlushRewarder Existing rewarder from which rewards are recovered. + * @param _escapeHatch Escape hatch contract to be activated on the new rollup. + */ + constructor(IRegistry _registry, IInstance _rollup, FlushRewarder _oldFlushRewarder, IEscapeHatch _escapeHatch) { + REGISTRY = _registry; + GOVERNANCE = IGovernance(REGISTRY.getGovernance()); + + ROLLUP = _rollup; + OLD_FLUSH_REWARDER = _oldFlushRewarder; + ESCAPE_HATCH = _escapeHatch; + + REWARD_ASSET = OLD_FLUSH_REWARDER.REWARD_ASSET(); + + NEW_FLUSH_REWARDER = + new FlushRewarder(address(GOVERNANCE), ROLLUP, REWARD_ASSET, OLD_FLUSH_REWARDER.rewardPerInsertion()); + } + + /** + * @notice Returns the exact action list executed by governance. + * @return - The array of actions to execute, in order: + * 0. Registry.addRollup(new rollup) + * 1. GSE.addRollup(new rollup) + * 2. Rollup.setRewardsClaimable(true) + * 3. Rollup.updateEscapeHatch(new escape hatch) + * 4. oldRewarder.recover(reward asset, new rewarder, rewardsAvailable) + * 5. Governance.updateConfiguration(executionDelay = 30 days) + */ + function getActions() external view override(IPayload) returns (IPayload.Action[] memory) { + IPayload.Action[] memory res = new IPayload.Action[](6); + + res[0] = + Action({target: address(REGISTRY), data: abi.encodeWithSelector(IRegistry.addRollup.selector, address(ROLLUP))}); + + res[1] = Action({ + target: address(ROLLUP.getGSE()), data: abi.encodeWithSelector(IGSECore.addRollup.selector, address(ROLLUP)) + }); + + res[2] = + Action({target: address(ROLLUP), data: abi.encodeWithSelector(IRollupCore.setRewardsClaimable.selector, true)}); + + // Note: Adding the escape hatch here ensures that only when the payload is executed will it be possible + // to produce blocks this way. + res[3] = Action({ + target: address(ROLLUP), + data: abi.encodeWithSelector(IValidatorSelectionCore.updateEscapeHatch.selector, address(ESCAPE_HATCH)) + }); + + // Note: rewardsAvailable() is called at execution time to recover all assets that are not owed to anyone at that + // time. This ensures that rewards that can be claimed by users won't be swept. + uint256 fundsToMove = OLD_FLUSH_REWARDER.rewardsAvailable(); + res[4] = Action({ + target: address(OLD_FLUSH_REWARDER), + data: abi.encodeWithSelector( + FlushRewarder.recover.selector, address(REWARD_ASSET), address(NEW_FLUSH_REWARDER), fundsToMove + ) + }); + + Configuration memory config = GOVERNANCE.getConfiguration(); + config.executionDelay = Timestamp.wrap(30 days); + + res[5] = Action({ + target: address(GOVERNANCE), data: abi.encodeWithSelector(IGovernance.updateConfiguration.selector, config) + }); + + return res; + } + + /** + * @notice Returns the URI describing this payload + * @return The payload URI string + */ + function getURI() external pure override(IPayload) returns (string memory) { + return "https://github.com/AztecProtocol/aztec-packages/pull/20865"; + } +} diff --git a/l1-contracts/src/alpha/AlphaVerifier.sol b/l1-contracts/src/alpha/AlphaVerifier.sol new file mode 100644 index 000000000000..77ba49548c82 --- /dev/null +++ b/l1-contracts/src/alpha/AlphaVerifier.sol @@ -0,0 +1,2307 @@ +// SPDX-License-Identifier: Apache-2.0 +// solhint-disable +// Copyright 2022 Aztec +pragma solidity >=0.8.21; + +uint256 constant N = 16_777_216; +uint256 constant LOG_N = 24; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 119; +uint256 constant VK_HASH = 0x059ad02b037fcfd4df2b9db771777d067a400f06fc55cf45fa601511e58e2c3e; + +library HonkVerificationKey { + function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { + Honk.VerificationKey memory vk = Honk.VerificationKey({ + circuitSize: uint256(16_777_216), + logCircuitSize: uint256(24), + publicInputsSize: uint256(119), + ql: Honk.G1Point({ + x: uint256(0x2434fb9ddfc507802b24762127ff216d762d75365624026d4fa799159ce7a451), + y: uint256(0x15f92f36ebb7c499597920729689d9c4b1d99de2529dbfa3885ace5654f5e245) + }), + qr: Honk.G1Point({ + x: uint256(0x29b527a6cd8fb6d1f9d5245dd87dcd315d7334cdfed7b9d88a017070cac9e699), + y: uint256(0x01a17712f6b6d482a5c36185a891ae85471d347dc7f4913f72ee0b86f391926c) + }), + qo: Honk.G1Point({ + x: uint256(0x2109dd5ae2b1d7383557078ce7c0c9421c15c51f0c7576d65acb841d20907b19), + y: uint256(0x1a2178a37615af5a9b4b2e1523e0c2de77236d611d0abb79283b73eb70d3b6de) + }), + q4: Honk.G1Point({ + x: uint256(0x02c916f7340368e71b370bb4b8fbd2e59377424883b5998b8bf9a17486833976), + y: uint256(0x2d33a88dd1f868f17acdc30673537070e78eba49d697c5c3f1c62c2a0d6a6031) + }), + qm: Honk.G1Point({ + x: uint256(0x286bec198b5c5503100cf44a9c1e4328a5fd6a06aecea37cf262f7564416e6e6), + y: uint256(0x0dc2055a9ff323fd8450d1fcefdd0f41d1c00063008223030e133ae0877b01b2) + }), + qc: Honk.G1Point({ + x: uint256(0x216f34176b502999458e8e04e4cade7d009f7af15990eb284401914f6d4b7132), + y: uint256(0x1a037a0ccdc42aae26a0ec288c25f91753e8ce24a20e56933f6c5afb882b2a05) + }), + qLookup: Honk.G1Point({ + x: uint256(0x2cb419ef3eccb0db6e471fd8fda94ff432adc91fa3d7b996e2620738b68f36b7), + y: uint256(0x02a10bfbecf3601c6c06b65fe96abc9fb42f812f9794cbded65e33942a411fef) + }), + qArith: Honk.G1Point({ + x: uint256(0x022bb2dd22a67b401123016dc8adc11c8227ba4c8e2f554d561259efde91dfa4), + y: uint256(0x03ca1832300271f320ebfc4d72f7df5b719a1c25f59305a5e0d4925e28b9a268) + }), + qDeltaRange: Honk.G1Point({ + x: uint256(0x2269af63c0c0d2b2d364c299c7534224b6fcb459ee84e3508a4e9b69f9e33228), + y: uint256(0x059cf6b2600ef9b44559428c39f52494e048aeeda07c6f111a5e0989b0d941ed) + }), + qElliptic: Honk.G1Point({ + x: uint256(0x193c47fe445a07262f68a277c80b8df67fe4b4b681f9985414a7f23a69bf943c), + y: uint256(0x224426a26aa66e59df94e961c098fe23a7c458e6534186b35c42b86371cdf777) + }), + qMemory: Honk.G1Point({ + x: uint256(0x19b76df422b7e7a363f8bf4e8d5d4b73a3fe4198fe0e385eb0ba6206863c42c3), + y: uint256(0x296198173ae9364f6caf7c9dde7237dc85e6591ff461d8ec359f3666cce03820) + }), + qNnf: Honk.G1Point({ + x: uint256(0x2868b7d782bd6e023e533ccfaa401de564782c932c6d86f5865544e0cb218d04), + y: uint256(0x26ea37ae38dabed56620648f40bb8f39526e063e7f6d4ecc0832b39a44bea896) + }), + qPoseidon2External: Honk.G1Point({ + x: uint256(0x0e58c45d7f5a0eba7e01b9c1a7eede2c68bf9ea76708772ba1093a0aa9661443), + y: uint256(0x16ecbf12fdc184848eb941cafcfe05117279e96361a89e2be833cd9c1ecc38ac) + }), + qPoseidon2Internal: Honk.G1Point({ + x: uint256(0x128b54b1d80a394e574135474bcbd826cecb86ba1205c1f1cdadfd0d2b463764), + y: uint256(0x0ab2441ff86203b13bf0a43e447c4c101f07be4d6bece5830fc5981a76d1794e) + }), + s1: Honk.G1Point({ + x: uint256(0x303e74bf724d33466d419ccb451f25dc95fb926ad73123fcf00663f46ed9faf9), + y: uint256(0x08169adca9493a2fa3b538b885774615b37c55fa881840e0b911d0a36fd13b00) + }), + s2: Honk.G1Point({ + x: uint256(0x0b411cbf36239d6cd8b940902b16130017a327ffaf4700db0795b7b9e455c6d8), + y: uint256(0x1e5a3410cc4d0f3144653e9dd82c8e5d65bbc687ae569a4c5cc837073d4e4fc8) + }), + s3: Honk.G1Point({ + x: uint256(0x2f583858f1a6f79ee312c16c6faf93773222d49c2b8d450eede399ffaf7d282c), + y: uint256(0x24d8d98f4ee6cb63e021ba305dfa21df0b7de16bfca963ffc3388f9c7b0d313e) + }), + s4: Honk.G1Point({ + x: uint256(0x16fb668a78d043d655ae4a5f3c6dcd8a9407cc3d1bee341eba937c7766f6383d), + y: uint256(0x20b38809380f481a534593daf0c47125e43f71c434df9bc9183fcfc949501882) + }), + t1: Honk.G1Point({ + x: uint256(0x2194d92617bbb3ba3d9e53f4f13c5c7c6fc5ae0c56f06e0a35747f24625e5763), + y: uint256(0x11ca88979a66ada092bb26e2b64b8602eb63f6e2fc21862f6fefead2978de7e0) + }), + t2: Honk.G1Point({ + x: uint256(0x1d346dd159688f85b77717f8197777c6ced8daf349335d74908736241f5f3328), + y: uint256(0x10e952d77959f4aa6fe9cb7222d9901d06ddfeea51240c79980d4daac8144443) + }), + t3: Honk.G1Point({ + x: uint256(0x20b8784b739f9968d269d3db4e7e7e7e666acd5804af5ff4a54f8dfbea8d2572), + y: uint256(0x21ad5edf078583d5e29052c50b4e8d46067ddaae2aa2345c15a755074d932f95) + }), + t4: Honk.G1Point({ + x: uint256(0x062b31b3719d79c253d59707efcc41556e43d9f4f122ba79d3484e023fb4da7d), + y: uint256(0x283eb52c07506637e09fb73d7875ada840d8e4c75295a90dc312b4d2571c52c3) + }), + id1: Honk.G1Point({ + x: uint256(0x049ad82cc9cfaba4521f90698a524e78a6c9954b903b8c7d7004ae43349884a7), + y: uint256(0x2d796f1ab1c2e178417438d57777fadfbe7cb6af8073524c191ae24baddfe936) + }), + id2: Honk.G1Point({ + x: uint256(0x16ca809b0a3768356f9532961298c949e2054937e1d7f50327cb65e438d9f2fc), + y: uint256(0x2c58fc68082149eba328505adf986d16db9fe6409a2b98136ddabe3f2a735e10) + }), + id3: Honk.G1Point({ + x: uint256(0x2756277322f08f84ceb6b5b28c1100c3ed689d77dc12b84111fce304b15415ee), + y: uint256(0x132c3b83f496eec2de70a8788db38c919ef8aaa4118389ff09685974e9ee5666) + }), + id4: Honk.G1Point({ + x: uint256(0x0243a8379b837880b584baf54d8ec29e8c22c4e212e58cad60d08195541fa004), + y: uint256(0x1a77e361a1251451d6057c74f33d868cd19e8a29a3a6b147750487d44318033b) + }), + lagrangeFirst: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) + }), + lagrangeLast: Honk.G1Point({ + x: uint256(0x28cac1bd2ae82a5589bac7a9df74e05a04692523aa860cf07ea57da5f20d7b29), + y: uint256(0x118c7af1ffc7f352a52f7cf4789efca13d54ab12407f1fcd2f4739c3a5de83b0) + }) + }); + return vk; + } +} + +pragma solidity ^0.8.27; + +interface IVerifier { + function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); +} + +/** + * @notice Library of error codes + * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier + */ +library Errors { + error ValueGeLimbMax(); + error ValueGeGroupOrder(); + error ValueGeFieldOrder(); + + error InvertOfZero(); + error NotPowerOfTwo(); + error ModExpFailed(); + + error ProofLengthWrong(); + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + error PublicInputsLengthWrong(); + error SumcheckFailed(); + error ShpleminiFailed(); + + error PointAtInfinity(); + + error ConsistencyCheckFailed(); + error GeminiChallengeInSubgroup(); +} + +type Fr is uint256; + +using {add as +} for Fr global; +using {sub as -} for Fr global; +using {mul as *} for Fr global; + +using {notEqual as !=} for Fr global; +using {equal as ==} for Fr global; + +uint256 constant SUBGROUP_SIZE = 256; +uint256 constant MODULUS = + 21_888_242_871_839_275_222_246_405_745_257_275_088_548_364_400_416_034_343_698_204_186_575_808_495_617; // Prime field + // order +uint256 constant P = MODULUS; +Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); +Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); +Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); +Fr constant ONE = Fr.wrap(1); +Fr constant ZERO = Fr.wrap(0); +// Instantiation + +library FrLib { + bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; + + function invert(Fr value) internal view returns (Fr) { + uint256 v = Fr.unwrap(value); + require(v != 0, Errors.InvertOfZero()); + + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), v) + mstore(add(free, 0x80), sub(MODULUS, 2)) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function pow(Fr base, uint256 v) internal view returns (Fr) { + uint256 b = Fr.unwrap(base); + // Only works for power of 2 + require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), b) + mstore(add(free, 0x80), v) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function div(Fr numerator, Fr denominator) internal view returns (Fr) { + unchecked { + return numerator * invert(denominator); + } + } + + function sqr(Fr value) internal pure returns (Fr) { + unchecked { + return value * value; + } + } + + function unwrap(Fr value) internal pure returns (uint256) { + unchecked { + return Fr.unwrap(value); + } + } + + function neg(Fr value) internal pure returns (Fr) { + unchecked { + return Fr.wrap(MODULUS - Fr.unwrap(value)); + } + } + + function from(uint256 value) internal pure returns (Fr) { + unchecked { + require(value < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(value); + } + } + + function fromBytes32(bytes32 value) internal pure returns (Fr) { + unchecked { + uint256 v = uint256(value); + require(v < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(v); + } + } + + function toBytes32(Fr value) internal pure returns (bytes32) { + unchecked { + return bytes32(Fr.unwrap(value)); + } + } +} + +// Free functions +function add(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function mul(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function sub(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); + } +} + +function notEqual(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) != Fr.unwrap(b); + } +} + +function equal(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) == Fr.unwrap(b); + } +} + +uint256 constant CONST_PROOF_SIZE_LOG_N = 28; + +uint256 constant NUMBER_OF_SUBRELATIONS = 28; +uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; +uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; +uint256 constant NUMBER_OF_ENTITIES = 41; +// The number of entities added for ZK (gemini_masking_poly) +uint256 constant NUM_MASKING_POLYNOMIALS = 1; +uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_UNSHIFTED = 36; +uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_TO_BE_SHIFTED = 5; +uint256 constant PAIRING_POINTS_SIZE = 8; + +uint256 constant FIELD_ELEMENT_SIZE = 0x20; +uint256 constant GROUP_ELEMENT_SIZE = 0x40; + +// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) +uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; + +// ENUM FOR WIRES +enum WIRE { + Q_M, + Q_C, + Q_L, + Q_R, + Q_O, + Q_4, + Q_LOOKUP, + Q_ARITH, + Q_RANGE, + Q_ELLIPTIC, + Q_MEMORY, + Q_NNF, + Q_POSEIDON2_EXTERNAL, + Q_POSEIDON2_INTERNAL, + SIGMA_1, + SIGMA_2, + SIGMA_3, + SIGMA_4, + ID_1, + ID_2, + ID_3, + ID_4, + TABLE_1, + TABLE_2, + TABLE_3, + TABLE_4, + LAGRANGE_FIRST, + LAGRANGE_LAST, + W_L, + W_R, + W_O, + W_4, + Z_PERM, + LOOKUP_INVERSES, + LOOKUP_READ_COUNTS, + LOOKUP_READ_TAGS, + W_L_SHIFT, + W_R_SHIFT, + W_O_SHIFT, + W_4_SHIFT, + Z_PERM_SHIFT +} + +library Honk { + struct G1Point { + uint256 x; + uint256 y; + } + + struct VerificationKey { + // Misc Params + uint256 circuitSize; + uint256 logCircuitSize; + uint256 publicInputsSize; + // Selectors + G1Point qm; + G1Point qc; + G1Point ql; + G1Point qr; + G1Point qo; + G1Point q4; + G1Point qLookup; // Lookup + G1Point qArith; // Arithmetic widget + G1Point qDeltaRange; // Delta Range sort + G1Point qMemory; // Memory + G1Point qNnf; // Non-native Field + G1Point qElliptic; // Auxillary + G1Point qPoseidon2External; + G1Point qPoseidon2Internal; + // Copy constraints + G1Point s1; + G1Point s2; + G1Point s3; + G1Point s4; + // Copy identity + G1Point id1; + G1Point id2; + G1Point id3; + G1Point id4; + // Precomputed lookup table + G1Point t1; + G1Point t2; + G1Point t3; + G1Point t4; + // Fixed first and last + G1Point lagrangeFirst; + G1Point lagrangeLast; + } + + struct RelationParameters { + // challenges + Fr eta; + Fr beta; + Fr gamma; + // derived + Fr publicInputsDelta; + } + + struct Proof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // Free wires + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Lookup helpers - Permutations + G1Point zPerm; + // Lookup helpers - logup + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Sumcheck + Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + G1Point shplonkQ; + G1Point kzgQuotient; + } + + /// forge-lint: disable-next-item(pascal-case-struct) + struct ZKProof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) + G1Point geminiMaskingPoly; + // Commitments to wire polynomials + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Commitments to logup witness polynomials + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Commitment to grand permutation polynomial + G1Point zPerm; + G1Point[3] libraCommitments; + // Sumcheck + Fr libraSum; + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr libraEvaluation; + Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + Fr[4] libraPolyEvals; + G1Point shplonkQ; + G1Point kzgQuotient; + } +} + +// Transcript library to generate fiat shamir challenges +struct Transcript { + // Oink + Honk.RelationParameters relationParameters; + Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] + Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; + // Sumcheck + Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; + // Gemini + Fr rho; + Fr geminiR; + // Shplonk + Fr shplonkNu; + Fr shplonkZ; +} + +library TranscriptLib { + function generateTranscript( + Honk.Proof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + uint256 logN + ) internal pure returns (Transcript memory t) { + Fr previousChallenge; + (t.relationParameters, previousChallenge) = + generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); + + (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); + + (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); + + (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); + + (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); + + (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); + + (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); + + (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); + + return t; + } + + function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { + uint256 challengeU256 = uint256(Fr.unwrap(challenge)); + // Split into two equal 127-bit chunks (254/2) + uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits + uint256 hi = challengeU256 >> 127; + first = FrLib.from(lo); + second = FrLib.from(hi); + } + + function generateRelationParametersChallenges( + Honk.Proof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + Fr previousChallenge + ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { + (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); + + (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); + } + + function generateEtaChallenge( + Honk.Proof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize + ) internal pure returns (Fr eta, Fr previousChallenge) { + bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 6); + round0[0] = bytes32(vkHash); + + for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { + require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); + round0[1 + i] = publicInputs[i]; + } + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); + } + + // Create the first challenge + // Note: w4 is added to the challenge later on + round0[1 + publicInputsSize] = bytes32(proof.w1.x); + round0[1 + publicInputsSize + 1] = bytes32(proof.w1.y); + round0[1 + publicInputsSize + 2] = bytes32(proof.w2.x); + round0[1 + publicInputsSize + 3] = bytes32(proof.w2.y); + round0[1 + publicInputsSize + 4] = bytes32(proof.w3.x); + round0[1 + publicInputsSize + 5] = bytes32(proof.w3.y); + + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); + (eta,) = splitChallenge(previousChallenge); + } + + function generateBetaGammaChallenges(Fr previousChallenge, Honk.Proof memory proof) + internal + pure + returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) + { + bytes32[7] memory round1; + round1[0] = FrLib.toBytes32(previousChallenge); + round1[1] = bytes32(proof.lookupReadCounts.x); + round1[2] = bytes32(proof.lookupReadCounts.y); + round1[3] = bytes32(proof.lookupReadTags.x); + round1[4] = bytes32(proof.lookupReadTags.y); + round1[5] = bytes32(proof.w4.x); + round1[6] = bytes32(proof.w4.y); + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); + (beta, gamma) = splitChallenge(nextPreviousChallenge); + } + + // Alpha challenges non-linearise the gate contributions + function generateAlphaChallenges(Fr previousChallenge, Honk.Proof memory proof) + internal + pure + returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) + { + // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup + uint256[5] memory alpha0; + alpha0[0] = Fr.unwrap(previousChallenge); + alpha0[1] = proof.lookupInverses.x; + alpha0[2] = proof.lookupInverses.y; + alpha0[3] = proof.zPerm.x; + alpha0[4] = proof.zPerm.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); + Fr alpha; + (alpha,) = splitChallenge(nextPreviousChallenge); + + // Compute powers of alpha for batching subrelations + alphas[0] = alpha; + for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { + alphas[i] = alphas[i - 1] * alpha; + } + } + + function generateGateChallenges(Fr previousChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) + { + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); + (gateChallenges[0],) = splitChallenge(previousChallenge); + for (uint256 i = 1; i < logN; i++) { + gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; + } + nextPreviousChallenge = previousChallenge; + } + + function generateSumcheckChallenges(Honk.Proof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) + { + for (uint256 i = 0; i < logN; i++) { + Fr[BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; + univariateChal[0] = prevChallenge; + + for (uint256 j = 0; j < BATCHED_RELATION_PARTIAL_LENGTH; j++) { + univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; + } + prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); + Fr unused; + (sumcheckChallenges[i], unused) = splitChallenge(prevChallenge); + } + nextPreviousChallenge = prevChallenge; + } + + function generateRhoChallenge(Honk.Proof memory proof, Fr prevChallenge) + internal + pure + returns (Fr rho, Fr nextPreviousChallenge) + { + Fr[NUMBER_OF_ENTITIES + 1] memory rhoChallengeElements; + rhoChallengeElements[0] = prevChallenge; + + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + rhoChallengeElements[i + 1] = proof.sumcheckEvaluations[i]; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); + Fr unused; + (rho, unused) = splitChallenge(nextPreviousChallenge); + } + + function generateGeminiRChallenge(Honk.Proof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr geminiR, Fr nextPreviousChallenge) + { + uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); + gR[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN - 1; i++) { + gR[1 + i * 2] = proof.geminiFoldComms[i].x; + gR[2 + i * 2] = proof.geminiFoldComms[i].y; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); + Fr unused; + (geminiR, unused) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkNuChallenge(Honk.Proof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr shplonkNu, Fr nextPreviousChallenge) + { + uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1); + shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN; i++) { + shplonkNuChallengeElements[i + 1] = Fr.unwrap(proof.geminiAEvaluations[i]); + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); + Fr unused; + (shplonkNu, unused) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkZChallenge(Honk.Proof memory proof, Fr prevChallenge) + internal + pure + returns (Fr shplonkZ, Fr nextPreviousChallenge) + { + uint256[3] memory shplonkZChallengeElements; + shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); + + shplonkZChallengeElements[1] = proof.shplonkQ.x; + shplonkZChallengeElements[2] = proof.shplonkQ.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); + Fr unused; + (shplonkZ, unused) = splitChallenge(nextPreviousChallenge); + } + + function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.Proof memory p) { + uint256 boundary = 0x00; + + // Pairing point object + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); + // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 + require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); + p.pairingPointObject[i] = FrLib.from(limb); + boundary += FIELD_ELEMENT_SIZE; + } + // Commitments + p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Lookup / Permutation Helper Commitments + p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Sumcheck univariates + for (uint256 i = 0; i < logN; i++) { + for (uint256 j = 0; j < BATCHED_RELATION_PARTIAL_LENGTH; j++) { + p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + } + // Sumcheck evaluations + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Gemini + // Read gemini fold univariates + for (uint256 i = 0; i < logN - 1; i++) { + p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + } + + // Read gemini a evaluations + for (uint256 i = 0; i < logN; i++) { + p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Shplonk + p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + // KZG + p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + } +} + +library RelationsLib { + struct EllipticParams { + // Points + Fr x_1; + Fr y_1; + Fr x_2; + Fr y_2; + Fr y_3; + Fr x_3; + // push accumulators into memory + Fr x_double_identity; + } + + // Parameters used within the Memory Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct MemParams { + Fr memory_record_check; + Fr partial_record_check; + Fr next_gate_access_type; + Fr record_delta; + Fr index_delta; + Fr adjacent_values_match_if_adjacent_indices_match; + Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; + Fr access_check; + Fr next_gate_access_type_is_boolean; + Fr ROM_consistency_check_identity; + Fr RAM_consistency_check_identity; + Fr timestamp_delta; + Fr RAM_timestamp_check_identity; + Fr memory_identity; + Fr index_is_monotonically_increasing; + } + + // Parameters used within the Non-Native Field Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct NnfParams { + Fr limb_subproduct; + Fr non_native_field_gate_1; + Fr non_native_field_gate_2; + Fr non_native_field_gate_3; + Fr limb_accumulator_1; + Fr limb_accumulator_2; + Fr nnf_identity; + } + + struct PoseidonExternalParams { + Fr s1; + Fr s2; + Fr s3; + Fr s4; + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr t0; + Fr t1; + Fr t2; + Fr t3; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr q_pos_by_scaling; + } + + struct PoseidonInternalParams { + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr u_sum; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr s1; + Fr q_pos_by_scaling; + } + + Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) + uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; + + // Constants for the Non-native Field relation + Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); + Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); + + function accumulateRelationEvaluations( + Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, + Fr powPartialEval + ) internal pure returns (Fr accumulator) { + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; + + // Accumulate all relations in Ultra Honk - each with varying number of subrelations + accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); + + // batch the subrelations with the precomputed alpha powers to obtain the full honk relation + accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); + } + + /** + * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids + * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code + * editors, and thus is noisy. + */ + function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { + return p[uint256(_wire)]; + } + + /** + * Ultra Arithmetic Relation + * + */ + function accumulateArithmeticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + // Relation 0 + Fr q_arith = wire(p, WIRE.Q_ARITH); + { + Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); + + Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; + accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) + + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); + accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); + accum = accum * q_arith; + accum = accum * domainSep; + evals[0] = accum; + } + + // Relation 1 + { + Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); + accum = accum * (q_arith - Fr.wrap(2)); + accum = accum * (q_arith - ONE); + accum = accum * q_arith; + accum = accum * domainSep; + evals[1] = accum; + } + } + + function accumulatePermutationRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr grand_product_numerator; + Fr grand_product_denominator; + + { + Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; + num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); + + grand_product_numerator = num; + } + { + Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; + den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); + + grand_product_denominator = den; + } + + // Contribution 2 + { + Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; + + acc = acc + - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) + * grand_product_denominator); + acc = acc * domainSep; + evals[2] = acc; + } + + // Contribution 3 + { + Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; + evals[3] = acc; + } + } + + function accumulateLogDerivativeLookupRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr table_term; + Fr lookup_term; + + // Calculate the write term (the table accumulation) + // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) + + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); + } + + // Calculate the read term + // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); + Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); + Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); + + lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) + + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); + } + + Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; + Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; + + Fr inverse_exists_xor = + wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); + + // Inverse calculated correctly relation + Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; + accumulatorNone = accumulatorNone * domainSep; + + // Inverse + Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; + + Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); + + Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; + + evals[4] = accumulatorNone; + evals[5] = accumulatorOne; + evals[6] = read_tag_boolean_relation * domainSep; + } + + function accumulateDeltaRangeRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr minus_one = ZERO - ONE; + Fr minus_two = ZERO - Fr.wrap(2); + Fr minus_three = ZERO - Fr.wrap(3); + + // Compute wire differences + Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); + Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); + Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); + Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); + + // Contribution 6 + { + Fr acc = delta_1; + acc = acc * (delta_1 + minus_one); + acc = acc * (delta_1 + minus_two); + acc = acc * (delta_1 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[7] = acc; + } + + // Contribution 7 + { + Fr acc = delta_2; + acc = acc * (delta_2 + minus_one); + acc = acc * (delta_2 + minus_two); + acc = acc * (delta_2 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[8] = acc; + } + + // Contribution 8 + { + Fr acc = delta_3; + acc = acc * (delta_3 + minus_one); + acc = acc * (delta_3 + minus_two); + acc = acc * (delta_3 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[9] = acc; + } + + // Contribution 9 + { + Fr acc = delta_4; + acc = acc * (delta_4 + minus_one); + acc = acc * (delta_4 + minus_two); + acc = acc * (delta_4 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[10] = acc; + } + } + + function accumulateEllipticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + EllipticParams memory ep; + ep.x_1 = wire(p, WIRE.W_R); + ep.y_1 = wire(p, WIRE.W_O); + + ep.x_2 = wire(p, WIRE.W_L_SHIFT); + ep.y_2 = wire(p, WIRE.W_4_SHIFT); + ep.y_3 = wire(p, WIRE.W_O_SHIFT); + ep.x_3 = wire(p, WIRE.W_R_SHIFT); + + Fr q_sign = wire(p, WIRE.Q_L); + Fr q_is_double = wire(p, WIRE.Q_M); + + // Contribution 10 point addition, x-coordinate check + // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 + Fr x_diff = (ep.x_2 - ep.x_1); + Fr y1_sqr = (ep.y_1 * ep.y_1); + { + // Move to top + Fr partialEval = domainSep; + + Fr y2_sqr = (ep.y_2 * ep.y_2); + Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; + Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); + x_add_identity = x_add_identity * x_diff * x_diff; + x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; + + evals[11] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 11 point addition, x-coordinate check + // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 + { + Fr y1_plus_y3 = ep.y_1 + ep.y_3; + Fr y_diff = ep.y_2 * q_sign - ep.y_1; + Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; + evals[12] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 10 point doubling, x-coordinate check + // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 + // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 + { + Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; + Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; + y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; + Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); + + // NOTE: pushed into memory (stack >:'( ) + ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; + + Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + evals[11] = evals[11] + acc; + } + + // Contribution 11 point doubling, y-coordinate check + // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 + { + Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; + Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); + evals[12] = evals[12] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + } + } + + function accumulateMemoryRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + MemParams memory ap; + + // Compute eta powers locally + Fr eta_two = rp.eta * rp.eta; + Fr eta_three = eta_two * rp.eta; + + /** + * MEMORY + * + * A RAM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) + * * v: `value` of memory cell being accessed + * * a: `access` type of record. read: 0 = read, 1 = write + * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three + * + * A ROM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three + * + * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + + * selectors, depending on whether the gate is a RAM read/write or a ROM read + * + * | gate type | i | v2/t | v | a | r | + * | --------- | -- | ----- | -- | -- | -- | + * | ROM | w1 | w2 | w3 | -- | w4 | + * | RAM | w1 | w2 | w3 | qc | w4 | + * + * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on + * `w2` to fix its value) + * + * + */ + + /** + * Memory Record Check + * Partial degree: 1 + * Total degree: 4 + * + * A ROM/ROM access gate can be evaluated with the identity: + * + * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 + * + * For ROM gates, qc = 0 + */ + ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); + ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); + ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 + ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); + + /** + * Contribution 13 & 14 + * ROM Consistency Check + * Partial degree: 1 + * Total degree: 4 + * + * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of + * records that are sorted. + * + * We apply the following checks for the sorted records: + * + * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 + * 2. index values for adjacent records are monotonically increasing + * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} + * + */ + ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); + ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); + + ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 + + ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 + + evals[14] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + evals[15] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + + ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 + + /** + * Contributions 15,16,17 + * RAM Consistency Check + * + * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` + * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. + * This is validated by requiring `access` to be boolean + * + * For two adjacent entries in the sorted list if _both_ + * A) index values match + * B) adjacent access value is 0 (i.e. next gate is a READ) + * then + * C) both values must match. + * The gate boolean check is + * (A && B) => C === !(A && B) || C === !A || !B || C + * + * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized + * with a WRITE operation. + */ + Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 + ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 + + // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta + // deg 1 or 4 + ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); + ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; + + Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); + ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = + (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 + + // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the + // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't + // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access + // type is correct, to cover this edge case + // deg 2 or 4 + ap.next_gate_access_type_is_boolean = ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; + + // Putting it all together... + evals[16] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation + * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 + evals[17] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg + // 4 + evals[18] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg + // 4 or 6 + + ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 + + /** + * RAM Timestamp Consistency Check + * + * | w1 | w2 | w3 | w4 | + * | index | timestamp | timestamp_check | -- | + * + * Let delta_index = index_{i + 1} - index_{i} + * + * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i + * Else timestamp_check = 0 + */ + ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); + ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg + // 3 + + /** + * Complete Contribution 12 + * The complete RAM/ROM memory identity + * Partial degree: + */ + ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg + // 4 + ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 + // or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 + + // (deg 3 or 9) + (deg 4) + (deg 3) + ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 + evals[13] = ap.memory_identity; + } + + function accumulateNnfRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + NnfParams memory ap; + + /** + * Contribution 12 + * Non native field arithmetic gate 2 + * deg 4 + * + * _ _ + * / _ _ _ 14 \ + * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | + * \_ _/ + * + * + */ + ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); + ap.non_native_field_gate_2 = + (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); + + ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; + ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); + ap.non_native_field_gate_1 = ap.limb_subproduct; + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); + + ap.non_native_field_gate_3 = ap.limb_subproduct; + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); + + Fr non_native_field_identity = ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; + non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); + + // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm + // deg 2 + ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); + ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); + + // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm + // deg 2 + ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); + ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); + + Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; + limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 + + ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; + ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); + evals[19] = ap.nnf_identity; + } + + function accumulatePoseidonExternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonExternalParams memory ep; + + ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); + ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); + ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); + + ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; + ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; + ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; + ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; + // matrix mul v = M_E * u with 14 additions + ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 + ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 + ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 + // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 + ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 + // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 + ep.v4 = ep.t1 + ep.t1; + ep.v4 = ep.v4 + ep.v4 + ep.t3; + // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 + ep.v2 = ep.t0 + ep.t0; + ep.v2 = ep.v2 + ep.v2 + ep.t2; + // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 + ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 + ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + + ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; + evals[20] = evals[20] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); + + evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); + + evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); + + evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + function accumulatePoseidonInternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonInternalParams memory ip; + + Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ + FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), + FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), + FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), + FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) + ]; + + // add round constants + ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + + // apply s-box round + ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; + ip.u2 = wire(p, WIRE.W_R); + ip.u3 = wire(p, WIRE.W_O); + ip.u4 = wire(p, WIRE.W_4); + + // matrix mul with v = M_I * u 4 muls and 7 additions + ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; + + ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; + + ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; + evals[24] = evals[24] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); + + ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; + evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); + + ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; + evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); + + ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; + evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + // Batch subrelation evaluations using precomputed powers of alpha + // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array + function scaleAndBatchSubrelations( + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges + ) internal pure returns (Fr accumulator) { + accumulator = evaluations[0]; + + for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { + accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; + } + } +} + +library CommitmentSchemeLib { + using FrLib for Fr; + + // Avoid stack too deep + struct ShpleminiIntermediates { + Fr unshiftedScalar; + Fr shiftedScalar; + Fr unshiftedScalarNeg; + Fr shiftedScalarNeg; + // Scalar to be multiplied by [1]₁ + Fr constantTermAccumulator; + // Accumulator for powers of rho + Fr batchingChallenge; + // Linear combination of multilinear (sumcheck) evaluations and powers of rho + Fr batchedEvaluation; + Fr[4] denominators; + Fr[4] batchingScalars; + // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr posInvertedDenominator; + // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr negInvertedDenominator; + // ν^{2i} * 1/(z - r^{2^i}) + Fr scalingFactorPos; + // ν^{2i+1} * 1/(z + r^{2^i}) + Fr scalingFactorNeg; + // Fold_i(r^{2^i}) reconstructed by Verifier + Fr[] foldPosEvaluations; + } + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 + function computeFoldPosEvaluations( + Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, + Fr batchedEvalAccumulator, + Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, + Fr[] memory geminiEvalChallengePowers, + uint256 logSize + ) internal view returns (Fr[] memory) { + Fr[] memory foldPosEvaluations = new Fr[](logSize); + for (uint256 i = logSize; i > 0; --i) { + Fr challengePower = geminiEvalChallengePowers[i - 1]; + Fr u = sumcheckUChallenges[i - 1]; + + Fr batchedEvalRoundAcc = ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] + * (challengePower * (ONE - u) - u)); + // Divide by the denominator + batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); + + batchedEvalAccumulator = batchedEvalRoundAcc; + foldPosEvaluations[i - 1] = batchedEvalRoundAcc; + } + return foldPosEvaluations; + } + + function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { + Fr[] memory squares = new Fr[](logN); + squares[0] = r; + for (uint256 i = 1; i < logN; ++i) { + squares[i] = squares[i - 1].sqr(); + } + return squares; + } +} + +uint256 constant Q = + 21_888_242_871_839_275_222_246_405_745_257_275_088_696_311_157_297_823_662_689_037_894_645_226_208_583; // EC group + // order. F_q + +// Fr utility + +function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { + scalar = FrLib.fromBytes32(bytes32(proofSection)); +} + +// EC Point utilities +function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { + uint256 x = uint256(bytes32(proofSection[0x00:0x20])); + uint256 y = uint256(bytes32(proofSection[0x20:0x40])); + require(x < Q && y < Q, Errors.ValueGeGroupOrder()); + + // Reject the point at infinity (0,0). EVM precompiles silently treat (0,0) + // as the identity element, which could zero out commitments. + // On-curve validation (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles + // per EIP-196, so we only need to catch this special case here. + require((x | y) != 0, Errors.PointAtInfinity()); + + point = Honk.G1Point({x: x, y: y}); +} + +function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { + // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. + if (point.y != 0) { + point.y = Q - point.y; + } + return point; +} + +/** + * Convert the pairing points to G1 points. + * + * The pairing points are serialised as an array of 2 limbs representing two points + * (P0 and P1, used for lhs and rhs of pairing operation). + * + * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. + * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] + * + * @param pairingPoints The pairing points to convert. + * @return lhs P0 point + * @return rhs P1 point + */ +function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) + pure + returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) +{ + // P0 (lhs): x = lo | (hi << 136) + uint256 lhsX = Fr.unwrap(pairingPoints[0]); + lhsX |= Fr.unwrap(pairingPoints[1]) << 136; + + uint256 lhsY = Fr.unwrap(pairingPoints[2]); + lhsY |= Fr.unwrap(pairingPoints[3]) << 136; + + // P1 (rhs): x = lo | (hi << 136) + uint256 rhsX = Fr.unwrap(pairingPoints[4]); + rhsX |= Fr.unwrap(pairingPoints[5]) << 136; + + uint256 rhsY = Fr.unwrap(pairingPoints[6]); + rhsY |= Fr.unwrap(pairingPoints[7]) << 136; + + // Reconstructed coordinates must be < Q to prevent malleability. + // Without this, two different limb encodings could map to the same curve point + // (via mulmod reduction in on-curve checks) but produce different transcript hashes. + require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); + + lhs.x = lhsX; + lhs.y = lhsY; + rhs.x = rhsX; + rhs.y = rhsY; +} + +/** + * Hash the pairing inputs from the present verification context with those extracted from the public inputs. + * + * @param proofPairingPoints Pairing points from the proof - (public inputs). + * @param accLhs Accumulator point for the left side - result of shplemini. + * @param accRhs Accumulator point for the right side - result of shplemini. + * @return recursionSeparator The recursion separator - generated from hashing the above. + */ +function generateRecursionSeparator( + Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, + Honk.G1Point memory accLhs, + Honk.G1Point memory accRhs +) pure returns (Fr recursionSeparator) { + // hash the proof aggregated X + // hash the proof aggregated Y + // hash the accum X + // hash the accum Y + + (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); + + uint256[8] memory recursionSeparatorElements; + + // Proof points + recursionSeparatorElements[0] = proofLhs.x; + recursionSeparatorElements[1] = proofLhs.y; + recursionSeparatorElements[2] = proofRhs.x; + recursionSeparatorElements[3] = proofRhs.y; + + // Accumulator points + recursionSeparatorElements[4] = accLhs.x; + recursionSeparatorElements[5] = accLhs.y; + recursionSeparatorElements[6] = accRhs.x; + recursionSeparatorElements[7] = accRhs.y; + + recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); +} + +/** + * G1 Mul with Separator + * Using the ecAdd and ecMul precompiles + * + * @param basePoint The point to multiply. + * @param other The other point to add. + * @param recursionSeperator The separator to use for the multiplication. + * @return `(recursionSeperator * basePoint) + other`. + */ +function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) + view + returns (Honk.G1Point memory) +{ + Honk.G1Point memory result; + + result = ecMul(recursionSeperator, basePoint); + result = ecAdd(result, other); + + return result; +} + +/** + * G1 Mul + * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. + * + * @param value The value to multiply the point by. + * @param point The point to multiply. + * @return result The result of the multiplication. + */ +function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write the point into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | point.x + // free + 0x20| point.y + mstore(free, mload(point)) + mstore(add(free, 0x20), mload(add(point, 0x20))) + // Write the scalar into memory (one 32 byte word) + // Memory layout: + // Address | value + // free + 0x40| value + mstore(add(free, 0x40), value) + + // Call the ecMul precompile, it takes in the following + // [point.x, point.y, scalar], and returns the result back into the free memory location. + let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) + if iszero(success) { + revert(0, 0) + } + // Copy the result of the multiplication back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x60)) + } + + return result; +} + +/** + * G1 Add + * Takes two G1 points and uses the ecAdd precompile to return the result. + * + * @param lhs The left hand side of the addition. + * @param rhs The right hand side of the addition. + * @return result The result of the addition. + */ +function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write lhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | lhs.x + // free + 0x20| lhs.y + mstore(free, mload(lhs)) + mstore(add(free, 0x20), mload(add(lhs, 0x20))) + + // Write rhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free + 0x40| rhs.x + // free + 0x60| rhs.y + mstore(add(free, 0x40), mload(rhs)) + mstore(add(free, 0x60), mload(add(rhs, 0x20))) + + // Call the ecAdd precompile, it takes in the following + // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. + let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) + if iszero(success) { revert(0, 0) } + + // Copy the result of the addition back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x80)) + } + + return result; +} + +function rejectPointAtInfinity(Honk.G1Point memory point) pure { + require((point.x | point.y) != 0, Errors.PointAtInfinity()); +} + +/** + * Check if pairing point limbs are all zero (default/infinity). + * Default pairing points indicate no recursive verification occurred. + */ +function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { + uint256 acc = 0; + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + acc |= Fr.unwrap(pairingPoints[i]); + } + return acc == 0; +} + +function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { + bytes memory input = abi.encodePacked( + rhs.x, + rhs.y, + // Fixed G2 point + uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), + uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), + uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), + uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), + lhs.x, + lhs.y, + // G2 point from VK + uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), + uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), + uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), + uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) + ); + + (bool success, bytes memory result) = address(0x08).staticcall(input); + decodedResult = success && abi.decode(result, (bool)); +} + +abstract contract BaseHonkVerifier is IVerifier { + using FrLib for Fr; + + // Constants for proof length calculation (matching UltraKeccakFlavor) + uint256 internal constant NUM_WITNESS_ENTITIES = 8; + uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points + uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements + + // Number of field elements in a ultra keccak honk proof for log_n = 25, including pairing point object. + uint256 internal constant PROOF_SIZE = 350; // Legacy constant - will be replaced by calculateProofSize($LOG_N) + uint256 internal constant SHIFTED_COMMITMENTS_START = 29; + + uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; + + uint256 internal immutable $N; + uint256 internal immutable $LOG_N; + uint256 internal immutable $VK_HASH; + uint256 internal immutable $NUM_PUBLIC_INPUTS; + + constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { + $N = _N; + $LOG_N = _logN; + $VK_HASH = _vkHash; + $NUM_PUBLIC_INPUTS = _numPublicInputs; + } + + function verify(bytes calldata proof, bytes32[] calldata publicInputs) public view override returns (bool) { + // Calculate expected proof size based on $LOG_N + uint256 expectedProofSize = calculateProofSize($LOG_N); + + // Check the received proof is the expected size where each field element is 32 bytes + if (proof.length != expectedProofSize * 32) { + revert Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize * 32); + } + + Honk.VerificationKey memory vk = loadVerificationKey(); + Honk.Proof memory p = TranscriptLib.loadProof(proof, $LOG_N); + if (publicInputs.length != vk.publicInputsSize - PAIRING_POINTS_SIZE) { + revert Errors.PublicInputsLengthWrong(); + } + + // Generate the fiat shamir challenges for the whole protocol + Transcript memory t = TranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); + + // Derive public input delta + t.relationParameters.publicInputsDelta = computePublicInputDelta( + publicInputs, + p.pairingPointObject, + t.relationParameters.beta, + t.relationParameters.gamma, /*pubInputsOffset=*/ + 1 + ); + + // Sumcheck + bool sumcheckVerified = verifySumcheck(p, t); + if (!sumcheckVerified) revert Errors.SumcheckFailed(); + + bool shpleminiVerified = verifyShplemini(p, vk, t); + if (!shpleminiVerified) revert Errors.ShpleminiFailed(); + + return sumcheckVerified && shpleminiVerified; // Boolean condition not required - nice for vanity :) + } + + function computePublicInputDelta( + bytes32[] memory publicInputs, + Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, + Fr beta, + Fr gamma, + uint256 offset + ) internal view returns (Fr publicInputDelta) { + Fr numerator = ONE; + Fr denominator = ONE; + + Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); + Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); + + { + for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { + Fr pubInput = FrLib.fromBytes32(publicInputs[i]); + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + Fr pubInput = pairingPointObject[i]; + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + } + + publicInputDelta = FrLib.div(numerator, denominator); + } + + function verifySumcheck(Honk.Proof memory proof, Transcript memory tp) internal view returns (bool verified) { + Fr roundTarget; + Fr powPartialEvaluation = ONE; + + // We perform sumcheck reductions over log n rounds ( the multivariate degree ) + for (uint256 round = 0; round < $LOG_N; ++round) { + Fr[BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; + bool valid = checkSum(roundUnivariate, roundTarget); + if (!valid) revert Errors.SumcheckFailed(); + + Fr roundChallenge = tp.sumCheckUChallenges[round]; + + // Update the round target for the next rounf + roundTarget = computeNextTargetSum(roundUnivariate, roundChallenge); + powPartialEvaluation = partiallyEvaluatePOW(tp.gateChallenges[round], powPartialEvaluation, roundChallenge); + } + + // Last round + Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( + proof.sumcheckEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation + ); + verified = (grandHonkRelationSum == roundTarget); + } + + // Return the new target sum for the next sumcheck round + function computeNextTargetSum(Fr[BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) + internal + view + returns (Fr targetSum) + { + Fr[BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000002d0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffff11), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000090), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffff71), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000000f0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000013b0) + ]; + // To compute the next target sum, we evaluate the given univariate at a point u (challenge). + + // Performing Barycentric evaluations + // Compute B(x) + Fr numeratorValue = ONE; + for (uint256 i = 0; i < BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); + } + + Fr[BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; + for (uint256 i = 0; i < BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + Fr inv = BARYCENTRIC_LAGRANGE_DENOMINATORS[i]; + inv = inv * (roundChallenge - Fr.wrap(i)); + inv = FrLib.invert(inv); + denominatorInverses[i] = inv; + } + + for (uint256 i = 0; i < BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + Fr term = roundUnivariates[i]; + term = term * denominatorInverses[i]; + targetSum = targetSum + term; + } + + // Scale the sum by the value of B(x) + targetSum = targetSum * numeratorValue; + } + + function verifyShplemini(Honk.Proof memory proof, Honk.VerificationKey memory vk, Transcript memory tp) + internal + view + returns (bool verified) + { + CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack + + // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size + Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); + + // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings + Fr[] memory scalars = new Fr[](NUMBER_UNSHIFTED + $LOG_N + 2); + Honk.G1Point[] memory commitments = new Honk.G1Point[](NUMBER_UNSHIFTED + $LOG_N + 2); + + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); + + mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); + mem.shiftedScalar = tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); + + scalars[0] = ONE; + commitments[0] = proof.shplonkQ; + + /* Batch multivariate opening claims, shifted and unshifted + * The vector of scalars is populated as follows: + * \f[ + * \left( + * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) + * \right) + * \f] + * + * The following vector is concatenated to the vector of commitments: + * \f[ + * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} + * \f] + * + * Simultaneously, the evaluation of the multilinear polynomial + * \f[ + * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} + * \f] + * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. + * + * This approach minimizes the number of iterations over the commitments to multilinear polynomials + * and eliminates the need to store the powers of \f$ \rho \f$. + */ + mem.batchingChallenge = ONE; + mem.batchedEvaluation = ZERO; + + mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); + mem.shiftedScalarNeg = mem.shiftedScalar.neg(); + for (uint256 i = 1; i <= NUMBER_UNSHIFTED; ++i) { + scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; + mem.batchedEvaluation = mem.batchedEvaluation + (proof.sumcheckEvaluations[i - 1] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + // g commitments are accumulated at r + // For each of the to be shifted commitments perform the shift in place by + // adding to the unshifted value. + // We do so, as the values are to be used in batchMul later, and as + // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. + // Applied to w1, w2, w3, w4 and zPerm + for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { + uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; + uint256 evaluationOff = i + NUMBER_UNSHIFTED; + + scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); + mem.batchedEvaluation = mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + + commitments[1] = vk.qm; + commitments[2] = vk.qc; + commitments[3] = vk.ql; + commitments[4] = vk.qr; + commitments[5] = vk.qo; + commitments[6] = vk.q4; + commitments[7] = vk.qLookup; + commitments[8] = vk.qArith; + commitments[9] = vk.qDeltaRange; + commitments[10] = vk.qElliptic; + commitments[11] = vk.qMemory; + commitments[12] = vk.qNnf; + commitments[13] = vk.qPoseidon2External; + commitments[14] = vk.qPoseidon2Internal; + commitments[15] = vk.s1; + commitments[16] = vk.s2; + commitments[17] = vk.s3; + commitments[18] = vk.s4; + commitments[19] = vk.id1; + commitments[20] = vk.id2; + commitments[21] = vk.id3; + commitments[22] = vk.id4; + commitments[23] = vk.t1; + commitments[24] = vk.t2; + commitments[25] = vk.t3; + commitments[26] = vk.t4; + commitments[27] = vk.lagrangeFirst; + commitments[28] = vk.lagrangeLast; + + // Accumulate proof points + commitments[29] = proof.w1; + commitments[30] = proof.w2; + commitments[31] = proof.w3; + commitments[32] = proof.w4; + commitments[33] = proof.zPerm; + commitments[34] = proof.lookupInverses; + commitments[35] = proof.lookupReadCounts; + commitments[36] = proof.lookupReadTags; + + /* Batch gemini claims from the prover + * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from + * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars + * + * 1. Moves the vector + * \f[ + * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) + * \f] + * to the 'commitments' vector. + * + * 2. Computes the scalars: + * \f[ + * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} + * \f] + * and places them into the 'scalars' vector. + * + * 3. Accumulates the summands of the constant term: + * \f[ + * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} + * \f] + * and adds them to the 'constant_term_accumulator'. + */ + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 + Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( + tp.sumCheckUChallenges, mem.batchedEvaluation, proof.geminiAEvaluations, powers_of_evaluation_challenge, $LOG_N + ); + + // Compute the Shplonk constant term contributions from A₀(±r) + mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; + mem.constantTermAccumulator = + mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); + + mem.batchingChallenge = tp.shplonkNu.sqr(); + + // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; + // Compute scalar multipliers for each fold commitment + for (uint256 i = 0; i < $LOG_N - 1; ++i) { + // Update inverted denominators + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); + + // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] + mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; + mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; + // [Aₗ] is multiplied by -v^{2l}/(z-r^{2^l}) - v^{2l+1} /(z+ r^{2^l}) + scalars[NUMBER_UNSHIFTED + 1 + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); + + // Accumulate the const term contribution given by + // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) + Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; + + accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; + mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; + // Update the running power of v + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; + + commitments[NUMBER_UNSHIFTED + 1 + i] = proof.geminiFoldComms[i]; + } + + // Finalize the batch opening claim + commitments[NUMBER_UNSHIFTED + $LOG_N] = Honk.G1Point({x: 1, y: 2}); + scalars[NUMBER_UNSHIFTED + $LOG_N] = mem.constantTermAccumulator; + + Honk.G1Point memory quotient_commitment = proof.kzgQuotient; + + commitments[NUMBER_UNSHIFTED + $LOG_N + 1] = quotient_commitment; + scalars[NUMBER_UNSHIFTED + $LOG_N + 1] = tp.shplonkZ; // evaluation challenge + + Honk.G1Point memory P_0_agg = batchMul(commitments, scalars); + Honk.G1Point memory P_1_agg = negateInplace(quotient_commitment); + + // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) + if (!arePairingPointsDefault(proof.pairingPointObject)) { + Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, P_0_agg, P_1_agg); + (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = + convertPairingPointsToG1(proof.pairingPointObject); + + // Validate the points from the proof are on the curve + rejectPointAtInfinity(P_0_other); + rejectPointAtInfinity(P_1_other); + + // accumulate with aggregate points in proof + P_0_agg = mulWithSeperator(P_0_agg, P_0_other, recursionSeparator); + P_1_agg = mulWithSeperator(P_1_agg, P_1_other, recursionSeparator); + } + + return pairing(P_0_agg, P_1_agg); + } + + function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) + internal + view + returns (Honk.G1Point memory result) + { + uint256 limit = NUMBER_UNSHIFTED + $LOG_N + 2; + + // Validate all points are on the curve + for (uint256 i = 0; i < limit; ++i) { + rejectPointAtInfinity(base[i]); + } + + bool success = true; + assembly { + let free := mload(0x40) + + let count := 0x01 + for {} lt(count, add(limit, 1)) { count := add(count, 1) } { + // Get loop offsets + let base_base := add(base, mul(count, 0x20)) + let scalar_base := add(scalars, mul(count, 0x20)) + + mstore(add(free, 0x40), mload(mload(base_base))) + mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) + // Add scalar + mstore(add(free, 0x80), mload(scalar_base)) + + success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) + // accumulator = accumulator + accumulator_2 + success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) + } + + // Return the result + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + } + + require(success, Errors.ShpleminiFailed()); + } + + // Calculate proof size based on log_n (matching UltraKeccakFlavor formula) + function calculateProofSize(uint256 logN) internal pure returns (uint256) { + // Witness commitments + uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments + + // Sumcheck + proofLength += logN * BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates + proofLength += NUMBER_OF_ENTITIES * NUM_ELEMENTS_FR; // sumcheck evaluations + + // Gemini + proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments + proofLength += logN * NUM_ELEMENTS_FR; // Gemini evaluations + + // Shplonk and KZG commitments + proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments + + // Pairing points + proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs + + return proofLength; + } + + function checkSum(Fr[BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate, Fr roundTarget) + internal + pure + returns (bool checked) + { + Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; + checked = totalSum == roundTarget; + } + + // Univariate evaluation of the monomial ((1-X_l) + X_l.B_l) at the challenge point X_l=u_l + function partiallyEvaluatePOW(Fr gateChallenge, Fr currentEvaluation, Fr roundChallenge) + internal + pure + returns (Fr newEvaluation) + { + Fr univariateEval = ONE + (roundChallenge * (gateChallenge - ONE)); + newEvaluation = currentEvaluation * univariateEval; + } + + function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); +} + +contract HonkVerifier is BaseHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { + function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { + return HonkVerificationKey.loadVerificationKey(); + } +} diff --git a/spartan/environments/network-defaults.yml b/spartan/environments/network-defaults.yml index 9eefd071c7ee..6e8f28bd01bb 100644 --- a/spartan/environments/network-defaults.yml +++ b/spartan/environments/network-defaults.yml @@ -52,7 +52,7 @@ l1-contracts: &l1-contracts-defaults # How many epochs to lag for validator set selection. AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET: 2 # How many epochs to lag for RANDAO selection. - AZTEC_LAG_IN_EPOCHS_FOR_RANDAO: 2 + AZTEC_LAG_IN_EPOCHS_FOR_RANDAO: 1 # Minimum stake to become a validator (in wei, 100 TST). AZTEC_ACTIVATION_THRESHOLD: 100e18 # Minimum stake before forced ejection (in wei, 50 TST).