diff --git a/.circleci/config.yml b/.circleci/config.yml index 5558f93fdbd..5bf7fd05c8b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -440,6 +440,17 @@ jobs: name: "Test" command: cond_spot_run_tests end-to-end e2e_2_rpc_servers.test.ts + e2e-multiple-accounts-1-enc-key: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_tests end-to-end e2e_multiple_accounts_1_enc_key.test.ts + e2e-deploy-contract: docker: - image: aztecprotocol/alpine-build-image @@ -843,6 +854,7 @@ workflows: - e2e-block-building: *e2e_test - e2e-nested-contract: *e2e_test - e2e-non-contract-account: *e2e_test + - e2e-multiple-accounts-1-enc-key: *e2e_test - e2e-public-token-contract: *e2e_test - e2e-cross-chain-messaging: *e2e_test - e2e-public-cross-chain-messaging: *e2e_test @@ -863,6 +875,7 @@ workflows: - e2e-block-building - e2e-nested-contract - e2e-non-contract-account + - e2e-multiple-accounts-1-enc-key - e2e-public-token-contract - e2e-cross-chain-messaging - e2e-public-cross-chain-messaging diff --git a/yarn-project/end-to-end/src/e2e_2_rpc_servers.test.ts b/yarn-project/end-to-end/src/e2e_2_rpc_servers.test.ts index 695fb770be4..195a346fce3 100644 --- a/yarn-project/end-to-end/src/e2e_2_rpc_servers.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_rpc_servers.test.ts @@ -75,7 +75,7 @@ describe('e2e_2_rpc_servers', () => { const isUserSynchronised = async () => { return await wallet.isAccountSynchronised(owner); }; - await retryUntil(isUserSynchronised, owner.toString(), 5); + await retryUntil(isUserSynchronised, owner.toString(), 10); // Then check the balance const contractWithWallet = new ZkTokenContract(contractWithWalletA.address, wallet); diff --git a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts index d43fa1ae4fa..058cfcd6a1a 100644 --- a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts +++ b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts @@ -1,14 +1,6 @@ import { AztecRPCServer } from '@aztec/aztec-rpc'; -import { - AccountImplementation, - AccountWallet, - ContractDeployer, - Fr, - SingleKeyAccountContract, - StoredKeyAccountContract, - generatePublicKey, -} from '@aztec/aztec.js'; -import { AztecAddress, PartialContractAddress, Point, getContractDeploymentInfo } from '@aztec/circuits.js'; +import { AccountWallet, Fr, SingleKeyAccountContract, StoredKeyAccountContract } from '@aztec/aztec.js'; +import { AztecAddress, PartialContractAddress, Point } from '@aztec/circuits.js'; import { Ecdsa, Schnorr } from '@aztec/circuits.js/barretenberg'; import { ContractAbi } from '@aztec/foundation/abi'; import { toBigInt } from '@aztec/foundation/serialize'; @@ -18,52 +10,10 @@ import { SchnorrSingleKeyAccountContractAbi, } from '@aztec/noir-contracts/artifacts'; import { ChildContract } from '@aztec/noir-contracts/types'; -import { AztecRPC, PublicKey } from '@aztec/types'; import { randomBytes } from 'crypto'; -import { setup } from './utils.js'; - -async function deployContract( - aztecRpcServer: AztecRPC, - publicKey: PublicKey, - abi: ContractAbi, - args: any[], - contractAddressSalt?: Fr, -) { - const deployer = new ContractDeployer(abi, aztecRpcServer, publicKey); - const deployMethod = deployer.deploy(...args); - await deployMethod.create({ contractAddressSalt }); - const tx = deployMethod.send(); - expect(await tx.isMined(0, 0.1)).toBeTruthy(); - const receipt = await tx.getReceipt(); - return { address: receipt.contractAddress!, partialContractAddress: deployMethod.partialContractAddress! }; -} - -async function createNewAccount( - aztecRpcServer: AztecRPC, - abi: ContractAbi, - args: any[], - encryptionPrivateKey: Buffer, - useProperKey: boolean, - createAccountImpl: CreateAccountImplFn, -) { - const salt = Fr.random(); - const publicKey = await generatePublicKey(encryptionPrivateKey); - const { address, partialAddress } = await getContractDeploymentInfo(abi, args, salt, publicKey); - await aztecRpcServer.addAccount(encryptionPrivateKey, address, partialAddress); - await deployContract(aztecRpcServer, publicKey, abi, args, salt); - const account = await createAccountImpl(address, useProperKey, partialAddress, encryptionPrivateKey); - const wallet = new AccountWallet(aztecRpcServer, account); - return { wallet, address, partialAddress }; -} - -type CreateAccountImplFn = ( - address: AztecAddress, - useProperKey: boolean, - partialAddress: PartialContractAddress, - encryptionPrivateKey: Buffer, -) => Promise; +import { CreateAccountImplFn, createNewAccount, deployContract, setup } from './utils.js'; function itShouldBehaveLikeAnAccountContract( abi: ContractAbi, diff --git a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts new file mode 100644 index 00000000000..14717277e0d --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts @@ -0,0 +1,154 @@ +import { AztecNodeService } from '@aztec/aztec-node'; +import { AztecRPCServer, Fr } from '@aztec/aztec-rpc'; +import { AztecAddress, StoredKeyAccountContract, Wallet, generatePublicKey } from '@aztec/aztec.js'; +import { Schnorr } from '@aztec/circuits.js/barretenberg'; +import { DebugLogger } from '@aztec/foundation/log'; +import { SchnorrMultiKeyAccountContractAbi } from '@aztec/noir-contracts/artifacts'; +import { ZkTokenContract } from '@aztec/noir-contracts/types'; +import { AztecRPC, TxStatus } from '@aztec/types'; + +import { randomBytes } from 'crypto'; + +import { + createNewAccount, + expectUnencryptedLogsFromLastBlockToBe, + expectsNumOfEncryptedLogsInTheLastBlockToBe, + setup, +} from './utils.js'; + +describe('e2e_multiple_accounts_1_enc_key', () => { + let aztecNode: AztecNodeService | undefined; + let aztecRpcServer: AztecRPC; + const wallets: Wallet[] = []; + const accounts: AztecAddress[] = []; + let logger: DebugLogger; + + let zkTokenAddress: AztecAddress; + + const initialBalance = 987n; + const numAccounts = 3; + + beforeEach(async () => { + ({ aztecNode, aztecRpcServer, logger } = await setup(0)); + + const encryptionPrivateKey = randomBytes(32); + for (let i = 0; i < numAccounts; i++) { + logger(`Deploying account contract ${i}/3...`); + const signingPrivateKey = randomBytes(32); + const createWallet = async (address: AztecAddress, useProperKey: boolean) => + new StoredKeyAccountContract(address, useProperKey ? signingPrivateKey : randomBytes(32), await Schnorr.new()); + + const schnorr = await Schnorr.new(); + const signingPublicKey = schnorr.computePublicKey(signingPrivateKey); + const constructorArgs = [ + Fr.fromBuffer(signingPublicKey.subarray(0, 32)), + Fr.fromBuffer(signingPublicKey.subarray(32, 64)), + ]; + + const { wallet, address } = await createNewAccount( + aztecRpcServer, + SchnorrMultiKeyAccountContractAbi, + constructorArgs, + encryptionPrivateKey, + true, + createWallet, + ); + wallets.push(wallet); + accounts.push(address); + } + logger('Account contracts deployed'); + + // Verify that all accounts use the same encryption key + const encryptionPublicKey = await generatePublicKey(encryptionPrivateKey); + for (let i = 0; i < numAccounts; i++) { + const accountEncryptionPublicKey = await aztecRpcServer.getPublicKey(accounts[i]); + expect(accountEncryptionPublicKey).toEqual(encryptionPublicKey); + } + + logger(`Deploying ZK Token...`); + const tx = ZkTokenContract.deploy(aztecRpcServer, initialBalance, accounts[0]).send(); + const receipt = await tx.getReceipt(); + zkTokenAddress = receipt.contractAddress!; + await tx.isMined(0, 0.1); + const minedReceipt = await tx.getReceipt(); + expect(minedReceipt.status).toEqual(TxStatus.MINED); + logger('ZK Token deployed'); + }, 100_000); + + afterEach(async () => { + await aztecNode?.stop(); + if (aztecRpcServer instanceof AztecRPCServer) { + await aztecRpcServer?.stop(); + } + }); + + const expectBalance = async (userIndex: number, expectedBalance: bigint) => { + const wallet = wallets[userIndex]; + const owner = accounts[userIndex]; + + // Then check the balance + const contractWithWallet = new ZkTokenContract(zkTokenAddress, wallet); + const [balance] = await contractWithWallet.methods.getBalance(owner).view({ from: owner }); + logger(`Account ${owner} balance: ${balance}`); + expect(balance).toBe(expectedBalance); + }; + + const transfer = async ( + senderIndex: number, + receiverIndex: number, + transferAmount: bigint, + expectedBalances: bigint[], + ) => { + logger(`Transfer ${transferAmount} from ${accounts[senderIndex]} to ${accounts[receiverIndex]}...`); + + const sender = accounts[senderIndex]; + const receiver = accounts[receiverIndex]; + + const contractWithWallet = new ZkTokenContract(zkTokenAddress, wallets[senderIndex]); + + const tx = contractWithWallet.methods.transfer(transferAmount, sender, receiver).send({ origin: sender }); + await tx.isMined(0, 0.1); + const receipt = await tx.getReceipt(); + + expect(receipt.status).toBe(TxStatus.MINED); + + for (let i = 0; i < expectedBalances.length; i++) { + await expectBalance(i, expectedBalances[i]); + } + + await expectsNumOfEncryptedLogsInTheLastBlockToBe(aztecNode, 2); + await expectUnencryptedLogsFromLastBlockToBe(aztecNode, ['Coins transferred']); + + logger(`Transfer ${transferAmount} from ${sender} to ${receiver} successful`); + }; + + /** + * Tests the ability of the Aztec RPC server to handle multiple accounts under the same encryption key. + */ + it('spends notes from multiple account under the same encryption key', async () => { + const transferAmount1 = 654n; // account 0 -> account 1 + const transferAmount2 = 123n; // account 0 -> account 2 + const transferAmount3 = 210n; // account 1 -> account 2 + + await expectBalance(0, initialBalance); + await expectBalance(1, 0n); + await expectBalance(2, 0n); + + const expectedBalancesAfterTransfer1 = [initialBalance - transferAmount1, transferAmount1, 0n]; + await transfer(0, 1, transferAmount1, expectedBalancesAfterTransfer1); + + const expectedBalancesAfterTransfer2 = [ + expectedBalancesAfterTransfer1[0] - transferAmount2, + expectedBalancesAfterTransfer1[1], + transferAmount2, + ]; + await transfer(0, 2, transferAmount2, expectedBalancesAfterTransfer2); + + const expectedBalancesAfterTransfer3 = [ + expectedBalancesAfterTransfer2[0], + expectedBalancesAfterTransfer2[1] - transferAmount3, + expectedBalancesAfterTransfer2[2] + transferAmount3, + ]; + await transfer(1, 2, transferAmount3, expectedBalancesAfterTransfer3); + }, 180_000); +}); diff --git a/yarn-project/end-to-end/src/utils.ts b/yarn-project/end-to-end/src/utils.ts index 8bf24eb2bf3..10d601161be 100644 --- a/yarn-project/end-to-end/src/utils.ts +++ b/yarn-project/end-to-end/src/utils.ts @@ -2,6 +2,7 @@ import { AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/azte import { RpcServerConfig, createAztecRPCServer, getConfigEnvVars as getRpcConfigEnvVars } from '@aztec/aztec-rpc'; import { AccountCollection, + AccountImplementation, AccountWallet, AztecAddress, Contract, @@ -15,7 +16,13 @@ import { generatePublicKey, getL1ContractAddresses, } from '@aztec/aztec.js'; -import { CircuitsWasm, DeploymentInfo, getContractDeploymentInfo } from '@aztec/circuits.js'; +import { + CircuitsWasm, + DeploymentInfo, + PartialContractAddress, + PublicKey, + getContractDeploymentInfo, +} from '@aztec/circuits.js'; import { Schnorr, pedersenPlookupCommitInputs } from '@aztec/circuits.js/barretenberg'; import { DeployL1Contracts, deployL1Contract, deployL1Contracts } from '@aztec/ethereum'; import { ContractAbi } from '@aztec/foundation/abi'; @@ -321,6 +328,75 @@ export async function setup(numberOfAccounts = 1): Promise<{ }; } +/** + * Deploys a smart contract on L2. + * @param aztecRpcServer - An instance of AztecRPC that will be used for contract deployment. + * @param publicKey - The encryption public key. + * @param abi - The Contract ABI (Application Binary Interface) that defines the contract's interface. + * @param args - An array of arguments to be passed to the contract constructor during deployment. + * @param contractAddressSalt - A random value used as a salt to generate the contract address. If not provided, the contract address will be deterministic. + * @returns An object containing the deployed contract's address and partial contract address. + */ +export async function deployContract( + aztecRpcServer: AztecRPC, + publicKey: PublicKey, + abi: ContractAbi, + args: any[], + contractAddressSalt?: Fr, +) { + const deployer = new ContractDeployer(abi, aztecRpcServer, publicKey); + const deployMethod = deployer.deploy(...args); + await deployMethod.create({ contractAddressSalt }); + const tx = deployMethod.send(); + expect(await tx.isMined(0, 0.1)).toBeTruthy(); + const receipt = await tx.getReceipt(); + return { address: receipt.contractAddress!, partialContractAddress: deployMethod.partialContractAddress! }; +} + +/** + * Represents a function that creates an AccountImplementation object asynchronously. + * + * @param address - The Aztec address associated with the account. + * @param useProperKey - A flag indicating whether the proper key should be used during account creation. + * @param partialAddress - The partial contract address associated with the account. + * @param encryptionPrivateKey - The encryption private key used during account creation. + * @returns A Promise that resolves to an AccountImplementation object. + */ +export type CreateAccountImplFn = ( + address: AztecAddress, + useProperKey: boolean, + partialAddress: PartialContractAddress, + encryptionPrivateKey: Buffer, +) => Promise; + +/** + * Creates a new account. + * @param aztecRpcServer - The AztecRPC server to interact with. + * @param abi - The ABI (Application Binary Interface) of the account contract. + * @param args - The arguments to pass to the account contract's constructor. + * @param encryptionPrivateKey - The encryption private key used by the account. + * @param useProperKey - A flag indicating whether the proper key should be used during account creation. + * @param createAccountImpl - A function that creates an AccountImplementation object. + * @returns A Promise that resolves to an object containing the created wallet, account address, and partial address. + */ +export async function createNewAccount( + aztecRpcServer: AztecRPC, + abi: ContractAbi, + args: any[], + encryptionPrivateKey: Buffer, + useProperKey: boolean, + createAccountImpl: CreateAccountImplFn, +) { + const salt = Fr.random(); + const publicKey = await generatePublicKey(encryptionPrivateKey); + const { address, partialAddress } = await getContractDeploymentInfo(abi, args, salt, publicKey); + await aztecRpcServer.addAccount(encryptionPrivateKey, address, partialAddress); + await deployContract(aztecRpcServer, publicKey, abi, args, salt); + const account = await createAccountImpl(address, useProperKey, partialAddress, encryptionPrivateKey); + const wallet = new AccountWallet(aztecRpcServer, account); + return { wallet, address, partialAddress }; +} + /** * Sets the timestamp of the next block. * @param rpcUrl - rpc url of the blockchain instance to connect to