@aztec/archiver 0.0.0-test.0 → 0.0.1-commit.24de95ac

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 (102) hide show
  1. package/README.md +27 -6
  2. package/dest/archiver/archiver.d.ts +126 -46
  3. package/dest/archiver/archiver.d.ts.map +1 -1
  4. package/dest/archiver/archiver.js +683 -261
  5. package/dest/archiver/archiver_store.d.ts +84 -49
  6. package/dest/archiver/archiver_store.d.ts.map +1 -1
  7. package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
  8. package/dest/archiver/archiver_store_test_suite.js +707 -213
  9. package/dest/archiver/config.d.ts +4 -20
  10. package/dest/archiver/config.d.ts.map +1 -1
  11. package/dest/archiver/config.js +16 -12
  12. package/dest/archiver/data_retrieval.d.ts +25 -20
  13. package/dest/archiver/data_retrieval.d.ts.map +1 -1
  14. package/dest/archiver/data_retrieval.js +147 -68
  15. package/dest/archiver/errors.d.ts +8 -0
  16. package/dest/archiver/errors.d.ts.map +1 -1
  17. package/dest/archiver/errors.js +12 -0
  18. package/dest/archiver/index.d.ts +2 -3
  19. package/dest/archiver/index.d.ts.map +1 -1
  20. package/dest/archiver/index.js +1 -2
  21. package/dest/archiver/instrumentation.d.ts +9 -3
  22. package/dest/archiver/instrumentation.d.ts.map +1 -1
  23. package/dest/archiver/instrumentation.js +58 -17
  24. package/dest/archiver/kv_archiver_store/block_store.d.ts +47 -10
  25. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
  26. package/dest/archiver/kv_archiver_store/block_store.js +216 -63
  27. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +2 -2
  28. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts.map +1 -1
  29. package/dest/archiver/kv_archiver_store/contract_class_store.js +12 -18
  30. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +10 -7
  31. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +1 -1
  32. package/dest/archiver/kv_archiver_store/contract_instance_store.js +30 -16
  33. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +49 -34
  34. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
  35. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +88 -46
  36. package/dest/archiver/kv_archiver_store/log_store.d.ts +1 -1
  37. package/dest/archiver/kv_archiver_store/log_store.d.ts.map +1 -1
  38. package/dest/archiver/kv_archiver_store/log_store.js +18 -46
  39. package/dest/archiver/kv_archiver_store/message_store.d.ts +22 -16
  40. package/dest/archiver/kv_archiver_store/message_store.d.ts.map +1 -1
  41. package/dest/archiver/kv_archiver_store/message_store.js +150 -48
  42. package/dest/archiver/structs/inbox_message.d.ts +15 -0
  43. package/dest/archiver/structs/inbox_message.d.ts.map +1 -0
  44. package/dest/archiver/structs/inbox_message.js +38 -0
  45. package/dest/archiver/structs/published.d.ts +1 -10
  46. package/dest/archiver/structs/published.d.ts.map +1 -1
  47. package/dest/archiver/structs/published.js +1 -1
  48. package/dest/archiver/validation.d.ts +11 -0
  49. package/dest/archiver/validation.d.ts.map +1 -0
  50. package/dest/archiver/validation.js +90 -0
  51. package/dest/factory.d.ts +7 -12
  52. package/dest/factory.d.ts.map +1 -1
  53. package/dest/factory.js +18 -49
  54. package/dest/rpc/index.d.ts +1 -2
  55. package/dest/rpc/index.d.ts.map +1 -1
  56. package/dest/rpc/index.js +1 -4
  57. package/dest/test/mock_archiver.d.ts +1 -1
  58. package/dest/test/mock_l1_to_l2_message_source.d.ts +4 -2
  59. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  60. package/dest/test/mock_l1_to_l2_message_source.js +14 -1
  61. package/dest/test/mock_l2_block_source.d.ts +32 -5
  62. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  63. package/dest/test/mock_l2_block_source.js +118 -7
  64. package/dest/test/mock_structs.d.ts +9 -0
  65. package/dest/test/mock_structs.d.ts.map +1 -0
  66. package/dest/test/mock_structs.js +37 -0
  67. package/package.json +25 -27
  68. package/src/archiver/archiver.ts +858 -317
  69. package/src/archiver/archiver_store.ts +97 -55
  70. package/src/archiver/archiver_store_test_suite.ts +663 -210
  71. package/src/archiver/config.ts +23 -41
  72. package/src/archiver/data_retrieval.ts +215 -92
  73. package/src/archiver/errors.ts +21 -0
  74. package/src/archiver/index.ts +2 -3
  75. package/src/archiver/instrumentation.ts +75 -20
  76. package/src/archiver/kv_archiver_store/block_store.ts +270 -72
  77. package/src/archiver/kv_archiver_store/contract_class_store.ts +13 -23
  78. package/src/archiver/kv_archiver_store/contract_instance_store.ts +35 -27
  79. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +127 -63
  80. package/src/archiver/kv_archiver_store/log_store.ts +24 -62
  81. package/src/archiver/kv_archiver_store/message_store.ts +209 -53
  82. package/src/archiver/structs/inbox_message.ts +41 -0
  83. package/src/archiver/structs/published.ts +1 -11
  84. package/src/archiver/validation.ts +99 -0
  85. package/src/factory.ts +24 -66
  86. package/src/rpc/index.ts +1 -5
  87. package/src/test/mock_archiver.ts +1 -1
  88. package/src/test/mock_l1_to_l2_message_source.ts +14 -3
  89. package/src/test/mock_l2_block_source.ts +152 -8
  90. package/src/test/mock_structs.ts +49 -0
  91. package/dest/archiver/kv_archiver_store/nullifier_store.d.ts +0 -12
  92. package/dest/archiver/kv_archiver_store/nullifier_store.d.ts.map +0 -1
  93. package/dest/archiver/kv_archiver_store/nullifier_store.js +0 -73
  94. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.d.ts +0 -23
  95. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.d.ts.map +0 -1
  96. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.js +0 -49
  97. package/dest/archiver/memory_archiver_store/memory_archiver_store.d.ts +0 -175
  98. package/dest/archiver/memory_archiver_store/memory_archiver_store.d.ts.map +0 -1
  99. package/dest/archiver/memory_archiver_store/memory_archiver_store.js +0 -636
  100. package/src/archiver/kv_archiver_store/nullifier_store.ts +0 -97
  101. package/src/archiver/memory_archiver_store/l1_to_l2_message_store.ts +0 -61
  102. package/src/archiver/memory_archiver_store/memory_archiver_store.ts +0 -801
