From cd464710cf2e7ef3d78af977c3deafde391c37bd Mon Sep 17 00:00:00 2001 From: Mitch Date: Thu, 13 Feb 2025 14:05:40 -0500 Subject: [PATCH] feat: fetch addresses from registry --- .../RegisterNewRollupVersionPayload.sol | 31 ++ .../files/config/config-prover-env.sh | 2 - .../aztec.js/src/contract/contract.test.ts | 1 - .../cli/src/cmds/l1/deploy_l1_contracts.ts | 2 +- .../cli/src/cmds/pxe/get_node_info.ts | 4 +- .../end-to-end/src/e2e_p2p/slashing.test.ts | 2 +- yarn-project/ethereum/package.json | 1 + .../ethereum/src/contracts/registry.test.ts | 256 +++++++++++++ .../ethereum/src/contracts/registry.ts | 100 +++++ yarn-project/ethereum/src/contracts/rollup.ts | 5 +- .../ethereum/src/deploy_l1_contracts.ts | 356 ++++++++++-------- .../ethereum/src/l1_contract_addresses.ts | 5 +- .../scripts/generate-artifacts.sh | 1 + yarn-project/yarn.lock | 1 + 14 files changed, 607 insertions(+), 160 deletions(-) create mode 100644 l1-contracts/test/governance/scenario/RegisterNewRollupVersionPayload.sol create mode 100644 yarn-project/ethereum/src/contracts/registry.test.ts create mode 100644 yarn-project/ethereum/src/contracts/registry.ts diff --git a/l1-contracts/test/governance/scenario/RegisterNewRollupVersionPayload.sol b/l1-contracts/test/governance/scenario/RegisterNewRollupVersionPayload.sol new file mode 100644 index 000000000000..abde0a0ef8fb --- /dev/null +++ b/l1-contracts/test/governance/scenario/RegisterNewRollupVersionPayload.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.27; + +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; + +/** + * @title RegisterNewRollupVersionPayload + * @author Aztec Labs + * @notice A payload that registers a new rollup version. + */ +contract RegisterNewRollupVersionPayload is IPayload { + IRegistry public immutable REGISTRY; + address public immutable ROLLUP; + + constructor(IRegistry _registry, address _rollup) { + REGISTRY = _registry; + ROLLUP = _rollup; + } + + function getActions() external view override(IPayload) returns (IPayload.Action[] memory) { + IPayload.Action[] memory res = new IPayload.Action[](1); + + res[0] = Action({ + target: address(REGISTRY), + data: abi.encodeWithSelector(IRegistry.upgrade.selector, ROLLUP) + }); + + return res; + } +} diff --git a/spartan/aztec-network/files/config/config-prover-env.sh b/spartan/aztec-network/files/config/config-prover-env.sh index 2d56ed1c897e..073547821d48 100644 --- a/spartan/aztec-network/files/config/config-prover-env.sh +++ b/spartan/aztec-network/files/config/config-prover-env.sh @@ -19,7 +19,6 @@ coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') governance_proposer_address=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}') governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}') -slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}') # Write the addresses to a file in the shared volume cat </shared/contracts/contracts.env @@ -35,7 +34,6 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$governance_proposer_address export GOVERNANCE_CONTRACT_ADDRESS=$governance_address -export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address EOF cat /shared/contracts/contracts.env diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index 55e8b4062feb..48d50f2b07f0 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -41,7 +41,6 @@ describe('Contract Class', () => { coinIssuerAddress: EthAddress.random(), rewardDistributorAddress: EthAddress.random(), governanceProposerAddress: EthAddress.random(), - slashFactoryAddress: EthAddress.random(), }; const defaultArtifact: ContractArtifact = { diff --git a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts index 01fdd04933d9..eeb07d431e09 100644 --- a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts +++ b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts @@ -58,6 +58,6 @@ export async function deployL1Contracts( log(`RewardDistributor Address: ${l1ContractAddresses.rewardDistributorAddress.toString()}`); log(`GovernanceProposer Address: ${l1ContractAddresses.governanceProposerAddress.toString()}`); log(`Governance Address: ${l1ContractAddresses.governanceAddress.toString()}`); - log(`SlashFactory Address: ${l1ContractAddresses.slashFactoryAddress.toString()}`); + log(`SlashFactory Address: ${l1ContractAddresses.slashFactoryAddress?.toString()}`); } } diff --git a/yarn-project/cli/src/cmds/pxe/get_node_info.ts b/yarn-project/cli/src/cmds/pxe/get_node_info.ts index c2e1bbfd9644..b9e9179d2d5b 100644 --- a/yarn-project/cli/src/cmds/pxe/get_node_info.ts +++ b/yarn-project/cli/src/cmds/pxe/get_node_info.ts @@ -34,7 +34,7 @@ export async function getNodeInfo( rewardDistributor: info.l1ContractAddresses.rewardDistributorAddress.toString(), governanceProposer: info.l1ContractAddresses.governanceProposerAddress.toString(), governance: info.l1ContractAddresses.governanceAddress.toString(), - slashFactory: info.l1ContractAddresses.slashFactoryAddress.toString(), + slashFactory: info.l1ContractAddresses.slashFactoryAddress?.toString(), }, protocolContractAddresses: { classRegisterer: info.protocolContractAddresses.classRegisterer.toString(), @@ -60,7 +60,7 @@ export async function getNodeInfo( log(` RewardDistributor Address: ${info.l1ContractAddresses.rewardDistributorAddress.toString()}`); log(` GovernanceProposer Address: ${info.l1ContractAddresses.governanceProposerAddress.toString()}`); log(` Governance Address: ${info.l1ContractAddresses.governanceAddress.toString()}`); - log(` SlashFactory Address: ${info.l1ContractAddresses.slashFactoryAddress.toString()}`); + log(` SlashFactory Address: ${info.l1ContractAddresses.slashFactoryAddress?.toString()}`); log(`L2 Contract Addresses:`); log(` Class Registerer: ${info.protocolContractAddresses.classRegisterer.toString()}`); diff --git a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts index 68f347db4ee8..16bc1b94cb4b 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts @@ -83,7 +83,7 @@ describe('e2e_p2p_slashing', () => { }); const slashFactory = getContract({ - address: getAddress(t.ctx.deployL1ContractsValues.l1ContractAddresses.slashFactoryAddress.toString()), + address: getAddress(t.ctx.deployL1ContractsValues.l1ContractAddresses.slashFactoryAddress!.toString()), abi: SlashFactoryAbi, client: t.ctx.deployL1ContractsValues.publicClient, }); diff --git a/yarn-project/ethereum/package.json b/yarn-project/ethereum/package.json index fb3fa89ced65..26b7da8e6134 100644 --- a/yarn-project/ethereum/package.json +++ b/yarn-project/ethereum/package.json @@ -49,6 +49,7 @@ "@viem/anvil": "^0.0.10", "get-port": "^7.1.0", "jest": "^29.5.0", + "lodash.omit": "^4.5.0", "ts-node": "^10.9.1", "typescript": "^5.0.4" }, diff --git a/yarn-project/ethereum/src/contracts/registry.test.ts b/yarn-project/ethereum/src/contracts/registry.test.ts new file mode 100644 index 000000000000..5c50347f09d2 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/registry.test.ts @@ -0,0 +1,256 @@ +import { Fr } from '@aztec/foundation/fields'; +import { type Logger, createLogger } from '@aztec/foundation/log'; +import { TestERC20Abi as FeeJuiceAbi, GovernanceAbi } from '@aztec/l1-artifacts'; + +import type { Anvil } from '@viem/anvil'; +import omit from 'lodash.omit'; +import { getContract } from 'viem'; +import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +import { DefaultL1ContractsConfig } from '../config.js'; +import { createL1Clients, deployL1Contracts, deployRollupAndPeriphery } from '../deploy_l1_contracts.js'; +import { EthCheatCodes } from '../eth_cheat_codes.js'; +import type { L1ContractAddresses } from '../l1_contract_addresses.js'; +import { defaultL1TxUtilsConfig } from '../l1_tx_utils.js'; +import { startAnvil } from '../test/start_anvil.js'; +import type { L1Clients } from '../types.js'; +import { RegistryContract } from './registry.js'; + +const originalVersionSalt = 42; + +describe('Registry', () => { + let anvil: Anvil; + let rpcUrl: string; + let privateKey: PrivateKeyAccount; + let logger: Logger; + + let vkTreeRoot: Fr; + let protocolContractTreeRoot: Fr; + let l2FeeJuiceAddress: Fr; + let publicClient: L1Clients['publicClient']; + let walletClient: L1Clients['walletClient']; + let registry: RegistryContract; + let deployedAddresses: L1ContractAddresses; + + beforeAll(async () => { + logger = createLogger('ethereum:test:registry'); + // this is the 6th address that gets funded by the junk mnemonic + privateKey = privateKeyToAccount('0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba'); + vkTreeRoot = Fr.random(); + protocolContractTreeRoot = Fr.random(); + l2FeeJuiceAddress = Fr.random(); + + ({ anvil, rpcUrl } = await startAnvil()); + + ({ publicClient, walletClient } = createL1Clients(rpcUrl, privateKey)); + + const deployed = await deployL1Contracts(rpcUrl, privateKey, foundry, logger, { + ...DefaultL1ContractsConfig, + salt: originalVersionSalt, + vkTreeRoot, + protocolContractTreeRoot, + l2FeeJuiceAddress, + genesisArchiveRoot: Fr.random(), + genesisBlockHash: Fr.random(), + }); + // Since the registry cannot "see" the slash factory, we omit it from the addresses for this test + deployedAddresses = omit(deployed.l1ContractAddresses, 'slashFactoryAddress'); + registry = new RegistryContract(publicClient, deployedAddresses.registryAddress); + }); + + afterAll(async () => { + await anvil.stop(); + }); + + it('gets rollup versions', async () => { + const rollupAddress = deployedAddresses.rollupAddress; + { + const address = await registry.getCanonicalAddress(); + expect(address).toEqual(rollupAddress); + } + { + const address = await registry.getRollupAddress('canonical'); + expect(address).toEqual(rollupAddress); + } + { + const address = await registry.getRollupAddress(1); + expect(address).toEqual(rollupAddress); + } + }); + + it('handles non-existent versions', async () => { + const address = await registry.getRollupAddress(2); + expect(address).toBeUndefined(); + }); + + it('collects addresses', async () => { + await expect( + RegistryContract.collectAddresses(publicClient, deployedAddresses.registryAddress, 'canonical'), + ).resolves.toEqual(deployedAddresses); + + await expect( + RegistryContract.collectAddresses(publicClient, deployedAddresses.registryAddress, 1), + ).resolves.toEqual(deployedAddresses); + + // Version 2 does not exist + + await expect(RegistryContract.collectAddresses(publicClient, deployedAddresses.registryAddress, 2)).rejects.toThrow( + 'Rollup address is undefined', + ); + }); + + it('adds a version to the registry', async () => { + const addresses = await RegistryContract.collectAddresses( + publicClient, + deployedAddresses.registryAddress, + 'canonical', + ); + const newVersionSalt = originalVersionSalt + 1; + + const { rollup: newRollup, payloadAddress } = await deployRollupAndPeriphery( + rpcUrl, + foundry, + privateKey, + { + ...DefaultL1ContractsConfig, + salt: newVersionSalt, + vkTreeRoot, + protocolContractTreeRoot, + l2FeeJuiceAddress, + genesisArchiveRoot: Fr.random(), + genesisBlockHash: Fr.random(), + }, + { + feeJuicePortalAddress: addresses.feeJuicePortalAddress, + rewardDistributorAddress: addresses.rewardDistributorAddress, + stakingAssetAddress: addresses.stakingAssetAddress, + registryAddress: deployedAddresses.registryAddress, + }, + logger, + defaultL1TxUtilsConfig, + ); + + const { governance, voteAmount } = await createGovernanceProposal( + payloadAddress.toString(), + addresses, + privateKey, + publicClient, + logger, + ); + + await executeGovernanceProposal(0n, governance, voteAmount, privateKey, publicClient, walletClient, rpcUrl, logger); + + const newAddresses = await newRollup.getRollupAddresses(); + + const newCanonicalAddresses = await RegistryContract.collectAddresses( + publicClient, + deployedAddresses.registryAddress, + 'canonical', + ); + + expect(newCanonicalAddresses).toEqual({ + ...deployedAddresses, + ...newAddresses, + }); + + await expect( + RegistryContract.collectAddresses(publicClient, deployedAddresses.registryAddress, 2), + ).resolves.toEqual(newCanonicalAddresses); + + await expect( + RegistryContract.collectAddresses(publicClient, deployedAddresses.registryAddress, 1), + ).resolves.toEqual(deployedAddresses); + }); +}); + +async function executeGovernanceProposal( + proposalId: bigint, + governance: any, + voteAmount: bigint, + privateKey: PrivateKeyAccount, + publicClient: L1Clients['publicClient'], + walletClient: L1Clients['walletClient'], + rpcUrl: string, + logger: Logger, +) { + const proposal = await governance.read.getProposal([proposalId]); + + const waitL1Block = async () => { + await publicClient.waitForTransactionReceipt({ + hash: await walletClient.sendTransaction({ + to: privateKey.address, + value: 1n, + account: privateKey, + }), + }); + }; + + const cheatCodes = new EthCheatCodes(rpcUrl, logger); + + const timeToActive = proposal.creation + proposal.config.votingDelay; + logger.info(`Warping to ${timeToActive + 1n}`); + await cheatCodes.warp(Number(timeToActive + 1n)); + logger.info(`Warped to ${timeToActive + 1n}`); + await waitL1Block(); + + logger.info(`Voting`); + const voteTx = await governance.write.vote([proposalId, voteAmount, true], { account: privateKey }); + await publicClient.waitForTransactionReceipt({ hash: voteTx }); + logger.info(`Voted`); + + const timeToExecutable = timeToActive + proposal.config.votingDuration + proposal.config.executionDelay + 1n; + logger.info(`Warping to ${timeToExecutable}`); + await cheatCodes.warp(Number(timeToExecutable)); + logger.info(`Warped to ${timeToExecutable}`); + await waitL1Block(); + + const executeTx = await governance.write.execute([proposalId], { account: privateKey }); + await publicClient.waitForTransactionReceipt({ hash: executeTx }); + logger.info(`Executed proposal`); +} + +async function createGovernanceProposal( + payloadAddress: `0x${string}`, + addresses: L1ContractAddresses, + privateKey: PrivateKeyAccount, + publicClient: L1Clients['publicClient'], + logger: Logger, +) { + const token = getContract({ + address: addresses.feeJuiceAddress.toString(), + abi: FeeJuiceAbi, + client: publicClient, + }); + + const governance = getContract({ + address: addresses.governanceAddress.toString(), + abi: GovernanceAbi, + client: publicClient, + }); + + const lockAmount = 10000n * 10n ** 18n; + const voteAmount = 10000n * 10n ** 18n; + + const mintTx = await token.write.mint([privateKey.address, lockAmount + voteAmount], { account: privateKey }); + await publicClient.waitForTransactionReceipt({ hash: mintTx }); + logger.info(`Minted tokens`); + + const approveTx = await token.write.approve([addresses.governanceAddress.toString(), lockAmount + voteAmount], { + account: privateKey, + }); + await publicClient.waitForTransactionReceipt({ hash: approveTx }); + logger.info(`Approved tokens`); + + const depositTx = await governance.write.deposit([privateKey.address, lockAmount + voteAmount], { + account: privateKey, + }); + await publicClient.waitForTransactionReceipt({ hash: depositTx }); + logger.info(`Deposited tokens`); + + await governance.write.proposeWithLock([payloadAddress, privateKey.address], { + account: privateKey, + }); + + return { governance, voteAmount }; +} diff --git a/yarn-project/ethereum/src/contracts/registry.ts b/yarn-project/ethereum/src/contracts/registry.ts new file mode 100644 index 000000000000..dd8383f263cf --- /dev/null +++ b/yarn-project/ethereum/src/contracts/registry.ts @@ -0,0 +1,100 @@ +import { EthAddress } from '@aztec/foundation/eth-address'; +import { RegistryAbi } from '@aztec/l1-artifacts/RegistryAbi'; +import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi'; + +import { + type Chain, + type GetContractReturnType, + type Hex, + type HttpTransport, + type PublicClient, + getContract, +} from 'viem'; + +import type { L1ContractAddresses } from '../l1_contract_addresses.js'; +import type { L1Clients } from '../types.js'; +import { GovernanceContract } from './governance.js'; +import { RollupContract } from './rollup.js'; + +export class RegistryContract { + public address: EthAddress; + private readonly registry: GetContractReturnType>; + + constructor(public readonly client: L1Clients['publicClient'], address: Hex | EthAddress) { + if (address instanceof EthAddress) { + address = address.toString(); + } + this.address = EthAddress.fromString(address); + this.registry = getContract({ address, abi: RegistryAbi, client }); + } + + /** + * Returns the address of the rollup for a given version. + * @param version - The version of the rollup. 'canonical' can be used to get the canonical address (i.e. the latest version). + * @returns The address of the rollup. If the rollup is not set for this version, returns undefined. + */ + public async getRollupAddress(version: number | bigint | 'canonical'): Promise { + if (version === 'canonical') { + return this.getCanonicalAddress(); + } + + if (typeof version === 'number') { + version = BigInt(version); + } + + const snapshot = await this.registry.read.getSnapshot([version]); + const address = EthAddress.fromString(snapshot.rollup); + return address.equals(EthAddress.ZERO) ? undefined : address; + } + + /** + * Returns the canonical address of the rollup. + * @returns The canonical address of the rollup. If the rollup is not set, returns undefined. + */ + public async getCanonicalAddress(): Promise { + const snapshot = await this.registry.read.getCurrentSnapshot(); + const address = EthAddress.fromString(snapshot.rollup); + return address.equals(EthAddress.ZERO) ? undefined : address; + } + + public async getGovernanceAddresses(): Promise< + Pick + > { + const governanceAddress = await this.registry.read.getGovernance(); + const governance = new GovernanceContract(this.client, governanceAddress); + const governanceProposer = await governance.getProposer(); + return { + governanceAddress: governance.address, + governanceProposerAddress: governanceProposer.address, + }; + } + + public static async collectAddresses( + client: L1Clients['publicClient'], + registryAddress: Hex | EthAddress, + rollupVersion: number | bigint | 'canonical', + ): Promise { + const registry = new RegistryContract(client, registryAddress); + const governanceAddresses = await registry.getGovernanceAddresses(); + const rollupAddress = await registry.getRollupAddress(rollupVersion); + + if (rollupAddress === undefined) { + throw new Error('Rollup address is undefined'); + } + + const rollup = new RollupContract(client, rollupAddress); + const addresses = await rollup.getRollupAddresses(); + const feeAsset = getContract({ + address: addresses.feeJuiceAddress.toString(), + abi: TestERC20Abi, + client, + }); + const coinIssuer = await feeAsset.read.owner(); + return { + registryAddress: registry.address, + ...governanceAddresses, + ...addresses, + coinIssuerAddress: EthAddress.fromString(coinIssuer), + }; + } +} diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index 8b2fc6d561f9..17cd89ffb7df 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -62,7 +62,10 @@ export class RollupContract { return new RollupContract(client, address); } - constructor(public readonly client: PublicClient, address: Hex) { + constructor(public readonly client: PublicClient, address: Hex | EthAddress) { + if (address instanceof EthAddress) { + address = address.toString(); + } this.rollup = getContract({ address, abi: RollupAbi, client }); } diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index b3eeaee8dab8..3cdd77fbc552 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -18,6 +18,8 @@ import { InboxBytecode, OutboxAbi, OutboxBytecode, + RegisterNewRollupVersionPayloadAbi, + RegisterNewRollupVersionPayloadBytecode, RegistryAbi, RegistryBytecode, RewardDistributorAbi, @@ -58,6 +60,8 @@ import { foundry } from 'viem/chains'; import { isAnvilTestChain } from './chain.js'; import type { L1ContractsConfig } from './config.js'; +import { RegistryContract } from './contracts/registry.js'; +import { RollupContract } from './contracts/rollup.js'; import type { L1ContractAddresses } from './l1_contract_addresses.js'; import { L1TxUtils, type L1TxUtilsConfig, defaultL1TxUtilsConfig } from './l1_tx_utils.js'; import type { L1Clients } from './types.js'; @@ -176,6 +180,10 @@ export const l1Artifacts = { contractAbi: SlashFactoryAbi, contractBytecode: SlashFactoryBytecode as Hex, }, + registerNewRollupVersionPayload: { + contractAbi: RegisterNewRollupVersionPayloadAbi, + contractBytecode: RegisterNewRollupVersionPayloadBytecode as Hex, + }, }; export interface DeployL1ContractsArgs extends L1ContractsConfig { @@ -241,6 +249,144 @@ export function createL1Clients( return { walletClient, publicClient } as L1Clients; } +export const deployRollupAndPeriphery = async ( + rpcUrl: string, + chain: Chain, + account: HDAccount | PrivateKeyAccount, + args: DeployL1ContractsArgs, + addresses: Pick< + L1ContractAddresses, + 'registryAddress' | 'feeJuicePortalAddress' | 'rewardDistributorAddress' | 'stakingAssetAddress' + >, + logger: Logger, + txUtilsConfig: L1TxUtilsConfig, +) => { + const { walletClient, publicClient } = createL1Clients(rpcUrl, account, chain); + const deployer = new L1Deployer(walletClient, publicClient, args.salt, logger, txUtilsConfig); + + const rollup = await deployRollup(walletClient, publicClient, deployer, args, addresses, logger); + const payloadAddress = await deployUpgradePayload(deployer, { + registryAddress: addresses.registryAddress, + rollupAddress: EthAddress.fromString(rollup.address), + }); + const slashFactoryAddress = await deploySlashFactory(deployer, rollup.address, logger); + + await deployer.waitForDeployments(); + + return { rollup, payloadAddress, slashFactoryAddress }; +}; + +export const deploySlashFactory = async (deployer: L1Deployer, rollupAddress: Hex, logger: Logger) => { + const slashFactoryAddress = await deployer.deploy(l1Artifacts.slashFactory, [rollupAddress]); + logger.verbose(`Deployed SlashFactory at ${slashFactoryAddress}`); + return slashFactoryAddress; +}; + +export const deployUpgradePayload = async ( + deployer: L1Deployer, + addresses: Pick, +) => { + const payloadAddress = await deployer.deploy(l1Artifacts.registerNewRollupVersionPayload, [ + addresses.registryAddress.toString(), + addresses.rollupAddress.toString(), + ]); + + return payloadAddress; +}; + +export const deployRollup = async ( + walletClient: L1Clients['walletClient'], + publicClient: L1Clients['publicClient'], + deployer: L1Deployer, + args: DeployL1ContractsArgs, + addresses: Pick, + logger: Logger, +): Promise => { + const rollupConfigArgs = { + aztecSlotDuration: args.aztecSlotDuration, + aztecEpochDuration: args.aztecEpochDuration, + targetCommitteeSize: args.aztecTargetCommitteeSize, + aztecProofSubmissionWindow: args.aztecProofSubmissionWindow, + minimumStake: args.minimumStake, + slashingQuorum: args.slashingQuorum, + slashingRoundSize: args.slashingRoundSize, + }; + logger.verbose(`Rollup config args`, rollupConfigArgs); + const rollupArgs = [ + addresses.feeJuicePortalAddress.toString(), + addresses.rewardDistributorAddress.toString(), + addresses.stakingAssetAddress.toString(), + args.vkTreeRoot.toString(), + args.protocolContractTreeRoot.toString(), + args.genesisArchiveRoot.toString(), + args.genesisBlockHash.toString(), + walletClient.account.address.toString(), + rollupConfigArgs, + ]; + + const rollupAddress = await deployer.deploy(l1Artifacts.rollup, rollupArgs); + logger.verbose(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs); + + await deployer.waitForDeployments(); + logger.verbose(`All core contracts have been deployed`); + + const rollup = getContract({ + address: getAddress(rollupAddress.toString()), + abi: l1Artifacts.rollup.contractAbi, + client: walletClient, + }); + + const txHashes: Hex[] = []; + + if (args.initialValidators && args.initialValidators.length > 0) { + // Check if some of the initial validators are already registered, so we support idempotent deployments + const validatorsInfo = await Promise.all( + args.initialValidators.map(async address => ({ address, ...(await rollup.read.getInfo([address.toString()])) })), + ); + const existingValidators = validatorsInfo.filter(v => v.status !== 0); + if (existingValidators.length > 0) { + logger.warn( + `Validators ${existingValidators.map(v => v.address).join(', ')} already exist. Skipping from initialization.`, + ); + } + + const newValidatorsAddresses = validatorsInfo.filter(v => v.status === 0).map(v => v.address.toString()); + + if (newValidatorsAddresses.length > 0) { + const stakingAsset = getContract({ + address: addresses.stakingAssetAddress.toString(), + abi: l1Artifacts.stakingAsset.contractAbi, + client: walletClient, + }); + // Mint tokens, approve them, use cheat code to initialise validator set without setting up the epoch. + const stakeNeeded = args.minimumStake * BigInt(newValidatorsAddresses.length); + await Promise.all( + [ + await stakingAsset.write.mint([walletClient.account.address, stakeNeeded], {} as any), + await stakingAsset.write.approve([rollupAddress.toString(), stakeNeeded], {} as any), + ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })), + ); + + const validators = newValidatorsAddresses.map(v => ({ + attester: v, + proposer: getExpectedAddress(ForwarderAbi, ForwarderBytecode, [v], v).address, + withdrawer: v, + amount: args.minimumStake, + })); + const initiateValidatorSetTxHash = await rollup.write.cheat__InitialiseValidatorSet([validators]); + txHashes.push(initiateValidatorSetTxHash); + logger.info(`Initialized validator set`, { + validators, + txHash: initiateValidatorSetTxHash, + }); + } + } + + await Promise.all(txHashes.map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash }))); + + return new RollupContract(publicClient, rollupAddress); +}; + /** * Deploys the aztec L1 contracts; Rollup & (optionally) Decoder Helper. * @param rpcUrl - URL of the ETH RPC to use for deployment. @@ -280,29 +426,24 @@ export const deployL1Contracts = async ( logger.verbose(`Deploying contracts from ${account.address.toString()}`); - const walletClient = createWalletClient({ account, chain, transport: http(rpcUrl) }); - const publicClient = createPublicClient({ chain, transport: http(rpcUrl) }); + const { walletClient, publicClient } = createL1Clients(rpcUrl, account, chain); // Governance stuff - const govDeployer = new L1Deployer(walletClient, publicClient, args.salt, logger, txUtilsConfig); + const deployer = new L1Deployer(walletClient, publicClient, args.salt, logger, txUtilsConfig); - const registryAddress = await govDeployer.deploy(l1Artifacts.registry, [account.address.toString()]); + const registryAddress = await deployer.deploy(l1Artifacts.registry, [account.address.toString()]); logger.verbose(`Deployed Registry at ${registryAddress}`); - const feeAssetAddress = await govDeployer.deploy(l1Artifacts.feeAsset, [ - 'FeeJuice', - 'FEE', - account.address.toString(), - ]); + const feeAssetAddress = await deployer.deploy(l1Artifacts.feeAsset, ['FeeJuice', 'FEE', account.address.toString()]); logger.verbose(`Deployed Fee Juice at ${feeAssetAddress}`); - const stakingAssetAddress = await govDeployer.deploy(l1Artifacts.stakingAsset, [ + const stakingAssetAddress = await deployer.deploy(l1Artifacts.stakingAsset, [ 'Staking', 'STK', account.address.toString(), ]); logger.verbose(`Deployed Staking Asset at ${stakingAssetAddress}`); - const governanceProposerAddress = await govDeployer.deploy(l1Artifacts.governanceProposer, [ + const governanceProposerAddress = await deployer.deploy(l1Artifacts.governanceProposer, [ registryAddress.toString(), args.governanceProposerQuorum, args.governanceProposerRoundSize, @@ -311,32 +452,26 @@ export const deployL1Contracts = async ( // @note @LHerskind the assets are expected to be the same at some point, but for better // configurability they are different for now. - const governanceAddress = await govDeployer.deploy(l1Artifacts.governance, [ + const governanceAddress = await deployer.deploy(l1Artifacts.governance, [ feeAssetAddress.toString(), governanceProposerAddress.toString(), ]); logger.verbose(`Deployed Governance at ${governanceAddress}`); - const coinIssuerAddress = await govDeployer.deploy(l1Artifacts.coinIssuer, [ + const coinIssuerAddress = await deployer.deploy(l1Artifacts.coinIssuer, [ feeAssetAddress.toString(), 1n * 10n ** 18n, // @todo #8084 governanceAddress.toString(), ]); logger.verbose(`Deployed CoinIssuer at ${coinIssuerAddress}`); - const rewardDistributorAddress = await govDeployer.deploy(l1Artifacts.rewardDistributor, [ + const rewardDistributorAddress = await deployer.deploy(l1Artifacts.rewardDistributor, [ feeAssetAddress.toString(), registryAddress.toString(), governanceAddress.toString(), ]); logger.verbose(`Deployed RewardDistributor at ${rewardDistributorAddress}`); - logger.verbose(`Waiting for governance contracts to be deployed`); - await govDeployer.waitForDeployments(); - logger.verbose(`All governance contracts deployed`); - - const deployer = new L1Deployer(walletClient, publicClient, args.salt, logger, args.l1TxConfig ?? {}); - const feeJuicePortalAddress = await deployer.deploy(l1Artifacts.feeJuicePortal, [ registryAddress.toString(), feeAssetAddress.toString(), @@ -344,37 +479,9 @@ export const deployL1Contracts = async ( ]); logger.verbose(`Deployed Fee Juice Portal at ${feeJuicePortalAddress}`); - const rollupConfigArgs = { - aztecSlotDuration: args.aztecSlotDuration, - aztecEpochDuration: args.aztecEpochDuration, - targetCommitteeSize: args.aztecTargetCommitteeSize, - aztecProofSubmissionWindow: args.aztecProofSubmissionWindow, - minimumStake: args.minimumStake, - slashingQuorum: args.slashingQuorum, - slashingRoundSize: args.slashingRoundSize, - }; - logger.verbose(`Rollup config args`, rollupConfigArgs); - const rollupArgs = [ - feeJuicePortalAddress.toString(), - rewardDistributorAddress.toString(), - stakingAssetAddress.toString(), - args.vkTreeRoot.toString(), - args.protocolContractTreeRoot.toString(), - args.genesisArchiveRoot.toString(), - args.genesisBlockHash.toString(), - account.address.toString(), - rollupConfigArgs, - ]; - await deployer.waitForDeployments(); - - const rollupAddress = await deployer.deploy(l1Artifacts.rollup, rollupArgs); - logger.verbose(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs); - - const slashFactoryAddress = await deployer.deploy(l1Artifacts.slashFactory, [rollupAddress.toString()]); - logger.verbose(`Deployed SlashFactory at ${slashFactoryAddress}`); - + logger.verbose(`Waiting for governance contracts to be deployed`); await deployer.waitForDeployments(); - logger.verbose(`All core contracts have been deployed`); + logger.verbose(`All governance contracts deployed`); const feeJuicePortal = getContract({ address: feeJuicePortalAddress.toString(), @@ -387,65 +494,19 @@ export const deployL1Contracts = async ( abi: l1Artifacts.feeAsset.contractAbi, client: walletClient, }); - - const stakingAsset = getContract({ - address: stakingAssetAddress.toString(), - abi: l1Artifacts.stakingAsset.contractAbi, - client: walletClient, - }); - - const rollup = getContract({ - address: getAddress(rollupAddress.toString()), - abi: l1Artifacts.rollup.contractAbi, - client: walletClient, - }); - // Transaction hashes to await const txHashes: Hex[] = []; - { + if (!(await feeAsset.read.freeForAll())) { const txHash = await feeAsset.write.setFreeForAll([true], {} as any); logger.verbose(`Fee asset set to free for all in ${txHash}`); txHashes.push(txHash); } - if (args.initialValidators && args.initialValidators.length > 0) { - // Check if some of the initial validators are already registered, so we support idempotent deployments - const validatorsInfo = await Promise.all( - args.initialValidators.map(async address => ({ address, ...(await rollup.read.getInfo([address.toString()])) })), - ); - const existingValidators = validatorsInfo.filter(v => v.status !== 0); - if (existingValidators.length > 0) { - logger.warn( - `Validators ${existingValidators.map(v => v.address).join(', ')} already exist. Skipping from initialization.`, - ); - } - - const newValidatorsAddresses = validatorsInfo.filter(v => v.status === 0).map(v => v.address.toString()); - - if (newValidatorsAddresses.length > 0) { - // Mint tokens, approve them, use cheat code to initialise validator set without setting up the epoch. - const stakeNeeded = args.minimumStake * BigInt(newValidatorsAddresses.length); - await Promise.all( - [ - await stakingAsset.write.mint([walletClient.account.address, stakeNeeded], {} as any), - await stakingAsset.write.approve([rollupAddress.toString(), stakeNeeded], {} as any), - ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })), - ); - - const validators = newValidatorsAddresses.map(v => ({ - attester: v, - proposer: getExpectedAddress(ForwarderAbi, ForwarderBytecode, [v], v).address, - withdrawer: v, - amount: args.minimumStake, - })); - const initiateValidatorSetTxHash = await rollup.write.cheat__InitialiseValidatorSet([validators]); - txHashes.push(initiateValidatorSetTxHash); - logger.info(`Initialized validator set`, { - validators, - txHash: initiateValidatorSetTxHash, - }); - } + if ((await feeAsset.read.owner()) !== getAddress(coinIssuerAddress.toString())) { + const txHash = await feeAsset.write.transferOwnership([coinIssuerAddress.toString()], { account }); + logger.verbose(`Fee asset transferred ownership to coin issuer in ${txHash}`); + txHashes.push(txHash); } // @note This value MUST match what is in `constants.nr`. It is currently specified here instead of just importing @@ -472,35 +533,19 @@ export const deployL1Contracts = async ( `Initialized Fee Juice Portal at ${feeJuicePortalAddress} to bridge between L1 ${feeAssetAddress} to L2 ${args.l2FeeJuiceAddress}`, ); - if (isAnvilTestChain(chain.id)) { - // @note We make a time jump PAST the very first slot to not have to deal with the edge case of the first slot. - // The edge case being that the genesis block is already occupying slot 0, so we cannot have another block. - try { - // Need to get the time - const currentSlot = (await rollup.read.getCurrentSlot()) as bigint; - - if (BigInt(currentSlot) === 0n) { - const ts = Number(await rollup.read.getTimestampForSlot([1n])); - await rpcCall('evm_setNextBlockTimestamp', [ts]); - await rpcCall('hardhat_mine', [1]); - const currentSlot = (await rollup.read.getCurrentSlot()) as bigint; - - if (BigInt(currentSlot) !== 1n) { - throw new Error(`Error jumping time: current slot is ${currentSlot}`); - } - logger.info(`Jumped to slot 1`); - } - } catch (e) { - throw new Error(`Error jumping time: ${e}`); - } - } - - // Inbox and Outbox are immutable and are deployed from Rollup's constructor so we just fetch them from the contract. - const inboxAddress = EthAddress.fromString((await rollup.read.getInbox()) as any); - logger.verbose(`Inbox available at ${inboxAddress}`); + const rollup = await deployRollup( + walletClient, + publicClient, + deployer, + args, + { feeJuicePortalAddress, rewardDistributorAddress, stakingAssetAddress }, + logger, + ); + const slashFactoryAddress = await deploySlashFactory(deployer, rollup.address, logger); - const outboxAddress = EthAddress.fromString((await rollup.read.getOutbox()) as any); - logger.verbose(`Outbox available at ${outboxAddress}`); + logger.verbose('Waiting for rollup and slash factory to be deployed'); + await deployer.waitForDeployments(); + logger.verbose(`Rollup and slash factory deployed`); // We need to call a function on the registry to set the various contract addresses. const registryContract = getContract({ @@ -508,14 +553,15 @@ export const deployL1Contracts = async ( abi: l1Artifacts.registry.contractAbi, client: walletClient, }); - if (!(await registryContract.read.isRollupRegistered([getAddress(rollupAddress.toString())]))) { - const upgradeTxHash = await registryContract.write.upgrade([getAddress(rollupAddress.toString())], { account }); + + if (!(await registryContract.read.isRollupRegistered([getAddress(rollup.address.toString())]))) { + const upgradeTxHash = await registryContract.write.upgrade([getAddress(rollup.address.toString())], { account }); logger.verbose( - `Upgrading registry contract at ${registryAddress} to rollup ${rollupAddress} in tx ${upgradeTxHash}`, + `Upgrading registry contract at ${registryAddress} to rollup ${rollup.address} in tx ${upgradeTxHash}`, ); txHashes.push(upgradeTxHash); } else { - logger.verbose(`Registry ${registryAddress} has already registered rollup ${rollupAddress}`); + logger.verbose(`Registry ${registryAddress} has already registered rollup ${rollup.address}`); } // If the owner is not the Governance contract, transfer ownership to the Governance contract @@ -535,28 +581,40 @@ export const deployL1Contracts = async ( // Wait for all actions to be mined await Promise.all(txHashes.map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash }))); logger.verbose(`All transactions for L1 deployment have been mined`); - - const l1Contracts: L1ContractAddresses = { - rollupAddress, - registryAddress, - inboxAddress, - outboxAddress, - feeJuiceAddress: feeAssetAddress, - stakingAssetAddress, - feeJuicePortalAddress, - coinIssuerAddress, - rewardDistributorAddress, - governanceProposerAddress, - governanceAddress, - slashFactoryAddress, - }; + const l1Contracts = await RegistryContract.collectAddresses(publicClient, registryAddress, 'canonical'); logger.info(`Aztec L1 contracts initialized`, l1Contracts); + if (isAnvilTestChain(chain.id)) { + // @note We make a time jump PAST the very first slot to not have to deal with the edge case of the first slot. + // The edge case being that the genesis block is already occupying slot 0, so we cannot have another block. + try { + // Need to get the time + const currentSlot = await rollup.getSlotNumber(); + + if (BigInt(currentSlot) === 0n) { + const ts = Number(await rollup.getTimestampForSlot(1n)); + await rpcCall('evm_setNextBlockTimestamp', [ts]); + await rpcCall('hardhat_mine', [1]); + const currentSlot = await rollup.getSlotNumber(); + + if (BigInt(currentSlot) !== 1n) { + throw new Error(`Error jumping time: current slot is ${currentSlot}`); + } + logger.info(`Jumped to slot 1`); + } + } catch (e) { + throw new Error(`Error jumping time: ${e}`); + } + } + return { walletClient, publicClient, - l1ContractAddresses: l1Contracts, + l1ContractAddresses: { + ...l1Contracts, + slashFactoryAddress, + }, }; }; diff --git a/yarn-project/ethereum/src/l1_contract_addresses.ts b/yarn-project/ethereum/src/l1_contract_addresses.ts index 872052586333..a4dd1da75389 100644 --- a/yarn-project/ethereum/src/l1_contract_addresses.ts +++ b/yarn-project/ethereum/src/l1_contract_addresses.ts @@ -21,13 +21,12 @@ export const L1ContractsNames = [ 'governanceProposerAddress', 'governanceAddress', 'stakingAssetAddress', - 'slashFactoryAddress', ] as const; /** Provides the directory of current L1 contract addresses */ export type L1ContractAddresses = { [K in (typeof L1ContractsNames)[number]]: EthAddress; -}; +} & { slashFactoryAddress?: EthAddress | undefined }; export const L1ContractAddressesSchema = z.object({ rollupAddress: schemas.EthAddress, @@ -41,7 +40,7 @@ export const L1ContractAddressesSchema = z.object({ rewardDistributorAddress: schemas.EthAddress, governanceProposerAddress: schemas.EthAddress, governanceAddress: schemas.EthAddress, - slashFactoryAddress: schemas.EthAddress, + slashFactoryAddress: schemas.EthAddress.optional(), }) satisfies ZodFor; const parseEnv = (val: string) => EthAddress.fromString(val); diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index 3c8a26f15d9f..2a4cf1b092a4 100755 --- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh +++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh @@ -27,6 +27,7 @@ contracts=( "GovernanceProposer" "Governance" "NewGovernanceProposerPayload" + "RegisterNewRollupVersionPayload" "ValidatorSelectionLib" "ExtRollupLib" "SlashingProposer" diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 7d4ce74b87a2..293884092d7e 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -694,6 +694,7 @@ __metadata: dotenv: "npm:^16.0.3" get-port: "npm:^7.1.0" jest: "npm:^29.5.0" + lodash.omit: "npm:^4.5.0" ts-node: "npm:^10.9.1" tslib: "npm:^2.4.0" typescript: "npm:^5.0.4"