From cf4a454a97ffa8d1858fbcdc5f9aef6bf97f554b Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 9 Jan 2024 16:29:51 +0000 Subject: [PATCH 01/12] fix: close leveldb handle when node stops --- yarn-project/aztec-node/src/aztec-node/server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 36d19ed675f8..1a8ff721839b 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -285,6 +285,7 @@ export class AztecNodeService implements AztecNode { await this.p2pClient.stop(); await this.worldStateSynchronizer.stop(); await this.blockSource.stop(); + await this.merkleTreesDb.close(); this.log.info(`Stopped`); } From 5de4fefca3d0a4186e42db91ae289d92b282c694 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 9 Jan 2024 16:30:41 +0000 Subject: [PATCH 02/12] fix: memory fifo warns when flushing --- yarn-project/foundation/src/fifo/memory_fifo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn-project/foundation/src/fifo/memory_fifo.ts b/yarn-project/foundation/src/fifo/memory_fifo.ts index 50af730cb059..5bb614eaf23a 100644 --- a/yarn-project/foundation/src/fifo/memory_fifo.ts +++ b/yarn-project/foundation/src/fifo/memory_fifo.ts @@ -60,6 +60,7 @@ export class MemoryFifo { */ public put(item: T) { if (this.flushing) { + this.log.warn('Discarding item because queue is flushing'); return; } else if (this.waiting.length) { this.waiting.shift()!(item); From 363bcc351a271850a62c7353e94953648823a09b Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 9 Jan 2024 21:45:00 +0000 Subject: [PATCH 03/12] fix: log how many accounts PXE restored --- yarn-project/pxe/src/pxe_service/pxe_service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index 636e0700fe07..ee487cce1264 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -112,12 +112,19 @@ export class PXEService implements PXE { const registeredAddresses = await this.db.getCompleteAddresses(); + let count = 0; for (const address of registeredAddresses) { if (!publicKeysSet.has(address.publicKey.toString())) { continue; } + + count++; this.synchronizer.addAccount(address.publicKey, this.keyStore, this.config.l2StartingBlock); } + + if (count > 0) { + this.log(`Restored ${count} accounts`); + } } /** From 3616c4c7ad96cd93265fc1dd91b304f1cb531846 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 9 Jan 2024 21:47:00 +0000 Subject: [PATCH 04/12] test: persistence --- .../end-to-end/src/e2e_2_pxes.test.ts | 2 +- .../end-to-end/src/e2e_persistence.test.ts | 76 +++++++++++++++++++ yarn-project/end-to-end/src/fixtures/utils.ts | 33 ++++++-- 3 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 yarn-project/end-to-end/src/e2e_persistence.test.ts diff --git a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts index e7022888367d..7b7e4778689c 100644 --- a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts @@ -52,7 +52,7 @@ describe('e2e_2_pxes', () => { pxe: pxeB, accounts: accounts, wallets: [walletB], - } = await setupPXEService(1, aztecNode!, undefined, true)); + } = await setupPXEService(1, aztecNode!, {}, undefined, true)); [userB] = accounts; }, 100_000); diff --git a/yarn-project/end-to-end/src/e2e_persistence.test.ts b/yarn-project/end-to-end/src/e2e_persistence.test.ts new file mode 100644 index 000000000000..ccbb801d954e --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_persistence.test.ts @@ -0,0 +1,76 @@ +import { getUnsafeSchnorrAccount, getUnsafeSchnorrWallet } from '@aztec/accounts/single_key'; +import { AccountWallet } from '@aztec/aztec.js'; +import { AztecAddress, Fq, Fr } from '@aztec/circuits.js'; +import { DeployL1Contracts } from '@aztec/ethereum'; +import { EasyPrivateTokenContract } from '@aztec/noir-contracts/EasyPrivateToken'; + +import { mkdtemp } from 'fs/promises'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +import { EndToEndContext, setup } from './fixtures/utils.js'; + +describe('Aztec persistence', () => { + let dataDirectory: string; + + let contract: EasyPrivateTokenContract; + let contractAddress: AztecAddress; + + let ownerPrivateKey: Fq; + let ownerWallet: AccountWallet; + + let deployL1ContractsValues: DeployL1Contracts; + let context: EndToEndContext; + + beforeAll(async () => { + dataDirectory = await mkdtemp(join(tmpdir(), 'aztec-node-')); + + const initialContext = await setup(0, { dataDirectory }, { dataDirectory }); + deployL1ContractsValues = initialContext.deployL1ContractsValues; + + ownerPrivateKey = Fq.random(); + ownerWallet = await getUnsafeSchnorrAccount(initialContext.pxe, ownerPrivateKey, Fr.ZERO).waitDeploy(); + + const deployer = EasyPrivateTokenContract.deploy(ownerWallet, 1000n, ownerWallet.getAddress()); + await deployer.simulate({}); + + contract = await deployer.send().deployed(); + contractAddress = contract.address; + + await initialContext.teardown(); + }, 100_000); + + beforeEach(async () => { + context = await setup(0, { dataDirectory, deployL1ContractsValues }, { dataDirectory }); + + ownerWallet = await getUnsafeSchnorrWallet(context.pxe, ownerWallet.getAddress(), ownerPrivateKey); + contract = await EasyPrivateTokenContract.at(contractAddress, ownerWallet); + }); + + afterEach(async () => { + await context.teardown(); + }); + + it('should correctly restore balances', async () => { + await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(1000n); + }); + + it('should track new notes for the owner', async () => { + await contract.methods.mint(1000n, ownerWallet.getAddress()).send().wait(); + await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(2000n); + }); + + it('should transfer tokens from owner', async () => { + const targetWallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + + await contract.methods.transfer(500n, ownerWallet.getAddress(), targetWallet.getAddress()).send().wait(); + + const [ownerBalance, targetBalance] = await Promise.all([ + contract.methods.getBalance(ownerWallet.getAddress()).view(), + contract.methods.getBalance(targetWallet.getAddress()).view(), + ]); + + expect(ownerBalance).toEqual(1500n); + expect(targetBalance).toEqual(500n); + }); +}); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index ae5755f56ea3..700af91b8a8d 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -33,7 +33,7 @@ import { RollupAbi, RollupBytecode, } from '@aztec/l1-artifacts'; -import { PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; +import { PXEService, PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; import { SequencerClient } from '@aztec/sequencer-client'; import * as path from 'path'; @@ -108,6 +108,7 @@ export const setupL1Contracts = async ( * Sets up Private eXecution Environment (PXE). * @param numberOfAccounts - The number of new accounts to be created once the PXE is initiated. * @param aztecNode - An instance of Aztec Node. + * @param opts - Partial configuration for the PXE service. * @param firstPrivKey - The private key of the first account to be created. * @param logger - The logger to be used. * @param useLogSuffix - Whether to add a randomly generated suffix to the PXE debug logs. @@ -116,6 +117,7 @@ export const setupL1Contracts = async ( export async function setupPXEService( numberOfAccounts: number, aztecNode: AztecNode, + opts: Partial = {}, logger = getLogger(), useLogSuffix = false, ): Promise<{ @@ -136,7 +138,7 @@ export async function setupPXEService( */ logger: DebugLogger; }> { - const pxeServiceConfig = getPXEServiceConfig(); + const pxeServiceConfig = { ...getPXEServiceConfig(), ...opts }; const pxe = await createPXEService(aztecNode, pxeServiceConfig, useLogSuffix); const wallets = await createAccounts(pxe, numberOfAccounts); @@ -215,7 +217,12 @@ async function setupWithRemoteEnvironment( } /** Options for the e2e tests setup */ -type SetupOptions = { /** State load */ stateLoad?: string } & Partial; +type SetupOptions = { + /** State load */ + stateLoad?: string; + /** Previously deployed contracts on L1 */ + deployL1ContractsValues?: DeployL1Contracts; +} & Partial; /** Context for an end-to-end test as returned by the `setup` function */ export type EndToEndContext = { @@ -247,8 +254,13 @@ export type EndToEndContext = { * Sets up the environment for the end-to-end tests. * @param numberOfAccounts - The number of new accounts to be created once the PXE is initiated. * @param opts - Options to pass to the node initialization and to the setup script. + * @param pxeOpts - Options to pass to the PXE initialization. */ -export async function setup(numberOfAccounts = 1, opts: SetupOptions = {}): Promise { +export async function setup( + numberOfAccounts = 1, + opts: SetupOptions = {}, + pxeOpts: Partial = {}, +): Promise { const config = { ...getConfigEnvVars(), ...opts }; // Enable logging metrics to a local file named after the test suite @@ -264,15 +276,20 @@ export async function setup(numberOfAccounts = 1, opts: SetupOptions = {}): Prom const logger = getLogger(); const hdAccount = mnemonicToAccount(MNEMONIC); + const privKeyRaw = hdAccount.getHdKey().privateKey; + const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); if (PXE_URL) { // we are setting up against a remote environment, l1 contracts are assumed to already be deployed return await setupWithRemoteEnvironment(hdAccount, config, logger, numberOfAccounts); } - const deployL1ContractsValues = await setupL1Contracts(config.rpcUrl, hdAccount, logger); - const privKeyRaw = hdAccount.getHdKey().privateKey; - const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); + let deployL1ContractsValues: DeployL1Contracts; + if (!opts.deployL1ContractsValues) { + deployL1ContractsValues = await setupL1Contracts(config.rpcUrl, hdAccount, logger); + } else { + deployL1ContractsValues = opts.deployL1ContractsValues; + } config.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; config.l1Contracts.rollupAddress = deployL1ContractsValues.l1ContractAddresses.rollupAddress; @@ -286,7 +303,7 @@ export async function setup(numberOfAccounts = 1, opts: SetupOptions = {}): Prom const aztecNode = await AztecNodeService.createAndSync(config); const sequencer = aztecNode.getSequencer(); - const { pxe, accounts, wallets } = await setupPXEService(numberOfAccounts, aztecNode!, logger); + const { pxe, accounts, wallets } = await setupPXEService(numberOfAccounts, aztecNode!, pxeOpts, logger); const cheatCodes = CheatCodes.create(config.rpcUrl, pxe!); From d1c3bfdfadf21836bd9ea1b8b22f11ed54b31d96 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 9 Jan 2024 22:01:26 +0000 Subject: [PATCH 05/12] test: test node/pxe database persistence --- .circleci/config.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 99bde18cac10..b065a5ee4b29 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -758,6 +758,17 @@ jobs: name: "Test" command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_cli.test.ts + e2e-persistence: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose-no-sandbox.yml TEST=e2e_persistence.test.ts + e2e-p2p: docker: - image: aztecprotocol/alpine-build-image @@ -1205,6 +1216,7 @@ workflows: - uniswap-trade-on-l1-from-l2: *e2e_test - integration-l1-publisher: *e2e_test - integration-archiver-l1-to-l2: *e2e_test + - e2e-persistence: *e2e_test - e2e-p2p: *e2e_test - e2e-browser: *e2e_test - e2e-card-game: *e2e_test @@ -1241,6 +1253,7 @@ workflows: - uniswap-trade-on-l1-from-l2 - integration-l1-publisher - integration-archiver-l1-to-l2 + - e2e-persistence - e2e-p2p - e2e-browser - e2e-card-game From 00353c814be31cb2b86ac06099bf4a3e1de41afc Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 9 Jan 2024 22:01:32 +0000 Subject: [PATCH 06/12] fix: reject transactions if pxe not running --- yarn-project/pxe/src/pxe_service/pxe_service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index ee487cce1264..f97f00aad883 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -76,6 +76,7 @@ export class PXEService implements PXE { // serialize synchronizer and calls to simulateTx. // ensures that state is not changed while simulating private jobQueue = new SerialQueue(); + private running = false; constructor( private keyStore: KeyStore, @@ -104,6 +105,7 @@ export class PXEService implements PXE { await this.restoreNoteProcessors(); const info = await this.getNodeInfo(); this.log.info(`Started PXE connected to chain ${info.chainId} version ${info.protocolVersion}`); + this.running = true; } private async restoreNoteProcessors() { @@ -353,6 +355,9 @@ export class PXEService implements PXE { if (txRequest.functionData.isInternal === undefined) { throw new Error(`Unspecified internal are not allowed`); } + if (!this.running) { + throw new Error('PXE Service is not running'); + } // all simulations must be serialized w.r.t. the synchronizer return await this.jobQueue.put(async () => { @@ -378,6 +383,10 @@ export class PXEService implements PXE { } public async sendTx(tx: Tx): Promise { + if (!this.running) { + throw new Error('PXE Service is not running'); + } + const txHash = await tx.getTxHash(); if (await this.node.getTx(txHash)) { throw new Error(`A settled tx with equal hash ${txHash.toString()} exists.`); @@ -393,6 +402,10 @@ export class PXEService implements PXE { to: AztecAddress, _from?: AztecAddress, ): Promise { + if (!this.running) { + throw new Error('PXE Service is not running'); + } + // all simulations must be serialized w.r.t. the synchronizer return await this.jobQueue.put(async () => { // TODO - Should check if `from` has the permission to call the view function. From 2dc8b54901e9587ab2449e98b2f683b3c74a0c2a Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 10 Jan 2024 08:29:57 +0000 Subject: [PATCH 07/12] feat: expose waitForAccountSynch --- .../aztec.js/src/account_manager/deploy_account_sent_tx.ts | 2 +- yarn-project/aztec.js/src/account_manager/index.ts | 2 +- yarn-project/aztec.js/src/index.ts | 1 + .../src/{account_manager/util.ts => utils/account.ts} | 4 ++-- yarn-project/aztec.js/src/utils/index.ts | 1 + 5 files changed, 6 insertions(+), 4 deletions(-) rename yarn-project/aztec.js/src/{account_manager/util.ts => utils/account.ts} (87%) diff --git a/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts b/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts index bc8f79bba3ad..5017320d21e2 100644 --- a/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts +++ b/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts @@ -3,7 +3,7 @@ import { TxHash, TxReceipt } from '@aztec/types'; import { Wallet } from '../account/index.js'; import { DefaultWaitOpts, SentTx, WaitOpts } from '../contract/index.js'; -import { waitForAccountSynch } from './util.js'; +import { waitForAccountSynch } from '../utils/account.js'; /** Extends a transaction receipt with a wallet instance for the newly deployed contract. */ export type DeployAccountTxReceipt = FieldsOf & { diff --git a/yarn-project/aztec.js/src/account_manager/index.ts b/yarn-project/aztec.js/src/account_manager/index.ts index 59d3ed9d4c97..18f7e31b9501 100644 --- a/yarn-project/aztec.js/src/account_manager/index.ts +++ b/yarn-project/aztec.js/src/account_manager/index.ts @@ -7,10 +7,10 @@ import { Salt } from '../account/index.js'; import { AccountInterface } from '../account/interface.js'; import { DefaultWaitOpts, DeployMethod, WaitOpts } from '../contract/index.js'; import { ContractDeployer } from '../contract_deployer/index.js'; +import { waitForAccountSynch } from '../utils/account.js'; import { generatePublicKey } from '../utils/index.js'; import { AccountWalletWithPrivateKey } from '../wallet/index.js'; import { DeployAccountSentTx } from './deploy_account_sent_tx.js'; -import { waitForAccountSynch } from './util.js'; /** * Manages a user account. Provides methods for calculating the account's address, deploying the account contract, diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index b851c8821a30..f53e9715644d 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -45,6 +45,7 @@ export { EthCheatCodes, computeAuthWitMessageHash, waitForPXE, + waitForAccountSynch, } from './utils/index.js'; export { createPXEClient } from './pxe_client.js'; diff --git a/yarn-project/aztec.js/src/account_manager/util.ts b/yarn-project/aztec.js/src/utils/account.ts similarity index 87% rename from yarn-project/aztec.js/src/account_manager/util.ts rename to yarn-project/aztec.js/src/utils/account.ts index 8d1bf4a0be76..56fe5b1d46f5 100644 --- a/yarn-project/aztec.js/src/account_manager/util.ts +++ b/yarn-project/aztec.js/src/utils/account.ts @@ -1,7 +1,7 @@ import { retryUntil } from '@aztec/foundation/retry'; import { CompleteAddress, PXE } from '@aztec/types'; -import { WaitOpts } from '../contract/index.js'; +import { DefaultWaitOpts, WaitOpts } from '../contract/index.js'; /** * Waits for the account to finish synchronizing with the PXE Service. @@ -12,7 +12,7 @@ import { WaitOpts } from '../contract/index.js'; export async function waitForAccountSynch( pxe: PXE, address: CompleteAddress, - { interval, timeout }: WaitOpts, + { interval, timeout }: WaitOpts = DefaultWaitOpts, ): Promise { const publicKey = address.publicKey.toString(); await retryUntil( diff --git a/yarn-project/aztec.js/src/utils/index.ts b/yarn-project/aztec.js/src/utils/index.ts index 593a8ae4e6e5..5be623ef38de 100644 --- a/yarn-project/aztec.js/src/utils/index.ts +++ b/yarn-project/aztec.js/src/utils/index.ts @@ -6,3 +6,4 @@ export * from './abi_types.js'; export * from './cheat_codes.js'; export * from './authwit.js'; export * from './pxe.js'; +export * from './account.js'; From cfb749222041ddaeebe4707149ce1dc392adecaf Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 10 Jan 2024 09:24:35 +0000 Subject: [PATCH 08/12] wip --- .../end-to-end/src/e2e_persistence.test.ts | 213 +++++++++++++++--- 1 file changed, 186 insertions(+), 27 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_persistence.test.ts b/yarn-project/end-to-end/src/e2e_persistence.test.ts index ccbb801d954e..24425b53eba8 100644 --- a/yarn-project/end-to-end/src/e2e_persistence.test.ts +++ b/yarn-project/end-to-end/src/e2e_persistence.test.ts @@ -1,6 +1,6 @@ import { getUnsafeSchnorrAccount, getUnsafeSchnorrWallet } from '@aztec/accounts/single_key'; -import { AccountWallet } from '@aztec/aztec.js'; -import { AztecAddress, Fq, Fr } from '@aztec/circuits.js'; +import { AccountWallet, waitForAccountSynch } from '@aztec/aztec.js'; +import { CompleteAddress, EthAddress, Fq, Fr } from '@aztec/circuits.js'; import { DeployL1Contracts } from '@aztec/ethereum'; import { EasyPrivateTokenContract } from '@aztec/noir-contracts/EasyPrivateToken'; @@ -14,14 +14,16 @@ describe('Aztec persistence', () => { let dataDirectory: string; let contract: EasyPrivateTokenContract; - let contractAddress: AztecAddress; + let contractAddress: CompleteAddress; let ownerPrivateKey: Fq; + let ownerAddress: CompleteAddress; let ownerWallet: AccountWallet; let deployL1ContractsValues: DeployL1Contracts; let context: EndToEndContext; + // deploy L1 contracts, start initial node & PXE, deploy test contract & shutdown node and PXE beforeAll(async () => { dataDirectory = await mkdtemp(join(tmpdir(), 'aztec-node-')); @@ -30,47 +32,204 @@ describe('Aztec persistence', () => { ownerPrivateKey = Fq.random(); ownerWallet = await getUnsafeSchnorrAccount(initialContext.pxe, ownerPrivateKey, Fr.ZERO).waitDeploy(); + ownerAddress = ownerWallet.getCompleteAddress(); const deployer = EasyPrivateTokenContract.deploy(ownerWallet, 1000n, ownerWallet.getAddress()); await deployer.simulate({}); contract = await deployer.send().deployed(); - contractAddress = contract.address; + contractAddress = contract.completeAddress; await initialContext.teardown(); }, 100_000); - beforeEach(async () => { - context = await setup(0, { dataDirectory, deployL1ContractsValues }, { dataDirectory }); - - ownerWallet = await getUnsafeSchnorrWallet(context.pxe, ownerWallet.getAddress(), ownerPrivateKey); - contract = await EasyPrivateTokenContract.at(contractAddress, ownerWallet); + // ie we were shutdown and now starting back up. Sync should be ~instant + describe('when restarting the node and PXE with previous data', () => { + beforeEach(async () => { + context = await setup(0, { dataDirectory, deployL1ContractsValues }, { dataDirectory }); + + ownerWallet = await getUnsafeSchnorrWallet(context.pxe, ownerAddress.address, ownerPrivateKey); + contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet); + }); + + afterEach(async () => { + await context.teardown(); + }); + + it('correctly restores balances', async () => { + // test for >0 instead of exact value so test isn't dependent on run order + await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toBeGreaterThan(0n); + }); + + it('tracks new notes for the owner', async () => { + const balance = await contract.methods.getBalance(ownerWallet.getAddress()).view(); + await contract.methods.mint(1000n, ownerWallet.getAddress()).send().wait(); + await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(balance + 1000n); + }); + + it('allows transfers of tokens from owner', async () => { + const targetWallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + + const initialOwnerBalance = await contract.methods.getBalance(ownerWallet.getAddress()).view(); + await contract.methods.transfer(500n, ownerWallet.getAddress(), targetWallet.getAddress()).send().wait(); + const [ownerBalance, targetBalance] = await Promise.all([ + contract.methods.getBalance(ownerWallet.getAddress()).view(), + contract.methods.getBalance(targetWallet.getAddress()).view(), + ]); + + expect(ownerBalance).toEqual(initialOwnerBalance - 500n); + expect(targetBalance).toEqual(500n); + }); }); - afterEach(async () => { - await context.teardown(); + describe('when starting a new node and PXE without previous data', () => { + // increase timeout since this would require a full node sync + beforeEach(async () => { + context = await setup(0, { deployL1ContractsValues }, {}); + }, 50_000); + + afterEach(async () => { + await context.teardown(); + }); + + it('pxe does not have the owner account', async () => { + await expect(context.pxe.getRecipient(ownerAddress.address)).resolves.toBeUndefined(); + }); + + it('the node has the contract', async () => { + await expect(context.aztecNode.getContractData(contractAddress.address)).resolves.toBeDefined(); + }); + + it('pxe does not know of the deployed contract', async () => { + await context.pxe.registerRecipient(ownerAddress); + + const wallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + const contract = await EasyPrivateTokenContract.at(contractAddress.address, wallet); + await expect(contract.methods.getBalance(ownerAddress.address).view()).rejects.toThrowError(/Unknown contract/); + }); + + it("pxe does not have owner's notes", async () => { + await context.pxe.addContracts([ + { + artifact: EasyPrivateTokenContract.artifact, + completeAddress: contractAddress, + portalContract: EthAddress.ZERO, + }, + ]); + await context.pxe.registerRecipient(ownerAddress); + + const wallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + const contract = await EasyPrivateTokenContract.at(contractAddress.address, wallet); + await expect(contract.methods.getBalance(ownerAddress.address).view()).resolves.toEqual(0n); + }); + + it('pxe restores notes after registering the owner', async () => { + await context.pxe.addContracts([ + { + artifact: EasyPrivateTokenContract.artifact, + completeAddress: contractAddress, + portalContract: EthAddress.ZERO, + }, + ]); + + await context.pxe.registerAccount(ownerPrivateKey, ownerAddress.partialAddress); + const ownerWallet = await getUnsafeSchnorrAccount(context.pxe, ownerPrivateKey, ownerAddress).getWallet(); + const contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet); + await waitForAccountSynch(context.pxe, ownerAddress, { interval: 1, timeout: 10 }); + await expect(contract.methods.getBalance(ownerAddress.address).view()).resolves.toEqual(1500n); + }); }); - it('should correctly restore balances', async () => { - await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(1000n); - }); + describe('when connecting a PXE to a new node', () => { + beforeEach(async () => { + context = await setup(0, { deployL1ContractsValues }, { dataDirectory }); - it('should track new notes for the owner', async () => { - await contract.methods.mint(1000n, ownerWallet.getAddress()).send().wait(); - await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(2000n); - }); + ownerWallet = await getUnsafeSchnorrWallet(context.pxe, ownerAddress.address, ownerPrivateKey); + contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet); + }, 50_000); + + afterEach(async () => { + await context.teardown(); + }); - it('should transfer tokens from owner', async () => { - const targetWallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + it('correctly restores balances', async () => { + await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(1500n); + }); - await contract.methods.transfer(500n, ownerWallet.getAddress(), targetWallet.getAddress()).send().wait(); + it('tracks new notes for the owner', async () => { + await contract.methods.mint(1000n, ownerWallet.getAddress()).send().wait(); + await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(2500n); + }); - const [ownerBalance, targetBalance] = await Promise.all([ - contract.methods.getBalance(ownerWallet.getAddress()).view(), - contract.methods.getBalance(targetWallet.getAddress()).view(), - ]); + it('allows transfers of tokens from owner', async () => { + const targetWallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + + await contract.methods.transfer(500n, ownerWallet.getAddress(), targetWallet.getAddress()).send().wait(); + + const [ownerBalance, targetBalance] = await Promise.all([ + contract.methods.getBalance(ownerWallet.getAddress()).view(), + contract.methods.getBalance(targetWallet.getAddress()).view(), + ]); + + expect(ownerBalance).toEqual(2000n); + expect(targetBalance).toEqual(500n); + }, 25_000); + }); - expect(ownerBalance).toEqual(1500n); - expect(targetBalance).toEqual(500n); + describe('when connecting a new PXE to a node with synced data', () => { + beforeEach(async () => { + context = await setup(0, { dataDirectory, deployL1ContractsValues }, {}); + }); + + afterEach(async () => { + await context.teardown(); + }); + + it('does not restore previous accounts', async () => { + await expect(context.pxe.getRecipient(ownerAddress.address)).resolves.toBeUndefined(); + }); + + it('the node has the contract', async () => { + await expect(context.aztecNode.getContractData(contractAddress.address)).resolves.toBeDefined(); + }); + + it('pxe does not know of the deployed contract', async () => { + await context.pxe.registerRecipient(ownerAddress); + + const wallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + const contract = await EasyPrivateTokenContract.at(contractAddress.address, wallet); + await expect(contract.methods.getBalance(ownerAddress.address).view()).rejects.toThrowError(/Unknown contract/); + }); + + it("pxe does not have owner's notes", async () => { + await context.pxe.addContracts([ + { + artifact: EasyPrivateTokenContract.artifact, + completeAddress: contractAddress, + portalContract: EthAddress.ZERO, + }, + ]); + await context.pxe.registerRecipient(ownerAddress); + + const wallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + const contract = await EasyPrivateTokenContract.at(contractAddress.address, wallet); + await expect(contract.methods.getBalance(ownerAddress.address).view()).resolves.toEqual(0n); + }); + + it('pxe restores notes after registering the owner', async () => { + await context.pxe.addContracts([ + { + artifact: EasyPrivateTokenContract.artifact, + completeAddress: contractAddress, + portalContract: EthAddress.ZERO, + }, + ]); + + await context.pxe.registerAccount(ownerPrivateKey, ownerAddress.partialAddress); + const ownerWallet = await getUnsafeSchnorrAccount(context.pxe, ownerPrivateKey, ownerAddress).getWallet(); + const contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet); + await waitForAccountSynch(context.pxe, ownerAddress, { interval: 1, timeout: 10 }); + await expect(contract.methods.getBalance(ownerAddress.address).view()).resolves.toEqual(2000n); + }); }); }); From 570a6f89ab7b05f05eb10b344cb51b99c73c95b3 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 10 Jan 2024 10:02:36 +0000 Subject: [PATCH 09/12] test: extended e2e persistence test --- .../end-to-end/src/e2e_persistence.test.ts | 175 +++++++----------- 1 file changed, 62 insertions(+), 113 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_persistence.test.ts b/yarn-project/end-to-end/src/e2e_persistence.test.ts index 24425b53eba8..0779417a59a9 100644 --- a/yarn-project/end-to-end/src/e2e_persistence.test.ts +++ b/yarn-project/end-to-end/src/e2e_persistence.test.ts @@ -11,16 +11,30 @@ import { join } from 'path'; import { EndToEndContext, setup } from './fixtures/utils.js'; describe('Aztec persistence', () => { - let dataDirectory: string; - - let contract: EasyPrivateTokenContract; + /** + * These tests check that the Aztec Node and PXE can be shutdown and restarted without losing data. + * + * There are four scenarios to check: + * 1. Node and PXE are started with an existing databases + * 2. PXE is started with an existing database and connects to a Node with an empty database + * 3. PXE is started with an empty database and connects to a Node with an existing database + * 4. PXE is started with an empty database and connects to a Node with an empty database + * + * All four scenarios use the same L1 state, which is deployed in the `beforeAll` hook. + */ + + // the test contract and account deploying it let contractAddress: CompleteAddress; - let ownerPrivateKey: Fq; let ownerAddress: CompleteAddress; - let ownerWallet: AccountWallet; + // a directory where data will be persisted by components + // passing this through to the Node or PXE will control whether they use persisted data or not + let dataDirectory: string; + + // state that is persisted between tests let deployL1ContractsValues: DeployL1Contracts; + let context: EndToEndContext; // deploy L1 contracts, start initial node & PXE, deploy test contract & shutdown node and PXE @@ -31,26 +45,40 @@ describe('Aztec persistence', () => { deployL1ContractsValues = initialContext.deployL1ContractsValues; ownerPrivateKey = Fq.random(); - ownerWallet = await getUnsafeSchnorrAccount(initialContext.pxe, ownerPrivateKey, Fr.ZERO).waitDeploy(); + const ownerWallet = await getUnsafeSchnorrAccount(initialContext.pxe, ownerPrivateKey, Fr.ZERO).waitDeploy(); ownerAddress = ownerWallet.getCompleteAddress(); const deployer = EasyPrivateTokenContract.deploy(ownerWallet, 1000n, ownerWallet.getAddress()); await deployer.simulate({}); - contract = await deployer.send().deployed(); + const contract = await deployer.send().deployed(); contractAddress = contract.completeAddress; await initialContext.teardown(); }, 100_000); - // ie we were shutdown and now starting back up. Sync should be ~instant - describe('when restarting the node and PXE with previous data', () => { - beforeEach(async () => { - context = await setup(0, { dataDirectory, deployL1ContractsValues }, { dataDirectory }); + describe.each([ + [ + // ie we were shutdown and now starting back up. Initial sync should be ~instant + 'when starting Node and PXE with existing databases', + () => setup(0, { dataDirectory, deployL1ContractsValues }, { dataDirectory }), + 1000, + ], + [ + // ie our PXE was restarted, data kept intact and now connects to a "new" Node. Initial synch will synch from scratch + 'when starting a PXE with an existing database, connected to a Node with database synched from scratch', + () => setup(0, { deployL1ContractsValues }, { dataDirectory }), + 10_000, + ], + ])('%s', (_, contextSetup, timeout) => { + let ownerWallet: AccountWallet; + let contract: EasyPrivateTokenContract; + beforeEach(async () => { + context = await contextSetup(); ownerWallet = await getUnsafeSchnorrWallet(context.pxe, ownerAddress.address, ownerPrivateKey); contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet); - }); + }, timeout); afterEach(async () => { await context.teardown(); @@ -68,13 +96,13 @@ describe('Aztec persistence', () => { }); it('allows transfers of tokens from owner', async () => { - const targetWallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); + const otherWallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); const initialOwnerBalance = await contract.methods.getBalance(ownerWallet.getAddress()).view(); - await contract.methods.transfer(500n, ownerWallet.getAddress(), targetWallet.getAddress()).send().wait(); + await contract.methods.transfer(500n, ownerWallet.getAddress(), otherWallet.getAddress()).send().wait(); const [ownerBalance, targetBalance] = await Promise.all([ contract.methods.getBalance(ownerWallet.getAddress()).view(), - contract.methods.getBalance(targetWallet.getAddress()).view(), + contract.methods.getBalance(otherWallet.getAddress()).view(), ]); expect(ownerBalance).toEqual(initialOwnerBalance - 500n); @@ -82,12 +110,23 @@ describe('Aztec persistence', () => { }); }); - describe('when starting a new node and PXE without previous data', () => { - // increase timeout since this would require a full node sync + describe.each([ + [ + // ie. I'm setting up a new full node, sync from scratch and restore wallets/notes + 'when starting the Node and PXE with empty databases', + () => setup(0, { deployL1ContractsValues }, {}), + 10_000, + ], + [ + // ie. I'm setting up a new PXE, restore wallets/notes from a Node + 'when starting a PXE with an empty database connected to a Node with an existing database', + () => setup(0, { dataDirectory, deployL1ContractsValues }, {}), + 10_000, + ], + ])('%s', (_, contextSetup, timeout) => { beforeEach(async () => { - context = await setup(0, { deployL1ContractsValues }, {}); - }, 50_000); - + context = await contextSetup(); + }, timeout); afterEach(async () => { await context.teardown(); }); @@ -135,101 +174,11 @@ describe('Aztec persistence', () => { await context.pxe.registerAccount(ownerPrivateKey, ownerAddress.partialAddress); const ownerWallet = await getUnsafeSchnorrAccount(context.pxe, ownerPrivateKey, ownerAddress).getWallet(); const contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet); - await waitForAccountSynch(context.pxe, ownerAddress, { interval: 1, timeout: 10 }); - await expect(contract.methods.getBalance(ownerAddress.address).view()).resolves.toEqual(1500n); - }); - }); - - describe('when connecting a PXE to a new node', () => { - beforeEach(async () => { - context = await setup(0, { deployL1ContractsValues }, { dataDirectory }); - - ownerWallet = await getUnsafeSchnorrWallet(context.pxe, ownerAddress.address, ownerPrivateKey); - contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet); - }, 50_000); - afterEach(async () => { - await context.teardown(); - }); - - it('correctly restores balances', async () => { - await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(1500n); - }); - - it('tracks new notes for the owner', async () => { - await contract.methods.mint(1000n, ownerWallet.getAddress()).send().wait(); - await expect(contract.methods.getBalance(ownerWallet.getAddress()).view()).resolves.toEqual(2500n); - }); - - it('allows transfers of tokens from owner', async () => { - const targetWallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); - - await contract.methods.transfer(500n, ownerWallet.getAddress(), targetWallet.getAddress()).send().wait(); - - const [ownerBalance, targetBalance] = await Promise.all([ - contract.methods.getBalance(ownerWallet.getAddress()).view(), - contract.methods.getBalance(targetWallet.getAddress()).view(), - ]); - - expect(ownerBalance).toEqual(2000n); - expect(targetBalance).toEqual(500n); - }, 25_000); - }); - - describe('when connecting a new PXE to a node with synced data', () => { - beforeEach(async () => { - context = await setup(0, { dataDirectory, deployL1ContractsValues }, {}); - }); - - afterEach(async () => { - await context.teardown(); - }); - - it('does not restore previous accounts', async () => { - await expect(context.pxe.getRecipient(ownerAddress.address)).resolves.toBeUndefined(); - }); - - it('the node has the contract', async () => { - await expect(context.aztecNode.getContractData(contractAddress.address)).resolves.toBeDefined(); - }); - - it('pxe does not know of the deployed contract', async () => { - await context.pxe.registerRecipient(ownerAddress); - - const wallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); - const contract = await EasyPrivateTokenContract.at(contractAddress.address, wallet); - await expect(contract.methods.getBalance(ownerAddress.address).view()).rejects.toThrowError(/Unknown contract/); - }); - - it("pxe does not have owner's notes", async () => { - await context.pxe.addContracts([ - { - artifact: EasyPrivateTokenContract.artifact, - completeAddress: contractAddress, - portalContract: EthAddress.ZERO, - }, - ]); - await context.pxe.registerRecipient(ownerAddress); - - const wallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy(); - const contract = await EasyPrivateTokenContract.at(contractAddress.address, wallet); - await expect(contract.methods.getBalance(ownerAddress.address).view()).resolves.toEqual(0n); - }); - - it('pxe restores notes after registering the owner', async () => { - await context.pxe.addContracts([ - { - artifact: EasyPrivateTokenContract.artifact, - completeAddress: contractAddress, - portalContract: EthAddress.ZERO, - }, - ]); - - await context.pxe.registerAccount(ownerPrivateKey, ownerAddress.partialAddress); - const ownerWallet = await getUnsafeSchnorrAccount(context.pxe, ownerPrivateKey, ownerAddress).getWallet(); - const contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet); await waitForAccountSynch(context.pxe, ownerAddress, { interval: 1, timeout: 10 }); - await expect(contract.methods.getBalance(ownerAddress.address).view()).resolves.toEqual(2000n); + + // check that notes total more than 0 so that this test isn't dependent on run order + await expect(contract.methods.getBalance(ownerAddress.address).view()).resolves.toBeGreaterThan(0n); }); }); }); From 434fed6c67936fef4b5795ae977816f085f7f816 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 10 Jan 2024 10:03:01 +0000 Subject: [PATCH 10/12] fix: make pxe.sendTx more lenient about running --- yarn-project/pxe/src/pxe_service/pxe_service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index f97f00aad883..c7dd25a5792b 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -383,10 +383,6 @@ export class PXEService implements PXE { } public async sendTx(tx: Tx): Promise { - if (!this.running) { - throw new Error('PXE Service is not running'); - } - const txHash = await tx.getTxHash(); if (await this.node.getTx(txHash)) { throw new Error(`A settled tx with equal hash ${txHash.toString()} exists.`); From 6c88858e51df23a44cad09cb4e9c7befb8d90785 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 10 Jan 2024 12:51:05 +0000 Subject: [PATCH 11/12] refactor: ternary over if/else + assignment Co-authored-by: Santiago Palladino --- yarn-project/end-to-end/src/fixtures/utils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 700af91b8a8d..364e22393f18 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -284,12 +284,7 @@ export async function setup( return await setupWithRemoteEnvironment(hdAccount, config, logger, numberOfAccounts); } - let deployL1ContractsValues: DeployL1Contracts; - if (!opts.deployL1ContractsValues) { - deployL1ContractsValues = await setupL1Contracts(config.rpcUrl, hdAccount, logger); - } else { - deployL1ContractsValues = opts.deployL1ContractsValues; - } + const deployL1ContractsValues = opts.deployL1ContractsValues ?? await setupL1Contracts(config.rpcUrl, hdAccount, logger); config.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; config.l1Contracts.rollupAddress = deployL1ContractsValues.l1ContractAddresses.rollupAddress; From 541e92f83253ead84de63195e3afca53b1d46da8 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 10 Jan 2024 13:13:17 +0000 Subject: [PATCH 12/12] chore: fix formatting --- yarn-project/end-to-end/src/fixtures/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 364e22393f18..9121ca0e114c 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -284,7 +284,8 @@ export async function setup( return await setupWithRemoteEnvironment(hdAccount, config, logger, numberOfAccounts); } - const deployL1ContractsValues = opts.deployL1ContractsValues ?? await setupL1Contracts(config.rpcUrl, hdAccount, logger); + const deployL1ContractsValues = + opts.deployL1ContractsValues ?? (await setupL1Contracts(config.rpcUrl, hdAccount, logger)); config.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; config.l1Contracts.rollupAddress = deployL1ContractsValues.l1ContractAddresses.rollupAddress;