Skip to content

Commit 69e8d3c

Browse files
authored
refactor: IndexedDB-friendly PXE stores (#20056)
In the past few days we've seen multiple instances of IndexedDB related issues, mostly due to the fact that [browsers are free to decide to commit transactions when a microtask doesn't generate new work for the DB](https://github.com/jakearchibald/idb?tab=readme-ov-file#transaction-lifetime). This PR introduces a number of changes to PXE stores to prevent that class of errors. The driving ideas for this PR are two: 1. Use `transactionAsync` for every store operation. 2. Write `transactionAsync` callbacks in such a way that auto-commits are guaranteed not to occur (at least according to specs).
2 parents db3980d + 324f032 commit 69e8d3c

File tree

9 files changed

+650
-473
lines changed

9 files changed

+650
-473
lines changed

yarn-project/pxe/src/storage/address_store/address_store.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,23 @@ export class AddressStore {
4343
});
4444
}
4545

46-
async #getCompleteAddress(address: AztecAddress): Promise<CompleteAddress | undefined> {
47-
const index = await this.#completeAddressIndex.getAsync(address.toString());
48-
if (index === undefined) {
49-
return undefined;
50-
}
51-
52-
const value = await this.#completeAddresses.atAsync(index);
53-
return value ? await CompleteAddress.fromBuffer(value) : undefined;
54-
}
55-
5646
getCompleteAddress(account: AztecAddress): Promise<CompleteAddress | undefined> {
57-
return this.#getCompleteAddress(account);
47+
return this.#store.transactionAsync(async () => {
48+
const index = await this.#completeAddressIndex.getAsync(account.toString());
49+
if (index === undefined) {
50+
return undefined;
51+
}
52+
53+
const value = await this.#completeAddresses.atAsync(index);
54+
return value ? await CompleteAddress.fromBuffer(value) : undefined;
55+
});
5856
}
5957

