diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 0ff37ad3c221..81090922254b 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,11 +9,20 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD -### [PXE] Local PXE database is reset on upgrade +### [PXE] Stores are now selected by `(l1ChainId, rollupAddress, schemaVersion)` instead of being wiped on mismatch -The persisted tagging stores now key every entry by the self-describing `::` form of `AppTaggingSecret`; unconstrained secrets previously used a two-part `:` key. This bumps the PXE data schema version, and there is no forward migration for the old keys: on first open the PXE clears any database whose stored schema version differs from the current one. The wipe resets the entire PXE store, not just the tagging data, because all of it shares one backing database. +Previously, connecting a PXE or embedded wallet to a different or redeployed rollup, or bumping the store schema version, wiped the existing on-disk store in place. That meant master account keys could be destroyed simply by pointing a wallet at a different network. PXE data stores now exist per `(l1ChainId, rollupAddress, schemaVersion)` triple, and switching networks (or upgrading) selects or creates the matching store instead of overwriting previous ones. The embedded wallet's `wallet_data` store is partitioned the same way, so accounts and aliases are per network: switching networks starts with an empty account list until accounts are re-imported, and switching back finds the originals intact. -**Impact**: On upgrade your local PXE state is reset. You must re-register accounts and re-sync from genesis. Wallets should surface a "your local state was reset, please re-register accounts and re-sync" path. +**Impact**: The first start after upgrading to this version begins with a fresh, empty store; the pre-upgrade data is not deleted. On Node.js environments (lmdb-v2) pre-upgrade data stays at `/` while new per-identity `pxe_data` stores live under `/-stores/`. The embedded Node.js wallet previously stored data in cwd-relative, rollup-address-suffixed directories instead (`pxe_data_/pxe_data` for the PXE store, `wallet_data_/wallet_data` for the wallet store): if you used it before this release, that is where the old data lives. The embedded wallet now defaults its data root to `aztec-wallet-data/`, with per-identity wallet stores under `/wallet_data-stores/` on Node.js and OPFS store names prefixed `wallet_data_` in the browser. Browser apps can enumerate and clean up `pxe_data` and `wallet_data` stores for networks no longer in use with the new `listStores()` / `deleteStore()` utilities: + +```ts +import { deleteStore, listStores } from '@aztec/kv-store/sqlite-opfs'; + +const names = await listStores(); +await deleteStore(names[0]); +``` + +This change also removes `createStore` from `@aztec/kv-store/sqlite-opfs` and `@aztec/kv-store/deprecated/indexeddb`: stores are now opened by name with `AztecSQLiteOPFSStore.open` / `AztecIndexedDBStore.open`. ### [Aztec.nr] `TestEnvironmentOptions::with_tagging_secret_strategy` replaced diff --git a/playground/src/components/navbar/components/NetworkSelector.tsx b/playground/src/components/navbar/components/NetworkSelector.tsx index 05854ca06611..0d707ef3d4ba 100644 --- a/playground/src/components/navbar/components/NetworkSelector.tsx +++ b/playground/src/components/navbar/components/NetworkSelector.tsx @@ -7,7 +7,7 @@ import AddIcon from '@mui/icons-material/Add'; import { AddNetworksDialog } from './AddNetworkDialog'; import CircularProgress from '@mui/material/CircularProgress'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import { createStore } from '@aztec/kv-store/sqlite-opfs'; +import { AztecSQLiteOPFSStore, storePoolDirectory } from '@aztec/kv-store/sqlite-opfs'; import { AztecContext } from '../../../aztecContext'; import { navbarButtonStyle, navbarSelect } from '../../../styles/common'; import { NETWORKS } from '../../../utils/networks'; @@ -50,10 +50,12 @@ export function NetworkSelector() { } setIsContextInitialized(true); WebLogger.create(setLogs, setTotalLogCount); - const store = await createStore('playground_data', { - dataDirectory: 'playground', - dataStoreMapSizeKb: 1e6, - }); + const store = await AztecSQLiteOPFSStore.open( + WebLogger.getInstance().createLogger('playground_data'), + 'playground_data', + false, + storePoolDirectory('playground_data'), + ); const playgroundDB = PlaygroundDB.getInstance(); playgroundDB.init(store, WebLogger.getInstance().createLogger('playground_db').info); setPlaygroundDB(PlaygroundDB.getInstance()); diff --git a/yarn-project/kv-store/src/deprecated/indexeddb/index.ts b/yarn-project/kv-store/src/deprecated/indexeddb/index.ts index 7ce637b8ab0c..e2ab6b6d5ad3 100644 --- a/yarn-project/kv-store/src/deprecated/indexeddb/index.ts +++ b/yarn-project/kv-store/src/deprecated/indexeddb/index.ts @@ -1,34 +1,9 @@ -import { type Logger, createLogger } from '@aztec/foundation/log'; -import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; +import { createLogger } from '@aztec/foundation/log'; -import { initStoreForRollupAndSchemaVersion } from '../../utils.js'; import { AztecIndexedDBStore } from './store.js'; export { AztecIndexedDBStore } from './store.js'; -/** - * @deprecated The IndexedDB backend is being retired. Use `@aztec/kv-store/sqlite-opfs` instead. - */ -export async function createStore( - name: string, - config: DataStoreConfig, - schemaVersion: number | undefined = undefined, - log: Logger = createLogger('kv-store'), -) { - let { dataDirectory } = config; - if (typeof dataDirectory !== 'undefined') { - dataDirectory = `${dataDirectory}/${name}`; - } - - log.info( - dataDirectory - ? `Creating ${name} data store at directory ${dataDirectory} with map size ${config.dataStoreMapSizeKb} KB` - : `Creating ${name} ephemeral data store with map size ${config.dataStoreMapSizeKb} KB`, - ); - const store = await AztecIndexedDBStore.open(createLogger('kv-store:indexeddb'), dataDirectory ?? '', false); - return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log); -} - /** * @deprecated The IndexedDB backend is being retired. Use `@aztec/kv-store/sqlite-opfs` instead. */ diff --git a/yarn-project/kv-store/src/sqlite-opfs/index.ts b/yarn-project/kv-store/src/sqlite-opfs/index.ts index 9599a1532144..65074aa2f865 100644 --- a/yarn-project/kv-store/src/sqlite-opfs/index.ts +++ b/yarn-project/kv-store/src/sqlite-opfs/index.ts @@ -1,28 +1,11 @@ -import { type Logger, createLogger } from '@aztec/foundation/log'; -import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; +import { createLogger } from '@aztec/foundation/log'; -import { initStoreForRollupAndSchemaVersion } from '../utils.js'; import { AztecSQLiteOPFSStore } from './store.js'; export { AztecSQLiteOPFSStore } from './store.js'; export { SqliteEncryptionError } from './errors.js'; export type { SqliteEncryptionErrorCode } from './errors.js'; - -export async function createStore( - name: string, - config: DataStoreConfig, - schemaVersion: number | undefined = undefined, - log: Logger = createLogger('kv-store'), -) { - const { dataDirectory } = config; - log.info( - dataDirectory - ? `Creating ${name} SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB` - : `Creating ${name} ephemeral SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`, - ); - const store = await AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), name, false); - return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log); -} +export { OPFS_POOL_DIR_PREFIX, deleteStore, listStores, storePoolDirectory } from './manage.js'; export function openTmpStore(ephemeral: boolean = false): Promise { return AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), undefined, ephemeral); diff --git a/yarn-project/kv-store/src/sqlite-opfs/manage.ts b/yarn-project/kv-store/src/sqlite-opfs/manage.ts new file mode 100644 index 000000000000..2f2f24fe02e3 --- /dev/null +++ b/yarn-project/kv-store/src/sqlite-opfs/manage.ts @@ -0,0 +1,36 @@ +/** Prefix for the per-store OPFS SAH pool directories owned by this package. */ +export const OPFS_POOL_DIR_PREFIX = '.aztec-kv-'; + +/** + * OPFS directory holding a store's SAH pool. One directory per store: the SAH-pool VFS allows only one + * concurrent instance per directory and its capacity does not grow automatically, so sharing a pool across + * stores would make concurrently opened stores contend for locks and orphaned stores exhaust pool slots. + */ +export function storePoolDirectory(effectiveName: string): string { + return `${OPFS_POOL_DIR_PREFIX}${effectiveName}`; +} + +/** + * Lists the names of every persistent sqlite-opfs store in this origin, by enumerating the per-store pool + * directories. Includes stores other than the current one, so wallets can surface and clean up data for + * networks/versions no longer in use. + */ +export async function listStores(): Promise { + const root = await navigator.storage.getDirectory(); + const names: string[] = []; + for await (const [entryName, handle] of root.entries()) { + if (handle.kind === 'directory' && entryName.startsWith(OPFS_POOL_DIR_PREFIX)) { + names.push(entryName.slice(OPFS_POOL_DIR_PREFIX.length)); + } + } + return names; +} + +/** + * Permanently deletes a store by effective name (as returned by {@link listStores}). The store must be closed: + * an open store's SAH pool holds locks on the directory and the removal will reject. + */ +export async function deleteStore(effectiveName: string): Promise { + const root = await navigator.storage.getDirectory(); + await root.removeEntry(storePoolDirectory(effectiveName), { recursive: true }); +} diff --git a/yarn-project/kv-store/src/sqlite-opfs/store_management.test.ts b/yarn-project/kv-store/src/sqlite-opfs/store_management.test.ts new file mode 100644 index 000000000000..4a99674e9b38 --- /dev/null +++ b/yarn-project/kv-store/src/sqlite-opfs/store_management.test.ts @@ -0,0 +1,56 @@ +import { mockLogger } from '../interfaces/utils.js'; +import { AztecSQLiteOPFSStore } from './index.js'; +import { deleteStore, listStores, storePoolDirectory } from './manage.js'; + +const openByName = (name: string) => AztecSQLiteOPFSStore.open(mockLogger, name, false, storePoolDirectory(name)); + +describe('sqlite-opfs store management', () => { + it('round-trips data for a store reopened by name', async () => { + const store = await openByName('mech_roundtrip'); + await store.openSingleton('payload').set('data'); + await store.close(); + + const reopened = await openByName('mech_roundtrip'); + expect(await reopened.openSingleton('payload').getAsync()).toEqual('data'); + await reopened.close(); + await deleteStore('mech_roundtrip'); + }); + + it('opens two different stores concurrently in the same tab', async () => { + const a = await openByName('mech_concurrent_a'); + const b = await openByName('mech_concurrent_b'); + + await a.openSingleton('k').set('a'); + await b.openSingleton('k').set('b'); + expect(await a.openSingleton('k').getAsync()).toEqual('a'); + expect(await b.openSingleton('k').getAsync()).toEqual('b'); + + await a.close(); + await b.close(); + await deleteStore('mech_concurrent_a'); + await deleteStore('mech_concurrent_b'); + }); + + it('lists created stores and deletes them', async () => { + const store = await openByName('mech_managed'); + await store.openSingleton('k').set('v'); + await store.close(); + + expect(await listStores()).toContain('mech_managed'); + await deleteStore('mech_managed'); + expect(await listStores()).not.toContain('mech_managed'); + + // Recreating after deletion starts empty. + const fresh = await openByName('mech_managed'); + expect(await fresh.openSingleton('k').getAsync()).toBeUndefined(); + await fresh.close(); + await deleteStore('mech_managed'); + }); + + it('refuses to delete a store that is currently open', async () => { + const store = await openByName('mech_locked'); + await expect(deleteStore('mech_locked')).rejects.toThrow(); + await store.close(); + await deleteStore('mech_locked'); + }); +}); diff --git a/yarn-project/kv-store/tsconfig.json b/yarn-project/kv-store/tsconfig.json index b9f4050a2816..673519ee091f 100644 --- a/yarn-project/kv-store/tsconfig.json +++ b/yarn-project/kv-store/tsconfig.json @@ -5,7 +5,8 @@ "rootDir": "src", "tsBuildInfoFile": ".tsbuildinfo", "allowJs": true, - "checkJs": false + "checkJs": false, + "lib": ["dom", "dom.asynciterable", "esnext", "es2017.object"] }, "references": [ { diff --git a/yarn-project/pxe/src/entrypoints/client/bundle/index.ts b/yarn-project/pxe/src/entrypoints/client/bundle/index.ts index 437bf2a74da3..12f498eabc41 100644 --- a/yarn-project/pxe/src/entrypoints/client/bundle/index.ts +++ b/yarn-project/pxe/src/entrypoints/client/bundle/index.ts @@ -6,3 +6,4 @@ export * from '../../../contract_logging.js'; export * from '../../../storage/index.js'; export * from './utils.js'; export type { PXECreationOptions } from '../../pxe_creation_options.js'; +export { openBrowserStore } from '../store.js'; diff --git a/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts b/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts index 3c37cd2d3490..7961cc7155ec 100644 --- a/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts +++ b/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts @@ -1,6 +1,5 @@ import { BBBundlePrivateKernelProver } from '@aztec/bb-prover/client/bundle'; import { createLogger } from '@aztec/foundation/log'; -import { createStore } from '@aztec/kv-store/sqlite-opfs'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { WASMSimulator } from '@aztec/simulator/client'; import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry'; @@ -12,6 +11,7 @@ import type { PXEConfig } from '../../../config/index.js'; import { PXE } from '../../../pxe.js'; import { PXE_DATA_SCHEMA_VERSION } from '../../../storage/metadata.js'; import { type PXECreationOptions, isPrivateKernelProver } from '../../pxe_creation_options.js'; +import { openBrowserStore } from '../store.js'; /** * Create and start an PXE instance with the given AztecNode. @@ -31,16 +31,28 @@ export async function createPXE( const actor = options.loggerActorLabel; const loggers = options.loggers ?? {}; - const l1ContractAddresses = await aztecNode.getL1ContractAddresses(); + const { l1ChainId, l1ContractAddresses, rollupVersion } = await aztecNode.getNodeInfo(); const configWithContracts = { ...config, ...l1ContractAddresses, + l1ChainId, + rollupVersion, } as PXEConfig; const storeLogger = loggers.store ?? createLogger('pxe:data', { actor }); const store = - options.store ?? (await createStore('pxe_data', configWithContracts, PXE_DATA_SCHEMA_VERSION, storeLogger)); + options.store ?? + (await openBrowserStore( + 'pxe_data', + PXE_DATA_SCHEMA_VERSION, + { + l1ChainId, + rollupAddress: l1ContractAddresses.rollupAddress, + dataStoreMapSizeKb: configWithContracts.dataStoreMapSizeKb, + }, + storeLogger, + )); const simulator = options.simulator ?? new WASMSimulator(); const proverLogger = loggers.prover ?? createLogger('pxe:bb:wasm:bundle', { actor }); diff --git a/yarn-project/pxe/src/entrypoints/client/lazy/index.ts b/yarn-project/pxe/src/entrypoints/client/lazy/index.ts index 3f417a8b5209..d9a339a33458 100644 --- a/yarn-project/pxe/src/entrypoints/client/lazy/index.ts +++ b/yarn-project/pxe/src/entrypoints/client/lazy/index.ts @@ -6,3 +6,4 @@ export * from '../../../error_enriching.js'; export * from '../../../contract_logging.js'; export * from './utils.js'; export { type PXECreationOptions } from '../../pxe_creation_options.js'; +export { openBrowserStore } from '../store.js'; diff --git a/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts b/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts index a036c9b7bc7a..f9f228db638a 100644 --- a/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts +++ b/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts @@ -1,6 +1,5 @@ import { BBLazyPrivateKernelProver } from '@aztec/bb-prover/client/lazy'; import { createLogger } from '@aztec/foundation/log'; -import { createStore } from '@aztec/kv-store/sqlite-opfs'; import { LazyProtocolContractsProvider } from '@aztec/protocol-contracts/providers/lazy'; import { WASMSimulator } from '@aztec/simulator/client'; import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry/lazy'; @@ -12,6 +11,7 @@ import type { PXEConfig } from '../../../config/index.js'; import { PXE } from '../../../pxe.js'; import { PXE_DATA_SCHEMA_VERSION } from '../../../storage/metadata.js'; import { type PXECreationOptions, isPrivateKernelProver } from '../../pxe_creation_options.js'; +import { openBrowserStore } from '../store.js'; /** * Create and start an PXE instance with the given AztecNode. @@ -29,10 +29,12 @@ export async function createPXE( ) { const actor = options.loggerActorLabel; - const l1ContractAddresses = await aztecNode.getL1ContractAddresses(); + const { l1ChainId, l1ContractAddresses, rollupVersion } = await aztecNode.getNodeInfo(); const configWithContracts = { ...config, ...l1ContractAddresses, + l1ChainId, + rollupVersion, } as PXEConfig; const loggers = options.loggers ?? {}; @@ -40,7 +42,17 @@ export async function createPXE( const storeLogger = loggers.store ?? createLogger('pxe:data', { actor }); const store = - options.store ?? (await createStore('pxe_data', configWithContracts, PXE_DATA_SCHEMA_VERSION, storeLogger)); + options.store ?? + (await openBrowserStore( + 'pxe_data', + PXE_DATA_SCHEMA_VERSION, + { + l1ChainId, + rollupAddress: l1ContractAddresses.rollupAddress, + dataStoreMapSizeKb: configWithContracts.dataStoreMapSizeKb, + }, + storeLogger, + )); const simulator = options.simulator ?? new WASMSimulator(); const proverLogger = loggers.prover ?? createLogger('pxe:bb:wasm:bundle', { actor }); diff --git a/yarn-project/pxe/src/entrypoints/client/store.ts b/yarn-project/pxe/src/entrypoints/client/store.ts new file mode 100644 index 000000000000..13d8583cab0d --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/client/store.ts @@ -0,0 +1,38 @@ +import type { EthAddress } from '@aztec/foundation/eth-address'; +import { type Logger, createLogger } from '@aztec/foundation/log'; +import { AztecSQLiteOPFSStore, storePoolDirectory } from '@aztec/kv-store/sqlite-opfs'; + +import { assertStoreIdentity, effectiveStoreName } from '../../storage/store_identity.js'; + +/** + * Opens the persistent browser (sqlite-opfs) store selected by `name` and identity `(config.l1ChainId, + * config.rollupAddress, schemaVersion)` triple. A store exists per identity: reopening with the same identity returns + * the same data, a different identity selects a different (possibly fresh) store. + */ +export async function openBrowserStore( + name: string, + schemaVersion: number, + config: { l1ChainId: number; rollupAddress: EthAddress; dataStoreMapSizeKb?: number }, + log: Logger = createLogger('pxe:data'), +): Promise { + const identity = { l1ChainId: config.l1ChainId, rollupAddress: config.rollupAddress, schemaVersion }; + const storeName = effectiveStoreName(name, identity); + log.info(`Creating ${storeName} SQLite-OPFS data store`, { + storeName, + dataStoreMapSizeKb: config.dataStoreMapSizeKb, + }); + const store = await AztecSQLiteOPFSStore.open( + createLogger('kv-store:sqlite-opfs'), + storeName, + false, + storePoolDirectory(storeName), + ); + try { + await assertStoreIdentity(store, storeName, identity); + } catch (err) { + // The store handle owns a worker and OPFS locks; release them before surfacing the refusal. + await store.close().catch(() => {}); + throw err; + } + return store; +} diff --git a/yarn-project/pxe/src/entrypoints/server/index.ts b/yarn-project/pxe/src/entrypoints/server/index.ts index c4a96d12cd9f..aaefc18a5a8b 100644 --- a/yarn-project/pxe/src/entrypoints/server/index.ts +++ b/yarn-project/pxe/src/entrypoints/server/index.ts @@ -6,6 +6,7 @@ export * from '../../config/index.js'; export * from '../../error_enriching.js'; export * from '../../storage/index.js'; export * from './utils.js'; +export * from './store.js'; export { NoteService } from '../../notes/note_service.js'; export { ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR } from '../../oracle_version.js'; export { type PXECreationOptions } from '../pxe_creation_options.js'; diff --git a/yarn-project/pxe/src/entrypoints/server/store.test.ts b/yarn-project/pxe/src/entrypoints/server/store.test.ts new file mode 100644 index 000000000000..006a6810b246 --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/server/store.test.ts @@ -0,0 +1,67 @@ +import { EthAddress } from '@aztec/foundation/eth-address'; + +import { mkdtemp, rm, stat } from 'fs/promises'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +import { openStore } from './store.js'; + +describe('openStore', () => { + let dataDirectory: string; + + beforeEach(async () => { + dataDirectory = await mkdtemp(join(tmpdir(), 'store-identity-test-')); + }); + + afterEach(async () => { + await rm(dataDirectory, { recursive: true, force: true }); + }); + + const configFor = (rollupAddress: EthAddress, l1ChainId = 31337) => ({ + dataDirectory, + dataStoreMapSizeKb: 10 * 1024, + rollupAddress, + l1ChainId, + }); + + it('keeps data intact when switching rollup addresses back and forth', async () => { + const addrA = EthAddress.random(); + const addrB = EthAddress.random(); + + const storeA = await openStore('test_store', 1, configFor(addrA)); + await storeA.openSingleton('payload').set('data-for-A'); + await storeA.close(); + + const storeB = await openStore('test_store', 1, configFor(addrB)); + expect(await storeB.openSingleton('payload').getAsync()).toBeUndefined(); + await storeB.close(); + + const reopenedA = await openStore('test_store', 1, configFor(addrA)); + expect(await reopenedA.openSingleton('payload').getAsync()).toEqual('data-for-A'); + await reopenedA.close(); + }); + + it('separates stores by schema version', async () => { + const addr = EthAddress.random(); + + const v1 = await openStore('test_store', 1, configFor(addr)); + await v1.openSingleton('k').set('v1-data'); + await v1.close(); + + const v2 = await openStore('test_store', 2, configFor(addr)); + expect(await v2.openSingleton('k').getAsync()).toBeUndefined(); + await v2.close(); + + const v1Again = await openStore('test_store', 1, configFor(addr)); + expect(await v1Again.openSingleton('k').getAsync()).toEqual('v1-data'); + await v1Again.close(); + }); + + it('places stores under a sibling -stores directory, not nested in ', async () => { + const store = await openStore('test_store', 1, configFor(EthAddress.random())); + await store.close(); + + await expect(stat(join(dataDirectory, 'test_store-stores'))).resolves.toBeDefined(); + await expect(stat(join(dataDirectory, 'test_store'))).rejects.toMatchObject({ code: 'ENOENT' }); + }); +}); diff --git a/yarn-project/pxe/src/entrypoints/server/store.ts b/yarn-project/pxe/src/entrypoints/server/store.ts new file mode 100644 index 000000000000..932a13cbff83 --- /dev/null +++ b/yarn-project/pxe/src/entrypoints/server/store.ts @@ -0,0 +1,47 @@ +import type { EthAddress } from '@aztec/foundation/eth-address'; +import { type LoggerBindings, createLogger } from '@aztec/foundation/log'; +import { type AztecLMDBStoreV2, openStoreAt } from '@aztec/kv-store/lmdb-v2'; + +import { mkdir } from 'fs/promises'; +import { join } from 'path'; + +import { storeIdentitySlug } from '../../storage/store_identity.js'; + +/** Location and identity inputs for opening an identity-partitioned PXE-side store. */ +export type IdentityStoreConfig = { + dataDirectory: string; + /** Maximum LMDB map size in KB. When omitted, the kv-store default map size applies. */ + dataStoreMapSizeKb?: number; + l1ChainId: number; + rollupAddress: EthAddress; +}; + +/** + * Opens the persistent LMDB store selected by `name` and identity triple `(l1ChainId, rollupAddress, schemaVersion)`. + * A store exists per identity: reopening with the same identity returns the same data, a different identity selects + * a different (possibly fresh) store. Callers wanting an ephemeral store use `openTmpStore` explicitly instead. + */ +export async function openStore( + name: string, + schemaVersion: number, + config: IdentityStoreConfig, + bindings?: LoggerBindings, +): Promise { + // Stores live under `/-stores/--v`, a sibling of the + // legacy `/` directory: older binaries reset the legacy directory on a rollup mismatch, so + // per-identity stores must not nest inside it. + const subDir = join( + config.dataDirectory, + `${name}-stores`, + storeIdentitySlug({ l1ChainId: config.l1ChainId, rollupAddress: config.rollupAddress, schemaVersion }), + ); + await mkdir(subDir, { recursive: true }); + + createLogger(`pxe:data:${name}`, bindings).info(`Opening ${name} data store (LMDB v2)`, { + storeName: name, + subDir, + dataStoreMapSizeKb: config.dataStoreMapSizeKb, + }); + + return openStoreAt(subDir, config.dataStoreMapSizeKb, undefined, bindings); +} diff --git a/yarn-project/pxe/src/entrypoints/server/utils.ts b/yarn-project/pxe/src/entrypoints/server/utils.ts index 815c30e01071..32e0d5db5b5f 100644 --- a/yarn-project/pxe/src/entrypoints/server/utils.ts +++ b/yarn-project/pxe/src/entrypoints/server/utils.ts @@ -1,7 +1,7 @@ import { BBBundlePrivateKernelProver } from '@aztec/bb-prover/client/bundle'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; import { createLogger } from '@aztec/foundation/log'; -import { createStore } from '@aztec/kv-store/lmdb-v2'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { WASMSimulator } from '@aztec/simulator/client'; import { MemoryCircuitRecorder, SimulatorRecorderWrapper } from '@aztec/simulator/server'; @@ -15,6 +15,7 @@ import type { PXEConfig } from '../../config/index.js'; import { PXE } from '../../pxe.js'; import { PXE_DATA_SCHEMA_VERSION } from '../../storage/index.js'; import { type PXECreationOptions, isPrivateKernelProver } from '../pxe_creation_options.js'; +import { openStore } from './store.js'; type PXEConfigWithoutDefaults = Omit< PXEConfig, @@ -46,12 +47,25 @@ export async function createPXE( if (!options.store) { const storeLogger = loggers.store ?? createLogger('pxe:data:lmdb', { actor }); - options.store = await createStore( - 'pxe_data', - PXE_DATA_SCHEMA_VERSION, - configWithContracts, - storeLogger.getBindings(), - ); + options.store = configWithContracts.dataDirectory + ? await openStore( + 'pxe_data', + PXE_DATA_SCHEMA_VERSION, + { + dataDirectory: configWithContracts.dataDirectory, + dataStoreMapSizeKb: configWithContracts.dataStoreMapSizeKb, + l1ChainId, + rollupAddress: l1ContractAddresses.rollupAddress, + }, + storeLogger.getBindings(), + ) + : await openTmpStore( + 'pxe_data', + true, + configWithContracts.dataStoreMapSizeKb, + undefined, + storeLogger.getBindings(), + ); } const proverLogger = loggers.prover ?? createLogger('pxe:bb:native', { actor }); diff --git a/yarn-project/pxe/src/storage/backwards_compatibility_tests/pxe_db_compatibility.test.ts b/yarn-project/pxe/src/storage/backwards_compatibility_tests/pxe_db_compatibility.test.ts index e40e0209e4a1..c92a6c4eb87a 100644 --- a/yarn-project/pxe/src/storage/backwards_compatibility_tests/pxe_db_compatibility.test.ts +++ b/yarn-project/pxe/src/storage/backwards_compatibility_tests/pxe_db_compatibility.test.ts @@ -2,7 +2,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { KeyStore } from '@aztec/key-store'; import type { AztecAsyncKVStore } from '@aztec/kv-store'; -import { createStore, openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/stdlib/block'; @@ -12,6 +12,7 @@ import { tmpdir } from 'os'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; +import { openStore } from '../../entrypoints/server/store.js'; import { PXE_DATA_SCHEMA_VERSION } from '../metadata.js'; import { openPxeStores } from '../open_pxe_stores.js'; import { SCHEMA_TESTS } from './schema_tests.js'; @@ -85,9 +86,9 @@ function compatibilityTestGuidance(name: string): string { 'If intentional, take these steps in order:', ' 1. Determine whether the change is BREAKING or READ-DEFAULTABLE:', ' - If BREAKING (e.g., a sub-store was renamed or removed; existing data becomes inaccessible):', - ' bump PXE_DATA_SCHEMA_VERSION in pxe/src/storage/metadata.ts. DatabaseVersionManager wipes any', - ' DB whose stored version is below the current one when it next opens; without this bump,', - ' existing wallets see corrupted data with no migration path.', + ' bump PXE_DATA_SCHEMA_VERSION in pxe/src/storage/metadata.ts. The schema version is part of the', + ' store identity, so bumping it selects a fresh store on next open (existing data is left on disk,', + ' unread); without this bump, existing wallets see corrupted data with no migration path.', ' - If READ-DEFAULTABLE (e.g., a new sub-store was added; existing data continues to work because', ' the new sub-store starts empty for pre-existing DBs and is populated as new events arrive):', ' leave PXE_DATA_SCHEMA_VERSION alone, but document the reasoning in the commit/PR description.', @@ -115,9 +116,9 @@ function compatibilityTestGuidance(name: string): string { ' Examples: a new key whose absence resolves to a sentinel (genesis, undefined, etc.); a new', ' field with a safe default. Existing wallets continue working after upgrade and the on-disk', ' state self-heals as new events arrive.', - ' 2. If BREAKING: bump PXE_DATA_SCHEMA_VERSION in pxe/src/storage/metadata.ts. DatabaseVersionManager', - ' wipes any DB whose stored version is below the current one when it next opens; without this bump,', - ' existing wallets see corrupted data with no migration path.', + ' 2. If BREAKING: bump PXE_DATA_SCHEMA_VERSION in pxe/src/storage/metadata.ts. The schema version is', + ' part of the store identity, so bumping it selects a fresh store on next open (existing data is left', + ' on disk, unread); without this bump, existing wallets see corrupted data with no migration path.', ' If READ-DEFAULTABLE: leave PXE_DATA_SCHEMA_VERSION alone, but document the reasoning in the', ' commit/PR description (which fallback applies, where, and what state pre-upgrade DBs converge to).', ` 3. Regenerate ONLY ${name}.json. From the yarn-project directory, run:`, @@ -146,36 +147,46 @@ async function collectOpenedStores() { } /** - * Seeds rows into a `pxe_data` store opened at `oldSchemaVersion`, then reopens it at the current - * `PXE_DATA_SCHEMA_VERSION`. The version mismatch makes DatabaseVersionManager wipe the database on open, so - * `assertWiped` runs against a cleared store and can prove the legacy rows are gone. + * Seeds rows into a `pxe_data` store opened at `oldSchemaVersion`, then opens the store for the current + * `PXE_DATA_SCHEMA_VERSION`. The schema version is part of the store identity, so the current version selects a + * fresh store: `assertNotRead` proves the legacy rows are invisible to new code, and `assertLegacyIntact` proves + * they still exist untouched in the old schema's own store. */ -async function expectStoreWipedOnUpgradeFrom( +async function expectFreshStoreSelectedOnUpgradeFrom( oldSchemaVersion: number, seedLegacyRows: (oldStore: AztecAsyncKVStore) => Promise, - assertWiped: (currentStore: AztecAsyncKVStore) => Promise, + assertNotRead: (currentStore: AztecAsyncKVStore) => Promise, + assertLegacyIntact: (oldStore: AztecAsyncKVStore) => Promise, ) { - const dataDirectory = await mkdtemp(join(tmpdir(), 'pxe-schema-wipe-')); + const dataDirectory = await mkdtemp(join(tmpdir(), 'pxe-schema-upgrade-')); const config = { dataDirectory, dataStoreMapSizeKb: 1024, + l1ChainId: 31337, rollupAddress: EthAddress.ZERO, }; try { - const oldStore = await createStore('pxe_data', oldSchemaVersion, config); + const oldStore = await openStore('pxe_data', oldSchemaVersion, config); try { await seedLegacyRows(oldStore); } finally { await oldStore.close(); } - const currentStore = await createStore('pxe_data', PXE_DATA_SCHEMA_VERSION, config); + const currentStore = await openStore('pxe_data', PXE_DATA_SCHEMA_VERSION, config); try { - await assertWiped(currentStore); + await assertNotRead(currentStore); } finally { await currentStore.close(); } + + const reopenedOldStore = await openStore('pxe_data', oldSchemaVersion, config); + try { + await assertLegacyIntact(reopenedOldStore); + } finally { + await reopenedOldStore.close(); + } } finally { await rm(dataDirectory, { recursive: true, force: true, maxRetries: 3 }); } @@ -198,36 +209,38 @@ async function expectStoreWipedOnUpgradeFrom( * class and compares the resulting bytes to a committed per-store snapshot. */ describe('PXE storage compatibility test suite', () => { - it('wipes key-store rows written before the message-signing and fallback secret keys were removed', async () => { + it('never reads key-store rows written under an older schema version, and leaves them intact', async () => { const account = AztecAddress.fromStringUnsafe('0x0b3683ee9df3ed6ed7027145bd6093f783b0bb4d8354501d906db7bb8cb58ea3'); - await expectStoreWipedOnUpgradeFrom( + const ivskKey = `${account.toString()}-ivsk_m`; + const ivskValue = Buffer.from('1fb01c42d1aaa2662041b899c77cb19e08192193acc5a94405f1b43c974eba7a', 'hex'); + await expectFreshStoreSelectedOnUpgradeFrom( PRE_MESSAGE_AND_FALLBACK_SECRET_KEY_REMOVAL_PXE_SCHEMA_VERSION, - oldStore => - oldStore - .openMap('key_store') - .set( - `${account.toString()}-ivsk_m`, - Buffer.from('1fb01c42d1aaa2662041b899c77cb19e08192193acc5a94405f1b43c974eba7a', 'hex'), - ), + oldStore => oldStore.openMap('key_store').set(ivskKey, ivskValue), async currentStore => { const keyStore = new KeyStore(currentStore); await expect(keyStore.hasAccount(account)).resolves.toBe(false); }, + async oldStore => { + expect(await oldStore.openMap('key_store').getAsync(ivskKey)).toEqual(ivskValue); + }, ); }); - it('wipes tagging-store rows written under the legacy two-part AppTaggingSecret key format', async () => { + it('never reads tagging-store rows written under the legacy two-part AppTaggingSecret key format, and leaves them intact', async () => { // The current toString() only emits the three-part `::` form, so build the legacy two-part // `:` key by hand. const legacyKey = `${new Fr(2n).toString()}:${AztecAddress.fromBigIntUnsafe(3n).toString()}`; - await expectStoreWipedOnUpgradeFrom( + await expectFreshStoreSelectedOnUpgradeFrom( PRE_KIND_PREFIXED_TAGGING_KEY_PXE_SCHEMA_VERSION, oldStore => oldStore.openMap('highest_aged_index').set(legacyKey, 13), async currentStore => { // Assert on the raw map, not a high-level getter: the new three-part toString() never reconstructs the - // legacy two-part key, so a getter would report it absent (false-pass) whether or not the wipe happened. + // legacy two-part key, so a getter would report it absent (false-pass) regardless of which store was opened. await expect(currentStore.openMap('highest_aged_index').sizeAsync()).resolves.toBe(0); }, + async oldStore => { + expect(await oldStore.openMap('highest_aged_index').getAsync(legacyKey)).toBe(13); + }, ); }); diff --git a/yarn-project/pxe/src/storage/store_identity.test.ts b/yarn-project/pxe/src/storage/store_identity.test.ts new file mode 100644 index 000000000000..b8288bbf2232 --- /dev/null +++ b/yarn-project/pxe/src/storage/store_identity.test.ts @@ -0,0 +1,68 @@ +import { EthAddress } from '@aztec/foundation/eth-address'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; + +import { + StoreIdentityMismatchError, + assertStoreIdentity, + effectiveStoreName, + storeIdentitySlug, +} from './store_identity.js'; + +describe('storeIdentitySlug', () => { + it('composes chain id, rollup address and schema version', () => { + const rollupAddress = EthAddress.fromString('0x1234567890abcdef1234567890abcdef12345678'); + expect(storeIdentitySlug({ l1ChainId: 31337, rollupAddress, schemaVersion: 12 })).toEqual( + '31337-0x1234567890abcdef1234567890abcdef12345678-v12', + ); + }); + + it('normalizes the rollup address to lowercase hex', () => { + const rollupAddress = EthAddress.fromString('0x1234567890ABCDEF1234567890ABCDEF12345678'); + expect(storeIdentitySlug({ l1ChainId: 0, rollupAddress, schemaVersion: 1 })).toEqual( + '0-0x1234567890abcdef1234567890abcdef12345678-v1', + ); + }); +}); + +describe('effectiveStoreName', () => { + it('joins the logical name and the slug with an underscore', () => { + const rollupAddress = EthAddress.fromString('0x1234567890abcdef1234567890abcdef12345678'); + expect(effectiveStoreName('pxe_data', { l1ChainId: 1, rollupAddress, schemaVersion: 2 })).toEqual( + 'pxe_data_1-0x1234567890abcdef1234567890abcdef12345678-v2', + ); + }); +}); + +describe('assertStoreIdentity', () => { + const identityFor = (rollupAddress: EthAddress) => ({ l1ChainId: 31337, rollupAddress, schemaVersion: 1 }); + + it('writes the marker on first open and accepts a matching reopen', async () => { + const store = await openTmpStore('identity-test'); + const identity = identityFor(EthAddress.random()); + await assertStoreIdentity(store, 'test_store', identity); + await expect(assertStoreIdentity(store, 'test_store', identity)).resolves.toBeUndefined(); + await store.close(); + }); + + it('refuses a mismatching identity and leaves data untouched', async () => { + const store = await openTmpStore('identity-test'); + const identity = identityFor(EthAddress.random()); + await assertStoreIdentity(store, 'test_store', identity); + await store.openSingleton('payload').set('precious'); + + const bumped = { ...identity, schemaVersion: identity.schemaVersion + 1 }; + await expect(assertStoreIdentity(store, 'test_store', bumped)).rejects.toThrow(StoreIdentityMismatchError); + + expect(await store.openSingleton('payload').getAsync()).toEqual('precious'); + await store.close(); + }); + + it('refuses a corrupted marker', async () => { + const store = await openTmpStore('identity-test'); + await store.openSingleton('dbVersion').set('garbage'); + await expect(assertStoreIdentity(store, 'test_store', identityFor(EthAddress.random()))).rejects.toThrow( + StoreIdentityMismatchError, + ); + await store.close(); + }); +}); diff --git a/yarn-project/pxe/src/storage/store_identity.ts b/yarn-project/pxe/src/storage/store_identity.ts new file mode 100644 index 000000000000..6daeb4a2e5a7 --- /dev/null +++ b/yarn-project/pxe/src/storage/store_identity.ts @@ -0,0 +1,72 @@ +import type { EthAddress } from '@aztec/foundation/eth-address'; +import type { AztecAsyncKVStore } from '@aztec/kv-store'; +import { DatabaseVersion } from '@aztec/stdlib/database-version/version'; + +/** The triple that determine which physical store a logical store name maps to. */ +export type StoreIdentity = { + /** Chain ID of the L1 the rollup is deployed to. */ + l1ChainId: number; + /** Address of the rollup contract the store's data pertains to. */ + rollupAddress: EthAddress; + /** Schema version of the data held in the store. */ + schemaVersion: number; +}; + +/** + * Composes the store-name discriminator for a store identity. Two identities map to the same physical store iff + * their slugs are equal, so the format must stay stable: `--v` — changing + * it orphans every existing store. + */ +export function storeIdentitySlug({ l1ChainId, rollupAddress, schemaVersion }: StoreIdentity): string { + return `${l1ChainId}-${rollupAddress.toString()}-v${schemaVersion}`; +} + +/** Composes the physical store name for a logical store name and identity. */ +export function effectiveStoreName(name: string, identity: StoreIdentity): string { + return `${name}_${storeIdentitySlug(identity)}`; +} + +/** + * Invariant check for stores whose physical name carries their identity: the recorded version can + * only disagree with the expected one if there is a store-naming bug. + */ +export async function assertStoreIdentity( + store: AztecAsyncKVStore, + storeName: string, + identity: StoreIdentity, +): Promise { + const expected = new DatabaseVersion(identity.schemaVersion, identity.rollupAddress); + const singleton = store.openSingleton('dbVersion'); + const stored = await singleton.getAsync(); + if (stored === undefined) { + await singleton.set(expected.toBuffer().toString('utf-8')); + return; + } + let storedVersion: DatabaseVersion; + try { + storedVersion = DatabaseVersion.fromBuffer(Buffer.from(stored, 'utf-8')); + } catch { + throw new StoreIdentityMismatchError(storeName, expected.toString(), stored); + } + if (!storedVersion.equals(expected)) { + throw new StoreIdentityMismatchError(storeName, expected.toString(), storedVersion.toString()); + } +} + +/** + * Thrown when a store's recorded identity does not match the identity it was opened under. Since the identity is + * part of the physical store name, this can only indicate a store-naming bug; the store is left untouched. + */ +export class StoreIdentityMismatchError extends Error { + constructor( + public readonly storeName: string, + public readonly expected: string, + public readonly actual: string, + ) { + super( + `Store '${storeName}' records identity ${actual} but was opened as ${expected}. ` + + `Refusing to open; data was NOT modified.`, + ); + this.name = 'StoreIdentityMismatchError'; + } +} diff --git a/yarn-project/wallets/src/embedded/entrypoints/browser.ts b/yarn-project/wallets/src/embedded/entrypoints/browser.ts index 8a5a7bdaf1ca..2b34f83ffbca 100644 --- a/yarn-project/wallets/src/embedded/entrypoints/browser.ts +++ b/yarn-project/wallets/src/embedded/entrypoints/browser.ts @@ -1,7 +1,7 @@ import { type AztecNode, createAztecNodeClient } from '@aztec/aztec.js/node'; import { type Logger, createLogger } from '@aztec/foundation/log'; -import { createStore, openTmpStore } from '@aztec/kv-store/sqlite-opfs'; -import { type PXE, type PXECreationOptions, createPXE } from '@aztec/pxe/client/lazy'; +import { openTmpStore } from '@aztec/kv-store/sqlite-opfs'; +import { type PXE, type PXECreationOptions, createPXE, openBrowserStore } from '@aztec/pxe/client/lazy'; import { type PXEConfig, getPXEConfig } from '@aztec/pxe/config'; import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry/lazy'; import { getStandardHandshakeRegistry } from '@aztec/standard-contracts/handshake-registry/lazy'; @@ -10,7 +10,7 @@ import { getStandardMultiCallEntrypoint } from '@aztec/standard-contracts/multi- import { LazyAccountContractsProvider } from '../account-contract-providers/lazy.js'; import type { AccountContractsProvider } from '../account-contract-providers/types.js'; import { EmbeddedWallet, type EmbeddedWalletOptions, splitPxeOptions } from '../embedded_wallet.js'; -import { WalletDB } from '../wallet_db.js'; +import { WALLET_DATA_SCHEMA_VERSION, WalletDB } from '../wallet_db.js'; export class BrowserEmbeddedWallet extends EmbeddedWallet { static async create( @@ -27,7 +27,6 @@ export class BrowserEmbeddedWallet extends EmbeddedWallet { const rootLogger = options.logger ?? createLogger('embedded-wallet'); const aztecNode = typeof nodeOrUrl === 'string' ? createAztecNodeClient(nodeOrUrl) : nodeOrUrl; - const l1Contracts = await aztecNode.getL1ContractAddresses(); // Support both the new unified `pxe` option and the deprecated `pxeConfig`/`pxeOptions`. const { config: pxeConfigFromPxe, creation: pxeCreationFromPxe } = splitPxeOptions(options.pxe); @@ -36,7 +35,8 @@ export class BrowserEmbeddedWallet extends EmbeddedWallet { const pxeConfig: PXEConfig = Object.assign(getPXEConfig(), { proverEnabled: mergedConfigOverrides.proverEnabled, - dataDirectory: `pxe_data_${l1Contracts.rollupAddress}`, + // Unused in the browser: sqlite-opfs keys stores by name, not directory. + dataDirectory: 'pxe_data', autoSync: false, ...mergedConfigOverrides, }); @@ -64,20 +64,20 @@ export class BrowserEmbeddedWallet extends EmbeddedWallet { const pxe = await createPXE(aztecNode, pxeConfig, pxeOptions); - const walletDBStore = - options.walletDb?.store ?? - (options.ephemeral - ? await openTmpStore(true) - : await createStore( - 'wallet_data', - { - dataDirectory: `wallet_data_${l1Contracts.rollupAddress}`, - dataStoreMapSizeKb: pxeConfig.dataStoreMapSizeKb, - rollupAddress: l1Contracts.rollupAddress, - }, - 1, - rootLogger.createChild('wallet:data'), - )); + let walletDBStore = options.walletDb?.store; + if (!walletDBStore) { + if (options.ephemeral) { + walletDBStore = await openTmpStore(true); + } else { + const { l1ChainId, l1ContractAddresses } = await aztecNode.getNodeInfo(); + walletDBStore = await openBrowserStore( + 'wallet_data', + WALLET_DATA_SCHEMA_VERSION, + { l1ChainId, rollupAddress: l1ContractAddresses.rollupAddress }, + rootLogger.createChild('wallet:data'), + ); + } + } const walletDB = new WalletDB(walletDBStore, rootLogger.createChild('wallet:db').info); const wallet = new this(pxe, aztecNode, walletDB, new LazyAccountContractsProvider(), rootLogger) as T; diff --git a/yarn-project/wallets/src/embedded/entrypoints/node.ts b/yarn-project/wallets/src/embedded/entrypoints/node.ts index f7772599c5a8..46b04d6899f7 100644 --- a/yarn-project/wallets/src/embedded/entrypoints/node.ts +++ b/yarn-project/wallets/src/embedded/entrypoints/node.ts @@ -1,8 +1,8 @@ import { createAztecNodeClient } from '@aztec/aztec.js/node'; import { type Logger, createLogger } from '@aztec/foundation/log'; -import { createStore, openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { type PXEConfig, getPXEConfig } from '@aztec/pxe/config'; -import { type PXE, type PXECreationOptions, createPXE } from '@aztec/pxe/server'; +import { type PXE, type PXECreationOptions, createPXE, openStore } from '@aztec/pxe/server'; import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry'; import { getStandardHandshakeRegistry } from '@aztec/standard-contracts/handshake-registry'; import { getStandardMultiCallEntrypoint } from '@aztec/standard-contracts/multi-call-entrypoint'; @@ -11,7 +11,9 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { BundleAccountContractsProvider } from '../account-contract-providers/bundle.js'; import type { AccountContractsProvider } from '../account-contract-providers/types.js'; import { EmbeddedWallet, type EmbeddedWalletOptions, splitPxeOptions } from '../embedded_wallet.js'; -import { WalletDB } from '../wallet_db.js'; +import { WALLET_DATA_SCHEMA_VERSION, WalletDB } from '../wallet_db.js'; + +const DEFAULT_WALLET_DATA_DIRECTORY = 'aztec-wallet-data'; export class NodeEmbeddedWallet extends EmbeddedWallet { static async create( @@ -28,7 +30,6 @@ export class NodeEmbeddedWallet extends EmbeddedWallet { const rootLogger = options.logger ?? createLogger('embedded-wallet'); const aztecNode = typeof nodeOrUrl === 'string' ? createAztecNodeClient(nodeOrUrl) : nodeOrUrl; - const l1Contracts = await aztecNode.getL1ContractAddresses(); // Support both the new unified `pxe` option and the deprecated `pxeConfig`/`pxeOptions`. const { config: pxeConfigFromPxe, creation: pxeCreationFromPxe } = splitPxeOptions(options.pxe); @@ -37,7 +38,7 @@ export class NodeEmbeddedWallet extends EmbeddedWallet { const pxeConfig: PXEConfig = Object.assign(getPXEConfig(), { proverEnabled: mergedConfigOverrides.proverEnabled, - dataDirectory: `pxe_data_${l1Contracts.rollupAddress}`, + dataDirectory: DEFAULT_WALLET_DATA_DIRECTORY, autoSync: false, ...mergedConfigOverrides, }); @@ -65,26 +66,26 @@ export class NodeEmbeddedWallet extends EmbeddedWallet { const pxe = await createPXE(aztecNode, pxeConfig, pxeOptions); - const walletDBStore = - options.walletDb?.store ?? - (options.ephemeral - ? await openTmpStore( - `wallet_data_${l1Contracts.rollupAddress}`, - true, - undefined, - undefined, - rootLogger.createChild('wallet:data').getBindings(), - ) - : await createStore( - 'wallet_data', - 1, - { - dataDirectory: `wallet_data_${l1Contracts.rollupAddress}`, - dataStoreMapSizeKb: pxeConfig.dataStoreMapSizeKb, - rollupAddress: l1Contracts.rollupAddress, - }, - rootLogger.createChild('wallet:data').getBindings(), - )); + let walletDBStore = options.walletDb?.store; + if (!walletDBStore) { + const bindings = rootLogger.createChild('wallet:data').getBindings(); + if (options.ephemeral) { + walletDBStore = await openTmpStore('wallet_data', true, undefined, undefined, bindings); + } else { + const { l1ChainId, l1ContractAddresses } = await aztecNode.getNodeInfo(); + walletDBStore = await openStore( + 'wallet_data', + WALLET_DATA_SCHEMA_VERSION, + { + dataDirectory: pxeConfig.dataDirectory ?? DEFAULT_WALLET_DATA_DIRECTORY, + dataStoreMapSizeKb: pxeConfig.dataStoreMapSizeKb, + l1ChainId, + rollupAddress: l1ContractAddresses.rollupAddress, + }, + bindings, + ); + } + } const walletDB = new WalletDB(walletDBStore, rootLogger.createChild('wallet:db').info); const wallet = new this(pxe, aztecNode, walletDB, new BundleAccountContractsProvider(), rootLogger) as T; diff --git a/yarn-project/wallets/src/embedded/wallet_db.ts b/yarn-project/wallets/src/embedded/wallet_db.ts index 6671cdee0171..3feba81ab538 100644 --- a/yarn-project/wallets/src/embedded/wallet_db.ts +++ b/yarn-project/wallets/src/embedded/wallet_db.ts @@ -11,6 +11,9 @@ function accountKey(field: string, address: AztecAddress | string): string { return `${field}:${address.toString()}`; } +/** Bump when the WalletDB layout changes; a new version selects a fresh store, leaving the old one intact. */ +export const WALLET_DATA_SCHEMA_VERSION = 1; + export class WalletDB { private accounts: AztecAsyncMap; private aliases: AztecAsyncMap;