Skip to content

Commit

Permalink
feat: Public view functions (unconstrained can read public storage) (#…
Browse files Browse the repository at this point in the history
…1421)

Addresses #1375 by giving the unconstrained execution access to public
storage through the node.

Good things: 
- Makes it simpler to read values

Bad things:
- Because the simulator is much slower than reading the value, makes
testing slower 😭.
  • Loading branch information
LHerskind authored Aug 7, 2023
1 parent fe41757 commit 912c1b4
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 46 deletions.
6 changes: 4 additions & 2 deletions yarn-project/acir-simulator/src/client/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { FunctionCall, TxExecutionRequest } from '@aztec/types';
import { AztecNode, FunctionCall, TxExecutionRequest } from '@aztec/types';

import { PackedArgsCache } from '../packed_args_cache.js';
import { ClientTxExecutionContext } from './client_execution_context.js';
Expand Down Expand Up @@ -92,6 +92,7 @@ export class AcirSimulator {
* @param contractAddress - The address of the contract.
* @param portalContractAddress - The address of the portal contract.
* @param historicRoots - The historic roots.
* @param aztecNode - The AztecNode instance.
* @returns The return values of the function.
*/
public async runUnconstrained(
Expand All @@ -101,6 +102,7 @@ export class AcirSimulator {
contractAddress: AztecAddress,
portalContractAddress: EthAddress,
historicRoots: PrivateHistoricTreeRoots,
aztecNode?: AztecNode,
) {
if (entryPointABI.functionType !== FunctionType.UNCONSTRAINED) {
throw new Error(`Cannot run ${entryPointABI.functionType} function as constrained`);
Expand Down Expand Up @@ -129,7 +131,7 @@ export class AcirSimulator {
callContext,
);

return execution.run();
return execution.run(aztecNode);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FunctionAbi, decodeReturnValues } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { Fr } from '@aztec/foundation/fields';
import { createDebugLogger } from '@aztec/foundation/log';
import { AztecNode } from '@aztec/types';

import { extractReturnWitness, frToAztecAddress } from '../acvm/deserialize.js';
import { ACVMField, ZERO_ACVM_FIELD, acvm, fromACVMField, toACVMField, toACVMWitness } from '../acvm/index.js';
Expand All @@ -26,9 +27,10 @@ export class UnconstrainedFunctionExecution {

/**
* Executes the unconstrained function.
* @param aztecNode - The aztec node.
* @returns The return values of the executed function.
*/
public async run(): Promise<any[]> {
public async run(aztecNode?: AztecNode): Promise<any[]> {
this.log(
`Executing unconstrained function ${this.contractAddress.toShortString()}:${this.functionData.functionSelectorBuffer.toString(
'hex',
Expand All @@ -54,6 +56,33 @@ export class UnconstrainedFunctionExecution {
},
getL1ToL2Message: ([msgKey]) => this.context.getL1ToL2Message(fromACVMField(msgKey)),
getCommitment: ([commitment]) => this.context.getCommitment(this.contractAddress, commitment),
storageRead: async ([slot], [numberOfElements]) => {
if (!aztecNode) {
const errMsg = `Aztec node is undefined, cannot read storage`;
this.log(errMsg);
throw new Error(errMsg);
}

const makeLogMsg = (slot: bigint, value: string) =>
`Oracle storage read: slot=${slot.toString()} value=${value}`;

const startStorageSlot = fromACVMField(slot);
const values = [];
for (let i = 0; i < Number(numberOfElements); i++) {
const storageSlot = startStorageSlot.value + BigInt(i);
const value = await aztecNode.getPublicStorageAt(this.contractAddress, storageSlot);
if (value === undefined) {
const logMsg = makeLogMsg(storageSlot, 'undefined');
this.log(logMsg);
throw new Error(logMsg);
}
const frValue = Fr.fromBuffer(value);
const logMsg = makeLogMsg(storageSlot, frValue.toString());
this.log(logMsg);
values.push(frValue);
}
return values.map(v => toACVMField(v));
},
});

const returnValues: ACVMField[] = extractReturnWitness(acir, partialWitness);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ export class AztecRPCServer implements AztecRPC {
contractAddress,
portalContract,
historicRoots,
this.node,
);
this.log('Unconstrained simulation completed!');

Expand Down
37 changes: 16 additions & 21 deletions yarn-project/end-to-end/src/e2e_lending_contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { AztecNodeService } from '@aztec/aztec-node';
import { AztecRPCServer } from '@aztec/aztec-rpc';
import { AztecAddress, Contract, Fr, Wallet } from '@aztec/aztec.js';
import { AztecAddress, Fr, Wallet } from '@aztec/aztec.js';
import { CircuitsWasm } from '@aztec/circuits.js';
import { pedersenPlookupCommitInputs } from '@aztec/circuits.js/barretenberg';
import { DebugLogger } from '@aztec/foundation/log';
import { LendingContract } from '@aztec/noir-contracts/types';
import { AztecRPC, TxStatus } from '@aztec/types';

import { CheatCodes } from './cheat_codes.js';
import { setup } from './fixtures/utils.js';

describe('e2e_lending_contract', () => {
Expand All @@ -17,9 +16,7 @@ describe('e2e_lending_contract', () => {
let accounts: AztecAddress[];
let logger: DebugLogger;

let contract: Contract;

let cc: CheatCodes;
let contract: LendingContract;

const deployContract = async () => {
logger(`Deploying L2 public contract...`);
Expand All @@ -35,7 +32,7 @@ describe('e2e_lending_contract', () => {
};

beforeEach(async () => {
({ aztecNode, aztecRpcServer, wallet, accounts, logger, cheatCodes: cc } = await setup());
({ aztecNode, aztecRpcServer, wallet, accounts, logger } = await setup());
}, 100_000);

afterEach(async () => {
Expand All @@ -46,24 +43,22 @@ describe('e2e_lending_contract', () => {
});

// Fetch a storage snapshot from the contract that we can use to compare between transitions.
const getStorageSnapshot = async (contract: Contract, aztecNode: AztecRPC, account: Account) => {
const loadPublicStorageInMap = async (slot: Fr | bigint, key: Fr | bigint) => {
return await cc.l2.loadPublic(contract.address, cc.l2.computeSlotInMap(slot, key));
};
const getStorageSnapshot = async (contract: LendingContract, aztecNode: AztecRPC, account: Account) => {
const storageValues: { [key: string]: Fr } = {};
const accountKey = await account.key();
const toFields = (res: any[]) => res[0].map((v: number | bigint | Fr) => new Fr(v));

const storageValues: { [key: string]: any } = {};
{
const baseSlot = cc.l2.computeSlotInMap(1n, 0n);
storageValues['interestAccumulator'] = await cc.l2.loadPublic(contract.address, baseSlot);
storageValues['last_updated_ts'] = await cc.l2.loadPublic(contract.address, baseSlot.value + 1n);
}
[storageValues['interestAccumulator'], storageValues['last_updated_ts']] = toFields(
await contract.methods.getTot(0).view(),
);

const accountKey = await account.key();
[storageValues['private_collateral'], storageValues['private_debt']] = toFields(
await contract.methods.getPosition(accountKey).view(),
);

storageValues['private_collateral'] = await loadPublicStorageInMap(2n, accountKey);
storageValues['public_collateral'] = await loadPublicStorageInMap(2n, account.address.toField());
storageValues['private_debt'] = await loadPublicStorageInMap(3n, accountKey);
storageValues['public_debt'] = await loadPublicStorageInMap(3n, account.address.toField());
[storageValues['public_collateral'], storageValues['public_debt']] = toFields(
await contract.methods.getPosition(account.address.toField()).view(),
);

return storageValues;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ describe('e2e_public_cross_chain_messaging', () => {
// Generate a claim secret using pedersen
const l1TokenBalance = 1000000n;
const bridgeAmount = 100n;
const publicBalanceSlot = 3n; // check contract's storage.nr for slot assignment

const [secret, secretHash] = await crossChainTestHarness.generateClaimSecret();

Expand All @@ -98,14 +97,14 @@ describe('e2e_public_cross_chain_messaging', () => {
await crossChainTestHarness.performL2Transfer(transferAmount);

await crossChainTestHarness.consumeMessageOnAztecAndMintPublicly(bridgeAmount, messageKey, secret);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount, publicBalanceSlot);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount);

// time to withdraw the funds again!
logger('Withdrawing funds from L2');
const withdrawAmount = 9n;
const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount);
await withdrawFundsFromAztec(withdrawAmount);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount - withdrawAmount, publicBalanceSlot);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount - withdrawAmount);

// Check balance before and after exit.
expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(l1TokenBalance - bridgeAmount);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ describe('e2e_public_to_private_messaging', () => {
const l1TokenBalance = 1000000n;
const bridgeAmount = 100n;
const shieldAmount = 50n;
const publicBalanceSlot = 3n; // check contract's storage.nr for slot assignment

const [secret, secretHash] = await crossChainTestHarness.generateClaimSecret();

Expand All @@ -83,19 +82,19 @@ describe('e2e_public_to_private_messaging', () => {
await crossChainTestHarness.expectBalanceOnL2(ownerAddress, initialBalance - transferAmount);

await crossChainTestHarness.consumeMessageOnAztecAndMintPublicly(bridgeAmount, messageKey, secret);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount, publicBalanceSlot);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount);

// Create the commitment to be spent in the private domain
await crossChainTestHarness.shieldFundsOnL2(shieldAmount, secretHash);

// Create the transaction spending the commitment
await crossChainTestHarness.redeemShieldPrivatelyOnL2(shieldAmount, secret);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount - shieldAmount, publicBalanceSlot);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount - shieldAmount);
await crossChainTestHarness.expectBalanceOnL2(ownerAddress, initialBalance + shieldAmount - transferAmount);

// Unshield the tokens again, sending them to the same account, however this can be any account.
await crossChainTestHarness.unshieldTokensOnL2(shieldAmount);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount, publicBalanceSlot);
await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount);
await crossChainTestHarness.expectBalanceOnL2(ownerAddress, initialBalance - transferAmount);
}, 200_000);
});
14 changes: 5 additions & 9 deletions yarn-project/end-to-end/src/e2e_public_token_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { AztecRPC, L2BlockL2Logs, TxStatus } from '@aztec/types';

import times from 'lodash.times';

import { CheatCodes } from './cheat_codes.js';
import { setup } from './fixtures/utils.js';

describe('e2e_public_token_contract', () => {
Expand All @@ -18,9 +17,6 @@ describe('e2e_public_token_contract', () => {
let logger: DebugLogger;

let contract: PublicTokenContract;
const balanceSlot = 1n;

let cc: CheatCodes;

const deployContract = async () => {
logger(`Deploying L2 public contract...`);
Expand All @@ -40,7 +36,7 @@ describe('e2e_public_token_contract', () => {
};

beforeEach(async () => {
({ aztecNode, aztecRpcServer, accounts, wallet, logger, cheatCodes: cc } = await setup());
({ aztecNode, aztecRpcServer, accounts, wallet, logger } = await setup());
}, 100_000);

afterEach(async () => {
Expand Down Expand Up @@ -70,8 +66,8 @@ describe('e2e_public_token_contract', () => {

expect(receipt.status).toBe(TxStatus.MINED);

const balance = await cc.l2.loadPublic(contract.address, cc.l2.computeSlotInMap(balanceSlot, recipient.toField()));
expect(balance.value).toBe(mintAmount);
const balance = (await contract.methods.publicBalanceOf(recipient.toField()).view({ from: recipient }))[0];
expect(balance).toBe(mintAmount);

await expectLogsFromLastBlockToBe(['Coins minted']);
}, 45_000);
Expand All @@ -95,8 +91,8 @@ describe('e2e_public_token_contract', () => {
expect(receipts.map(r => r.status)).toEqual(times(3, () => TxStatus.MINED));
expect(receipts.map(r => r.blockNumber)).toEqual(times(3, () => receipts[0].blockNumber));

const balance = await cc.l2.loadPublic(contract.address, cc.l2.computeSlotInMap(balanceSlot, recipient.toField()));
expect(balance.value).toBe(mintAmount * 3n);
const balance = (await contract.methods.publicBalanceOf(recipient.toField()).view({ from: recipient }))[0];
expect(balance).toBe(mintAmount * 3n);

await expectLogsFromLastBlockToBe(['Coins minted', 'Coins minted', 'Coins minted']);
}, 60_000);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,9 @@ export class CrossChainTestHarness {
expect(balance).toBe(expectedBalance);
}

async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint, publicBalanceSlot: bigint) {
const balance = await this.cc.l2.loadPublic(
this.l2Contract.address,
this.cc.l2.computeSlotInMap(publicBalanceSlot, owner.toField()),
);
expect(balance.value).toBe(expectedBalance);
async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) {
const balance = (await this.l2Contract.methods.publicBalanceOf(owner.toField()).view({ from: owner }))[0];
expect(balance).toBe(expectedBalance);
}

async checkEntryIsNotInOutbox(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Promise<Fr> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,22 @@ contract Lending {
debt_loc.write(static_debt - amount);
1
}

unconstrained fn getTot(
assetId: Field,
) -> [Field; 2]{
let storage = Storage::init();
let asset = storage.assets.at(assetId);
let tot = asset.read();
[tot.interest_accumulator as Field, tot.last_updated_ts as Field]
}

unconstrained fn getPosition(
owner: Field,
) -> [Field; 2] {
let storage = Storage::init();
let collateral = storage.collateral.at(owner).read();
let debt = storage.static_debt.at(owner).read();
[collateral, debt]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -346,4 +346,11 @@ contract NonNativeToken {
note_utils::compute_note_hash_and_nullifier(ValueNoteMethods, note_header, preimage)
}
}

unconstrained fn publicBalanceOf(
owner: Field,
) -> Field {
let storage = Storage::init();
storage.public_balances.at(owner).read()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,10 @@ contract PublicToken {
}
}

unconstrained fn publicBalanceOf(
owner: Field,
) -> Field {
let storage = Storage::init();
storage.balances.at(owner).read()
}
}

0 comments on commit 912c1b4

Please sign in to comment.