Skip to content

Commit 075eda3

Browse files
perf: Skip updating registry if signature has not changed (#3779)
Since verifying and updating the registry can be expensive on slower devices, we can store the current signature and only re-verify when the signature changes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Stores and persists the registry signature to skip verification and database updates when unchanged, updating controller logic and tests. > > - **Snaps Registry Controller (`json.ts`)**: > - Add `signature` to `SnapsRegistryState` and controller metadata (persisted, logged; not exposed to UI). > - Update flow: fetch `registry.json` and `signature.json`, parse signature; if `signature` matches state, skip verification/update and only refresh `lastUpdated`/`databaseUnavailable`. > - On change, verify using parsed `SignatureStruct` and persist new `database` and `signature`. > - Adjust `#verifySignature` to accept `SignatureStruct` (using `@metamask/superstruct` `Infer`). > - **Tests (`json.test.ts`)**: > - Add test ensuring update is skipped when signature unchanged; update metadata snapshots to include `signature`. > - **Dependencies**: > - Add `@metamask/superstruct`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8ff96dd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent dd973e4 commit 075eda3

File tree

4 files changed

+56
-4
lines changed

4 files changed

+56
-4
lines changed

packages/snaps-controllers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"@metamask/snaps-rpc-methods": "workspace:^",
9595
"@metamask/snaps-sdk": "workspace:^",
9696
"@metamask/snaps-utils": "workspace:^",
97+
"@metamask/superstruct": "^3.2.1",
9798
"@metamask/utils": "^11.8.1",
9899
"@xstate/fsm": "^2.0.0",
99100
"async-mutex": "^0.5.0",

packages/snaps-controllers/src/snaps/registry/json.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,27 @@ describe('JsonSnapsRegistry', () => {
617617
expect(fetchMock).toHaveBeenCalledTimes(2);
618618
});
619619

620+
it('skips update if the signature matches the existing one', async () => {
621+
const spy = jest.spyOn(globalThis.crypto.subtle, 'digest');
622+
623+
fetchMock
624+
.mockResponseOnce(JSON.stringify(MOCK_DATABASE))
625+
.mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE));
626+
627+
const { messenger } = getRegistry({
628+
state: {
629+
database: MOCK_DATABASE,
630+
signature: MOCK_SIGNATURE,
631+
lastUpdated: 0,
632+
databaseUnavailable: false,
633+
},
634+
});
635+
await messenger.call('SnapsRegistry:update');
636+
637+
expect(fetchMock).toHaveBeenCalledTimes(2);
638+
expect(spy).not.toHaveBeenCalled();
639+
});
640+
620641
it('does not fetch if a second call is made under the threshold', async () => {
621642
fetchMock
622643
.mockResponseOnce(JSON.stringify(MOCK_DATABASE))
@@ -667,6 +688,7 @@ describe('JsonSnapsRegistry', () => {
667688
"database": null,
668689
"databaseUnavailable": false,
669690
"lastUpdated": null,
691+
"signature": null,
670692
}
671693
`);
672694
});
@@ -681,6 +703,7 @@ describe('JsonSnapsRegistry', () => {
681703
"database": null,
682704
"databaseUnavailable": false,
683705
"lastUpdated": null,
706+
"signature": null,
684707
}
685708
`);
686709
});

packages/snaps-controllers/src/snaps/registry/json.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import type {
44
} from '@metamask/base-controller';
55
import { BaseController } from '@metamask/base-controller';
66
import type { Messenger } from '@metamask/messenger';
7-
import type { SnapsRegistryDatabase } from '@metamask/snaps-registry';
7+
import type {
8+
SnapsRegistryDatabase,
9+
SignatureStruct,
10+
} from '@metamask/snaps-registry';
811
import { verify } from '@metamask/snaps-registry';
912
import { getTargetVersion } from '@metamask/snaps-utils';
13+
import type { Infer } from '@metamask/superstruct';
1014
import type { Hex, SemVerRange, SemVerVersion } from '@metamask/utils';
1115
import {
1216
assert,
@@ -102,6 +106,7 @@ export type SnapsRegistryMessenger = Messenger<
102106

103107
export type SnapsRegistryState = {
104108
database: SnapsRegistryDatabase | null;
109+
signature: string | null;
105110
lastUpdated: number | null;
106111
databaseUnavailable: boolean;
107112
};
@@ -110,6 +115,7 @@ const controllerName = 'SnapsRegistry';
110115

111116
const defaultState = {
112117
database: null,
118+
signature: null,
113119
lastUpdated: null,
114120
databaseUnavailable: false,
115121
};
@@ -155,6 +161,12 @@ export class JsonSnapsRegistry extends BaseController<
155161
includeInDebugSnapshot: false,
156162
usedInUi: true,
157163
},
164+
signature: {
165+
includeInStateLogs: true,
166+
persist: true,
167+
includeInDebugSnapshot: true,
168+
usedInUi: false,
169+
},
158170
lastUpdated: {
159171
includeInStateLogs: true,
160172
persist: true,
@@ -244,12 +256,24 @@ export class JsonSnapsRegistry extends BaseController<
244256
this.#safeFetch(this.#url.signature),
245257
]);
246258

247-
await this.#verifySignature(database, signature);
259+
const signatureJson = JSON.parse(signature);
260+
261+
// If the signature matches the existing state, we can skip verification and don't need to update the database.
262+
if (signatureJson.signature === this.state.signature) {
263+
this.update((state) => {
264+
state.lastUpdated = Date.now();
265+
state.databaseUnavailable = false;
266+
});
267+
return;
268+
}
269+
270+
await this.#verifySignature(database, signatureJson);
248271

249272
this.update((state) => {
250273
state.database = JSON.parse(database);
251274
state.lastUpdated = Date.now();
252275
state.databaseUnavailable = false;
276+
state.signature = signatureJson.signature;
253277
});
254278
} catch {
255279
// Ignore
@@ -402,12 +426,15 @@ export class JsonSnapsRegistry extends BaseController<
402426
* @param signature - The signature of the registry.
403427
* @throws If the signature is invalid.
404428
*/
405-
async #verifySignature(database: string, signature: string) {
429+
async #verifySignature(
430+
database: string,
431+
signature: Infer<typeof SignatureStruct>,
432+
) {
406433
assert(this.#publicKey, 'No public key provided.');
407434

408435
const valid = await verify({
409436
registry: database,
410-
signature: JSON.parse(signature),
437+
signature,
411438
publicKey: this.#publicKey,
412439
});
413440

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4269,6 +4269,7 @@ __metadata:
42694269
"@metamask/snaps-rpc-methods": "workspace:^"
42704270
"@metamask/snaps-sdk": "workspace:^"
42714271
"@metamask/snaps-utils": "workspace:^"
4272+
"@metamask/superstruct": "npm:^3.2.1"
42724273
"@metamask/utils": "npm:^11.8.1"
42734274
"@noble/hashes": "npm:^1.7.1"
42744275
"@swc/core": "npm:1.11.31"

0 commit comments

Comments
 (0)