From 3d66aa63fe63705121145bb07e3783e9e9f99540 Mon Sep 17 00:00:00 2001 From: xorsal Date: Fri, 7 Feb 2025 16:24:38 -0300 Subject: [PATCH] test: dual pxe tests (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To run these tests you need to run two separate of the aztec sandboxes - First you need to run the sandbox normally `aztec start --sandbox` - Then you need to run a PXE instance pointing to the previously started sandbox: `aztec start --port 8081 --pxe --pxe.nodeUrl=http://host.docker.internal:8080` Currently blocked because although I've sent the tokens to the escrow contract I'm unable to withdraw from it. ## Summary by CodeRabbit - **New Features** - Launched an additional container service with configurable settings for improved resilience. - **Tests** - Expanded validation scenarios for token exchanges and escrow operations across multiple instances. - **Chores** - Streamlined the automated workflow to ensure clearer separation of environment setup and testing, with a defined job timeout for enhanced process efficiency. --------- Co-authored-by: Weißer Hase --- .github/workflows/tests.yaml | 25 +- docker-compose.override.yml | 18 ++ src/escrow_contract/src/test/escrow.test.ts | 229 +++++++++++++++ .../src/test/{main.test.ts => token.test.ts} | 261 +++++++++++++++--- 4 files changed, 490 insertions(+), 43 deletions(-) create mode 100644 docker-compose.override.yml create mode 100644 src/escrow_contract/src/test/escrow.test.ts rename src/token_contract/src/test/{main.test.ts => token.test.ts} (61%) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f77aef3..71de830 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -3,13 +3,11 @@ on: branches: - main pull_request: - branches: - - main - - dev - + jobs: setup-and-run: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout repository @@ -26,11 +24,14 @@ jobs: - name: Update path run: echo "/home/runner/.aztec/bin" >> $GITHUB_PATH - - name: Set Aztec version and start sandbox + - name: Set Aztec version run: | VERSION=0.72.1 aztec-up - aztec start --sandbox & - + + - name: Start sandbox + run: | + docker compose -p sandbox -f ~/.aztec/docker-compose.sandbox.yml -f docker-compose.override.yml up & + - name: Install project dependencies run: yarn @@ -39,7 +40,11 @@ jobs: - name: Codegen run: script -e -c "aztec codegen target --outdir src/artifacts" + + - name: Run js tests + run: script -e -c "BASE_PXE_URL=http://localhost NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --config jest.integration.config.json" - - name: Run tests - run: script -e -c "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --config jest.integration.config.json && aztec test" - \ No newline at end of file + - name: Run nr tests + run: | + script -e -c "aztec test" + diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..4ca531f --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,18 @@ +services: + alternative-pxe: + image: "aztecprotocol/aztec:latest" + ports: + - "${PXE_PORT:-8081}:${PXE_PORT:-8081}" + environment: + LOG_LEVEL: '${LOG_LEVEL:-info; verbose: simulator:avm:debug_log}' + HOST_WORKDIR: "${PWD}" + VERSION: latest + volumes: + - ./pxe/log:/usr/src/yarn-project/aztec/log:rw + # TODO: Use `condition` to start only after the sandbox is healthy + depends_on: + - ethereum + - aztec + command: "start --port 8081 --pxe --pxe.nodeUrl=http://aztec:8080" + # TODO: If started at the same time it usually takes three attempts to connect to the sandbox's node + restart: unless-stopped diff --git a/src/escrow_contract/src/test/escrow.test.ts b/src/escrow_contract/src/test/escrow.test.ts new file mode 100644 index 0000000..9577d23 --- /dev/null +++ b/src/escrow_contract/src/test/escrow.test.ts @@ -0,0 +1,229 @@ +import { TokenContractArtifact, TokenContract, Transfer } from '../../../artifacts/Token.js'; +import { + AccountWallet, + createLogger, + Fr, + PXE, + waitForPXE, + TxStatus, + createPXEClient, + getContractInstanceFromDeployParams, + Logger, + Contract, + AztecAddress, + AccountWalletWithSecretKey, + Wallet, + UniqueNote, +} from '@aztec/aztec.js'; +import { createAccount } from '@aztec/accounts/testing'; +import { computePartialAddress, deriveKeys } from '@aztec/circuits.js'; +import { EscrowContract, EscrowContractArtifact } from '@aztec/noir-contracts.js/Escrow'; + +const createPXE = async (id: number = 0) => { + // TODO: we should probably define testing fixtures for this kind of configuration + const { BASE_PXE_URL = `http://localhost` } = process.env; + const url = `${BASE_PXE_URL}:${8080 + id}`; + const pxe = createPXEClient(url); + await waitForPXE(pxe); + return pxe; +}; + +const setupSandbox = async () => { + return createPXE(); +}; + +async function deployToken(deployer: AccountWallet, minter: AztecAddress) { + const contract = await Contract.deploy(deployer, TokenContractArtifact, [minter, 'PrivateToken', 'PT', 18]) + .send() + .deployed(); + console.log('Token contract deployed at', contract.address); + return contract; +} + +async function deployEscrow(pxes: PXE[], wallet: Wallet, owner: AztecAddress) { + const escrowSecretKey = Fr.random(); + const escrowPublicKeys = (await deriveKeys(escrowSecretKey)).publicKeys; + const escrowDeployment = EscrowContract.deployWithPublicKeys(escrowPublicKeys, wallet, owner); + const escrowInstance = await escrowDeployment.getInstance(); + + await Promise.all( + pxes.map(async (pxe) => pxe.registerAccount(escrowSecretKey, await computePartialAddress(escrowInstance))), + ); + + const escrowContract = await escrowDeployment.send().deployed(); + console.log(`Escrow contract deployed at ${escrowContract.address}`); + + return escrowContract; +} + +describe('Multi PXE', () => { + let alicePXE: PXE; + let bobPXE: PXE; + + let aliceWallet: AccountWalletWithSecretKey; + let bobWallet: AccountWalletWithSecretKey; + + let alice: AccountWallet; + let bob: AccountWallet; + let carl: AccountWallet; + + let token: TokenContract; + let escrow: EscrowContract; + const AMOUNT = 1000n; + + let logger: Logger; + + beforeAll(async () => { + logger = createLogger('aztec:aztec-starter'); + logger.info('Aztec-Starter tests running.'); + + alicePXE = await createPXE(0); + bobPXE = await createPXE(1); + + // TODO: assert that the used PXEs are actually separate instances? + + aliceWallet = await createAccount(alicePXE); + bobWallet = await createAccount(bobPXE); + + alice = aliceWallet; + bob = bobWallet; + console.log({ + alice: aliceWallet.getAddress(), + bob: bobWallet.getAddress(), + }); + }); + + beforeEach(async () => { + token = (await deployToken(alice, alice.getAddress())) as TokenContract; + + await bobPXE.registerContract(token); + + escrow = await deployEscrow([alicePXE, bobPXE], alice, bob.getAddress()); + await bobPXE.registerContract({ + instance: escrow.instance, + artifact: EscrowContractArtifact, + }); + await alicePXE.registerContract({ + instance: escrow.instance, + artifact: EscrowContractArtifact, + }); + + // alice knows bob + await alicePXE.registerAccount(bobWallet.getSecretKey(), bob.getCompleteAddress().partialAddress); + alicePXE.registerSender(bob.getAddress()); + alice.setScopes([ + alice.getAddress(), + bob.getAddress(), + // token.address, + ]); + // bob knows alice + await bobPXE.registerAccount(aliceWallet.getSecretKey(), alice.getCompleteAddress().partialAddress); + bobPXE.registerSender(alice.getAddress()); + + bob.setScopes([ + bob.getAddress(), + alice.getAddress(), + // token.address + escrow.address, + ]); + }); + + const expectAddressNote = (note: UniqueNote, address: AztecAddress, owner: AztecAddress) => { + logger.info('checking address note {} {}', [address, owner]); + expect(note.note.items[0]).toEqual(new Fr(address.toBigInt())); + expect(note.note.items[1]).toEqual(new Fr(owner.toBigInt())); + }; + + const expectNote = (note: UniqueNote, amount: bigint, owner: AztecAddress) => { + // 4th element of items is randomness, so we slice the first 3 + // dev: why the second element is always 0? + expect(note.note.items.slice(0, 3)).toStrictEqual([new Fr(amount), new Fr(0), new Fr(owner.toBigInt())]); + }; + + const expectBalances = async (address: AztecAddress, publicBalance: bigint, privateBalance: bigint) => { + logger.info('checking balances for', address.toString()); + expect(await token.methods.balance_of_public(address).simulate()).toBe(publicBalance); + expect(await token.methods.balance_of_private(address).simulate()).toBe(privateBalance); + }; + + const wad = (n: number = 1) => AMOUNT * BigInt(n); + + it('escrow', async () => { + let events, notes; + + // this is here because the note is created in the constructor + await escrow.withWallet(alice).methods.sync_notes().simulate({}); + await escrow.withWallet(bob).methods.sync_notes().simulate({}); + + // alice should have no notes (But it has because I gave it access to Bob's notes) + notes = await alice.getNotes({ contractAddress: escrow.address }); + expect(notes.length).toBe(1); + expectAddressNote(notes[0], bob.getAddress(), bob.getAddress()); + + // bob should have a note with himself as owner, encrypted by alice + notes = await bob.getNotes({ contractAddress: escrow.address }); + expect(notes.length).toBe(1); + expectAddressNote(notes[0], bob.getAddress(), bob.getAddress()); + + // mint initial amount + await token.withWallet(alice).methods.mint_to_public(alice.getAddress(), wad(10)).send().wait(); + + await token.withWallet(alice).methods.transfer_to_private(alice.getAddress(), wad(5)).send().wait(); + await token.withWallet(alice).methods.sync_notes().simulate({}); + + // assert balances + await expectBalances(alice.getAddress(), wad(5), wad(5)); + await expectBalances(bob.getAddress(), wad(0), wad(0)); + + // Transfer both in private and public + const fundEscrowTx = await token + .withWallet(alice) + .methods.transfer_in_private(alice.getAddress(), escrow.address, wad(5), 0) + .send() + .wait({ + debug: true, + }); + + const fundEscrowTx2 = await token + .withWallet(alice) + .methods.transfer_in_public(alice.getAddress(), escrow.address, wad(5), 0) + .send() + .wait({ + debug: true, + }); + + await token.withWallet(alice).methods.sync_notes().simulate({}); + + // assert balances, alice 0 and 0, escrow 5 and 5 + await expectBalances(alice.getAddress(), wad(0), wad(0)); + await expectBalances(escrow.address, wad(5), wad(5)); + + // alice should have a note with escrow as owner (why alice can see the escrow's note?) + notes = await alice.getNotes({ contractAddress: token.address }); + expect(notes.length).toBe(1); + expectNote(notes[0], wad(5), escrow.address); + + await escrow.withWallet(alice).methods.sync_notes().simulate({}); + await escrow.withWallet(bob).methods.sync_notes().simulate({}); + + // Q: why only alice can see the escrow's notes if both have the escrow registered? + notes = await alice.getNotes({ owner: escrow.address }); + expect(notes.length).toBe(1); + expectNote(notes[0], wad(5), escrow.address); + + notes = await bob.getNotes({ owner: escrow.address }); + expect(notes.length).toBe(0); + + // withdraw 1 from the escrow + const withdrawTx = await escrow + .withWallet(bob) + .methods.withdraw(token.address, wad(1), bob.getAddress()) + .send() + .wait({ + debug: true, + }); + + await expectBalances(escrow.address, wad(5), wad(4)); + await expectBalances(bob.getAddress(), wad(0), wad(1)); + }, 300_000); +}); diff --git a/src/token_contract/src/test/main.test.ts b/src/token_contract/src/test/token.test.ts similarity index 61% rename from src/token_contract/src/test/main.test.ts rename to src/token_contract/src/test/token.test.ts index 6ad63b2..b12190b 100644 --- a/src/token_contract/src/test/main.test.ts +++ b/src/token_contract/src/test/token.test.ts @@ -1,4 +1,4 @@ -import { TokenContractArtifact, TokenContract } from '../../../artifacts/Token.js'; +import { TokenContractArtifact, TokenContract, Transfer } from '../../../artifacts/Token.js'; import { AccountWallet, CompleteAddress, @@ -12,19 +12,61 @@ import { getContractInstanceFromDeployParams, Logger, Contract, + AztecAddress, + AccountWalletWithSecretKey, + Wallet, + UniqueNote, } from '@aztec/aztec.js'; -import { getInitialTestAccountsWallets } from '@aztec/accounts/testing'; - -const setupSandbox = async () => { - const { PXE_URL = 'http://localhost:8080' } = process.env; - const pxe = createPXEClient(PXE_URL); +import { createAccount, getInitialTestAccountsWallets } from '@aztec/accounts/testing'; +import { + computePartialAddress, + deriveKeys, + deriveMasterIncomingViewingSecretKey, + derivePublicKeyFromSecretKey, +} from '@aztec/circuits.js'; +import { EscrowContract, EscrowContractArtifact } from '@aztec/noir-contracts.js/Escrow'; +import { ContractInstanceDeployerContract } from '@aztec/noir-contracts.js/ContractInstanceDeployer'; + +const createPXE = async (id: number = 0) => { + // TODO: we should probably define testing fixtures for this kind of configuration + const { BASE_PXE_URL = `http://localhost` } = process.env; + const url = `${BASE_PXE_URL}:${8080 + id}`; + const pxe = createPXEClient(url); await waitForPXE(pxe); return pxe; }; -describe('Token', () => { +const setupSandbox = async () => { + return createPXE(); +}; + +async function deployToken(deployer: AccountWallet, minter: AztecAddress) { + const contract = await Contract.deploy(deployer, TokenContractArtifact, [minter, 'PrivateToken', 'PT', 18]) + .send() + .deployed(); + console.log('Token contract deployed at', contract.address); + return contract; +} + +async function deployEscrow(pxes: PXE[], wallet: Wallet, owner: AztecAddress) { + const escrowSecretKey = Fr.random(); + const escrowPublicKeys = (await deriveKeys(escrowSecretKey)).publicKeys; + const escrowDeployment = EscrowContract.deployWithPublicKeys(escrowPublicKeys, wallet, owner); + const escrowInstance = await escrowDeployment.getInstance(); + + await Promise.all( + pxes.map(async (pxe) => pxe.registerAccount(escrowSecretKey, await computePartialAddress(escrowInstance))), + ); + + const escrowContract = await escrowDeployment.send().deployed(); + console.log(`Escrow contract deployed at ${escrowContract.address}`); + + return escrowContract; +} + +describe('Token - Single PXE', () => { let pxe: PXE; - let wallets: AccountWallet[] = []; + let wallets: AccountWalletWithSecretKey[] = []; let accounts: CompleteAddress[] = []; let alice: AccountWallet; @@ -52,7 +94,7 @@ describe('Token', () => { }); beforeEach(async () => { - token = (await deployToken()) as TokenContract; + token = (await deployToken(alice, alice.getAddress())) as TokenContract; }); it('deploys the contract', async () => { @@ -90,20 +132,6 @@ describe('Token', () => { expect(receiptAfterMined.contract.instance.address).toEqual(deploymentData.address); }, 300_000); - async function deployToken() { - const [deployerWallet] = wallets; // using first account as deployer - - const contract = await Contract.deploy(alice, TokenContractArtifact, [ - deployerWallet.getAddress(), - 'PrivateToken', - 'PT', - 18, - ]) - .send() - .deployed(); - return contract; - } - it('mints', async () => { await token.withWallet(alice); const tx = await token.methods.mint_to_public(bob.getAddress(), AMOUNT).send().wait(); @@ -320,20 +348,13 @@ describe('Token', () => { expect(await token.methods.total_supply().simulate()).toBe(AMOUNT); // alice prepares partial note for bob - await token.methods.prepare_private_balance_increase(bob.getAddress(), alice.getAddress()).send().wait(); + await token.methods.prepare_private_balance_increase(bob.getAddress(), alice.getAddress()).send().wait({ + debug: true, + }); // alice still has tokens in public expect(await token.methods.balance_of_public(alice.getAddress()).simulate()).toBe(AMOUNT); - // TODO: i removed the event, so I need anoter way to figure out the hiding point slot to finalize the note - // read bob's encrypted logs - // const bobEncryptedEvents = await bob.getPrivateEvents( - // TokenContract.events.PreparePrivateBalanceIncrease, - // 1, - // 100 // todo: add a default value for limit? - // ) - // get the latest event - // const latestEvent = bobEncryptedEvents[bobEncryptedEvents.length - 1] // finalize partial note passing the hiding point slot // await token.methods.finalize_transfer_to_private(AMOUNT, latestEvent.hiding_point_slot).send().wait(); @@ -405,3 +426,177 @@ describe('Token', () => { expect(await token.methods.balance_of_private(bob.getAddress()).simulate()).toBe(AMOUNT); }, 300_000); }); + +describe('Token - Multi PXE', () => { + let alicePXE: PXE; + let bobPXE: PXE; + + let aliceWallet: AccountWalletWithSecretKey; + let bobWallet: AccountWalletWithSecretKey; + + let alice: AccountWallet; + let bob: AccountWallet; + let carl: AccountWallet; + + let token: TokenContract; + let escrow: EscrowContract; + const AMOUNT = 1000n; + + let logger: Logger; + + beforeAll(async () => { + logger = createLogger('aztec:aztec-starter'); + logger.info('Aztec-Starter tests running.'); + + alicePXE = await createPXE(0); + bobPXE = await createPXE(1); + + // TODO: assert that the used PXEs are actually separate instances? + + aliceWallet = await createAccount(alicePXE); + bobWallet = await createAccount(bobPXE); + + alice = aliceWallet; + bob = bobWallet; + console.log({ + alice: aliceWallet.getAddress(), + bob: bobWallet.getAddress(), + }); + }); + + beforeEach(async () => { + token = (await deployToken(alice, alice.getAddress())) as TokenContract; + + await bobPXE.registerContract(token); + + escrow = await deployEscrow([alicePXE, bobPXE], alice, bob.getAddress()); + await bobPXE.registerContract({ + instance: escrow.instance, + artifact: EscrowContractArtifact, + }); + await alicePXE.registerContract({ + instance: escrow.instance, + artifact: EscrowContractArtifact, + }); + + // alice knows bob + await alicePXE.registerAccount(bobWallet.getSecretKey(), bob.getCompleteAddress().partialAddress); + alicePXE.registerSender(bob.getAddress()); + alice.setScopes([ + alice.getAddress(), + bob.getAddress(), + // token.address, + ]); + // bob knows alice + await bobPXE.registerAccount(aliceWallet.getSecretKey(), alice.getCompleteAddress().partialAddress); + bobPXE.registerSender(alice.getAddress()); + + bob.setScopes([ + bob.getAddress(), + alice.getAddress(), + // token.address + escrow.address, + ]); + }); + + const expectAddressNote = (note: UniqueNote, address: AztecAddress, owner: AztecAddress) => { + logger.info('checking address note {} {}', [address, owner]); + expect(note.note.items[0]).toEqual(new Fr(address.toBigInt())); + expect(note.note.items[1]).toEqual(new Fr(owner.toBigInt())); + }; + + const expectNote = (note: UniqueNote, amount: bigint, owner: AztecAddress) => { + // 4th element of items is randomness, so we slice the first 3 + // dev: why the second element is always 0? + expect(note.note.items.slice(0, 3)).toStrictEqual([new Fr(amount), new Fr(0), new Fr(owner.toBigInt())]); + }; + + const expectBalances = async (address: AztecAddress, publicBalance: bigint, privateBalance: bigint) => { + logger.info('checking balances for', address.toString()); + expect(await token.methods.balance_of_public(address).simulate()).toBe(publicBalance); + expect(await token.methods.balance_of_private(address).simulate()).toBe(privateBalance); + }; + + const wad = (n: number = 1) => AMOUNT * BigInt(n); + + it('transfers', async () => { + let events, notes; + + // mint initial amount to alice + await token.withWallet(alice).methods.mint_to_public(alice.getAddress(), wad(10)).send().wait(); + + // self-transfer 5 public tokens to private + const aliceShieldTx = await token + .withWallet(alice) + .methods.transfer_to_private(alice.getAddress(), wad(5)) + .send() + .wait(); + await token.methods.sync_notes().simulate({}); + + // assert balances + await expectBalances(alice.getAddress(), wad(5), wad(5)); + + // retrieve notes from last tx + notes = await alice.getNotes({ txHash: aliceShieldTx.txHash }); + expect(notes.length).toBe(1); + expectNote(notes[0], wad(5), alice.getAddress()); + + // `transfer_to_private` does not emit an event + events = await alice.getPrivateEvents(TokenContract.events.Transfer, aliceShieldTx.blockNumber!, 2); + expect(events.length).toBe(0); + + // transfer some private tokens to bob + const fundBobTx = await token.withWallet(alice).methods.transfer_to_private(bob.getAddress(), wad(5)).send().wait(); + + await token.withWallet(alice).methods.sync_notes().simulate({}); + await token.withWallet(bob).methods.sync_notes().simulate({}); + + notes = await alice.getNotes({ txHash: fundBobTx.txHash }); + expect(notes.length).toBe(1); + expectNote(notes[0], wad(5), bob.getAddress()); + + notes = await bob.getNotes({ txHash: fundBobTx.txHash }); + expect(notes.length).toBe(1); + expectNote(notes[0], wad(5), bob.getAddress()); + + events = await bob.getPrivateEvents(TokenContract.events.Transfer, fundBobTx.blockNumber!, 2); + expect(events.length).toBe(0); + + // fund bob again + const fundBobTx2 = await token.withWallet(alice).methods.transfer(bob.getAddress(), wad(5)).send().wait({ + debug: true, + }); + + await token.withWallet(alice).methods.sync_notes().simulate({}); + await token.withWallet(bob).methods.sync_notes().simulate({}); + + // assert balances + await expectBalances(alice.getAddress(), wad(0), wad(0)); + await expectBalances(bob.getAddress(), wad(0), wad(10)); + + // Alice shouldn't have any notes because it not a sender/registered account in her PXE + // (but she has because I gave her access to Bob's notes) + notes = await alice.getNotes({ txHash: fundBobTx2.txHash }); + expect(notes.length).toBe(1); + expectNote(notes[0], wad(5), bob.getAddress()); + + // Bob should have a note with himself as owner + // TODO: why noteTypeId is always `Selector<0x00000000>`? + notes = await bob.getNotes({ txHash: fundBobTx2.txHash }); + expect(notes.length).toBe(1); + expectNote(notes[0], wad(5), bob.getAddress()); + + events = await bob.getPrivateEvents(TokenContract.events.Transfer, fundBobTx2.blockNumber!, 2); + expect(events.length).toBe(1); + expect(events[0]).toEqual({ + from: alice.getAddress().toBigInt(), + to: bob.getAddress().toBigInt(), + amount: wad(5), + }); + + // assert alice's balances again + await expectBalances(alice.getAddress(), wad(0), wad(0)); + // assert bob's balances + await expectBalances(bob.getAddress(), wad(0), wad(10)); + }, 300_000); +});