Skip to content

Commit 85bf66a

Browse files
spalladinoclaude
andcommitted
fix: wrap multi-operation store mutations in db transactions
Ensures atomicity for methods that perform multiple database operations. Affected stores: ContractClassStore, KeyStore, AztecDatastore, WalletDB, SlasherPayloadsStore. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 4ffdee5 commit 85bf66a

File tree

5 files changed

+100
-71
lines changed

5 files changed

+100
-71
lines changed

yarn-project/archiver/src/store/contract_class_store.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,22 @@ export class ContractClassStore {
2828
bytecodeCommitment: Fr,
2929
blockNumber: number,
3030
): Promise<void> {
31-
await this.#contractClasses.setIfNotExists(
32-
contractClass.id.toString(),
33-
serializeContractClassPublic({ ...contractClass, l2BlockNumber: blockNumber }),
34-
);
35-
await this.#bytecodeCommitments.setIfNotExists(contractClass.id.toString(), bytecodeCommitment.toBuffer());
31+
await this.db.transactionAsync(async () => {
32+
await this.#contractClasses.setIfNotExists(
33+
contractClass.id.toString(),
34+
serializeContractClassPublic({ ...contractClass, l2BlockNumber: blockNumber }),
35+
);
36+
await this.#bytecodeCommitments.setIfNotExists(contractClass.id.toString(), bytecodeCommitment.toBuffer());
37+
});
3638
}
3739

3840
async deleteContractClasses(contractClass: ContractClassPublic, blockNumber: number): Promise<void> {
3941
const restoredContractClass = await this.#contractClasses.getAsync(contractClass.id.toString());
4042
if (restoredContractClass && deserializeContractClassPublic(restoredContractClass).l2BlockNumber >= blockNumber) {
41-
await this.#contractClasses.delete(contractClass.id.toString());
42-
await this.#bytecodeCommitments.delete(contractClass.id.toString());
43+
await this.db.transactionAsync(async () => {
44+
await this.#contractClasses.delete(contractClass.id.toString());
45+
await this.#bytecodeCommitments.delete(contractClass.id.toString());
46+
});
4347
}
4448
}
4549