@@ -1,4 +1,4 @@
1
- import { INITIAL_L2_BLOCK_NUM, MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS } from '@aztec/constants';
1
+ import { INITIAL_L2_BLOCK_NUM, MAX_NOTE_HASHES_PER_TX } from '@aztec/constants';
2
2
  import type { Fr } from '@aztec/foundation/fields';
3
3
  import { createLogger } from '@aztec/foundation/log';
4
4
  import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize';
@@ -30,7 +30,11 @@ export class LogStore {
30
30
  #logsMaxPageSize: number;
31
31
  #log = createLogger('archiver:log_store');
32
32
 
33
- constructor(private db: AztecAsyncKVStore, private blockStore: BlockStore, logsMaxPageSize: number = 1000) {
33
+ constructor(
34
+ private db: AztecAsyncKVStore,
35
+ private blockStore: BlockStore,
36
+ logsMaxPageSize: number = 1000,
37
+ ) {
34
38
  this.#logsByTag = db.openMap('archiver_tagged_logs_by_tag');
35
39
  this.#logTagsByBlock = db.openMap('archiver_log_tags_by_block');
36
40
  this.#privateLogsByBlock = db.openMap('archiver_private_logs_by_block');
@@ -40,7 +44,7 @@ export class LogStore {
40
44
  this.#logsMaxPageSize = logsMaxPageSize;
41
45
  }
42
46
 
43
- #extractTaggedLogsFromPrivate(block: L2Block) {
47
+ #extractTaggedLogs(block: L2Block) {
44
48
  const taggedLogs = new Map<string, Buffer[]>();
45
49
  const dataStartIndexForBlock =
46
50
  block.header.state.partial.noteHashTree.nextAvailableLeafIndex -
@@ -48,68 +52,22 @@ export class LogStore {
48
52
  block.body.txEffects.forEach((txEffect, txIndex) => {
49
53
  const txHash = txEffect.txHash;
50
54
  const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX;
51
- txEffect.privateLogs.forEach(log => {
55
+
56
+ txEffect.privateLogs.forEach((log, logIndex) => {
52
57
  const tag = log.fields[0];
58
+ this.#log.debug(`Found private log with tag ${tag.toString()} in block ${block.number}`);
59
+
53
60
  const currentLogs = taggedLogs.get(tag.toString()) ?? [];
54
- currentLogs.push(
55
- new TxScopedL2Log(
56
- txHash,
57
- dataStartIndexForTx,
58
- block.number,
59
- /* isFromPublic */ false,
60
- log.toBuffer(),
61
- ).toBuffer(),
62
- );
61
+ currentLogs.push(new TxScopedL2Log(txHash, dataStartIndexForTx, logIndex, block.number, log).toBuffer());
63
62
  taggedLogs.set(tag.toString(), currentLogs);
64
63
  });
65
- });
66
- return taggedLogs;
67
- }
68
-
69
- #extractTaggedLogsFromPublic(block: L2Block) {
70
- const taggedLogs = new Map<string, Buffer[]>();
71
- const dataStartIndexForBlock =
72
- block.header.state.partial.noteHashTree.nextAvailableLeafIndex -
73
- block.body.txEffects.length * MAX_NOTE_HASHES_PER_TX;
74
- block.body.txEffects.forEach((txEffect, txIndex) => {
75
- const txHash = txEffect.txHash;
76
- const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX;
77
- txEffect.publicLogs.forEach(log => {
78
- // Check that each log stores 2 lengths in its first field. If not, it's not a tagged log:
79
- const firstFieldBuf = log.log[0].toBuffer();
80
- // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me).
81
- // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here.
82
- if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) {
83
- // See parseLogFromPublic - the first field of a tagged log is 5 bytes structured:
84
- // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]]
85
- this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`);
86
- return;
87
- }
88
- // Check that the length values line up with the log contents
89
- const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE();
90
- const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3);
91
- // Add 1 for the first field holding lengths
92
- const totalLogLength = 1 + publicValuesLength + privateValuesLength;
93
- // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length
94
- if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) {
95
- this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`);
96
- return;
97
- }
98
64
 
99
- // The first elt stores lengths as above => tag is in fields[1]
100
- const tag = log.log[1];
65
+ txEffect.publicLogs.forEach((log, logIndex) => {
66
+ const tag = log.fields[0];
67
+ this.#log.debug(`Found public log with tag ${tag.toString()} in block ${block.number}`);
101
68
 
102
- this.#log.debug(`Found tagged public log with tag ${tag.toString()} in block ${block.number}`);
103
69
  const currentLogs = taggedLogs.get(tag.toString()) ?? [];
104
- currentLogs.push(
105
- new TxScopedL2Log(
106
- txHash,
107
- dataStartIndexForTx,
108
- block.number,
109
- /* isFromPublic */ true,
110
- log.toBuffer(),
111
- ).toBuffer(),
112
- );
70
+ currentLogs.push(new TxScopedL2Log(txHash, dataStartIndexForTx, logIndex, block.number, log).toBuffer());
113
71
  taggedLogs.set(tag.toString(), currentLogs);
114
72
  });
115
73
  });
@@ -123,14 +81,14 @@ export class LogStore {
123
81
  */
124
82
  addLogs(blocks: L2Block[]): Promise<boolean> {
125
83
  const taggedLogsToAdd = blocks
126
- .flatMap(block => [this.#extractTaggedLogsFromPrivate(block), this.#extractTaggedLogsFromPublic(block)])
84
+ .map(block => this.#extractTaggedLogs(block))
127
85
  .reduce((acc, val) => {
128
86
  for (const [tag, logs] of val.entries()) {
129
87
  const currentLogs = acc.get(tag) ?? [];
130
88
  acc.set(tag, currentLogs.concat(logs));
131
89
  }
132
90
  return acc;
133
- });
91
+ }, new Map());
134
92
  const tagsToUpdate = Array.from(taggedLogsToAdd.keys());
135
93
 
136
94
  return this.db.transactionAsync(async () => {
@@ -204,6 +162,7 @@ export class LogStore {
204
162
  this.#privateLogsByBlock.delete(block.number),
205
163
  this.#publicLogsByBlock.delete(block.number),
206
164
  this.#logTagsByBlock.delete(block.number),
165
+ this.#contractClassLogsByBlock.delete(block.number),
207
166
  ]),
208
167
  ),
209
168
  );
@@ -236,10 +195,13 @@ export class LogStore {
236
195
  * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
237
196
  * that tag.
238
197
  */
239
- async getLogsByTags(tags: Fr[]): Promise<TxScopedL2Log[][]> {
198
+ async getLogsByTags(tags: Fr[], limitPerTag?: number): Promise<TxScopedL2Log[][]> {
199
+ if (limitPerTag !== undefined && limitPerTag <= 0) {
200
+ throw new TypeError('limitPerTag needs to be greater than 0');
201
+ }
240
202
  const logs = await Promise.all(tags.map(tag => this.#logsByTag.getAsync(tag.toString())));
241
203
  return logs.map(
242
- noteLogBuffers => noteLogBuffers?.map(noteLogBuffer => TxScopedL2Log.fromBuffer(noteLogBuffer)) ?? [],
204
+ logBuffers => logBuffers?.slice(0, limitPerTag).map(logBuffer => TxScopedL2Log.fromBuffer(logBuffer)) ?? [],
243
205
  );
244
206
  }
245
207
 
@@ -1,71 +1,172 @@
1
- import { L1_TO_L2_MSG_SUBTREE_HEIGHT } from '@aztec/constants';
1
+ import type { L1BlockId } from '@aztec/ethereum';
2
+ import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
2
3
  import { Fr } from '@aztec/foundation/fields';
4
+ import { toArray } from '@aztec/foundation/iterable';
3
5
  import { createLogger } from '@aztec/foundation/log';
4
- import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncSingleton } from '@aztec/kv-store';
6
+ import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize';
7
+ import {
8
+ type AztecAsyncKVStore,
9
+ type AztecAsyncMap,
10
+ type AztecAsyncSingleton,
11
+ type CustomRange,
12
+ mapRange,
13
+ } from '@aztec/kv-store';
5
14
  import { InboxLeaf } from '@aztec/stdlib/messaging';
6
15
 
7
- import type { DataRetrieval } from '../structs/data_retrieval.js';
16
+ import {
17
+ type InboxMessage,
18
+ deserializeInboxMessage,
19
+ serializeInboxMessage,
20
+ updateRollingHash,
21
+ } from '../structs/inbox_message.js';
22
+
23
+ export class MessageStoreError extends Error {
24
+ constructor(
25
+ message: string,
26
+ public readonly inboxMessage: InboxMessage,
27
+ ) {
28
+ super(message);
29
+ this.name = 'MessageStoreError';
30
+ }
31
+ }
8
32
 
9
- /**
10
- * LMDB implementation of the ArchiverDataStore interface.
11
- */
12
33
  export class MessageStore {
13
- #l1ToL2Messages: AztecAsyncMap<string, Buffer>;
34
+ /** Maps from message index to serialized InboxMessage */
35
+ #l1ToL2Messages: AztecAsyncMap<number, Buffer>;
36
+ /** Maps from hex-stringified message leaf to its index */
14
37
  #l1ToL2MessageIndices: AztecAsyncMap<string, bigint>;
15
- #lastSynchedL1Block: AztecAsyncSingleton<bigint>;
38
+ /** Stores L1 block number and hash of the L1 synchpoint */
39
+ #lastSynchedL1Block: AztecAsyncSingleton<Buffer>;
40
+ /** Stores total messages stored */
16
41
  #totalMessageCount: AztecAsyncSingleton<bigint>;
17
42
 
18
43
  #log = createLogger('archiver:message_store');
19
44
 
20
- #l1ToL2MessagesSubtreeSize = 2 ** L1_TO_L2_MSG_SUBTREE_HEIGHT;
21
-
22
45
  constructor(private db: AztecAsyncKVStore) {
23
46
  this.#l1ToL2Messages = db.openMap('archiver_l1_to_l2_messages');
24
47
  this.#l1ToL2MessageIndices = db.openMap('archiver_l1_to_l2_message_indices');
25
- this.#lastSynchedL1Block = db.openSingleton('archiver_last_l1_block_new_messages');
48
+ this.#lastSynchedL1Block = db.openSingleton('archiver_last_l1_block_id');
26
49
  this.#totalMessageCount = db.openSingleton('archiver_l1_to_l2_message_count');
27
50
  }
28
51
 
29
- async getTotalL1ToL2MessageCount(): Promise<bigint> {
52
+ public async getTotalL1ToL2MessageCount(): Promise<bigint> {
30
53
  return (await this.#totalMessageCount.getAsync()) ?? 0n;
31
54
  }
32
55
 
33
- /**
34
- * Gets the last L1 block number that emitted new messages.
35
- * @returns The last L1 block number processed
36
- */
37
- getSynchedL1BlockNumber(): Promise<bigint | undefined> {
38
- return this.#lastSynchedL1Block.getAsync();
56
+ /** Gets the last L1 block synced. */
57
+ public async getSynchedL1Block(): Promise<L1BlockId | undefined> {
58
+ const buffer = await this.#lastSynchedL1Block.getAsync();
59
+ if (!buffer) {
60
+ return undefined;
61
+ }
62
+
63
+ const reader = BufferReader.asReader(buffer);
64
+ return { l1BlockNumber: reader.readUInt256(), l1BlockHash: Buffer32.fromBuffer(reader.readBytes(Buffer32.SIZE)) };
39
65
  }
40
66
 
41
- async setSynchedL1BlockNumber(l1BlockNumber: bigint): Promise<void> {
42
- await this.#lastSynchedL1Block.set(l1BlockNumber);
67
+ /** Sets the last L1 block synced */
68
+ public async setSynchedL1Block(l1Block: L1BlockId): Promise<void> {
69
+ const buffer = serializeToBuffer([l1Block.l1BlockNumber, l1Block.l1BlockHash]);
70
+ await this.#lastSynchedL1Block.set(buffer);
43
71
  }
44
72
 
45
73
  /**
46
74
  * Append L1 to L2 messages to the store.
47
- * @param messages - The L1 to L2 messages to be added to the store and the last processed L1 block.
48
- * @returns True if the operation is successful.
75
+ * Requires new messages to be in order and strictly after the last message added.
76
+ * Throws if out of order messages are added or if the rolling hash is invalid.
49
77
  */
50
- addL1ToL2Messages(messages: DataRetrieval<InboxLeaf>): Promise<boolean> {
78
+ public addL1ToL2Messages(messages: InboxMessage[]): Promise<void> {
79
+ if (messages.length === 0) {
80
+ return Promise.resolve();
81
+ }
82
+
51
83
  return this.db.transactionAsync(async () => {
52
- const lastL1BlockNumber = (await this.#lastSynchedL1Block.getAsync()) ?? 0n;
53
- if (lastL1BlockNumber >= messages.lastProcessedL1BlockNumber) {
54
- return false;
55
- }
84
+ let lastMessage = await this.getLastMessage();
85
+ let messageCount = 0;
86
+
87
+ for (const message of messages) {
88
+ // Check messages are inserted in increasing order, but allow reinserting messages.
89
+ if (lastMessage && message.index <= lastMessage.index) {
90
+ const existing = await this.#l1ToL2Messages.getAsync(this.indexToKey(message.index));
91
+ if (existing && deserializeInboxMessage(existing).rollingHash.equals(message.rollingHash)) {
92
+ // We reinsert instead of skipping in case the message was re-orged and got added in a different L1 block.
93
+ this.#log.trace(`Reinserting message with index ${message.index} in the store`);
94
+ await this.#l1ToL2Messages.set(this.indexToKey(message.index), serializeInboxMessage(message));
95
+ continue;
96
+ }
56
97
 
57
- await this.#lastSynchedL1Block.set(messages.lastProcessedL1BlockNumber);
98
+ throw new MessageStoreError(
99
+ `Cannot insert L1 to L2 message with index ${message.index} before last message with index ${lastMessage.index}`,
100
+ message,
101
+ );
102
+ }
58
103
 
59
- for (const message of messages.retrievedData) {
60
- const key = `${message.index}`;
61
- await this.#l1ToL2Messages.set(key, message.leaf.toBuffer());
62
- await this.#l1ToL2MessageIndices.set(message.leaf.toString(), message.index);
104
+ // Check rolling hash is valid.
105
+ const previousRollingHash = lastMessage?.rollingHash ?? Buffer16.ZERO;
106
+ const expectedRollingHash = updateRollingHash(previousRollingHash, message.leaf);
107
+ if (!expectedRollingHash.equals(message.rollingHash)) {
108
+ throw new MessageStoreError(
109
+ `Invalid rolling hash for incoming L1 to L2 message ${message.leaf.toString()} ` +
110
+ `with index ${message.index} ` +
111
+ `(expected ${expectedRollingHash.toString()} from previous hash ${previousRollingHash} but got ${message.rollingHash.toString()})`,
112
+ message,
113
+ );
114
+ }
115
+
116
+ // Check index corresponds to the L2 block number.
117
+ const [expectedStart, expectedEnd] = InboxLeaf.indexRangeFromL2Block(message.l2BlockNumber);
118
+ if (message.index < expectedStart || message.index >= expectedEnd) {
119
+ throw new MessageStoreError(
120
+ `Invalid index ${message.index} for incoming L1 to L2 message ${message.leaf.toString()} ` +
121
+ `at block ${message.l2BlockNumber} (expected value in range [${expectedStart}, ${expectedEnd}))`,
122
+ message,
123
+ );
124
+ }
125
+
126
+ // Check there are no gaps in the indices within the same block.
127
+ if (
128
+ lastMessage &&
129
+ message.l2BlockNumber === lastMessage.l2BlockNumber &&
130
+ message.index !== lastMessage.index + 1n
131
+ ) {
132
+ throw new MessageStoreError(
133
+ `Missing prior message for incoming L1 to L2 message ${message.leaf.toString()} ` +
134
+ `with index ${message.index}`,
135
+ message,
136
+ );
137
+ }
138
+
139
+ // Check the first message in a block has the correct index.
140
+ if (
141
+ (!lastMessage || message.l2BlockNumber > lastMessage.l2BlockNumber) &&
142
+ message.index !== InboxLeaf.smallestIndexFromL2Block(message.l2BlockNumber)
143
+ ) {
144
+ throw new MessageStoreError(
145
+ `Message ${message.leaf.toString()} for L2 block ${message.l2BlockNumber} has wrong index ` +
146
+ `${message.index} (expected ${InboxLeaf.smallestIndexFromL2Block(message.l2BlockNumber)})`,
147
+ message,
148
+ );
149
+ }
150
+
151
+ // Perform the insertions.
152
+ await this.#l1ToL2Messages.set(this.indexToKey(message.index), serializeInboxMessage(message));
153
+ await this.#l1ToL2MessageIndices.set(this.leafToIndexKey(message.leaf), message.index);
154
+ messageCount++;
155
+ this.#log.trace(`Inserted L1 to L2 message ${message.leaf} with index ${message.index} into the store`);
156
+ lastMessage = message;
63
157
  }
64
158
 
65
- const lastTotalMessageCount = await this.getTotalL1ToL2MessageCount();
66
- await this.#totalMessageCount.set(lastTotalMessageCount + BigInt(messages.retrievedData.length));
159
+ // Update the L1 sync point to that of the last message added.
160
+ const currentSyncPoint = await this.getSynchedL1Block();
161
+ if (!currentSyncPoint || currentSyncPoint.l1BlockNumber < lastMessage!.l1BlockNumber) {
162
+ await this.setSynchedL1Block({
163
+ l1BlockNumber: lastMessage!.l1BlockNumber,
164
+ l1BlockHash: lastMessage!.l1BlockHash,
165
+ });
166
+ }
67
167
 
68
- return true;
168
+ // Update total message count with the number of inserted messages.
169
+ await this.increaseTotalMessageCount(messageCount);
69
170
  });
70
171
  }
71
172
 
@@ -74,29 +175,84 @@ export class MessageStore {
74
175
  * @param l1ToL2Message - The L1 to L2 message.
75
176
  * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found).
76
177
  */
77
- getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise<bigint | undefined> {
78
- return this.#l1ToL2MessageIndices.getAsync(l1ToL2Message.toString());
178
+ public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise<bigint | undefined> {
179
+ return this.#l1ToL2MessageIndices.getAsync(this.leafToIndexKey(l1ToL2Message));
180
+ }
181
+
182
+ public async getLastMessage(): Promise<InboxMessage | undefined> {
183
+ const [msg] = await toArray(this.#l1ToL2Messages.valuesAsync({ reverse: true, limit: 1 }));
184
+ return msg ? deserializeInboxMessage(msg) : undefined;
79
185
  }
80
186
 
81
- async getL1ToL2Messages(blockNumber: bigint): Promise<Fr[]> {
187
+ public async getL1ToL2Messages(blockNumber: number): Promise<Fr[]> {
82
188
  const messages: Fr[] = [];
83
- let undefinedMessageFound = false;
84
- const startIndex = Number(InboxLeaf.smallestIndexFromL2Block(blockNumber));
85
- for (let i = startIndex; i < startIndex + this.#l1ToL2MessagesSubtreeSize; i++) {
86
- // This is inefficient but probably fine for now.
87
- const key = `${i}`;
88
- const message = await this.#l1ToL2Messages.getAsync(key);
89
- if (message) {
90
- if (undefinedMessageFound) {
91
- throw new Error(`L1 to L2 message gap found in block ${blockNumber}`);
92
- }
93
- messages.push(Fr.fromBuffer(message));
94
- } else {
95
- undefinedMessageFound = true;
96
- // We continue iterating over messages here to verify that there are no more messages after the undefined one.
97
- // --> If this was the case this would imply there is some issue with log fetching.
189
+
190
+ const [startIndex, endIndex] = InboxLeaf.indexRangeFromL2Block(blockNumber);
191
+ let lastIndex = startIndex - 1n;
192
+
193
+ for await (const msgBuffer of this.#l1ToL2Messages.valuesAsync({
194
+ start: this.indexToKey(startIndex),
195
+ end: this.indexToKey(endIndex),
196
+ })) {
197
+ const msg = deserializeInboxMessage(msgBuffer);
198
+ if (msg.l2BlockNumber !== blockNumber) {
199
+ throw new Error(`L1 to L2 message with index ${msg.index} has invalid block number ${msg.l2BlockNumber}`);
200
+ } else if (msg.index !== lastIndex + 1n) {
201
+ throw new Error(`Expected L1 to L2 message with index ${lastIndex + 1n} but got ${msg.index}`);
98
202
  }
203
+ lastIndex = msg.index;
204
+ messages.push(msg.leaf);
99
205
  }
206
+
100
207
  return messages;
101
208
  }
209
+
210
+ public async *iterateL1ToL2Messages(range: CustomRange<bigint> = {}): AsyncIterableIterator<InboxMessage> {
211
+ const entriesRange = mapRange(range, this.indexToKey);
212
+ for await (const msgBuffer of this.#l1ToL2Messages.valuesAsync(entriesRange)) {
213
+ yield deserializeInboxMessage(msgBuffer);
214
+ }
215
+ }
216
+
217
+ public removeL1ToL2Messages(startIndex: bigint): Promise<void> {
218
+ this.#log.debug(`Deleting L1 to L2 messages from index ${startIndex}`);
219
+ let deleteCount = 0;
220
+
221
+ return this.db.transactionAsync(async () => {
222
+ for await (const [key, msgBuffer] of this.#l1ToL2Messages.entriesAsync({
223
+ start: this.indexToKey(startIndex),
224
+ })) {
225
+ this.#log.trace(`Deleting L1 to L2 message with index ${key - 1} from the store`);
226
+ await this.#l1ToL2Messages.delete(key);
227
+ await this.#l1ToL2MessageIndices.delete(this.leafToIndexKey(deserializeInboxMessage(msgBuffer).leaf));
228
+ deleteCount++;
229
+ }
230
+ await this.increaseTotalMessageCount(-deleteCount);
231
+ this.#log.warn(`Deleted ${deleteCount} L1 to L2 messages from index ${startIndex} from the store`);
232
+ });
233
+ }
234
+
235
+ public rollbackL1ToL2MessagesToL2Block(targetBlockNumber: number): Promise<void> {
236
+ this.#log.debug(`Deleting L1 to L2 messages up to target L2 block ${targetBlockNumber}`);
237
+ const startIndex = InboxLeaf.smallestIndexFromL2Block(targetBlockNumber + 1);
238
+ return this.removeL1ToL2Messages(startIndex);
239
+ }
240
+
241
+ private indexToKey(index: bigint): number {
242
+ return Number(index);
243
+ }
244
+
245
+ private leafToIndexKey(leaf: Fr): string {
246
+ return leaf.toString();
247
+ }
248
+
249
+ private async increaseTotalMessageCount(count: bigint | number): Promise<void> {
250
+ if (count === 0) {
251
+ return;
252
+ }
253
+ return await this.db.transactionAsync(async () => {
254
+ const lastTotalMessageCount = await this.getTotalL1ToL2MessageCount();
255
+ await this.#totalMessageCount.set(lastTotalMessageCount + BigInt(count));
256
+ });
257
+ }
102
258
  }
@@ -0,0 +1,41 @@
1
+ import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
2
+ import { keccak256 } from '@aztec/foundation/crypto';
3
+ import { Fr } from '@aztec/foundation/fields';
4
+ import { BufferReader, bigintToUInt64BE, numToUInt32BE, serializeToBuffer } from '@aztec/foundation/serialize';
5
+ import type { UInt32 } from '@aztec/stdlib/types';
6
+
7
+ export type InboxMessage = {
8
+ index: bigint;
9
+ leaf: Fr;
10
+ l2BlockNumber: UInt32;
11
+ l1BlockNumber: bigint;
12
+ l1BlockHash: Buffer32;
13
+ rollingHash: Buffer16;
14
+ };
15
+
16
+ export function updateRollingHash(currentRollingHash: Buffer16, leaf: Fr): Buffer16 {
17
+ const input = Buffer.concat([currentRollingHash.toBuffer(), leaf.toBuffer()]);
18
+ return Buffer16.fromBuffer(keccak256(input));
19
+ }
20
+
21
+ export function serializeInboxMessage(message: InboxMessage): Buffer {
22
+ return serializeToBuffer([
23
+ bigintToUInt64BE(message.index),
24
+ message.leaf,
25
+ message.l1BlockHash,
26
+ numToUInt32BE(Number(message.l1BlockNumber)),
27
+ numToUInt32BE(message.l2BlockNumber),
28
+ message.rollingHash,
29
+ ]);
30
+ }
31
+
32
+ export function deserializeInboxMessage(buffer: Buffer): InboxMessage {
33
+ const reader = BufferReader.asReader(buffer);
34
+ const index = reader.readUInt64();
35
+ const leaf = reader.readObject(Fr);
36
+ const l1BlockHash = reader.readObject(Buffer32);
37
+ const l1BlockNumber = BigInt(reader.readNumber());
38
+ const l2BlockNumber = reader.readNumber();
39
+ const rollingHash = reader.readObject(Buffer16);
40
+ return { index, leaf, l1BlockHash, l1BlockNumber, l2BlockNumber, rollingHash };
41
+ }
@@ -1,11 +1 @@
1
- /** Extends a type with L1 published info (block number, hash, and timestamp) */
2
- export type L1Published<T> = {
3
- data: T;
4
- l1: L1PublishedData;
5
- };
6
-
7
- export type L1PublishedData = {
8
- blockNumber: bigint;
9
- timestamp: bigint;
10
- blockHash: string;
11
- };
1
+ export type { PublishedL2Block, L1PublishedData } from '@aztec/stdlib/block';
@@ -0,0 +1,99 @@
1
+ import type { EpochCache } from '@aztec/epoch-cache';
2
+ import { compactArray } from '@aztec/foundation/collection';
3
+ import type { Logger } from '@aztec/foundation/log';
4
+ import {
5
+ type PublishedL2Block,
6
+ type ValidateBlockNegativeResult,
7
+ type ValidateBlockResult,
8
+ getAttestationInfoFromPublishedL2Block,
9
+ } from '@aztec/stdlib/block';
10
+ import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
11
+
12
+ export type { ValidateBlockResult };
13
+
14
+ /**
15
+ * Validates the attestations submitted for the given block.
16
+ * Returns true if the attestations are valid and sufficient, false otherwise.
17
+ */
18
+ export async function validateBlockAttestations(
19
+ publishedBlock: PublishedL2Block,
20
+ epochCache: EpochCache,
21
+ constants: Pick<L1RollupConstants, 'epochDuration'>,
22
+ logger?: Logger,
23
+ ): Promise<ValidateBlockResult> {
24
+ const attestorInfos = getAttestationInfoFromPublishedL2Block(publishedBlock);
25
+ const attestors = compactArray(attestorInfos.map(info => ('address' in info ? info.address : undefined)));
26
+ const { block } = publishedBlock;
27
+ const blockHash = await block.hash().then(hash => hash.toString());
28
+ const archiveRoot = block.archive.root.toString();
29
+ const slot = block.header.getSlot();
30
+ const epoch = getEpochAtSlot(slot, constants);
31
+ const { committee, seed } = await epochCache.getCommitteeForEpoch(epoch);
32
+ const logData = { blockNumber: block.number, slot, epoch, blockHash, archiveRoot };
33
+
34
+ logger?.debug(`Validating attestations for block ${block.number} at slot ${slot} in epoch ${epoch}`, {
35
+ committee: (committee ?? []).map(member => member.toString()),
36
+ recoveredAttestors: attestorInfos,
37
+ postedAttestations: publishedBlock.attestations.map(a => (a.address.isZero() ? a.signature : a.address).toString()),
38
+ ...logData,
39
+ });
40
+
41
+ if (!committee || committee.length === 0) {
42
+ logger?.warn(`No committee found for epoch ${epoch} at slot ${slot}. Accepting block without validation.`, logData);
43
+ return { valid: true };
44
+ }
45
+
46
+ const committeeSet = new Set(committee.map(member => member.toString()));
47
+ const requiredAttestationCount = Math.floor((committee.length * 2) / 3) + 1;
48
+
49
+ const failedValidationResult = <TReason extends ValidateBlockNegativeResult['reason']>(reason: TReason) => ({
50
+ valid: false as const,
51
+ reason,
52
+ block: publishedBlock.block.toBlockInfo(),
53
+ committee,
54
+ seed,
55
+ epoch,
56
+ attestors,
57
+ attestations: publishedBlock.attestations,
58
+ });
59
+
60
+ for (let i = 0; i < attestorInfos.length; i++) {
61
+ const info = attestorInfos[i];
62
+
63
+ // Fail on invalid signatures (no address recovered)
64
+ if (info.status === 'invalid-signature' || info.status === 'empty') {
65
+ logger?.warn(`Attestation with empty or invalid signature at slot ${slot}`, {
66
+ committee,
67
+ invalidIndex: i,
68
+ ...logData,
69
+ });
70
+ return { ...failedValidationResult('invalid-attestation'), invalidIndex: i };
71
+ }
72
+
73
+ // Check if the attestor is in the committee
74
+ if (info.status === 'recovered-from-signature' || info.status === 'provided-as-address') {
75
+ const signer = info.address.toString();
76
+ if (!committeeSet.has(signer)) {
77
+ logger?.warn(`Attestation from non-committee member ${signer} at slot ${slot}`, {
78
+ committee,
79
+ invalidIndex: i,
80
+ ...logData,
81
+ });
82
+ return { ...failedValidationResult('invalid-attestation'), invalidIndex: i };
83
+ }
84
+ }
85
+ }
86
+
87
+ const validAttestationCount = attestorInfos.filter(info => info.status === 'recovered-from-signature').length;
88
+ if (validAttestationCount < requiredAttestationCount) {
89
+ logger?.warn(`Insufficient attestations for block at slot ${slot}`, {
90
+ requiredAttestations: requiredAttestationCount,
91
+ actualAttestations: validAttestationCount,
92
+ ...logData,
93
+ });
94
+ return failedValidationResult('insufficient-attestations');
95
+ }
96
+
97
+ logger?.debug(`Block attestations validated successfully for block ${block.number} at slot ${slot}`, logData);
98
+ return { valid: true };
99
+ }