diff --git a/boxes/boxes/vite/package.json b/boxes/boxes/vite/package.json index f8ed2fa832fb..5b401630da3d 100644 --- a/boxes/boxes/vite/package.json +++ b/boxes/boxes/vite/package.json @@ -18,12 +18,7 @@ "dependencies": { "@aztec/accounts": "latest", "@aztec/aztec.js": "latest", - "@aztec/bb-prover": "latest", - "@aztec/key-store": "latest", - "@aztec/kv-store": "latest", - "@aztec/protocol-contracts": "latest", "@aztec/pxe": "latest", - "@aztec/simulator": "latest", "@aztec/stdlib": "latest", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/boxes/boxes/vite/src/config.ts b/boxes/boxes/vite/src/config.ts index 992a6e7c4c6f..5cdefcbb8890 100644 --- a/boxes/boxes/vite/src/config.ts +++ b/boxes/boxes/vite/src/config.ts @@ -3,17 +3,13 @@ import { getSchnorrAccount } from "@aztec/accounts/schnorr/lazy"; import { AccountWalletWithSecretKey, createAztecNodeClient, - createLogger, } from "@aztec/aztec.js"; -import { BBWASMLazyPrivateKernelProver } from "@aztec/bb-prover/wasm/lazy"; -import { KeyStore } from "@aztec/key-store"; -import { createStore } from "@aztec/kv-store/indexeddb"; -import { L2TipsStore } from "@aztec/kv-store/stores"; -import { PXEServiceConfig, getPXEServiceConfig } from "@aztec/pxe/config"; -import { KVPxeDatabase } from "@aztec/pxe/database"; -import { PXEService } from "@aztec/pxe/service"; -import { WASMSimulator } from "@aztec/simulator/client"; -import { LazyProtocolContractsProvider } from "@aztec/protocol-contracts/providers/lazy"; +import { + PXEServiceConfig, + getPXEServiceConfig, + PXEService, + createPXEService, +} from "@aztec/pxe/client/lazy"; export class PrivateEnv { pxe: PXEService; @@ -24,44 +20,15 @@ export class PrivateEnv { async init() { const nodeURL = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; + const aztecNode = await createAztecNodeClient(nodeURL); const config = getPXEServiceConfig(); config.dataDirectory = "pxe"; - const aztecNode = await createAztecNodeClient(nodeURL); - const simulationProvider = new WASMSimulator(); - const proofCreator = new BBWASMLazyPrivateKernelProver( - simulationProvider, - 16, - ); const l1Contracts = await aztecNode.getL1ContractAddresses(); const configWithContracts = { ...config, l1Contracts, } as PXEServiceConfig; - - const store = await createStore( - "pxe_data", - configWithContracts, - createLogger("pxe:data:idb"), - ); - - const keyStore = new KeyStore(store); - - const db = await KVPxeDatabase.create(store); - const tips = new L2TipsStore(store, "pxe"); - - const protocolContractsProvider = new LazyProtocolContractsProvider(); - - this.pxe = new PXEService( - keyStore, - aztecNode, - db, - tips, - proofCreator, - simulationProvider, - protocolContractsProvider, - config, - ); - await this.pxe.init(); + this.pxe = await createPXEService(aztecNode, configWithContracts); const [accountData] = await getInitialTestAccounts(); const account = await getSchnorrAccount( this.pxe, diff --git a/boxes/yarn.lock b/boxes/yarn.lock index f8b6145f10e9..36ecf11634d4 100644 --- a/boxes/yarn.lock +++ b/boxes/yarn.lock @@ -27,30 +27,6 @@ __metadata: languageName: node linkType: soft -"@aztec/bb-prover@link:../yarn-project/bb-prover::locator=aztec-app%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/bb-prover@link:../yarn-project/bb-prover::locator=aztec-app%40workspace%3A." - languageName: node - linkType: soft - -"@aztec/key-store@link:../yarn-project/key-store::locator=aztec-app%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/key-store@link:../yarn-project/key-store::locator=aztec-app%40workspace%3A." - languageName: node - linkType: soft - -"@aztec/kv-store@link:../yarn-project/kv-store::locator=aztec-app%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/kv-store@link:../yarn-project/kv-store::locator=aztec-app%40workspace%3A." - languageName: node - linkType: soft - -"@aztec/protocol-contracts@link:../yarn-project/protocol-contracts::locator=aztec-app%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/protocol-contracts@link:../yarn-project/protocol-contracts::locator=aztec-app%40workspace%3A." - languageName: node - linkType: soft - "@aztec/pxe@link:../yarn-project/pxe::locator=aztec-app%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec/pxe@link:../yarn-project/pxe::locator=aztec-app%40workspace%3A." @@ -106,12 +82,6 @@ __metadata: languageName: unknown linkType: soft -"@aztec/simulator@link:../yarn-project/simulator::locator=aztec-app%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/simulator@link:../yarn-project/simulator::locator=aztec-app%40workspace%3A." - languageName: node - linkType: soft - "@aztec/stdlib@link:../yarn-project/stdlib::locator=aztec-app%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec/stdlib@link:../yarn-project/stdlib::locator=aztec-app%40workspace%3A." @@ -144,12 +114,7 @@ __metadata: dependencies: "@aztec/accounts": "npm:latest" "@aztec/aztec.js": "npm:latest" - "@aztec/bb-prover": "npm:latest" - "@aztec/key-store": "npm:latest" - "@aztec/kv-store": "npm:latest" - "@aztec/protocol-contracts": "npm:latest" "@aztec/pxe": "npm:latest" - "@aztec/simulator": "npm:latest" "@aztec/stdlib": "npm:latest" "@eslint/js": "npm:^9.13.0" "@types/react": "npm:^18.3.12" diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_in_private.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_in_private.nr index dd2056484d28..186e20b9f5c4 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_in_private.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_in_private.nr @@ -103,7 +103,7 @@ unconstrained fn transfer_in_private_failure_on_behalf_of_self_non_zero_nonce() ); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn transfer_in_private_failure_on_behalf_of_other_without_approval() { // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. // The authwit check is in the beginning so we don't need to waste time on minting the NFT and transferring @@ -122,7 +122,7 @@ unconstrained fn transfer_in_private_failure_on_behalf_of_other_without_approval ); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn transfer_in_private_failure_on_behalf_of_other_wrong_caller() { // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. // The authwit check is in the beginning so we don't need to waste time on minting the NFT and transferring diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_public.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_public.nr index 83f3690a5ddf..1b4bf9dc1459 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_public.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_public.nr @@ -75,7 +75,7 @@ unconstrained fn transfer_to_public_failure_on_behalf_of_self_non_zero_nonce() { ); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn transfer_to_public_failure_on_behalf_of_other_invalid_designated_caller() { let (env, nft_contract_address, sender, recipient, token_id) = utils::setup_mint_and_transfer_to_private(/* with_account_contracts */ true); @@ -93,7 +93,7 @@ unconstrained fn transfer_to_public_failure_on_behalf_of_other_invalid_designate transfer_to_public_call_interface.call(&mut env.private()); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn transfer_to_public_failure_on_behalf_of_other_no_approval() { let (env, nft_contract_address, sender, recipient, token_id) = utils::setup_mint_and_transfer_to_private(/* with_account_contracts */ true); diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/burn_private.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/burn_private.nr index 340421ebf8ea..85d5bca73b69 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test/burn_private.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/burn_private.nr @@ -77,7 +77,7 @@ unconstrained fn burn_private_failure_on_behalf_of_other_more_than_balance() { burn_call_interface.call(&mut env.private()); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn burn_private_failure_on_behalf_of_other_without_approval() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint_to_public(/* with_account_contracts */ true); @@ -91,7 +91,7 @@ unconstrained fn burn_private_failure_on_behalf_of_other_without_approval() { burn_call_interface.call(&mut env.private()); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn burn_private_failure_on_behalf_of_other_wrong_designated_caller() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint_to_public(/* with_account_contracts */ true); diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_in_private.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_in_private.nr index 679265fbd0d9..f802a04c3f83 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_in_private.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_in_private.nr @@ -67,7 +67,7 @@ unconstrained fn transfer_private_failure_on_behalf_of_more_than_balance() { transfer_private_from_call_interface.call(&mut env.private()); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn transfer_private_failure_on_behalf_of_other_without_approval() { // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. let (env, token_contract_address, owner, recipient, _) = @@ -82,7 +82,7 @@ unconstrained fn transfer_private_failure_on_behalf_of_other_without_approval() transfer_private_from_call_interface.call(&mut env.private()); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn transfer_private_failure_on_behalf_of_other_wrong_caller() { // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. let (env, token_contract_address, owner, recipient, _) = diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_to_public.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_to_public.nr index 13f9ba3c2382..671c9da4d2bc 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_to_public.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_to_public.nr @@ -97,7 +97,7 @@ unconstrained fn transfer_to_public_failure_on_behalf_of_other_more_than_balance transfer_to_public_call_interface.call(&mut env.private()); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn transfer_to_public_failure_on_behalf_of_other_invalid_designated_caller() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint_to_private(/* with_account_contracts */ true); @@ -120,7 +120,7 @@ unconstrained fn transfer_to_public_failure_on_behalf_of_other_invalid_designate transfer_to_public_call_interface.call(&mut env.private()); } -#[test(should_fail_with = "Authorization not found for message hash")] +#[test(should_fail_with = "Unknown auth witness for message hash")] unconstrained fn transfer_to_public_failure_on_behalf_of_other_no_approval() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint_to_private(/* with_account_contracts */ true); diff --git a/playground/package.json b/playground/package.json index e0167538089f..3c04319c3707 100644 --- a/playground/package.json +++ b/playground/package.json @@ -18,13 +18,9 @@ "dependencies": { "@aztec/accounts": "link:../yarn-project/accounts", "@aztec/aztec.js": "link:../yarn-project/aztec.js", - "@aztec/bb-prover": "link:../yarn-project/bb-prover", "@aztec/foundation": "link:../yarn-project/foundation", - "@aztec/key-store": "link:../yarn-project/key-store", "@aztec/kv-store": "link:../yarn-project/kv-store", - "@aztec/protocol-contracts": "link:../yarn-project/protocol-contracts", "@aztec/pxe": "link:../yarn-project/pxe", - "@aztec/simulator": "link:../yarn-project/simulator", "@aztec/stdlib": "link:../yarn-project/stdlib", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", diff --git a/playground/src/aztecEnv.ts b/playground/src/aztecEnv.ts index ee113de98d8a..5aceaff7e5a9 100644 --- a/playground/src/aztecEnv.ts +++ b/playground/src/aztecEnv.ts @@ -4,19 +4,12 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; import { AccountWalletWithSecretKey } from '@aztec/aztec.js/wallet'; import { Contract } from '@aztec/aztec.js/contracts'; import { type PXE } from '@aztec/aztec.js/interfaces/pxe'; -import { PXEService } from '@aztec/pxe/service'; -import { type PXEServiceConfig, getPXEServiceConfig } from '@aztec/pxe/config'; -import { KVPxeDatabase } from '@aztec/pxe/database'; -import { KeyStore } from '@aztec/key-store'; -import { L2TipsStore } from '@aztec/kv-store/stores'; +import { createPXEService, type PXEServiceConfig, getPXEServiceConfig } from '@aztec/pxe/client/lazy'; import { createStore } from '@aztec/kv-store/indexeddb'; -import { BBWASMLazyPrivateKernelProver } from '@aztec/bb-prover/wasm/lazy'; -import { WASMSimulator } from '@aztec/simulator/client'; import { createContext } from 'react'; import { NetworkDB, WalletDB } from './utils/storage'; import { type ContractFunctionInteractionTx } from './utils/txs'; import { type Logger, createLogger } from '@aztec/aztec.js/log'; -import { LazyProtocolContractsProvider } from '@aztec/protocol-contracts/providers/lazy'; const logLevel = ['silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug', 'trace'] as const; @@ -151,44 +144,19 @@ export class AztecEnv { const config = getPXEServiceConfig(); config.dataDirectory = 'pxe'; config.proverEnabled = true; - - const simulationProvider = new WASMSimulator(); - const proofCreator = new BBWASMLazyPrivateKernelProver( - simulationProvider, - 16, - WebLogger.getInstance().createLogger('bb:wasm:lazy'), - ); const l1Contracts = await aztecNode.getL1ContractAddresses(); const configWithContracts = { ...config, l1Contracts, } as PXEServiceConfig; - const store = await createStore( - 'pxe_data', - configWithContracts, - WebLogger.getInstance().createLogger('pxe:data:indexeddb'), - ); - - const keyStore = new KeyStore(store); - - const db = await KVPxeDatabase.create(store); - const tips = new L2TipsStore(store, 'pxe'); - - const protocolContractsProvider = new LazyProtocolContractsProvider(); - - const pxe = new PXEService( - keyStore, - aztecNode, - db, - tips, - proofCreator, - simulationProvider, - protocolContractsProvider, - config, - WebLogger.getInstance().createLogger('pxe:service'), - ); - await pxe.init(); + const pxe = await createPXEService(aztecNode, configWithContracts, { + loggers: { + store: WebLogger.getInstance().createLogger('pxe:data:indexeddb'), + pxe: WebLogger.getInstance().createLogger('pxe:service'), + prover: WebLogger.getInstance().createLogger('bb:wasm:lazy'), + } + }); return pxe; } } diff --git a/playground/yarn.lock b/playground/yarn.lock index a57e177b6bbf..af2050e15594 100644 --- a/playground/yarn.lock +++ b/playground/yarn.lock @@ -17,24 +17,12 @@ __metadata: languageName: node linkType: soft -"@aztec/bb-prover@link:../yarn-project/bb-prover::locator=%40aztec%2Fplayground%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/bb-prover@link:../yarn-project/bb-prover::locator=%40aztec%2Fplayground%40workspace%3A." - languageName: node - linkType: soft - "@aztec/foundation@link:../yarn-project/foundation::locator=%40aztec%2Fplayground%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec/foundation@link:../yarn-project/foundation::locator=%40aztec%2Fplayground%40workspace%3A." languageName: node linkType: soft -"@aztec/key-store@link:../yarn-project/key-store::locator=%40aztec%2Fplayground%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/key-store@link:../yarn-project/key-store::locator=%40aztec%2Fplayground%40workspace%3A." - languageName: node - linkType: soft - "@aztec/kv-store@link:../yarn-project/kv-store::locator=%40aztec%2Fplayground%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec/kv-store@link:../yarn-project/kv-store::locator=%40aztec%2Fplayground%40workspace%3A." @@ -47,13 +35,9 @@ __metadata: dependencies: "@aztec/accounts": "link:../yarn-project/accounts" "@aztec/aztec.js": "link:../yarn-project/aztec.js" - "@aztec/bb-prover": "link:../yarn-project/bb-prover" "@aztec/foundation": "link:../yarn-project/foundation" - "@aztec/key-store": "link:../yarn-project/key-store" "@aztec/kv-store": "link:../yarn-project/kv-store" - "@aztec/protocol-contracts": "link:../yarn-project/protocol-contracts" "@aztec/pxe": "link:../yarn-project/pxe" - "@aztec/simulator": "link:../yarn-project/simulator" "@aztec/stdlib": "link:../yarn-project/stdlib" "@emotion/react": "npm:^11.14.0" "@emotion/styled": "npm:^11.14.0" @@ -88,24 +72,12 @@ __metadata: languageName: unknown linkType: soft -"@aztec/protocol-contracts@link:../yarn-project/protocol-contracts::locator=%40aztec%2Fplayground%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/protocol-contracts@link:../yarn-project/protocol-contracts::locator=%40aztec%2Fplayground%40workspace%3A." - languageName: node - linkType: soft - "@aztec/pxe@link:../yarn-project/pxe::locator=%40aztec%2Fplayground%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec/pxe@link:../yarn-project/pxe::locator=%40aztec%2Fplayground%40workspace%3A." languageName: node linkType: soft -"@aztec/simulator@link:../yarn-project/simulator::locator=%40aztec%2Fplayground%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@aztec/simulator@link:../yarn-project/simulator::locator=%40aztec%2Fplayground%40workspace%3A." - languageName: node - linkType: soft - "@aztec/stdlib@link:../yarn-project/stdlib::locator=%40aztec%2Fplayground%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec/stdlib@link:../yarn-project/stdlib::locator=%40aztec%2Fplayground%40workspace%3A." diff --git a/yarn-project/aztec/src/cli/cmds/start_pxe.ts b/yarn-project/aztec/src/cli/cmds/start_pxe.ts index 19a778cb63c4..2f582318a22a 100644 --- a/yarn-project/aztec/src/cli/cmds/start_pxe.ts +++ b/yarn-project/aztec/src/cli/cmds/start_pxe.ts @@ -15,7 +15,7 @@ import { type PXEServiceConfig, allPxeConfigMappings, createPXEService, -} from '@aztec/pxe'; +} from '@aztec/pxe/server'; import { type AztecNode, PXESchema, createAztecNodeClient } from '@aztec/stdlib/interfaces/client'; import { L2BasicContractsMap, Network } from '@aztec/stdlib/network'; import { makeTracedFetch } from '@aztec/telemetry-client'; diff --git a/yarn-project/aztec/src/cli/util.ts b/yarn-project/aztec/src/cli/util.ts index 015a6cc25348..bc9532a79b4c 100644 --- a/yarn-project/aztec/src/cli/util.ts +++ b/yarn-project/aztec/src/cli/util.ts @@ -1,7 +1,7 @@ import type { AccountManager, Fr } from '@aztec/aztec.js'; import type { ConfigMappingsType } from '@aztec/foundation/config'; import type { LogFn } from '@aztec/foundation/log'; -import type { PXEService } from '@aztec/pxe'; +import type { PXEService } from '@aztec/pxe/server'; import chalk from 'chalk'; import type { Command } from 'commander'; diff --git a/yarn-project/aztec/src/sandbox/sandbox.ts b/yarn-project/aztec/src/sandbox/sandbox.ts index f9c800f51ee8..b5b4990b2d04 100644 --- a/yarn-project/aztec/src/sandbox/sandbox.ts +++ b/yarn-project/aztec/src/sandbox/sandbox.ts @@ -17,7 +17,7 @@ import { Fr } from '@aztec/foundation/fields'; import { type LogFn, createLogger } from '@aztec/foundation/log'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import { ProtocolContractAddress, protocolContractTreeRoot } from '@aztec/protocol-contracts'; -import { type PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; +import { type PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe/server'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { PublicDataTreeLeaf } from '@aztec/stdlib/trees'; import { diff --git a/yarn-project/cli-wallet/src/utils/pxe_wrapper.ts b/yarn-project/cli-wallet/src/utils/pxe_wrapper.ts index 8a83c05d1adb..52883f36b4af 100644 --- a/yarn-project/cli-wallet/src/utils/pxe_wrapper.ts +++ b/yarn-project/cli-wallet/src/utils/pxe_wrapper.ts @@ -1,4 +1,4 @@ -import { type PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; +import { type PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe/server'; import { type AztecNode, type PXE, createAztecNodeClient } from '@aztec/stdlib/interfaces/client'; /* diff --git a/yarn-project/end-to-end/src/bench/utils.ts b/yarn-project/end-to-end/src/bench/utils.ts index c3ae647bd6a1..760237774276 100644 --- a/yarn-project/end-to-end/src/bench/utils.ts +++ b/yarn-project/end-to-end/src/bench/utils.ts @@ -3,7 +3,7 @@ import { type AztecNode, BatchCall, INITIAL_L2_BLOCK_NUM, type SentTx, type Wait import { mean, stdDev, timesParallel } from '@aztec/foundation/collection'; import { randomInt } from '@aztec/foundation/crypto'; import { BenchmarkingContract } from '@aztec/noir-contracts.js/Benchmarking'; -import { type PXEService, type PXEServiceConfig, createPXEService } from '@aztec/pxe'; +import { type PXEService, type PXEServiceConfig, createPXEService } from '@aztec/pxe/server'; import type { MetricsType } from '@aztec/telemetry-client'; import type { BenchmarkDataPoint, BenchmarkMetricsType, BenchmarkTelemetryClient } from '@aztec/telemetry-client/bench'; diff --git a/yarn-project/end-to-end/src/composed/e2e_pxe.test.ts b/yarn-project/end-to-end/src/composed/e2e_pxe.test.ts index 89bcae3be1a7..98db76e755a7 100644 --- a/yarn-project/end-to-end/src/composed/e2e_pxe.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_pxe.test.ts @@ -1,5 +1,5 @@ import { waitForPXE } from '@aztec/aztec.js'; -import { pxeTestSuite } from '@aztec/pxe'; +import { pxeTestSuite } from '@aztec/pxe/testing'; import { setup } from '../fixtures/utils.js'; diff --git a/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts b/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts index d72c9cb9e71e..e55dfe120753 100644 --- a/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts @@ -1,7 +1,7 @@ import type { Logger, PXE, Wallet } from '@aztec/aztec.js'; import { getL1ContractsConfigEnvVars } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; -import type { PXEService } from '@aztec/pxe'; +import type { PXEService } from '@aztec/pxe/server'; import { jest } from '@jest/globals'; import { privateKeyToAccount } from 'viem/accounts'; diff --git a/yarn-project/end-to-end/src/e2e_p2p/shared.ts b/yarn-project/end-to-end/src/e2e_p2p/shared.ts index dd9b581f47e0..6c625ac1ed15 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/shared.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/shared.ts @@ -3,7 +3,7 @@ import type { InitialAccountData } from '@aztec/accounts/testing'; import type { AztecNodeService } from '@aztec/aztec-node'; import { type Logger, type SentTx, TxStatus } from '@aztec/aztec.js'; import type { SpamContract } from '@aztec/noir-contracts.js/Spam'; -import { createPXEService, getPXEServiceConfig as getRpcConfig } from '@aztec/pxe'; +import { createPXEService, getPXEServiceConfig as getRpcConfig } from '@aztec/pxe/server'; import type { NodeContext } from '../fixtures/setup_p2p_test.js'; import { submitTxsTo } from '../shared/submit-transactions.js'; diff --git a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts index a7811c7a9e2f..2dd3dcaf6e5a 100644 --- a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts @@ -24,7 +24,7 @@ import { Buffer32 } from '@aztec/foundation/buffer'; import { HonkVerifierAbi, HonkVerifierBytecode, RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { type ProverNode, type ProverNodeConfig, createProverNode } from '@aztec/prover-node'; -import type { PXEService } from '@aztec/pxe'; +import type { PXEService } from '@aztec/pxe/server'; import { getGenesisValues } from '@aztec/world-state/testing'; import { type Hex, getContract } from 'viem'; diff --git a/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.test.ts b/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.test.ts index dd29b0d5fc76..5979422dfb4c 100644 --- a/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.test.ts +++ b/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.test.ts @@ -9,7 +9,7 @@ import { import { EthAddress } from '@aztec/foundation/eth-address'; import { NewGovernanceProposerPayloadAbi } from '@aztec/l1-artifacts/NewGovernanceProposerPayloadAbi'; import { NewGovernanceProposerPayloadBytecode } from '@aztec/l1-artifacts/NewGovernanceProposerPayloadBytecode'; -import type { PXEService } from '@aztec/pxe'; +import type { PXEService } from '@aztec/pxe/server'; import { privateKeyToAccount } from 'viem/accounts'; diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index 3a6d962e1a35..89c4a858e582 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -61,7 +61,7 @@ import { RollupAbi } from '@aztec/l1-artifacts'; import { SchnorrHardcodedAccountContract } from '@aztec/noir-contracts.js/SchnorrHardcodedAccount'; import { SpamContract } from '@aztec/noir-contracts.js/Spam'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; -import type { PXEService } from '@aztec/pxe'; +import type { PXEService } from '@aztec/pxe/server'; import { SequencerPublisher } from '@aztec/sequencer-client'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2Block } from '@aztec/stdlib/block'; diff --git a/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts b/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts index 84396c497a17..abe39cc01008 100644 --- a/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts +++ b/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts @@ -5,7 +5,7 @@ import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; import type { SentTx } from '@aztec/aztec.js'; import { addLogNameHandler, removeLogNameHandler } from '@aztec/foundation/log'; import type { DateProvider } from '@aztec/foundation/timer'; -import type { PXEService } from '@aztec/pxe'; +import type { PXEService } from '@aztec/pxe/server'; import type { PublicDataTreeLeaf } from '@aztec/stdlib/trees'; import getPort from 'get-port'; diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 5e56093da067..d9ea3e2b42a2 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -26,7 +26,7 @@ import { createLogger } from '@aztec/foundation/log'; import { resolver, reviver } from '@aztec/foundation/serialize'; import { TestDateProvider } from '@aztec/foundation/timer'; import type { ProverNode } from '@aztec/prover-node'; -import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; +import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe/server'; import { getConfigEnvVars as getTelemetryConfig, initTelemetryClient } from '@aztec/telemetry-client'; import { getGenesisValues } from '@aztec/world-state/testing'; diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index d52264bc8c0e..18edbcce1aac 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -53,7 +53,7 @@ import { FeeJuiceContract } from '@aztec/noir-contracts.js/FeeJuice'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import { ProtocolContractAddress, protocolContractTreeRoot } from '@aztec/protocol-contracts'; import { type ProverNode, type ProverNodeConfig, createProverNode } from '@aztec/prover-node'; -import { type PXEService, type PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; +import { type PXEService, type PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe/server'; import type { SequencerClient } from '@aztec/sequencer-client'; import type { TestSequencerClient } from '@aztec/sequencer-client/test'; import { getContractClassFromArtifact } from '@aztec/stdlib/contract'; diff --git a/yarn-project/end-to-end/src/shared/submit-transactions.ts b/yarn-project/end-to-end/src/shared/submit-transactions.ts index 02cacfe6f83e..f00698a2fd59 100644 --- a/yarn-project/end-to-end/src/shared/submit-transactions.ts +++ b/yarn-project/end-to-end/src/shared/submit-transactions.ts @@ -1,7 +1,7 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { Fr, GrumpkinScalar, type Logger, type SentTx, TxStatus, type Wallet } from '@aztec/aztec.js'; import { times } from '@aztec/foundation/collection'; -import type { PXEService } from '@aztec/pxe'; +import type { PXEService } from '@aztec/pxe/server'; // submits a set of transactions to the provided Private eXecution Environment (PXE) export const submitTxsTo = async ( diff --git a/yarn-project/pxe/package.json b/yarn-project/pxe/package.json index f1c4afef806b..04a5083b2b97 100644 --- a/yarn-project/pxe/package.json +++ b/yarn-project/pxe/package.json @@ -3,20 +3,13 @@ "version": "0.1.0", "type": "module", "exports": { - ".": "./dest/index.js", - "./service": "./dest/pxe_service/index.js", + "./server": "./dest/entrypoints/server/index.js", + "./client/lazy": "./dest/entrypoints/client/lazy/index.js", + "./client/bundle": "./dest/entrypoints/client/bundle/index.js", "./config": "./dest/config/index.js", - "./database": "./dest/database/index.js", - "./kernel_prover": "./dest/kernel_prover/index.js" + "./testing": "./dest/test/pxe_test_suite.js" }, "bin": "./dest/bin/index.js", - "typedocOptions": { - "entryPoints": [ - "./src/index.ts" - ], - "name": "Wallet", - "tsconfig": "./tsconfig.json" - }, "scripts": { "build": "yarn clean && yarn generate && tsc -b", "build:dev": "tsc -b --watch", diff --git a/yarn-project/pxe/src/bin/index.ts b/yarn-project/pxe/src/bin/index.ts index 0a60ce14cead..9b6224b8e25e 100644 --- a/yarn-project/pxe/src/bin/index.ts +++ b/yarn-project/pxe/src/bin/index.ts @@ -3,8 +3,8 @@ import { createLogger } from '@aztec/foundation/log'; import { createAztecNodeClient } from '@aztec/stdlib/interfaces/client'; import { getPXEServiceConfig } from '../config/index.js'; +import { createPXEService } from '../entrypoints/server/utils.js'; import { startPXEHttpServer } from '../pxe_http/index.js'; -import { createPXEService } from '../utils/create_pxe_service.js'; const { PXE_PORT = 8080, AZTEC_NODE_URL = 'http://localhost:8079' } = process.env; diff --git a/yarn-project/pxe/src/config/index.ts b/yarn-project/pxe/src/config/index.ts index 62b788c831f9..abef74bee99f 100644 --- a/yarn-project/pxe/src/config/index.ts +++ b/yarn-project/pxe/src/config/index.ts @@ -10,6 +10,8 @@ import { type DataStoreConfig, dataConfigMappings } from '@aztec/kv-store/config import { type ChainConfig, chainConfigMappings } from '@aztec/stdlib/config'; import type { Network } from '@aztec/stdlib/network'; +export { getPackageInfo } from './package_info.js'; + /** * Temporary configuration until WASM can be used instead of native */ diff --git a/yarn-project/pxe/src/database/index.ts b/yarn-project/pxe/src/database/index.ts deleted file mode 100644 index 933d0356eed1..000000000000 --- a/yarn-project/pxe/src/database/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './kv_pxe_database.js'; -export * from './interfaces/index.js'; diff --git a/yarn-project/pxe/src/database/interfaces/contract_artifact_db.ts b/yarn-project/pxe/src/database/interfaces/contract_artifact_db.ts deleted file mode 100644 index 9cf9c217dec8..000000000000 --- a/yarn-project/pxe/src/database/interfaces/contract_artifact_db.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Fr } from '@aztec/foundation/fields'; -import type { ContractArtifact } from '@aztec/stdlib/abi'; - -/** - * PXE database for managing contract artifacts. - */ -export interface ContractArtifactDatabase { - /** - * Adds a new contract artifact to the database or updates an existing one. - * @param id - Id of the corresponding contract class. - * @param contract - Contract artifact to add. - * @throws - If there are duplicate private function selectors. - */ - addContractArtifact(id: Fr, contract: ContractArtifact): Promise; - /** - * Gets a contract artifact given its resulting contract class id. - * @param id - Contract class id for the given artifact. - */ - getContractArtifact(id: Fr): Promise; -} diff --git a/yarn-project/pxe/src/database/interfaces/contract_instance_db.ts b/yarn-project/pxe/src/database/interfaces/contract_instance_db.ts deleted file mode 100644 index 6bf084a375b0..000000000000 --- a/yarn-project/pxe/src/database/interfaces/contract_instance_db.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; - -/** - * PXE database for managing contract instances. - */ -export interface ContractInstanceDatabase { - /** - * Adds a new contract to the db or updates an existing one. - * @param contract - Contract to insert. - */ - addContractInstance(contract: ContractInstanceWithAddress): Promise; - /** - * Gets a contract given its address. - * @param address - Address of the contract. - */ - getContractInstance(address: AztecAddress): Promise; - - /** Returns the addresses all contract instances registered in the DB. */ - getContractsAddresses(): Promise; -} diff --git a/yarn-project/pxe/src/database/interfaces/index.ts b/yarn-project/pxe/src/database/interfaces/index.ts deleted file mode 100644 index 1a772fe87508..000000000000 --- a/yarn-project/pxe/src/database/interfaces/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { ContractArtifactDatabase } from './contract_artifact_db.js'; -export type { ContractInstanceDatabase } from './contract_instance_db.js'; -export type { PxeDatabase } from './pxe_database.js'; diff --git a/yarn-project/pxe/src/database/interfaces/pxe_database.ts b/yarn-project/pxe/src/database/interfaces/pxe_database.ts deleted file mode 100644 index ccb000397b7c..000000000000 --- a/yarn-project/pxe/src/database/interfaces/pxe_database.ts +++ /dev/null @@ -1,240 +0,0 @@ -import type { Fr } from '@aztec/foundation/fields'; -import type { ContractArtifact } from '@aztec/stdlib/abi'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { InBlock } from '@aztec/stdlib/block'; -import type { CompleteAddress, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import type { PublicKey } from '@aztec/stdlib/keys'; -import type { IndexedTaggingSecret } from '@aztec/stdlib/logs'; -import type { NotesFilter } from '@aztec/stdlib/note'; -import type { BlockHeader } from '@aztec/stdlib/tx'; - -import type { NoteDao } from '../note_dao.js'; -import type { ContractArtifactDatabase } from './contract_artifact_db.js'; -import type { ContractInstanceDatabase } from './contract_instance_db.js'; - -/** - * A database interface that provides methods for retrieving, adding, and removing transactional data related to Aztec - * addresses, storage slots, and nullifiers. - */ -export interface PxeDatabase extends ContractArtifactDatabase, ContractInstanceDatabase { - getContract(address: AztecAddress): Promise<(ContractInstanceWithAddress & ContractArtifact) | undefined>; - - /** - * Add a auth witness to the database. - * @param messageHash - The message hash. - * @param witness - An array of field elements representing the auth witness. - */ - addAuthWitness(messageHash: Fr, witness: Fr[]): Promise; - - /** - * Fetching the auth witness for a given message hash. - * @param messageHash - The message hash. - * @returns A Promise that resolves to an array of field elements representing the auth witness. - */ - getAuthWitness(messageHash: Fr): Promise; - - /** - * Gets notes based on the provided filter. - * @param filter - The filter to apply to the notes. - * @returns The requested notes. - */ - getNotes(filter: NotesFilter): Promise; - - /** - * Adds a note to DB. - * @param note - The note to add. - * @param scope - The scope to add the note under. Currently optional. - * @remark - Will create a database for the scope if it does not already exist. - */ - addNote(note: NoteDao, scope?: AztecAddress): Promise; - - /** - * Adds a nullified note to DB. - * @param note - The note to add. - */ - addNullifiedNote(note: NoteDao): Promise; - - /** - * Adds an array of notes to DB. - * This function is used to insert multiple notes to the database at once, - * which can improve performance when dealing with large numbers of transactions. - * - * @param notes - An array of notes. - * @param scope - The scope to add the notes under. Currently optional. - * @remark - Will create a database for the scope if it does not already exist. - */ - addNotes(notes: NoteDao[], scope?: AztecAddress): Promise; - - /** - * Remove nullified notes associated with the given account and nullifiers. - * - * @param nullifiers - An array of Fr instances representing nullifiers to be matched. - * @param account - A PublicKey instance representing the account for which the records are being removed. - * @returns Removed notes. - */ - removeNullifiedNotes(nullifiers: InBlock[], account: PublicKey): Promise; - - /** - * Gets the most recently processed block number. - * @returns The most recently processed block number or undefined if never synched. - */ - getBlockNumber(): Promise; - - /** - * Retrieve the stored Block Header from the database. - * The function returns a Promise that resolves to the Block Header. - * This data is required to reproduce block attestations. - * Throws an error if the block header is not available within the database. - * - * note: this data is a combination of the tree roots and the global variables hash. - * - * @returns The Block Header. - * @throws If no block have been processed yet. - */ - getBlockHeader(): Promise; - - /** - * Set the latest Block Header. - * Note that this will overwrite any existing hash or roots in the database. - * - * @param header - An object containing the most recent block header. - * @returns A Promise that resolves when the hash has been successfully updated in the database. - */ - setHeader(header: BlockHeader): Promise; - - /** - * Adds sender address to the database. - * @param address - The address to add to the address book. - * @returns A promise resolving to true if the address was added, false if it already exists. - */ - addSenderAddress(address: AztecAddress): Promise; - - /** - * Retrieves the list of sender addresses in the address book. - * @returns An array of Aztec addresses. - */ - getSenderAddresses(): Promise; - - /** - * Removes a sender address from the database. - * @param address - The address to remove from the address book. - * @returns A promise resolving to true if the address was removed, false if it does not exist. - */ - removeSenderAddress(address: AztecAddress): Promise; - - /** - * Adds complete address to the database. - * @param address - The complete address to add. - * @returns A promise resolving to true if the address was added, false if it already exists. - * @throws If we try to add a CompleteAddress with the same AztecAddress but different public key or partial - * address. - */ - addCompleteAddress(address: CompleteAddress): Promise; - - /** - * Retrieve the complete address associated to a given address. - * @param account - The account address. - * @returns A promise that resolves to a CompleteAddress instance if found, or undefined if not found. - */ - getCompleteAddress(account: AztecAddress): Promise; - - /** - * Retrieves the list of complete addresses added to this database - * @returns A promise that resolves to an array of AztecAddress instances. - */ - getCompleteAddresses(): Promise; - - /** - * Returns the estimated size in bytes of this db. - * @returns The estimated size in bytes of this db. - */ - estimateSize(): Promise; - - /** - * Returns the last seen indexes for the provided app siloed tagging secrets or 0 if they've never been seen. - * @param appTaggingSecrets - The app siloed tagging secrets. - * @returns The indexes for the provided secrets, 0 if they've never been seen. - */ - getTaggingSecretsIndexesAsRecipient(appTaggingSecrets: Fr[]): Promise; - - /** - * Returns the last seen indexes for the provided app siloed tagging secrets or 0 if they've never been used - * @param appTaggingSecrets - The app siloed tagging secrets. - * @returns The indexes for the provided secrets, 0 if they've never been seen. - */ - getTaggingSecretsIndexesAsSender(appTaggingSecrets: Fr[]): Promise; - - /** - * Sets the index for the provided app siloed tagging secrets - * To be used when the generated tags have been "seen" as a sender - * @param appTaggingSecrets - The app siloed tagging secrets. - */ - setTaggingSecretsIndexesAsSender(indexedTaggingSecrets: IndexedTaggingSecret[]): Promise; - - /** - * Sets the index for the provided app siloed tagging secrets - * To be used when the generated tags have been "seen" as a recipient - * @param appTaggingSecrets - The app siloed tagging secrets. - */ - setTaggingSecretsIndexesAsRecipient(indexedTaggingSecrets: IndexedTaggingSecret[]): Promise; - - /** - * Deletes all notes synched after this block number. - * @param blockNumber - All notes strictly after this block number are removed. - */ - removeNotesAfter(blockNumber: number): Promise; - - /** - * Restores notes nullified after the given block. - * @param blockNumber - All nullifiers strictly after this block are removed. - */ - unnullifyNotesAfter(blockNumber: number): Promise; - - /** - * Resets the indexes used to sync notes to 0 for every sender and recipient, causing the next sync process to - * start from scratch, taking longer than usual. - * This can help fix desynchronization issues, including finding logs that had previously been overlooked, and - * is also required to deal with chain reorgs. - */ - resetNoteSyncData(): Promise; - - /** - * Stores arbitrary information in a per-contract non-volatile database (called capsules), which can later - * be retrieved with `loadCapsule`. If data was already stored at this slot, it is overwritten. - * @param contractAddress - The contract address to scope the data under. - * @param slot - The slot in the database in which to store the value. Slots need not be contiguous. - * @param capsule - An array of field elements representing the capsule. - * @remarks A capsule is a "blob" of data that is passed to the contract through an oracle. It works similarly - * to public contract storage in that it's indexed by the contract address and storage slot but instead of the global - * network state it's backed by local PXE db. - */ - storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[]): Promise; - - /** - * Returns data previously stored via `storeCapsule` in the per-contract non-volatile database (called capsules). - * @param contractAddress - The contract address under which the data is scoped. - * @param slot - The slot in the database to read. - * @returns The stored data or `null` if no data is stored under the slot. - */ - loadCapsule(contractAddress: AztecAddress, slot: Fr): Promise; - - /** - * Deletes data in the per-contract non-volatile database (called capsules). Does nothing if no data was present. - * @param contractAddress - The contract address under which the data is scoped. - * @param slot - The slot in the database to delete. - */ - deleteCapsule(contractAddress: AztecAddress, slot: Fr): Promise; - - /** - * Copies a number of contiguous entries in the per-contract non-volatile database (called capsules). This allows for - * efficient data structures by avoiding repeated calls to `loadCapsule` and `storeCapsule`. - * Supports overlapping source and destination regions (which will result in the overlapped source values being - * overwritten). All copied slots must exist in the database (i.e. have been stored and not deleted) - * - * @param contractAddress - The contract address under which the data is scoped. - * @param srcSlot - The first slot to copy from. - * @param dstSlot - The first slot to copy to. - * @param numEntries - The number of entries to copy. - */ - copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise; -} diff --git a/yarn-project/pxe/src/database/interfaces/pxe_database_test_suite.ts b/yarn-project/pxe/src/database/interfaces/pxe_database_test_suite.ts deleted file mode 100644 index f6ae11547613..000000000000 --- a/yarn-project/pxe/src/database/interfaces/pxe_database_test_suite.ts +++ /dev/null @@ -1,558 +0,0 @@ -import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; -import { timesParallel } from '@aztec/foundation/collection'; -import { randomInt } from '@aztec/foundation/crypto'; -import { Fr, Point } from '@aztec/foundation/fields'; -import { BenchmarkingContractArtifact } from '@aztec/noir-contracts.js/Benchmarking'; -import { TestContractArtifact } from '@aztec/noir-contracts.js/Test'; -import { FunctionType } from '@aztec/stdlib/abi'; -import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { CompleteAddress, SerializableContractInstance } from '@aztec/stdlib/contract'; -import { PublicKeys } from '@aztec/stdlib/keys'; -import { NoteStatus, type NotesFilter } from '@aztec/stdlib/note'; -import { makeHeader, randomTxHash } from '@aztec/stdlib/testing'; - -import times from 'lodash.times'; - -import { NoteDao } from '../note_dao.js'; -import type { PxeDatabase } from './pxe_database.js'; - -/** - * A common test suite for a PXE database. - * @param getDatabase - A function that returns a database instance. - */ -export function describePxeDatabase(getDatabase: () => PxeDatabase) { - let database: PxeDatabase; - - beforeEach(() => { - database = getDatabase(); - }); - - describe('Database', () => { - describe('auth witnesses', () => { - it('stores and retrieves auth witnesses', async () => { - const messageHash = Fr.random(); - const witness = [Fr.random(), Fr.random()]; - - await database.addAuthWitness(messageHash, witness); - await expect(database.getAuthWitness(messageHash)).resolves.toEqual(witness); - }); - - it("returns undefined if it doesn't have auth witnesses for the message", async () => { - const messageHash = Fr.random(); - await expect(database.getAuthWitness(messageHash)).resolves.toBeUndefined(); - }); - - it.skip('refuses to overwrite auth witnesses for the same message', async () => { - const messageHash = Fr.random(); - const witness = [Fr.random(), Fr.random()]; - - await database.addAuthWitness(messageHash, witness); - await expect(database.addAuthWitness(messageHash, witness)).rejects.toThrow(); - }); - }); - - describe('incoming notes', () => { - let owners: CompleteAddress[]; - let contractAddresses: AztecAddress[]; - let storageSlots: Fr[]; - let notes: NoteDao[]; - - const filteringTests: [() => Promise, () => Promise][] = [ - [() => Promise.resolve({}), () => Promise.resolve(notes)], - - [ - () => Promise.resolve({ contractAddress: contractAddresses[0] }), - () => Promise.resolve(notes.filter(note => note.contractAddress.equals(contractAddresses[0]))), - ], - [async () => ({ contractAddress: await AztecAddress.random() }), () => Promise.resolve([])], - - [ - () => Promise.resolve({ storageSlot: storageSlots[0] }), - () => Promise.resolve(notes.filter(note => note.storageSlot.equals(storageSlots[0]))), - ], - [() => Promise.resolve({ storageSlot: Fr.random() }), () => Promise.resolve([])], - - [() => Promise.resolve({ txHash: notes[0].txHash }), () => Promise.resolve([notes[0]])], - [() => Promise.resolve({ txHash: randomTxHash() }), () => Promise.resolve([])], - - [ - () => Promise.resolve({ owner: owners[0].address }), - async () => { - const ownerAddressPoint = await owners[0].address.toAddressPoint(); - return notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); - }, - ], - - [ - () => Promise.resolve({ contractAddress: contractAddresses[0], storageSlot: storageSlots[0] }), - () => - Promise.resolve( - notes.filter( - note => note.contractAddress.equals(contractAddresses[0]) && note.storageSlot.equals(storageSlots[0]), - ), - ), - ], - [ - () => Promise.resolve({ contractAddress: contractAddresses[0], storageSlot: storageSlots[1] }), - () => Promise.resolve([]), - ], - ]; - - beforeEach(async () => { - owners = await timesParallel(2, () => CompleteAddress.random()); - contractAddresses = await timesParallel(2, () => AztecAddress.random()); - storageSlots = times(2, () => Fr.random()); - - notes = await timesParallel(10, async i => { - const addressPoint = await owners[i % owners.length].address.toAddressPoint(); - return NoteDao.random({ - contractAddress: contractAddresses[i % contractAddresses.length], - storageSlot: storageSlots[i % storageSlots.length], - addressPoint, - index: BigInt(i), - l2BlockNumber: i, - }); - }); - - for (const owner of owners) { - await database.addCompleteAddress(owner); - } - }); - - it.each(filteringTests)('stores notes in bulk and retrieves notes', async (getFilter, getExpected) => { - await database.addNotes(notes); - const returnedNotes = await database.getNotes(await getFilter()); - const expected = await getExpected(); - expect(returnedNotes.sort()).toEqual(expected.sort()); - }); - - it.each(filteringTests)('stores notes one by one and retrieves notes', async (getFilter, getExpected) => { - for (const note of notes) { - await database.addNote(note); - } - - const returnedNotes = await database.getNotes(await getFilter()); - - const expected = await getExpected(); - expect(returnedNotes.sort()).toEqual(expected.sort()); - }); - - it.each(filteringTests)('retrieves nullified notes', async (getFilter, getExpected) => { - await database.addNotes(notes); - - // Nullify all notes and use the same filter as other test cases - for (const owner of owners) { - const ownerAddressPoint = await owner.address.toAddressPoint(); - const notesToNullify = notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); - const nullifiers = notesToNullify.map(note => ({ - data: note.siloedNullifier, - l2BlockNumber: note.l2BlockNumber, - l2BlockHash: note.l2BlockHash, - })); - await expect(database.removeNullifiedNotes(nullifiers, ownerAddressPoint)).resolves.toEqual(notesToNullify); - } - const filter = await getFilter(); - const returnedNotes = await database.getNotes({ ...filter, status: NoteStatus.ACTIVE_OR_NULLIFIED }); - const expected = await getExpected(); - expect(returnedNotes.sort()).toEqual(expected.sort()); - }); - - it('skips nullified notes by default or when requesting active', async () => { - await database.addNotes(notes); - const ownerAddressPoint = await owners[0].address.toAddressPoint(); - const notesToNullify = notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); - const nullifiers = notesToNullify.map(note => ({ - data: note.siloedNullifier, - l2BlockNumber: note.l2BlockNumber, - l2BlockHash: note.l2BlockHash, - })); - await expect(database.removeNullifiedNotes(nullifiers, notesToNullify[0].addressPoint)).resolves.toEqual( - notesToNullify, - ); - - const actualNotesWithDefault = await database.getNotes({}); - const actualNotesWithActive = await database.getNotes({ status: NoteStatus.ACTIVE }); - - expect(actualNotesWithDefault).toEqual(actualNotesWithActive); - expect(actualNotesWithActive).toEqual(notes.filter(note => !notesToNullify.includes(note))); - }); - - it('handles note unnullification', async () => { - await database.setHeader(makeHeader(randomInt(1000), 100, 0 /** slot number */)); - await database.addNotes(notes); - const ownerAddressPoint = await owners[0].address.toAddressPoint(); - - const notesToNullify = notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); - const nullifiers = notesToNullify.map(note => ({ - data: note.siloedNullifier, - l2BlockNumber: 99, - l2BlockHash: Fr.random().toString(), - })); - await expect(database.removeNullifiedNotes(nullifiers, notesToNullify[0].addressPoint)).resolves.toEqual( - notesToNullify, - ); - await expect(database.unnullifyNotesAfter(98)).resolves.toEqual(undefined); - - const result = await database.getNotes({ status: NoteStatus.ACTIVE, owner: owners[0].address }); - - expect(result.sort()).toEqual([...notesToNullify].sort()); - }); - - it('returns active and nullified notes when requesting either', async () => { - await database.addNotes(notes); - const ownerAddressPoint = await owners[0].address.toAddressPoint(); - - const notesToNullify = notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); - const nullifiers = notesToNullify.map(note => ({ - data: note.siloedNullifier, - l2BlockNumber: note.l2BlockNumber, - l2BlockHash: note.l2BlockHash, - })); - await expect(database.removeNullifiedNotes(nullifiers, notesToNullify[0].addressPoint)).resolves.toEqual( - notesToNullify, - ); - - const result = await database.getNotes({ - status: NoteStatus.ACTIVE_OR_NULLIFIED, - }); - - // We have to compare the sorted arrays since the database does not return the same order as when originally - // inserted combining active and nullified results. - expect(result.sort()).toEqual([...notes].sort()); - }); - - it('stores notes one by one and retrieves notes with siloed account', async () => { - for (const note of notes.slice(0, 5)) { - await database.addNote(note, owners[0].address); - } - - for (const note of notes.slice(5)) { - await database.addNote(note, owners[1].address); - } - - const owner0Notes = await database.getNotes({ - scopes: [owners[0].address], - }); - - expect(owner0Notes.sort()).toEqual(notes.slice(0, 5).sort()); - - const owner1Notes = await database.getNotes({ - scopes: [owners[1].address], - }); - - expect(owner1Notes.sort()).toEqual(notes.slice(5).sort()); - - const bothOwnerNotes = await database.getNotes({ - scopes: [owners[0].address, owners[1].address], - }); - - expect(bothOwnerNotes.sort()).toEqual(notes.sort()); - }); - - it('a nullified note removes notes from all accounts in the pxe', async () => { - await database.addNote(notes[0], owners[0].address); - await database.addNote(notes[0], owners[1].address); - - await expect( - database.getNotes({ - scopes: [owners[0].address], - }), - ).resolves.toEqual([notes[0]]); - await expect( - database.getNotes({ - scopes: [owners[1].address], - }), - ).resolves.toEqual([notes[0]]); - const ownerAddressPoint = await owners[0].address.toAddressPoint(); - await expect( - database.removeNullifiedNotes( - [ - { - data: notes[0].siloedNullifier, - l2BlockHash: notes[0].l2BlockHash, - l2BlockNumber: notes[0].l2BlockNumber, - }, - ], - ownerAddressPoint, - ), - ).resolves.toEqual([notes[0]]); - - await expect( - database.getNotes({ - scopes: [owners[0].address], - }), - ).resolves.toEqual([]); - await expect( - database.getNotes({ - scopes: [owners[1].address], - }), - ).resolves.toEqual([]); - }); - - it('removes notes after a given block', async () => { - await database.addNotes(notes, owners[0].address); - - await database.removeNotesAfter(5); - const result = await database.getNotes({ scopes: [owners[0].address] }); - expect(new Set(result)).toEqual(new Set(notes.slice(0, 6))); - }); - }); - - describe('block header', () => { - it('stores and retrieves the block header', async () => { - const header = makeHeader(randomInt(1000), INITIAL_L2_BLOCK_NUM, 0 /** slot number */); - - await database.setHeader(header); - await expect(database.getBlockHeader()).resolves.toEqual(header); - }); - - it('rejects getting header if no block set', async () => { - await expect(() => database.getBlockHeader()).rejects.toThrow(); - }); - }); - - describe('addresses', () => { - it('stores and retrieves addresses', async () => { - const address = await CompleteAddress.random(); - await expect(database.addCompleteAddress(address)).resolves.toBe(true); - await expect(database.getCompleteAddress(address.address)).resolves.toEqual(address); - }); - - it('silently ignores an address it already knows about', async () => { - const address = await CompleteAddress.random(); - await expect(database.addCompleteAddress(address)).resolves.toBe(true); - await expect(database.addCompleteAddress(address)).resolves.toBe(false); - }); - - it.skip('refuses to overwrite an address with a different public key', async () => { - const address = await CompleteAddress.random(); - const otherAddress = await CompleteAddress.create( - address.address, - new PublicKeys(await Point.random(), await Point.random(), await Point.random(), await Point.random()), - address.partialAddress, - ); - - await database.addCompleteAddress(address); - await expect(database.addCompleteAddress(otherAddress)).rejects.toThrow(); - }); - - it('returns all addresses', async () => { - const addresses = await timesParallel(10, () => CompleteAddress.random()); - for (const address of addresses) { - await database.addCompleteAddress(address); - } - - const result = await database.getCompleteAddresses(); - expect(result).toEqual(expect.arrayContaining(addresses)); - }); - - it('returns a single address', async () => { - const addresses = await timesParallel(10, () => CompleteAddress.random()); - for (const address of addresses) { - await database.addCompleteAddress(address); - } - - const result = await database.getCompleteAddress(addresses[3].address); - expect(result).toEqual(addresses[3]); - }); - - it("returns an empty array if it doesn't have addresses", async () => { - expect(await database.getCompleteAddresses()).toEqual([]); - }); - - it("returns undefined if it doesn't have an address", async () => { - const completeAddress = await CompleteAddress.random(); - expect(await database.getCompleteAddress(completeAddress.address)).toBeUndefined(); - }); - }); - - describe('contracts', () => { - it('stores a contract artifact', async () => { - const artifact = BenchmarkingContractArtifact; - const id = Fr.random(); - await database.addContractArtifact(id, artifact); - await expect(database.getContractArtifact(id)).resolves.toEqual(artifact); - }); - - it('does not store a contract artifact with a duplicate private function selector', async () => { - const artifact = TestContractArtifact; - const index = artifact.functions.findIndex(fn => fn.functionType === FunctionType.PRIVATE); - - const copiedFn = structuredClone(artifact.functions[index]); - artifact.functions.push(copiedFn); - - const id = Fr.random(); - await expect(database.addContractArtifact(id, artifact)).rejects.toThrow( - 'Repeated function selectors of private functions', - ); - }); - - it('stores a contract instance', async () => { - const address = await AztecAddress.random(); - const instance = (await SerializableContractInstance.random()).withAddress(address); - await database.addContractInstance(instance); - await expect(database.getContractInstance(address)).resolves.toEqual(instance); - }); - }); - - describe('contract non-volatile database', () => { - let contract: AztecAddress; - - beforeEach(async () => { - // Setup mock contract address - contract = await AztecAddress.random(); - }); - - it('stores and loads a single value', async () => { - const slot = new Fr(1); - const values = [new Fr(42)]; - - await database.storeCapsule(contract, slot, values); - const result = await database.loadCapsule(contract, slot); - expect(result).toEqual(values); - }); - - it('stores and loads multiple values', async () => { - const slot = new Fr(1); - const values = [new Fr(42), new Fr(43), new Fr(44)]; - - await database.storeCapsule(contract, slot, values); - const result = await database.loadCapsule(contract, slot); - expect(result).toEqual(values); - }); - - it('overwrites existing values', async () => { - const slot = new Fr(1); - const initialValues = [new Fr(42)]; - const newValues = [new Fr(100)]; - - await database.storeCapsule(contract, slot, initialValues); - await database.storeCapsule(contract, slot, newValues); - - const result = await database.loadCapsule(contract, slot); - expect(result).toEqual(newValues); - }); - - it('stores values for different contracts independently', async () => { - const anotherContract = await AztecAddress.random(); - const slot = new Fr(1); - const values1 = [new Fr(42)]; - const values2 = [new Fr(100)]; - - await database.storeCapsule(contract, slot, values1); - await database.storeCapsule(anotherContract, slot, values2); - - const result1 = await database.loadCapsule(contract, slot); - const result2 = await database.loadCapsule(anotherContract, slot); - - expect(result1).toEqual(values1); - expect(result2).toEqual(values2); - }); - - it('returns null for non-existent slots', async () => { - const slot = Fr.random(); - const result = await database.loadCapsule(contract, slot); - expect(result).toBeNull(); - }); - - it('deletes a slot', async () => { - const slot = new Fr(1); - const values = [new Fr(42)]; - - await database.storeCapsule(contract, slot, values); - await database.deleteCapsule(contract, slot); - - expect(await database.loadCapsule(contract, slot)).toBeNull(); - }); - - it('deletes an empty slot', async () => { - const slot = new Fr(1); - await database.deleteCapsule(contract, slot); - - expect(await database.loadCapsule(contract, slot)).toBeNull(); - }); - - it('copies a single value', async () => { - const slot = new Fr(1); - const values = [new Fr(42)]; - - await database.storeCapsule(contract, slot, values); - - const dstSlot = new Fr(5); - await database.copyCapsule(contract, slot, dstSlot, 1); - - expect(await database.loadCapsule(contract, dstSlot)).toEqual(values); - }); - - it('copies multiple non-overlapping values', async () => { - const src = new Fr(1); - const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - - await database.storeCapsule(contract, src, valuesArray[0]); - await database.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1]); - await database.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2]); - - const dst = new Fr(5); - await database.copyCapsule(contract, src, dst, 3); - - expect(await database.loadCapsule(contract, dst)).toEqual(valuesArray[0]); - expect(await database.loadCapsule(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); - expect(await database.loadCapsule(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); - }); - - it('copies overlapping values with src ahead', async () => { - const src = new Fr(1); - const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - - await database.storeCapsule(contract, src, valuesArray[0]); - await database.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1]); - await database.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2]); - - const dst = new Fr(2); - await database.copyCapsule(contract, src, dst, 3); - - expect(await database.loadCapsule(contract, dst)).toEqual(valuesArray[0]); - expect(await database.loadCapsule(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); - expect(await database.loadCapsule(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); - - // Slots 2 and 3 (src[1] and src[2]) should have been overwritten since they are also dst[0] and dst[1] - expect(await database.loadCapsule(contract, src)).toEqual(valuesArray[0]); // src[0] (unchanged) - expect(await database.loadCapsule(contract, src.add(new Fr(1)))).toEqual(valuesArray[0]); // dst[0] - expect(await database.loadCapsule(contract, src.add(new Fr(2)))).toEqual(valuesArray[1]); // dst[1] - }); - - it('copies overlapping values with dst ahead', async () => { - const src = new Fr(5); - const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - - await database.storeCapsule(contract, src, valuesArray[0]); - await database.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1]); - await database.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2]); - - const dst = new Fr(4); - await database.copyCapsule(contract, src, dst, 3); - - expect(await database.loadCapsule(contract, dst)).toEqual(valuesArray[0]); - expect(await database.loadCapsule(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); - expect(await database.loadCapsule(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); - - // Slots 5 and 6 (src[0] and src[1]) should have been overwritten since they are also dst[1] and dst[2] - expect(await database.loadCapsule(contract, src)).toEqual(valuesArray[1]); // dst[1] - expect(await database.loadCapsule(contract, src.add(new Fr(1)))).toEqual(valuesArray[2]); // dst[2] - expect(await database.loadCapsule(contract, src.add(new Fr(2)))).toEqual(valuesArray[2]); // src[2] (unchanged) - }); - - it('copying fails if any value is empty', async () => { - const src = new Fr(1); - const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - - await database.storeCapsule(contract, src, valuesArray[0]); - // We skip src[1] - await database.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2]); - - const dst = new Fr(5); - await expect(database.copyCapsule(contract, src, dst, 3)).rejects.toThrow('Attempted to copy empty slot'); - }); - }); - }); -} diff --git a/yarn-project/pxe/src/database/kv_pxe_database.test.ts b/yarn-project/pxe/src/database/kv_pxe_database.test.ts deleted file mode 100644 index 294f219fa628..000000000000 --- a/yarn-project/pxe/src/database/kv_pxe_database.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; - -import { describePxeDatabase } from './interfaces/pxe_database_test_suite.js'; -import { KVPxeDatabase } from './kv_pxe_database.js'; - -describe('KVPxeDatabase', () => { - let database: KVPxeDatabase; - - beforeEach(async () => { - database = await KVPxeDatabase.create(await openTmpStore('test')); - }); - - describePxeDatabase(() => database); -}); diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts deleted file mode 100644 index dd76d73b5daa..000000000000 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ /dev/null @@ -1,670 +0,0 @@ -import { toBufferBE } from '@aztec/foundation/bigint-buffer'; -import { Fr, type Point } from '@aztec/foundation/fields'; -import { toArray } from '@aztec/foundation/iterable'; -import { type LogFn, createDebugOnlyLogger } from '@aztec/foundation/log'; -import type { - AztecAsyncArray, - AztecAsyncKVStore, - AztecAsyncMap, - AztecAsyncMultiMap, - AztecAsyncSingleton, -} from '@aztec/kv-store'; -import { type ContractArtifact, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; -import { contractArtifactFromBuffer, contractArtifactToBuffer } from '@aztec/stdlib/abi'; -import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { InBlock } from '@aztec/stdlib/block'; -import { - CompleteAddress, - type ContractInstanceWithAddress, - SerializableContractInstance, -} from '@aztec/stdlib/contract'; -import type { PublicKey } from '@aztec/stdlib/keys'; -import type { IndexedTaggingSecret } from '@aztec/stdlib/logs'; -import { NoteStatus, type NotesFilter } from '@aztec/stdlib/note'; -import { MerkleTreeId } from '@aztec/stdlib/trees'; -import { BlockHeader } from '@aztec/stdlib/tx'; - -import type { PxeDatabase } from './interfaces/pxe_database.js'; -import { NoteDao } from './note_dao.js'; - -/** - * A PXE database backed by LMDB. - */ -export class KVPxeDatabase implements PxeDatabase { - public static readonly SCHEMA_VERSION = 1; - - #synchronizedBlock: AztecAsyncSingleton; - #completeAddresses: AztecAsyncArray; - #completeAddressIndex: AztecAsyncMap; - #addressBook: AztecAsyncMap; - #authWitnesses: AztecAsyncMap; - #notes: AztecAsyncMap; - #nullifiedNotes: AztecAsyncMap; - #nullifierToNoteId: AztecAsyncMap; - #nullifiersByBlockNumber: AztecAsyncMultiMap; - - #nullifiedNotesToScope: AztecAsyncMultiMap; - #nullifiedNotesByContract: AztecAsyncMultiMap; - #nullifiedNotesByStorageSlot: AztecAsyncMultiMap; - #nullifiedNotesByTxHash: AztecAsyncMultiMap; - #nullifiedNotesByAddressPoint: AztecAsyncMultiMap; - #nullifiedNotesByNullifier: AztecAsyncMap; - #contractArtifacts: AztecAsyncMap; - #contractInstances: AztecAsyncMap; - #db: AztecAsyncKVStore; - - #scopes: AztecAsyncMap; - #notesToScope: AztecAsyncMultiMap; - #notesByContractAndScope: Map>; - #notesByStorageSlotAndScope: Map>; - #notesByTxHashAndScope: Map>; - #notesByAddressPointAndScope: Map>; - - // Stores the last index used for each tagging secret, taking direction into account - // This is necessary to avoid reusing the same index for the same secret, which happens if - // sender and recipient are the same - #taggingSecretIndexesForSenders: AztecAsyncMap; - #taggingSecretIndexesForRecipients: AztecAsyncMap; - - // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${key}` - #capsules: AztecAsyncMap; - - debug: LogFn; - - protected constructor(private db: AztecAsyncKVStore) { - this.#db = db; - - this.#completeAddresses = db.openArray('complete_addresses'); - this.#completeAddressIndex = db.openMap('complete_address_index'); - - this.#addressBook = db.openMap('address_book'); - - this.#authWitnesses = db.openMap('auth_witnesses'); - - this.#contractArtifacts = db.openMap('contract_artifacts'); - this.#contractInstances = db.openMap('contracts_instances'); - - this.#synchronizedBlock = db.openSingleton('header'); - - this.#notes = db.openMap('notes'); - this.#nullifiedNotes = db.openMap('nullified_notes'); - this.#nullifierToNoteId = db.openMap('nullifier_to_note'); - this.#nullifiersByBlockNumber = db.openMultiMap('nullifier_to_block_number'); - - this.#nullifiedNotesToScope = db.openMultiMap('nullified_notes_to_scope'); - this.#nullifiedNotesByContract = db.openMultiMap('nullified_notes_by_contract'); - this.#nullifiedNotesByStorageSlot = db.openMultiMap('nullified_notes_by_storage_slot'); - this.#nullifiedNotesByTxHash = db.openMultiMap('nullified_notes_by_tx_hash'); - this.#nullifiedNotesByAddressPoint = db.openMultiMap('nullified_notes_by_address_point'); - this.#nullifiedNotesByNullifier = db.openMap('nullified_notes_by_nullifier'); - - this.#scopes = db.openMap('scopes'); - this.#notesToScope = db.openMultiMap('notes_to_scope'); - this.#notesByContractAndScope = new Map>(); - this.#notesByStorageSlotAndScope = new Map>(); - this.#notesByTxHashAndScope = new Map>(); - this.#notesByAddressPointAndScope = new Map>(); - - this.#taggingSecretIndexesForSenders = db.openMap('tagging_secret_indexes_for_senders'); - this.#taggingSecretIndexesForRecipients = db.openMap('tagging_secret_indexes_for_recipients'); - - this.#capsules = db.openMap('capsules'); - - this.debug = createDebugOnlyLogger('aztec:kv-pxe-database'); - } - - public static async create(db: AztecAsyncKVStore): Promise { - const pxeDB = new KVPxeDatabase(db); - for await (const scope of pxeDB.#scopes.keysAsync()) { - pxeDB.#notesByContractAndScope.set(scope, db.openMultiMap(`${scope}:notes_by_contract`)); - pxeDB.#notesByStorageSlotAndScope.set(scope, db.openMultiMap(`${scope}:notes_by_storage_slot`)); - pxeDB.#notesByTxHashAndScope.set(scope, db.openMultiMap(`${scope}:notes_by_tx_hash`)); - pxeDB.#notesByAddressPointAndScope.set(scope, db.openMultiMap(`${scope}:notes_by_address_point`)); - } - return pxeDB; - } - - public async getContract( - address: AztecAddress, - ): Promise<(ContractInstanceWithAddress & ContractArtifact) | undefined> { - const instance = await this.getContractInstance(address); - const artifact = instance && (await this.getContractArtifact(instance?.currentContractClassId)); - if (!instance || !artifact) { - return undefined; - } - return { ...instance, ...artifact }; - } - - public async addContractArtifact(id: Fr, contract: ContractArtifact): Promise { - const privateFunctions = contract.functions.filter( - functionArtifact => functionArtifact.functionType === FunctionType.PRIVATE, - ); - - const privateSelectors = await Promise.all( - privateFunctions.map(async privateFunctionArtifact => - ( - await FunctionSelector.fromNameAndParameters(privateFunctionArtifact.name, privateFunctionArtifact.parameters) - ).toString(), - ), - ); - - if (privateSelectors.length !== new Set(privateSelectors).size) { - throw new Error('Repeated function selectors of private functions'); - } - - await this.#contractArtifacts.set(id.toString(), contractArtifactToBuffer(contract)); - } - - public async getContractArtifact(id: Fr): Promise { - const contract = await this.#contractArtifacts.getAsync(id.toString()); - // TODO(@spalladino): AztecAsyncMap lies and returns Uint8Arrays instead of Buffers, hence the extra Buffer.from. - return contract && contractArtifactFromBuffer(Buffer.from(contract)); - } - - async addContractInstance(contract: ContractInstanceWithAddress): Promise { - await this.#contractInstances.set( - contract.address.toString(), - new SerializableContractInstance(contract).toBuffer(), - ); - } - - async getContractInstance(address: AztecAddress): Promise { - const contract = await this.#contractInstances.getAsync(address.toString()); - return contract && SerializableContractInstance.fromBuffer(contract).withAddress(address); - } - - async getContractsAddresses(): Promise { - const keys = await toArray(this.#contractInstances.keysAsync()); - return keys.map(AztecAddress.fromString); - } - - async addAuthWitness(messageHash: Fr, witness: Fr[]): Promise { - await this.#authWitnesses.set( - messageHash.toString(), - witness.map(w => w.toBuffer()), - ); - } - - async getAuthWitness(messageHash: Fr): Promise { - const witness = await this.#authWitnesses.getAsync(messageHash.toString()); - return Promise.resolve(witness?.map(w => Fr.fromBuffer(w))); - } - - async addNote(note: NoteDao, scope?: AztecAddress): Promise { - await this.addNotes([note], scope); - } - - async addNotes(notes: NoteDao[], scope: AztecAddress = AztecAddress.ZERO): Promise { - if (!(await this.#scopes.hasAsync(scope.toString()))) { - await this.#addScope(scope); - } - - return this.db.transactionAsync(async () => { - for (const dao of notes) { - // store notes by their index in the notes hash tree - // this provides the uniqueness we need to store individual notes - // and should also return notes in the order that they were created. - // Had we stored them by their nullifier, they would be returned in random order - const noteIndex = toBufferBE(dao.index, 32).toString('hex'); - await this.#notes.set(noteIndex, dao.toBuffer()); - await this.#notesToScope.set(noteIndex, scope.toString()); - await this.#nullifierToNoteId.set(dao.siloedNullifier.toString(), noteIndex); - - await this.#notesByContractAndScope.get(scope.toString())!.set(dao.contractAddress.toString(), noteIndex); - await this.#notesByStorageSlotAndScope.get(scope.toString())!.set(dao.storageSlot.toString(), noteIndex); - await this.#notesByTxHashAndScope.get(scope.toString())!.set(dao.txHash.toString(), noteIndex); - await this.#notesByAddressPointAndScope.get(scope.toString())!.set(dao.addressPoint.toString(), noteIndex); - } - }); - } - - public removeNotesAfter(blockNumber: number): Promise { - return this.db.transactionAsync(async () => { - const notes = await toArray(this.#notes.valuesAsync()); - for (const note of notes) { - const noteDao = NoteDao.fromBuffer(note); - if (noteDao.l2BlockNumber > blockNumber) { - const noteIndex = toBufferBE(noteDao.index, 32).toString('hex'); - await this.#notes.delete(noteIndex); - await this.#notesToScope.delete(noteIndex); - await this.#nullifierToNoteId.delete(noteDao.siloedNullifier.toString()); - const scopes = await toArray(this.#scopes.keysAsync()); - for (const scope of scopes) { - await this.#notesByAddressPointAndScope.get(scope)!.deleteValue(noteDao.addressPoint.toString(), noteIndex); - await this.#notesByTxHashAndScope.get(scope)!.deleteValue(noteDao.txHash.toString(), noteIndex); - await this.#notesByContractAndScope.get(scope)!.deleteValue(noteDao.contractAddress.toString(), noteIndex); - await this.#notesByStorageSlotAndScope.get(scope)!.deleteValue(noteDao.storageSlot.toString(), noteIndex); - } - } - } - }); - } - - public async unnullifyNotesAfter(blockNumber: number): Promise { - const nullifiersToUndo: string[] = []; - const currentBlockNumber = blockNumber + 1; - const maxBlockNumber = (await this.getBlockNumber()) ?? currentBlockNumber; - for (let i = currentBlockNumber; i <= maxBlockNumber; i++) { - nullifiersToUndo.push(...(await toArray(this.#nullifiersByBlockNumber.getValuesAsync(i)))); - } - - const notesIndexesToReinsert = await Promise.all( - nullifiersToUndo.map(nullifier => this.#nullifiedNotesByNullifier.getAsync(nullifier)), - ); - const notNullNoteIndexes = notesIndexesToReinsert.filter(noteIndex => noteIndex != undefined); - const nullifiedNoteBuffers = await Promise.all( - notNullNoteIndexes.map(noteIndex => this.#nullifiedNotes.getAsync(noteIndex!)), - ); - const noteDaos = nullifiedNoteBuffers - .filter(buffer => buffer != undefined) - .map(buffer => NoteDao.fromBuffer(buffer!)); - - await this.db.transactionAsync(async () => { - for (const dao of noteDaos) { - const noteIndex = toBufferBE(dao.index, 32).toString('hex'); - await this.#notes.set(noteIndex, dao.toBuffer()); - await this.#nullifierToNoteId.set(dao.siloedNullifier.toString(), noteIndex); - - let scopes = (await toArray(this.#nullifiedNotesToScope.getValuesAsync(noteIndex))) ?? []; - - if (scopes.length === 0) { - scopes = [new AztecAddress(dao.addressPoint.x).toString()]; - } - - for (const scope of scopes) { - await this.#notesByContractAndScope.get(scope.toString())!.set(dao.contractAddress.toString(), noteIndex); - await this.#notesByStorageSlotAndScope.get(scope.toString())!.set(dao.storageSlot.toString(), noteIndex); - await this.#notesByTxHashAndScope.get(scope.toString())!.set(dao.txHash.toString(), noteIndex); - await this.#notesByAddressPointAndScope.get(scope.toString())!.set(dao.addressPoint.toString(), noteIndex); - await this.#notesToScope.set(noteIndex, scope); - } - - await this.#nullifiedNotes.delete(noteIndex); - await this.#nullifiedNotesToScope.delete(noteIndex); - await this.#nullifiersByBlockNumber.deleteValue(dao.l2BlockNumber, dao.siloedNullifier.toString()); - await this.#nullifiedNotesByContract.deleteValue(dao.contractAddress.toString(), noteIndex); - await this.#nullifiedNotesByStorageSlot.deleteValue(dao.storageSlot.toString(), noteIndex); - await this.#nullifiedNotesByTxHash.deleteValue(dao.txHash.toString(), noteIndex); - await this.#nullifiedNotesByAddressPoint.deleteValue(dao.addressPoint.toString(), noteIndex); - await this.#nullifiedNotesByNullifier.delete(dao.siloedNullifier.toString()); - } - }); - } - - async getNotes(filter: NotesFilter): Promise { - const publicKey: PublicKey | undefined = filter.owner ? await filter.owner.toAddressPoint() : undefined; - - filter.status = filter.status ?? NoteStatus.ACTIVE; - - const candidateNoteSources = []; - - filter.scopes ??= (await toArray(this.#scopes.keysAsync())).map(addressString => - AztecAddress.fromString(addressString), - ); - - const activeNoteIdsPerScope: string[][] = []; - - for (const scope of new Set(filter.scopes)) { - const formattedScopeString = scope.toString(); - if (!(await this.#scopes.hasAsync(formattedScopeString))) { - throw new Error('Trying to get incoming notes of an scope that is not in the PXE database'); - } - - activeNoteIdsPerScope.push( - publicKey - ? await toArray( - this.#notesByAddressPointAndScope.get(formattedScopeString)!.getValuesAsync(publicKey.toString()), - ) - : filter.txHash - ? await toArray( - this.#notesByTxHashAndScope.get(formattedScopeString)!.getValuesAsync(filter.txHash.toString()), - ) - : filter.contractAddress - ? await toArray( - this.#notesByContractAndScope - .get(formattedScopeString)! - .getValuesAsync(filter.contractAddress.toString()), - ) - : filter.storageSlot - ? await toArray( - this.#notesByStorageSlotAndScope.get(formattedScopeString)!.getValuesAsync(filter.storageSlot.toString()), - ) - : await toArray(this.#notesByAddressPointAndScope.get(formattedScopeString)!.valuesAsync()), - ); - } - - candidateNoteSources.push({ - ids: new Set(activeNoteIdsPerScope.flat()), - notes: this.#notes, - }); - - if (filter.status == NoteStatus.ACTIVE_OR_NULLIFIED) { - candidateNoteSources.push({ - ids: publicKey - ? await toArray(this.#nullifiedNotesByAddressPoint.getValuesAsync(publicKey.toString())) - : filter.txHash - ? await toArray(this.#nullifiedNotesByTxHash.getValuesAsync(filter.txHash.toString())) - : filter.contractAddress - ? await toArray(this.#nullifiedNotesByContract.getValuesAsync(filter.contractAddress.toString())) - : filter.storageSlot - ? await toArray(this.#nullifiedNotesByStorageSlot.getValuesAsync(filter.storageSlot.toString())) - : await toArray(this.#nullifiedNotes.keysAsync()), - notes: this.#nullifiedNotes, - }); - } - - const result: NoteDao[] = []; - for (const { ids, notes } of candidateNoteSources) { - for (const id of ids) { - const serializedNote = await notes.getAsync(id); - if (!serializedNote) { - continue; - } - - const note = NoteDao.fromBuffer(serializedNote); - if (filter.contractAddress && !note.contractAddress.equals(filter.contractAddress)) { - continue; - } - - if (filter.txHash && !note.txHash.equals(filter.txHash)) { - continue; - } - - if (filter.storageSlot && !note.storageSlot.equals(filter.storageSlot!)) { - continue; - } - - if (publicKey && !note.addressPoint.equals(publicKey)) { - continue; - } - - if (filter.siloedNullifier && !note.siloedNullifier.equals(filter.siloedNullifier)) { - continue; - } - - result.push(note); - } - } - - return result; - } - - removeNullifiedNotes(nullifiers: InBlock[], accountAddressPoint: Point): Promise { - if (nullifiers.length === 0) { - return Promise.resolve([]); - } - - return this.db.transactionAsync(async () => { - const nullifiedNotes: NoteDao[] = []; - - for (const blockScopedNullifier of nullifiers) { - const { data: nullifier, l2BlockNumber: blockNumber } = blockScopedNullifier; - const noteIndex = await this.#nullifierToNoteId.getAsync(nullifier.toString()); - if (!noteIndex) { - continue; - } - - const noteBuffer = noteIndex ? await this.#notes.getAsync(noteIndex) : undefined; - - if (!noteBuffer) { - // note doesn't exist. Maybe it got nullified already - continue; - } - const noteScopes = (await toArray(this.#notesToScope.getValuesAsync(noteIndex))) ?? []; - const note = NoteDao.fromBuffer(noteBuffer); - if (!note.addressPoint.equals(accountAddressPoint)) { - // tried to nullify someone else's note - continue; - } - - nullifiedNotes.push(note); - - await this.#notes.delete(noteIndex); - await this.#notesToScope.delete(noteIndex); - - const scopes = await toArray(this.#scopes.keysAsync()); - - for (const scope of scopes) { - await this.#notesByAddressPointAndScope.get(scope)!.deleteValue(accountAddressPoint.toString(), noteIndex); - await this.#notesByTxHashAndScope.get(scope)!.deleteValue(note.txHash.toString(), noteIndex); - await this.#notesByContractAndScope.get(scope)!.deleteValue(note.contractAddress.toString(), noteIndex); - await this.#notesByStorageSlotAndScope.get(scope)!.deleteValue(note.storageSlot.toString(), noteIndex); - } - - if (noteScopes !== undefined) { - for (const scope of noteScopes) { - await this.#nullifiedNotesToScope.set(noteIndex, scope); - } - } - await this.#nullifiedNotes.set(noteIndex, note.toBuffer()); - await this.#nullifiersByBlockNumber.set(blockNumber, nullifier.toString()); - await this.#nullifiedNotesByContract.set(note.contractAddress.toString(), noteIndex); - await this.#nullifiedNotesByStorageSlot.set(note.storageSlot.toString(), noteIndex); - await this.#nullifiedNotesByTxHash.set(note.txHash.toString(), noteIndex); - await this.#nullifiedNotesByAddressPoint.set(note.addressPoint.toString(), noteIndex); - await this.#nullifiedNotesByNullifier.set(nullifier.toString(), noteIndex); - - await this.#nullifierToNoteId.delete(nullifier.toString()); - } - return nullifiedNotes; - }); - } - - async addNullifiedNote(note: NoteDao): Promise { - const noteIndex = toBufferBE(note.index, 32).toString('hex'); - - await this.#nullifiedNotes.set(noteIndex, note.toBuffer()); - await this.#nullifiedNotesByContract.set(note.contractAddress.toString(), noteIndex); - await this.#nullifiedNotesByStorageSlot.set(note.storageSlot.toString(), noteIndex); - await this.#nullifiedNotesByTxHash.set(note.txHash.toString(), noteIndex); - await this.#nullifiedNotesByAddressPoint.set(note.addressPoint.toString(), noteIndex); - } - - async setHeader(header: BlockHeader): Promise { - await this.#synchronizedBlock.set(header.toBuffer()); - } - - async getBlockNumber(): Promise { - const headerBuffer = await this.#synchronizedBlock.getAsync(); - if (!headerBuffer) { - return undefined; - } - - return Number(BlockHeader.fromBuffer(headerBuffer).globalVariables.blockNumber.toBigInt()); - } - - async getBlockHeader(): Promise { - const headerBuffer = await this.#synchronizedBlock.getAsync(); - if (!headerBuffer) { - throw new Error(`Header not set`); - } - - return BlockHeader.fromBuffer(headerBuffer); - } - - async #addScope(scope: AztecAddress): Promise { - const scopeString = scope.toString(); - - if (await this.#scopes.hasAsync(scopeString)) { - return false; - } - - await this.#scopes.set(scopeString, true); - this.#notesByContractAndScope.set(scopeString, this.#db.openMultiMap(`${scopeString}:notes_by_contract`)); - this.#notesByStorageSlotAndScope.set(scopeString, this.#db.openMultiMap(`${scopeString}:notes_by_storage_slot`)); - this.#notesByTxHashAndScope.set(scopeString, this.#db.openMultiMap(`${scopeString}:notes_by_tx_hash`)); - this.#notesByAddressPointAndScope.set(scopeString, this.#db.openMultiMap(`${scopeString}:notes_by_address_point`)); - - return true; - } - - addCompleteAddress(completeAddress: CompleteAddress): Promise { - return this.db.transactionAsync(async () => { - await this.#addScope(completeAddress.address); - - const addressString = completeAddress.address.toString(); - const buffer = completeAddress.toBuffer(); - const existing = await this.#completeAddressIndex.getAsync(addressString); - if (existing === undefined) { - const index = await this.#completeAddresses.lengthAsync(); - await this.#completeAddresses.push(buffer); - await this.#completeAddressIndex.set(addressString, index); - - return true; - } else { - const existingBuffer = await this.#completeAddresses.atAsync(existing); - - if (existingBuffer && Buffer.from(existingBuffer).equals(buffer)) { - return false; - } - - throw new Error( - `Complete address with aztec address ${addressString} but different public key or partial key already exists in memory database`, - ); - } - }); - } - - async #getCompleteAddress(address: AztecAddress): Promise { - const index = await this.#completeAddressIndex.getAsync(address.toString()); - if (index === undefined) { - return undefined; - } - - const value = await this.#completeAddresses.atAsync(index); - return value ? await CompleteAddress.fromBuffer(value) : undefined; - } - - getCompleteAddress(account: AztecAddress): Promise { - return this.#getCompleteAddress(account); - } - - async getCompleteAddresses(): Promise { - return await Promise.all( - (await toArray(this.#completeAddresses.valuesAsync())).map(v => CompleteAddress.fromBuffer(v)), - ); - } - - async addSenderAddress(address: AztecAddress): Promise { - if (await this.#addressBook.hasAsync(address.toString())) { - return false; - } - - await this.#addressBook.set(address.toString(), true); - - return true; - } - - async getSenderAddresses(): Promise { - return (await toArray(this.#addressBook.keysAsync())).map(AztecAddress.fromString); - } - - async removeSenderAddress(address: AztecAddress): Promise { - if (!(await this.#addressBook.hasAsync(address.toString()))) { - return false; - } - - await this.#addressBook.delete(address.toString()); - - return true; - } - - async estimateSize(): Promise { - const noteSize = (await this.getNotes({})).reduce((sum, note) => sum + note.getSize(), 0); - - const authWitsSize = (await toArray(this.#authWitnesses.valuesAsync())).reduce( - (sum, value) => sum + value.length * Fr.SIZE_IN_BYTES, - 0, - ); - const addressesSize = (await this.#completeAddresses.lengthAsync()) * CompleteAddress.SIZE_IN_BYTES; - const treeRootsSize = Object.keys(MerkleTreeId).length * Fr.SIZE_IN_BYTES; - - return noteSize + treeRootsSize + authWitsSize + addressesSize; - } - - async setTaggingSecretsIndexesAsSender(indexedSecrets: IndexedTaggingSecret[]): Promise { - await this.#setTaggingSecretsIndexes(indexedSecrets, this.#taggingSecretIndexesForSenders); - } - - async setTaggingSecretsIndexesAsRecipient(indexedSecrets: IndexedTaggingSecret[]): Promise { - await this.#setTaggingSecretsIndexes(indexedSecrets, this.#taggingSecretIndexesForRecipients); - } - - async #setTaggingSecretsIndexes(indexedSecrets: IndexedTaggingSecret[], storageMap: AztecAsyncMap) { - await Promise.all( - indexedSecrets.map(indexedSecret => - storageMap.set(indexedSecret.appTaggingSecret.toString(), indexedSecret.index), - ), - ); - } - - async getTaggingSecretsIndexesAsRecipient(appTaggingSecrets: Fr[]) { - return await this.#getTaggingSecretsIndexes(appTaggingSecrets, this.#taggingSecretIndexesForRecipients); - } - - async getTaggingSecretsIndexesAsSender(appTaggingSecrets: Fr[]) { - return await this.#getTaggingSecretsIndexes(appTaggingSecrets, this.#taggingSecretIndexesForSenders); - } - - #getTaggingSecretsIndexes(appTaggingSecrets: Fr[], storageMap: AztecAsyncMap): Promise { - return Promise.all(appTaggingSecrets.map(async secret => (await storageMap.getAsync(`${secret.toString()}`)) ?? 0)); - } - - resetNoteSyncData(): Promise { - return this.db.transactionAsync(async () => { - const recipients = await toArray(this.#taggingSecretIndexesForRecipients.keysAsync()); - await Promise.all(recipients.map(recipient => this.#taggingSecretIndexesForRecipients.delete(recipient))); - const senders = await toArray(this.#taggingSecretIndexesForSenders.keysAsync()); - await Promise.all(senders.map(sender => this.#taggingSecretIndexesForSenders.delete(sender))); - }); - } - - async storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[]): Promise { - await this.#capsules.set(dbSlotToKey(contractAddress, slot), Buffer.concat(capsule.map(value => value.toBuffer()))); - } - - async loadCapsule(contractAddress: AztecAddress, slot: Fr): Promise { - const dataBuffer = await this.#capsules.getAsync(dbSlotToKey(contractAddress, slot)); - if (!dataBuffer) { - this.debug(`Data not found for contract ${contractAddress.toString()} and slot ${slot.toString()}`); - return null; - } - const capsule: Fr[] = []; - for (let i = 0; i < dataBuffer.length; i += Fr.SIZE_IN_BYTES) { - capsule.push(Fr.fromBuffer(dataBuffer.subarray(i, i + Fr.SIZE_IN_BYTES))); - } - return capsule; - } - - async deleteCapsule(contractAddress: AztecAddress, slot: Fr): Promise { - await this.#capsules.delete(dbSlotToKey(contractAddress, slot)); - } - - async copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { - // In order to support overlapping source and destination regions, we need to check the relative positions of source - // and destination. If destination is ahead of source, then by the time we overwrite source elements using forward - // indexes we'll have already read those. On the contrary, if source is ahead of destination we need to use backward - // indexes to avoid reading elements that've been overwritten. - - const indexes = Array.from(Array(numEntries).keys()); - if (srcSlot.lt(dstSlot)) { - indexes.reverse(); - } - - for (const i of indexes) { - const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i))); - const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i))); - - const toCopy = await this.#capsules.getAsync(currentSrcSlot); - if (!toCopy) { - throw new Error(`Attempted to copy empty slot ${currentSrcSlot} for contract ${contractAddress.toString()}`); - } - - await this.#capsules.set(currentDstSlot, toCopy); - } - } -} - -function dbSlotToKey(contractAddress: AztecAddress, slot: Fr): string { - return `${contractAddress.toString()}:${slot.toString()}`; -} diff --git a/yarn-project/pxe/src/entrypoints/client/bundle/index.ts b/yarn-project/pxe/src/entrypoints/client/bundle/index.ts new file mode 100644 index 000000000000..3f4a339f0e71 --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/client/bundle/index.ts @@ -0,0 +1,5 @@ +export * from '../../../pxe_service/index.js'; +export * from '../../../config/index.js'; +export * from '../../../storage/index.js'; +export * from './utils.js'; +export { PXEOracleInterface } from '../../../pxe_oracle_interface/index.js'; diff --git a/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts b/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts new file mode 100644 index 000000000000..b686a09116e0 --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts @@ -0,0 +1,58 @@ +import { BBWASMBundlePrivateKernelProver } from '@aztec/bb-prover/wasm/bundle'; +import { createLogger } from '@aztec/foundation/log'; +import { createStore } from '@aztec/kv-store/indexeddb'; +import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; +import { WASMSimulator } from '@aztec/simulator/client'; +import type { AztecNode } from '@aztec/stdlib/interfaces/client'; + +import type { PXEServiceConfig } from '../../../config/index.js'; +import { PXEService } from '../../../pxe_service/pxe_service.js'; +import type { PXECreationOptions } from '../pxe_creation_options.js'; + +/** + * Create and start an PXEService instance with the given AztecNode. + * If no keyStore or database is provided, it will use KeyStore and MemoryDB as default values. + * Returns a Promise that resolves to the started PXEService instance. + * + * @param aztecNode - The AztecNode instance to be used by the server. + * @param config - The PXE Service Config to use + * @param options - (Optional) Optional information for creating an PXEService. + * @returns A Promise that resolves to the started PXEService instance. + */ +export async function createPXEService( + aztecNode: AztecNode, + config: PXEServiceConfig, + options: PXECreationOptions = { loggers: {} }, +) { + const l1Contracts = await aztecNode.getL1ContractAddresses(); + const configWithContracts = { + ...config, + l1Contracts, + } as PXEServiceConfig; + + const store = await createStore( + 'pxe_data', + configWithContracts, + options.loggers.store ?? createLogger('pxe:data:indexeddb'), + ); + + const simulationProvider = new WASMSimulator(); + const prover = + options.prover ?? + new BBWASMBundlePrivateKernelProver( + simulationProvider, + 16, + options.loggers.prover ?? createLogger('bb:wasm:bundle'), + ); + const protocolContractsProvider = new BundledProtocolContractsProvider(); + const pxe = await PXEService.create( + aztecNode, + store, + prover, + simulationProvider, + protocolContractsProvider, + config, + options.loggers.pxe ?? createLogger('pxe:service'), + ); + return pxe; +} diff --git a/yarn-project/pxe/src/entrypoints/client/lazy/index.ts b/yarn-project/pxe/src/entrypoints/client/lazy/index.ts new file mode 100644 index 000000000000..3f4a339f0e71 --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/client/lazy/index.ts @@ -0,0 +1,5 @@ +export * from '../../../pxe_service/index.js'; +export * from '../../../config/index.js'; +export * from '../../../storage/index.js'; +export * from './utils.js'; +export { PXEOracleInterface } from '../../../pxe_oracle_interface/index.js'; diff --git a/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts b/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts new file mode 100644 index 000000000000..bd5877e25177 --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts @@ -0,0 +1,53 @@ +import { BBWASMLazyPrivateKernelProver } from '@aztec/bb-prover/wasm/lazy'; +import { createLogger } from '@aztec/foundation/log'; +import { createStore } from '@aztec/kv-store/indexeddb'; +import { LazyProtocolContractsProvider } from '@aztec/protocol-contracts/providers/lazy'; +import { WASMSimulator } from '@aztec/simulator/client'; +import type { AztecNode } from '@aztec/stdlib/interfaces/client'; + +import type { PXEServiceConfig } from '../../../config/index.js'; +import { PXEService } from '../../../pxe_service/pxe_service.js'; +import type { PXECreationOptions } from '../pxe_creation_options.js'; + +/** + * Create and start an PXEService instance with the given AztecNode. + * Returns a Promise that resolves to the started PXEService instance. + * + * @param aztecNode - The AztecNode instance to be used by the server. + * @param config - The PXE Service Config to use + * @param + * @returns A Promise that resolves to the started PXEService instance. + */ +export async function createPXEService( + aztecNode: AztecNode, + config: PXEServiceConfig, + options: PXECreationOptions = { loggers: {} }, +) { + const l1Contracts = await aztecNode.getL1ContractAddresses(); + const configWithContracts = { + ...config, + l1Contracts, + } as PXEServiceConfig; + + const store = await createStore( + 'pxe_data', + configWithContracts, + options.loggers.store ?? createLogger('pxe:data:indexeddb'), + ); + + const simulationProvider = new WASMSimulator(); + const prover = + options.prover ?? + new BBWASMLazyPrivateKernelProver(simulationProvider, 16, options.loggers.prover ?? createLogger('bb:wasm:lazy')); + const protocolContractsProvider = new LazyProtocolContractsProvider(); + const pxe = await PXEService.create( + aztecNode, + store, + prover, + simulationProvider, + protocolContractsProvider, + config, + options.loggers.pxe ?? createLogger('pxe:service'), + ); + return pxe; +} diff --git a/yarn-project/pxe/src/entrypoints/client/pxe_creation_options.ts b/yarn-project/pxe/src/entrypoints/client/pxe_creation_options.ts new file mode 100644 index 000000000000..5f76dbffb58c --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/client/pxe_creation_options.ts @@ -0,0 +1,7 @@ +import type { Logger } from '@aztec/foundation/log'; +import type { PrivateKernelProver } from '@aztec/stdlib/interfaces/client'; + +export type PXECreationOptions = { + loggers: { store?: Logger; pxe?: Logger; prover?: Logger }; + prover?: PrivateKernelProver; +}; diff --git a/yarn-project/pxe/src/entrypoints/server/index.ts b/yarn-project/pxe/src/entrypoints/server/index.ts new file mode 100644 index 000000000000..05cc5088853b --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/server/index.ts @@ -0,0 +1,6 @@ +export * from '../../pxe_service/index.js'; +export * from '../../pxe_http/index.js'; +export * from '../../config/index.js'; +export * from '../../storage/index.js'; +export * from './utils.js'; +export { PXEOracleInterface } from '../../pxe_oracle_interface/index.js'; diff --git a/yarn-project/pxe/src/utils/create_pxe_service.ts b/yarn-project/pxe/src/entrypoints/server/utils.ts similarity index 78% rename from yarn-project/pxe/src/utils/create_pxe_service.ts rename to yarn-project/pxe/src/entrypoints/server/utils.ts index d13f49c67e1a..8d157bdb1d51 100644 --- a/yarn-project/pxe/src/utils/create_pxe_service.ts +++ b/yarn-project/pxe/src/entrypoints/server/utils.ts @@ -2,16 +2,14 @@ import { BBNativePrivateKernelProver } from '@aztec/bb-prover'; import { BBWASMBundlePrivateKernelProver } from '@aztec/bb-prover/wasm/bundle'; import { randomBytes } from '@aztec/foundation/crypto'; import { createLogger } from '@aztec/foundation/log'; -import { KeyStore } from '@aztec/key-store'; import { createStore } from '@aztec/kv-store/lmdb-v2'; -import { L2TipsStore } from '@aztec/kv-store/stores'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { type SimulationProvider, WASMSimulator } from '@aztec/simulator/client'; import type { AztecNode, PrivateKernelProver } from '@aztec/stdlib/interfaces/client'; -import type { PXEServiceConfig } from '../config/index.js'; -import { KVPxeDatabase } from '../database/kv_pxe_database.js'; -import { PXEService } from '../pxe_service/pxe_service.js'; +import type { PXEServiceConfig } from '../../config/index.js'; +import { PXEService } from '../../pxe_service/pxe_service.js'; +import { PXE_DATA_SCHEMA_VERSION } from './index.js'; /** * Create and start an PXEService instance with the given AztecNode. @@ -20,7 +18,7 @@ import { PXEService } from '../pxe_service/pxe_service.js'; * * @param aztecNode - The AztecNode instance to be used by the server. * @param config - The PXE Service Config to use - * @param options - (Optional) Optional information for creating an PXEService. + * @param useLogSuffix - (Optional) Log suffix for PXE's logger. * @param proofCreator - An optional proof creator to use in place of any other configuration * @returns A Promise that resolves to the started PXEService instance. */ @@ -39,34 +37,25 @@ export async function createPXEService( l1Contracts, } as PXEServiceConfig; - const keyStore = new KeyStore( - await createStore('pxe_key_store', KeyStore.SCHEMA_VERSION, configWithContracts, createLogger('pxe:keystore:lmdb')), - ); - const store = await createStore( 'pxe_data', - KVPxeDatabase.SCHEMA_VERSION, + PXE_DATA_SCHEMA_VERSION, configWithContracts, createLogger('pxe:data:lmdb'), ); - const db = await KVPxeDatabase.create(store); - const tips = new L2TipsStore(store, 'pxe'); const simulationProvider = new WASMSimulator(); const prover = proofCreator ?? (await createProver(config, simulationProvider, logSuffix)); const protocolContractsProvider = new BundledProtocolContractsProvider(); - const pxe = new PXEService( - keyStore, + const pxe = await PXEService.create( aztecNode, - db, - tips, + store, prover, simulationProvider, protocolContractsProvider, config, logSuffix, ); - await pxe.init(); return pxe; } diff --git a/yarn-project/pxe/src/index.ts b/yarn-project/pxe/src/index.ts deleted file mode 100644 index 950af4ae067a..000000000000 --- a/yarn-project/pxe/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './pxe_service/index.js'; -export { pxeTestSuite } from './pxe_service/test/pxe_test_suite.js'; -export * from './pxe_http/index.js'; -export * from './config/index.js'; -export * from './utils/create_pxe_service.js'; - -export * from './database/index.js'; -export { PXEDataProvider } from './pxe_data_provider/index.js'; -export * from './contract_data_provider/index.js'; diff --git a/yarn-project/pxe/src/kernel_oracle/index.ts b/yarn-project/pxe/src/kernel_oracle/index.ts index 72da149e1e2b..aad67ddb93a2 100644 --- a/yarn-project/pxe/src/kernel_oracle/index.ts +++ b/yarn-project/pxe/src/kernel_oracle/index.ts @@ -17,7 +17,7 @@ import { SharedMutableValues, SharedMutableValuesWithHash } from '@aztec/stdlib/ import type { NullifierMembershipWitness } from '@aztec/stdlib/trees'; import type { VerificationKeyAsFields } from '@aztec/stdlib/vks'; -import type { ContractDataProvider } from '../contract_data_provider/index.js'; +import type { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; import type { ProvingDataOracle } from './../kernel_prover/proving_data_oracle.js'; // TODO: Block number should not be "latest". diff --git a/yarn-project/pxe/src/note_decryption_utils/add_public_values_to_payload.ts b/yarn-project/pxe/src/note_decryption_utils/add_public_values_to_payload.ts index 17c911040fee..cb174bdc6dcc 100644 --- a/yarn-project/pxe/src/note_decryption_utils/add_public_values_to_payload.ts +++ b/yarn-project/pxe/src/note_decryption_utils/add_public_values_to_payload.ts @@ -2,7 +2,7 @@ import { ContractNotFoundError } from '@aztec/simulator/client'; import type { L1NotePayload } from '@aztec/stdlib/logs'; import { Note } from '@aztec/stdlib/note'; -import type { PxeDatabase } from '../database/interfaces/pxe_database.js'; +import type { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; /** * Merges privately and publicly delivered note values. @@ -11,21 +11,21 @@ import type { PxeDatabase } from '../database/interfaces/pxe_database.js'; * @returns Note payload with public fields added. */ export async function getOrderedNoteItems( - db: PxeDatabase, + contractDataProvider: ContractDataProvider, { contractAddress, noteTypeId, privateNoteValues, publicNoteValues }: L1NotePayload, ): Promise { if (publicNoteValues.length === 0) { return new Note(privateNoteValues); } - const instance = await db.getContractInstance(contractAddress); + const instance = await contractDataProvider.getContractInstance(contractAddress); if (!instance) { throw new ContractNotFoundError( `Could not find instance for ${contractAddress.toString()}. This should never happen here as the partial notes flow should be triggered only for non-deferred notes.`, ); } - const artifact = await db.getContractArtifact(instance.currentContractClassId); + const artifact = await contractDataProvider.getContractArtifact(instance.currentContractClassId); if (!artifact) { throw new Error( `Could not find artifact for contract class ${instance.currentContractClassId.toString()}. This should never happen here as the partial notes flow should be triggered only for non-deferred notes.`, diff --git a/yarn-project/pxe/src/pxe_data_provider/index.ts b/yarn-project/pxe/src/pxe_oracle_interface/index.ts similarity index 90% rename from yarn-project/pxe/src/pxe_data_provider/index.ts rename to yarn-project/pxe/src/pxe_oracle_interface/index.ts index 8a7374c67289..0ce346472d95 100644 --- a/yarn-project/pxe/src/pxe_data_provider/index.ts +++ b/yarn-project/pxe/src/pxe_oracle_interface/index.ts @@ -42,23 +42,33 @@ import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from import type { BlockHeader } from '@aztec/stdlib/tx'; import { TxHash } from '@aztec/stdlib/tx'; -import { ContractDataProvider } from '../contract_data_provider/index.js'; -import type { PxeDatabase } from '../database/index.js'; -import { NoteDao } from '../database/note_dao.js'; import { getOrderedNoteItems } from '../note_decryption_utils/add_public_values_to_payload.js'; +import type { AddressDataProvider } from '../storage/address_data_provider/address_data_provider.js'; +import type { AuthWitnessDataProvider } from '../storage/auth_witness_data_provider/auth_witness_data_provider.js'; +import type { CapsuleDataProvider } from '../storage/capsule_data_provider/capsule_data_provider.js'; +import type { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; +import { NoteDao } from '../storage/note_data_provider/note_dao.js'; +import type { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js'; +import type { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; +import type { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; import { WINDOW_HALF_SIZE, getIndexedTaggingSecretsForTheWindow, getInitialIndexesMap } from './tagging_utils.js'; /** * A data layer that provides and stores information needed for simulating/proving a transaction. */ -export class PXEDataProvider implements ExecutionDataProvider { +export class PXEOracleInterface implements ExecutionDataProvider { constructor( - private db: PxeDatabase, - private keyStore: KeyStore, private aztecNode: AztecNode, + private keyStore: KeyStore, private simulationProvider: SimulationProvider, private contractDataProvider: ContractDataProvider, - private log = createLogger('pxe:pxe_data_provider'), + private noteDataProvider: NoteDataProvider, + private capsuleDataProvider: CapsuleDataProvider, + private syncDataProvider: SyncDataProvider, + private taggingDataProvider: TaggingDataProvider, + private addressDataProvider: AddressDataProvider, + private authWitnessDataProvider: AuthWitnessDataProvider, + private log = createLogger('pxe:pxe_data_manager'), ) {} getKeyValidationRequest(pkMHash: Fr, contractAddress: AztecAddress): Promise { @@ -66,7 +76,7 @@ export class PXEDataProvider implements ExecutionDataProvider { } async getCompleteAddress(account: AztecAddress): Promise { - const completeAddress = await this.db.getCompleteAddress(account); + const completeAddress = await this.addressDataProvider.getCompleteAddress(account); if (!completeAddress) { throw new Error( `No public key registered for address ${account}. @@ -77,23 +87,20 @@ export class PXEDataProvider implements ExecutionDataProvider { } async getContractInstance(address: AztecAddress): Promise { - const instance = await this.db.getContractInstance(address); + const instance = await this.contractDataProvider.getContractInstance(address); if (!instance) { throw new Error(`No contract instance found for address ${address.toString()}`); } return instance; } - async getAuthWitness(messageHash: Fr): Promise { - const witness = await this.db.getAuthWitness(messageHash); - if (!witness) { - throw new Error(`Unknown auth witness for message hash ${messageHash.toString()}`); - } + async getAuthWitness(messageHash: Fr): Promise { + const witness = await this.authWitnessDataProvider.getAuthWitness(messageHash); return witness; } async getNotes(contractAddress: AztecAddress, storageSlot: Fr, status: NoteStatus, scopes?: AztecAddress[]) { - const noteDaos = await this.db.getNotes({ + const noteDaos = await this.noteDataProvider.getNotes({ contractAddress, storageSlot, status, @@ -244,7 +251,7 @@ export class PXEDataProvider implements ExecutionDataProvider { * @returns A Promise that resolves to a BlockHeader object. */ getBlockHeader(): Promise { - return this.db.getBlockHeader(); + return this.syncDataProvider.getBlockHeader(); } /** @@ -282,7 +289,7 @@ export class PXEDataProvider implements ExecutionDataProvider { * @returns The full list of the users contact addresses. */ public getSenders(): Promise { - return this.db.getSenderAddresses(); + return this.taggingDataProvider.getSenderAddresses(); } /** @@ -301,7 +308,7 @@ export class PXEDataProvider implements ExecutionDataProvider { await this.syncTaggedLogsAsSender(contractAddress, sender, recipient); const appTaggingSecret = await this.#calculateAppTaggingSecret(contractAddress, sender, recipient); - const [index] = await this.db.getTaggingSecretsIndexesAsSender([appTaggingSecret]); + const [index] = await this.taggingDataProvider.getTaggingSecretsIndexesAsSender([appTaggingSecret]); return new IndexedTaggingSecret(appTaggingSecret, index); } @@ -327,8 +334,8 @@ export class PXEDataProvider implements ExecutionDataProvider { contractAddress, }); - const [index] = await this.db.getTaggingSecretsIndexesAsSender([secret]); - await this.db.setTaggingSecretsIndexesAsSender([new IndexedTaggingSecret(secret, index + 1)]); + const [index] = await this.taggingDataProvider.getTaggingSecretsIndexesAsSender([secret]); + await this.taggingDataProvider.setTaggingSecretsIndexesAsSender([new IndexedTaggingSecret(secret, index + 1)]); } async #calculateAppTaggingSecret(contractAddress: AztecAddress, sender: AztecAddress, recipient: AztecAddress) { @@ -358,16 +365,17 @@ export class PXEDataProvider implements ExecutionDataProvider { // We implicitly add all PXE accounts as senders, this helps us decrypt tags on notes that we send to ourselves // (recipient = us, sender = us) - const senders = [...(await this.db.getSenderAddresses()), ...(await this.keyStore.getAccounts())].filter( - (address, index, self) => index === self.findIndex(otherAddress => otherAddress.equals(address)), - ); + const senders = [ + ...(await this.taggingDataProvider.getSenderAddresses()), + ...(await this.keyStore.getAccounts()), + ].filter((address, index, self) => index === self.findIndex(otherAddress => otherAddress.equals(address))); const appTaggingSecrets = await Promise.all( senders.map(async contact => { const sharedSecret = await computeTaggingSecretPoint(recipientCompleteAddress, recipientIvsk, contact); return poseidon2Hash([sharedSecret.x, sharedSecret.y, contractAddress]); }), ); - const indexes = await this.db.getTaggingSecretsIndexesAsRecipient(appTaggingSecrets); + const indexes = await this.taggingDataProvider.getTaggingSecretsIndexesAsRecipient(appTaggingSecrets); return appTaggingSecrets.map((secret, i) => new IndexedTaggingSecret(secret, indexes[i])); } @@ -384,7 +392,7 @@ export class PXEDataProvider implements ExecutionDataProvider { recipient: AztecAddress, ): Promise { const appTaggingSecret = await this.#calculateAppTaggingSecret(contractAddress, sender, recipient); - const [oldIndex] = await this.db.getTaggingSecretsIndexesAsSender([appTaggingSecret]); + const [oldIndex] = await this.taggingDataProvider.getTaggingSecretsIndexesAsSender([appTaggingSecret]); // This algorithm works such that: // 1. If we find minimum consecutive empty logs in a window of logs we set the index to the index of the last log @@ -422,7 +430,9 @@ export class PXEDataProvider implements ExecutionDataProvider { const contractName = await this.contractDataProvider.getDebugContractName(contractAddress); if (currentIndex !== oldIndex) { - await this.db.setTaggingSecretsIndexesAsSender([new IndexedTaggingSecret(appTaggingSecret, currentIndex)]); + await this.taggingDataProvider.setTaggingSecretsIndexesAsSender([ + new IndexedTaggingSecret(appTaggingSecret, currentIndex), + ]); this.log.debug(`Syncing logs for sender ${sender} at contract ${contractName}(${contractAddress})`, { sender, @@ -591,7 +601,7 @@ export class PXEDataProvider implements ExecutionDataProvider { ); // At this point we have processed all the logs for the recipient so we store the new largest indexes in the db. - await this.db.setTaggingSecretsIndexesAsRecipient( + await this.taggingDataProvider.setTaggingSecretsIndexesAsRecipient( Object.entries(newLargestIndexMapToStore).map( ([appTaggingSecret, index]) => new IndexedTaggingSecret(Fr.fromHexString(appTaggingSecret), index), ), @@ -632,7 +642,7 @@ export class PXEDataProvider implements ExecutionDataProvider { excludedIndices.set(scopedLog.txHash.toString(), new Set()); } - const note = await getOrderedNoteItems(this.db, payload); + const note = await getOrderedNoteItems(this.contractDataProvider, payload); const plaintext = [payload.storageSlot, payload.noteTypeId.toField(), ...note.items]; decrypted.push({ plaintext, txHash: scopedLog.txHash, contractAddress: payload.contractAddress }); @@ -702,7 +712,7 @@ export class PXEDataProvider implements ExecutionDataProvider { recipient, ); - await this.db.addNotes([noteDao], recipient); + await this.noteDataProvider.addNotes([noteDao], recipient); this.log.verbose('Added note', { contract: noteDao.contractAddress, slot: noteDao.storageSlot, @@ -751,7 +761,7 @@ export class PXEDataProvider implements ExecutionDataProvider { this.log.verbose('Searching for nullifiers of known notes', { contract: contractAddress }); for (const recipient of await this.keyStore.getAccounts()) { - const currentNotesForRecipient = await this.db.getNotes({ contractAddress, owner: recipient }); + const currentNotesForRecipient = await this.noteDataProvider.getNotes({ contractAddress, owner: recipient }); const nullifiersToCheck = currentNotesForRecipient.map(note => note.siloedNullifier); const nullifierIndexes = await this.aztecNode.findNullifiersIndexesWithBlock('latest', nullifiersToCheck); @@ -763,7 +773,10 @@ export class PXEDataProvider implements ExecutionDataProvider { }) .filter(nullifier => nullifier !== undefined) as InBlock[]; - const nullifiedNotes = await this.db.removeNullifiedNotes(foundNullifiers, await recipient.toAddressPoint()); + const nullifiedNotes = await this.noteDataProvider.removeNullifiedNotes( + foundNullifiers, + await recipient.toAddressPoint(), + ); nullifiedNotes.forEach(noteDao => { this.log.verbose(`Removed note for contract ${noteDao.contractAddress} at slot ${noteDao.storageSlot}`, { contract: noteDao.contractAddress, @@ -801,7 +814,7 @@ export class PXEDataProvider implements ExecutionDataProvider { // note existence in said tree. Note that while this is technically a historical query, we perform it at the latest // locally synced block number which *should* be recent enough to be available. We avoid querying at 'latest' since // we want to avoid accidentally processing notes that only exist ahead in time of the locally synced state. - const syncedBlockNumber = await this.db.getBlockNumber(); + const syncedBlockNumber = await this.syncDataProvider.getBlockNumber(); const uniqueNoteHashTreeIndex = ( await this.aztecNode.findLeavesIndexes(syncedBlockNumber!, MerkleTreeId.NOTE_HASH_TREE, [uniqueNoteHash]) )[0]; @@ -836,7 +849,7 @@ export class PXEDataProvider implements ExecutionDataProvider { recipient: AztecAddress, simulator?: AcirSimulator, ) { - const artifact: FunctionArtifact | undefined = await new ContractDataProvider(this.db).getFunctionArtifactByName( + const artifact: FunctionArtifact | undefined = await this.contractDataProvider.getFunctionArtifactByName( contractAddress, 'process_log', ); @@ -872,19 +885,19 @@ export class PXEDataProvider implements ExecutionDataProvider { } storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[]): Promise { - return this.db.storeCapsule(contractAddress, slot, capsule); + return this.capsuleDataProvider.storeCapsule(contractAddress, slot, capsule); } loadCapsule(contractAddress: AztecAddress, slot: Fr): Promise { - return this.db.loadCapsule(contractAddress, slot); + return this.capsuleDataProvider.loadCapsule(contractAddress, slot); } deleteCapsule(contractAddress: AztecAddress, slot: Fr): Promise { - return this.db.deleteCapsule(contractAddress, slot); + return this.capsuleDataProvider.deleteCapsule(contractAddress, slot); } copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { - return this.db.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries); + return this.capsuleDataProvider.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries); } } diff --git a/yarn-project/pxe/src/pxe_data_provider/pxe_data_provider.test.ts b/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.test.ts similarity index 85% rename from yarn-project/pxe/src/pxe_data_provider/pxe_data_provider.test.ts rename to yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.test.ts index e491e2132fb9..a1e5e180c0d1 100644 --- a/yarn-project/pxe/src/pxe_data_provider/pxe_data_provider.test.ts +++ b/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.test.ts @@ -19,10 +19,14 @@ import { TxEffect, TxHash } from '@aztec/stdlib/tx'; import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { ContractDataProvider } from '../contract_data_provider/index.js'; -import type { PxeDatabase } from '../database/index.js'; -import { KVPxeDatabase } from '../database/kv_pxe_database.js'; -import { PXEDataProvider } from './index.js'; +import { AddressDataProvider } from '../storage/address_data_provider/address_data_provider.js'; +import { AuthWitnessDataProvider } from '../storage/auth_witness_data_provider/auth_witness_data_provider.js'; +import { CapsuleDataProvider } from '../storage/capsule_data_provider/capsule_data_provider.js'; +import { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; +import { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js'; +import { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; +import { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; +import { PXEOracleInterface } from './index.js'; import { WINDOW_HALF_SIZE } from './tagging_utils.js'; const TXS_PER_BLOCK = 4; @@ -102,31 +106,54 @@ async function computeSiloedTagForIndex( return poseidon2Hash([contractAddress, tag]); } -describe('PXE data provider', () => { +describe('PXEOracleInterface', () => { let aztecNode: MockProxy; - let database: PxeDatabase; + + let addressDataProvider: AddressDataProvider; + let authWitnessDataProvider: AuthWitnessDataProvider; let contractDataProvider: ContractDataProvider; - let pxeDataProvider: PXEDataProvider; + let noteDataProvider: NoteDataProvider; + let syncDataProvider: SyncDataProvider; + let taggingDataProvider: TaggingDataProvider; + let capsuleDataProvider: CapsuleDataProvider; let keyStore: KeyStore; let simulationProvider: SimulationProvider; let recipient: CompleteAddress; let contractAddress: AztecAddress; + let pxeOracleInterface: PXEOracleInterface; + beforeEach(async () => { - const db = await openTmpStore('test'); + const store = await openTmpStore('test'); aztecNode = mock(); - database = await KVPxeDatabase.create(db); - contractDataProvider = new ContractDataProvider(database); + contractDataProvider = new ContractDataProvider(store); jest.spyOn(contractDataProvider, 'getDebugContractName').mockImplementation(() => Promise.resolve('TestContract')); - keyStore = new KeyStore(db); + + addressDataProvider = new AddressDataProvider(store); + authWitnessDataProvider = new AuthWitnessDataProvider(store); + noteDataProvider = await NoteDataProvider.create(store); + syncDataProvider = new SyncDataProvider(store); + taggingDataProvider = new TaggingDataProvider(store); + capsuleDataProvider = new CapsuleDataProvider(store); + keyStore = new KeyStore(store); simulationProvider = new WASMSimulator(); - pxeDataProvider = new PXEDataProvider(database, keyStore, aztecNode, simulationProvider, contractDataProvider); - // Set up contract address + pxeOracleInterface = new PXEOracleInterface( + aztecNode, + keyStore, + simulationProvider, + contractDataProvider, + noteDataProvider, + capsuleDataProvider, + syncDataProvider, + taggingDataProvider, + addressDataProvider, + authWitnessDataProvider, + ); // Set up contract address contractAddress = await AztecAddress.random(); // Set up recipient account recipient = await keyStore.addAccount(new Fr(69), Fr.random()); - await database.addCompleteAddress(recipient); + await addressDataProvider.addCompleteAddress(recipient); }); describe('sync tagged logs', () => { @@ -217,7 +244,7 @@ describe('PXE data provider', () => { return { completeAddress, ivsk: keys.masterIncomingViewingSecretKey, secretKey: new Fr(index) }; }); for (const sender of senders) { - await database.addSenderAddress(sender.completeAddress.address); + await taggingDataProvider.addSenderAddress(sender.completeAddress.address); } aztecNode.getLogsByTags.mockReset(); }); @@ -225,7 +252,7 @@ describe('PXE data provider', () => { it('should sync tagged logs', async () => { const tagIndex = 0; await generateMockLogs(tagIndex); - const syncedLogs = await pxeDataProvider.syncTaggedLogs(contractAddress, 3); + const syncedLogs = await pxeOracleInterface.syncTaggedLogs(contractAddress, 3); // We expect to have all logs intended for the recipient, one per sender + 1 with a duplicated tag for the first // one + half of the logs for the second index expect(syncedLogs.get(recipient.address.toString())).toHaveLength(NUM_SENDERS + 1 + NUM_SENDERS / 2); @@ -247,7 +274,7 @@ describe('PXE data provider', () => { // First sender should have 2 logs, but keep index 1 since they were built using the same tag // Next 4 senders should also have index 1 = offset + 1 // Last 5 senders should have index 2 = offset + 2 - const indexes = await database.getTaggingSecretsIndexesAsRecipient(secrets); + const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]); @@ -259,7 +286,7 @@ describe('PXE data provider', () => { it('should sync tagged logs as senders', async () => { for (const sender of senders) { - await database.addCompleteAddress(sender.completeAddress); + await addressDataProvider.addCompleteAddress(sender.completeAddress); await keyStore.addAccount(sender.secretKey, sender.completeAddress.partialAddress); } @@ -279,20 +306,20 @@ describe('PXE data provider', () => { }), ); - const indexesAsSender = await database.getTaggingSecretsIndexesAsSender(secrets); + const indexesAsSender = await taggingDataProvider.getTaggingSecretsIndexesAsSender(secrets); expect(indexesAsSender).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); expect(aztecNode.getLogsByTags.mock.calls.length).toBe(0); for (let i = 0; i < senders.length; i++) { - await pxeDataProvider.syncTaggedLogsAsSender( + await pxeOracleInterface.syncTaggedLogsAsSender( contractAddress, senders[i].completeAddress.address, recipient.address, ); } - let indexesAsSenderAfterSync = await database.getTaggingSecretsIndexesAsSender(secrets); + let indexesAsSenderAfterSync = await taggingDataProvider.getTaggingSecretsIndexesAsSender(secrets); expect(indexesAsSenderAfterSync).toStrictEqual([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]); // Only 1 window is obtained for each sender @@ -304,14 +331,14 @@ describe('PXE data provider', () => { tagIndex = 11; await generateMockLogs(tagIndex); for (let i = 0; i < senders.length; i++) { - await pxeDataProvider.syncTaggedLogsAsSender( + await pxeOracleInterface.syncTaggedLogsAsSender( contractAddress, senders[i].completeAddress.address, recipient.address, ); } - indexesAsSenderAfterSync = await database.getTaggingSecretsIndexesAsSender(secrets); + indexesAsSenderAfterSync = await taggingDataProvider.getTaggingSecretsIndexesAsSender(secrets); expect(indexesAsSenderAfterSync).toStrictEqual([12, 12, 12, 12, 12, 13, 13, 13, 13, 13]); expect(aztecNode.getLogsByTags.mock.calls.length).toBe(NUM_SENDERS * 2); @@ -320,7 +347,7 @@ describe('PXE data provider', () => { it('should sync tagged logs with a sender index offset', async () => { const tagIndex = 5; await generateMockLogs(tagIndex); - const syncedLogs = await pxeDataProvider.syncTaggedLogs(contractAddress, 3); + const syncedLogs = await pxeOracleInterface.syncTaggedLogs(contractAddress, 3); // We expect to have all logs intended for the recipient, one per sender + 1 with a duplicated tag for the first one + half of the logs for the second index expect(syncedLogs.get(recipient.address.toString())).toHaveLength(NUM_SENDERS + 1 + NUM_SENDERS / 2); @@ -340,7 +367,7 @@ describe('PXE data provider', () => { // First sender should have 2 logs, but keep index 1 since they were built using the same tag // Next 4 senders should also have index 6 = offset + 1 // Last 5 senders should have index 7 = offset + 2 - const indexes = await database.getTaggingSecretsIndexesAsRecipient(secrets); + const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([6, 6, 6, 6, 6, 7, 7, 7, 7, 7]); @@ -368,9 +395,11 @@ describe('PXE data provider', () => { ); // Increase our indexes to 2 - await database.setTaggingSecretsIndexesAsRecipient(secrets.map(secret => new IndexedTaggingSecret(secret, 2))); + await taggingDataProvider.setTaggingSecretsIndexesAsRecipient( + secrets.map(secret => new IndexedTaggingSecret(secret, 2)), + ); - const syncedLogs = await pxeDataProvider.syncTaggedLogs(contractAddress, 3); + const syncedLogs = await pxeOracleInterface.syncTaggedLogs(contractAddress, 3); // Even if our index as recipient is higher than what the sender sent, we should be able to find the logs // since the window starts at Math.max(0, 2 - window_size) = 0 @@ -379,7 +408,7 @@ describe('PXE data provider', () => { // First sender should have 2 logs, but keep index 2 since they were built using the same tag // Next 4 senders should also have index 2 = tagIndex + 1 // Last 5 senders should have index 3 = tagIndex + 2 - const indexes = await database.getTaggingSecretsIndexesAsRecipient(secrets); + const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([2, 2, 2, 2, 2, 3, 3, 3, 3, 3]); @@ -408,17 +437,17 @@ describe('PXE data provider', () => { // We set the indexes to WINDOW_HALF_SIZE + 1 so that it's outside the window and for this reason no updates // should be triggered. - await database.setTaggingSecretsIndexesAsRecipient( + await taggingDataProvider.setTaggingSecretsIndexesAsRecipient( secrets.map(secret => new IndexedTaggingSecret(secret, WINDOW_HALF_SIZE + 1)), ); - const syncedLogs = await pxeDataProvider.syncTaggedLogs(contractAddress, 3); + const syncedLogs = await pxeOracleInterface.syncTaggedLogs(contractAddress, 3); // Only half of the logs should be synced since we start from index 1 = (11 - window_size), the other half should be skipped expect(syncedLogs.get(recipient.address.toString())).toHaveLength(NUM_SENDERS / 2); // Indexes should remain where we set them (window_size + 1) - const indexes = await database.getTaggingSecretsIndexesAsRecipient(secrets); + const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([11, 11, 11, 11, 11, 11, 11, 11, 11, 11]); @@ -444,11 +473,11 @@ describe('PXE data provider', () => { }), ); - await database.setTaggingSecretsIndexesAsRecipient( + await taggingDataProvider.setTaggingSecretsIndexesAsRecipient( secrets.map(secret => new IndexedTaggingSecret(secret, WINDOW_HALF_SIZE + 2)), ); - let syncedLogs = await pxeDataProvider.syncTaggedLogs(contractAddress, 3); + let syncedLogs = await pxeOracleInterface.syncTaggedLogs(contractAddress, 3); // No logs should be synced since we start from index 2 = 12 - window_size expect(syncedLogs.get(recipient.address.toString())).toHaveLength(0); @@ -459,14 +488,14 @@ describe('PXE data provider', () => { aztecNode.getLogsByTags.mockClear(); // Wipe the database - await database.resetNoteSyncData(); + await taggingDataProvider.resetNoteSyncData(); - syncedLogs = await pxeDataProvider.syncTaggedLogs(contractAddress, 3); + syncedLogs = await pxeOracleInterface.syncTaggedLogs(contractAddress, 3); // First sender should have 2 logs, but keep index 1 since they were built using the same tag // Next 4 senders should also have index 1 = offset + 1 // Last 5 senders should have index 2 = offset + 2 - const indexes = await database.getTaggingSecretsIndexesAsRecipient(secrets); + const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]); @@ -479,7 +508,7 @@ describe('PXE data provider', () => { it('should not sync tagged logs with a blockNumber > maxBlockNumber', async () => { const tagIndex = 0; await generateMockLogs(tagIndex); - const syncedLogs = await pxeDataProvider.syncTaggedLogs(contractAddress, 1); + const syncedLogs = await pxeOracleInterface.syncTaggedLogs(contractAddress, 1); // Only NUM_SENDERS + 1 logs should be synched, since the rest have blockNumber > 1 expect(syncedLogs.get(recipient.address.toString())).toHaveLength(NUM_SENDERS + 1); @@ -500,7 +529,7 @@ describe('PXE data provider', () => { aztecNode.getLogsByTags.mockImplementation(tags => { return Promise.resolve(tags.map(tag => logs[tag.toString()] ?? [])); }); - const syncedLogs = await pxeDataProvider.syncTaggedLogs(contractAddress, 1); + const syncedLogs = await pxeOracleInterface.syncTaggedLogs(contractAddress, 1); // We expect the above log to be discarded, and so none to be synced expect(syncedLogs.get(recipient.address.toString())).toHaveLength(0); @@ -535,13 +564,13 @@ describe('PXE data provider', () => { const contractInstance = await randomContractInstanceWithAddress(); const contractArtifact = randomContractArtifact(); contractArtifact.functions = [processLogFuncArtifact]; - await database.addContractInstance(contractInstance); - await database.addContractArtifact(contractInstance.currentContractClassId, contractArtifact); + await contractDataProvider.addContractInstance(contractInstance); + await contractDataProvider.addContractArtifact(contractInstance.currentContractClassId, contractArtifact); contractAddress = contractInstance.address; - addNotesSpy = jest.spyOn(database, 'addNotes'); - getNotesSpy = jest.spyOn(database, 'getNotes'); - removeNullifiedNotesSpy = jest.spyOn(database, 'removeNullifiedNotes'); + addNotesSpy = jest.spyOn(noteDataProvider, 'addNotes'); + getNotesSpy = jest.spyOn(noteDataProvider, 'getNotes'); + removeNullifiedNotesSpy = jest.spyOn(noteDataProvider, 'removeNullifiedNotes'); removeNullifiedNotesSpy.mockImplementation(() => Promise.resolve([])); simulator = mock(); simulator.runUnconstrained.mockImplementation(() => Promise.resolve({})); @@ -632,7 +661,7 @@ describe('PXE data provider', () => { const taggedLogs = await mockTaggedLogs(requests); - await pxeDataProvider.processTaggedLogs(taggedLogs, recipient.address, simulator); + await pxeOracleInterface.processTaggedLogs(taggedLogs, recipient.address, simulator); // We test that a call to `processLog` is made with the correct function artifact and contract address expect(runUnconstrainedSpy).toHaveBeenCalledTimes(3); @@ -653,7 +682,7 @@ describe('PXE data provider', () => { const taggedLogs = await mockTaggedLogs(requests); - await pxeDataProvider.processTaggedLogs(taggedLogs, recipient.address, simulator); + await pxeOracleInterface.processTaggedLogs(taggedLogs, recipient.address, simulator); expect(addNotesSpy).toHaveBeenCalledTimes(0); }); @@ -675,7 +704,7 @@ describe('PXE data provider', () => { return [await wrapInBlock(1n, await L2Block.random(2)), undefined, undefined]; }); - await pxeDataProvider.removeNullifiedNotes(contractAddress); + await pxeOracleInterface.removeNullifiedNotes(contractAddress); expect(removeNullifiedNotesSpy).toHaveBeenCalledTimes(1); expect(removeNullifiedNotesSpy).toHaveBeenCalledWith( diff --git a/yarn-project/pxe/src/pxe_data_provider/tagging_utils.ts b/yarn-project/pxe/src/pxe_oracle_interface/tagging_utils.ts similarity index 100% rename from yarn-project/pxe/src/pxe_data_provider/tagging_utils.ts rename to yarn-project/pxe/src/pxe_oracle_interface/tagging_utils.ts diff --git a/yarn-project/pxe/src/pxe_service/error_enriching.ts b/yarn-project/pxe/src/pxe_service/error_enriching.ts index d50e3b7b347a..4dd95907797e 100644 --- a/yarn-project/pxe/src/pxe_service/error_enriching.ts +++ b/yarn-project/pxe/src/pxe_service/error_enriching.ts @@ -6,15 +6,18 @@ import { FunctionSelector } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type SimulationError, isNoirCallStackUnresolved } from '@aztec/stdlib/errors'; -import { ContractDataProvider } from '../contract_data_provider/index.js'; -import type { PxeDatabase } from '../database/interfaces/pxe_database.js'; +import type { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; /** * Adds contract and function names to a simulation error, if they * can be found in the PXE database * @param err - The error to enrich. */ -export async function enrichSimulationError(err: SimulationError, db: PxeDatabase, logger: Logger) { +export async function enrichSimulationError( + err: SimulationError, + contractDataProvider: ContractDataProvider, + logger: Logger, +) { // Maps contract addresses to the set of function names that were in error. // Map and Set do reference equality for their keys instead of value equality, so we store the string // representation to get e.g. different contract address objects with the same address value to match. @@ -30,7 +33,7 @@ export async function enrichSimulationError(err: SimulationError, db: PxeDatabas await Promise.all( [...mentionedFunctions.entries()].map(async ([contractAddress, fnNames]) => { const parsedContractAddress = AztecAddress.fromString(contractAddress); - const contract = await db.getContract(parsedContractAddress); + const contract = await contractDataProvider.getContract(parsedContractAddress); if (contract) { err.enrichWithContractName(parsedContractAddress, contract.name); for (const fnName of fnNames) { @@ -59,7 +62,6 @@ export async function enrichSimulationError(err: SimulationError, db: PxeDatabas export async function enrichPublicSimulationError( err: SimulationError, contractDataProvider: ContractDataProvider, - db: PxeDatabase, logger: Logger, ) { const callStack = err.getCallStack(); @@ -99,6 +101,6 @@ export async function enrichPublicSimulationError( ); } } - await enrichSimulationError(err, db, logger); + await enrichSimulationError(err, contractDataProvider, logger); } } diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index 0e1f5d52f404..f10fb2300e1e 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -3,8 +3,9 @@ import { Fr, type Point } from '@aztec/foundation/fields'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import type { SiblingPath } from '@aztec/foundation/trees'; -import type { KeyStore } from '@aztec/key-store'; -import type { L2TipsStore } from '@aztec/kv-store/stores'; +import { KeyStore } from '@aztec/key-store'; +import type { AztecAsyncKVStore } from '@aztec/kv-store'; +import { L2TipsStore } from '@aztec/kv-store/stores'; import { ProtocolContractAddress, type ProtocolContractsProvider, @@ -24,12 +25,12 @@ import { import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { InBlock, L2Block } from '@aztec/stdlib/block'; -import type { +import { CompleteAddress, - ContractClassWithId, - ContractInstanceWithAddress, - NodeInfo, - PartialAddress, + type ContractClassWithId, + type ContractInstanceWithAddress, + type NodeInfo, + type PartialAddress, } from '@aztec/stdlib/contract'; import { computeContractAddressFromInstance, getContractClassFromArtifact } from '@aztec/stdlib/contract'; import { SimulationError } from '@aztec/stdlib/errors'; @@ -50,6 +51,7 @@ import { computeAddressSecret } from '@aztec/stdlib/keys'; import type { LogFilter } from '@aztec/stdlib/logs'; import { getNonNullifiedL1ToL2MessageWitness } from '@aztec/stdlib/messaging'; import { type NotesFilter, UniqueNote } from '@aztec/stdlib/note'; +import { MerkleTreeId } from '@aztec/stdlib/trees'; import { PrivateExecutionResult, PrivateSimulationResult, @@ -67,11 +69,16 @@ import { inspect } from 'util'; import type { PXEServiceConfig } from '../config/index.js'; import { getPackageInfo } from '../config/package_info.js'; -import { ContractDataProvider } from '../contract_data_provider/index.js'; -import type { PxeDatabase } from '../database/index.js'; import { KernelOracle } from '../kernel_oracle/index.js'; import { KernelProver, type ProvingConfig } from '../kernel_prover/kernel_prover.js'; -import { PXEDataProvider } from '../pxe_data_provider/index.js'; +import { PXEOracleInterface } from '../pxe_oracle_interface/index.js'; +import { AddressDataProvider } from '../storage/address_data_provider/address_data_provider.js'; +import { AuthWitnessDataProvider } from '../storage/auth_witness_data_provider/auth_witness_data_provider.js'; +import { CapsuleDataProvider } from '../storage/capsule_data_provider/capsule_data_provider.js'; +import { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; +import { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js'; +import { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; +import { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; import { Synchronizer } from '../synchronizer/index.js'; import { enrichPublicSimulationError, enrichSimulationError } from './error_enriching.js'; @@ -79,53 +86,102 @@ import { enrichPublicSimulationError, enrichSimulationError } from './error_enri * A Private eXecution Environment (PXE) implementation. */ export class PXEService implements PXE { - private synchronizer: Synchronizer; - private contractDataProvider: ContractDataProvider; - private pxeDataProvider: PXEDataProvider; - private simulator: AcirSimulator; - private log: Logger; - private packageVersion: string; - private proverEnabled: boolean; - - constructor( - private keyStore: KeyStore, + private constructor( private node: AztecNode, - private db: PxeDatabase, - tipsStore: L2TipsStore, + private synchronizer: Synchronizer, + private keyStore: KeyStore, + private contractDataProvider: ContractDataProvider, + private noteDataProvider: NoteDataProvider, + private capsuleDataProvider: CapsuleDataProvider, + private syncDataProvider: SyncDataProvider, + private taggingDataProvider: TaggingDataProvider, + private addressDataProvider: AddressDataProvider, + private authWitnessDataProvider: AuthWitnessDataProvider, + private simulator: AcirSimulator, + private packageVersion: string, + private proverEnabled: boolean, private proofCreator: PrivateKernelProver, - simulationProvider: SimulationProvider, private protocolContractsProvider: ProtocolContractsProvider, + private log: Logger, + ) {} + + /** + * Creates an instance of a PXE Service by instantiating all the necessary data providers and services. + * Also triggers the registration of the protocol contracts and makes sure the provided node + * can be contacted. + * + * @returns A promise that resolves PXE service is ready to be used. + */ + public static async create( + node: AztecNode, + store: AztecAsyncKVStore, + proofCreator: PrivateKernelProver, + simulationProvider: SimulationProvider, + protocolContractsProvider: ProtocolContractsProvider, config: PXEServiceConfig, loggerOrSuffix?: string | Logger, ) { - this.log = + const log = !loggerOrSuffix || typeof loggerOrSuffix === 'string' ? createLogger(loggerOrSuffix ? `pxe:service:${loggerOrSuffix}` : `pxe:service`) : loggerOrSuffix; - this.synchronizer = new Synchronizer(node, db, tipsStore, config, loggerOrSuffix); - this.contractDataProvider = new ContractDataProvider(db); - this.pxeDataProvider = new PXEDataProvider( - db, - keyStore, + + const packageVersion = getPackageInfo().version; + const proverEnabled = !!config.proverEnabled; + const addressDataProvider = new AddressDataProvider(store); + const authWitnessDataProvider = new AuthWitnessDataProvider(store); + const contractDataProvider = new ContractDataProvider(store); + const noteDataProvider = await NoteDataProvider.create(store); + const syncDataProvider = new SyncDataProvider(store); + const taggingDataProvider = new TaggingDataProvider(store); + const capsuleDataProvider = new CapsuleDataProvider(store); + const keyStore = new KeyStore(store); + const tipsStore = new L2TipsStore(store, 'pxe'); + const synchronizer = new Synchronizer( node, + syncDataProvider, + noteDataProvider, + taggingDataProvider, + tipsStore, + config, + loggerOrSuffix, + ); + const pxeOracleInterface = new PXEOracleInterface( + node, + keyStore, simulationProvider, - this.contractDataProvider, - this.log, + contractDataProvider, + noteDataProvider, + capsuleDataProvider, + syncDataProvider, + taggingDataProvider, + addressDataProvider, + authWitnessDataProvider, + log, ); - this.simulator = new AcirSimulator(this.pxeDataProvider, simulationProvider); - this.packageVersion = getPackageInfo().version; - this.proverEnabled = !!config.proverEnabled; - } - - /** - * Starts the PXE Service by beginning the synchronization process between the Aztec node and the database. - * - * @returns A promise that resolves when the server has started successfully. - */ - public async init() { - await this.#registerProtocolContracts(); - const info = await this.getNodeInfo(); - this.log.info(`Started PXE connected to chain ${info.l1ChainId} version ${info.protocolVersion}`); + const simulator = new AcirSimulator(pxeOracleInterface, simulationProvider); + const pxeService = new PXEService( + node, + synchronizer, + keyStore, + contractDataProvider, + noteDataProvider, + capsuleDataProvider, + syncDataProvider, + taggingDataProvider, + addressDataProvider, + authWitnessDataProvider, + simulator, + packageVersion, + proverEnabled, + proofCreator, + protocolContractsProvider, + log, + ); + await pxeService.#registerProtocolContracts(); + const info = await pxeService.getNodeInfo(); + log.info(`Started PXE connected to chain ${info.l1ChainId} version ${info.protocolVersion}`); + return pxeService; } isL1ToL2MessageSynced(l1ToL2Message: Fr): Promise { @@ -133,24 +189,34 @@ export class PXEService implements PXE { } /** Returns an estimate of the db size in bytes. */ - public estimateDbSize() { - return this.db.estimateSize(); + public async estimateDbSize() { + const treeRootsSize = Object.keys(MerkleTreeId).length * Fr.SIZE_IN_BYTES; + const dbSizes = await Promise.all([ + this.addressDataProvider.getSize(), + this.authWitnessDataProvider.getSize(), + this.capsuleDataProvider.getSize(), + this.contractDataProvider.getSize(), + this.noteDataProvider.getSize(), + this.syncDataProvider.getSize(), + this.taggingDataProvider.getSize(), + ]); + return [...dbSizes, treeRootsSize].reduce((sum, size) => sum + size, 0); } public addAuthWitness(witness: AuthWitness) { - return this.db.addAuthWitness(witness.requestHash, witness.witness); + return this.authWitnessDataProvider.addAuthWitness(witness.requestHash, witness.witness); } public getAuthWitness(messageHash: Fr): Promise { - return this.db.getAuthWitness(messageHash); + return this.authWitnessDataProvider.getAuthWitness(messageHash); } public storeCapsule(contract: AztecAddress, storageSlot: Fr, capsule: Fr[]) { - return this.db.storeCapsule(contract, storageSlot, capsule); + return this.capsuleDataProvider.storeCapsule(contract, storageSlot, capsule); } public getContractInstance(address: AztecAddress): Promise { - return this.db.getContractInstance(address); + return this.contractDataProvider.getContractInstance(address); } public async getContractClassMetadata( @@ -161,7 +227,12 @@ export class PXEService implements PXE { isContractClassPubliclyRegistered: boolean; artifact: ContractArtifact | undefined; }> { - const artifact = await this.db.getContractArtifact(id); + let artifact; + try { + artifact = await this.contractDataProvider.getContractArtifact(id); + } catch { + this.log.warn(`No artifact found for contract class ${id.toString()} when looking for its metadata`); + } return { contractClass: artifact && (await getContractClassFromArtifact(artifact)), @@ -175,8 +246,14 @@ export class PXEService implements PXE { isContractInitialized: boolean; isContractPubliclyDeployed: boolean; }> { + let instance; + try { + instance = await this.contractDataProvider.getContractInstance(address); + } catch { + this.log.warn(`No instance found for contract ${address.toString()} when looking for its metadata`); + } return { - contractInstance: await this.db.getContractInstance(address), + contractInstance: instance, isContractInitialized: await this.#isContractInitialized(address), isContractPubliclyDeployed: await this.#isContractPubliclyDeployed(address), }; @@ -193,7 +270,8 @@ export class PXEService implements PXE { this.log.debug(`Registered account\n ${accountCompleteAddress.toReadableString()}`); } - await this.db.addCompleteAddress(accountCompleteAddress); + await this.addressDataProvider.addCompleteAddress(accountCompleteAddress); + await this.noteDataProvider.addScope(accountCompleteAddress.address); return accountCompleteAddress; } @@ -204,7 +282,7 @@ export class PXEService implements PXE { return address; } - const wasAdded = await this.db.addSenderAddress(address); + const wasAdded = await this.taggingDataProvider.addSenderAddress(address); if (wasAdded) { this.log.info(`Added sender:\n ${address.toString()}`); @@ -216,13 +294,13 @@ export class PXEService implements PXE { } public getSenders(): Promise { - const senders = this.db.getSenderAddresses(); + const senders = this.taggingDataProvider.getSenderAddresses(); return Promise.resolve(senders); } public async removeSender(address: AztecAddress): Promise { - const wasRemoved = await this.db.removeSenderAddress(address); + const wasRemoved = await this.taggingDataProvider.removeSenderAddress(address); if (wasRemoved) { this.log.info(`Removed sender:\n ${address.toString()}`); @@ -235,7 +313,7 @@ export class PXEService implements PXE { public async getRegisteredAccounts(): Promise { // Get complete addresses of both the recipients and the accounts - const completeAddresses = await this.db.getCompleteAddresses(); + const completeAddresses = await this.addressDataProvider.getCompleteAddresses(); // Filter out the addresses not corresponding to accounts const accounts = await this.keyStore.getAccounts(); return completeAddresses.filter(completeAddress => @@ -245,7 +323,7 @@ export class PXEService implements PXE { public async registerContractClass(artifact: ContractArtifact): Promise { const { id: contractClassId } = await getContractClassFromArtifact(artifact); - await this.db.addContractArtifact(contractClassId, artifact); + await this.contractDataProvider.addContractArtifact(contractClassId, artifact); this.log.info(`Added contract class ${artifact.name} with id ${contractClassId}`); } @@ -266,8 +344,7 @@ export class PXEService implements PXE { if (!computedAddress.equals(instance.address)) { throw new Error('Added a contract in which the address does not match the contract instance.'); } - - await this.db.addContractArtifact(contractClass.id, artifact); + await this.contractDataProvider.addContractArtifact(contractClass.id, artifact); const publicFunctionSignatures = artifact.functions .filter(fn => fn.functionType === FunctionType.PUBLIC) @@ -278,41 +355,33 @@ export class PXEService implements PXE { await this.node.addContractClass({ ...contractClass, privateFunctions: [], unconstrainedFunctions: [] }); } else { // Otherwise, make sure there is an artifact already registered for that class id - artifact = await this.db.getContractArtifact(instance.currentContractClassId); - if (!artifact) { - throw new Error( - `Missing contract artifact for class id ${instance.currentContractClassId} for contract ${instance.address}`, - ); - } + artifact = await this.contractDataProvider.getContractArtifact(instance.currentContractClassId); } - await this.db.addContractInstance(instance); + await this.contractDataProvider.addContractInstance(instance); this.log.info( `Added contract ${artifact.name} at ${instance.address.toString()} with class ${instance.currentContractClassId}`, ); } public async updateContract(contractAddress: AztecAddress, artifact: ContractArtifact): Promise { - const currentInstance = await this.db.getContractInstance(contractAddress); - if (!currentInstance) { - throw new Error(`Contract ${contractAddress.toString()} is not registered.`); - } + const currentInstance = await this.contractDataProvider.getContractInstance(contractAddress); const contractClass = await getContractClassFromArtifact(artifact); await this.synchronizer.sync(); - const header = await this.db.getBlockHeader(); + const header = await this.syncDataProvider.getBlockHeader(); const currentClassId = await readCurrentClassId( contractAddress, currentInstance, - this.pxeDataProvider, + this.node, header.globalVariables.blockNumber.toNumber(), ); if (!contractClass.id.equals(currentClassId)) { throw new Error('Could not update contract to a class different from the current one.'); } - await this.db.addContractArtifact(contractClass.id, artifact); + await this.contractDataProvider.addContractArtifact(contractClass.id, artifact); const publicFunctionSignatures = artifact.functions .filter(fn => fn.functionType === FunctionType.PUBLIC) @@ -322,28 +391,25 @@ export class PXEService implements PXE { // TODO(#10007): Node should get public contract class from the registration event, not from PXE registration await this.node.addContractClass({ ...contractClass, privateFunctions: [], unconstrainedFunctions: [] }); currentInstance.currentContractClassId = contractClass.id; - await this.db.addContractInstance(currentInstance); + await this.contractDataProvider.addContractInstance(currentInstance); this.log.info(`Updated contract ${artifact.name} at ${contractAddress.toString()} to class ${contractClass.id}`); } public getContracts(): Promise { - return this.db.getContractsAddresses(); + return this.contractDataProvider.getContractsAddresses(); } public async getPublicStorageAt(contract: AztecAddress, slot: Fr) { - if (!(await this.getContractInstance(contract))) { - throw new Error(`Contract ${contract.toString()} is not deployed`); - } return await this.node.getPublicStorageAt('latest', contract, slot); } public async getNotes(filter: NotesFilter): Promise { - const noteDaos = await this.db.getNotes(filter); + const noteDaos = await this.noteDataProvider.getNotes(filter); const extendedNotes = noteDaos.map(async dao => { let owner = filter.owner; if (owner === undefined) { - const completeAddresses = await this.db.getCompleteAddresses(); + const completeAddresses = await this.addressDataProvider.getCompleteAddresses(); const completeAddressIndex = ( await Promise.all(completeAddresses.map(completeAddresses => completeAddresses.address.toAddressPoint())) ).findIndex(addressPoint => addressPoint.equals(dao.addressPoint)); @@ -559,7 +625,7 @@ export class PXEService implements PXE { } async #getFunctionCall(functionName: string, args: any[], to: AztecAddress): Promise { - const contract = await this.db.getContract(to); + const contract = await this.contractDataProvider.getContract(to); if (!contract) { throw new Error( `Unknown contract ${to}: add it to PXE Service by calling server.addContracts(...).\nSee docs for context: https://docs.aztec.network/developers/reference/debugging/aztecnr-errors#unknown-contract-0x0-add-it-to-pxe-by-calling-serveraddcontracts`, @@ -622,8 +688,8 @@ export class PXEService implements PXE { for (const name of protocolContractNames) { const { address, contractClass, instance, artifact } = await this.protocolContractsProvider.getProtocolContractArtifact(name); - await this.db.addContractArtifact(contractClass.id, artifact); - await this.db.addContractInstance(instance); + await this.contractDataProvider.addContractArtifact(contractClass.id, artifact); + await this.contractDataProvider.addContractInstance(instance); registered[name] = address.toString(); } this.log.verbose(`Registered protocol contracts in pxe`, registered); @@ -661,7 +727,7 @@ export class PXEService implements PXE { return result; } catch (err) { if (err instanceof SimulationError) { - await enrichSimulationError(err, this.db, this.log); + await enrichSimulationError(err, this.contractDataProvider, this.log); } throw err; } @@ -687,7 +753,7 @@ export class PXEService implements PXE { return result; } catch (err) { if (err instanceof SimulationError) { - await enrichSimulationError(err, this.db, this.log); + await enrichSimulationError(err, this.contractDataProvider, this.log); } throw err; } @@ -711,7 +777,7 @@ export class PXEService implements PXE { } catch (err) { if (err instanceof SimulationError) { try { - await enrichPublicSimulationError(err, this.contractDataProvider, this.db, this.log); + await enrichPublicSimulationError(err, this.contractDataProvider, this.log); } catch (enrichErr) { this.log.error(`Failed to enrich public simulation error: ${enrichErr}`); } @@ -864,7 +930,7 @@ export class PXEService implements PXE { } async resetNoteSyncData() { - return await this.db.resetNoteSyncData(); + return await this.taggingDataProvider.resetNoteSyncData(); } private contextualizeError(err: Error, ...context: string[]): Error { diff --git a/yarn-project/pxe/src/storage/address_data_provider/address_data_provider.test.ts b/yarn-project/pxe/src/storage/address_data_provider/address_data_provider.test.ts new file mode 100644 index 000000000000..ee7cc84d3c6c --- /dev/null +++ b/yarn-project/pxe/src/storage/address_data_provider/address_data_provider.test.ts @@ -0,0 +1,69 @@ +import { timesParallel } from '@aztec/foundation/collection'; +import { Point } from '@aztec/foundation/fields'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { CompleteAddress } from '@aztec/stdlib/contract'; +import { PublicKeys } from '@aztec/stdlib/keys'; + +import { AddressDataProvider } from './address_data_provider.js'; + +describe('addresses', () => { + let addressDataProvider: AddressDataProvider; + + beforeEach(async () => { + const store = await openTmpStore('address_data_provider_test'); + addressDataProvider = new AddressDataProvider(store); + }); + + it('stores and retrieves addresses', async () => { + const address = await CompleteAddress.random(); + await expect(addressDataProvider.addCompleteAddress(address)).resolves.toBe(true); + await expect(addressDataProvider.getCompleteAddress(address.address)).resolves.toEqual(address); + }); + + it('silently ignores an address it already knows about', async () => { + const address = await CompleteAddress.random(); + await expect(addressDataProvider.addCompleteAddress(address)).resolves.toBe(true); + await expect(addressDataProvider.addCompleteAddress(address)).resolves.toBe(false); + }); + + it.skip('refuses to overwrite an address with a different public key', async () => { + const address = await CompleteAddress.random(); + const otherAddress = await CompleteAddress.create( + address.address, + new PublicKeys(await Point.random(), await Point.random(), await Point.random(), await Point.random()), + address.partialAddress, + ); + + await addressDataProvider.addCompleteAddress(address); + await expect(addressDataProvider.addCompleteAddress(otherAddress)).rejects.toThrow(); + }); + + it('returns all addresses', async () => { + const addresses = await timesParallel(10, () => CompleteAddress.random()); + for (const address of addresses) { + await addressDataProvider.addCompleteAddress(address); + } + + const result = await addressDataProvider.getCompleteAddresses(); + expect(result).toEqual(expect.arrayContaining(addresses)); + }); + + it('returns a single address', async () => { + const addresses = await timesParallel(10, () => CompleteAddress.random()); + for (const address of addresses) { + await addressDataProvider.addCompleteAddress(address); + } + + const result = await addressDataProvider.getCompleteAddress(addresses[3].address); + expect(result).toEqual(addresses[3]); + }); + + it("returns an empty array if it doesn't have addresses", async () => { + expect(await addressDataProvider.getCompleteAddresses()).toEqual([]); + }); + + it("returns undefined if it doesn't have an address", async () => { + const completeAddress = await CompleteAddress.random(); + expect(await addressDataProvider.getCompleteAddress(completeAddress.address)).toBeUndefined(); + }); +}); diff --git a/yarn-project/pxe/src/storage/address_data_provider/address_data_provider.ts b/yarn-project/pxe/src/storage/address_data_provider/address_data_provider.ts new file mode 100644 index 000000000000..c59c5368acd1 --- /dev/null +++ b/yarn-project/pxe/src/storage/address_data_provider/address_data_provider.ts @@ -0,0 +1,71 @@ +import { toArray } from '@aztec/foundation/iterable'; +import type { AztecAsyncArray, AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { CompleteAddress } from '@aztec/stdlib/contract'; + +import type { DataProvider } from '../data_provider.js'; + +export class AddressDataProvider implements DataProvider { + #store: AztecAsyncKVStore; + #completeAddresses: AztecAsyncArray; + #completeAddressIndex: AztecAsyncMap; + + constructor(store: AztecAsyncKVStore) { + this.#store = store; + + this.#completeAddresses = this.#store.openArray('complete_addresses'); + this.#completeAddressIndex = this.#store.openMap('complete_address_index'); + } + + addCompleteAddress(completeAddress: CompleteAddress): Promise { + return this.#store.transactionAsync(async () => { + // TODO readd this + // await this.#addScope(completeAddress.address); + + const addressString = completeAddress.address.toString(); + const buffer = completeAddress.toBuffer(); + const existing = await this.#completeAddressIndex.getAsync(addressString); + if (existing === undefined) { + const index = await this.#completeAddresses.lengthAsync(); + await this.#completeAddresses.push(buffer); + await this.#completeAddressIndex.set(addressString, index); + + return true; + } else { + const existingBuffer = await this.#completeAddresses.atAsync(existing); + + if (existingBuffer && Buffer.from(existingBuffer).equals(buffer)) { + return false; + } + + throw new Error( + `Complete address with aztec address ${addressString} but different public key or partial key already exists in memory database`, + ); + } + }); + } + + async #getCompleteAddress(address: AztecAddress): Promise { + const index = await this.#completeAddressIndex.getAsync(address.toString()); + if (index === undefined) { + return undefined; + } + + const value = await this.#completeAddresses.atAsync(index); + return value ? await CompleteAddress.fromBuffer(value) : undefined; + } + + getCompleteAddress(account: AztecAddress): Promise { + return this.#getCompleteAddress(account); + } + + async getCompleteAddresses(): Promise { + return await Promise.all( + (await toArray(this.#completeAddresses.valuesAsync())).map(v => CompleteAddress.fromBuffer(v)), + ); + } + + async getSize(): Promise { + return (await this.#completeAddresses.lengthAsync()) * CompleteAddress.SIZE_IN_BYTES; + } +} diff --git a/yarn-project/pxe/src/storage/address_data_provider/index.ts b/yarn-project/pxe/src/storage/address_data_provider/index.ts new file mode 100644 index 000000000000..c4263314cee4 --- /dev/null +++ b/yarn-project/pxe/src/storage/address_data_provider/index.ts @@ -0,0 +1 @@ +export { AddressDataProvider } from './address_data_provider.js'; diff --git a/yarn-project/pxe/src/storage/auth_witness_data_provider/auth_witness_data_provider.ts b/yarn-project/pxe/src/storage/auth_witness_data_provider/auth_witness_data_provider.ts new file mode 100644 index 000000000000..1b130691a1ea --- /dev/null +++ b/yarn-project/pxe/src/storage/auth_witness_data_provider/auth_witness_data_provider.ts @@ -0,0 +1,34 @@ +import { Fr } from '@aztec/foundation/fields'; +import { toArray } from '@aztec/foundation/iterable'; +import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; + +import type { DataProvider } from '../data_provider.js'; + +export class AuthWitnessDataProvider implements DataProvider { + #store: AztecAsyncKVStore; + #authWitnesses: AztecAsyncMap; + + constructor(store: AztecAsyncKVStore) { + this.#store = store; + this.#authWitnesses = this.#store.openMap('auth_witnesses'); + } + + async addAuthWitness(messageHash: Fr, witness: Fr[]): Promise { + await this.#authWitnesses.set( + messageHash.toString(), + witness.map(w => w.toBuffer()), + ); + } + + async getAuthWitness(messageHash: Fr): Promise { + const witness = await this.#authWitnesses.getAsync(messageHash.toString()); + return witness?.map(w => Fr.fromBuffer(w)); + } + + async getSize(): Promise { + return (await toArray(this.#authWitnesses.valuesAsync())).reduce( + (sum, value) => sum + value.length * Fr.SIZE_IN_BYTES, + 0, + ); + } +} diff --git a/yarn-project/pxe/src/storage/auth_witness_data_provider/auth_witness_data_providert.test.ts b/yarn-project/pxe/src/storage/auth_witness_data_provider/auth_witness_data_providert.test.ts new file mode 100644 index 000000000000..e2cbb088c0e6 --- /dev/null +++ b/yarn-project/pxe/src/storage/auth_witness_data_provider/auth_witness_data_providert.test.ts @@ -0,0 +1,33 @@ +import { Fr } from '@aztec/foundation/fields'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; + +import { AuthWitnessDataProvider } from './auth_witness_data_provider.js'; + +describe('auth witnesses', () => { + let authWitnessDataProvider: AuthWitnessDataProvider; + + beforeEach(async () => { + const store = await openTmpStore('auth_witness_data_provider_test'); + authWitnessDataProvider = new AuthWitnessDataProvider(store); + }); + it('stores and retrieves auth witnesses', async () => { + const messageHash = Fr.random(); + const witness = [Fr.random(), Fr.random()]; + + await authWitnessDataProvider.addAuthWitness(messageHash, witness); + await expect(authWitnessDataProvider.getAuthWitness(messageHash)).resolves.toEqual(witness); + }); + + it("returns undefined if it doesn't have auth witnesses for the message", async () => { + const messageHash = Fr.random(); + await expect(authWitnessDataProvider.getAuthWitness(messageHash)).resolves.toBeUndefined(); + }); + + it.skip('refuses to overwrite auth witnesses for the same message', async () => { + const messageHash = Fr.random(); + const witness = [Fr.random(), Fr.random()]; + + await authWitnessDataProvider.addAuthWitness(messageHash, witness); + await expect(authWitnessDataProvider.addAuthWitness(messageHash, witness)).rejects.toThrow(); + }); +}); diff --git a/yarn-project/pxe/src/storage/auth_witness_data_provider/index.ts b/yarn-project/pxe/src/storage/auth_witness_data_provider/index.ts new file mode 100644 index 000000000000..9fcc2e76e803 --- /dev/null +++ b/yarn-project/pxe/src/storage/auth_witness_data_provider/index.ts @@ -0,0 +1 @@ +export { AuthWitnessDataProvider } from './auth_witness_data_provider.js'; diff --git a/yarn-project/pxe/src/storage/capsule_data_provider/capsule_data_provider.test.ts b/yarn-project/pxe/src/storage/capsule_data_provider/capsule_data_provider.test.ts new file mode 100644 index 000000000000..ad11e9b5790a --- /dev/null +++ b/yarn-project/pxe/src/storage/capsule_data_provider/capsule_data_provider.test.ts @@ -0,0 +1,171 @@ +import { Fr } from '@aztec/foundation/fields'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; + +import { CapsuleDataProvider } from './capsule_data_provider.js'; + +describe('contract non-volatile database', () => { + let contract: AztecAddress; + let capsuleDataProvider: CapsuleDataProvider; + + beforeEach(async () => { + // Setup mock contract address + contract = await AztecAddress.random(); + // Setup data provider + const store = await openTmpStore('capsule_data_provider_test'); + capsuleDataProvider = new CapsuleDataProvider(store); + }); + + it('stores and loads a single value', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + await capsuleDataProvider.storeCapsule(contract, slot, values); + const result = await capsuleDataProvider.loadCapsule(contract, slot); + expect(result).toEqual(values); + }); + + it('stores and loads multiple values', async () => { + const slot = new Fr(1); + const values = [new Fr(42), new Fr(43), new Fr(44)]; + + await capsuleDataProvider.storeCapsule(contract, slot, values); + const result = await capsuleDataProvider.loadCapsule(contract, slot); + expect(result).toEqual(values); + }); + + it('overwrites existing values', async () => { + const slot = new Fr(1); + const initialValues = [new Fr(42)]; + const newValues = [new Fr(100)]; + + await capsuleDataProvider.storeCapsule(contract, slot, initialValues); + await capsuleDataProvider.storeCapsule(contract, slot, newValues); + + const result = await capsuleDataProvider.loadCapsule(contract, slot); + expect(result).toEqual(newValues); + }); + + it('stores values for different contracts independently', async () => { + const anotherContract = await AztecAddress.random(); + const slot = new Fr(1); + const values1 = [new Fr(42)]; + const values2 = [new Fr(100)]; + + await capsuleDataProvider.storeCapsule(contract, slot, values1); + await capsuleDataProvider.storeCapsule(anotherContract, slot, values2); + + const result1 = await capsuleDataProvider.loadCapsule(contract, slot); + const result2 = await capsuleDataProvider.loadCapsule(anotherContract, slot); + + expect(result1).toEqual(values1); + expect(result2).toEqual(values2); + }); + + it('returns null for non-existent slots', async () => { + const slot = Fr.random(); + const result = await capsuleDataProvider.loadCapsule(contract, slot); + expect(result).toBeNull(); + }); + + it('deletes a slot', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + await capsuleDataProvider.storeCapsule(contract, slot, values); + await capsuleDataProvider.deleteCapsule(contract, slot); + + expect(await capsuleDataProvider.loadCapsule(contract, slot)).toBeNull(); + }); + + it('deletes an empty slot', async () => { + const slot = new Fr(1); + await capsuleDataProvider.deleteCapsule(contract, slot); + + expect(await capsuleDataProvider.loadCapsule(contract, slot)).toBeNull(); + }); + + it('copies a single value', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + await capsuleDataProvider.storeCapsule(contract, slot, values); + + const dstSlot = new Fr(5); + await capsuleDataProvider.copyCapsule(contract, slot, dstSlot, 1); + + expect(await capsuleDataProvider.loadCapsule(contract, dstSlot)).toEqual(values); + }); + + it('copies multiple non-overlapping values', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await capsuleDataProvider.storeCapsule(contract, src, valuesArray[0]); + await capsuleDataProvider.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1]); + await capsuleDataProvider.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(5); + await capsuleDataProvider.copyCapsule(contract, src, dst, 3); + + expect(await capsuleDataProvider.loadCapsule(contract, dst)).toEqual(valuesArray[0]); + expect(await capsuleDataProvider.loadCapsule(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await capsuleDataProvider.loadCapsule(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + }); + + it('copies overlapping values with src ahead', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await capsuleDataProvider.storeCapsule(contract, src, valuesArray[0]); + await capsuleDataProvider.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1]); + await capsuleDataProvider.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(2); + await capsuleDataProvider.copyCapsule(contract, src, dst, 3); + + expect(await capsuleDataProvider.loadCapsule(contract, dst)).toEqual(valuesArray[0]); + expect(await capsuleDataProvider.loadCapsule(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await capsuleDataProvider.loadCapsule(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + + // Slots 2 and 3 (src[1] and src[2]) should have been overwritten since they are also dst[0] and dst[1] + expect(await capsuleDataProvider.loadCapsule(contract, src)).toEqual(valuesArray[0]); // src[0] (unchanged) + expect(await capsuleDataProvider.loadCapsule(contract, src.add(new Fr(1)))).toEqual(valuesArray[0]); // dst[0] + expect(await capsuleDataProvider.loadCapsule(contract, src.add(new Fr(2)))).toEqual(valuesArray[1]); // dst[1] + }); + + it('copies overlapping values with dst ahead', async () => { + const src = new Fr(5); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await capsuleDataProvider.storeCapsule(contract, src, valuesArray[0]); + await capsuleDataProvider.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1]); + await capsuleDataProvider.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(4); + await capsuleDataProvider.copyCapsule(contract, src, dst, 3); + + expect(await capsuleDataProvider.loadCapsule(contract, dst)).toEqual(valuesArray[0]); + expect(await capsuleDataProvider.loadCapsule(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await capsuleDataProvider.loadCapsule(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + + // Slots 5 and 6 (src[0] and src[1]) should have been overwritten since they are also dst[1] and dst[2] + expect(await capsuleDataProvider.loadCapsule(contract, src)).toEqual(valuesArray[1]); // dst[1] + expect(await capsuleDataProvider.loadCapsule(contract, src.add(new Fr(1)))).toEqual(valuesArray[2]); // dst[2] + expect(await capsuleDataProvider.loadCapsule(contract, src.add(new Fr(2)))).toEqual(valuesArray[2]); // src[2] (unchanged) + }); + + it('copying fails if any value is empty', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await capsuleDataProvider.storeCapsule(contract, src, valuesArray[0]); + // We skip src[1] + await capsuleDataProvider.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(5); + await expect(capsuleDataProvider.copyCapsule(contract, src, dst, 3)).rejects.toThrow( + 'Attempted to copy empty slot', + ); + }); +}); diff --git a/yarn-project/pxe/src/storage/capsule_data_provider/capsule_data_provider.ts b/yarn-project/pxe/src/storage/capsule_data_provider/capsule_data_provider.ts new file mode 100644 index 000000000000..0d987ee0fe1c --- /dev/null +++ b/yarn-project/pxe/src/storage/capsule_data_provider/capsule_data_provider.ts @@ -0,0 +1,80 @@ +import { Fr } from '@aztec/foundation/fields'; +import { toArray } from '@aztec/foundation/iterable'; +import { type LogFn, createDebugOnlyLogger } from '@aztec/foundation/log'; +import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; + +import type { DataProvider } from '../data_provider.js'; + +export class CapsuleDataProvider implements DataProvider { + #store: AztecAsyncKVStore; + + // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${key}` + #capsules: AztecAsyncMap; + + debug: LogFn; + + constructor(store: AztecAsyncKVStore) { + this.#store = store; + + this.#capsules = this.#store.openMap('capsules'); + + this.debug = createDebugOnlyLogger('pxe:capsule-data-provider'); + } + + async storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[]): Promise { + await this.#capsules.set(dbSlotToKey(contractAddress, slot), Buffer.concat(capsule.map(value => value.toBuffer()))); + } + + async loadCapsule(contractAddress: AztecAddress, slot: Fr): Promise { + const dataBuffer = await this.#capsules.getAsync(dbSlotToKey(contractAddress, slot)); + if (!dataBuffer) { + this.debug(`Data not found for contract ${contractAddress.toString()} and slot ${slot.toString()}`); + return null; + } + const capsule: Fr[] = []; + for (let i = 0; i < dataBuffer.length; i += Fr.SIZE_IN_BYTES) { + capsule.push(Fr.fromBuffer(dataBuffer.subarray(i, i + Fr.SIZE_IN_BYTES))); + } + return capsule; + } + + async deleteCapsule(contractAddress: AztecAddress, slot: Fr): Promise { + await this.#capsules.delete(dbSlotToKey(contractAddress, slot)); + } + + async copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + // In order to support overlapping source and destination regions, we need to check the relative positions of source + // and destination. If destination is ahead of source, then by the time we overwrite source elements using forward + // indexes we'll have already read those. On the contrary, if source is ahead of destination we need to use backward + // indexes to avoid reading elements that've been overwritten. + + const indexes = Array.from(Array(numEntries).keys()); + if (srcSlot.lt(dstSlot)) { + indexes.reverse(); + } + + for (const i of indexes) { + const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i))); + const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i))); + + const toCopy = await this.#capsules.getAsync(currentSrcSlot); + if (!toCopy) { + throw new Error(`Attempted to copy empty slot ${currentSrcSlot} for contract ${contractAddress.toString()}`); + } + + await this.#capsules.set(currentDstSlot, toCopy); + } + } + + public async getSize() { + return (await toArray(this.#capsules.valuesAsync())).reduce( + (sum, value) => sum + value.length * Fr.SIZE_IN_BYTES, + 0, + ); + } +} + +function dbSlotToKey(contractAddress: AztecAddress, slot: Fr): string { + return `${contractAddress.toString()}:${slot.toString()}`; +} diff --git a/yarn-project/pxe/src/storage/capsule_data_provider/index.ts b/yarn-project/pxe/src/storage/capsule_data_provider/index.ts new file mode 100644 index 000000000000..69397935116c --- /dev/null +++ b/yarn-project/pxe/src/storage/capsule_data_provider/index.ts @@ -0,0 +1 @@ +export { CapsuleDataProvider } from './capsule_data_provider.js'; diff --git a/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.test.ts b/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.test.ts new file mode 100644 index 000000000000..83547f0d4a14 --- /dev/null +++ b/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.test.ts @@ -0,0 +1,45 @@ +import { Fr } from '@aztec/foundation/fields'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { BenchmarkingContractArtifact } from '@aztec/noir-contracts.js/Benchmarking'; +import { TestContractArtifact } from '@aztec/noir-contracts.js/Test'; +import { FunctionType } from '@aztec/stdlib/abi'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { SerializableContractInstance } from '@aztec/stdlib/contract'; + +import { ContractDataProvider } from './contract_data_provider.js'; + +describe('ContractDataProvider', () => { + let contractDataProvider: ContractDataProvider; + + beforeEach(async () => { + const store = await openTmpStore('contract_data_provider_test'); + contractDataProvider = new ContractDataProvider(store); + }); + + it('stores a contract artifact', async () => { + const artifact = BenchmarkingContractArtifact; + const id = Fr.random(); + await contractDataProvider.addContractArtifact(id, artifact); + await expect(contractDataProvider.getContractArtifact(id)).resolves.toEqual(artifact); + }); + + it('does not store a contract artifact with a duplicate private function selector', async () => { + const artifact = TestContractArtifact; + const index = artifact.functions.findIndex(fn => fn.functionType === FunctionType.PRIVATE); + + const copiedFn = structuredClone(artifact.functions[index]); + artifact.functions.push(copiedFn); + + const id = Fr.random(); + await expect(contractDataProvider.addContractArtifact(id, artifact)).rejects.toThrow( + 'Repeated function selectors of private functions', + ); + }); + + it('stores a contract instance', async () => { + const address = await AztecAddress.random(); + const instance = (await SerializableContractInstance.random()).withAddress(address); + await contractDataProvider.addContractInstance(instance); + await expect(contractDataProvider.getContractInstance(address)).resolves.toEqual(instance); + }); +}); diff --git a/yarn-project/pxe/src/contract_data_provider/contract_data_provider.ts b/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.ts similarity index 67% rename from yarn-project/pxe/src/contract_data_provider/contract_data_provider.ts rename to yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.ts index a6003000942d..adb494d64ab7 100644 --- a/yarn-project/pxe/src/contract_data_provider/contract_data_provider.ts +++ b/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.ts @@ -1,18 +1,26 @@ import type { Fr } from '@aztec/foundation/fields'; +import { toArray } from '@aztec/foundation/iterable'; import type { MembershipWitness } from '@aztec/foundation/trees'; +import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; import { ContractClassNotFoundError, ContractNotFoundError } from '@aztec/simulator/client'; import { type ContractArtifact, type FunctionArtifact, type FunctionDebugMetadata, - type FunctionSelector, + FunctionSelector, + FunctionType, + contractArtifactFromBuffer, + contractArtifactToBuffer, getFunctionDebugMetadata, } from '@aztec/stdlib/abi'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { ContractClass, ContractInstance } from '@aztec/stdlib/contract'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { + type ContractClass, + type ContractInstanceWithAddress, + SerializableContractInstance, +} from '@aztec/stdlib/contract'; -import type { ContractArtifactDatabase } from '../database/interfaces/contract_artifact_db.js'; -import type { ContractInstanceDatabase } from '../database/interfaces/contract_instance_db.js'; +import type { DataProvider } from '../data_provider.js'; import { PrivateFunctionsTree } from './private_functions_tree.js'; /** @@ -22,30 +30,133 @@ import { PrivateFunctionsTree } from './private_functions_tree.js'; * to efficiently serve the requested data. It interacts with the ContractDatabase and AztecNode to fetch * the required information and facilitate cryptographic proof generation. */ -export class ContractDataProvider { +export class ContractDataProvider implements DataProvider { /** Map from contract class id to private function tree. */ - private contractClasses: Map = new Map(); + private contractClassesCache: Map = new Map(); + + #contractArtifacts: AztecAsyncMap; + #contractInstances: AztecAsyncMap; + + constructor(store: AztecAsyncKVStore) { + this.#contractArtifacts = store.openMap('contract_artifacts'); + this.#contractInstances = store.openMap('contracts_instances'); + } + + // Setters + + public async addContractArtifact(id: Fr, contract: ContractArtifact): Promise { + const privateFunctions = contract.functions.filter( + functionArtifact => functionArtifact.functionType === FunctionType.PRIVATE, + ); + + const privateSelectors = await Promise.all( + privateFunctions.map(async privateFunctionArtifact => + ( + await FunctionSelector.fromNameAndParameters(privateFunctionArtifact.name, privateFunctionArtifact.parameters) + ).toString(), + ), + ); + + if (privateSelectors.length !== new Set(privateSelectors).size) { + throw new Error('Repeated function selectors of private functions'); + } + + await this.#contractArtifacts.set(id.toString(), contractArtifactToBuffer(contract)); + } + + async addContractInstance(contract: ContractInstanceWithAddress): Promise { + await this.#contractInstances.set( + contract.address.toString(), + new SerializableContractInstance(contract).toBuffer(), + ); + } + + // Private getters + + async #getContractInstance(address: AztecAddress): Promise { + const contract = await this.#contractInstances.getAsync(address.toString()); + return contract && SerializableContractInstance.fromBuffer(contract).withAddress(address); + } - constructor(private db: ContractArtifactDatabase & ContractInstanceDatabase) {} + async #getContractArtifact(id: Fr): Promise { + const contract = await this.#contractArtifacts.getAsync(id.toString()); + // TODO(@spalladino): AztecAsyncMap lies and returns Uint8Arrays instead of Buffers, hence the extra Buffer.from. + return contract && contractArtifactFromBuffer(Buffer.from(contract)); + } + + /** + * Retrieve or create a ContractTree instance based on the provided class id. + * If an existing tree with the same class id is found in the cache, it will be returned. + * Otherwise, a new ContractTree instance will be created using the contract data from the database + * and added to the cache before returning. + * + * @param classId - The class id of the contract for which the ContractTree is required. + * @returns A ContractTree instance associated with the specified contract address. + * @throws An Error if the contract is not found in the ContractDatabase. + */ + private async getTreeForClassId(classId: Fr): Promise { + if (!this.contractClassesCache.has(classId.toString())) { + const artifact = await this.#getContractArtifact(classId); + if (!artifact) { + throw new ContractClassNotFoundError(classId.toString()); + } + const tree = await PrivateFunctionsTree.create(artifact); + this.contractClassesCache.set(classId.toString(), tree); + } + return this.contractClassesCache.get(classId.toString())!; + } + + /** + * Retrieve or create a ContractTree instance based on the provided AztecAddress. + * If an existing tree with the same contract address is found in the cache, it will be returned. + * Otherwise, a new ContractTree instance will be created using the contract data from the database + * and added to the cache before returning. + * + * @param contractAddress - The AztecAddress of the contract for which the ContractTree is required. + * @returns A ContractTree instance associated with the specified contract address. + * @throws An Error if the contract is not found in the ContractDatabase. + */ + private async getTreeForAddress(contractAddress: AztecAddress): Promise { + const instance = await this.getContractInstance(contractAddress); + return this.getTreeForClassId(instance.currentContractClassId); + } + + // Public getters + + async getContractsAddresses(): Promise { + const keys = await toArray(this.#contractInstances.keysAsync()); + return keys.map(AztecAddress.fromString); + } /** Returns a contract instance for a given address. Throws if not found. */ - public async getContractInstance(contractAddress: AztecAddress): Promise { - const instance = await this.db.getContractInstance(contractAddress); + public async getContractInstance(contractAddress: AztecAddress): Promise { + const instance = await this.#getContractInstance(contractAddress); if (!instance) { throw new ContractNotFoundError(contractAddress.toString()); } return instance; } + public async getContractArtifact(contractClassId: Fr): Promise { + const tree = await this.getTreeForClassId(contractClassId); + return tree.getArtifact(); + } + /** Returns a contract class for a given class id. Throws if not found. */ public async getContractClass(contractClassId: Fr): Promise { const tree = await this.getTreeForClassId(contractClassId); return tree.getContractClass(); } - public async getContractArtifact(contractClassId: Fr): Promise { - const tree = await this.getTreeForClassId(contractClassId); - return tree.getArtifact(); + public async getContract( + address: AztecAddress, + ): Promise<(ContractInstanceWithAddress & ContractArtifact) | undefined> { + const instance = await this.getContractInstance(address); + const artifact = instance && (await this.getContractArtifact(instance?.currentContractClassId)); + if (!instance || !artifact) { + return undefined; + } + return { ...instance, ...artifact }; } /** @@ -142,40 +253,9 @@ export class ContractDataProvider { return `${contractName}:${functionName}`; } - /** - * Retrieve or create a ContractTree instance based on the provided class id. - * If an existing tree with the same class id is found in the cache, it will be returned. - * Otherwise, a new ContractTree instance will be created using the contract data from the database - * and added to the cache before returning. - * - * @param classId - The class id of the contract for which the ContractTree is required. - * @returns A ContractTree instance associated with the specified contract address. - * @throws An Error if the contract is not found in the ContractDatabase. - */ - private async getTreeForClassId(classId: Fr): Promise { - if (!this.contractClasses.has(classId.toString())) { - const artifact = await this.db.getContractArtifact(classId); - if (!artifact) { - throw new ContractClassNotFoundError(classId.toString()); - } - const tree = await PrivateFunctionsTree.create(artifact); - this.contractClasses.set(classId.toString(), tree); - } - return this.contractClasses.get(classId.toString())!; - } - - /** - * Retrieve or create a ContractTree instance based on the provided AztecAddress. - * If an existing tree with the same contract address is found in the cache, it will be returned. - * Otherwise, a new ContractTree instance will be created using the contract data from the database - * and added to the cache before returning. - * - * @param contractAddress - The AztecAddress of the contract for which the ContractTree is required. - * @returns A ContractTree instance associated with the specified contract address. - * @throws An Error if the contract is not found in the ContractDatabase. - */ - private async getTreeForAddress(contractAddress: AztecAddress): Promise { - const instance = await this.getContractInstance(contractAddress); - return this.getTreeForClassId(instance.currentContractClassId); + public async getSize() { + return (await toArray(this.#contractInstances.valuesAsync())) + .concat(await toArray(this.#contractArtifacts.valuesAsync())) + .reduce((sum, value) => sum + value.length, 0); } } diff --git a/yarn-project/pxe/src/contract_data_provider/index.ts b/yarn-project/pxe/src/storage/contract_data_provider/index.ts similarity index 100% rename from yarn-project/pxe/src/contract_data_provider/index.ts rename to yarn-project/pxe/src/storage/contract_data_provider/index.ts diff --git a/yarn-project/pxe/src/contract_data_provider/private_functions_tree.ts b/yarn-project/pxe/src/storage/contract_data_provider/private_functions_tree.ts similarity index 100% rename from yarn-project/pxe/src/contract_data_provider/private_functions_tree.ts rename to yarn-project/pxe/src/storage/contract_data_provider/private_functions_tree.ts diff --git a/yarn-project/pxe/src/storage/data_provider.ts b/yarn-project/pxe/src/storage/data_provider.ts new file mode 100644 index 000000000000..e70adf3baab2 --- /dev/null +++ b/yarn-project/pxe/src/storage/data_provider.ts @@ -0,0 +1,3 @@ +export interface DataProvider { + getSize(): Promise; +} diff --git a/yarn-project/pxe/src/storage/index.ts b/yarn-project/pxe/src/storage/index.ts new file mode 100644 index 000000000000..be6fb54e6a57 --- /dev/null +++ b/yarn-project/pxe/src/storage/index.ts @@ -0,0 +1,10 @@ +export * from './address_data_provider/index.js'; +export * from './auth_witness_data_provider/index.js'; +export * from './capsule_data_provider/index.js'; +export * from './contract_data_provider/index.js'; +export * from './note_data_provider/index.js'; +export * from './sync_data_provider/index.js'; +export * from './tagging_data_provider/index.js'; +export * from './data_provider.js'; + +export const PXE_DATA_SCHEMA_VERSION = 2; diff --git a/yarn-project/pxe/src/storage/note_data_provider/index.ts b/yarn-project/pxe/src/storage/note_data_provider/index.ts new file mode 100644 index 000000000000..37c34f153d49 --- /dev/null +++ b/yarn-project/pxe/src/storage/note_data_provider/index.ts @@ -0,0 +1,2 @@ +export { NoteDao } from './note_dao.js'; +export { NoteDataProvider } from './note_data_provider.js'; diff --git a/yarn-project/pxe/src/database/note_dao.test.ts b/yarn-project/pxe/src/storage/note_data_provider/note_dao.test.ts similarity index 100% rename from yarn-project/pxe/src/database/note_dao.test.ts rename to yarn-project/pxe/src/storage/note_data_provider/note_dao.test.ts diff --git a/yarn-project/pxe/src/database/note_dao.ts b/yarn-project/pxe/src/storage/note_data_provider/note_dao.ts similarity index 100% rename from yarn-project/pxe/src/database/note_dao.ts rename to yarn-project/pxe/src/storage/note_data_provider/note_dao.ts diff --git a/yarn-project/pxe/src/storage/note_data_provider/note_data_provider.test.ts b/yarn-project/pxe/src/storage/note_data_provider/note_data_provider.test.ts new file mode 100644 index 000000000000..d859bbfddf6b --- /dev/null +++ b/yarn-project/pxe/src/storage/note_data_provider/note_data_provider.test.ts @@ -0,0 +1,266 @@ +import { timesParallel } from '@aztec/foundation/collection'; +import { Fr } from '@aztec/foundation/fields'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { CompleteAddress } from '@aztec/stdlib/contract'; +import { NoteStatus, type NotesFilter } from '@aztec/stdlib/note'; +import { randomTxHash } from '@aztec/stdlib/testing'; + +import times from 'lodash.times'; + +import { NoteDao } from './note_dao.js'; +import { NoteDataProvider } from './note_data_provider.js'; + +describe('NoteDataProvider', () => { + let noteDataProvider: NoteDataProvider; + let owners: CompleteAddress[]; + let contractAddresses: AztecAddress[]; + let storageSlots: Fr[]; + let notes: NoteDao[]; + + beforeEach(async () => { + const store = await openTmpStore('note_data_provider_test'); + noteDataProvider = await NoteDataProvider.create(store); + }); + + const filteringTests: [() => Promise, () => Promise][] = [ + [() => Promise.resolve({}), () => Promise.resolve(notes)], + + [ + () => Promise.resolve({ contractAddress: contractAddresses[0] }), + () => Promise.resolve(notes.filter(note => note.contractAddress.equals(contractAddresses[0]))), + ], + [async () => ({ contractAddress: await AztecAddress.random() }), () => Promise.resolve([])], + + [ + () => Promise.resolve({ storageSlot: storageSlots[0] }), + () => Promise.resolve(notes.filter(note => note.storageSlot.equals(storageSlots[0]))), + ], + [() => Promise.resolve({ storageSlot: Fr.random() }), () => Promise.resolve([])], + + [() => Promise.resolve({ txHash: notes[0].txHash }), () => Promise.resolve([notes[0]])], + [() => Promise.resolve({ txHash: randomTxHash() }), () => Promise.resolve([])], + + [ + () => Promise.resolve({ owner: owners[0].address }), + async () => { + const ownerAddressPoint = await owners[0].address.toAddressPoint(); + return notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); + }, + ], + + [ + () => Promise.resolve({ contractAddress: contractAddresses[0], storageSlot: storageSlots[0] }), + () => + Promise.resolve( + notes.filter( + note => note.contractAddress.equals(contractAddresses[0]) && note.storageSlot.equals(storageSlots[0]), + ), + ), + ], + [ + () => Promise.resolve({ contractAddress: contractAddresses[0], storageSlot: storageSlots[1] }), + () => Promise.resolve([]), + ], + ]; + + beforeEach(async () => { + owners = await timesParallel(2, () => CompleteAddress.random()); + contractAddresses = await timesParallel(2, () => AztecAddress.random()); + storageSlots = times(2, () => Fr.random()); + + notes = await timesParallel(10, async i => { + const addressPoint = await owners[i % owners.length].address.toAddressPoint(); + return NoteDao.random({ + contractAddress: contractAddresses[i % contractAddresses.length], + storageSlot: storageSlots[i % storageSlots.length], + addressPoint, + index: BigInt(i), + l2BlockNumber: i, + }); + }); + + for (const owner of owners) { + await noteDataProvider.addScope(owner.address); + } + }); + + it.each(filteringTests)('stores notes in bulk and retrieves notes', async (getFilter, getExpected) => { + await noteDataProvider.addNotes(notes); + const returnedNotes = await noteDataProvider.getNotes(await getFilter()); + const expected = await getExpected(); + expect(returnedNotes.sort()).toEqual(expected.sort()); + }); + + it.each(filteringTests)('stores notes one by one and retrieves notes', async (getFilter, getExpected) => { + for (const note of notes) { + await noteDataProvider.addNote(note); + } + + const returnedNotes = await noteDataProvider.getNotes(await getFilter()); + + const expected = await getExpected(); + expect(returnedNotes.sort()).toEqual(expected.sort()); + }); + + it.each(filteringTests)('retrieves nullified notes', async (getFilter, getExpected) => { + await noteDataProvider.addNotes(notes); + + // Nullify all notes and use the same filter as other test cases + for (const owner of owners) { + const ownerAddressPoint = await owner.address.toAddressPoint(); + const notesToNullify = notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); + const nullifiers = notesToNullify.map(note => ({ + data: note.siloedNullifier, + l2BlockNumber: note.l2BlockNumber, + l2BlockHash: note.l2BlockHash, + })); + await expect(noteDataProvider.removeNullifiedNotes(nullifiers, ownerAddressPoint)).resolves.toEqual( + notesToNullify, + ); + } + const filter = await getFilter(); + const returnedNotes = await noteDataProvider.getNotes({ ...filter, status: NoteStatus.ACTIVE_OR_NULLIFIED }); + const expected = await getExpected(); + expect(returnedNotes.sort()).toEqual(expected.sort()); + }); + + it('skips nullified notes by default or when requesting active', async () => { + await noteDataProvider.addNotes(notes); + const ownerAddressPoint = await owners[0].address.toAddressPoint(); + const notesToNullify = notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); + const nullifiers = notesToNullify.map(note => ({ + data: note.siloedNullifier, + l2BlockNumber: note.l2BlockNumber, + l2BlockHash: note.l2BlockHash, + })); + await expect(noteDataProvider.removeNullifiedNotes(nullifiers, notesToNullify[0].addressPoint)).resolves.toEqual( + notesToNullify, + ); + + const actualNotesWithDefault = await noteDataProvider.getNotes({}); + const actualNotesWithActive = await noteDataProvider.getNotes({ status: NoteStatus.ACTIVE }); + + expect(actualNotesWithDefault).toEqual(actualNotesWithActive); + expect(actualNotesWithActive).toEqual(notes.filter(note => !notesToNullify.includes(note))); + }); + + it('handles note unnullification', async () => { + await noteDataProvider.addNotes(notes); + const ownerAddressPoint = await owners[0].address.toAddressPoint(); + + const notesToNullify = notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); + const nullifiers = notesToNullify.map(note => ({ + data: note.siloedNullifier, + l2BlockNumber: 99, + l2BlockHash: Fr.random().toString(), + })); + await expect(noteDataProvider.removeNullifiedNotes(nullifiers, notesToNullify[0].addressPoint)).resolves.toEqual( + notesToNullify, + ); + await expect(noteDataProvider.unnullifyNotesAfter(98)).resolves.toEqual(undefined); + + const result = await noteDataProvider.getNotes({ status: NoteStatus.ACTIVE, owner: owners[0].address }); + + expect(result.sort()).toEqual([...notesToNullify].sort()); + }); + + it('returns active and nullified notes when requesting either', async () => { + await noteDataProvider.addNotes(notes); + const ownerAddressPoint = await owners[0].address.toAddressPoint(); + + const notesToNullify = notes.filter(note => note.addressPoint.equals(ownerAddressPoint)); + const nullifiers = notesToNullify.map(note => ({ + data: note.siloedNullifier, + l2BlockNumber: note.l2BlockNumber, + l2BlockHash: note.l2BlockHash, + })); + await expect(noteDataProvider.removeNullifiedNotes(nullifiers, notesToNullify[0].addressPoint)).resolves.toEqual( + notesToNullify, + ); + + const result = await noteDataProvider.getNotes({ + status: NoteStatus.ACTIVE_OR_NULLIFIED, + }); + + // We have to compare the sorted arrays since the database does not return the same order as when originally + // inserted combining active and nullified results. + expect(result.sort()).toEqual([...notes].sort()); + }); + + it('stores notes one by one and retrieves notes with siloed account', async () => { + for (const note of notes.slice(0, 5)) { + await noteDataProvider.addNote(note, owners[0].address); + } + + for (const note of notes.slice(5)) { + await noteDataProvider.addNote(note, owners[1].address); + } + + const owner0Notes = await noteDataProvider.getNotes({ + scopes: [owners[0].address], + }); + + expect(owner0Notes.sort()).toEqual(notes.slice(0, 5).sort()); + + const owner1Notes = await noteDataProvider.getNotes({ + scopes: [owners[1].address], + }); + + expect(owner1Notes.sort()).toEqual(notes.slice(5).sort()); + + const bothOwnerNotes = await noteDataProvider.getNotes({ + scopes: [owners[0].address, owners[1].address], + }); + + expect(bothOwnerNotes.sort()).toEqual(notes.sort()); + }); + + it('a nullified note removes notes from all accounts in the pxe', async () => { + await noteDataProvider.addNote(notes[0], owners[0].address); + await noteDataProvider.addNote(notes[0], owners[1].address); + + await expect( + noteDataProvider.getNotes({ + scopes: [owners[0].address], + }), + ).resolves.toEqual([notes[0]]); + await expect( + noteDataProvider.getNotes({ + scopes: [owners[1].address], + }), + ).resolves.toEqual([notes[0]]); + const ownerAddressPoint = await owners[0].address.toAddressPoint(); + await expect( + noteDataProvider.removeNullifiedNotes( + [ + { + data: notes[0].siloedNullifier, + l2BlockHash: notes[0].l2BlockHash, + l2BlockNumber: notes[0].l2BlockNumber, + }, + ], + ownerAddressPoint, + ), + ).resolves.toEqual([notes[0]]); + + await expect( + noteDataProvider.getNotes({ + scopes: [owners[0].address], + }), + ).resolves.toEqual([]); + await expect( + noteDataProvider.getNotes({ + scopes: [owners[1].address], + }), + ).resolves.toEqual([]); + }); + + it('removes notes after a given block', async () => { + await noteDataProvider.addNotes(notes, owners[0].address); + + await noteDataProvider.removeNotesAfter(5); + const result = await noteDataProvider.getNotes({ scopes: [owners[0].address] }); + expect(new Set(result)).toEqual(new Set(notes.slice(0, 6))); + }); +}); diff --git a/yarn-project/pxe/src/storage/note_data_provider/note_data_provider.ts b/yarn-project/pxe/src/storage/note_data_provider/note_data_provider.ts new file mode 100644 index 000000000000..fe0865bef220 --- /dev/null +++ b/yarn-project/pxe/src/storage/note_data_provider/note_data_provider.ts @@ -0,0 +1,359 @@ +import { toBufferBE } from '@aztec/foundation/bigint-buffer'; +import type { Fr, Point } from '@aztec/foundation/fields'; +import { toArray } from '@aztec/foundation/iterable'; +import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncMultiMap } from '@aztec/kv-store'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { InBlock } from '@aztec/stdlib/block'; +import type { PublicKey } from '@aztec/stdlib/keys'; +import { NoteStatus, type NotesFilter } from '@aztec/stdlib/note'; + +import type { DataProvider } from '../data_provider.js'; +import { NoteDao } from './note_dao.js'; + +export class NoteDataProvider implements DataProvider { + #store: AztecAsyncKVStore; + + #notes: AztecAsyncMap; + #nullifiedNotes: AztecAsyncMap; + #nullifierToNoteId: AztecAsyncMap; + #nullifiersByBlockNumber: AztecAsyncMultiMap; + + #nullifiedNotesToScope: AztecAsyncMultiMap; + #nullifiedNotesByContract: AztecAsyncMultiMap; + #nullifiedNotesByStorageSlot: AztecAsyncMultiMap; + #nullifiedNotesByTxHash: AztecAsyncMultiMap; + #nullifiedNotesByAddressPoint: AztecAsyncMultiMap; + #nullifiedNotesByNullifier: AztecAsyncMap; + + #scopes: AztecAsyncMap; + #notesToScope: AztecAsyncMultiMap; + #notesByContractAndScope: Map>; + #notesByStorageSlotAndScope: Map>; + #notesByTxHashAndScope: Map>; + #notesByAddressPointAndScope: Map>; + + private constructor(store: AztecAsyncKVStore) { + this.#store = store; + this.#notes = store.openMap('notes'); + this.#nullifiedNotes = store.openMap('nullified_notes'); + this.#nullifierToNoteId = store.openMap('nullifier_to_note'); + this.#nullifiersByBlockNumber = store.openMultiMap('nullifier_to_block_number'); + + this.#nullifiedNotesToScope = store.openMultiMap('nullified_notes_to_scope'); + this.#nullifiedNotesByContract = store.openMultiMap('nullified_notes_by_contract'); + this.#nullifiedNotesByStorageSlot = store.openMultiMap('nullified_notes_by_storage_slot'); + this.#nullifiedNotesByTxHash = store.openMultiMap('nullified_notes_by_tx_hash'); + this.#nullifiedNotesByAddressPoint = store.openMultiMap('nullified_notes_by_address_point'); + this.#nullifiedNotesByNullifier = store.openMap('nullified_notes_by_nullifier'); + + this.#scopes = store.openMap('scopes'); + this.#notesToScope = store.openMultiMap('notes_to_scope'); + this.#notesByContractAndScope = new Map>(); + this.#notesByStorageSlotAndScope = new Map>(); + this.#notesByTxHashAndScope = new Map>(); + this.#notesByAddressPointAndScope = new Map>(); + } + + public static async create(store: AztecAsyncKVStore): Promise { + const pxeDB = new NoteDataProvider(store); + for await (const scope of pxeDB.#scopes.keysAsync()) { + pxeDB.#notesByContractAndScope.set(scope, store.openMultiMap(`${scope}:notes_by_contract`)); + pxeDB.#notesByStorageSlotAndScope.set(scope, store.openMultiMap(`${scope}:notes_by_storage_slot`)); + pxeDB.#notesByTxHashAndScope.set(scope, store.openMultiMap(`${scope}:notes_by_tx_hash`)); + pxeDB.#notesByAddressPointAndScope.set(scope, store.openMultiMap(`${scope}:notes_by_address_point`)); + } + return pxeDB; + } + + public async addScope(scope: AztecAddress): Promise { + const scopeString = scope.toString(); + + if (await this.#scopes.hasAsync(scopeString)) { + return false; + } + + await this.#scopes.set(scopeString, true); + this.#notesByContractAndScope.set(scopeString, this.#store.openMultiMap(`${scopeString}:notes_by_contract`)); + this.#notesByStorageSlotAndScope.set(scopeString, this.#store.openMultiMap(`${scopeString}:notes_by_storage_slot`)); + this.#notesByTxHashAndScope.set(scopeString, this.#store.openMultiMap(`${scopeString}:notes_by_tx_hash`)); + this.#notesByAddressPointAndScope.set( + scopeString, + this.#store.openMultiMap(`${scopeString}:notes_by_address_point`), + ); + + return true; + } + + async addNote(note: NoteDao, scope?: AztecAddress): Promise { + await this.addNotes([note], scope); + } + + async addNotes(notes: NoteDao[], scope: AztecAddress = AztecAddress.ZERO): Promise { + if (!(await this.#scopes.hasAsync(scope.toString()))) { + await this.addScope(scope); + } + + return this.#store.transactionAsync(async () => { + for (const dao of notes) { + // store notes by their index in the notes hash tree + // this provides the uniqueness we need to store individual notes + // and should also return notes in the order that they were created. + // Had we stored them by their nullifier, they would be returned in random order + const noteIndex = toBufferBE(dao.index, 32).toString('hex'); + await this.#notes.set(noteIndex, dao.toBuffer()); + await this.#notesToScope.set(noteIndex, scope.toString()); + await this.#nullifierToNoteId.set(dao.siloedNullifier.toString(), noteIndex); + + await this.#notesByContractAndScope.get(scope.toString())!.set(dao.contractAddress.toString(), noteIndex); + await this.#notesByStorageSlotAndScope.get(scope.toString())!.set(dao.storageSlot.toString(), noteIndex); + await this.#notesByTxHashAndScope.get(scope.toString())!.set(dao.txHash.toString(), noteIndex); + await this.#notesByAddressPointAndScope.get(scope.toString())!.set(dao.addressPoint.toString(), noteIndex); + } + }); + } + + public removeNotesAfter(blockNumber: number): Promise { + return this.#store.transactionAsync(async () => { + const notes = await toArray(this.#notes.valuesAsync()); + for (const note of notes) { + const noteDao = NoteDao.fromBuffer(note); + if (noteDao.l2BlockNumber > blockNumber) { + const noteIndex = toBufferBE(noteDao.index, 32).toString('hex'); + await this.#notes.delete(noteIndex); + await this.#notesToScope.delete(noteIndex); + await this.#nullifierToNoteId.delete(noteDao.siloedNullifier.toString()); + const scopes = await toArray(this.#scopes.keysAsync()); + for (const scope of scopes) { + await this.#notesByAddressPointAndScope.get(scope)!.deleteValue(noteDao.addressPoint.toString(), noteIndex); + await this.#notesByTxHashAndScope.get(scope)!.deleteValue(noteDao.txHash.toString(), noteIndex); + await this.#notesByContractAndScope.get(scope)!.deleteValue(noteDao.contractAddress.toString(), noteIndex); + await this.#notesByStorageSlotAndScope.get(scope)!.deleteValue(noteDao.storageSlot.toString(), noteIndex); + } + } + } + }); + } + + public async unnullifyNotesAfter(blockNumber: number, synchedBlockNumber?: number): Promise { + const nullifiersToUndo: string[] = []; + const currentBlockNumber = blockNumber + 1; + const maxBlockNumber = synchedBlockNumber ?? currentBlockNumber; + for (let i = currentBlockNumber; i <= maxBlockNumber; i++) { + nullifiersToUndo.push(...(await toArray(this.#nullifiersByBlockNumber.getValuesAsync(i)))); + } + const notesIndexesToReinsert = await Promise.all( + nullifiersToUndo.map(nullifier => this.#nullifiedNotesByNullifier.getAsync(nullifier)), + ); + const notNullNoteIndexes = notesIndexesToReinsert.filter(noteIndex => noteIndex != undefined); + const nullifiedNoteBuffers = await Promise.all( + notNullNoteIndexes.map(noteIndex => this.#nullifiedNotes.getAsync(noteIndex!)), + ); + const noteDaos = nullifiedNoteBuffers + .filter(buffer => buffer != undefined) + .map(buffer => NoteDao.fromBuffer(buffer!)); + + await this.#store.transactionAsync(async () => { + for (const dao of noteDaos) { + const noteIndex = toBufferBE(dao.index, 32).toString('hex'); + await this.#notes.set(noteIndex, dao.toBuffer()); + await this.#nullifierToNoteId.set(dao.siloedNullifier.toString(), noteIndex); + + let scopes = (await toArray(this.#nullifiedNotesToScope.getValuesAsync(noteIndex))) ?? []; + + if (scopes.length === 0) { + scopes = [new AztecAddress(dao.addressPoint.x).toString()]; + } + + for (const scope of scopes) { + await this.#notesByContractAndScope.get(scope.toString())!.set(dao.contractAddress.toString(), noteIndex); + await this.#notesByStorageSlotAndScope.get(scope.toString())!.set(dao.storageSlot.toString(), noteIndex); + await this.#notesByTxHashAndScope.get(scope.toString())!.set(dao.txHash.toString(), noteIndex); + await this.#notesByAddressPointAndScope.get(scope.toString())!.set(dao.addressPoint.toString(), noteIndex); + await this.#notesToScope.set(noteIndex, scope); + } + + await this.#nullifiedNotes.delete(noteIndex); + await this.#nullifiedNotesToScope.delete(noteIndex); + await this.#nullifiersByBlockNumber.deleteValue(dao.l2BlockNumber, dao.siloedNullifier.toString()); + await this.#nullifiedNotesByContract.deleteValue(dao.contractAddress.toString(), noteIndex); + await this.#nullifiedNotesByStorageSlot.deleteValue(dao.storageSlot.toString(), noteIndex); + await this.#nullifiedNotesByTxHash.deleteValue(dao.txHash.toString(), noteIndex); + await this.#nullifiedNotesByAddressPoint.deleteValue(dao.addressPoint.toString(), noteIndex); + await this.#nullifiedNotesByNullifier.delete(dao.siloedNullifier.toString()); + } + }); + } + + async getNotes(filter: NotesFilter): Promise { + const publicKey: PublicKey | undefined = filter.owner ? await filter.owner.toAddressPoint() : undefined; + + filter.status = filter.status ?? NoteStatus.ACTIVE; + + const candidateNoteSources = []; + + filter.scopes ??= (await toArray(this.#scopes.keysAsync())).map(addressString => + AztecAddress.fromString(addressString), + ); + + const activeNoteIdsPerScope: string[][] = []; + + for (const scope of new Set(filter.scopes)) { + const formattedScopeString = scope.toString(); + if (!(await this.#scopes.hasAsync(formattedScopeString))) { + throw new Error('Trying to get incoming notes of an scope that is not in the PXE database'); + } + + activeNoteIdsPerScope.push( + publicKey + ? await toArray( + this.#notesByAddressPointAndScope.get(formattedScopeString)!.getValuesAsync(publicKey.toString()), + ) + : filter.txHash + ? await toArray( + this.#notesByTxHashAndScope.get(formattedScopeString)!.getValuesAsync(filter.txHash.toString()), + ) + : filter.contractAddress + ? await toArray( + this.#notesByContractAndScope + .get(formattedScopeString)! + .getValuesAsync(filter.contractAddress.toString()), + ) + : filter.storageSlot + ? await toArray( + this.#notesByStorageSlotAndScope.get(formattedScopeString)!.getValuesAsync(filter.storageSlot.toString()), + ) + : await toArray(this.#notesByAddressPointAndScope.get(formattedScopeString)!.valuesAsync()), + ); + } + + candidateNoteSources.push({ + ids: new Set(activeNoteIdsPerScope.flat()), + notes: this.#notes, + }); + + if (filter.status == NoteStatus.ACTIVE_OR_NULLIFIED) { + candidateNoteSources.push({ + ids: publicKey + ? await toArray(this.#nullifiedNotesByAddressPoint.getValuesAsync(publicKey.toString())) + : filter.txHash + ? await toArray(this.#nullifiedNotesByTxHash.getValuesAsync(filter.txHash.toString())) + : filter.contractAddress + ? await toArray(this.#nullifiedNotesByContract.getValuesAsync(filter.contractAddress.toString())) + : filter.storageSlot + ? await toArray(this.#nullifiedNotesByStorageSlot.getValuesAsync(filter.storageSlot.toString())) + : await toArray(this.#nullifiedNotes.keysAsync()), + notes: this.#nullifiedNotes, + }); + } + + const result: NoteDao[] = []; + for (const { ids, notes } of candidateNoteSources) { + for (const id of ids) { + const serializedNote = await notes.getAsync(id); + if (!serializedNote) { + continue; + } + + const note = NoteDao.fromBuffer(serializedNote); + if (filter.contractAddress && !note.contractAddress.equals(filter.contractAddress)) { + continue; + } + + if (filter.txHash && !note.txHash.equals(filter.txHash)) { + continue; + } + + if (filter.storageSlot && !note.storageSlot.equals(filter.storageSlot!)) { + continue; + } + + if (publicKey && !note.addressPoint.equals(publicKey)) { + continue; + } + + if (filter.siloedNullifier && !note.siloedNullifier.equals(filter.siloedNullifier)) { + continue; + } + + result.push(note); + } + } + + return result; + } + + removeNullifiedNotes(nullifiers: InBlock[], accountAddressPoint: Point): Promise { + if (nullifiers.length === 0) { + return Promise.resolve([]); + } + + return this.#store.transactionAsync(async () => { + const nullifiedNotes: NoteDao[] = []; + + for (const blockScopedNullifier of nullifiers) { + const { data: nullifier, l2BlockNumber: blockNumber } = blockScopedNullifier; + const noteIndex = await this.#nullifierToNoteId.getAsync(nullifier.toString()); + if (!noteIndex) { + continue; + } + + const noteBuffer = noteIndex ? await this.#notes.getAsync(noteIndex) : undefined; + + if (!noteBuffer) { + // note doesn't exist. Maybe it got nullified already + continue; + } + const noteScopes = (await toArray(this.#notesToScope.getValuesAsync(noteIndex))) ?? []; + const note = NoteDao.fromBuffer(noteBuffer); + if (!note.addressPoint.equals(accountAddressPoint)) { + // tried to nullify someone else's note + continue; + } + + nullifiedNotes.push(note); + + await this.#notes.delete(noteIndex); + await this.#notesToScope.delete(noteIndex); + + const scopes = await toArray(this.#scopes.keysAsync()); + + for (const scope of scopes) { + await this.#notesByAddressPointAndScope.get(scope)!.deleteValue(accountAddressPoint.toString(), noteIndex); + await this.#notesByTxHashAndScope.get(scope)!.deleteValue(note.txHash.toString(), noteIndex); + await this.#notesByContractAndScope.get(scope)!.deleteValue(note.contractAddress.toString(), noteIndex); + await this.#notesByStorageSlotAndScope.get(scope)!.deleteValue(note.storageSlot.toString(), noteIndex); + } + + if (noteScopes !== undefined) { + for (const scope of noteScopes) { + await this.#nullifiedNotesToScope.set(noteIndex, scope); + } + } + await this.#nullifiedNotes.set(noteIndex, note.toBuffer()); + await this.#nullifiersByBlockNumber.set(blockNumber, nullifier.toString()); + await this.#nullifiedNotesByContract.set(note.contractAddress.toString(), noteIndex); + await this.#nullifiedNotesByStorageSlot.set(note.storageSlot.toString(), noteIndex); + await this.#nullifiedNotesByTxHash.set(note.txHash.toString(), noteIndex); + await this.#nullifiedNotesByAddressPoint.set(note.addressPoint.toString(), noteIndex); + await this.#nullifiedNotesByNullifier.set(nullifier.toString(), noteIndex); + + await this.#nullifierToNoteId.delete(nullifier.toString()); + } + return nullifiedNotes; + }); + } + + async addNullifiedNote(note: NoteDao): Promise { + const noteIndex = toBufferBE(note.index, 32).toString('hex'); + + await this.#nullifiedNotes.set(noteIndex, note.toBuffer()); + await this.#nullifiedNotesByContract.set(note.contractAddress.toString(), noteIndex); + await this.#nullifiedNotesByStorageSlot.set(note.storageSlot.toString(), noteIndex); + await this.#nullifiedNotesByTxHash.set(note.txHash.toString(), noteIndex); + await this.#nullifiedNotesByAddressPoint.set(note.addressPoint.toString(), noteIndex); + } + + async getSize() { + return (await this.getNotes({})).reduce((sum, note) => sum + note.getSize(), 0); + } +} diff --git a/yarn-project/pxe/src/storage/sync_data_provider/index.ts b/yarn-project/pxe/src/storage/sync_data_provider/index.ts new file mode 100644 index 000000000000..92419737c0f3 --- /dev/null +++ b/yarn-project/pxe/src/storage/sync_data_provider/index.ts @@ -0,0 +1 @@ +export { SyncDataProvider } from './sync_data_provider.js'; diff --git a/yarn-project/pxe/src/storage/sync_data_provider/sync_data_provider.test.ts b/yarn-project/pxe/src/storage/sync_data_provider/sync_data_provider.test.ts new file mode 100644 index 000000000000..3fd32fd2fcb2 --- /dev/null +++ b/yarn-project/pxe/src/storage/sync_data_provider/sync_data_provider.test.ts @@ -0,0 +1,26 @@ +import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; +import { randomInt } from '@aztec/foundation/crypto'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { makeHeader } from '@aztec/stdlib/testing'; + +import { SyncDataProvider } from './sync_data_provider.js'; + +describe('block header', () => { + let syncDataProvider: SyncDataProvider; + + beforeEach(async () => { + const store = await openTmpStore('sync_data_provider_test'); + syncDataProvider = new SyncDataProvider(store); + }); + + it('stores and retrieves the block header', async () => { + const header = makeHeader(randomInt(1000), INITIAL_L2_BLOCK_NUM, 0 /** slot number */); + + await syncDataProvider.setHeader(header); + await expect(syncDataProvider.getBlockHeader()).resolves.toEqual(header); + }); + + it('rejects getting header if no block set', async () => { + await expect(() => syncDataProvider.getBlockHeader()).rejects.toThrow(); + }); +}); diff --git a/yarn-project/pxe/src/storage/sync_data_provider/sync_data_provider.ts b/yarn-project/pxe/src/storage/sync_data_provider/sync_data_provider.ts new file mode 100644 index 000000000000..32fd14040d09 --- /dev/null +++ b/yarn-project/pxe/src/storage/sync_data_provider/sync_data_provider.ts @@ -0,0 +1,40 @@ +import type { AztecAsyncKVStore, AztecAsyncSingleton } from '@aztec/kv-store'; +import { BlockHeader } from '@aztec/stdlib/tx'; + +import type { DataProvider } from '../data_provider.js'; + +export class SyncDataProvider implements DataProvider { + #store: AztecAsyncKVStore; + #synchronizedHeader: AztecAsyncSingleton; + + constructor(store: AztecAsyncKVStore) { + this.#store = store; + this.#synchronizedHeader = this.#store.openSingleton('header'); + } + + async setHeader(header: BlockHeader): Promise { + await this.#synchronizedHeader.set(header.toBuffer()); + } + + async getBlockNumber(): Promise { + const headerBuffer = await this.#synchronizedHeader.getAsync(); + if (!headerBuffer) { + return undefined; + } + + return Number(BlockHeader.fromBuffer(headerBuffer).globalVariables.blockNumber.toBigInt()); + } + + async getBlockHeader(): Promise { + const headerBuffer = await this.#synchronizedHeader.getAsync(); + if (!headerBuffer) { + throw new Error(`Header not set`); + } + + return BlockHeader.fromBuffer(headerBuffer); + } + + async getSize(): Promise { + return (await this.#synchronizedHeader.getAsync())?.length ?? 0; + } +} diff --git a/yarn-project/pxe/src/storage/tagging_data_provider/index.ts b/yarn-project/pxe/src/storage/tagging_data_provider/index.ts new file mode 100644 index 000000000000..42085f6867aa --- /dev/null +++ b/yarn-project/pxe/src/storage/tagging_data_provider/index.ts @@ -0,0 +1 @@ +export { TaggingDataProvider } from './tagging_data_provider.js'; diff --git a/yarn-project/pxe/src/storage/tagging_data_provider/tagging_data_provider.ts b/yarn-project/pxe/src/storage/tagging_data_provider/tagging_data_provider.ts new file mode 100644 index 000000000000..2d121a284463 --- /dev/null +++ b/yarn-project/pxe/src/storage/tagging_data_provider/tagging_data_provider.ts @@ -0,0 +1,92 @@ +import type { Fr } from '@aztec/foundation/fields'; +import { toArray } from '@aztec/foundation/iterable'; +import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { IndexedTaggingSecret } from '@aztec/stdlib/logs'; + +export class TaggingDataProvider { + #store: AztecAsyncKVStore; + #addressBook: AztecAsyncMap; + + // Stores the last index used for each tagging secret, taking direction into account + // This is necessary to avoid reusing the same index for the same secret, which happens if + // sender and recipient are the same + #taggingSecretIndexesForSenders: AztecAsyncMap; + #taggingSecretIndexesForRecipients: AztecAsyncMap; + + constructor(store: AztecAsyncKVStore) { + this.#store = store; + + this.#addressBook = this.#store.openMap('address_book'); + + this.#taggingSecretIndexesForSenders = this.#store.openMap('tagging_secret_indexes_for_senders'); + this.#taggingSecretIndexesForRecipients = this.#store.openMap('tagging_secret_indexes_for_recipients'); + } + + async setTaggingSecretsIndexesAsSender(indexedSecrets: IndexedTaggingSecret[]): Promise { + await this.#setTaggingSecretsIndexes(indexedSecrets, this.#taggingSecretIndexesForSenders); + } + + async setTaggingSecretsIndexesAsRecipient(indexedSecrets: IndexedTaggingSecret[]): Promise { + await this.#setTaggingSecretsIndexes(indexedSecrets, this.#taggingSecretIndexesForRecipients); + } + + async #setTaggingSecretsIndexes(indexedSecrets: IndexedTaggingSecret[], storageMap: AztecAsyncMap) { + await Promise.all( + indexedSecrets.map(indexedSecret => + storageMap.set(indexedSecret.appTaggingSecret.toString(), indexedSecret.index), + ), + ); + } + + async getTaggingSecretsIndexesAsRecipient(appTaggingSecrets: Fr[]) { + return await this.#getTaggingSecretsIndexes(appTaggingSecrets, this.#taggingSecretIndexesForRecipients); + } + + async getTaggingSecretsIndexesAsSender(appTaggingSecrets: Fr[]) { + return await this.#getTaggingSecretsIndexes(appTaggingSecrets, this.#taggingSecretIndexesForSenders); + } + + #getTaggingSecretsIndexes(appTaggingSecrets: Fr[], storageMap: AztecAsyncMap): Promise { + return Promise.all(appTaggingSecrets.map(async secret => (await storageMap.getAsync(`${secret.toString()}`)) ?? 0)); + } + + resetNoteSyncData(): Promise { + return this.#store.transactionAsync(async () => { + const recipients = await toArray(this.#taggingSecretIndexesForRecipients.keysAsync()); + await Promise.all(recipients.map(recipient => this.#taggingSecretIndexesForRecipients.delete(recipient))); + const senders = await toArray(this.#taggingSecretIndexesForSenders.keysAsync()); + await Promise.all(senders.map(sender => this.#taggingSecretIndexesForSenders.delete(sender))); + }); + } + + async addSenderAddress(address: AztecAddress): Promise { + if (await this.#addressBook.hasAsync(address.toString())) { + return false; + } + + await this.#addressBook.set(address.toString(), true); + + return true; + } + + async getSenderAddresses(): Promise { + return (await toArray(this.#addressBook.keysAsync())).map(AztecAddress.fromString); + } + + async removeSenderAddress(address: AztecAddress): Promise { + if (!(await this.#addressBook.hasAsync(address.toString()))) { + return false; + } + + await this.#addressBook.delete(address.toString()); + + return true; + } + + async getSize() { + const addressesCount = (await toArray(this.#addressBook.keysAsync())).length; + // All keys are addresses + return 3 * addressesCount * AztecAddress.SIZE_IN_BYTES; + } +} diff --git a/yarn-project/pxe/src/synchronizer/synchronizer.test.ts b/yarn-project/pxe/src/synchronizer/synchronizer.test.ts index cbe65671435e..f676fd12d2a1 100644 --- a/yarn-project/pxe/src/synchronizer/synchronizer.test.ts +++ b/yarn-project/pxe/src/synchronizer/synchronizer.test.ts @@ -7,15 +7,17 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; -import type { PxeDatabase } from '../database/index.js'; -import { KVPxeDatabase } from '../database/kv_pxe_database.js'; +import { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js'; +import { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; +import { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; import { Synchronizer } from './synchronizer.js'; describe('Synchronizer', () => { - let database: PxeDatabase; let synchronizer: Synchronizer; - let tipsStore: L2TipsStore; // eslint-disable-line @typescript-eslint/no-unused-vars - + let tipsStore: L2TipsStore; + let syncDataProvider: SyncDataProvider; + let noteDataProvider: NoteDataProvider; + let taggingDataProvider: TaggingDataProvider; let aztecNode: MockProxy; let blockStream: MockProxy; @@ -29,23 +31,31 @@ describe('Synchronizer', () => { const store = await openTmpStore('test'); blockStream = mock(); aztecNode = mock(); - database = await KVPxeDatabase.create(store); tipsStore = new L2TipsStore(store, 'pxe'); - synchronizer = new TestSynchronizer(aztecNode, database, tipsStore); + syncDataProvider = new SyncDataProvider(store); + noteDataProvider = await NoteDataProvider.create(store); + taggingDataProvider = new TaggingDataProvider(store); + synchronizer = new TestSynchronizer(aztecNode, syncDataProvider, noteDataProvider, taggingDataProvider, tipsStore); }); it('sets header from latest block', async () => { const block = await L2Block.random(1, 4); await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); - const obtainedHeader = await database.getBlockHeader(); + const obtainedHeader = await syncDataProvider.getBlockHeader(); expect(obtainedHeader).toEqual(block.header); }); it('removes notes from db on a reorg', async () => { - const removeNotesAfter = jest.spyOn(database, 'removeNotesAfter').mockImplementation(() => Promise.resolve()); - const unnullifyNotesAfter = jest.spyOn(database, 'unnullifyNotesAfter').mockImplementation(() => Promise.resolve()); - const resetNoteSyncData = jest.spyOn(database, 'resetNoteSyncData').mockImplementation(() => Promise.resolve()); + const removeNotesAfter = jest + .spyOn(noteDataProvider, 'removeNotesAfter') + .mockImplementation(() => Promise.resolve()); + const unnullifyNotesAfter = jest + .spyOn(noteDataProvider, 'unnullifyNotesAfter') + .mockImplementation(() => Promise.resolve()); + const resetNoteSyncData = jest + .spyOn(taggingDataProvider, 'resetNoteSyncData') + .mockImplementation(() => Promise.resolve()); aztecNode.getBlockHeader.mockImplementation( async blockNumber => (await L2Block.random(blockNumber as number)).header, ); @@ -57,7 +67,7 @@ describe('Synchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', blockNumber: 3 }); expect(removeNotesAfter).toHaveBeenCalledWith(3); - expect(unnullifyNotesAfter).toHaveBeenCalledWith(3); + expect(unnullifyNotesAfter).toHaveBeenCalledWith(3, 4); expect(resetNoteSyncData).toHaveBeenCalled(); }); }); diff --git a/yarn-project/pxe/src/synchronizer/synchronizer.ts b/yarn-project/pxe/src/synchronizer/synchronizer.ts index ba89d5a4da11..d418feacf1d7 100644 --- a/yarn-project/pxe/src/synchronizer/synchronizer.ts +++ b/yarn-project/pxe/src/synchronizer/synchronizer.ts @@ -5,7 +5,9 @@ import { L2BlockStream, type L2BlockStreamEvent, type L2BlockStreamEventHandler import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { PXEConfig } from '../config/index.js'; -import type { PxeDatabase } from '../database/index.js'; +import type { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js'; +import type { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; +import type { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; /** * The Synchronizer class manages the synchronization with the aztec node, allowing PXE to retrieve the @@ -21,7 +23,9 @@ export class Synchronizer implements L2BlockStreamEventHandler { constructor( private node: AztecNode, - private db: PxeDatabase, + private syncDataProvider: SyncDataProvider, + private noteDataProvider: NoteDataProvider, + private taggingDataProvider: TaggingDataProvider, private l2TipsStore: L2TipsStore, config: Partial> = {}, loggerOrSuffix?: string | Logger, @@ -51,23 +55,24 @@ export class Synchronizer implements L2BlockStreamEventHandler { archive: lastBlock.archive.root.toString(), header: lastBlock.header.toInspect(), }); - await this.db.setHeader(lastBlock.header); + await this.syncDataProvider.setHeader(lastBlock.header); break; } case 'chain-pruned': { this.log.warn(`Pruning data after block ${event.blockNumber} due to reorg`); // We first unnullify and then remove so that unnullified notes that were created after the block number end up deleted. - await this.db.unnullifyNotesAfter(event.blockNumber); - await this.db.removeNotesAfter(event.blockNumber); + const lastSynchedBlockNumber = await this.syncDataProvider.getBlockNumber(); + await this.noteDataProvider.unnullifyNotesAfter(event.blockNumber, lastSynchedBlockNumber); + await this.noteDataProvider.removeNotesAfter(event.blockNumber); // Remove all note tagging indexes to force a full resync. This is suboptimal, but unless we track the // block number in which each index is used it's all we can do. - await this.db.resetNoteSyncData(); + await this.taggingDataProvider.resetNoteSyncData(); // Update the header to the last block. const newHeader = await this.node.getBlockHeader(event.blockNumber); if (!newHeader) { this.log.error(`Block header not found for block number ${event.blockNumber} during chain prune`); } else { - await this.db.setHeader(newHeader); + await this.syncDataProvider.setHeader(newHeader); } break; } @@ -99,18 +104,18 @@ export class Synchronizer implements L2BlockStreamEventHandler { let currentHeader; try { - currentHeader = await this.db.getBlockHeader(); + currentHeader = await this.syncDataProvider.getBlockHeader(); } catch (e) { this.log.debug('Header is not set, requesting from the node'); } if (!currentHeader) { // REFACTOR: We should know the header of the genesis block without having to request it from the node. - await this.db.setHeader((await this.node.getBlockHeader(0))!); + await this.syncDataProvider.setHeader((await this.node.getBlockHeader(0))!); } await this.blockStream.sync(); } public async getSynchedBlockNumber() { - return (await this.db.getBlockNumber()) ?? this.initialSyncBlockNumber; + return (await this.syncDataProvider.getBlockNumber()) ?? this.initialSyncBlockNumber; } } diff --git a/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts b/yarn-project/pxe/src/test/pxe_service.test.ts similarity index 79% rename from yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts rename to yarn-project/pxe/src/test/pxe_service.test.ts index e0e26b7d4917..9cfa935f5ed5 100644 --- a/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts +++ b/yarn-project/pxe/src/test/pxe_service.test.ts @@ -2,9 +2,8 @@ import { BBWASMBundlePrivateKernelProver } from '@aztec/bb-prover/wasm/bundle'; import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; import { EthAddress } from '@aztec/foundation/eth-address'; -import { KeyStore } from '@aztec/key-store'; +import type { AztecAsyncKVStore } from '@aztec/kv-store'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; -import { L2TipsStore } from '@aztec/kv-store/stores'; import type { ProtocolContractsProvider } from '@aztec/protocol-contracts'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { type SimulationProvider, WASMSimulator } from '@aztec/simulator/client'; @@ -15,20 +14,15 @@ import { TxEffect } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; -import type { PxeDatabase } from '../../database/interfaces/pxe_database.js'; -import { KVPxeDatabase } from '../../database/kv_pxe_database.js'; -import type { PXEServiceConfig } from '../../index.js'; -import { PXEService } from '../pxe_service.js'; +import type { PXEServiceConfig } from '../config/index.js'; +import { PXEService } from '../pxe_service/pxe_service.js'; import { pxeTestSuite } from './pxe_test_suite.js'; async function createPXEService(): Promise { const kvStore = await openTmpStore('test'); - const keyStore = new KeyStore(kvStore); const node = mock(); - const db = await KVPxeDatabase.create(kvStore); const simulationProvider = new WASMSimulator(); const kernelProver = new BBWASMBundlePrivateKernelProver(simulationProvider); - const tips = new L2TipsStore(kvStore, 'pxe'); const protocolContractsProvider = new BundledProtocolContractsProvider(); const config: PXEServiceConfig = { l2StartingBlock: INITIAL_L2_BLOCK_NUM, @@ -59,29 +53,22 @@ async function createPXEService(): Promise { }; node.getL1ContractAddresses.mockResolvedValue(mockedContracts); - return Promise.resolve( - new PXEService(keyStore, node, db, tips, kernelProver, simulationProvider, protocolContractsProvider, config), - ); + return await PXEService.create(node, kvStore, kernelProver, simulationProvider, protocolContractsProvider, config); } pxeTestSuite('PXEService', createPXEService); describe('PXEService', () => { - let keyStore: KeyStore; + let kvStore: AztecAsyncKVStore; let node: MockProxy; - let db: PxeDatabase; let simulationProvider: SimulationProvider; let kernelProver: PrivateKernelProver; let config: PXEServiceConfig; - let tips: L2TipsStore; let protocolContractsProvider: ProtocolContractsProvider; beforeEach(async () => { - const kvStore = await openTmpStore('test'); - keyStore = new KeyStore(kvStore); + kvStore = await openTmpStore('test'); node = mock(); - tips = new L2TipsStore(kvStore, 'pxe'); - db = await KVPxeDatabase.create(kvStore); simulationProvider = new WASMSimulator(); kernelProver = new BBWASMBundlePrivateKernelProver(simulationProvider); protocolContractsProvider = new BundledProtocolContractsProvider(); @@ -103,11 +90,9 @@ describe('PXEService', () => { node.getTxEffect.mockResolvedValue(randomInBlock(settledTx)); - const pxe = new PXEService( - keyStore, + const pxe = await PXEService.create( node, - db, - tips, + kvStore, kernelProver, simulationProvider, protocolContractsProvider, diff --git a/yarn-project/pxe/src/pxe_service/test/pxe_test_suite.ts b/yarn-project/pxe/src/test/pxe_test_suite.ts similarity index 91% rename from yarn-project/pxe/src/pxe_service/test/pxe_test_suite.ts rename to yarn-project/pxe/src/test/pxe_test_suite.ts index 6764b008a2f6..c1559dc13bb8 100644 --- a/yarn-project/pxe/src/pxe_service/test/pxe_test_suite.ts +++ b/yarn-project/pxe/src/test/pxe_test_suite.ts @@ -81,7 +81,7 @@ export const pxeTestSuite = (testName: string, pxeSetup: () => Promise) => it('refuses to register a contract with a class that has not been registered', async () => { const instance = await randomContractInstanceWithAddress(); - await expect(pxe.registerContract({ instance })).rejects.toThrow(/Missing contract artifact/i); + await expect(pxe.registerContract({ instance })).rejects.toThrow(/DB has no contract class with id/i); }); it('refuses to register a contract with an artifact with mismatching class id', async () => { @@ -93,14 +93,7 @@ export const pxeTestSuite = (testName: string, pxeSetup: () => Promise) => // Note: Not testing a successful run of `proveTx`, `sendTx`, `getTxReceipt` and `simulateUnconstrained` here as it requires // a larger setup and it's sufficiently tested in the e2e tests. - it('throws when getting public storage for non-existent contract', async () => { - const contract = await AztecAddress.random(); - await expect(async () => await pxe.getPublicStorageAt(contract, new Fr(0n))).rejects.toThrow( - `Contract ${contract.toString()} is not deployed`, - ); - }); - - // Note: Not testing `getContractData` and `getPublicLogs` here as these + // Note: Not testing `getContractData`, `getPublicLogs` and `getPublicStorageAt` here as these // functions only call AztecNode and these methods are frequently used by the e2e tests. it('successfully gets a block number', async () => { diff --git a/yarn-project/simulator/src/private/acvm/oracle/oracle.ts b/yarn-project/simulator/src/private/acvm/oracle/oracle.ts index c0305d30569e..c0792338d1e8 100644 --- a/yarn-project/simulator/src/private/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/private/acvm/oracle/oracle.ts @@ -138,7 +138,7 @@ export class Oracle { const messageHashField = fromACVMField(messageHash); const witness = await this.typedOracle.getAuthWitness(messageHashField); if (!witness) { - throw new Error(`Authorization not found for message hash ${messageHashField}`); + throw new Error(`Unknown auth witness for message hash ${messageHashField}`); } return witness.map(toACVMField); } diff --git a/yarn-project/simulator/src/private/execution_data_provider.ts b/yarn-project/simulator/src/private/execution_data_provider.ts index d2913e529c11..5dff5778d87f 100644 --- a/yarn-project/simulator/src/private/execution_data_provider.ts +++ b/yarn-project/simulator/src/private/execution_data_provider.ts @@ -54,7 +54,7 @@ export interface ExecutionDataProvider extends CommitmentsDB { * @param messageHash - The message hash. * @returns A Promise that resolves to an array of field elements representing the auth witness. */ - getAuthWitness(messageHash: Fr): Promise; + getAuthWitness(messageHash: Fr): Promise; /** * Retrieve keys associated with a specific master public key and app address. diff --git a/yarn-project/simulator/src/private/private_execution.ts b/yarn-project/simulator/src/private/private_execution.ts index 51bbfa26f5cf..006733051924 100644 --- a/yarn-project/simulator/src/private/private_execution.ts +++ b/yarn-project/simulator/src/private/private_execution.ts @@ -6,6 +6,7 @@ import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { type FunctionArtifact, type FunctionSelector, countArgumentsSize } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractInstance } from '@aztec/stdlib/contract'; +import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { PrivateCircuitPublicInputs } from '@aztec/stdlib/kernel'; import { SharedMutableValues, SharedMutableValuesWithHash } from '@aztec/stdlib/shared-mutable'; import type { CircuitWitnessGenerationStats } from '@aztec/stdlib/stats'; @@ -120,7 +121,7 @@ export function extractPrivateCircuitPublicInputs( export async function readCurrentClassId( contractAddress: AztecAddress, instance: ContractInstance, - executionDataProvider: ExecutionDataProvider, + executionDataProvider: ExecutionDataProvider | AztecNode, blockNumber: number, ) { const { sharedMutableSlot } = await SharedMutableValuesWithHash.getContractUpdateSlots(contractAddress); diff --git a/yarn-project/stdlib/src/database-version/README.md b/yarn-project/stdlib/src/database-version/README.md index 7fcb94cff82c..ea2b739377c7 100644 --- a/yarn-project/stdlib/src/database-version/README.md +++ b/yarn-project/stdlib/src/database-version/README.md @@ -12,8 +12,8 @@ The Version Manager helps manage database migrations and version compatibility a ## Usage ```typescript -import { EthAddress } from '@aztec/foundation/eth-address'; import { version } from '@aztec/foundation'; +import { EthAddress } from '@aztec/foundation/eth-address'; // Define your current database version const DB_VERSION = 3; @@ -44,7 +44,7 @@ await versionManager.checkVersionAndHandle( // Unsupported migration path, will fall back to reset throw new Error(`Cannot upgrade from ${oldVersion} to ${newVersion}`); } - } + }, ); // Get the data directory for your service @@ -60,4 +60,4 @@ The database will be reset in the following conditions: 3. Version has changed and no upgrade callback is provided 4. Upgrade callback throws an error -When a reset occurs, the data directory is deleted and recreated, and the reset callback is called to initialize a fresh database. \ No newline at end of file +When a reset occurs, the data directory is deleted and recreated, and the reset callback is called to initialize a fresh database. diff --git a/yarn-project/txe/src/index.ts b/yarn-project/txe/src/index.ts index 6af9cadb934e..23918460c91c 100644 --- a/yarn-project/txe/src/index.ts +++ b/yarn-project/txe/src/index.ts @@ -11,6 +11,8 @@ import { } from '@aztec/aztec.js'; import { createSafeJsonRpcServer } from '@aztec/foundation/json-rpc/server'; import type { Logger } from '@aztec/foundation/log'; +import { type ProtocolContract, protocolContractNames } from '@aztec/protocol-contracts'; +import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import type { ApiSchemaFor, ZodFor } from '@aztec/stdlib/schemas'; import { readFile, readdir } from 'fs/promises'; @@ -59,6 +61,8 @@ const TXEForeignCallInputSchema = z.object({ }) satisfies ZodFor; class TXEDispatcher { + private protocolContracts!: ProtocolContract[]; + constructor(private logger: Logger) {} async #processDeployInputs({ inputs, root_path: rootPath, package_name: packageName }: TXEForeignCallInput) { @@ -162,7 +166,12 @@ class TXEDispatcher { if (!TXESessions.has(sessionId) && functionName != 'reset') { this.logger.debug(`Creating new session ${sessionId}`); - TXESessions.set(sessionId, await TXEService.init(this.logger)); + if (!this.protocolContracts) { + this.protocolContracts = await Promise.all( + protocolContractNames.map(name => new BundledProtocolContractsProvider().getProtocolContractArtifact(name)), + ); + } + TXESessions.set(sessionId, await TXEService.init(this.logger, this.protocolContracts)); } switch (functionName) { diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 34839d032a08..2b9ff4973a74 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -11,20 +11,31 @@ import { PUBLIC_DISPATCH_SELECTOR, } from '@aztec/constants'; import { padArrayEnd } from '@aztec/foundation/collection'; -import { Schnorr, poseidon2Hash } from '@aztec/foundation/crypto'; +import { Aes128, Schnorr, poseidon2Hash } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; -import { type LogFn, type Logger, applyStringFormatting, createDebugOnlyLogger } from '@aztec/foundation/log'; +import { type Logger, applyStringFormatting } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; -import type { KeyStore } from '@aztec/key-store'; -import { ContractDataProvider, PXEDataProvider, enrichPublicSimulationError } from '@aztec/pxe'; +import { KeyStore } from '@aztec/key-store'; +import type { AztecAsyncKVStore } from '@aztec/kv-store'; +import type { ProtocolContract } from '@aztec/protocol-contracts'; +import { + AddressDataProvider, + AuthWitnessDataProvider, + CapsuleDataProvider, + ContractDataProvider, + NoteDataProvider, + PXEOracleInterface, + SyncDataProvider, + TaggingDataProvider, + enrichPublicSimulationError, +} from '@aztec/pxe/server'; import { ExecutionNoteCache, - type HashedValuesCache, + HashedValuesCache, type MessageLoadOracleInputs, type NoteData, Oracle, type TypedOracle, - UnconstrainedExecutionOracle, WASMSimulator, extractCallStack, extractPrivateCircuitPublicInputs, @@ -62,7 +73,7 @@ import { } from '@aztec/stdlib/hash'; import type { MerkleTreeReadOperations, MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; import { type KeyValidationRequest, PrivateContextInputs } from '@aztec/stdlib/kernel'; -import { computeTaggingSecretPoint, deriveKeys } from '@aztec/stdlib/keys'; +import { deriveKeys } from '@aztec/stdlib/keys'; import { ContractClassLog, LogWithTxData } from '@aztec/stdlib/logs'; import { IndexedTaggingSecret, type PrivateLog, type PublicLog } from '@aztec/stdlib/logs'; import type { NoteStatus } from '@aztec/stdlib/note'; @@ -83,10 +94,10 @@ import { PublicDataWitness, } from '@aztec/stdlib/trees'; import { BlockHeader, CallContext, GlobalVariables, PublicExecutionRequest, TxEffect, TxHash } from '@aztec/stdlib/tx'; -import { ForkCheckpoint, type NativeWorldStateService } from '@aztec/world-state/native'; +import { ForkCheckpoint, NativeWorldStateService } from '@aztec/world-state/native'; import { TXENode } from '../node/txe_node.js'; -import type { TXEDatabase } from '../util/txe_database.js'; +import { TXEAccountDataProvider } from '../util/txe_account_data_provider.js'; import { TXEPublicContractDataSource } from '../util/txe_public_contract_data_source.js'; import { TXEWorldStateDB } from '../util/txe_world_state_db.js'; @@ -100,9 +111,7 @@ export class TXE implements TypedOracle { private nestedCallReturndata: Fr[] = []; private nestedCallSuccess: boolean = false; - private contractDataProvider: ContractDataProvider; - private pxeDataProvider: PXEDataProvider; - private viewDataOracle: UnconstrainedExecutionOracle; + private pxeOracleInterface: PXEOracleInterface; private publicDataWrites: PublicDataWrite[] = []; private uniqueNoteHashesFromPublic: Fr[] = []; @@ -121,57 +130,77 @@ export class TXE implements TypedOracle { private noteCache: ExecutionNoteCache; - debug: LogFn; - private constructor( private logger: Logger, - private executionCache: HashedValuesCache, private keyStore: KeyStore, - private txeDatabase: TXEDatabase, + private contractDataProvider: ContractDataProvider, + private noteDataProvider: NoteDataProvider, + private capsuleDataProvider: CapsuleDataProvider, + private syncDataProvider: SyncDataProvider, + private taggingDataProvider: TaggingDataProvider, + private addressDataProvider: AddressDataProvider, + private authWitnessDataProvider: AuthWitnessDataProvider, + private accountDataProvider: TXEAccountDataProvider, + private executionCache: HashedValuesCache, private contractAddress: AztecAddress, private nativeWorldStateService: NativeWorldStateService, private baseFork: MerkleTreeWriteOperations, ) { this.noteCache = new ExecutionNoteCache(this.getTxRequestHash()); - this.contractDataProvider = new ContractDataProvider(txeDatabase); this.node = new TXENode(this.blockNumber, this.VERSION, this.CHAIN_ID, nativeWorldStateService, baseFork); - // Default msg_sender (for entrypoints) is now Fr.max_value rather than 0 addr (see #7190 & #7404) this.msgSender = AztecAddress.fromField(Fr.MAX_FIELD_VALUE); - this.pxeDataProvider = new PXEDataProvider( - txeDatabase, - keyStore, + + this.pxeOracleInterface = new PXEOracleInterface( this.node, + this.keyStore, this.simulationProvider, this.contractDataProvider, + this.noteDataProvider, + this.capsuleDataProvider, + this.syncDataProvider, + this.taggingDataProvider, + this.addressDataProvider, + this.authWitnessDataProvider, + this.logger, ); + } - this.viewDataOracle = new UnconstrainedExecutionOracle( - this.contractAddress, - [] /* authWitnesses */, - [] /* capsules */, - this.pxeDataProvider, // note: PXEDataProvider implements ExecutionDataProvider - /* log, */ - /* scopes, */ - ); + static async create(logger: Logger, store: AztecAsyncKVStore, protocolContracts: ProtocolContract[]) { + const executionCache = new HashedValuesCache(); + const nativeWorldStateService = await NativeWorldStateService.tmp(); + const baseFork = await nativeWorldStateService.fork(); - this.debug = createDebugOnlyLogger('aztec:kv-pxe-database'); - } + const addressDataProvider = new AddressDataProvider(store); + const authWitnessDataProvider = new AuthWitnessDataProvider(store); + const contractDataProvider = new ContractDataProvider(store); + const noteDataProvider = await NoteDataProvider.create(store); + const syncDataProvider = new SyncDataProvider(store); + const taggingDataProvider = new TaggingDataProvider(store); + const capsuleDataProvider = new CapsuleDataProvider(store); + const keyStore = new KeyStore(store); + + const accountDataProvider = new TXEAccountDataProvider(store); + + // Register protocol contracts. + for (const { contractClass, instance, artifact } of protocolContracts) { + await contractDataProvider.addContractArtifact(contractClass.id, artifact); + await contractDataProvider.addContractInstance(instance); + } - static async create( - logger: Logger, - executionCache: HashedValuesCache, - keyStore: KeyStore, - txeDatabase: TXEDatabase, - nativeWorldStateService: NativeWorldStateService, - baseFork: MerkleTreeWriteOperations, - ) { return new TXE( logger, - executionCache, keyStore, - txeDatabase, + contractDataProvider, + noteDataProvider, + capsuleDataProvider, + syncDataProvider, + taggingDataProvider, + addressDataProvider, + authWitnessDataProvider, + accountDataProvider, + executionCache, await AztecAddress.random(), nativeWorldStateService, baseFork, @@ -233,20 +262,24 @@ export class TXE implements TypedOracle { return this.contractDataProvider; } - getTXEDatabase() { - return this.txeDatabase; - } - getKeyStore() { return this.keyStore; } + getAccountDataProvider() { + return this.accountDataProvider; + } + + getAddressDataProvider() { + return this.addressDataProvider; + } + async addContractInstance(contractInstance: ContractInstanceWithAddress) { - await this.txeDatabase.addContractInstance(contractInstance); + await this.contractDataProvider.addContractInstance(contractInstance); } async addContractArtifact(contractClassId: Fr, artifact: ContractArtifact) { - await this.txeDatabase.addContractArtifact(contractClassId, artifact); + await this.contractDataProvider.addContractArtifact(contractClassId, artifact); } async getPrivateContextInputs( @@ -287,12 +320,12 @@ export class TXE implements TypedOracle { } async addAuthWitness(address: AztecAddress, messageHash: Fr) { - const account = await this.txeDatabase.getAccount(address); + const account = await this.accountDataProvider.getAccount(address); const privateKey = await this.keyStore.getMasterSecretKey(account.publicKeys.masterIncomingViewingPublicKey); const schnorr = new Schnorr(); const signature = await schnorr.constructSignature(messageHash.toBuffer(), privateKey); const authWitness = new AuthWitness(messageHash, [...signature.toBuffer()]); - return this.txeDatabase.addAuthWitness(authWitness.requestHash, authWitness.witness); + return this.authWitnessDataProvider.addAuthWitness(authWitness.requestHash, authWitness.witness); } async addPublicDataWrites(writes: PublicDataWrite[]) { @@ -501,11 +534,11 @@ export class TXE implements TypedOracle { } getCompleteAddress(account: AztecAddress) { - return Promise.resolve(this.txeDatabase.getAccount(account)); + return Promise.resolve(this.accountDataProvider.getAccount(account)); } getAuthWitness(messageHash: Fr) { - return this.txeDatabase.getAuthWitness(messageHash); + return this.pxeOracleInterface.getAuthWitness(messageHash); } async getNotes( @@ -528,7 +561,7 @@ export class TXE implements TypedOracle { const pendingNotes = this.noteCache.getNotes(this.contractAddress, storageSlot); const pendingNullifiers = this.noteCache.getNullifiers(this.contractAddress); - const dbNotes = await this.pxeDataProvider.getNotes(this.contractAddress, storageSlot, status); + const dbNotes = await this.pxeOracleInterface.getNotes(this.contractAddress, storageSlot, status); const dbNotesFiltered = dbNotes.filter(n => !pendingNullifiers.has((n.siloedNullifier as Fr).value)); const notes = pickNotes([...dbNotesFiltered, ...pendingNotes], { @@ -986,12 +1019,7 @@ export class TXE implements TypedOracle { // Poor man's revert handling if (!executionResult.revertCode.isOK()) { if (executionResult.revertReason && executionResult.revertReason instanceof SimulationError) { - await enrichPublicSimulationError( - executionResult.revertReason, - this.contractDataProvider, - this.txeDatabase, - this.logger, - ); + await enrichPublicSimulationError(executionResult.revertReason, this.contractDataProvider, this.logger); throw new Error(executionResult.revertReason.message); } else { throw new Error(`Enqueued public function call reverted: ${executionResult.revertReason}`); @@ -1043,38 +1071,25 @@ export class TXE implements TypedOracle { } async incrementAppTaggingSecretIndexAsSender(sender: AztecAddress, recipient: AztecAddress): Promise { - const appSecret = await this.#calculateAppTaggingSecret(this.contractAddress, sender, recipient); - const [index] = await this.txeDatabase.getTaggingSecretsIndexesAsSender([appSecret]); - await this.txeDatabase.setTaggingSecretsIndexesAsSender([new IndexedTaggingSecret(appSecret, index + 1)]); + await this.pxeOracleInterface.incrementAppTaggingSecretIndexAsSender(this.contractAddress, sender, recipient); } async getIndexedTaggingSecretAsSender(sender: AztecAddress, recipient: AztecAddress): Promise { - const secret = await this.#calculateAppTaggingSecret(this.contractAddress, sender, recipient); - const [index] = await this.txeDatabase.getTaggingSecretsIndexesAsSender([secret]); - return new IndexedTaggingSecret(secret, index); - } - - async #calculateAppTaggingSecret(contractAddress: AztecAddress, sender: AztecAddress, recipient: AztecAddress) { - const senderCompleteAddress = await this.getCompleteAddress(sender); - const senderIvsk = await this.keyStore.getMasterIncomingViewingSecretKey(sender); - const secretPoint = await computeTaggingSecretPoint(senderCompleteAddress, senderIvsk, recipient); - // Silo the secret to the app so it can't be used to track other app's notes - const appSecret = poseidon2Hash([secretPoint.x, secretPoint.y, contractAddress]); - return appSecret; + return await this.pxeOracleInterface.getIndexedTaggingSecretAsSender(this.contractAddress, sender, recipient); } async syncNotes() { - const taggedLogsByRecipient = await this.pxeDataProvider.syncTaggedLogs( + const taggedLogsByRecipient = await this.pxeOracleInterface.syncTaggedLogs( this.contractAddress, await this.getBlockNumber(), undefined, ); for (const [recipient, taggedLogs] of taggedLogsByRecipient.entries()) { - await this.pxeDataProvider.processTaggedLogs(taggedLogs, AztecAddress.fromString(recipient)); + await this.pxeOracleInterface.processTaggedLogs(taggedLogs, AztecAddress.fromString(recipient)); } - await this.pxeDataProvider.removeNullifiedNotes(this.contractAddress); + await this.pxeOracleInterface.removeNullifiedNotes(this.contractAddress); return Promise.resolve(); } @@ -1093,7 +1108,7 @@ export class TXE implements TypedOracle { } async getLogByTag(tag: Fr): Promise { - return await this.pxeDataProvider.getLogByTag(tag); + return await this.pxeOracleInterface.getLogByTag(tag); } // AVM oracles @@ -1192,7 +1207,7 @@ export class TXE implements TypedOracle { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.txeDatabase.storeCapsule(this.contractAddress, slot, capsule); + return this.pxeOracleInterface.storeCapsule(this.contractAddress, slot, capsule); } loadCapsule(contractAddress: AztecAddress, slot: Fr): Promise { @@ -1200,7 +1215,7 @@ export class TXE implements TypedOracle { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.txeDatabase.loadCapsule(this.contractAddress, slot); + return this.pxeOracleInterface.loadCapsule(this.contractAddress, slot); } deleteCapsule(contractAddress: AztecAddress, slot: Fr): Promise { @@ -1208,7 +1223,7 @@ export class TXE implements TypedOracle { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.txeDatabase.deleteCapsule(this.contractAddress, slot); + return this.pxeOracleInterface.deleteCapsule(this.contractAddress, slot); } copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { @@ -1216,10 +1231,11 @@ export class TXE implements TypedOracle { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.txeDatabase.copyCapsule(this.contractAddress, srcSlot, dstSlot, numEntries); + return this.pxeOracleInterface.copyCapsule(this.contractAddress, srcSlot, dstSlot, numEntries); } aes128Decrypt(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise { - return this.viewDataOracle.aes128Decrypt(ciphertext, iv, symKey); + const aes128 = new Aes128(); + return aes128.decryptBufferCBC(ciphertext, iv, symKey); } } diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 6c5b09606abe..ee2831fa21c6 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -1,12 +1,10 @@ import { type ContractInstanceWithAddress, Fr } from '@aztec/aztec.js'; import { DEPLOYER_CONTRACT_ADDRESS } from '@aztec/constants'; import type { Logger } from '@aztec/foundation/log'; -import { KeyStore } from '@aztec/key-store'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; -import { protocolContractNames } from '@aztec/protocol-contracts'; -import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; -import { enrichPublicSimulationError } from '@aztec/pxe'; -import { HashedValuesCache, type TypedOracle } from '@aztec/simulator/client'; +import type { ProtocolContract } from '@aztec/protocol-contracts'; +import { enrichPublicSimulationError } from '@aztec/pxe/server'; +import type { TypedOracle } from '@aztec/simulator/client'; import { type ContractArtifact, FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import { PublicDataWrite } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -14,7 +12,6 @@ import { computePartialAddress } from '@aztec/stdlib/contract'; import { SimulationError } from '@aztec/stdlib/errors'; import { computePublicDataTreeLeafSlot, siloNullifier } from '@aztec/stdlib/hash'; import { MerkleTreeId } from '@aztec/stdlib/trees'; -import { NativeWorldStateService } from '@aztec/world-state'; import { TXE } from '../oracle/txe_oracle.js'; import { @@ -31,28 +28,14 @@ import { toSingle, } from '../util/encoding.js'; import { ExpectedFailureError } from '../util/expected_failure_error.js'; -import { TXEDatabase } from '../util/txe_database.js'; export class TXEService { constructor(private logger: Logger, private typedOracle: TypedOracle) {} - static async init(logger: Logger) { - const store = await openTmpStore('test'); - const executionCache = new HashedValuesCache(); - const nativeWorldStateService = await NativeWorldStateService.tmp(); - const baseFork = await nativeWorldStateService.fork(); - - const keyStore = new KeyStore(store); - const txeDatabase = new TXEDatabase(store); - // Register protocol contracts. - const provider = new BundledProtocolContractsProvider(); - for (const name of protocolContractNames) { - const { contractClass, instance, artifact } = await provider.getProtocolContractArtifact(name); - await txeDatabase.addContractArtifact(contractClass.id, artifact); - await txeDatabase.addContractInstance(instance); - } + static async init(logger: Logger, protocolContracts: ProtocolContract[]) { logger.debug(`TXE service initialized`); - const txe = await TXE.create(logger, executionCache, keyStore, txeDatabase, nativeWorldStateService, baseFork); + const store = await openTmpStore('test'); + const txe = await TXE.create(logger, store, protocolContracts); const service = new TXEService(logger, txe); await service.advanceBlocksBy(toSingle(new Fr(1n))); return service; @@ -140,8 +123,10 @@ export class TXEService { const secretFr = fromSingle(secret); // This is a footgun ! const completeAddress = await keyStore.addAccount(secretFr, secretFr); - const accountStore = (this.typedOracle as TXE).getTXEDatabase(); - await accountStore.setAccount(completeAddress.address, completeAddress); + const accountDataProvider = (this.typedOracle as TXE).getAccountDataProvider(); + await accountDataProvider.setAccount(completeAddress.address, completeAddress); + const addressDataProvider = (this.typedOracle as TXE).getAddressDataProvider(); + await addressDataProvider.addCompleteAddress(completeAddress); this.logger.debug(`Created account ${completeAddress.address}`); return toForeignCallResult([ toSingle(completeAddress.address), @@ -156,8 +141,10 @@ export class TXEService { const keyStore = (this.typedOracle as TXE).getKeyStore(); const completeAddress = await keyStore.addAccount(fromSingle(secret), await computePartialAddress(instance)); - const accountStore = (this.typedOracle as TXE).getTXEDatabase(); - await accountStore.setAccount(completeAddress.address, completeAddress); + const accountDataProvider = (this.typedOracle as TXE).getAccountDataProvider(); + await accountDataProvider.setAccount(completeAddress.address, completeAddress); + const addressDataProvider = (this.typedOracle as TXE).getAddressDataProvider(); + await addressDataProvider.addCompleteAddress(completeAddress); this.logger.debug(`Created account ${completeAddress.address}`); return toForeignCallResult([ toSingle(completeAddress.address), @@ -715,7 +702,6 @@ export class TXEService { await enrichPublicSimulationError( result.revertReason, (this.typedOracle as TXE).getContractDataProvider(), - (this.typedOracle as TXE).getTXEDatabase(), this.logger, ); throw new Error(result.revertReason.message); @@ -745,7 +731,6 @@ export class TXEService { await enrichPublicSimulationError( result.revertReason, (this.typedOracle as TXE).getContractDataProvider(), - (this.typedOracle as TXE).getTXEDatabase(), this.logger, ); throw new Error(result.revertReason.message); diff --git a/yarn-project/txe/src/util/txe_database.ts b/yarn-project/txe/src/util/txe_account_data_provider.ts similarity index 73% rename from yarn-project/txe/src/util/txe_database.ts rename to yarn-project/txe/src/util/txe_account_data_provider.ts index b4fcf8eb66bc..feeb2bb723c8 100644 --- a/yarn-project/txe/src/util/txe_database.ts +++ b/yarn-project/txe/src/util/txe_account_data_provider.ts @@ -1,14 +1,12 @@ import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; -import { KVPxeDatabase } from '@aztec/pxe'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { CompleteAddress } from '@aztec/stdlib/contract'; -export class TXEDatabase extends KVPxeDatabase { +export class TXEAccountDataProvider { #accounts: AztecAsyncMap; - constructor(db: AztecAsyncKVStore) { - super(db); - this.#accounts = db.openMap('accounts'); + constructor(store: AztecAsyncKVStore) { + this.#accounts = store.openMap('accounts'); } async getAccount(key: AztecAddress) { @@ -21,6 +19,5 @@ export class TXEDatabase extends KVPxeDatabase { async setAccount(key: AztecAddress, value: CompleteAddress) { await this.#accounts.set(key.toString(), value.toBuffer()); - await this.addCompleteAddress(value); } } diff --git a/yarn-project/txe/src/util/txe_public_contract_data_source.ts b/yarn-project/txe/src/util/txe_public_contract_data_source.ts index 6014dd3b3faf..6446a8438624 100644 --- a/yarn-project/txe/src/util/txe_public_contract_data_source.ts +++ b/yarn-project/txe/src/util/txe_public_contract_data_source.ts @@ -1,6 +1,6 @@ import { PUBLIC_DISPATCH_SELECTOR } from '@aztec/constants'; import { Fr } from '@aztec/foundation/fields'; -import { PrivateFunctionsTree } from '@aztec/pxe'; +import { PrivateFunctionsTree } from '@aztec/pxe/server'; import { type ContractArtifact, FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import {