yarn-project/cli-wallet/src/storage/wallet_db.ts

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const Aliases = ['accounts', 'contracts', 'artifacts', 'secrets', 'transa
1212
export type AliasType = (typeof Aliases)[number];
1313

1414
export class WalletDB {
15+
#store!: AztecAsyncKVStore;
1516
#accounts!: AztecAsyncMap<string, Buffer>;
1617
#aliases!: AztecAsyncMap<string, Buffer>;
1718
#bridgedFeeJuice!: AztecAsyncMap<string, Buffer>;
@@ -29,6 +30,7 @@ export class WalletDB {
2930
}
3031

3132
async init(store: AztecAsyncKVStore) {
33+
this.#store = store;
3234
this.#accounts = store.openMap('accounts');
3335
this.#aliases = store.openMap('aliases');
3436
this.#bridgedFeeJuice = store.openMap('bridgedFeeJuice');
@@ -41,14 +43,17 @@ export class WalletDB {
4143
}
4244

4345
async pushBridgedFeeJuice(recipient: AztecAddress, secret: Fr, amount: bigint, leafIndex: bigint, log: LogFn) {
44-
let stackPointer = (await this.#bridgedFeeJuice.getAsync(`${recipient.toString()}:stackPointer`))?.readInt8() || 0;
45-
stackPointer++;
46-
await this.#bridgedFeeJuice.set(
47-
`${recipient.toString()}:${stackPointer}`,
48-
Buffer.from(`${amount.toString()}:${secret.toString()}:${leafIndex.toString()}`),
49-
);
50-
await this.#bridgedFeeJuice.set(`${recipient.toString()}:stackPointer`, Buffer.from([stackPointer]));
51-
log(`Pushed ${amount} fee juice for recipient ${recipient.toString()}. Stack pointer ${stackPointer}`);
46+
await this.#store.transactionAsync(async () => {
47+
let stackPointer =
48+
(await this.#bridgedFeeJuice.getAsync(`${recipient.toString()}:stackPointer`))?.readInt8() || 0;
49+
stackPointer++;
50+
await this.#bridgedFeeJuice.set(
51+
`${recipient.toString()}:${stackPointer}`,
52+
Buffer.from(`${amount.toString()}:${secret.toString()}:${leafIndex.toString()}`),
53+
);
54+
await this.#bridgedFeeJuice.set(`${recipient.toString()}:stackPointer`, Buffer.from([stackPointer]));
55+
log(`Pushed ${amount} fee juice for recipient ${recipient.toString()}. Stack pointer ${stackPointer}`);
56+
});
5257
}
5358

5459
async popBridgedFeeJuice(recipient: AztecAddress, log: LogFn) {
@@ -76,19 +81,24 @@ export class WalletDB {
7681
}: { type: AccountType; secretKey: Fr; salt: Fr; alias: string | undefined; publicKey: string | undefined },
7782
log: LogFn,
7883
) {
79-
if (alias) {
80-
await this.#aliases.set(`accounts:${alias}`, Buffer.from(address.toString()));
81-
}
82-
await this.#accounts.set(`${address.toString()}:type`, Buffer.from(type));
83-
await this.#accounts.set(`${address.toString()}:sk`, secretKey.toBuffer());
84-
await this.#accounts.set(`${address.toString()}:salt`, salt.toBuffer());
84+
let publicSigningKey: Buffer | undefined;
8585
if (type === 'ecdsasecp256r1ssh' && publicKey) {
86-
const publicSigningKey = extractECDSAPublicKeyFromBase64String(publicKey);
87-
await this.storeAccountMetadata(address, 'publicSigningKey', publicSigningKey);
86+
publicSigningKey = extractECDSAPublicKeyFromBase64String(publicKey);
8887
}
89-
await this.#aliases.set('accounts:last', Buffer.from(address.toString()));
90-
log(`Account stored in database with alias${alias ? `es last & ${alias}` : ' last'}`);
9188

89+
await this.#store.transactionAsync(async () => {
90+
if (alias) {
91+
await this.#aliases.set(`accounts:${alias}`, Buffer.from(address.toString()));
92+
}
93+
await this.#accounts.set(`${address.toString()}:type`, Buffer.from(type));
94+
await this.#accounts.set(`${address.toString()}:sk`, secretKey.toBuffer());
95+
await this.#accounts.set(`${address.toString()}:salt`, salt.toBuffer());
96+
if (publicSigningKey) {
97+
await this.#accounts.set(`${address.toString()}:publicSigningKey`, publicSigningKey);
98+
}
99+
await this.#aliases.set('accounts:last', Buffer.from(address.toString()));
100+
});
101+
log(`Account stored in database with alias${alias ? `es last & ${alias}` : ' last'}`);
92102
await this.refreshAliasCache();
93103
}
94104

@@ -100,35 +110,38 @@ export class WalletDB {
100110
}
101111

102112
async storeContract(address: AztecAddress, artifactPath: string, log: LogFn, alias?: string) {
103-
if (alias) {
104-
await this.#aliases.set(`contracts:${alias}`, Buffer.from(address.toString()));
105-
await this.#aliases.set(`artifacts:${alias}`, Buffer.from(artifactPath));
106-
}
107-
await this.#aliases.set(`contracts:last`, Buffer.from(address.toString()));
108-
await this.#aliases.set(`artifacts:last`, Buffer.from(artifactPath));
109-
await this.#aliases.set(`artifacts:${address.toString()}`, Buffer.from(artifactPath));
113+
await this.#store.transactionAsync(async () => {
114+
if (alias) {
115+
await this.#aliases.set(`contracts:${alias}`, Buffer.from(address.toString()));
116+
await this.#aliases.set(`artifacts:${alias}`, Buffer.from(artifactPath));
117+
}
118+
await this.#aliases.set(`contracts:last`, Buffer.from(address.toString()));
119+
await this.#aliases.set(`artifacts:last`, Buffer.from(artifactPath));
120+
await this.#aliases.set(`artifacts:${address.toString()}`, Buffer.from(artifactPath));
121+
});
110122
log(`Contract stored in database with alias${alias ? `es last & ${alias}` : ' last'}`);
111-
112123
await this.refreshAliasCache();
113124
}
114125

115126
async storeAuthwitness(authWit: AuthWitness, log: LogFn, alias?: string) {
116-
if (alias) {
117-
await this.#aliases.set(`authwits:${alias}`, Buffer.from(authWit.toString()));
118-
}
119-
await this.#aliases.set(`authwits:last`, Buffer.from(authWit.toString()));
127+
await this.#store.transactionAsync(async () => {
128+
if (alias) {
129+
await this.#aliases.set(`authwits:${alias}`, Buffer.from(authWit.toString()));
130+
}
131+
await this.#aliases.set(`authwits:last`, Buffer.from(authWit.toString()));
132+
});
120133
log(`Authorization witness stored in database with alias${alias ? `es last & ${alias}` : ' last'}`);
121-
122134
await this.refreshAliasCache();
123135
}
124136