60-
async getCompleteAddresses(): Promise<CompleteAddress[]> {
61-
return await Promise.all(
62-
(await toArray(this.#completeAddresses.valuesAsync())).map(v => CompleteAddress.fromBuffer(v)),
63-
);
58+
getCompleteAddresses(): Promise<CompleteAddress[]> {
59+
return this.#store.transactionAsync(async () => {
60+
return await Promise.all(
61+
(await toArray(this.#completeAddresses.valuesAsync())).map(v => CompleteAddress.fromBuffer(v)),
62+
);
63+
});
6464
}
6565
}

yarn-project/pxe/src/storage/anchor_block_store/anchor_block_store.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ export class AnchorBlockStore {
1010
this.#synchronizedHeader = this.#store.openSingleton('header');
1111
}
1212

13+
/**
14+
* Sets the currently synchronized block
15+
*
16+
* Important: this method is only called from BlockSynchronizer, and since we need it to run atomically with other
17+
* stores in the case of a reorg, it MUST NOT be wrapped in a `transactionAsync` call. Doing so would result in a
18+
* deadlock when the backend is IndexedDB, because `transactionAsync` is not designed to support reentrancy.
19+
*
20+
*/
1321
async setHeader(header: BlockHeader): Promise<void> {
1422
await this.#synchronizedHeader.set(header.toBuffer());
1523
}

yarn-project/pxe/src/storage/capsule_store/capsule_store.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ export class CapsuleStore implements StagedStore {
5757
*/
5858
async #getFromStage(jobId: string, dbSlotKey: string): Promise<Buffer | null | undefined> {
5959
const jobStagedCapsules = this.#getJobStagedCapsules(jobId);
60-
let staged: Buffer | null | undefined = jobStagedCapsules.get(dbSlotKey);
61-
// Note that if staged === null, we marked it for deletion, so we don't want to
62-
// re-read it from DB
63-
if (staged === undefined) {
64-
// If we don't have a staged version of this dbSlotKey, first we check if there's one in DB
65-
staged = await this.#loadCapsuleFromDb(dbSlotKey);
66-
}
67-
return staged;
60+
const staged: Buffer | null | undefined = jobStagedCapsules.get(dbSlotKey);
61+
62+
// Always issue DB read to keep IndexedDB transaction alive, even if the value is in the job staged data. This
63+
// keeps IndexedDB transactions alive (they auto-commit when a new micro-task starts and there are no pending read
64+
// requests). The staged value still takes precedence if it exists (including null for deletions).
65+
const dbValue = await this.#loadCapsuleFromDb(dbSlotKey);
66+
67+
return staged !== undefined ? staged : dbValue;
6868
}
6969

7070
/**

yarn-project/pxe/src/storage/contract_store/contract_store.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,20 @@ export class ContractStore {
4242
/** Map from contract address to contract class id */
4343
#contractClassIdMap: Map<string, Fr> = new Map();
4444

45+
#store: AztecAsyncKVStore;
4546
#contractArtifacts: AztecAsyncMap<string, Buffer>;
4647
#contractInstances: AztecAsyncMap<string, Buffer>;
4748

4849
constructor(store: AztecAsyncKVStore) {
50+
this.#store = store;
4951
this.#contractArtifacts = store.openMap('contract_artifacts');
5052
this.#contractInstances = store.openMap('contracts_instances');
5153
}
5254

5355
// Setters
5456

5557
public async addContractArtifact(id: Fr, contract: ContractArtifact): Promise<void> {
58+
// Validation outside transactionAsync - these are not DB operations
5659
const privateFunctions = contract.functions.filter(
5760
functionArtifact => functionArtifact.functionType === FunctionType.PRIVATE,
5861
);
@@ -69,7 +72,9 @@ export class ContractStore {
6972
throw new Error('Repeated function selectors of private functions');
7073
}
7174

72-
await this.#contractArtifacts.set(id.toString(), contractArtifactToBuffer(contract));
75+
await this.#store.transactionAsync(() =>
76+
this.#contractArtifacts.set(id.toString(), contractArtifactToBuffer(contract)),
77+
);
7378
}
7479

7580
async addContractInstance(contract: ContractInstanceWithAddress): Promise<void> {
@@ -123,21 +128,27 @@ export class ContractStore {
123128

124129
// Public getters
125130

126-
async getContractsAddresses(): Promise<AztecAddress[]> {
127-
const keys = await toArray(this.#contractInstances.keysAsync());
128-
return keys.map(AztecAddress.fromString);
131+
getContractsAddresses(): Promise<AztecAddress[]> {
132+
return this.#store.transactionAsync(async () => {
133+
const keys = await toArray(this.#contractInstances.keysAsync());
134+
return keys.map(AztecAddress.fromString);
135+
});
129136
}
130137

131138
/** Returns a contract instance for a given address. Throws if not found. */
132-
public async getContractInstance(contractAddress: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
133-
const contract = await this.#contractInstances.getAsync(contractAddress.toString());
134-
return contract && SerializableContractInstance.fromBuffer(contract).withAddress(contractAddress);
139+
public getContractInstance(contractAddress: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
140+
return this.#store.transactionAsync(async () => {
141+
const contract = await this.#contractInstances.getAsync(contractAddress.toString());
142+
return contract && SerializableContractInstance.fromBuffer(contract).withAddress(contractAddress);
143+
});
135144
}
136145

137-
public async getContractArtifact(contractClassId: Fr): Promise<ContractArtifact | undefined> {
138-
const contract = await this.#contractArtifacts.getAsync(contractClassId.toString());
139-
// TODO(@spalladino): AztecAsyncMap lies and returns Uint8Arrays instead of Buffers, hence the extra Buffer.from.
140-
return contract && contractArtifactFromBuffer(Buffer.from(contract));
146+
public getContractArtifact(contractClassId: Fr): Promise<ContractArtifact | undefined> {
147+
return this.#store.transactionAsync(async () => {
148+
const contract = await this.#contractArtifacts.getAsync(contractClassId.toString());
149+
// TODO(@spalladino): AztecAsyncMap lies and returns Uint8Arrays instead of Buffers, hence the extra Buffer.from.
150+
return contract && contractArtifactFromBuffer(Buffer.from(contract));
151+
});
141152
}
142153

143154
/** Returns a contract class for a given class id. Throws if not found. */

0 commit comments

Comments
 (0)