Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions yarn-project/wallets/src/embedded/wallet_db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,10 @@ describe('WalletDB', () => {
const wrapMap = (map: AztecAsyncMap<string, Buffer>): AztecAsyncMap<string, Buffer> =>
new Proxy(map, {
get(target, prop) {
if (prop === 'set') {
return (key: string, value: Buffer) =>
shouldCrash(key) ? Promise.reject(new Error('simulated write failure')) : target.set(key, value);
if (prop === 'set' || prop === 'delete') {
const write = (target[prop] as (key: string, value?: Buffer) => Promise<void>).bind(target);
return (key: string, value?: Buffer) =>
shouldCrash(key) ? Promise.reject(new Error('simulated write failure')) : write(key, value);
}
const member = Reflect.get(target, prop);
return typeof member === 'function' ? member.bind(target) : member;
Expand Down Expand Up @@ -226,6 +227,34 @@ describe('WalletDB', () => {
expect(await dbAfterCrash.listAccounts()).toEqual([]);
expect(await store.openMap<string, Buffer>('aliases').getAsync('accounts:alice')).toBeUndefined();
});

it('deletes no account data when a write fails midway through deleteAccount', async () => {
const store = await openTmpStore('wallet-db-atomicity-test');
const db = new WalletDB(store, () => {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny thing: this db shadows the one from the outer describe. Could we name it seedDb or similar? Otherwise if the const ever gets dropped we'd seed the beforeEach store instead and the failure would point somewhere confusing.

const address = await AztecAddress.random();
const data = makeAccountData('schnorr', 'alice');
await db.storeAccount(address, data);

// Fail the alias delete, which happens after all the account entry deletes
const failingDb = new WalletDB(
crashingStore(store, key => key.startsWith('accounts:')),
() => {},
);
await expect(failingDb.deleteAccount(address)).rejects.toThrow('simulated write failure');

// Inspect the same underlying store with a fresh WalletDB: the account must be fully intact
const dbAfterCrash = new WalletDB(store, () => {});
const retrieved = await dbAfterCrash.retrieveAccount(address);
expect(retrieved.secretKey).toEqual(data.secretKey);
expect(retrieved.salt).toEqual(data.salt);
expect(retrieved.type).toEqual('schnorr');
expect(retrieved.signingKey).toEqual(data.signingKey.toBuffer());

const accounts = await dbAfterCrash.listAccounts();
expect(accounts).toHaveLength(1);
expect(accounts[0].alias).toEqual('alice');
expect(accounts[0].item.toString()).toEqual(address.toString());
});
});

describe('all account types', () => {
Expand Down
30 changes: 18 additions & 12 deletions yarn-project/wallets/src/embedded/wallet_db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,25 @@ export class WalletDB {
return addresses;
}

/**
* Deletes an account's stored data and its alias atomically. Deletion is local to this store;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not introduced here, but since the doc now promises we delete "its alias": #readAccountAliases is keyed by address, so if an account ever got a second alias we'd only delete one of them and the other would keep pointing at a deleted address. Is one-alias-per-account an invariant somewhere? If not, then maybe we could fix it or add a TODO? Just checking

* any state the PXE holds for the account is unaffected.
*/
async deleteAccount(address: AztecAddress) {
await Promise.all([
this.accounts.delete(accountKey('sk', address)),
this.accounts.delete(accountKey('salt', address)),
this.accounts.delete(accountKey('type', address)),
this.accounts.delete(accountKey('signingKey', address)),
]);
// Clean up alias if one exists
const aliasesByAddress = await this.#readAccountAliases();
const alias = aliasesByAddress.get(address.toString());
if (alias) {
await this.aliases.delete(`accounts:${alias}`);
}
await this.store.transactionAsync(async () => {
await Promise.all([
this.accounts.delete(accountKey('sk', address)),
this.accounts.delete(accountKey('salt', address)),
this.accounts.delete(accountKey('type', address)),
this.accounts.delete(accountKey('signingKey', address)),
]);
// Clean up alias if one exists
const aliasesByAddress = await this.#readAccountAliases();
Comment thread
nventuro marked this conversation as resolved.
const alias = aliasesByAddress.get(address.toString());
if (alias) {
await this.aliases.delete(`accounts:${alias}`);
}
});
}

async close() {
Expand Down
Loading