Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: persistence uses TokenContract #3930

Merged
merged 5 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion yarn-project/aztec.js/src/account_manager/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PublicKey, getContractDeploymentInfo } from '@aztec/circuits.js';
import { EthAddress, PublicKey, getContractDeploymentInfo } from '@aztec/circuits.js';
import { Fr } from '@aztec/foundation/fields';
import { CompleteAddress, GrumpkinPrivateKey, PXE } from '@aztec/types';

Expand Down Expand Up @@ -92,6 +92,15 @@ export class AccountManager {
*/
public async register(opts: WaitOpts = DefaultWaitOpts): Promise<AccountWalletWithPrivateKey> {
const address = await this.#register();

await this.pxe.addContracts([
{
artifact: this.accountContract.getContractArtifact(),
completeAddress: address,
portalContract: EthAddress.ZERO,
},
]);
Comment on lines +96 to +102
Copy link
Contributor Author

@alexghr alexghr Jan 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the best place to put it but what this is trying to solve is: adding an existing account, that's been deployed through PXE A, to a new PXE, B and calling contracts using this new PXE instance, B.

I'm not a fan of the random portalContract: EthAddress.ZERO in here 😟

LMK if there's a better test utility to register an existing account with a new PXE

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! I doubt account contracts will have a corresponding portal, so I think this is ok.


await waitForAccountSynch(this.pxe, address, opts);
return this.getWallet();
}
Expand Down
218 changes: 190 additions & 28 deletions yarn-project/end-to-end/src/e2e_persistence.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { getUnsafeSchnorrAccount, getUnsafeSchnorrWallet } from '@aztec/accounts/single_key';
import { AccountWallet, waitForAccountSynch } from '@aztec/aztec.js';
import {
AccountWallet,
ExtendedNote,
Note,
TxHash,
computeMessageSecretHash,
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';
import { TokenContract } from '@aztec/noir-contracts/Token';

import { mkdtemp } from 'fs/promises';
import { tmpdir } from 'os';
Expand All @@ -14,13 +21,14 @@ describe('Aztec persistence', () => {
/**
* These tests check that the Aztec Node and PXE can be shutdown and restarted without losing data.
*
* There are four scenarios to check:
* There are five 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
* 5. Node and PXE are started with existing databases, but the chain has advanced since they were shutdown
*
* All four scenarios use the same L1 state, which is deployed in the `beforeAll` hook.
* All five scenarios use the same L1 state, which is deployed in the `beforeAll` hook.
*/

// the test contract and account deploying it
Expand Down Expand Up @@ -48,12 +56,30 @@ describe('Aztec persistence', () => {
const ownerWallet = await getUnsafeSchnorrAccount(initialContext.pxe, ownerPrivateKey, Fr.ZERO).waitDeploy();
ownerAddress = ownerWallet.getCompleteAddress();

const deployer = EasyPrivateTokenContract.deploy(ownerWallet, 1000n, ownerWallet.getAddress());
const deployer = TokenContract.deploy(ownerWallet, ownerWallet.getAddress(), 'Test token', 'TEST', 2);
await deployer.simulate({});

const contract = await deployer.send().deployed();
contractAddress = contract.completeAddress;

const secret = Fr.random();

const mintTx = contract.methods.mint_private(1000n, computeMessageSecretHash(secret));
await mintTx.simulate();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that you don't need to explicitly call simulate, just doing send will take care of it.

const mintTxReceipt = await mintTx.send().wait();

await addPendingShieldNoteToPXE(
ownerWallet,
contractAddress,
1000n,
computeMessageSecretHash(secret),
mintTxReceipt.txHash,
);

const redeemTx = contract.methods.redeem_shield(ownerAddress.address, 1000n, secret);
await redeemTx.simulate();
await redeemTx.send().wait();

await initialContext.teardown();
}, 100_000);

Expand All @@ -72,37 +98,57 @@ describe('Aztec persistence', () => {
],
])('%s', (_, contextSetup, timeout) => {
let ownerWallet: AccountWallet;
let contract: EasyPrivateTokenContract;
let contract: TokenContract;

beforeEach(async () => {
context = await contextSetup();
ownerWallet = await getUnsafeSchnorrWallet(context.pxe, ownerAddress.address, ownerPrivateKey);
contract = await EasyPrivateTokenContract.at(contractAddress.address, ownerWallet);
contract = await TokenContract.at(contractAddress.address, ownerWallet);
}, timeout);

afterEach(async () => {
await context.teardown();
});

it('correctly restores balances', async () => {
it('correctly restores private notes', 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);
await expect(contract.methods.balance_of_private(ownerWallet.getAddress()).view()).resolves.toBeGreaterThan(0n);
});

it('correctly restores public storage', async () => {
await expect(contract.methods.total_supply().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);
const balance = await contract.methods.balance_of_private(ownerWallet.getAddress()).view();

const secret = Fr.random();
const mintTxReceipt = await contract.methods.mint_private(1000n, computeMessageSecretHash(secret)).send().wait();
await addPendingShieldNoteToPXE(
ownerWallet,
contractAddress,
1000n,
computeMessageSecretHash(secret),
mintTxReceipt.txHash,
);

await contract.methods.redeem_shield(ownerWallet.getAddress(), 1000n, secret).send().wait();

await expect(contract.methods.balance_of_private(ownerWallet.getAddress()).view()).resolves.toEqual(
balance + 1000n,
);
});

it('allows transfers of tokens from owner', async () => {
it('allows spending of private notes', async () => {
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(), otherWallet.getAddress()).send().wait();
const initialOwnerBalance = await contract.methods.balance_of_private(ownerWallet.getAddress()).view();

await contract.methods.transfer(ownerWallet.getAddress(), otherWallet.getAddress(), 500n, Fr.ZERO).send().wait();

const [ownerBalance, targetBalance] = await Promise.all([
contract.methods.getBalance(ownerWallet.getAddress()).view(),
contract.methods.getBalance(otherWallet.getAddress()).view(),
contract.methods.balance_of_private(ownerWallet.getAddress()).view(),
contract.methods.balance_of_private(otherWallet.getAddress()).view(),
]);

expect(ownerBalance).toEqual(initialOwnerBalance - 500n);
Expand Down Expand Up @@ -143,42 +189,158 @@ describe('Aztec persistence', () => {
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/);
const contract = await TokenContract.at(contractAddress.address, wallet);
await expect(contract.methods.balance_of_private(ownerAddress.address).view()).rejects.toThrowError(
/Unknown contract/,
);
});

it("pxe does not have owner's notes", async () => {
it("pxe does not have owner's private notes", async () => {
await context.pxe.addContracts([
{
artifact: EasyPrivateTokenContract.artifact,
artifact: TokenContract.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);
const contract = await TokenContract.at(contractAddress.address, wallet);
await expect(contract.methods.balance_of_private(ownerAddress.address).view()).resolves.toEqual(0n);
});

it('has access to public storage', async () => {
await context.pxe.addContracts([
{
artifact: TokenContract.artifact,
completeAddress: contractAddress,
portalContract: EthAddress.ZERO,
},
]);

const wallet = await getUnsafeSchnorrAccount(context.pxe, Fq.random(), Fr.ZERO).waitDeploy();
const contract = await TokenContract.at(contractAddress.address, wallet);

await expect(contract.methods.total_supply().view()).resolves.toBeGreaterThan(0n);
});

it('pxe restores notes after registering the owner', async () => {
await context.pxe.addContracts([
{
artifact: EasyPrivateTokenContract.artifact,
artifact: TokenContract.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);
const ownerAccount = getUnsafeSchnorrAccount(context.pxe, ownerPrivateKey, ownerAddress);
await ownerAccount.register();
const ownerWallet = await ownerAccount.getWallet();
const contract = await TokenContract.at(contractAddress.address, ownerWallet);

await waitForAccountSynch(context.pxe, ownerAddress, { interval: 1, timeout: 10 });

// 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);
await expect(contract.methods.balance_of_private(ownerAddress.address).view()).resolves.toBeGreaterThan(0n);
});
});

describe('when starting Node and PXE with existing databases, but chain has advanced since they were shutdown', () => {
let secret: Fr;
let mintTxHash: TxHash;
let mintAmount: bigint;
let revealedAmount: bigint;

// The test system is shutdown. Its state is saved to disk
// Start a temporary node and PXE, synch it and add the contract and account to it.
// Perform some actions with these temporary components to advance the chain
// Then shutdown the temporary components and restart the original components
// They should sync up from where they left off and be able to see the actions performed by the temporary node & PXE.
beforeAll(async () => {
const temporaryContext = await setup(0, { deployL1ContractsValues }, {});

await temporaryContext.pxe.addContracts([
{
artifact: TokenContract.artifact,
completeAddress: contractAddress,
portalContract: EthAddress.ZERO,
},
]);

const ownerAccount = getUnsafeSchnorrAccount(temporaryContext.pxe, ownerPrivateKey, ownerAddress);
await ownerAccount.register();
const ownerWallet = await ownerAccount.getWallet();

const contract = await TokenContract.at(contractAddress.address, ownerWallet);

// mint some tokens with a secret we know and redeem later on a separate PXE
secret = Fr.random();
mintAmount = 1000n;
const mintTxReceipt = await contract.methods
.mint_private(mintAmount, computeMessageSecretHash(secret))
.send()
.wait();
mintTxHash = mintTxReceipt.txHash;

// publicly reveal that I have 1000 tokens
revealedAmount = 1000n;
await contract.methods.unshield(ownerAddress, ownerAddress, revealedAmount, 0).send().wait();

// shut everything down
await temporaryContext.teardown();
}, 100_000);

let ownerWallet: AccountWallet;
let contract: TokenContract;

beforeEach(async () => {
context = await setup(0, { dataDirectory, deployL1ContractsValues }, { dataDirectory });
ownerWallet = await getUnsafeSchnorrWallet(context.pxe, ownerAddress.address, ownerPrivateKey);
contract = await TokenContract.at(contractAddress.address, ownerWallet);

await waitForAccountSynch(context.pxe, ownerAddress, { interval: 0.1, timeout: 5 });
}, 5000);

afterEach(async () => {
await context.teardown();
});

it("restores owner's public balance", async () => {
await expect(contract.methods.balance_of_public(ownerAddress.address).view()).resolves.toEqual(revealedAmount);
});

it('allows consuming transparent note created on another PXE', async () => {
// this was created in the temporary PXE in `beforeAll`
await addPendingShieldNoteToPXE(
ownerWallet,
contractAddress,
mintAmount,
computeMessageSecretHash(secret),
mintTxHash,
);

const balanceBeforeRedeem = await contract.methods.balance_of_private(ownerWallet.getAddress()).view();

await contract.methods.redeem_shield(ownerWallet.getAddress(), mintAmount, secret).send().wait();
const balanceAfterRedeem = await contract.methods.balance_of_private(ownerWallet.getAddress()).view();

expect(balanceAfterRedeem).toEqual(balanceBeforeRedeem + mintAmount);
});
});
});

async function addPendingShieldNoteToPXE(
wallet: AccountWallet,
asset: CompleteAddress,
amount: bigint,
secretHash: Fr,
txHash: TxHash,
) {
// The storage slot of `pending_shields` is 5.
// TODO AlexG, this feels brittle
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It most definitely is, but we don't have a better way around it yet. Maybe we could extract it to a shared function, but that's about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be cool if we extracted storage slots from the Noir code and made them available on the Contract interface. Something like this would be really nice to use

async function addPendingShieldNoteToPXE(
  wallet: AccountWallet,
  asset: TokenContract,
  amount: bigint,
  secretHash: Fr,
  txHash: TxHash,
) {
  const note = new Note([new Fr(amount), secretHash]);
  const extendedNote = new ExtendedNote(note, wallet.getAddress(), contract.address, contract.storageSlots.pendingShields, txHash);
  await wallet.addNote(extendedNote);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to #2806. Having a mapping of a contract storage available has popped up multiple times.

const storageSlot = new Fr(5);
const note = new Note([new Fr(amount), secretHash]);
const extendedNote = new ExtendedNote(note, wallet.getAddress(), asset.address, storageSlot, txHash);
await wallet.addNote(extendedNote);
}