From 912c1b44b83a87ce6da7e9c5a99b9d5d3ba8aaf4 Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Mon, 7 Aug 2023 23:08:28 +0200 Subject: [PATCH] feat: Public view functions (unconstrained can read public storage) (#1421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 😭. --- .../acir-simulator/src/client/simulator.ts | 6 ++- .../src/client/unconstrained_execution.ts | 31 +++++++++++++++- .../src/aztec_rpc_server/aztec_rpc_server.ts | 1 + .../src/e2e_lending_contract.test.ts | 37 ++++++++----------- .../e2e_public_cross_chain_messaging.test.ts | 5 +-- .../e2e_public_to_private_messaging.test.ts | 7 ++-- .../src/e2e_public_token_contract.test.ts | 14 +++---- .../src/fixtures/cross_chain_test_harness.ts | 9 ++--- .../contracts/lending_contract/src/main.nr | 18 +++++++++ .../non_native_token_contract/src/main.nr | 7 ++++ .../public_token_contract/src/main.nr | 6 +++ 11 files changed, 95 insertions(+), 46 deletions(-) diff --git a/yarn-project/acir-simulator/src/client/simulator.ts b/yarn-project/acir-simulator/src/client/simulator.ts index 97d10eb9d39..6359e6b5a64 100644 --- a/yarn-project/acir-simulator/src/client/simulator.ts +++ b/yarn-project/acir-simulator/src/client/simulator.ts @@ -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'; @@ -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( @@ -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`); @@ -129,7 +131,7 @@ export class AcirSimulator { callContext, ); - return execution.run(); + return execution.run(aztecNode); } /** diff --git a/yarn-project/acir-simulator/src/client/unconstrained_execution.ts b/yarn-project/acir-simulator/src/client/unconstrained_execution.ts index 77a6e890626..bf659acc31f 100644 --- a/yarn-project/acir-simulator/src/client/unconstrained_execution.ts +++ b/yarn-project/acir-simulator/src/client/unconstrained_execution.ts @@ -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'; @@ -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 { + public async run(aztecNode?: AztecNode): Promise { this.log( `Executing unconstrained function ${this.contractAddress.toShortString()}:${this.functionData.functionSelectorBuffer.toString( 'hex', @@ -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); diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts index a12ec1654fa..f3e2e9d2875 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts @@ -383,6 +383,7 @@ export class AztecRPCServer implements AztecRPC { contractAddress, portalContract, historicRoots, + this.node, ); this.log('Unconstrained simulation completed!'); diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index b4ec41e38f6..14123b40ddf 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -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', () => { @@ -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...`); @@ -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 () => { @@ -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; }; diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index f238c6d276c..677b2c72599 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -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(); @@ -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); diff --git a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts index 2dbb4e62965..7e1988e6740 100644 --- a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts @@ -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(); @@ -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); }); diff --git a/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts index 204922e8514..a7901c1f6a9 100644 --- a/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts @@ -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', () => { @@ -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...`); @@ -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 () => { @@ -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); @@ -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); diff --git a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts index 1bdcb632d8a..670922bfd63 100644 --- a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts @@ -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 { diff --git a/yarn-project/noir-contracts/src/contracts/lending_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/lending_contract/src/main.nr index 0e4f63d4e16..b4552485254 100644 --- a/yarn-project/noir-contracts/src/contracts/lending_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/lending_contract/src/main.nr @@ -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] + } } diff --git a/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/main.nr index 2662f816e29..b13639921fd 100644 --- a/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/main.nr @@ -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() + } } diff --git a/yarn-project/noir-contracts/src/contracts/public_token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/public_token_contract/src/main.nr index b718586ddb5..827681f98d6 100644 --- a/yarn-project/noir-contracts/src/contracts/public_token_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/public_token_contract/src/main.nr @@ -61,4 +61,10 @@ contract PublicToken { } } + unconstrained fn publicBalanceOf( + owner: Field, + ) -> Field { + let storage = Storage::init(); + storage.balances.at(owner).read() + } }