@aztec/archiver 0.0.1-commit.86469d5 → 0.0.1-commit.8655d4a

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.
Files changed (132) hide show
  1. package/README.md +19 -11
  2. package/dest/archiver.d.ts +39 -17
  3. package/dest/archiver.d.ts.map +1 -1
  4. package/dest/archiver.js +260 -160
  5. package/dest/config.d.ts +6 -3
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +23 -15
  8. package/dest/errors.d.ts +55 -9
  9. package/dest/errors.d.ts.map +1 -1
  10. package/dest/errors.js +81 -14
  11. package/dest/factory.d.ts +13 -9
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +51 -37
  14. package/dest/index.d.ts +12 -3
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +11 -2
  17. package/dest/l1/bin/retrieve-calldata.js +36 -33
  18. package/dest/l1/calldata_retriever.d.ts +74 -50
  19. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  20. package/dest/l1/calldata_retriever.js +201 -260
  21. package/dest/l1/data_retrieval.d.ts +26 -17
  22. package/dest/l1/data_retrieval.d.ts.map +1 -1
  23. package/dest/l1/data_retrieval.js +42 -47
  24. package/dest/l1/spire_proposer.d.ts +5 -5
  25. package/dest/l1/spire_proposer.d.ts.map +1 -1
  26. package/dest/l1/spire_proposer.js +9 -17
  27. package/dest/l1/trace_tx.d.ts +12 -66
  28. package/dest/l1/trace_tx.d.ts.map +1 -1
  29. package/dest/l1/validate_historical_logs.d.ts +23 -0
  30. package/dest/l1/validate_historical_logs.d.ts.map +1 -0
  31. package/dest/l1/validate_historical_logs.js +108 -0
  32. package/dest/modules/contract_data_source_adapter.d.ts +25 -0
  33. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
  34. package/dest/modules/contract_data_source_adapter.js +40 -0
  35. package/dest/modules/data_source_base.d.ts +71 -42
  36. package/dest/modules/data_source_base.d.ts.map +1 -1
  37. package/dest/modules/data_source_base.js +270 -179
  38. package/dest/modules/data_store_updater.d.ts +49 -17
  39. package/dest/modules/data_store_updater.d.ts.map +1 -1
  40. package/dest/modules/data_store_updater.js +211 -121
  41. package/dest/modules/instrumentation.d.ts +21 -3
  42. package/dest/modules/instrumentation.d.ts.map +1 -1
  43. package/dest/modules/instrumentation.js +44 -9
  44. package/dest/modules/l1_synchronizer.d.ts +14 -12
  45. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  46. package/dest/modules/l1_synchronizer.js +443 -211
  47. package/dest/modules/validation.d.ts +4 -3
  48. package/dest/modules/validation.d.ts.map +1 -1
  49. package/dest/modules/validation.js +6 -6
  50. package/dest/store/block_store.d.ts +174 -66
  51. package/dest/store/block_store.d.ts.map +1 -1
  52. package/dest/store/block_store.js +743 -245
  53. package/dest/store/contract_class_store.d.ts +17 -4
  54. package/dest/store/contract_class_store.d.ts.map +1 -1
  55. package/dest/store/contract_class_store.js +24 -68
  56. package/dest/store/contract_instance_store.d.ts +28 -1
  57. package/dest/store/contract_instance_store.d.ts.map +1 -1
  58. package/dest/store/contract_instance_store.js +37 -2
  59. package/dest/store/data_stores.d.ts +68 -0
  60. package/dest/store/data_stores.d.ts.map +1 -0
  61. package/dest/store/data_stores.js +54 -0
  62. package/dest/store/function_names_cache.d.ts +17 -0
  63. package/dest/store/function_names_cache.d.ts.map +1 -0
  64. package/dest/store/function_names_cache.js +30 -0
  65. package/dest/store/l2_tips_cache.d.ts +25 -0
  66. package/dest/store/l2_tips_cache.d.ts.map +1 -0
  67. package/dest/store/l2_tips_cache.js +26 -0
  68. package/dest/store/log_store.d.ts +42 -37
  69. package/dest/store/log_store.d.ts.map +1 -1
  70. package/dest/store/log_store.js +262 -388
  71. package/dest/store/log_store_codec.d.ts +70 -0
  72. package/dest/store/log_store_codec.d.ts.map +1 -0
  73. package/dest/store/log_store_codec.js +101 -0
  74. package/dest/store/message_store.d.ts +11 -1
  75. package/dest/store/message_store.d.ts.map +1 -1
  76. package/dest/store/message_store.js +51 -9
  77. package/dest/test/fake_l1_state.d.ts +25 -1
  78. package/dest/test/fake_l1_state.d.ts.map +1 -1
  79. package/dest/test/fake_l1_state.js +166 -32
  80. package/dest/test/mock_archiver.d.ts +1 -1
  81. package/dest/test/mock_archiver.d.ts.map +1 -1
  82. package/dest/test/mock_archiver.js +3 -2
  83. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  84. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  85. package/dest/test/mock_l1_to_l2_message_source.js +2 -1
  86. package/dest/test/mock_l2_block_source.d.ts +62 -41
  87. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  88. package/dest/test/mock_l2_block_source.js +321 -202
  89. package/dest/test/mock_structs.d.ts +4 -1
  90. package/dest/test/mock_structs.d.ts.map +1 -1
  91. package/dest/test/mock_structs.js +13 -1
  92. package/dest/test/noop_l1_archiver.d.ts +12 -6
  93. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  94. package/dest/test/noop_l1_archiver.js +26 -9
  95. package/package.json +14 -14
  96. package/src/archiver.ts +319 -181
  97. package/src/config.ts +32 -12
  98. package/src/errors.ts +122 -21
  99. package/src/factory.ts +75 -36
  100. package/src/index.ts +19 -2
  101. package/src/l1/README.md +25 -68
  102. package/src/l1/bin/retrieve-calldata.ts +46 -39
  103. package/src/l1/calldata_retriever.ts +260 -379
  104. package/src/l1/data_retrieval.ts +58 -69
  105. package/src/l1/spire_proposer.ts +7 -15
  106. package/src/l1/validate_historical_logs.ts +140 -0
  107. package/src/modules/contract_data_source_adapter.ts +55 -0
  108. package/src/modules/data_source_base.ts +347 -221
  109. package/src/modules/data_store_updater.ts +248 -153
  110. package/src/modules/instrumentation.ts +56 -9
  111. package/src/modules/l1_synchronizer.ts +585 -258
  112. package/src/modules/validation.ts +10 -9
  113. package/src/store/block_store.ts +924 -300
  114. package/src/store/contract_class_store.ts +31 -103
  115. package/src/store/contract_instance_store.ts +51 -5
  116. package/src/store/data_stores.ts +104 -0
  117. package/src/store/function_names_cache.ts +37 -0
  118. package/src/store/l2_tips_cache.ts +35 -0
  119. package/src/store/log_store.ts +303 -499
  120. package/src/store/log_store_codec.ts +132 -0
  121. package/src/store/message_store.ts +60 -10
  122. package/src/structs/inbox_message.ts +1 -1
  123. package/src/test/fake_l1_state.ts +213 -42
  124. package/src/test/mock_archiver.ts +3 -2
  125. package/src/test/mock_l1_to_l2_message_source.ts +1 -0
  126. package/src/test/mock_l2_block_source.ts +394 -210
  127. package/src/test/mock_structs.ts +20 -6
  128. package/src/test/noop_l1_archiver.ts +39 -9
  129. package/dest/store/kv_archiver_store.d.ts +0 -340
  130. package/dest/store/kv_archiver_store.d.ts.map +0 -1
  131. package/dest/store/kv_archiver_store.js +0 -446
  132. package/src/store/kv_archiver_store.ts +0 -639