125137
async storeTx({ txHash }: { txHash: TxHash }, log: LogFn, alias?: string) {
126-
if (alias) {
127-
await this.#aliases.set(`transactions:${alias}`, Buffer.from(txHash.toString()));
128-
}
129-
await this.#aliases.set(`transactions:last`, Buffer.from(txHash.toString()));
138+
await this.#store.transactionAsync(async () => {
139+
if (alias) {
140+
await this.#aliases.set(`transactions:${alias}`, Buffer.from(txHash.toString()));
141+
}
142+
await this.#aliases.set(`transactions:last`, Buffer.from(txHash.toString()));
143+
});
130144
log(`Transaction hash stored in database with alias${alias ? `es last & ${alias}` : ' last'}`);
131-
132145
await this.refreshAliasCache();
133146
}
134147

yarn-project/key-store/src/key_store.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import {
2222
*/
2323
export class KeyStore {
2424
public static readonly SCHEMA_VERSION = 1;
25+
#db: AztecAsyncKVStore;
2526
#keys: AztecAsyncMap<string, Buffer>;
2627

2728
constructor(database: AztecAsyncKVStore) {
29+
this.#db = database;
2830
this.#keys = database.openMap('key_store');
2931
}
3032

@@ -56,27 +58,31 @@ export class KeyStore {
5658
const completeAddress = await CompleteAddress.fromSecretKeyAndPartialAddress(sk, partialAddress);
5759
const { address: account } = completeAddress;
5860

59-
// Naming of keys is as follows ${account}-${n/iv/ov/t}${sk/pk}_m
60-
await this.#keys.set(`${account.toString()}-ivsk_m`, masterIncomingViewingSecretKey.toBuffer());
61-
await this.#keys.set(`${account.toString()}-ovsk_m`, masterOutgoingViewingSecretKey.toBuffer());
62-
await this.#keys.set(`${account.toString()}-tsk_m`, masterTaggingSecretKey.toBuffer());
63-
await this.#keys.set(`${account.toString()}-nsk_m`, masterNullifierSecretKey.toBuffer());
64-
65-
await this.#keys.set(`${account.toString()}-npk_m`, publicKeys.masterNullifierPublicKey.toBuffer());
66-
await this.#keys.set(`${account.toString()}-ivpk_m`, publicKeys.masterIncomingViewingPublicKey.toBuffer());
67-
await this.#keys.set(`${account.toString()}-ovpk_m`, publicKeys.masterOutgoingViewingPublicKey.toBuffer());
68-
await this.#keys.set(`${account.toString()}-tpk_m`, publicKeys.masterTaggingPublicKey.toBuffer());
69-
70-
// We store pk_m_hash under `account-{n/iv/ov/t}pk_m_hash` key to be able to obtain address and key prefix
71-
// using the #getKeyPrefixAndAccount function later on
61+
// Compute hashes before transaction
7262
const masterNullifierPublicKeyHash = await publicKeys.masterNullifierPublicKey.hash();
73-
await this.#keys.set(`${account.toString()}-npk_m_hash`, masterNullifierPublicKeyHash.toBuffer());
7463
const masterIncomingViewingPublicKeyHash = await publicKeys.masterIncomingViewingPublicKey.hash();
75-
await this.#keys.set(`${account.toString()}-ivpk_m_hash`, masterIncomingViewingPublicKeyHash.toBuffer());
7664
const masterOutgoingViewingPublicKeyHash = await publicKeys.masterOutgoingViewingPublicKey.hash();
77-
await this.#keys.set(`${account.toString()}-ovpk_m_hash`, masterOutgoingViewingPublicKeyHash.toBuffer());
7865
const masterTaggingPublicKeyHash = await publicKeys.masterTaggingPublicKey.hash();
79-
await this.#keys.set(`${account.toString()}-tpk_m_hash`, masterTaggingPublicKeyHash.toBuffer());
66+
67+
await this.#db.transactionAsync(async () => {
68+
// Naming of keys is as follows ${account}-${n/iv/ov/t}${sk/pk}_m
69+
await this.#keys.set(`${account.toString()}-ivsk_m`, masterIncomingViewingSecretKey.toBuffer());
70+
await this.#keys.set(`${account.toString()}-ovsk_m`, masterOutgoingViewingSecretKey.toBuffer());
71+
await this.#keys.set(`${account.toString()}-tsk_m`, masterTaggingSecretKey.toBuffer());
72+
await this.#keys.set(`${account.toString()}-nsk_m`, masterNullifierSecretKey.toBuffer());
73+
74+
await this.#keys.set(`${account.toString()}-npk_m`, publicKeys.masterNullifierPublicKey.toBuffer());
75+
await this.#keys.set(`${account.toString()}-ivpk_m`, publicKeys.masterIncomingViewingPublicKey.toBuffer());
76+
await this.#keys.set(`${account.toString()}-ovpk_m`, publicKeys.masterOutgoingViewingPublicKey.toBuffer());
77+
await this.#keys.set(`${account.toString()}-tpk_m`, publicKeys.masterTaggingPublicKey.toBuffer());
78+
79+
// We store pk_m_hash under `account-{n/iv/ov/t}pk_m_hash` key to be able to obtain address and key prefix
80+
// using the #getKeyPrefixAndAccount function later on
81+
await this.#keys.set(`${account.toString()}-npk_m_hash`, masterNullifierPublicKeyHash.toBuffer());
82+
await this.#keys.set(`${account.toString()}-ivpk_m_hash`, masterIncomingViewingPublicKeyHash.toBuffer());
83+
await this.#keys.set(`${account.toString()}-ovpk_m_hash`, masterOutgoingViewingPublicKeyHash.toBuffer());
84+
await this.#keys.set(`${account.toString()}-tpk_m_hash`, masterTaggingPublicKeyHash.toBuffer());
85+
});
8086

8187
// At last, we return the newly derived account address
8288
return completeAddress;

yarn-project/p2p/src/services/data_store.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class KeyNotFoundError extends Error {
2424
}
2525

2626
export class AztecDatastore implements Datastore {
27+
#db: AztecAsyncKVStore;
2728
#memoryDatastore: Map<string, MemoryItem>;
2829
#dbDatastore: AztecAsyncMap<string, Uint8Array>;
2930

@@ -32,9 +33,9 @@ export class AztecDatastore implements Datastore {
3233
private maxMemoryItems: number;
3334

3435
constructor(db: AztecAsyncKVStore, { maxMemoryItems } = { maxMemoryItems: 50 }) {
36+
this.#db = db;
3537
this.#memoryDatastore = new Map();
3638
this.#dbDatastore = db.openMap('p2p_datastore');
37-
3839
this.maxMemoryItems = maxMemoryItems;
3940
}
4041

@@ -106,13 +107,15 @@ export class AztecDatastore implements Datastore {
106107
});
107108
},
108109
commit: async () => {
109-
for (const op of this.#batchOps) {
110-
if (op.type === 'put' && op.value) {
111-
await this.put(op.key, op.value);
112-
} else if (op.type === 'del') {
113-
await this.delete(op.key);
110+
await this.#db.transactionAsync(async () => {
111+
for (const op of this.#batchOps) {
112+
if (op.type === 'put' && op.value) {
113+
await this.put(op.key, op.value);
114+
} else if (op.type === 'del') {
115+
await this.delete(op.key);
116+
}
114117
}
115-
}
118+
});
116119
this.#batchOps = []; // Clear operations after commit
117120
},
118121
};

yarn-project/slasher/src/stores/payloads_store.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,13 @@ export class SlasherPayloadsStore {
118118

119119
public async incrementPayloadVotes(payloadAddress: EthAddress, round: bigint): Promise<bigint> {
120120
const key = this.getPayloadVotesKey(round, payloadAddress);
121-
const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
122-
const newVotes = currentVotes + 1n;
123-
await this.roundPayloadVotes.set(key, newVotes);
124-
return newVotes;
121+
let newVotes: bigint;
122+
await this.kvStore.transactionAsync(async () => {
123+
const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
124+
newVotes = currentVotes + 1n;
125+
await this.roundPayloadVotes.set(key, newVotes);
126+
});
127+
return newVotes!;
125128
}
126129

127130
public async addPayload(payload: SlashPayloadRound): Promise<void> {

0 commit comments

Comments
 (0)