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 8030b0dcfa4..e553b5273a2 100644 --- a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts +++ b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts @@ -112,8 +112,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 360252453fe..551c7b5902e 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, - }); - // @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 { @@ -278,26 +285,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. @@ -307,12 +298,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, @@ -332,6 +330,7 @@ export const deployL1Contracts = async ( class L1Deployer { private salt: Hex | undefined; + private txHashes: Hex[] = []; constructor( private walletClient: WalletClient, private publicClient: PublicClient, @@ -341,11 +340,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, @@ -354,6 +353,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 }))); } } @@ -365,6 +372,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( @@ -375,38 +383,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'