@@ -1,436 +1,310 @@
1
1
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
- import { BlockNumber } from '@aztec/foundation/branded-types';
3
- import { filterAsync } from '@aztec/foundation/collection';
4
- import { Fr } from '@aztec/foundation/curves/bn254';
5
2
  import { createLogger } from '@aztec/foundation/log';
6
- import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize';
7
- import { BlockHash } from '@aztec/stdlib/block';
8
3
  import { MAX_LOGS_PER_TAG } from '@aztec/stdlib/interfaces/api-limit';
9
- import { ContractClassLog, ExtendedContractClassLog, ExtendedPublicLog, LogId, PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs';
4
+ import { decodeKeyTail, decodeValue, encodeKey, encodePublicPrefix, encodeValue, endOfTagRange, endOfTxRange, fieldHex, incKey } from './log_store_codec.js';
10
5
  /**
11
- * A store for logs
6
+ * Indexes every emitted private and public log under a composite hex-string key
7
+ * `[contractAddress (public only)]-tag-blockNumber-txIndexWithinBlock-logIndexWithinTx`,
8
+ * where each numeric segment is zero-padded to 8 lowercase hex digits (4 bytes BE) and
9
+ * `contractAddress` / `tag` are the bare 64-hex-char field representations (no `0x` prefix). The
10
+ * fixed-width zero-padded hex segments sort lexicographically in the same order as the canonical
11
+ * `(contract, tag, blockNumber, txIndexWithinBlock, logIndexWithinTx)` tuple, so a single ordered
12
+ * range scan answers every {@link PrivateLogsQuery} / {@link PublicLogsQuery}.
13
+ *
14
+ * Per-block secondary indices (`#privateKeysByBlock`, `#publicKeysByBlock`) record the exact primary
15
+ * keys written for each block so {@link deleteLogs} can drop them on reorg without having to range
16
+ * scan by block (block isn't the leading key segment).
17
+ *
18
+ * Contract-class logs are no longer stored or served by the log store.
12
19
  */ export class LogStore {
13
20
  db;
14
21
  blockStore;
15
- // `tag` --> private logs
16
- #privateLogsByTag;
17
- // `{contractAddress}_${tag}` --> public logs
18
- #publicLogsByContractAndTag;
19
- #privateLogKeysByBlock;
20
- #publicLogKeysByBlock;
21
- #publicLogsByBlock;
22
- #contractClassLogsByBlock;
23
- #logsMaxPageSize;
22
+ genesisBlockHash;
23
+ /** Primary map: composite private key (tag + tail = 96 hex chars + separators) -> serialized {@link StoredLogValue}. */ #privateLogs;
24
+ /** Primary map: composite public key (contract + tag + tail) -> serialized {@link StoredLogValue}. */ #publicLogs;
25
+ /** Secondary deletion index: blockNumber -> the exact primary keys written for that block. */ #privateKeysByBlock;
26
+ #publicKeysByBlock;
24
27
  #log;
25
- constructor(db, blockStore, logsMaxPageSize = 1000){
28
+ /**
29
+ * @param genesisBlockHash - Hash of the synthetic genesis block. During early sync the PXE anchors to
30
+ * genesis and passes its hash as a query `referenceBlock`; since the archiver never indexes the
31
+ * genesis block, the store recognizes this hash directly and resolves it to the genesis block number
32
+ * rather than mistaking it for a reorg.
33
+ */ constructor(db, blockStore, genesisBlockHash){
26
34
  this.db = db;
27
35
  this.blockStore = blockStore;
36
+ this.genesisBlockHash = genesisBlockHash;
28
37
  this.#log = createLogger('archiver:log_store');
29
- this.#privateLogsByTag = db.openMap('archiver_private_tagged_logs_by_tag');
30
- this.#publicLogsByContractAndTag = db.openMap('archiver_public_tagged_logs_by_tag');
31
- this.#privateLogKeysByBlock = db.openMap('archiver_private_log_keys_by_block');
32
- this.#publicLogKeysByBlock = db.openMap('archiver_public_log_keys_by_block');
33
- this.#publicLogsByBlock = db.openMap('archiver_public_logs_by_block');
34
- this.#contractClassLogsByBlock = db.openMap('archiver_contract_class_logs_by_block');
35
- this.#logsMaxPageSize = logsMaxPageSize;
38
+ this.#privateLogs = db.openMap('archiver_private_logs');
39
+ this.#publicLogs = db.openMap('archiver_public_logs');
40
+ this.#privateKeysByBlock = db.openMap('archiver_private_log_keys_by_block');
41
+ this.#publicKeysByBlock = db.openMap('archiver_public_log_keys_by_block');
36
42
  }
37
43
  /**
38
- * Extracts tagged logs from a single block, grouping them into private and public maps.
44
+ * Indexes every emitted private and public log from the given blocks. Wraps the write in a single
45
+ * `db.transactionAsync` so the primary entries and the per-block secondary indices stay consistent.
39
46
  *
40
- * @param block - The L2 block to extract logs from.
41
- * @returns An object containing the private and public tagged logs for the block.
42
- */ #extractTaggedLogsFromBlock(block) {
43
- // SiloedTag (as string) -> array of log buffers.
44
- const privateTaggedLogs = new Map();
45
- // "{contractAddress}_{tag}" (as string) -> array of log buffers.
46
- const publicTaggedLogs = new Map();
47
- block.body.txEffects.forEach((txEffect)=>{
48
- const txHash = txEffect.txHash;
49
- txEffect.privateLogs.forEach((log)=>{
50
- // Private logs use SiloedTag (already siloed by kernel)
51
- const tag = log.fields[0];
52
- this.#log.debug(`Found private log with tag ${tag.toString()} in block ${block.number}`);
53
- const currentLogs = privateTaggedLogs.get(tag.toString()) ?? [];
54
- currentLogs.push(new TxScopedL2Log(txHash, block.number, block.timestamp, log.getEmittedFields(), txEffect.noteHashes, txEffect.nullifiers[0]).toBuffer());
55
- privateTaggedLogs.set(tag.toString(), currentLogs);
56
- });
57
- txEffect.publicLogs.forEach((log)=>{
58
- // Public logs use Tag directly (not siloed) and are stored with contract address
59
- const tag = log.fields[0];
60
- const contractAddress = log.contractAddress;
61
- const key = `${contractAddress.toString()}_${tag.toString()}`;
62
- this.#log.debug(`Found public log with tag ${tag.toString()} from contract ${contractAddress.toString()} in block ${block.number}`);
63
- const currentLogs = publicTaggedLogs.get(key) ?? [];
64
- currentLogs.push(new TxScopedL2Log(txHash, block.number, block.timestamp, log.getEmittedFields(), txEffect.noteHashes, txEffect.nullifiers[0]).toBuffer());
65
- publicTaggedLogs.set(key, currentLogs);
66
- });
67
- });
68
- return {
69
- privateTaggedLogs,
70
- publicTaggedLogs
71
- };
72
- }
73
- /**
74
- * Extracts and aggregates tagged logs from a list of blocks.
75
- * @param blocks - The blocks to extract logs from.
76
- * @returns A map from tag (as string) to an array of serialized private logs belonging to that tag, and a map from
77
- * "{contractAddress}_{tag}" (as string) to an array of serialized public logs belonging to that key.
78
- */ #extractTaggedLogs(blocks) {
79
- const taggedLogsInBlocks = blocks.map((block)=>this.#extractTaggedLogsFromBlock(block));
80
- // Now we merge the maps from each block into a single map.
81
- const privateTaggedLogs = taggedLogsInBlocks.reduce((acc, { privateTaggedLogs })=>{
82
- for (const [tag, logs] of privateTaggedLogs.entries()){
83
- const currentLogs = acc.get(tag) ?? [];
84
- acc.set(tag, currentLogs.concat(logs));
85
- }
86
- return acc;
87
- }, new Map());
88
- const publicTaggedLogs = taggedLogsInBlocks.reduce((acc, { publicTaggedLogs })=>{
89
- for (const [key, logs] of publicTaggedLogs.entries()){
90
- const currentLogs = acc.get(key) ?? [];
91
- acc.set(key, currentLogs.concat(logs));
92
- }
93
- return acc;
94
- }, new Map());
95
- return {
96
- privateTaggedLogs,
97
- publicTaggedLogs
98
- };
99
- }
100
- async #addPrivateLogs(blocks) {
101
- const newBlocks = await filterAsync(blocks, async (block)=>!await this.#privateLogKeysByBlock.hasAsync(block.number));
102
- const { privateTaggedLogs } = this.#extractTaggedLogs(newBlocks);
103
- const keysOfPrivateLogsToUpdate = Array.from(privateTaggedLogs.keys());
104
- const currentPrivateTaggedLogs = await Promise.all(keysOfPrivateLogsToUpdate.map(async (key)=>({
105
- tag: key,
106
- logBuffers: await this.#privateLogsByTag.getAsync(key)
107
- })));
108
- for (const taggedLogBuffer of currentPrivateTaggedLogs){
109
- if (taggedLogBuffer.logBuffers && taggedLogBuffer.logBuffers.length > 0) {
110
- privateTaggedLogs.set(taggedLogBuffer.tag, taggedLogBuffer.logBuffers.concat(privateTaggedLogs.get(taggedLogBuffer.tag)));
111
- }
112
- }
113
- for (const block of newBlocks){
114
- const privateTagsInBlock = [];
115
- for (const [tag, logs] of privateTaggedLogs.entries()){
116
- await this.#privateLogsByTag.set(tag, logs);
117
- privateTagsInBlock.push(tag);
118
- }
119
- await this.#privateLogKeysByBlock.set(block.number, privateTagsInBlock);
120
- }
121
- }
122
- async #addPublicLogs(blocks) {
123
- const newBlocks = await filterAsync(blocks, async (block)=>!await this.#publicLogKeysByBlock.hasAsync(block.number));
124
- const { publicTaggedLogs } = this.#extractTaggedLogs(newBlocks);
125
- const keysOfPublicLogsToUpdate = Array.from(publicTaggedLogs.keys());
126
- const currentPublicTaggedLogs = await Promise.all(keysOfPublicLogsToUpdate.map(async (key)=>({
127
- tag: key,
128
- logBuffers: await this.#publicLogsByContractAndTag.getAsync(key)
129
- })));
130
- for (const taggedLogBuffer of currentPublicTaggedLogs){
131
- if (taggedLogBuffer.logBuffers && taggedLogBuffer.logBuffers.length > 0) {
132
- publicTaggedLogs.set(taggedLogBuffer.tag, taggedLogBuffer.logBuffers.concat(publicTaggedLogs.get(taggedLogBuffer.tag)));
133
- }
134
- }
135
- for (const block of newBlocks){
136
- const blockHash = await block.hash();
137
- const publicTagsInBlock = [];
138
- for (const [tag, logs] of publicTaggedLogs.entries()){
139
- await this.#publicLogsByContractAndTag.set(tag, logs);
140
- publicTagsInBlock.push(tag);
141
- }
142
- await this.#publicLogKeysByBlock.set(block.number, publicTagsInBlock);
143
- const publicLogsInBlock = block.body.txEffects.map((txEffect, txIndex)=>[
144
- numToUInt32BE(txIndex),
145
- numToUInt32BE(txEffect.publicLogs.length),
146
- txEffect.publicLogs.map((log)=>log.toBuffer())
147
- ].flat()).flat();
148
- await this.#publicLogsByBlock.set(block.number, this.#packWithBlockHash(blockHash, publicLogsInBlock));
149
- }
150
- }
151
- async #addContractClassLogs(blocks) {
152
- const newBlocks = await filterAsync(blocks, async (block)=>!await this.#contractClassLogsByBlock.hasAsync(block.number));
153
- for (const block of newBlocks){
154
- const blockHash = await block.hash();
155
- const contractClassLogsInBlock = block.body.txEffects.map((txEffect, txIndex)=>[
156
- numToUInt32BE(txIndex),
157
- numToUInt32BE(txEffect.contractClassLogs.length),
158
- txEffect.contractClassLogs.map((log)=>log.toBuffer())
159
- ].flat()).flat();
160
- await this.#contractClassLogsByBlock.set(block.number, this.#packWithBlockHash(blockHash, contractClassLogsInBlock));
161
- }
162
- }
163
- /**
164
- * Append new logs to the store's list.
165
- * @param blocks - The blocks for which to add the logs.
166
- * @returns True if the operation is successful.
47
+ * A block is only ever added once; on reorg the archiver calls {@link deleteLogs} first, so we write
48
+ * the secondary index entries with a plain `set` (overwrite) rather than read-modify-append.
167
49
  */ addLogs(blocks) {
168
50
  return this.db.transactionAsync(async ()=>{
169
- await Promise.all([
170
- this.#addPrivateLogs(blocks),
171
- this.#addPublicLogs(blocks),
172
- this.#addContractClassLogs(blocks)
173
- ]);
51
+ for (const block of blocks){
52
+ const blockHash = await block.hash();
53
+ const blockNumber = block.number;
54
+ const blockTimestamp = block.timestamp;
55
+ const privateKeys = [];
56
+ const privateValues = [];
57
+ const publicKeys = [];
58
+ const publicValues = [];
59
+ for(let txIndexWithinBlock = 0; txIndexWithinBlock < block.body.txEffects.length; txIndexWithinBlock++){
60
+ const txEffect = block.body.txEffects[txIndexWithinBlock];
61
+ const txHash = txEffect.txHash;
62
+ // Private and public log indices are counted independently per tx, each starting at 0.
63
+ let privateLogIndexWithinTx = 0;
64
+ let publicLogIndexWithinTx = 0;
65
+ for (const log of txEffect.privateLogs){
66
+ const tagHex = fieldHex(log.fields[0]);
67
+ const key = encodeKey(tagHex, blockNumber, txIndexWithinBlock, privateLogIndexWithinTx);
68
+ const value = encodeValue({
69
+ txHash,
70
+ blockHash,
71
+ blockTimestamp,
72
+ logData: log.getEmittedFields()
73
+ });
74
+ privateKeys.push(key);
75
+ privateValues.push(value);
76
+ privateLogIndexWithinTx++;
77
+ }
78
+ for (const log of txEffect.publicLogs){
79
+ const contractHex = fieldHex(log.contractAddress);
80
+ const tagHex = fieldHex(log.fields[0]);
81
+ const key = encodeKey(encodePublicPrefix(contractHex, tagHex), blockNumber, txIndexWithinBlock, publicLogIndexWithinTx);
82
+ const value = encodeValue({
83
+ txHash,
84
+ blockHash,
85
+ blockTimestamp,
86
+ logData: log.getEmittedFields()
87
+ });
88
+ publicKeys.push(key);
89
+ publicValues.push(value);
90
+ publicLogIndexWithinTx++;
91
+ }
92
+ }
93
+ for(let i = 0; i < privateKeys.length; i++){
94
+ await this.#privateLogs.set(privateKeys[i], privateValues[i]);
95
+ }
96
+ for(let i = 0; i < publicKeys.length; i++){
97
+ await this.#publicLogs.set(publicKeys[i], publicValues[i]);
98
+ }
99
+ await this.#privateKeysByBlock.set(blockNumber, privateKeys);
100
+ await this.#publicKeysByBlock.set(blockNumber, publicKeys);
101
+ this.#log.debug(`Indexed logs for block ${blockNumber}`, {
102
+ blockNumber,
103
+ privateCount: privateKeys.length,
104
+ publicCount: publicKeys.length
105
+ });
106
+ }
174
107
  return true;
175
108
  });
176
109
  }
