Skip to content

Commit 0bb0628

Browse files
authored
feat: preserve stores on schema version or rollup address change (#24631)
Changes store management so that changes in rollup addresses and schema versions cause different stores to be used. Previously we only supported one store to be present at a time, which meant any version or rollup change wiped pre-existing stores (note this includes network changes). An underlying design decision in this PR is to strip the kv-store package from responsibility over location on disk, knowledge about rollup addresses, etc, at least as as regards PXE and wallet storage. Other users of LMDB-v2 should not be impacted by this change. In consonance, IndexedDB and SQLite backends drop their `createStore` functions, which shoehorned wallet and PXE store creation to an homogeneous interface that made it hard to let them independently evolve. Since we're changing this, I decided to also include the chain id as a component of the store id, in addition to the already present schema version and rollup address. It's not clear that we'll ever work on a testnet or a different L1, but doing so is trivial and removes the need to deal with this in the future. Closes F-809
1 parent 7a77232 commit 0bb0628

22 files changed

Lines changed: 551 additions & 139 deletions

File tree

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,20 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12-
### [PXE] Local PXE database is reset on upgrade
12+
### [PXE] Stores are now selected by `(l1ChainId, rollupAddress, schemaVersion)` instead of being wiped on mismatch
1313

14-
The persisted tagging stores now key every entry by the self-describing `<kind>:<secret>:<app>` form of `AppTaggingSecret`; unconstrained secrets previously used a two-part `<secret>:<app>` 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.
14+
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.
1515

16-
**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.
16+
**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 `<dataDirectory>/<name>` while new per-identity `pxe_data` stores live under `<dataDirectory>/<name>-stores/`. The embedded Node.js wallet previously stored data in cwd-relative, rollup-address-suffixed directories instead (`pxe_data_<rollupAddress>/pxe_data` for the PXE store, `wallet_data_<rollupAddress>/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 `<dataDirectory>/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:
17+
18+
```ts
19+
import { deleteStore, listStores } from '@aztec/kv-store/sqlite-opfs';
20+
21+
const names = await listStores();
22+
await deleteStore(names[0]);
23+
```
24+
25+
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`.
1726

1827
### [Aztec.nr] `TestEnvironmentOptions::with_tagging_secret_strategy` replaced
1928

playground/src/components/navbar/components/NetworkSelector.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import AddIcon from '@mui/icons-material/Add';
77
import { AddNetworksDialog } from './AddNetworkDialog';
88
import CircularProgress from '@mui/material/CircularProgress';
99
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
10-
import { createStore } from '@aztec/kv-store/sqlite-opfs';
10+
import { AztecSQLiteOPFSStore, storePoolDirectory } from '@aztec/kv-store/sqlite-opfs';
1111
import { AztecContext } from '../../../aztecContext';
1212
import { navbarButtonStyle, navbarSelect } from '../../../styles/common';
1313
import { NETWORKS } from '../../../utils/networks';
@@ -50,10 +50,12 @@ export function NetworkSelector() {
5050
}
5151
setIsContextInitialized(true);
5252
WebLogger.create(setLogs, setTotalLogCount);
53-
const store = await createStore('playground_data', {
54-
dataDirectory: 'playground',
55-
dataStoreMapSizeKb: 1e6,
56-
});
53+
const store = await AztecSQLiteOPFSStore.open(
54+
WebLogger.getInstance().createLogger('playground_data'),
55+
'playground_data',
56+
false,
57+
storePoolDirectory('playground_data'),
58+
);
5759
const playgroundDB = PlaygroundDB.getInstance();
5860
playgroundDB.init(store, WebLogger.getInstance().createLogger('playground_db').info);
5961
setPlaygroundDB(PlaygroundDB.getInstance());

yarn-project/kv-store/src/deprecated/indexeddb/index.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,9 @@
1-
import { type Logger, createLogger } from '@aztec/foundation/log';
2-
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
1+
import { createLogger } from '@aztec/foundation/log';
32

4-
import { initStoreForRollupAndSchemaVersion } from '../../utils.js';
53
import { AztecIndexedDBStore } from './store.js';
64

75
export { AztecIndexedDBStore } from './store.js';
86

9-
/**
10-
* @deprecated The IndexedDB backend is being retired. Use `@aztec/kv-store/sqlite-opfs` instead.
11-
*/
12-
export async function createStore(
13-
name: string,
14-
config: DataStoreConfig,
15-
schemaVersion: number | undefined = undefined,
16-
log: Logger = createLogger('kv-store'),
17-
) {
18-
let { dataDirectory } = config;
19-
if (typeof dataDirectory !== 'undefined') {
20-
dataDirectory = `${dataDirectory}/${name}`;
21-
}
22-
23-
log.info(
24-
dataDirectory
25-
? `Creating ${name} data store at directory ${dataDirectory} with map size ${config.dataStoreMapSizeKb} KB`
26-
: `Creating ${name} ephemeral data store with map size ${config.dataStoreMapSizeKb} KB`,
27-
);
28-
const store = await AztecIndexedDBStore.open(createLogger('kv-store:indexeddb'), dataDirectory ?? '', false);
29-
return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log);
30-
}
31-
327
/**
338
* @deprecated The IndexedDB backend is being retired. Use `@aztec/kv-store/sqlite-opfs` instead.
349
*/

yarn-project/kv-store/src/sqlite-opfs/index.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,11 @@
1-
import { type Logger, createLogger } from '@aztec/foundation/log';
2-
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
1+
import { createLogger } from '@aztec/foundation/log';
32

4-
import { initStoreForRollupAndSchemaVersion } from '../utils.js';
53
import { AztecSQLiteOPFSStore } from './store.js';
64

75
export { AztecSQLiteOPFSStore } from './store.js';
86
export { SqliteEncryptionError } from './errors.js';
97
export type { SqliteEncryptionErrorCode } from './errors.js';
10-
11-
export async function createStore(
12-
name: string,
13-
config: DataStoreConfig,
14-
schemaVersion: number | undefined = undefined,
15-
log: Logger = createLogger('kv-store'),
16-
) {
17-
const { dataDirectory } = config;
18-
log.info(
19-
dataDirectory
20-
? `Creating ${name} SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`
21-
: `Creating ${name} ephemeral SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`,
22-
);
23-
const store = await AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), name, false);
24-
return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log);
25-
}
8+
export { OPFS_POOL_DIR_PREFIX, deleteStore, listStores, storePoolDirectory } from './manage.js';
269

2710
export function openTmpStore(ephemeral: boolean = false): Promise<AztecSQLiteOPFSStore> {
2811
return AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), undefined, ephemeral);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/** Prefix for the per-store OPFS SAH pool directories owned by this package. */
2+
export const OPFS_POOL_DIR_PREFIX = '.aztec-kv-';
3+
4+
/**
5+
* OPFS directory holding a store's SAH pool. One directory per store: the SAH-pool VFS allows only one
6+
* concurrent instance per directory and its capacity does not grow automatically, so sharing a pool across
7+
* stores would make concurrently opened stores contend for locks and orphaned stores exhaust pool slots.
8+
*/
9+
export function storePoolDirectory(effectiveName: string): string {
10+
return `${OPFS_POOL_DIR_PREFIX}${effectiveName}`;
11+
}
12+
13+
/**
14+
* Lists the names of every persistent sqlite-opfs store in this origin, by enumerating the per-store pool
15+
* directories. Includes stores other than the current one, so wallets can surface and clean up data for
16+
* networks/versions no longer in use.
17+
*/
18+
export async function listStores(): Promise<string[]> {
19+
const root = await navigator.storage.getDirectory();
20+
const names: string[] = [];
21+
for await (const [entryName, handle] of root.entries()) {
22+
if (handle.kind === 'directory' && entryName.startsWith(OPFS_POOL_DIR_PREFIX)) {
23+
names.push(entryName.slice(OPFS_POOL_DIR_PREFIX.length));
24+
}
25+
}
26+
return names;
27+
}
28+
29+
/**
30+
* Permanently deletes a store by effective name (as returned by {@link listStores}). The store must be closed:
31+
* an open store's SAH pool holds locks on the directory and the removal will reject.
32+
*/
33+
export async function deleteStore(effectiveName: string): Promise<void> {
34+
const root = await navigator.storage.getDirectory();
35+
await root.removeEntry(storePoolDirectory(effectiveName), { recursive: true });
36+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { mockLogger } from '../interfaces/utils.js';
2+
import { AztecSQLiteOPFSStore } from './index.js';
3+
import { deleteStore, listStores, storePoolDirectory } from './manage.js';
4+
5+
const openByName = (name: string) => AztecSQLiteOPFSStore.open(mockLogger, name, false, storePoolDirectory(name));
6+
7+
describe('sqlite-opfs store management', () => {
8+
it('round-trips data for a store reopened by name', async () => {
9+
const store = await openByName('mech_roundtrip');
10+
await store.openSingleton<string>('payload').set('data');
11+
await store.close();
12+
13+
const reopened = await openByName('mech_roundtrip');
14+
expect(await reopened.openSingleton<string>('payload').getAsync()).toEqual('data');
15+
await reopened.close();
16+
await deleteStore('mech_roundtrip');
17+
});
18+
19+
it('opens two different stores concurrently in the same tab', async () => {
20+
const a = await openByName('mech_concurrent_a');
21+
const b = await openByName('mech_concurrent_b');
22+
23+
await a.openSingleton<string>('k').set('a');
24+
await b.openSingleton<string>('k').set('b');
25+
expect(await a.openSingleton<string>('k').getAsync()).toEqual('a');
26+
expect(await b.openSingleton<string>('k').getAsync()).toEqual('b');
27+
28+
await a.close();
29+
await b.close();
30+
await deleteStore('mech_concurrent_a');
31+
await deleteStore('mech_concurrent_b');
32+
});
33+
34+
it('lists created stores and deletes them', async () => {
35+
const store = await openByName('mech_managed');
36+
await store.openSingleton<string>('k').set('v');
37+
await store.close();
38+
39+
expect(await listStores()).toContain('mech_managed');
40+
await deleteStore('mech_managed');
41+
expect(await listStores()).not.toContain('mech_managed');
42+
43+
// Recreating after deletion starts empty.
44+
const fresh = await openByName('mech_managed');
45+
expect(await fresh.openSingleton<string>('k').getAsync()).toBeUndefined();
46+
await fresh.close();
47+
await deleteStore('mech_managed');
48+
});
49+
50+
it('refuses to delete a store that is currently open', async () => {
51+
const store = await openByName('mech_locked');
52+
await expect(deleteStore('mech_locked')).rejects.toThrow();
53+
await store.close();
54+
await deleteStore('mech_locked');
55+
});
56+
});

yarn-project/kv-store/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"rootDir": "src",
66
"tsBuildInfoFile": ".tsbuildinfo",
77
"allowJs": true,
8-
"checkJs": false
8+
"checkJs": false,
9+
"lib": ["dom", "dom.asynciterable", "esnext", "es2017.object"]
910
},
1011
"references": [
1112
{

yarn-project/pxe/src/entrypoints/client/bundle/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from '../../../contract_logging.js';
66
export * from '../../../storage/index.js';
77
export * from './utils.js';
88
export type { PXECreationOptions } from '../../pxe_creation_options.js';
9+
export { openBrowserStore } from '../store.js';

yarn-project/pxe/src/entrypoints/client/bundle/utils.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { BBBundlePrivateKernelProver } from '@aztec/bb-prover/client/bundle';
22
import { createLogger } from '@aztec/foundation/log';
3-
import { createStore } from '@aztec/kv-store/sqlite-opfs';
43
import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle';
54
import { WASMSimulator } from '@aztec/simulator/client';
65
import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry';
@@ -12,6 +11,7 @@ import type { PXEConfig } from '../../../config/index.js';
1211
import { PXE } from '../../../pxe.js';
1312
import { PXE_DATA_SCHEMA_VERSION } from '../../../storage/metadata.js';
1413
import { type PXECreationOptions, isPrivateKernelProver } from '../../pxe_creation_options.js';
14+
import { openBrowserStore } from '../store.js';
1515

1616
/**
1717
* Create and start an PXE instance with the given AztecNode.
@@ -31,16 +31,28 @@ export async function createPXE(
3131
const actor = options.loggerActorLabel;
3232
const loggers = options.loggers ?? {};
3333

34-
const l1ContractAddresses = await aztecNode.getL1ContractAddresses();
34+
const { l1ChainId, l1ContractAddresses, rollupVersion } = await aztecNode.getNodeInfo();
3535
const configWithContracts = {
3636
...config,
3737
...l1ContractAddresses,
38+
l1ChainId,
39+
rollupVersion,
3840
} as PXEConfig;
3941

4042
const storeLogger = loggers.store ?? createLogger('pxe:data', { actor });
4143

4244
const store =
43-
options.store ?? (await createStore('pxe_data', configWithContracts, PXE_DATA_SCHEMA_VERSION, storeLogger));
45+
options.store ??
46+
(await openBrowserStore(
47+
'pxe_data',
48+
PXE_DATA_SCHEMA_VERSION,
49+
{
50+
l1ChainId,
51+
rollupAddress: l1ContractAddresses.rollupAddress,
52+
dataStoreMapSizeKb: configWithContracts.dataStoreMapSizeKb,
53+
},
54+
storeLogger,
55+
));
4456

4557
const simulator = options.simulator ?? new WASMSimulator();
4658
const proverLogger = loggers.prover ?? createLogger('pxe:bb:wasm:bundle', { actor });

yarn-project/pxe/src/entrypoints/client/lazy/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from '../../../error_enriching.js';
66
export * from '../../../contract_logging.js';
77
export * from './utils.js';
88
export { type PXECreationOptions } from '../../pxe_creation_options.js';
9+
export { openBrowserStore } from '../store.js';

0 commit comments

Comments
 (0)