From 51d66991161ffdf6f04b87b600a213d3cf0a662f Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 29 Aug 2024 09:58:01 -0300 Subject: [PATCH] feat: Faster L1 deployment (#8234) Do not wait for each tx to be mined before progressing into the next one for L1 deployment. Only enabled for when deployment salt is set. Makes the life of the developer easier after #8211. --- .../aztec/src/cli/aztec_start_options.ts | 10 +- yarn-project/aztec/src/cli/cmds/start_node.ts | 3 +- yarn-project/aztec/src/sandbox.ts | 4 +- .../cli/src/cmds/devnet/bootstrap_network.ts | 14 +- .../cli/src/cmds/l1/deploy_l1_verifier.ts | 9 +- .../integration_proof_verification.test.ts | 2 +- .../src/e2e_prover/e2e_prover_test.ts | 2 +- .../src/shared/cross_chain_test_harness.ts | 18 ++- .../end-to-end/src/shared/uniswap_l1_l2.ts | 2 +- .../ethereum/src/deploy_l1_contracts.ts | 137 +++++++++--------- yarn-project/foundation/src/config/env_var.ts | 1 + 11 files changed, 122 insertions(+), 80 deletions(-) diff --git a/yarn-project/aztec/src/cli/aztec_start_options.ts b/yarn-project/aztec/src/cli/aztec_start_options.ts index bdd9229bb15..1a405bb5475 100644 --- a/yarn-project/aztec/src/cli/aztec_start_options.ts +++ b/yarn-project/aztec/src/cli/aztec_start_options.ts @@ -172,10 +172,18 @@ export const aztecStartOptions: { [key: string]: AztecStartOption[] } = { }, { flag: '--node.deployAztecContracts', - description: 'Deploys L1 Aztec contracts before starting the node. Needs mnemonic or private key to be set', + description: 'Deploys L1 Aztec contracts before starting the node. Needs mnemonic or private key to be set.', envVar: 'DEPLOY_AZTEC_CONTRACTS', ...booleanConfigHelper(), }, + { + flag: '--node.deployAztecContractsSalt', + description: + 'Numeric salt for deploying L1 Aztec contracts before starting the node. Needs mnemonic or private key to be set. Implies --node.deployAztecContracts.', + envVar: 'DEPLOY_AZTEC_CONTRACTS_SALT', + defaultValue: undefined, + parseVal: (val: string) => (val ? parseInt(val) : undefined), + }, { flag: '--node.assumeProvenUntilBlockNumber', description: diff --git a/yarn-project/aztec/src/cli/cmds/start_node.ts b/yarn-project/aztec/src/cli/cmds/start_node.ts index c6f9814bc07..85f0f14fa54 100644 --- a/yarn-project/aztec/src/cli/cmds/start_node.ts +++ b/yarn-project/aztec/src/cli/cmds/start_node.ts @@ -37,7 +37,7 @@ export const startNode = async ( } // Deploy contracts if needed - if (nodeSpecificOptions.deployAztecContracts) { + if (nodeSpecificOptions.deployAztecContracts || nodeSpecificOptions.deployAztecContractsSalt) { let account; if (nodeSpecificOptions.publisherPrivateKey) { account = privateKeyToAccount(nodeSpecificOptions.publisherPrivateKey); @@ -48,6 +48,7 @@ export const startNode = async ( } await deployContractsToL1(nodeConfig, account!, undefined, { assumeProvenUntilBlockNumber: nodeSpecificOptions.assumeProvenUntilBlockNumber, + salt: nodeSpecificOptions.deployAztecContractsSalt, }); } diff --git a/yarn-project/aztec/src/sandbox.ts b/yarn-project/aztec/src/sandbox.ts index c50bae06142..8ba2f06256a 100644 --- a/yarn-project/aztec/src/sandbox.ts +++ b/yarn-project/aztec/src/sandbox.ts @@ -90,7 +90,7 @@ export async function deployContractsToL1( aztecNodeConfig: AztecNodeConfig, hdAccount: HDAccount | PrivateKeyAccount, contractDeployLogger = logger, - opts: { assumeProvenUntilBlockNumber?: number } = {}, + opts: { assumeProvenUntilBlockNumber?: number; salt?: number } = {}, ) { const l1Artifacts: L1ContractArtifactsForDeployment = { registry: { @@ -132,7 +132,7 @@ export async function deployContractsToL1( l2FeeJuiceAddress: FeeJuiceAddress, vkTreeRoot: getVKTreeRoot(), assumeProvenUntil: opts.assumeProvenUntilBlockNumber, - salt: undefined, + salt: opts.salt, }), ); diff --git a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts index 217b844cf4d..fe37e56979a 100644 --- a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts +++ b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts @@ -113,8 +113,18 @@ async function deployERC20({ walletClient, publicClient }: L1Clients) { contractBytecode: TokenPortalBytecode, }; - const erc20Address = await deployL1Contract(walletClient, publicClient, erc20.contractAbi, erc20.contractBytecode); - const portalAddress = await deployL1Contract(walletClient, publicClient, portal.contractAbi, portal.contractBytecode); + const { address: erc20Address } = await deployL1Contract( + walletClient, + publicClient, + erc20.contractAbi, + erc20.contractBytecode, + ); + const { address: portalAddress } = await deployL1Contract( + walletClient, + publicClient, + portal.contractAbi, + portal.contractBytecode, + ); return { erc20Address, diff --git a/yarn-project/cli/src/cmds/l1/deploy_l1_verifier.ts b/yarn-project/cli/src/cmds/l1/deploy_l1_verifier.ts index 783f5e5c4f0..970018f9896 100644 --- a/yarn-project/cli/src/cmds/l1/deploy_l1_verifier.ts +++ b/yarn-project/cli/src/cmds/l1/deploy_l1_verifier.ts @@ -66,7 +66,7 @@ export async function deployUltraHonkVerifier( createEthereumChain(ethRpcUrl, l1ChainId).chainInfo, ); - const verifierAddress = await deployL1Contract(walletClient, publicClient, abi, `0x${bytecode}`); + const { address: verifierAddress } = await deployL1Contract(walletClient, publicClient, abi, `0x${bytecode}`); log(`Deployed HonkVerifier at ${verifierAddress.toString()}`); const pxe = await createCompatibleClient(pxeRpcUrl, debugLogger); @@ -100,7 +100,12 @@ export async function deployMockVerifier( ); const { MockVerifierAbi, MockVerifierBytecode, RollupAbi } = await import('@aztec/l1-artifacts'); - const mockVerifierAddress = await deployL1Contract(walletClient, publicClient, MockVerifierAbi, MockVerifierBytecode); + const { address: mockVerifierAddress } = await deployL1Contract( + walletClient, + publicClient, + MockVerifierAbi, + MockVerifierBytecode, + ); log(`Deployed MockVerifier at ${mockVerifierAddress.toString()}`); const pxe = await createCompatibleClient(pxeRpcUrl, debugLogger); diff --git a/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts b/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts index 48c7160d5cf..f77e6069050 100644 --- a/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts @@ -98,7 +98,7 @@ describe('proof_verification', () => { const abi = output.contracts['UltraHonkVerifier.sol']['HonkVerifier'].abi; const bytecode: string = output.contracts['UltraHonkVerifier.sol']['HonkVerifier'].evm.bytecode.object; - const verifierAddress = await deployL1Contract(walletClient, publicClient, abi, `0x${bytecode}`); + const { address: verifierAddress } = await deployL1Contract(walletClient, publicClient, abi, `0x${bytecode}`); verifierContract = getContract({ address: verifierAddress.toString(), client: publicClient, diff --git a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts index f3631a0eca7..b75763aa6dc 100644 --- a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts @@ -383,7 +383,7 @@ export class FullProverTest { const abi = output.contracts['UltraHonkVerifier.sol']['HonkVerifier'].abi; const bytecode: string = output.contracts['UltraHonkVerifier.sol']['HonkVerifier'].evm.bytecode.object; - const verifierAddress = await deployL1Contract(walletClient, publicClient, abi, `0x${bytecode}`); + const { address: verifierAddress } = await deployL1Contract(walletClient, publicClient, abi, `0x${bytecode}`); this.logger.info(`Deployed Real verifier at ${verifierAddress}`); diff --git a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts index e96a98893de..64ce7d56196 100644 --- a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts @@ -82,16 +82,26 @@ export async function deployAndInitializeTokenAndBridgeContracts( underlyingERC20: any; }> { if (!underlyingERC20Address) { - underlyingERC20Address = await deployL1Contract(walletClient, publicClient, PortalERC20Abi, PortalERC20Bytecode); + underlyingERC20Address = await deployL1Contract( + walletClient, + publicClient, + PortalERC20Abi, + PortalERC20Bytecode, + ).then(({ address }) => address); } const underlyingERC20 = getContract({ - address: underlyingERC20Address.toString(), + address: underlyingERC20Address!.toString(), abi: PortalERC20Abi, client: walletClient, }); // deploy the token portal - const tokenPortalAddress = await deployL1Contract(walletClient, publicClient, TokenPortalAbi, TokenPortalBytecode); + const { address: tokenPortalAddress } = await deployL1Contract( + walletClient, + publicClient, + TokenPortalAbi, + TokenPortalBytecode, + ); const tokenPortal = getContract({ address: tokenPortalAddress.toString(), abi: TokenPortalAbi, @@ -120,7 +130,7 @@ export async function deployAndInitializeTokenAndBridgeContracts( // initialize portal await tokenPortal.write.initialize( - [rollupRegistryAddress.toString(), underlyingERC20Address.toString(), bridge.address.toString()], + [rollupRegistryAddress.toString(), underlyingERC20Address!.toString(), bridge.address.toString()], {} as any, ); diff --git a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts index 8fe97ad2065..ac626458c4e 100644 --- a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts +++ b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts @@ -148,7 +148,7 @@ export const uniswapL1L2TestSuite = ( publicClient, UniswapPortalAbi, UniswapPortalBytecode, - ); + ).then(({ address }) => address); uniswapPortal = getContract({ address: uniswapPortalAddress.toString(), diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 92bd78e4a8d..f09c1963d70 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -196,36 +196,59 @@ export const deployL1Contracts = async ( logger.info(`Deployed Gas Portal at ${feeJuicePortalAddress}`); + const rollupAddress = await deployer.deploy(contractsToDeploy.rollup, [ + getAddress(registryAddress.toString()), + getAddress(availabilityOracleAddress.toString()), + getAddress(feeJuicePortalAddress.toString()), + args.vkTreeRoot.toString(), + account.address.toString(), + args.initialValidators?.map(v => v.toString()) ?? [], + ]); + logger.info(`Deployed Rollup at ${rollupAddress}`); + + await deployer.waitForDeployments(); + logger.info(`All contracts deployed`); + const feeJuicePortal = getContract({ address: feeJuicePortalAddress.toString(), abi: contractsToDeploy.feeJuicePortal.contractAbi, client: walletClient, }); - // fund the portal contract with Fee Juice const feeJuice = getContract({ address: feeJuiceAddress.toString(), abi: contractsToDeploy.feeJuice.contractAbi, client: walletClient, }); + const rollup = getContract({ + address: getAddress(rollupAddress.toString()), + abi: contractsToDeploy.rollup.contractAbi, + client: walletClient, + }); + + // Transaction hashes to await + const txHashes: Hex[] = []; + // @note This value MUST match what is in `constants.nr`. It is currently specified here instead of just importing // because there is circular dependency hell. This is a temporary solution. #3342 // @todo #8084 + // fund the portal contract with Fee Juice const FEE_JUICE_INITIAL_MINT = 20000000000; - const receipt = await feeJuice.write.mint([feeJuicePortalAddress.toString(), FEE_JUICE_INITIAL_MINT], {} as any); - await publicClient.waitForTransactionReceipt({ hash: receipt }); - logger.info(`Funded fee juice portal contract with Fee Juice`); + const mintTxHash = await feeJuice.write.mint([feeJuicePortalAddress.toString(), FEE_JUICE_INITIAL_MINT], {} as any); + txHashes.push(mintTxHash); + logger.info(`Funding fee juice portal contract with fee juice in ${mintTxHash}`); if ((await feeJuicePortal.read.registry([])) === zeroAddress) { - await publicClient.waitForTransactionReceipt({ - hash: await feeJuicePortal.write.initialize([ - registryAddress.toString(), - feeJuiceAddress.toString(), - args.l2FeeJuiceAddress.toString(), - ]), - }); - logger.verbose(`Fee juice portal initialized with registry ${registryAddress.toString()}`); + const initPortalTxHash = await feeJuicePortal.write.initialize([ + registryAddress.toString(), + feeJuiceAddress.toString(), + args.l2FeeJuiceAddress.toString(), + ]); + txHashes.push(initPortalTxHash); + logger.verbose( + `Fee juice portal initializing with registry ${registryAddress.toString()} in tx ${initPortalTxHash}`, + ); } else { logger.verbose(`Fee juice portal is already initialized`); } @@ -234,22 +257,6 @@ export const deployL1Contracts = async ( `Initialized Gas Portal at ${feeJuicePortalAddress} to bridge between L1 ${feeJuiceAddress} to L2 ${args.l2FeeJuiceAddress}`, ); - const rollupAddress = await deployer.deploy(contractsToDeploy.rollup, [ - getAddress(registryAddress.toString()), - getAddress(availabilityOracleAddress.toString()), - getAddress(feeJuicePortalAddress.toString()), - args.vkTreeRoot.toString(), - account.address.toString(), - args.initialValidators?.map(v => v.toString()) ?? [], - ]); - logger.info(`Deployed Rollup at ${rollupAddress}`); - - const rollup = getContract({ - address: getAddress(rollupAddress.toString()), - abi: contractsToDeploy.rollup.contractAbi, - client: walletClient, - }); - if (chain.id == foundry.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. @@ -280,26 +287,10 @@ export const deployL1Contracts = async ( } // Inbox and Outbox are immutable and are deployed from Rollup's constructor so we just fetch them from the contract. - let inboxAddress!: EthAddress; - { - const rollup = getContract({ - address: getAddress(rollupAddress.toString()), - abi: contractsToDeploy.rollup.contractAbi, - client: publicClient, - }); - inboxAddress = EthAddress.fromString((await rollup.read.INBOX([])) as any); - } + const inboxAddress = EthAddress.fromString((await rollup.read.INBOX([])) as any); logger.info(`Inbox available at ${inboxAddress}`); - let outboxAddress!: EthAddress; - { - const rollup = getContract({ - address: getAddress(rollupAddress.toString()), - abi: contractsToDeploy.rollup.contractAbi, - client: publicClient, - }); - outboxAddress = EthAddress.fromString((await rollup.read.OUTBOX([])) as any); - } + const outboxAddress = EthAddress.fromString((await rollup.read.OUTBOX([])) as any); logger.info(`Outbox available at ${outboxAddress}`); // We need to call a function on the registry to set the various contract addresses. @@ -309,12 +300,19 @@ export const deployL1Contracts = async ( client: walletClient, }); if (!(await registryContract.read.isRollupRegistered([getAddress(rollupAddress.toString())]))) { - await registryContract.write.upgrade([getAddress(rollupAddress.toString())], { account }); - logger.verbose(`Upgraded registry contract at ${registryAddress} to rollup ${rollupAddress}`); + const upgradeTxHash = await registryContract.write.upgrade([getAddress(rollupAddress.toString())], { account }); + logger.verbose( + `Upgrading registry contract at ${registryAddress} to rollup ${rollupAddress} in tx ${upgradeTxHash}`, + ); + txHashes.push(upgradeTxHash); } else { logger.verbose(`Registry ${registryAddress} has already registered rollup ${rollupAddress}`); } + // 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 = { availabilityOracleAddress, rollupAddress, @@ -334,6 +332,7 @@ export const deployL1Contracts = async ( class L1Deployer { private salt: Hex | undefined; + private txHashes: Hex[] = []; constructor( private walletClient: WalletClient, private publicClient: PublicClient, @@ -343,11 +342,11 @@ class L1Deployer { this.salt = maybeSalt ? padHex(numberToHex(maybeSalt), { size: 32 }) : undefined; } - deploy( + async deploy( params: { contractAbi: Narrow; contractBytecode: Hex }, args: readonly unknown[] = [], ): Promise { - return deployL1Contract( + const { txHash, address } = await deployL1Contract( this.walletClient, this.publicClient, params.contractAbi, @@ -356,6 +355,14 @@ class L1Deployer { this.salt, this.logger, ); + if (txHash) { + this.txHashes.push(txHash); + } + return address; + } + + async waitForDeployments(): Promise { + await Promise.all(this.txHashes.map(txHash => this.publicClient.waitForTransactionReceipt({ hash: txHash }))); } } @@ -367,6 +374,7 @@ class L1Deployer { * @param abi - The ETH contract's ABI (as abitype's Abi). * @param bytecode - The ETH contract's bytecode. * @param args - Constructor arguments for the contract. + * @param maybeSalt - Optional salt for CREATE2 deployment (does not wait for deployment tx to be mined if set, does not send tx if contract already exists). * @returns The ETH address the contract was deployed to. */ export async function deployL1Contract( @@ -377,38 +385,37 @@ export async function deployL1Contract( args: readonly unknown[] = [], maybeSalt?: Hex, logger?: DebugLogger, -): Promise { +): Promise<{ address: EthAddress; txHash: Hex | undefined }> { + let txHash: Hex | undefined = undefined; + let address: Hex | null | undefined = undefined; + if (maybeSalt) { const salt = padHex(maybeSalt, { size: 32 }); const deployer: Hex = '0x4e59b44847b379578588920cA78FbF26c0B4956C'; const calldata = encodeDeployData({ abi, bytecode, args }); - const address = getContractAddress({ from: deployer, salt, bytecode: calldata, opcode: 'CREATE2' }); + address = getContractAddress({ from: deployer, salt, bytecode: calldata, opcode: 'CREATE2' }); const existing = await publicClient.getBytecode({ address }); if (existing === undefined || existing === '0x') { - const hash = await walletClient.sendTransaction({ - to: deployer, - data: concatHex([salt, calldata]), - }); - logger?.verbose(`Deploying contract with salt ${salt} to address ${address} in tx ${hash}`); - await publicClient.waitForTransactionReceipt({ hash, pollingInterval: 100 }); + txHash = await walletClient.sendTransaction({ to: deployer, data: concatHex([salt, calldata]) }); + logger?.verbose(`Deploying contract with salt ${salt} to address ${address} in tx ${txHash}`); } else { logger?.verbose(`Skipping existing deployment of contract with salt ${salt} to address ${address}`); } - return EthAddress.fromString(address); } else { - const hash = await walletClient.deployContract({ abi, bytecode, args }); - logger?.verbose(`Deploying contract in tx ${hash}`); - const receipt = await publicClient.waitForTransactionReceipt({ hash, pollingInterval: 100 }); - const contractAddress = receipt.contractAddress; - if (!contractAddress) { + txHash = await walletClient.deployContract({ abi, bytecode, args }); + logger?.verbose(`Deploying contract in tx ${txHash}`); + const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, pollingInterval: 100 }); + address = receipt.contractAddress; + if (!address) { throw new Error( `No contract address found in receipt: ${JSON.stringify(receipt, (_, val) => typeof val === 'bigint' ? String(val) : val, )}`, ); } - return EthAddress.fromString(contractAddress); } + + return { address: EthAddress.fromString(address!), txHash }; } // docs:end:deployL1Contract diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 7f034f51296..2d138655b13 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -16,6 +16,7 @@ export type EnvVar = | 'FEE_JUICE_PORTAL_CONTRACT_ADDRESS' | 'ARCHIVER_URL' | 'DEPLOY_AZTEC_CONTRACTS' + | 'DEPLOY_AZTEC_CONTRACTS_SALT' | 'L1_PRIVATE_KEY' | 'L2_QUEUE_SIZE' | 'WS_BLOCK_CHECK_INTERVAL_MS'