177
- #packWithBlockHash(blockHash, data) {
178
- return Buffer.concat([
179
- blockHash.toBuffer(),
180
- ...data
181
- ]);
182
- }
183
- #unpackBlockHash(reader) {
184
- const blockHash = reader.remainingBytes() > 0 ? reader.readObject(Fr) : undefined;
185
- if (!blockHash) {
186
- throw new Error('Failed to read block hash from log entry buffer');
187
- }
188
- return new BlockHash(blockHash);
189
- }
190
- deleteLogs(blocks) {
110
+ /**
111
+ * Deletes every log indexed under any of the given blocks. Secondary-index driven, so it doesn't
112
+ * have to range-scan the primary maps.
113
+ */ deleteLogs(blocks) {
191
114
  return this.db.transactionAsync(async ()=>{
192
- await Promise.all(blocks.map(async (block)=>{
193
- // Delete private logs
194
- const privateKeys = await this.#privateLogKeysByBlock.getAsync(block.number) ?? [];
195
- await Promise.all(privateKeys.map((tag)=>this.#privateLogsByTag.delete(tag)));
196
- // Delete public logs
197
- const publicKeys = await this.#publicLogKeysByBlock.getAsync(block.number) ?? [];
198
- await Promise.all(publicKeys.map((key)=>this.#publicLogsByContractAndTag.delete(key)));
199
- }));
200
- await Promise.all(blocks.map((block)=>Promise.all([
201
- this.#publicLogsByBlock.delete(block.number),
202
- this.#privateLogKeysByBlock.delete(block.number),
203
- this.#publicLogKeysByBlock.delete(block.number),
204
- this.#contractClassLogsByBlock.delete(block.number)
205
- ])));
115
+ for (const block of blocks){
116
+ const blockNumber = block.number;
117
+ const [privateKeys, publicKeys] = await Promise.all([
118
+ this.#privateKeysByBlock.getAsync(blockNumber),
119
+ this.#publicKeysByBlock.getAsync(blockNumber)
120
+ ]);
121
+ if (privateKeys) {
122
+ for (const key of privateKeys){
123
+ await this.#privateLogs.delete(key);
124
+ }
125
+ await this.#privateKeysByBlock.delete(blockNumber);
126
+ }
127
+ if (publicKeys) {
128
+ for (const key of publicKeys){
129
+ await this.#publicLogs.delete(key);
130
+ }
131
+ await this.#publicKeysByBlock.delete(blockNumber);
132
+ }
133
+ }
206
134
  return true;
207
135
  });
208
136
  }
209
- /**
210
- * Gets private logs that match any of the `tags`. For each tag, an array of matching logs is returned. An empty
211
- * array implies no logs match that tag.
212
- * @param tags - The tags to search for.
213
- * @param page - The page number (0-indexed) for pagination.
214
- * @returns An array of log arrays, one per tag. Returns at most MAX_LOGS_PER_TAG logs per tag per page. If
215
- * MAX_LOGS_PER_TAG logs are returned for a tag, the caller should fetch the next page to check for more logs.
216
- */ async getPrivateLogsByTags(tags, page = 0) {
217
- const logs = await Promise.all(tags.map((tag)=>this.#privateLogsByTag.getAsync(tag.toString())));
218
- const start = page * MAX_LOGS_PER_TAG;
219
- const end = start + MAX_LOGS_PER_TAG;
220
- return logs.map((logBuffers)=>logBuffers?.slice(start, end).map((logBuffer)=>TxScopedL2Log.fromBuffer(logBuffer)) ?? []);
137
+ /** Returns one inner array per element of `query.tags`, in input order. */ getPrivateLogsByTags(query) {
138
+ LogStore.#validateQuery(query);
139
+ return this.db.transactionAsync(()=>this.#runQuery(query, /* contractHex */ undefined));
221
140
  }
222
- /**
223
- * Gets public logs that match any of the `tags` from the specified contract. For each tag, an array of matching
224
- * logs is returned. An empty array implies no logs match that tag.
225
- * @param contractAddress - The contract address to search logs for.
226
- * @param tags - The tags to search for.
227
- * @param page - The page number (0-indexed) for pagination.
228
- * @returns An array of log arrays, one per tag. Returns at most MAX_LOGS_PER_TAG logs per tag per page. If
229
- * MAX_LOGS_PER_TAG logs are returned for a tag, the caller should fetch the next page to check for more logs.
230
- */ async getPublicLogsByTagsFromContract(contractAddress, tags, page = 0) {
231
- const logs = await Promise.all(tags.map((tag)=>{
232
- const key = `${contractAddress.toString()}_${tag.value.toString()}`;
233
- return this.#publicLogsByContractAndTag.getAsync(key);
234
- }));
235
- const start = page * MAX_LOGS_PER_TAG;
236
- const end = start + MAX_LOGS_PER_TAG;
237
- return logs.map((logBuffers)=>logBuffers?.slice(start, end).map((logBuffer)=>TxScopedL2Log.fromBuffer(logBuffer)) ?? []);
141
+ /** Returns one inner array per element of `query.tags`, in input order. */ getPublicLogsByTags(query) {
142
+ LogStore.#validateQuery(query);
143
+ return this.db.transactionAsync(()=>this.#runQuery(query, fieldHex(query.contractAddress)));
238
144
  }
239
- /**
240
- * Gets public logs based on the provided filter.
241
- * @param filter - The filter to apply to the logs.
242
- * @returns The requested logs.
243
- */ getPublicLogs(filter) {
244
- if (filter.afterLog) {
245
- return this.#filterPublicLogsBetweenBlocks(filter);
246
- } else if (filter.txHash) {
247
- return this.#filterPublicLogsOfTx(filter);
248
- } else {
249
- return this.#filterPublicLogsBetweenBlocks(filter);
145
+ static #validateQuery(query) {
146
+ if (query.txHash !== undefined && (query.fromBlock !== undefined || query.toBlock !== undefined)) {
147
+ throw new Error('`txHash` is mutually exclusive with `fromBlock`/`toBlock`');
250
148
  }
251
149
  }
252
- async #filterPublicLogsOfTx(filter) {
253
- if (!filter.txHash) {
254
- throw new Error('Missing txHash');
150
+ async #runQuery(query, contractHex) {
151
+ const isPublic = contractHex !== undefined;
152
+ const tags = query.tags ?? [];
153
+ const primaryMap = isPublic ? this.#publicLogs : this.#privateLogs;
154
+ // referenceBlock reorg check, in-transaction, against the same db the log primary maps live on. The
155
+ // genesis block is a valid anchor during early sync but is synthetic and never indexed in the block
156
+ // store, so resolve it directly to the genesis block number rather than mistaking it for a reorg.
157
+ let referenceBlockNumber;
158
+ if (query.referenceBlock) {
159
+ if (query.referenceBlock.equals(this.genesisBlockHash)) {
160
+ referenceBlockNumber = INITIAL_L2_BLOCK_NUM - 1;
161
+ } else {
162
+ const refBlk = await this.blockStore.getBlockData({
163
+ hash: query.referenceBlock
164
+ });
165
+ if (!refBlk) {
166
+ throw new Error(`Reference block ${query.referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`);
167
+ }
168
+ referenceBlockNumber = refBlk.header.globalVariables.blockNumber;
169
+ }
255
170
  }
256
- const [blockNumber, txIndex] = await this.blockStore.getTxLocation(filter.txHash) ?? [];
257
- if (typeof blockNumber !== 'number' || typeof txIndex !== 'number') {
258
- return {
259
- logs: [],
260
- maxLogsHit: false
261
- };
171
+ // Compute the exclusive upper-block bound across `toBlock` and `referenceBlock`.
172
+ // `toBlock` is already exclusive; `referenceBlock` caps inclusively, so its exclusive form is +1.
173
+ let upperExclusive;
174
+ if (query.toBlock !== undefined) {
175
+ upperExclusive = query.toBlock;
262
176
  }
263
- const buffer = await this.#publicLogsByBlock.getAsync(blockNumber) ?? Buffer.alloc(0);
264
- const publicLogsInBlock = [
265
- []
266
- ];
267
- const reader = new BufferReader(buffer);
268
- const blockHash = this.#unpackBlockHash(reader);
269
- while(reader.remainingBytes() > 0){
270
- const indexOfTx = reader.readNumber();
271
- const numLogsInTx = reader.readNumber();
272
- publicLogsInBlock[indexOfTx] = [];
273
- for(let i = 0; i < numLogsInTx; i++){
274
- publicLogsInBlock[indexOfTx].push(reader.readObject(PublicLog));
177
+ if (referenceBlockNumber !== undefined) {
178
+ const refExclusive = referenceBlockNumber + 1;
179
+ upperExclusive = upperExclusive === undefined ? refExclusive : Math.min(upperExclusive, refExclusive);
180
+ }
181
+ // Resolve txHash -> (blockNumber, txIndexInBlock) once for the whole query.
182
+ let txLocation;
183
+ if (query.txHash) {
184
+ const loc = await this.blockStore.getTxLocation(query.txHash);
185
+ if (!loc) {
186
+ return tags.map(()=>[]);
187
+ }
188
+ txLocation = loc;
189
+ if (upperExclusive !== undefined && txLocation[0] >= upperExclusive) {
190
+ return tags.map(()=>[]);
275
191
  }
276
192
  }
277
- const txLogs = publicLogsInBlock[txIndex];
278
- const logs = [];
279
- const maxLogsHit = this.#accumulateLogs(logs, blockNumber, blockHash, txIndex, txLogs, filter);
280
- return {
281
- logs,
282
- maxLogsHit
283
- };
284
- }
285
- async #filterPublicLogsBetweenBlocks(filter) {
286
- const start = filter.afterLog?.blockNumber ?? Math.max(filter.fromBlock ?? INITIAL_L2_BLOCK_NUM, INITIAL_L2_BLOCK_NUM);
287
- const end = filter.toBlock;
288
- if (typeof end === 'number' && end < start) {
289
- return {
290
- logs: [],
291
- maxLogsHit: true
292
- };
193
+ const fromBlock = query.fromBlock ?? INITIAL_L2_BLOCK_NUM;
194
+ const includeEffects = query.includeEffects === true;
195
+ const perTagResults = [];
196
+ for (const tagEntry of tags){
197
+ const { tagHex, afterLog } = normalizeTagEntry(tagEntry);
198
+ const prefix = contractHex !== undefined ? encodePublicPrefix(contractHex, tagHex) : tagHex;
199
+ const end = txLocation ? endOfTxRange(prefix, txLocation[0], txLocation[1]) : endOfTagRange(prefix, upperExclusive);
200
+ let start;
201
+ if (afterLog) {
202
+ // Cursor wins as the start; `fromBlock` is ignored (fine if the cursor sits below it). The cursor
203
+ // carries `(blockNumber, txIndexWithinBlock, logIndexWithinTx)`, which slot directly into the
204
+ // composite key no tx-hash lookup needed.
205
+ start = incKey(encodeKey(prefix, afterLog.blockNumber, afterLog.txIndexWithinBlock, afterLog.logIndexWithinTx));
206
+ } else if (txLocation) {
207
+ start = encodeKey(prefix, txLocation[0], txLocation[1], 0);
208
+ } else {
209
+ start = encodeKey(prefix, fromBlock, 0, 0);
210
+ }
211
+ const limit = query.limitPerTag ?? MAX_LOGS_PER_TAG;
212
+ const out = [];
213
+ for await (const [rawKey, rawVal] of primaryMap.entriesAsync({
214
+ start,
215
+ end,
216
+ limit
217
+ })){
218
+ const tail = decodeKeyTail(rawKey);
219
+ const value = decodeValue(rawVal);
220
+ out.push({
221
+ logData: value.logData,
222
+ blockNumber: tail.blockNumber,
223
+ blockHash: value.blockHash,
224
+ blockTimestamp: value.blockTimestamp,
225
+ txHash: value.txHash,
226
+ txIndexWithinBlock: tail.txIndexWithinBlock,
227
+ logIndexWithinTx: tail.logIndexWithinTx
228
+ });
229
+ }
230
+ perTagResults.push(out);
293
231
  }
294
- const logs = [];
295
- let maxLogsHit = false;
296
- loopOverBlocks: for await (const [blockNumber, logBuffer] of this.#publicLogsByBlock.entriesAsync({
297
- start,
298
- end
299
- })){
300
- const publicLogsInBlock = [
301
- []
302
- ];
303
- const reader = new BufferReader(logBuffer);
304
- const blockHash = this.#unpackBlockHash(reader);
305
- while(reader.remainingBytes() > 0){
306
- const indexOfTx = reader.readNumber();
307
- const numLogsInTx = reader.readNumber();
308
- publicLogsInBlock[indexOfTx] = [];
309
- for(let i = 0; i < numLogsInTx; i++){
310
- publicLogsInBlock[indexOfTx].push(reader.readObject(PublicLog));
232
+ if (includeEffects) {
233
+ // Dedupe by txHash across the entire page so a tx with many tagged logs costs one fetch.
234
+ const txHashByKey = new Map();
235
+ for (const arr of perTagResults){
236
+ for (const log of arr){
237
+ txHashByKey.set(log.txHash.toString(), log.txHash);
311
238
  }
312
239
  }
313
- for(let txIndex = filter.afterLog?.txIndex ?? 0; txIndex < publicLogsInBlock.length; txIndex++){
314
- const txLogs = publicLogsInBlock[txIndex];
315
- maxLogsHit = this.#accumulateLogs(logs, blockNumber, blockHash, txIndex, txLogs, filter);
316
- if (maxLogsHit) {
317
- this.#log.debug(`Max logs hit at block ${blockNumber}`);
318
- break loopOverBlocks;
240
+ const uniqueTxs = Array.from(txHashByKey.values());
241
+ if (uniqueTxs.length > 0) {
242
+ const effects = await this.blockStore.getNoteHashesAndNullifiers(uniqueTxs);
243
+ const byTxHash = new Map();
244
+ uniqueTxs.forEach((tx, i)=>byTxHash.set(tx.toString(), effects[i]));
245
+ for(let i = 0; i < perTagResults.length; i++){
246
+ perTagResults[i] = perTagResults[i].map((log)=>{
247
+ const [noteHashes, nullifiers] = byTxHash.get(log.txHash.toString()) ?? [
248
+ [],
249
+ []
250
+ ];
251
+ return {
252
+ ...log,
253
+ noteHashes,
254
+ nullifiers
255
+ };
256
+ });
319
257
  }
320
258
  }
321
259
  }
322
- return {
323
- logs,
324
- maxLogsHit
325
- };
260
+ return perTagResults;
326
261
  }
327
262
  /**
328
- * Gets contract class logs based on the provided filter.
329
- * @param filter - The filter to apply to the logs.
330
- * @returns The requested logs.
331
- */ getContractClassLogs(filter) {
332
- if (filter.afterLog) {
333
- return this.#filterContractClassLogsBetweenBlocks(filter);
334
- } else if (filter.txHash) {
335
- return this.#filterContractClassLogsOfTx(filter);
336
- } else {
337
- return this.#filterContractClassLogsBetweenBlocks(filter);
338
- }
263
+ * Reads back every private log indexed for the given block via the per-block secondary index. Order
264
+ * matches the canonical composite-key order (`tag`, `blockNumber`, `txIndexWithinBlock`,
265
+ * `logIndexWithinTx`). Used by the data-store-updater test suite to verify the indexed-vs-block-body
266
+ * counts without depending on the removed `getPublicLogs(LogFilter)` API.
267
+ */ getPrivateLogsForBlock(blockNumber) {
268
+ return this.db.transactionAsync(()=>this.#readBlockLogs(this.#privateKeysByBlock, this.#privateLogs, blockNumber));
339
269
  }
340
- async #filterContractClassLogsOfTx(filter) {
341
- if (!filter.txHash) {
342
- throw new Error('Missing txHash');
343
- }
344
- const [blockNumber, txIndex] = await this.blockStore.getTxLocation(filter.txHash) ?? [];
345
- if (typeof blockNumber !== 'number' || typeof txIndex !== 'number') {
346
- return {
347
- logs: [],
348
- maxLogsHit: false
349
- };
350
- }
351
- const contractClassLogsBuffer = await this.#contractClassLogsByBlock.getAsync(blockNumber) ?? Buffer.alloc(0);
352
- const contractClassLogsInBlock = [
353
- []
354
- ];
355
- const reader = new BufferReader(contractClassLogsBuffer);
356
- const blockHash = this.#unpackBlockHash(reader);
357
- while(reader.remainingBytes() > 0){
358
- const indexOfTx = reader.readNumber();
359
- const numLogsInTx = reader.readNumber();
360
- contractClassLogsInBlock[indexOfTx] = [];
361
- for(let i = 0; i < numLogsInTx; i++){
362
- contractClassLogsInBlock[indexOfTx].push(reader.readObject(ContractClassLog));
363
- }
364
- }
365
- const txLogs = contractClassLogsInBlock[txIndex];
366
- const logs = [];
367
- const maxLogsHit = this.#accumulateLogs(logs, blockNumber, blockHash, txIndex, txLogs, filter);
368
- return {
369
- logs,
370
- maxLogsHit
371
- };
270
+ /** {@inheritDoc LogStore.getPrivateLogsForBlock} */ getPublicLogsForBlock(blockNumber) {
271
+ return this.db.transactionAsync(()=>this.#readBlockLogs(this.#publicKeysByBlock, this.#publicLogs, blockNumber));
372
272
  }
373
- async #filterContractClassLogsBetweenBlocks(filter) {
374
- const start = filter.afterLog?.blockNumber ?? Math.max(filter.fromBlock ?? INITIAL_L2_BLOCK_NUM, INITIAL_L2_BLOCK_NUM);
375
- const end = filter.toBlock;
376
- if (typeof end === 'number' && end < start) {
377
- return {
378
- logs: [],
379
- maxLogsHit: true
380
- };
273
+ async #readBlockLogs(keysByBlock, primaryMap, blockNumber) {
274
+ const keys = await keysByBlock.getAsync(blockNumber);
275
+ if (!keys || keys.length === 0) {
276
+ return [];
381
277
  }
382
- const logs = [];
383
- let maxLogsHit = false;
384
- loopOverBlocks: for await (const [blockNumber, logBuffer] of this.#contractClassLogsByBlock.entriesAsync({
385
- start,
386
- end
387
- })){
388
- const contractClassLogsInBlock = [
389
- []
390
- ];
391
- const reader = new BufferReader(logBuffer);
392
- const blockHash = this.#unpackBlockHash(reader);
393
- while(reader.remainingBytes() > 0){
394
- const indexOfTx = reader.readNumber();
395
- const numLogsInTx = reader.readNumber();
396
- contractClassLogsInBlock[indexOfTx] = [];
397
- for(let i = 0; i < numLogsInTx; i++){
398
- contractClassLogsInBlock[indexOfTx].push(reader.readObject(ContractClassLog));
399
- }
400
- }
401
- for(let txIndex = filter.afterLog?.txIndex ?? 0; txIndex < contractClassLogsInBlock.length; txIndex++){
402
- const txLogs = contractClassLogsInBlock[txIndex];
403
- maxLogsHit = this.#accumulateLogs(logs, blockNumber, blockHash, txIndex, txLogs, filter);
404
- if (maxLogsHit) {
405
- this.#log.debug(`Max logs hit at block ${blockNumber}`);
406
- break loopOverBlocks;
407
- }
278
+ const results = [];
279
+ for (const key of keys){
280
+ const raw = await primaryMap.getAsync(key);
281
+ if (!raw) {
282
+ continue;
408
283
  }
284
+ const tail = decodeKeyTail(key);
285
+ const value = decodeValue(raw);
286
+ results.push({
287
+ logData: value.logData,
288
+ blockNumber: tail.blockNumber,
289
+ blockHash: value.blockHash,
290
+ blockTimestamp: value.blockTimestamp,
291
+ txHash: value.txHash,
292
+ txIndexWithinBlock: tail.txIndexWithinBlock,
293
+ logIndexWithinTx: tail.logIndexWithinTx
294
+ });
409
295
  }
296
+ return results;
297
+ }
298
+ }
299
+ /** Pulls `{ tagHex, afterLog }` out of a {@link TagQuery}, normalizing the bare-tag form. */ function normalizeTagEntry(entry) {
300
+ if (typeof entry === 'object' && entry !== null && 'tag' in entry) {
410
301
  return {
411
- logs,
412
- maxLogsHit
302
+ tagHex: fieldHex(entry.tag.value),
303
+ afterLog: entry.afterLog
413
304
  };
414
305
  }
415
- #accumulateLogs(results, blockNumber, blockHash, txIndex, txLogs, filter = {}) {
416
- let maxLogsHit = false;
417
- let logIndex = typeof filter.afterLog?.logIndex === 'number' ? filter.afterLog.logIndex + 1 : 0;
418
- for(; logIndex < txLogs.length; logIndex++){
419
- const log = txLogs[logIndex];
420
- if (!filter.contractAddress || log.contractAddress.equals(filter.contractAddress)) {
421
- if (log instanceof ContractClassLog) {
422
- results.push(new ExtendedContractClassLog(new LogId(BlockNumber(blockNumber), blockHash, txIndex, logIndex), log));
423
- } else if (log instanceof PublicLog) {
424
- results.push(new ExtendedPublicLog(new LogId(BlockNumber(blockNumber), blockHash, txIndex, logIndex), log));
425
- } else {
426
- throw new Error('Unknown log type');
427
- }
428
- if (results.length >= this.#logsMaxPageSize) {
429
- maxLogsHit = true;
430
- break;
431
- }
432
- }
433
- }
434
- return maxLogsHit;
435
- }
306
+ return {
307
+ tagHex: fieldHex(entry.value),
308
+ afterLog: undefined
309
+ };
436
310
  }