@aztec/archiver 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type EncryptedL2NoteLog,
2
3
  type FromLogType,
3
4
  type GetUnencryptedLogsResponse,
4
5
  type InboxLeaf,
@@ -136,6 +137,14 @@ export interface ArchiverDataStore {
136
137
  logType: TLogType,
137
138
  ): Promise<L2BlockL2Logs<FromLogType<TLogType>>[]>;
138
139
 
140
+ /**
141
+ * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
142
+ * @param tags - The tags to filter the logs by.
143
+ * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
144
+ * that tag.
145
+ */
146
+ getLogsByTags(tags: Fr[]): Promise<EncryptedL2NoteLog[][]>;
147
+
139
148
  /**
140
149
  * Gets unencrypted logs based on the provided filter.
141
150
  * @param filter - The filter to apply to the logs.
@@ -14,6 +14,7 @@ import {
14
14
  makeExecutablePrivateFunctionWithMembershipProof,
15
15
  makeUnconstrainedFunctionWithMembershipProof,
16
16
  } from '@aztec/circuits.js/testing';
17
+ import { toBufferBE } from '@aztec/foundation/bigint-buffer';
17
18
  import { times } from '@aztec/foundation/collection';
18
19
  import { randomBytes, randomInt } from '@aztec/foundation/crypto';
19
20
 
@@ -354,6 +355,123 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
354
355
  });
355
356
  });
356
357
 
358
+ describe('getLogsByTags', () => {
359
+ const txsPerBlock = 4;
360
+ const numPrivateFunctionCalls = 3;
361
+ const numNoteEncryptedLogs = 2;
362
+ const numBlocks = 10;
363
+ let blocks: L1Published<L2Block>[];
364
+ let tags: { [i: number]: { [j: number]: Buffer[] } } = {};
365
+
366
+ beforeEach(async () => {
367
+ blocks = times(numBlocks, (index: number) => ({
368
+ data: L2Block.random(index + 1, txsPerBlock, numPrivateFunctionCalls, 2, numNoteEncryptedLogs, 2),
369
+ l1: { blockNumber: BigInt(index), blockHash: `0x${index}`, timestamp: BigInt(index) },
370
+ }));
371
+ // Last block has the note encrypted log tags of the first tx copied from the previous block
372
+ blocks[numBlocks - 1].data.body.noteEncryptedLogs.txLogs[0].functionLogs.forEach((fnLogs, fnIndex) => {
373
+ fnLogs.logs.forEach((log, logIndex) => {
374
+ const previousLogData =
375
+ blocks[numBlocks - 2].data.body.noteEncryptedLogs.txLogs[0].functionLogs[fnIndex].logs[logIndex].data;
376
+ previousLogData.copy(log.data, 0, 0, 32);
377
+ });
378
+ });
379
+ // Last block has invalid tags in the second tx
380
+ const tooBig = toBufferBE(Fr.MODULUS, 32);
381
+ blocks[numBlocks - 1].data.body.noteEncryptedLogs.txLogs[1].functionLogs.forEach(fnLogs => {
382
+ fnLogs.logs.forEach(log => {
383
+ tooBig.copy(log.data, 0, 0, 32);
384
+ });
385
+ });
386
+
387
+ await store.addBlocks(blocks);
388
+ await store.addLogs(blocks.map(b => b.data));
389
+
390
+ tags = {};
391
+ blocks.forEach((b, blockIndex) => {
392
+ if (!tags[blockIndex]) {
393
+ tags[blockIndex] = {};
394
+ }
395
+ b.data.body.noteEncryptedLogs.txLogs.forEach((txLogs, txIndex) => {
396
+ if (!tags[blockIndex][txIndex]) {
397
+ tags[blockIndex][txIndex] = [];
398
+ }
399
+ tags[blockIndex][txIndex].push(...txLogs.unrollLogs().map(log => log.data.subarray(0, 32)));
400
+ });
401
+ });
402
+ });
403
+
404
+ it('is possible to batch request all logs of a tx via tags', async () => {
405
+ // get random tx from any block that's not the last one
406
+ const targetBlockIndex = randomInt(numBlocks - 2);
407
+ const targetTxIndex = randomInt(txsPerBlock);
408
+
409
+ const logsByTags = await store.getLogsByTags(
410
+ tags[targetBlockIndex][targetTxIndex].map(buffer => new Fr(buffer)),
411
+ );
412
+
413
+ const expectedResponseSize = numPrivateFunctionCalls * numNoteEncryptedLogs;
414
+ expect(logsByTags.length).toEqual(expectedResponseSize);
415
+
416
+ logsByTags.forEach((logsByTag, logIndex) => {
417
+ expect(logsByTag).toHaveLength(1);
418
+ const [log] = logsByTag;
419
+ expect(log).toEqual(
420
+ blocks[targetBlockIndex].data.body.noteEncryptedLogs.txLogs[targetTxIndex].unrollLogs()[logIndex],
421
+ );
422
+ });
423
+ });
424
+
425
+ it('is possible to batch request all logs of different blocks via tags', async () => {
426
+ // get first tx of first block and second tx of second block
427
+ const logsByTags = await store.getLogsByTags([...tags[0][0], ...tags[1][1]].map(buffer => new Fr(buffer)));
428
+
429
+ const expectedResponseSize = 2 * numPrivateFunctionCalls * numNoteEncryptedLogs;
430
+ expect(logsByTags.length).toEqual(expectedResponseSize);
431
+
432
+ logsByTags.forEach(logsByTag => expect(logsByTag).toHaveLength(1));
433
+ });
434
+
435
+ it('is possible to batch request logs that have the same tag but different content', async () => {
436
+ // get first tx of last block
437
+ const logsByTags = await store.getLogsByTags(tags[numBlocks - 1][0].map(buffer => new Fr(buffer)));
438
+
439
+ const expectedResponseSize = numPrivateFunctionCalls * numNoteEncryptedLogs;
440
+ expect(logsByTags.length).toEqual(expectedResponseSize);
441
+
442
+ logsByTags.forEach(logsByTag => {
443
+ expect(logsByTag).toHaveLength(2);
444
+ const [tag0, tag1] = logsByTag.map(log => new Fr(log.data.subarray(0, 32)));
445
+ expect(tag0).toEqual(tag1);
446
+ });
447
+ });
448
+
449
+ it('is possible to request logs for non-existing tags and determine their position', async () => {
450
+ // get random tx from any block that's not the last one
451
+ const targetBlockIndex = randomInt(numBlocks - 2);
452
+ const targetTxIndex = randomInt(txsPerBlock);
453
+
454
+ const logsByTags = await store.getLogsByTags([
455
+ Fr.random(),
456
+ ...tags[targetBlockIndex][targetTxIndex].slice(1).map(buffer => new Fr(buffer)),
457
+ ]);
458
+
459
+ const expectedResponseSize = numPrivateFunctionCalls * numNoteEncryptedLogs;
460
+ expect(logsByTags.length).toEqual(expectedResponseSize);
461
+
462
+ const [emptyLogsByTag, ...populatedLogsByTags] = logsByTags;
463
+ expect(emptyLogsByTag).toHaveLength(0);
464
+
465
+ populatedLogsByTags.forEach((logsByTag, logIndex) => {
466
+ expect(logsByTag).toHaveLength(1);
467
+ const [log] = logsByTag;
468
+ expect(log).toEqual(
469
+ blocks[targetBlockIndex].data.body.noteEncryptedLogs.txLogs[targetTxIndex].unrollLogs()[logIndex + 1],
470
+ );
471
+ });
472
+ });
473
+ });
474
+
357
475
  describe('getUnencryptedLogs', () => {
358
476
  const txsPerBlock = 4;
359
477
  const numPublicFunctionCalls = 3;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type EncryptedL2NoteLog,
2
3
  type FromLogType,
3
4
  type GetUnencryptedLogsResponse,
4
5
  type InboxLeaf,
@@ -239,6 +240,20 @@ export class KVArchiverDataStore implements ArchiverDataStore {
239
240
  }
240
241
  }
241
242
 
243
+ /**
244
+ * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
245
+ * @param tags - The tags to filter the logs by.
246
+ * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
247
+ * that tag.
248
+ */
249
+ getLogsByTags(tags: Fr[]): Promise<EncryptedL2NoteLog[][]> {
250
+ try {
251
+ return this.#logStore.getLogsByTags(tags);
252
+ } catch (err) {
253
+ return Promise.reject(err);
254
+ }
255
+ }
256
+
242
257
  /**
243
258
  * Gets unencrypted logs based on the provided filter.
244
259
  * @param filter - The filter to apply to the logs.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  EncryptedL2BlockL2Logs,
3
+ EncryptedL2NoteLog,
3
4
  EncryptedNoteL2BlockL2Logs,
4
5
  ExtendedUnencryptedL2Log,
5
6
  type FromLogType,
@@ -12,9 +13,10 @@ import {
12
13
  UnencryptedL2BlockL2Logs,
13
14
  type UnencryptedL2Log,
14
15
  } from '@aztec/circuit-types';
16
+ import { Fr } from '@aztec/circuits.js';
15
17
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js/constants';
16
18
  import { createDebugLogger } from '@aztec/foundation/log';
17
- import { type AztecKVStore, type AztecMap } from '@aztec/kv-store';
19
+ import { type AztecKVStore, type AztecMap, type AztecMultiMap } from '@aztec/kv-store';
18
20
 
19
21
  import { type BlockStore } from './block_store.js';
20
22
 
@@ -22,16 +24,22 @@ import { type BlockStore } from './block_store.js';
22
24
  * A store for logs
23
25
  */
