diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index fb1c72b14e48..04e2f262ced3 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -504,6 +504,10 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { return rollupStore.provingCostPerMana; } + function getSlashFactory() external view override(IRollup) returns (address) { + return rollupStore.slashFactory; + } + function getProvingCostPerManaInFeeAsset() external view diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 6272fa1010d6..79226001c411 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -240,6 +240,10 @@ contract RollupCore is return accumulatedRewards; } + function setSlashFactory(address _slashFactory) external override(IRollupCore) onlyOwner { + rollupStore.slashFactory = _slashFactory; + } + function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) external override(IStakingCore) diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 864f20504d16..d5ca0e78edcd 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -58,6 +58,7 @@ struct EpochRewards { // The below blobPublicInputsHashes are filled when proposing a block, then used to verify an epoch proof. // TODO(#8955): When implementing batched kzg proofs, store one instance per epoch rather than block struct RollupStore { + address slashFactory; mapping(uint256 blockNumber => BlockLog log) blocks; mapping(uint256 blockNumber => bytes32) blobPublicInputsHashes; ChainTips tips; @@ -109,6 +110,8 @@ interface IRollupCore { function setProvingCostPerMana(EthValue _provingCostPerMana) external; + function setSlashFactory(address _slashFactory) external; + function propose( ProposeArgs calldata _args, Signature[] memory _signatures, @@ -201,4 +204,6 @@ interface IRollup is IRollupCore { function getProvingCostPerManaInEth() external view returns (EthValue); function getProvingCostPerManaInFeeAsset() external view returns (FeeAssetValue); + + function getSlashFactory() external view returns (address); } diff --git a/l1-contracts/src/governance/Governance.sol b/l1-contracts/src/governance/Governance.sol index 32bc7485557d..0613e275fe01 100644 --- a/l1-contracts/src/governance/Governance.sol +++ b/l1-contracts/src/governance/Governance.sol @@ -48,13 +48,13 @@ contract Governance is IGovernance { configuration = DataStructures.Configuration({ proposeConfig: DataStructures.ProposeConfiguration({ - lockDelay: Timestamp.wrap(3600), + lockDelay: Timestamp.wrap(360), lockAmount: 1000e18 }), - votingDelay: Timestamp.wrap(3600), - votingDuration: Timestamp.wrap(3600), - executionDelay: Timestamp.wrap(3600), - gracePeriod: Timestamp.wrap(3600), + votingDelay: Timestamp.wrap(360), + votingDuration: Timestamp.wrap(360), + executionDelay: Timestamp.wrap(360), + gracePeriod: Timestamp.wrap(360), quorum: 0.1e18, voteDifferential: 0.04e18, minimumVotes: 1000e18 diff --git a/l1-contracts/src/governance/libraries/ConfigurationLib.sol b/l1-contracts/src/governance/libraries/ConfigurationLib.sol index 482dee7ff901..4d9cfdff1a7d 100644 --- a/l1-contracts/src/governance/libraries/ConfigurationLib.sol +++ b/l1-contracts/src/governance/libraries/ConfigurationLib.sol @@ -16,7 +16,7 @@ library ConfigurationLib { uint256 internal constant VOTES_LOWER = 1; - Timestamp internal constant TIME_LOWER = Timestamp.wrap(3600); + Timestamp internal constant TIME_LOWER = Timestamp.wrap(360); Timestamp internal constant TIME_UPPER = Timestamp.wrap(30 * 24 * 3600); function withdrawalDelay(DataStructures.Configuration storage _self) 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/values/1-validators.yaml b/spartan/aztec-network/values/1-validators.yaml index ba3d9e14e3e7..31a5692f2d2b 100644 --- a/spartan/aztec-network/values/1-validators.yaml +++ b/spartan/aztec-network/values/1-validators.yaml @@ -3,6 +3,8 @@ telemetry: validator: replicas: 1 + sequencer: + minTxsPerBlock: 0 validator: disabled: false diff --git a/yarn-project/cli/src/cmds/l1/deploy_new_rollup.ts b/yarn-project/cli/src/cmds/l1/deploy_new_rollup.ts new file mode 100644 index 000000000000..64892c00e20b --- /dev/null +++ b/yarn-project/cli/src/cmds/l1/deploy_new_rollup.ts @@ -0,0 +1,58 @@ +import { getInitialTestAccounts } from '@aztec/accounts/testing'; +import { getL1ContractsConfigEnvVars } from '@aztec/ethereum'; +import { type EthAddress } from '@aztec/foundation/eth-address'; +import { type LogFn, type Logger } from '@aztec/foundation/log'; +import { getGenesisValues } from '@aztec/world-state/testing'; + +import { deployNewRollupContracts } from '../../utils/aztec.js'; + +export async function deployNewRollup( + registryAddress: EthAddress, + rpcUrl: string, + chainId: number, + privateKey: string | undefined, + mnemonic: string, + mnemonicIndex: number, + salt: number | undefined, + testAccounts: boolean, + json: boolean, + initialValidators: EthAddress[], + log: LogFn, + debugLogger: Logger, +) { + const config = getL1ContractsConfigEnvVars(); + + const initialFundedAccounts = testAccounts ? await getInitialTestAccounts() : []; + const { genesisBlockHash, genesisArchiveRoot } = await getGenesisValues(initialFundedAccounts.map(a => a.address)); + + const { payloadAddress, rollup } = await deployNewRollupContracts( + registryAddress, + rpcUrl, + chainId, + privateKey, + mnemonic, + mnemonicIndex, + salt, + initialValidators, + genesisArchiveRoot, + genesisBlockHash, + config, + debugLogger, + ); + + if (json) { + log( + JSON.stringify( + { + payloadAddress: payloadAddress.toString(), + rollupAddress: rollup.address, + }, + null, + 2, + ), + ); + } else { + log(`Payload Address: ${payloadAddress.toString()}`); + log(`Rollup Address: ${rollup.address}`); + } +} diff --git a/yarn-project/cli/src/cmds/l1/index.ts b/yarn-project/cli/src/cmds/l1/index.ts index 4ed172b4ad82..8627d5de4a0b 100644 --- a/yarn-project/cli/src/cmds/l1/index.ts +++ b/yarn-project/cli/src/cmds/l1/index.ts @@ -57,6 +57,48 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger ); }); + program + .command('deploy-new-rollup') + .description('Deploys a new rollup contract and a payload to upgrade the registry with it.') + .requiredOption('-r, --registry-address ', 'The address of the registry contract', parseEthereumAddress) + .requiredOption( + '-u, --rpc-url ', + 'Url of the ethereum host. Chain identifiers localhost and testnet can be used', + ETHEREUM_HOST, + ) + .option('-pk, --private-key ', 'The private key to use for deployment', PRIVATE_KEY) + .option('--validators ', 'Comma separated list of validators') + .option( + '-m, --mnemonic ', + 'The mnemonic to use in deployment', + 'test test test test test test test test test test test junk', + ) + .option('-i, --mnemonic-index ', 'The index of the mnemonic to use in deployment', arg => parseInt(arg), 0) + .addOption(l1ChainIdOption) + .option('--salt ', 'The optional salt to use in deployment', arg => parseInt(arg)) + .option('--json', 'Output the contract addresses in JSON format') + .option('--test-accounts', 'Populate genesis state with initial fee juice for test accounts') + .action(async options => { + const { deployNewRollup } = await import('./deploy_new_rollup.js'); + + const initialValidators = + options.validators?.split(',').map((validator: string) => EthAddress.fromString(validator)) || []; + await deployNewRollup( + options.registryAddress, + options.rpcUrl, + options.l1ChainId, + options.privateKey, + options.mnemonic, + options.mnemonicIndex, + options.salt, + options.testAccounts, + options.json, + initialValidators, + log, + debugLogger, + ); + }); + program .command('generate-l1-account') .description('Generates a new private key for an account on L1.') diff --git a/yarn-project/cli/src/utils/aztec.ts b/yarn-project/cli/src/utils/aztec.ts index 6172e36a87ec..28484c3a74dc 100644 --- a/yarn-project/cli/src/utils/aztec.ts +++ b/yarn-project/cli/src/utils/aztec.ts @@ -1,6 +1,6 @@ import { type ContractArtifact, type FunctionArtifact, loadContractArtifact } from '@aztec/aztec.js/abi'; import { type PXE } from '@aztec/circuit-types'; -import { type DeployL1Contracts, type L1ContractsConfig } from '@aztec/ethereum'; +import { type DeployL1Contracts, type L1ContractsConfig, type RollupContract } from '@aztec/ethereum'; import { FunctionType } from '@aztec/foundation/abi'; import { type EthAddress } from '@aztec/foundation/eth-address'; import { type Fr } from '@aztec/foundation/fields'; @@ -87,6 +87,51 @@ export async function deployAztecContracts( ); } +export async function deployNewRollupContracts( + registryAddress: EthAddress, + rpcUrl: string, + chainId: number, + privateKey: string | undefined, + mnemonic: string, + mnemonicIndex: number, + salt: number | undefined, + initialValidators: EthAddress[], + genesisArchiveRoot: Fr, + genesisBlockHash: Fr, + config: L1ContractsConfig, + logger: Logger, +): Promise<{ payloadAddress: EthAddress; rollup: RollupContract }> { + const { createEthereumChain, deployRollupAndUpgradePayload } = await import('@aztec/ethereum'); + const { mnemonicToAccount, privateKeyToAccount } = await import('viem/accounts'); + const { getVKTreeRoot } = await import('@aztec/noir-protocol-circuits-types/vks'); + + const account = !privateKey + ? mnemonicToAccount(mnemonic!, { addressIndex: mnemonicIndex }) + : privateKeyToAccount(`${privateKey.startsWith('0x') ? '' : '0x'}${privateKey}` as `0x${string}`); + const chain = createEthereumChain(rpcUrl, chainId); + + const { payloadAddress, rollup } = await deployRollupAndUpgradePayload( + chain.rpcUrl, + chain.chainInfo, + account, + { + salt, + vkTreeRoot: getVKTreeRoot(), + protocolContractTreeRoot, + l2FeeJuiceAddress: ProtocolContractAddress.FeeJuice, + genesisArchiveRoot, + genesisBlockHash, + initialValidators, + ...config, + }, + registryAddress, + logger, + config, + ); + + return { payloadAddress, rollup }; +} + /** Sets the assumed proven block number on the rollup contract on L1 */ export async function setAssumeProvenThrough( blockNumber: number, diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index c40c15fedf1d..9ea136c14851 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -78,18 +78,7 @@ import getPort from 'get-port'; import { tmpdir } from 'os'; import * as path from 'path'; import { inspect } from 'util'; -import { - type Account, - type Chain, - type HDAccount, - type Hex, - type HttpTransport, - type PrivateKeyAccount, - createPublicClient, - createWalletClient, - getContract, - http, -} from 'viem'; +import { type Chain, type HDAccount, type Hex, type PrivateKeyAccount, getContract } from 'viem'; import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; @@ -210,7 +199,7 @@ export async function setupPXEService( * @returns Private eXecution Environment (PXE) client, viem wallets, contract addresses etc. */ async function setupWithRemoteEnvironment( - account: Account, + account: HDAccount | PrivateKeyAccount, config: AztecNodeConfig, logger: Logger, numberOfAccounts: number, @@ -226,15 +215,8 @@ async function setupWithRemoteEnvironment( logger.verbose(`Retrieving contract addresses from ${PXE_URL}`); const l1Contracts = (await pxeClient.getNodeInfo()).l1ContractAddresses; - const walletClient = createWalletClient({ - account, - chain: foundry, - transport: http(config.l1RpcUrl), - }); - const publicClient = createPublicClient({ - chain: foundry, - transport: http(config.l1RpcUrl), - }); + const { walletClient, publicClient } = createL1Clients(config.l1RpcUrl, account, foundry); + const deployL1ContractsValues: DeployL1Contracts = { l1ContractAddresses: l1Contracts, walletClient, @@ -597,21 +579,21 @@ export async function setup( return { aztecNode, - proverNode, - pxe, - deployL1ContractsValues, + blobSink, + cheatCodes, config, + dateProvider, + deployL1ContractsValues, initialFundedAccounts, - wallet: wallets[0], - wallets, logger, - cheatCodes, + proverNode, + pxe, sequencer, - watcher, - dateProvider, - blobSink, - telemetryClient: telemetry, teardown, + telemetryClient: telemetry, + wallet: wallets[0], + wallets, + watcher, }; } diff --git a/yarn-project/end-to-end/src/spartan/upgrade_rollup_version.test.ts b/yarn-project/end-to-end/src/spartan/upgrade_rollup_version.test.ts new file mode 100644 index 000000000000..67fd10949830 --- /dev/null +++ b/yarn-project/end-to-end/src/spartan/upgrade_rollup_version.test.ts @@ -0,0 +1,283 @@ +import { type NodeInfo, type PXE, createCompatibleClient, sleep } from '@aztec/aztec.js'; +import { + GovernanceProposerContract, + L1TxUtils, + RegistryContract, + RollupContract, + createEthereumChain, + createL1Clients, + defaultL1TxUtilsConfig, + deployRollupAndUpgradePayload, +} from '@aztec/ethereum'; +import { createLogger } from '@aztec/foundation/log'; +import { GovernanceAbi } from '@aztec/l1-artifacts/GovernanceAbi'; +import { TestERC20Abi as FeeJuiceAbi } from '@aztec/l1-artifacts/TestERC20Abi'; +import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vks'; +import { ProtocolContractAddress, protocolContractTreeRoot } from '@aztec/protocol-contracts'; +import { getGenesisValues } from '@aztec/world-state/testing'; + +import { getContract } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { parseEther, stringify } from 'viem/utils'; + +import { MNEMONIC } from '../fixtures/fixtures.js'; +import { isK8sConfig, setupEnvironment, startPortForward, updateSequencersConfig } from './utils.js'; + +// random private key +const deployerPrivateKey = '0x23206a40226aad90d5673b8adbbcfe94a617e7a6f9e59fc68615fe1bd4bc72f1'; + +const config = setupEnvironment(process.env); +if (!isK8sConfig(config)) { + throw new Error('This test must be run in a k8s environment'); +} + +const debugLogger = createLogger('e2e:spartan-test:upgrade_rollup_version'); + +describe('spartan_upgrade_rollup_version', () => { + let pxe: PXE; + let nodeInfo: NodeInfo; + let ETHEREUM_HOST: string; + beforeAll(async () => { + await startPortForward({ + resource: `svc/${config.INSTANCE_NAME}-aztec-network-pxe`, + namespace: config.NAMESPACE, + containerPort: config.CONTAINER_PXE_PORT, + hostPort: config.HOST_PXE_PORT, + }); + await startPortForward({ + resource: `svc/${config.INSTANCE_NAME}-aztec-network-eth-execution`, + namespace: config.NAMESPACE, + containerPort: config.CONTAINER_ETHEREUM_PORT, + hostPort: config.HOST_ETHEREUM_PORT, + }); + ETHEREUM_HOST = `http://127.0.0.1:${config.HOST_ETHEREUM_PORT}`; + + const PXE_URL = `http://127.0.0.1:${config.HOST_PXE_PORT}`; + pxe = await createCompatibleClient(PXE_URL, debugLogger); + nodeInfo = await pxe.getNodeInfo(); + }); + + // We need a separate account to deploy the new governance proposer + // because the underlying validators are currently producing blob transactions + // and you can't submit blob and non-blob transactions from the same account + const setupDeployerAccount = async () => { + const chain = createEthereumChain(ETHEREUM_HOST, 1337); + const { walletClient: validatorWalletClient } = createL1Clients(ETHEREUM_HOST, MNEMONIC, chain.chainInfo); + // const privateKey = generatePrivateKey(); + const privateKey = deployerPrivateKey; + debugLogger.info(`deployer privateKey: ${privateKey}`); + const account = privateKeyToAccount(privateKey); + // check the balance of the account + const balance = await validatorWalletClient.getBalance({ address: account.address }); + debugLogger.info(`deployer balance: ${balance}`); + if (balance <= parseEther('5')) { + debugLogger.info('sending some eth to the deployer account'); + // send some eth to the account + const tx = await validatorWalletClient.sendTransaction({ + to: account.address, + value: parseEther('10'), + }); + const receipt = await validatorWalletClient.waitForTransactionReceipt({ hash: tx }); + debugLogger.info(`receipt: ${stringify(receipt)}`); + } + return createL1Clients(ETHEREUM_HOST, account, chain.chainInfo); + }; + + it( + 'should upgrade the rollup version', + async () => { + /** Helpers */ + const govInfo = async () => { + const bn = await l1PublicClient.getBlockNumber(); + const slot = await rollup.getSlotNumber(); + const round = await governanceProposer.computeRound(slot); + const info = await governanceProposer.getRoundInfo( + nodeInfo.l1ContractAddresses.rollupAddress.toString(), + round, + ); + const leaderVotes = await governanceProposer.getProposalVotes( + nodeInfo.l1ContractAddresses.rollupAddress.toString(), + round, + info.leader, + ); + return { bn, slot, round, info, leaderVotes }; + }; + + /** Setup */ + + const { walletClient: l1WalletClient, publicClient: l1PublicClient } = await setupDeployerAccount(); + const chain = createEthereumChain(ETHEREUM_HOST, nodeInfo.l1ChainId); + + const { genesisBlockHash, genesisArchiveRoot } = await getGenesisValues([]); + + const { rollup: newRollup, payloadAddress } = await deployRollupAndUpgradePayload( + ETHEREUM_HOST, + chain.chainInfo, + l1WalletClient.account, + { + salt: 2, + vkTreeRoot: getVKTreeRoot(), + protocolContractTreeRoot, + l2FeeJuiceAddress: ProtocolContractAddress.FeeJuice, + genesisArchiveRoot, + genesisBlockHash, + ethereumSlotDuration: 12, + aztecSlotDuration: 36, + aztecEpochDuration: 32, + aztecTargetCommitteeSize: 48, + aztecProofSubmissionWindow: 64, + minimumStake: BigInt(100e18), + slashingQuorum: 6, + slashingRoundSize: 10, + governanceProposerQuorum: 6, + governanceProposerRoundSize: 10, + }, + nodeInfo.l1ContractAddresses.registryAddress, + debugLogger, + defaultL1TxUtilsConfig, + ); + + await updateSequencersConfig(config, { + governanceProposerPayload: payloadAddress, + }); + + const rollup = new RollupContract(l1PublicClient, nodeInfo.l1ContractAddresses.rollupAddress.toString()); + const governanceProposer = new GovernanceProposerContract( + l1PublicClient, + nodeInfo.l1ContractAddresses.governanceProposerAddress.toString(), + ); + + let info = await govInfo(); + expect(info.bn).toBeDefined(); + expect(info.slot).toBeDefined(); + debugLogger.info(`info: ${stringify(info)}`); + + const quorumSize = await governanceProposer.getQuorumSize(); + debugLogger.info(`quorumSize: ${quorumSize}`); + expect(quorumSize).toBeGreaterThan(0); + + /** GovernanceProposer Voting */ + + // Wait until we have enough votes to execute the proposal. + while (true) { + info = await govInfo(); + debugLogger.info(`Leader votes: ${info.leaderVotes}`); + if (info.leaderVotes >= quorumSize) { + debugLogger.info(`Leader votes have reached quorum size`); + break; + } + await sleep(12000); + } + + const executableRound = info.round; + debugLogger.info(`Waiting for round ${executableRound + 1n}`); + + while (info.round === executableRound) { + await sleep(12500); + info = await govInfo(); + debugLogger.info(`slot: ${info.slot}`); + } + + expect(info.round).toBeGreaterThan(executableRound); + + debugLogger.info(`Executing proposal ${info.round}`); + + const l1TxUtils = new L1TxUtils(l1PublicClient, l1WalletClient, debugLogger); + const { receipt } = await governanceProposer.executeProposal(executableRound, l1TxUtils); + expect(receipt).toBeDefined(); + expect(receipt.status).toEqual('success'); + debugLogger.info(`Executed proposal ${info.round}`); + + const addresses = await RegistryContract.collectAddresses( + l1PublicClient, + nodeInfo.l1ContractAddresses.registryAddress, + 'canonical', + ); + + // Set up the primary voter + const token = getContract({ + address: addresses.feeJuiceAddress.toString(), + abi: FeeJuiceAbi, + client: l1PublicClient, + }); + + const governance = getContract({ + address: addresses.governanceAddress.toString(), + abi: GovernanceAbi, + client: l1PublicClient, + }); + + const voteAmount = 10_000n * 10n ** 18n; + + const mintTx = await token.write.mint([l1WalletClient.account.address, voteAmount], { + account: l1WalletClient.account, + }); + await l1PublicClient.waitForTransactionReceipt({ hash: mintTx }); + + const approveTx = await token.write.approve([addresses.governanceAddress.toString(), voteAmount], { + account: l1WalletClient.account, + }); + await l1PublicClient.waitForTransactionReceipt({ hash: approveTx }); + + const depositTx = await governance.write.deposit([l1WalletClient.account.address, voteAmount], { + account: l1WalletClient.account, + }); + await l1PublicClient.waitForTransactionReceipt({ hash: depositTx }); + + // Wait for the proposal to be in the voting phase + let proposalState = await governance.read.getProposalState([0n]); + expect(proposalState).toBeLessThan(2); + debugLogger.info(`Got proposal state`, proposalState); + while (proposalState !== 1) { + await sleep(5000); + debugLogger.info(`Waiting for proposal to be in the voting phase`); + proposalState = await governance.read.getProposalState([0n]); + } + debugLogger.info(`Proposal is in the voting phase`); + // Vote for the proposal + const voteTx = await governance.write.vote([0n, voteAmount, true], { + account: l1WalletClient.account, + }); + await l1PublicClient.waitForTransactionReceipt({ hash: voteTx }); + debugLogger.info(`Voted for the proposal`); + + // Wait for the proposal to be in the executable phase + proposalState = await governance.read.getProposalState([0n]); + while (proposalState !== 3) { + await sleep(5000); + debugLogger.info(`Waiting for proposal to be in the executable phase`); + proposalState = await governance.read.getProposalState([0n]); + } + debugLogger.info(`Proposal is in the executable phase`); + + // Execute the proposal + const executeTx = await governance.write.execute([0n], { + account: l1WalletClient.account, + }); + await l1PublicClient.waitForTransactionReceipt({ hash: executeTx }); + debugLogger.info(`Executed the proposal`); + + const newAddresses = await newRollup.getRollupAddresses(); + + const newCanonicalAddresses = await RegistryContract.collectAddresses( + l1PublicClient, + nodeInfo.l1ContractAddresses.registryAddress, + 'canonical', + ); + + expect(newCanonicalAddresses).toEqual({ + ...nodeInfo.l1ContractAddresses, + ...newAddresses, + }); + + await expect( + RegistryContract.collectAddresses(l1PublicClient, nodeInfo.l1ContractAddresses.registryAddress, 2), + ).resolves.toEqual(newCanonicalAddresses); + + await expect( + RegistryContract.collectAddresses(l1PublicClient, nodeInfo.l1ContractAddresses.registryAddress, 1), + ).resolves.toEqual(nodeInfo.l1ContractAddresses); + }, + 1000 * 60 * 30, + ); +}); diff --git a/yarn-project/end-to-end/src/upgrades/upgrade.test.ts b/yarn-project/end-to-end/src/upgrades/upgrade.test.ts new file mode 100644 index 000000000000..7e0d3b0fb25b --- /dev/null +++ b/yarn-project/end-to-end/src/upgrades/upgrade.test.ts @@ -0,0 +1,197 @@ +import { generateSchnorrAccounts } from '@aztec/accounts/testing'; +import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; +import { type AztecNode, ContractDeployer, type DeployL1Contracts, Fr, type Wallet } from '@aztec/aztec.js'; +import { createBlobSinkClient } from '@aztec/blob-sink/client'; +import { + DefaultL1ContractsConfig, + RegistryContract, + defaultL1TxUtilsConfig, + deployRollupAndUpgradePayload, +} from '@aztec/ethereum'; +import { createGovernanceProposal, executeGovernanceProposal } from '@aztec/ethereum/test'; +import type { Logger } from '@aztec/foundation/log'; +import type { TestDateProvider } from '@aztec/foundation/timer'; +import { StatefulTestContractArtifact } from '@aztec/noir-contracts.js/StatefulTest'; +import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vks'; +import { ProtocolContractAddress, protocolContractTreeRoot } from '@aztec/protocol-contracts'; +import { getGenesisValues } from '@aztec/world-state/testing'; + +import { jest } from '@jest/globals'; +import 'jest-extended'; +import { randomBytes } from 'node:crypto'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { tmpdir } from 'os'; +import { foundry } from 'viem/chains'; + +import { getACVMConfig } from '../fixtures/get_acvm_config.js'; +import { getBBConfig } from '../fixtures/get_bb_config.js'; +import { type SetupOptions, setup } from '../fixtures/utils.js'; + +const originalVersionSalt = 1; + +describe('upgrade', () => { + jest.setTimeout(20 * 60 * 1000); // 20 minutes + + let teardown: () => Promise; + let deployL1ContractsValues: DeployL1Contracts; + let logger: Logger; + let originalNode: AztecNode; + let upgradedNode: AztecNodeService; + afterEach(() => { + jest.restoreAllMocks(); + }); + + let owner: Wallet; + let config: AztecNodeConfig; + let dateProvider: TestDateProvider | undefined; + const artifact = StatefulTestContractArtifact; + let directoryToCleanup: string; + beforeAll(async () => { + ({ + teardown, + wallets: [owner], + aztecNode: originalNode, + config, + dateProvider, + deployL1ContractsValues, + logger, + } = await setup(1, { + archiverPollingIntervalMS: 200, + transactionPollingIntervalMS: 200, + worldStateBlockCheckIntervalMS: 200, + blockCheckIntervalMS: 200, + minTxsPerBlock: 1, + aztecEpochDuration: 4, + aztecProofSubmissionWindow: 8, + aztecSlotDuration: 12, + ethereumSlotDuration: 12, + startProverNode: true, + salt: originalVersionSalt, + })); + }); + + afterAll(async () => { + await teardown(); + await upgradedNode.stop(); + if (directoryToCleanup) { + await fs.rm(directoryToCleanup, { recursive: true, force: true }); + } + }); + + it('upgrades the rollup', async () => { + // Should be able to deploy a contract on the original node + const deployer = new ContractDeployer(artifact, owner); + const ownerAddress = owner.getCompleteAddress().address; + const sender = ownerAddress; + const provenTx = await deployer.deploy(ownerAddress, sender, 1).prove({ + contractAddressSalt: new Fr(BigInt(1)), + skipClassRegistration: true, + skipPublicDeployment: true, + }); + const tx = await provenTx.send().wait({ proven: true }); + expect(tx.blockNumber).toBeDefined(); + + const publicClient = deployL1ContractsValues.publicClient; + const walletClient = deployL1ContractsValues.walletClient; + const account = deployL1ContractsValues.walletClient.account; + const registryAddress = deployL1ContractsValues.l1ContractAddresses.registryAddress; + const rpcUrl = config.l1RpcUrl; + + const addresses = await RegistryContract.collectAddresses(publicClient, registryAddress, 'canonical'); + const newVersionSalt = originalVersionSalt + 1; + + const opts: SetupOptions = { + numberOfInitialFundedAccounts: 1, + }; + + const initialFundedAccounts = await generateSchnorrAccounts(opts.numberOfInitialFundedAccounts!); + const { genesisBlockHash, genesisArchiveRoot, prefilledPublicData } = await getGenesisValues( + initialFundedAccounts.map(a => a.address), + opts.initialAccountFeeJuice, + opts.genesisPublicData, + ); + + const { payloadAddress } = await deployRollupAndUpgradePayload( + rpcUrl, + foundry, + account, + { + ...DefaultL1ContractsConfig, + salt: newVersionSalt, + vkTreeRoot: getVKTreeRoot(), + protocolContractTreeRoot, + l2FeeJuiceAddress: ProtocolContractAddress.FeeJuice, + genesisArchiveRoot, + genesisBlockHash, + }, + registryAddress, + logger, + defaultL1TxUtilsConfig, + ); + + const { governance, voteAmount } = await createGovernanceProposal( + payloadAddress.toString(), + addresses, + account, + publicClient, + logger, + ); + + await executeGovernanceProposal(0n, governance, voteAmount, account, publicClient, walletClient, rpcUrl, logger); + + const newCanonicalAddresses = await RegistryContract.collectAddresses(publicClient, registryAddress, 'canonical'); + + const blobSinkClient = createBlobSinkClient(config); + logger.info('Creating and syncing upgraded node', config); + + directoryToCleanup = path.join(tmpdir(), randomBytes(8).toString('hex')); + await fs.mkdir(directoryToCleanup, { recursive: true }); + + const acvmConfig = await getACVMConfig(logger); + if (!acvmConfig) { + throw new Error('ACVM config not found'); + } + + const bbConfig = await getBBConfig(logger); + if (!bbConfig) { + throw new Error('BB config not found'); + } + + upgradedNode = await AztecNodeService.createAndSync( + { + ...config, + dataDirectory: directoryToCleanup, + version: 2, + l1Contracts: newCanonicalAddresses, + acvmBinaryPath: acvmConfig.acvmBinaryPath, + acvmWorkingDirectory: acvmConfig.acvmWorkingDirectory, + bbBinaryPath: bbConfig.bbBinaryPath, + bbWorkingDirectory: bbConfig.bbWorkingDirectory, + }, + { + dateProvider, + blobSinkClient, + }, + { prefilledPublicData }, + ); + + { + expect(await upgradedNode.isReady()).toBe(true); + expect(await upgradedNode.getVersion()).toBe(2); + const l2Tips = await upgradedNode.getL2Tips(); + expect(l2Tips.latest.number).toBe(0); + await expect(upgradedNode.getArchiveSiblingPath(1, 1n)).rejects.toThrow(/Block 1 not yet synced/); + } + + { + expect(await originalNode.isReady()).toBe(true); + expect(await originalNode.getVersion()).toBe(1); + const originalL2Tips = await originalNode.getL2Tips(); + expect(originalL2Tips.latest.number).toBeGreaterThan(0); + const siblingPath = await originalNode.getArchiveSiblingPath(1, 1n); + expect(siblingPath).toBeDefined(); + expect(siblingPath.toFields()[0]).not.toBe(Fr.ZERO); + } + }); +}); diff --git a/yarn-project/ethereum/src/contracts/index.ts b/yarn-project/ethereum/src/contracts/index.ts index 9745bced5745..422c6b5d37bb 100644 --- a/yarn-project/ethereum/src/contracts/index.ts +++ b/yarn-project/ethereum/src/contracts/index.ts @@ -2,5 +2,6 @@ export * from './empire_base.js'; export * from './forwarder.js'; export * from './governance.js'; export * from './governance_proposer.js'; +export * from './registry.js'; export * from './rollup.js'; export * from './slashing_proposer.js'; 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..93be7894ec19 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/registry.test.ts @@ -0,0 +1,167 @@ +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr } from '@aztec/foundation/fields'; +import { type Logger, createLogger } from '@aztec/foundation/log'; + +import { type Anvil } from '@viem/anvil'; +import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +import { DefaultL1ContractsConfig } from '../config.js'; +import { createL1Clients, deployL1Contracts, deployRollupAndUpgradePayload } from '../deploy_l1_contracts.js'; +import { type L1ContractAddresses } from '../l1_contract_addresses.js'; +import { defaultL1TxUtilsConfig } from '../l1_tx_utils.js'; +import { startAnvil } from '../test/start_anvil.js'; +import { createGovernanceProposal, executeGovernanceProposal } from '../test/upgrade_utils.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: AztecAddress; + 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 = await AztecAddress.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(), + }); + + deployedAddresses = deployed.l1ContractAddresses; + + 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', + ); + + await expect( + RegistryContract.collectAddressesSafe(publicClient, deployedAddresses.registryAddress, 2), + ).resolves.toEqual({ + governanceAddress: deployedAddresses.governanceAddress, + governanceProposerAddress: deployedAddresses.governanceProposerAddress, + registryAddress: deployedAddresses.registryAddress, + }); + }); + + 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 deployRollupAndUpgradePayload( + rpcUrl, + foundry, + privateKey, + { + ...DefaultL1ContractsConfig, + salt: newVersionSalt, + vkTreeRoot, + protocolContractTreeRoot, + l2FeeJuiceAddress, + genesisArchiveRoot: Fr.random(), + genesisBlockHash: Fr.random(), + }, + 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); + }); +}); diff --git a/yarn-project/ethereum/src/contracts/registry.ts b/yarn-project/ethereum/src/contracts/registry.ts new file mode 100644 index 000000000000..11d1cb689cf4 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/registry.ts @@ -0,0 +1,132 @@ +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 collectAddressesSafe( + 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) { + return { + registryAddress: registry.address, + ...governanceAddresses, + }; + } else { + 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), + }; + } + } + + 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'); + } else { + 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 638c2fd77448..4419dfbc1582 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -32,6 +32,7 @@ export type L1RollupContractAddresses = Pick< | 'feeJuiceAddress' | 'stakingAssetAddress' | 'rewardDistributorAddress' + | 'slashFactoryAddress' >; export class RollupContract { @@ -62,7 +63,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 }); } @@ -184,6 +188,7 @@ export class RollupContract { rewardDistributorAddress, feeJuiceAddress, stakingAssetAddress, + slashFactoryAddress, ] = ( await Promise.all([ this.rollup.read.INBOX(), @@ -192,6 +197,7 @@ export class RollupContract { this.rollup.read.REWARD_DISTRIBUTOR(), this.rollup.read.ASSET(), this.rollup.read.getStakingAsset(), + this.rollup.read.getSlashFactory(), ] as const) ).map(EthAddress.fromString); @@ -203,6 +209,7 @@ export class RollupContract { feeJuiceAddress, stakingAssetAddress, rewardDistributorAddress, + slashFactoryAddress, }; } diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 90193bd045de..36388363a3ab 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -19,6 +19,8 @@ import { InboxBytecode, OutboxAbi, OutboxBytecode, + RegisterNewRollupVersionPayloadAbi, + RegisterNewRollupVersionPayloadBytecode, RegistryAbi, RegistryBytecode, RewardDistributorAbi, @@ -61,6 +63,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'; @@ -74,11 +78,11 @@ export type DeployL1Contracts = { /** * Wallet Client Type. */ - walletClient: WalletClient; + walletClient: L1Clients['walletClient']; /** * Public Client Type. */ - publicClient: PublicClient; + publicClient: L1Clients['publicClient']; /** * The currently deployed l1 contract addresses */ @@ -183,6 +187,10 @@ export const l1Artifacts = { contractAbi: SlashFactoryAbi, contractBytecode: SlashFactoryBytecode as Hex, }, + registerNewRollupVersionPayload: { + contractAbi: RegisterNewRollupVersionPayloadAbi, + contractBytecode: RegisterNewRollupVersionPayloadBytecode as Hex, + }, }; export interface DeployL1ContractsArgs extends L1ContractsConfig { @@ -246,6 +254,167 @@ export function createL1Clients( return { walletClient, publicClient } as L1Clients; } +export const deployRollupAndUpgradePayload = async ( + rpcUrl: string, + chain: Chain, + account: HDAccount | PrivateKeyAccount, + args: DeployL1ContractsArgs, + registryAddress: EthAddress, + logger: Logger, + txUtilsConfig: L1TxUtilsConfig, +) => { + const { publicClient } = createL1Clients(rpcUrl, account, chain); + const addresses = await RegistryContract.collectAddresses(publicClient, registryAddress, 'canonical'); + + const rollup = await deployRollup(rpcUrl, chain, account, args, addresses, logger, txUtilsConfig); + const payloadAddress = await deployUpgradePayload( + rpcUrl, + chain, + account, + args, + { + ...addresses, + rollupAddress: EthAddress.fromString(rollup.address), + }, + logger, + txUtilsConfig, + ); + + return { rollup, payloadAddress }; +}; + +export const deployUpgradePayload = async ( + rpcUrl: string, + chain: Chain, + account: HDAccount | PrivateKeyAccount, + args: DeployL1ContractsArgs, + addresses: Pick, + logger: Logger, + txUtilsConfig: L1TxUtilsConfig, +) => { + const { walletClient, publicClient } = createL1Clients(rpcUrl, account, chain); + const deployer = new L1Deployer(walletClient, publicClient, args.salt, logger, txUtilsConfig); + + const payloadAddress = await deployer.deploy(l1Artifacts.registerNewRollupVersionPayload, [ + addresses.registryAddress.toString(), + addresses.rollupAddress.toString(), + ]); + + return payloadAddress; +}; + +export const deployRollup = async ( + rpcUrl: string, + chain: Chain, + account: HDAccount | PrivateKeyAccount, + args: DeployL1ContractsArgs, + addresses: Pick, + logger: Logger, + txUtilsConfig: L1TxUtilsConfig, +): Promise => { + const { walletClient, publicClient } = createL1Clients(rpcUrl, account, chain); + const deployer = new L1Deployer(walletClient, publicClient, args.salt, logger, txUtilsConfig); + + 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(), + account.address.toString(), + rollupConfigArgs, + ]; + + 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}`); + + 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[] = []; + + // Set initial blocks as proven if requested + if (args.assumeProvenThrough && args.assumeProvenThrough > 0) { + await rollup.write.setAssumeProvenThroughBlockNumber([BigInt(args.assumeProvenThrough)], { account }); + logger.warn(`Rollup set to assumedProvenUntil to ${args.assumeProvenThrough}`); + } + + 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, + }); + } + } + + { + const txHash = await rollup.write.setSlashFactory([slashFactoryAddress.toString()], { account }); + logger.verbose(`Rollup set slash factory in ${txHash}`); + txHashes.push(txHash); + } + + 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. @@ -285,8 +454,7 @@ 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); @@ -336,50 +504,16 @@ export const deployL1Contracts = async ( ]); 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, [ + const feeJuicePortalAddress = await govDeployer.deploy(l1Artifacts.feeJuicePortal, [ registryAddress.toString(), feeAssetAddress.toString(), args.l2FeeJuiceAddress.toString(), ]); 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}`); - - await deployer.waitForDeployments(); - logger.verbose(`All core contracts have been deployed`); + logger.verbose(`Waiting for governance contracts to be deployed`); + await govDeployer.waitForDeployments(); + logger.verbose(`All governance contracts deployed`); const feeJuicePortal = getContract({ address: feeJuicePortalAddress.toString(), @@ -392,65 +526,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 @@ -477,41 +565,15 @@ 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}`); - } - } - - // Set initial blocks as proven if requested - if (args.assumeProvenThrough && args.assumeProvenThrough > 0) { - await rollup.write.setAssumeProvenThroughBlockNumber([BigInt(args.assumeProvenThrough)], { account }); - logger.warn(`Rollup set to assumedProvenUntil to ${args.assumeProvenThrough}`); - } - - // 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.INBOX()) as any); - logger.verbose(`Inbox available at ${inboxAddress}`); - - const outboxAddress = EthAddress.fromString((await rollup.read.OUTBOX()) as any); - logger.verbose(`Outbox available at ${outboxAddress}`); + const rollup = await deployRollup( + rpcUrl, + chain, + account, + args, + { feeJuicePortalAddress, rewardDistributorAddress, stakingAssetAddress }, + logger, + txUtilsConfig, + ); // We need to call a function on the registry to set the various contract addresses. const registryContract = getContract({ @@ -519,14 +581,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 @@ -546,24 +609,33 @@ 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, diff --git a/yarn-project/ethereum/src/queries.ts b/yarn-project/ethereum/src/queries.ts index f65849aff370..94a091827398 100644 --- a/yarn-project/ethereum/src/queries.ts +++ b/yarn-project/ethereum/src/queries.ts @@ -4,6 +4,7 @@ import { type Chain, type HttpTransport, type PublicClient } from 'viem'; import { type L1ContractsConfig } from './config.js'; import { GovernanceContract } from './contracts/governance.js'; +import { RegistryContract } from './contracts/registry.js'; import { RollupContract } from './contracts/rollup.js'; import { type L1ContractAddresses } from './l1_contract_addresses.js'; @@ -11,17 +12,11 @@ import { type L1ContractAddresses } from './l1_contract_addresses.js'; export async function getL1ContractsAddresses( publicClient: PublicClient, governanceAddress: EthAddress, -): Promise> { +): Promise { const governance = new GovernanceContract(publicClient, governanceAddress.toString()); - const governanceAddresses = await governance.getGovernanceAddresses(); - - const rollup = new RollupContract(publicClient, governanceAddresses.rollupAddress.toString()); - const rollupAddresses = await rollup.getRollupAddresses(); - - return { - ...governanceAddresses, - ...rollupAddresses, - }; + const proposer = await governance.getProposer(); + const registry = await proposer.getRegistryAddress(); + return RegistryContract.collectAddresses(publicClient, registry, 'canonical'); } /** Reads the L1ContractsConfig from L1 contracts. */ diff --git a/yarn-project/ethereum/src/test/index.ts b/yarn-project/ethereum/src/test/index.ts index 13734d1e6da3..345924d3695c 100644 --- a/yarn-project/ethereum/src/test/index.ts +++ b/yarn-project/ethereum/src/test/index.ts @@ -2,3 +2,4 @@ export * from './delayed_tx_utils.js'; export * from './eth_cheat_codes_with_state.js'; export * from './start_anvil.js'; export * from './tx_delayer.js'; +export * from './upgrade_utils.js'; diff --git a/yarn-project/ethereum/src/test/upgrade_utils.ts b/yarn-project/ethereum/src/test/upgrade_utils.ts new file mode 100644 index 000000000000..f8db6f5be7c6 --- /dev/null +++ b/yarn-project/ethereum/src/test/upgrade_utils.ts @@ -0,0 +1,100 @@ +import type { Logger } from '@aztec/foundation/log'; +import { TestERC20Abi as FeeJuiceAbi } from '@aztec/l1-artifacts'; +import { GovernanceAbi } from '@aztec/l1-artifacts/GovernanceAbi'; + +import { type GetContractReturnType, type PrivateKeyAccount, getContract } from 'viem'; + +import { EthCheatCodes } from '../eth_cheat_codes.js'; +import type { L1ContractAddresses } from '../l1_contract_addresses.js'; +import type { L1Clients } from '../types.js'; + +export async function executeGovernanceProposal( + proposalId: bigint, + governance: GetContractReturnType, + 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`); +} + +export async function createGovernanceProposal( + payloadAddress: `0x${string}`, + addresses: L1ContractAddresses, + privateKey: PrivateKeyAccount, + publicClient: L1Clients['publicClient'], + logger: Logger, +): Promise<{ governance: GetContractReturnType; voteAmount: bigint }> { + 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/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index ae44c3ad20da..be1245a92859 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"