Improve mock-server orphaned storage key check

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-08-24 17:18:17 +00:00
committed by GitHub
co-authored by trevor-signal
parent 892702d206
commit 714ae1c440
6 changed files with 70 additions and 80 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@signalapp/mock-server': minor
---
Make the storage item/manifest accessors synchronous so that writes can't interleave with reads
+32 -38
View File
@@ -117,11 +117,9 @@ export type Config = Readonly<{
modifyGroup: (options: ModifyGroupOptions) => Promise<ModifyGroupResult>;
waitForGroupUpdate: (group: GroupData) => Promise<void>;
getStorageManifest: () => Promise<Proto.StorageManifest.Params | undefined>;
getStorageItem: (
key: Buffer<ArrayBuffer>,
) => Promise<Buffer<ArrayBuffer> | undefined>;
getAllStorageKeys: () => Promise<Array<Buffer<ArrayBuffer>>>;
getStorageManifest: () => Proto.StorageManifest.Params | undefined;
getStorageItem: (key: Buffer<ArrayBuffer>) => Buffer<ArrayBuffer> | undefined;
getAllStorageKeys: () => Array<Buffer<ArrayBuffer>>;
waitForStorageManifest: (afterVersion?: bigint) => Promise<void>;
applyStorageWrite: (
operation: Proto.WriteOperation.Params,
@@ -1187,7 +1185,7 @@ export class PrimaryDevice {
}
public async getStorageState(): Promise<StorageState | undefined> {
const manifest = await this.config.getStorageManifest();
const manifest = this.config.getStorageManifest();
if (!manifest) {
return undefined;
}
@@ -1228,13 +1226,13 @@ export class PrimaryDevice {
}
public async getOrphanedStorageKeys(): Promise<Array<Buffer<ArrayBuffer>>> {
const manifest = await this.config.getStorageManifest();
const manifest = this.config.getStorageManifest();
if (!manifest) {
return [];
}
const state = await this.convertManifestToStorageState(manifest);
const keys = await this.config.getAllStorageKeys();
const state = this.convertManifestToStorageState(manifest);
const keys = this.config.getAllStorageKeys();
return keys.filter((key) => !state.hasKey(key));
}
@@ -2453,41 +2451,37 @@ export class PrimaryDevice {
);
}
private async convertManifestToStorageState(
private convertManifestToStorageState(
manifest: Proto.StorageManifest.Params,
): Promise<StorageState> {
): StorageState {
const decryptedManifest = decryptStorageManifest(this.storageKey, manifest);
assert(decryptedManifest.version, 'Consistency check');
const version = decryptedManifest.version;
const items = await Promise.all(
decryptedManifest.identifiers.map(async ({ type, raw: key }) => {
const keyBuffer = Buffer.from(key);
const item = await this.config.getStorageItem(keyBuffer);
if (!item) {
throw new Error(`Missing item ${keyBuffer.toString('base64')}`);
}
const items = decryptedManifest.identifiers.map(({ type, raw: key }) => {
const keyBuffer = Buffer.from(key);
const item = this.config.getStorageItem(keyBuffer);
if (!item) {
throw new Error(`Missing item ${keyBuffer.toString('base64')}`);
}
const decrypted = decryptStorageItem({
storageKey: this.storageKey,
recordIkm: this.storageRecordIkm,
item: {
key,
value: item,
},
});
if (!decrypted.record) {
throw new Error(
`Missing item record ${keyBuffer.toString('base64')}`,
);
}
return {
type: type as Proto.ManifestRecord.Identifier.Type,
key: keyBuffer,
record: decrypted.record,
};
}),
);
const decrypted = decryptStorageItem({
storageKey: this.storageKey,
recordIkm: this.storageRecordIkm,
item: {
key,
value: item,
},
});
if (!decrypted.record) {
throw new Error(`Missing item record ${keyBuffer.toString('base64')}`);
}
return {
type: type as Proto.ManifestRecord.Identifier.Type,
key: keyBuffer,
record: decrypted.record,
};
});
return new StorageState(version, items);
}
+2 -2
View File
@@ -856,10 +856,10 @@ export class Server extends BaseServer {
// Override `getStorageItems` to provide configurable limit for maximum
// storage read keys.
public override async getStorageItems(
public override getStorageItems(
device: Device,
keys: ReadonlyArray<Buffer<ArrayBuffer>>,
): Promise<Array<Proto.StorageItem.Params> | undefined> {
): Array<Proto.StorageItem.Params> | undefined {
if (
this.config.maxStorageReadKeys !== undefined &&
keys.length > this.config.maxStorageReadKeys
+25 -34
View File
@@ -1038,9 +1038,9 @@ export abstract class Server {
return auth.device;
}
public async getStorageManifest(
public getStorageManifest(
device: Device,
): Promise<Proto.StorageManifest.Params | undefined> {
): Proto.StorageManifest.Params | undefined {
return this.storageManifestByAci.get(device.aci);
}
@@ -1056,7 +1056,7 @@ export abstract class Server {
return { error: 'missing `writeOperation.manifest.version`' };
}
const existing = await this.getStorageManifest(device);
const existing = this.getStorageManifest(device);
if (existing) {
// Atomicity
assert(existing.version, 'consistency check');
@@ -1072,27 +1072,25 @@ export abstract class Server {
if (clearAll) {
debug('clearing storage items for=%j', device.debugId);
await this.clearStorageItems(device);
this.clearStorageItems(device);
}
const inserts = (insertItem ?? []).map(async (item) => {
for (const item of insertItem ?? []) {
assert(item.key instanceof Uint8Array, 'insertItem.key must be a Buffer');
assert(
item.value instanceof Uint8Array,
'insertItem.value must be a Buffer',
);
return this.setStorageItem(
this.setStorageItem(
device,
Buffer.from(item.key),
Buffer.from(item.value),
);
});
await Promise.all(inserts);
}
const deletes = (deleteKey ?? []).map(async (key) => {
return this.deleteStorageItem(device, Buffer.from(key));
});
await Promise.all(deletes);
for (const key of deleteKey ?? []) {
this.deleteStorageItem(device, Buffer.from(key));
}
debug(
'updating storage manifest to version=%d for=%j',
@@ -1108,15 +1106,15 @@ export abstract class Server {
return { updated: true };
}
private async clearStorageItems(device: Device): Promise<void> {
private clearStorageItems(device: Device): void {
this.storageItemsByAci.get(device.aci)?.clear();
}
private async setStorageItem(
private setStorageItem(
device: Device,
key: Buffer<ArrayBuffer>,
value: Buffer<ArrayBuffer>,
): Promise<void> {
): void {
let map = this.storageItemsByAci.get(device.aci);
if (!map) {
map = new Map();
@@ -1126,10 +1124,10 @@ export abstract class Server {
map.set(key.toString('hex'), value);
}
public async getStorageItem(
public getStorageItem(
device: Device,
key: Buffer<ArrayBuffer>,
): Promise<Buffer<ArrayBuffer> | undefined> {
): Buffer<ArrayBuffer> | undefined {
const map = this.storageItemsByAci.get(device.aci);
if (!map) {
return undefined;
@@ -1138,9 +1136,7 @@ export abstract class Server {
return map.get(key.toString('hex'));
}
public async getAllStorageKeys(
device: Device,
): Promise<Array<Buffer<ArrayBuffer>>> {
public getAllStorageKeys(device: Device): Array<Buffer<ArrayBuffer>> {
const map = this.storageItemsByAci.get(device.aci);
if (!map) {
return [];
@@ -1149,28 +1145,23 @@ export abstract class Server {
return Array.from(map.keys()).map((hex) => Buffer.from(hex, 'hex'));
}
public async getStorageItems(
public getStorageItems(
device: Device,
keys: ReadonlyArray<Buffer<ArrayBuffer>>,
): Promise<Array<Proto.StorageItem.Params> | undefined> {
): Array<Proto.StorageItem.Params> | undefined {
const result = new Array<Proto.StorageItem.Params>();
await Promise.all(
keys.map(async (key) => {
const value = await this.getStorageItem(device, key);
if (value !== undefined) {
result.push({ key, value });
}
}),
);
for (const key of keys) {
const value = this.getStorageItem(device, key);
if (value !== undefined) {
result.push({ key, value });
}
}
return result;
}
public async deleteStorageItem(
device: Device,
key: Buffer<ArrayBuffer>,
): Promise<void> {
public deleteStorageItem(device: Device, key: Buffer<ArrayBuffer>): void {
const map = this.storageItemsByAci.get(device.aci);
if (!map) {
return;
+3 -3
View File
@@ -848,7 +848,7 @@ export const createHandler = (
return;
}
const manifest = await server.getStorageManifest(device);
const manifest = server.getStorageManifest(device);
if (!manifest) {
return send(res, 404, { error: 'Manifest not found' });
}
@@ -866,7 +866,7 @@ export const createHandler = (
assert(req.params.after != null, 'Missing after param');
const after = BigInt(req.params.after);
const manifest = await server.getStorageManifest(device);
const manifest = server.getStorageManifest(device);
if (manifest === undefined) {
return send(res, 404);
}
@@ -912,7 +912,7 @@ export const createHandler = (
const keys = readOperation.readKey.map((key) => Buffer.from(key));
const items = await server.getStorageItems(device, keys);
const items = server.getStorageItems(device, keys);
if (!items) {
return send(res, 413, { error: 'Requested too many items' });
}
@@ -103,13 +103,13 @@ async function createPrimaryDevice(name: string): Promise<PrimaryDevice> {
async waitForGroupUpdate() {
throw new Error('Not implemented');
},
async getStorageManifest() {
getStorageManifest() {
throw new Error('Not implemented');
},
async getStorageItem() {
getStorageItem() {
throw new Error('Not implemented');
},
async getAllStorageKeys() {
getAllStorageKeys() {
throw new Error('Not implemented');
},
async waitForStorageManifest() {