24
26
  export class LogStore {
25
- #noteEncryptedLogs: AztecMap<number, Buffer>;
26
- #encryptedLogs: AztecMap<number, Buffer>;
27
- #unencryptedLogs: AztecMap<number, Buffer>;
27
+ #noteEncryptedLogsByBlock: AztecMap<number, Buffer>;
28
+ #noteEncryptedLogsByHash: AztecMap<string, Buffer>;
29
+ #noteEncryptedLogHashesByTag: AztecMultiMap<string, string>;
30
+ #noteEncryptedLogTagsByBlock: AztecMultiMap<number, string>;
31
+ #encryptedLogsByBlock: AztecMap<number, Buffer>;
32
+ #unencryptedLogsByBlock: AztecMap<number, Buffer>;
28
33
  #logsMaxPageSize: number;
29
34
  #log = createDebugLogger('aztec:archiver:log_store');
30
35
 
31
36
  constructor(private db: AztecKVStore, private blockStore: BlockStore, logsMaxPageSize: number = 1000) {
32
- this.#noteEncryptedLogs = db.openMap('archiver_note_encrypted_logs');
33
- this.#encryptedLogs = db.openMap('archiver_encrypted_logs');
34
- this.#unencryptedLogs = db.openMap('archiver_unencrypted_logs');
37
+ this.#noteEncryptedLogsByBlock = db.openMap('archiver_note_encrypted_logs_by_block');
38
+ this.#noteEncryptedLogsByHash = db.openMap('archiver_note_encrypted_logs_by_hash');
39
+ this.#noteEncryptedLogHashesByTag = db.openMultiMap('archiver_tagged_note_encrypted_log_hashes_by_tag');
40
+ this.#noteEncryptedLogTagsByBlock = db.openMultiMap('archiver_note_encrypted_log_tags_by_block');
41
+ this.#encryptedLogsByBlock = db.openMap('archiver_encrypted_logs_by_block');
42
+ this.#unencryptedLogsByBlock = db.openMap('archiver_unencrypted_logs_by_block');
35
43
 
36
44
  this.#logsMaxPageSize = logsMaxPageSize;
37
45
  }
@@ -44,21 +52,58 @@ export class LogStore {
44
52
  addLogs(blocks: L2Block[]): Promise<boolean> {
45
53
  return this.db.transaction(() => {
46
54
  blocks.forEach(block => {
47
- void this.#noteEncryptedLogs.set(block.number, block.body.noteEncryptedLogs.toBuffer());
48
- void this.#encryptedLogs.set(block.number, block.body.encryptedLogs.toBuffer());
49
- void this.#unencryptedLogs.set(block.number, block.body.unencryptedLogs.toBuffer());
55
+ void this.#noteEncryptedLogsByBlock.set(block.number, block.body.noteEncryptedLogs.toBuffer());
56
+ block.body.noteEncryptedLogs.txLogs.forEach(txLogs => {
57
+ const noteLogs = txLogs.unrollLogs();
58
+ noteLogs.forEach(noteLog => {
59
+ if (noteLog.data.length < 32) {
60
+ this.#log.warn(`Skipping note log with invalid data length: ${noteLog.data.length}`);
61
+ return;
62
+ }
63
+ try {
64
+ const tag = new Fr(noteLog.data.subarray(0, 32));
65
+ const hexHash = noteLog.hash().toString('hex');
66
+ // Ideally we'd store all of the logs for a matching tag in an AztecMultiMap, but this type doesn't doesn't
67
+ // handle storing buffers well. The 'ordered-binary' encoding returns an error trying to decode buffers
68
+ // ('the number <> cannot be converted to a BigInt because it is not an integer'). We therefore store
69
+ // instead the hashes of the logs.
70
+ void this.#noteEncryptedLogHashesByTag.set(tag.toString(), hexHash);
71
+ void this.#noteEncryptedLogsByHash.set(hexHash, noteLog.toBuffer());
72
+ void this.#noteEncryptedLogTagsByBlock.set(block.number, tag.toString());
73
+ } catch (err) {
74
+ this.#log.warn(`Failed to add tagged note log to store: ${err}`);
75
+ }
76
+ });
77
+ });
78
+ void this.#encryptedLogsByBlock.set(block.number, block.body.encryptedLogs.toBuffer());
79
+ void this.#unencryptedLogsByBlock.set(block.number, block.body.unencryptedLogs.toBuffer());
50
80
  });
51
81
 
52
82
  return true;
53
83
  });
54
84
  }
55
85
 
56
- deleteLogs(blocks: L2Block[]): Promise<boolean> {
86
+ async deleteLogs(blocks: L2Block[]): Promise<boolean> {
87
+ const noteTagsToDelete = await this.db.transaction(() => {
88
+ return blocks.flatMap(block => Array.from(this.#noteEncryptedLogTagsByBlock.getValues(block.number)));
89
+ });
90
+ const noteLogHashesToDelete = await this.db.transaction(() => {
91
+ return noteTagsToDelete.flatMap(tag => Array.from(this.#noteEncryptedLogHashesByTag.getValues(tag)));
92
+ });
57
93
  return this.db.transaction(() => {
58
94
  blocks.forEach(block => {
59
- void this.#noteEncryptedLogs.delete(block.number);
60
- void this.#encryptedLogs.delete(block.number);
61
- void this.#unencryptedLogs.delete(block.number);
95
+ void this.#noteEncryptedLogsByBlock.delete(block.number);
96
+ void this.#encryptedLogsByBlock.delete(block.number);
97
+ void this.#unencryptedLogsByBlock.delete(block.number);
98
+ void this.#noteEncryptedLogTagsByBlock.delete(block.number);
99
+ });
100
+
101
+ noteTagsToDelete.forEach(tag => {
102
+ void this.#noteEncryptedLogHashesByTag.delete(tag.toString());
103
+ });
104
+
105
+ noteLogHashesToDelete.forEach(hash => {
106
+ void this.#noteEncryptedLogsByHash.delete(hash);
62
107
  });
63
108
 
64
109
  return true;
@@ -80,12 +125,12 @@ export class LogStore {
80
125
  const logMap = (() => {
81
126
  switch (logType) {
82
127
  case LogType.ENCRYPTED:
83
- return this.#encryptedLogs;
128
+ return this.#encryptedLogsByBlock;
84
129
  case LogType.NOTEENCRYPTED:
85
- return this.#noteEncryptedLogs;
130
+ return this.#noteEncryptedLogsByBlock;
86
131
  case LogType.UNENCRYPTED:
87
132
  default:
88
- return this.#unencryptedLogs;
133
+ return this.#unencryptedLogsByBlock;
89
134
  }
90
135
  })();
91
136
  const logTypeMap = (() => {
@@ -105,6 +150,28 @@ export class LogStore {
105
150
  }
106
151
  }
107
152
 
153
+ /**
154
+ * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
155
+ * @param tags - The tags to filter the logs by.
156
+ * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
157
+ * that tag.
158
+ */
159
+ getLogsByTags(tags: Fr[]): Promise<EncryptedL2NoteLog[][]> {
160
+ return this.db.transaction(() => {
161
+ return tags.map(tag => {
162
+ const logHashes = Array.from(this.#noteEncryptedLogHashesByTag.getValues(tag.toString()));
163
+ return (
164
+ logHashes
165
+ .map(hash => this.#noteEncryptedLogsByHash.get(hash))
166
+ // addLogs should ensure that we never have undefined logs, but we filter them out regardless to protect
167
+ // ourselves from database corruption
168
+ .filter(noteLogBuffer => noteLogBuffer != undefined)
169
+ .map(noteLogBuffer => EncryptedL2NoteLog.fromBuffer(noteLogBuffer!))
170
+ );
171
+ });
172
+ });
173
+ }
174
+
108
175
  /**
109
176
  * Gets unencrypted logs based on the provided filter.
110
177
  * @param filter - The filter to apply to the logs.
@@ -154,7 +221,7 @@ export class LogStore {
154
221
  const logs: ExtendedUnencryptedL2Log[] = [];
155
222
 
156
223
  let maxLogsHit = false;
157
- loopOverBlocks: for (const [blockNumber, logBuffer] of this.#unencryptedLogs.entries({ start, end })) {
224
+ loopOverBlocks: for (const [blockNumber, logBuffer] of this.#unencryptedLogsByBlock.entries({ start, end })) {
158
225
  const unencryptedLogsInBlock = UnencryptedL2BlockL2Logs.fromBuffer(logBuffer);
159
226
  for (let txIndex = filter.afterLog?.txIndex ?? 0; txIndex < unencryptedLogsInBlock.txLogs.length; txIndex++) {
160
227
  const txLogs = unencryptedLogsInBlock.txLogs[txIndex].unrollLogs();
@@ -199,12 +266,12 @@ export class LogStore {
199
266
  const logMap = (() => {
200
267
  switch (logType) {
201
268
  case LogType.ENCRYPTED:
202
- return this.#encryptedLogs;
269
+ return this.#encryptedLogsByBlock;
203
270
  case LogType.NOTEENCRYPTED:
204
- return this.#noteEncryptedLogs;
271
+ return this.#noteEncryptedLogsByBlock;
205
272
  case LogType.UNENCRYPTED:
206
273
  default:
207
- return this.#unencryptedLogs;
274
+ return this.#unencryptedLogsByBlock;
208
275
  }
209
276
  })();
210
277
  const logTypeMap = (() => {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type EncryptedL2BlockL2Logs,
3
+ type EncryptedL2NoteLog,
3
4
  type EncryptedNoteL2BlockL2Logs,
4
5
  ExtendedUnencryptedL2Log,
5
6
  type FromLogType,
@@ -27,6 +28,7 @@ import {
27
28
  } from '@aztec/circuits.js';
28
29
  import { type ContractArtifact } from '@aztec/foundation/abi';
29
30
  import { type AztecAddress } from '@aztec/foundation/aztec-address';
31
+ import { createDebugLogger } from '@aztec/foundation/log';
30
32
 
31
33
  import { type ArchiverDataStore, type ArchiverL1SynchPoint } from '../archiver_store.js';
32
34
  import { type DataRetrieval } from '../structs/data_retrieval.js';
@@ -49,6 +51,10 @@ export class MemoryArchiverStore implements ArchiverDataStore {
49
51
 
50
52
  private noteEncryptedLogsPerBlock: Map<number, EncryptedNoteL2BlockL2Logs> = new Map();
51
53
 
54
+ private taggedNoteEncryptedLogs: Map<string, EncryptedL2NoteLog[]> = new Map();
55
+
56
+ private noteEncryptedLogTagsPerBlock: Map<number, Fr[]> = new Map();
57
+
52
58
  private encryptedLogsPerBlock: Map<number, EncryptedL2BlockL2Logs> = new Map();
53
59
 
54
60
  private unencryptedLogsPerBlock: Map<number, UnencryptedL2BlockL2Logs> = new Map();
@@ -74,6 +80,8 @@ export class MemoryArchiverStore implements ArchiverDataStore {
74
80
  private lastProvenL2BlockNumber: number = 0;
75
81
  private lastProvenL2EpochNumber: number = 0;
76
82
 
83
+ #log = createDebugLogger('aztec:archiver:data-store');
84
+
77
85
  constructor(
78
86
  /** The max number of logs that can be obtained in 1 "getUnencryptedLogs" call. */
79
87
  public readonly maxLogs: number,
@@ -206,6 +214,24 @@ export class MemoryArchiverStore implements ArchiverDataStore {
206
214
  addLogs(blocks: L2Block[]): Promise<boolean> {
207
215
  blocks.forEach(block => {
208
216
  this.noteEncryptedLogsPerBlock.set(block.number, block.body.noteEncryptedLogs);
217
+ block.body.noteEncryptedLogs.txLogs.forEach(txLogs => {
218
+ const noteLogs = txLogs.unrollLogs();
219
+ noteLogs.forEach(noteLog => {
220
+ if (noteLog.data.length < 32) {
221
+ this.#log.warn(`Skipping note log with invalid data length: ${noteLog.data.length}`);
222
+ return;
223
+ }
224
+ try {
225
+ const tag = new Fr(noteLog.data.subarray(0, 32));
226
+ const currentNoteLogs = this.taggedNoteEncryptedLogs.get(tag.toString()) || [];
227
+ this.taggedNoteEncryptedLogs.set(tag.toString(), [...currentNoteLogs, noteLog]);
228
+ const currentTagsInBlock = this.noteEncryptedLogTagsPerBlock.get(block.number) || [];
229
+ this.noteEncryptedLogTagsPerBlock.set(block.number, [...currentTagsInBlock, tag]);
230
+ } catch (err) {
231
+ this.#log.warn(`Failed to add tagged note log to store: ${err}`);
232
+ }
233
+ });
234
+ });
209
235
  this.encryptedLogsPerBlock.set(block.number, block.body.encryptedLogs);
210
236
  this.unencryptedLogsPerBlock.set(block.number, block.body.unencryptedLogs);
211
237
  });
@@ -213,10 +239,18 @@ export class MemoryArchiverStore implements ArchiverDataStore {
213
239
  }
214
240
 
215
241
  deleteLogs(blocks: L2Block[]): Promise<boolean> {
242
+ const noteTagsToDelete = blocks.flatMap(block => this.noteEncryptedLogTagsPerBlock.get(block.number));
243
+ noteTagsToDelete
244
+ .filter(tag => tag != undefined)
245
+ .forEach(tag => {
246
+ this.taggedNoteEncryptedLogs.delete(tag!.toString());
247
+ });
248
+
216
249
  blocks.forEach(block => {
217
250
  this.encryptedLogsPerBlock.delete(block.number);
218
251
  this.noteEncryptedLogsPerBlock.delete(block.number);
219
252
  this.unencryptedLogsPerBlock.delete(block.number);
253
+ this.noteEncryptedLogTagsPerBlock.delete(block.number);
220
254
  });
221
255
 
222
256
  return Promise.resolve(true);
@@ -380,6 +414,17 @@ export class MemoryArchiverStore implements ArchiverDataStore {
380
414
  return Promise.resolve(l);
381
415
  }
382
416
 
417
+ /**
418
+ * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
419
+ * @param tags - The tags to filter the logs by.
420
+ * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
421
+ * that tag.
422
+ */
423
+ getLogsByTags(tags: Fr[]): Promise<EncryptedL2NoteLog[][]> {
424
+ const noteLogs = tags.map(tag => this.taggedNoteEncryptedLogs.get(tag.toString()) || []);
425
+ return Promise.resolve(noteLogs);
426
+ }
427
+
383
428
  /**
384
429
  * Gets unencrypted logs based on the provided filter.
385
430
  * @param filter - The filter to apply to the logs.