@aztec/archiver 0.1.0-alpha11

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 (45) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/.tsbuildinfo +1 -0
  3. package/README.md +17 -0
  4. package/dest/archiver/archiver.d.ts +147 -0
  5. package/dest/archiver/archiver.d.ts.map +1 -0
  6. package/dest/archiver/archiver.js +260 -0
  7. package/dest/archiver/archiver.test.d.ts +2 -0
  8. package/dest/archiver/archiver.test.d.ts.map +1 -0
  9. package/dest/archiver/archiver.test.js +188 -0
  10. package/dest/archiver/archiver_store.d.ts +269 -0
  11. package/dest/archiver/archiver_store.d.ts.map +1 -0
  12. package/dest/archiver/archiver_store.js +259 -0
  13. package/dest/archiver/config.d.ts +29 -0
  14. package/dest/archiver/config.d.ts.map +1 -0
  15. package/dest/archiver/config.js +21 -0
  16. package/dest/archiver/data_retrieval.d.ts +64 -0
  17. package/dest/archiver/data_retrieval.d.ts.map +1 -0
  18. package/dest/archiver/data_retrieval.js +102 -0
  19. package/dest/archiver/eth_log_handlers.d.ts +77 -0
  20. package/dest/archiver/eth_log_handlers.d.ts.map +1 -0
  21. package/dest/archiver/eth_log_handlers.js +178 -0
  22. package/dest/archiver/index.d.ts +3 -0
  23. package/dest/archiver/index.d.ts.map +1 -0
  24. package/dest/archiver/index.js +3 -0
  25. package/dest/archiver/l1_to_l2_message_store.d.ts +40 -0
  26. package/dest/archiver/l1_to_l2_message_store.d.ts.map +1 -0
  27. package/dest/archiver/l1_to_l2_message_store.js +71 -0
  28. package/dest/archiver/l1_to_l2_message_store.test.d.ts +2 -0
  29. package/dest/archiver/l1_to_l2_message_store.test.d.ts.map +1 -0
  30. package/dest/archiver/l1_to_l2_message_store.test.js +77 -0
  31. package/dest/index.d.ts +2 -0
  32. package/dest/index.d.ts.map +1 -0
  33. package/dest/index.js +37 -0
  34. package/package.json +19 -0
  35. package/src/archiver/archiver.test.ts +225 -0
  36. package/src/archiver/archiver.ts +363 -0
  37. package/src/archiver/archiver_store.ts +425 -0
  38. package/src/archiver/config.ts +55 -0
  39. package/src/archiver/data_retrieval.ts +167 -0
  40. package/src/archiver/eth_log_handlers.ts +238 -0
  41. package/src/archiver/index.ts +2 -0
  42. package/src/archiver/l1_to_l2_message_store.test.ts +97 -0
  43. package/src/archiver/l1_to_l2_message_store.ts +88 -0
  44. package/src/index.ts +51 -0
  45. package/tsconfig.json +23 -0
@@ -0,0 +1,238 @@
1
+ import { Hex, Log, PublicClient, decodeFunctionData, getAbiItem, getAddress, hexToBytes } from 'viem';
2
+ import { InboxAbi, RollupAbi, ContractDeploymentEmitterAbi } from '@aztec/l1-artifacts';
3
+ import { Fr } from '@aztec/foundation/fields';
4
+ import {
5
+ L1ToL2Message,
6
+ L1Actor,
7
+ L2Actor,
8
+ L2Block,
9
+ ContractPublicData,
10
+ BufferReader,
11
+ ContractData,
12
+ EncodedContractFunction,
13
+ } from '@aztec/types';
14
+ import { EthAddress } from '@aztec/foundation/eth-address';
15
+ import { AztecAddress } from '@aztec/foundation/aztec-address';
16
+
17
+ /**
18
+ * Processes newly received MessageAdded (L1 to L2) logs.
19
+ * @param logs - MessageAdded logs.
20
+ * @returns Array of all Pending L1 to L2 messages that were processed
21
+ */
22
+ export function processPendingL1ToL2MessageAddedLogs(
23
+ logs: Log<bigint, number, undefined, true, typeof InboxAbi, 'MessageAdded'>[],
24
+ ): L1ToL2Message[] {
25
+ const l1ToL2Messages: L1ToL2Message[] = [];
26
+ for (const log of logs) {
27
+ const { sender, senderChainId, recipient, recipientVersion, content, secretHash, deadline, fee, entryKey } =
28
+ log.args;
29
+ l1ToL2Messages.push(
30
+ new L1ToL2Message(
31
+ new L1Actor(EthAddress.fromString(sender), Number(senderChainId)),
32
+ new L2Actor(AztecAddress.fromString(recipient), Number(recipientVersion)),
33
+ Fr.fromString(content),
34
+ Fr.fromString(secretHash),
35
+ deadline,
36
+ Number(fee),
37
+ Fr.fromString(entryKey),
38
+ ),
39
+ );
40
+ }
41
+ return l1ToL2Messages;
42
+ }
43
+
44
+ /**
45
+ * Process newly received L1ToL2MessageCancelled logs.
46
+ * @param logs - L1ToL2MessageCancelled logs.
47
+ * @returns Array of message keys of the L1 to L2 messages that were cancelled
48
+ */
49
+ export function processCancelledL1ToL2MessagesLogs(
50
+ logs: Log<bigint, number, undefined, true, typeof InboxAbi, 'L1ToL2MessageCancelled'>[],
51
+ ): Fr[] {
52
+ const cancelledL1ToL2Messages: Fr[] = [];
53
+ for (const log of logs) {
54
+ cancelledL1ToL2Messages.push(Fr.fromString(log.args.entryKey));
55
+ }
56
+ return cancelledL1ToL2Messages;
57
+ }
58
+
59
+ /**
60
+ * Processes newly received L2BlockProcessed logs.
61
+ * @param publicClient - The viem public client to use for transaction retrieval.
62
+ * @param expectedL2BlockNumber - The next expected L2 block number.
63
+ * @param logs - L2BlockProcessed logs.
64
+ */
65
+ export async function processBlockLogs(
66
+ publicClient: PublicClient,
67
+ expectedL2BlockNumber: bigint,
68
+ logs: Log<bigint, number, undefined, true, typeof RollupAbi, 'L2BlockProcessed'>[],
69
+ ) {
70
+ const retrievedBlocks: L2Block[] = [];
71
+ for (const log of logs) {
72
+ const blockNum = log.args.blockNum;
73
+ if (blockNum !== expectedL2BlockNumber) {
74
+ throw new Error('Block number mismatch. Expected: ' + expectedL2BlockNumber + ' but got: ' + blockNum + '.');
75
+ }
76
+ // TODO: Fetch blocks from calldata in parallel
77
+ const newBlock = await getBlockFromCallData(publicClient, log.transactionHash!, log.args.blockNum);
78
+ retrievedBlocks.push(newBlock);
79
+ expectedL2BlockNumber++;
80
+ }
81
+ return retrievedBlocks;
82
+ }
83
+
84
+ /**
85
+ * Builds an L2 block out of calldata from the tx that published it.
86
+ * Assumes that the block was published from an EOA.
87
+ * TODO: Add retries and error management.
88
+ * @param publicClient - The viem public client to use for transaction retrieval.
89
+ * @param txHash - Hash of the tx that published it.
90
+ * @param l2BlockNum - L2 block number.
91
+ * @returns An L2 block deserialized from the calldata.
92
+ */
93
+ async function getBlockFromCallData(
94
+ publicClient: PublicClient,
95
+ txHash: `0x${string}`,
96
+ l2BlockNum: bigint,
97
+ ): Promise<L2Block> {
98
+ const { input: data } = await publicClient.getTransaction({ hash: txHash });
99
+ // TODO: File a bug in viem who complains if we dont remove the ctor from the abi here
100
+ const { functionName, args } = decodeFunctionData({
101
+ abi: RollupAbi.filter(item => item.type.toString() !== 'constructor'),
102
+ data,
103
+ });
104
+ if (functionName !== 'process') throw new Error(`Unexpected method called ${functionName}`);
105
+ const [, l2BlockHex] = args! as [Hex, Hex];
106
+ const block = L2Block.decode(Buffer.from(hexToBytes(l2BlockHex)));
107
+ if (BigInt(block.number) !== l2BlockNum) {
108
+ throw new Error(`Block number mismatch: expected ${l2BlockNum} but got ${block.number}`);
109
+ }
110
+ return block;
111
+ }
112
+
113
+ /**
114
+ * Gets relevant `L2BlockProcessed` logs from chain.
115
+ * @param publicClient - The viem public client to use for transaction retrieval.
116
+ * @param rollupAddress - The address of the rollup contract.
117
+ * @param fromBlock - First block to get logs from (inclusive).
118
+ * @returns An array of `L2BlockProcessed` logs.
119
+ */
120
+ export async function getL2BlockProcessedLogs(
121
+ publicClient: PublicClient,
122
+ rollupAddress: EthAddress,
123
+ fromBlock: bigint,
124
+ ) {
125
+ // Note: For some reason the return type of `getLogs` would not get correctly derived if I didn't set the abiItem
126
+ // as a standalone constant.
127
+ const abiItem = getAbiItem({
128
+ abi: RollupAbi,
129
+ name: 'L2BlockProcessed',
130
+ });
131
+ return await publicClient.getLogs<typeof abiItem, true>({
132
+ address: getAddress(rollupAddress.toString()),
133
+ event: abiItem,
134
+ fromBlock,
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Gets relevant `ContractDeployment` logs from chain.
140
+ * @param publicClient - The viem public client to use for transaction retrieval.
141
+ * @param contractDeploymentEmitterAddress - The address of the L2 contract deployment emitter contract.
142
+ * @param fromBlock - First block to get logs from (inclusive).
143
+ * @returns An array of `ContractDeployment` logs.
144
+ */
145
+ export async function getContractDeploymentLogs(
146
+ publicClient: PublicClient,
147
+ contractDeploymentEmitterAddress: EthAddress,
148
+ fromBlock: bigint,
149
+ ): Promise<Log<bigint, number, undefined, true, typeof ContractDeploymentEmitterAbi, 'ContractDeployment'>[]> {
150
+ const abiItem = getAbiItem({
151
+ abi: ContractDeploymentEmitterAbi,
152
+ name: 'ContractDeployment',
153
+ });
154
+ return await publicClient.getLogs({
155
+ address: getAddress(contractDeploymentEmitterAddress.toString()),
156
+ event: abiItem,
157
+ fromBlock,
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Processes newly received ContractDeployment logs.
163
+ * @param blockHashMapping - A mapping from block number to relevant block hash.
164
+ * @param logs - ContractDeployment logs.
165
+ * @returns The set of retrieved contract public data items.
166
+ */
167
+ export function processContractDeploymentLogs(
168
+ blockHashMapping: { [key: number]: Buffer | undefined },
169
+ logs: Log<bigint, number, undefined, true, typeof ContractDeploymentEmitterAbi, 'ContractDeployment'>[],
170
+ ): [ContractPublicData[], number][] {
171
+ const contractPublicData: [ContractPublicData[], number][] = [];
172
+ for (let i = 0; i < logs.length; i++) {
173
+ const log = logs[i];
174
+ const l2BlockNum = Number(log.args.l2BlockNum);
175
+ const blockHash = Buffer.from(hexToBytes(log.args.l2BlockHash));
176
+ const expectedBlockHash = blockHashMapping[l2BlockNum];
177
+ if (expectedBlockHash === undefined || !blockHash.equals(expectedBlockHash)) {
178
+ continue;
179
+ }
180
+ const publicFnsReader = BufferReader.asReader(Buffer.from(log.args.acir.slice(2), 'hex'));
181
+ const contractData = new ContractPublicData(
182
+ new ContractData(AztecAddress.fromString(log.args.aztecAddress), EthAddress.fromString(log.args.portalAddress)),
183
+ publicFnsReader.readVector(EncodedContractFunction),
184
+ );
185
+ if (contractPublicData[i]) {
186
+ contractPublicData[i][0].push(contractData);
187
+ } else {
188
+ contractPublicData[i] = [[contractData], l2BlockNum];
189
+ }
190
+ }
191
+ return contractPublicData;
192
+ }
193
+
194
+ /**
195
+ * Get relevant `MessageAdded` logs emitted by Inbox on chain.
196
+ * @param publicClient - The viem public client to use for transaction retrieval.
197
+ * @param inboxAddress - The address of the inbox contract.
198
+ * @param fromBlock - First block to get logs from (inclusive).
199
+ * @returns An array of `MessageAdded` logs.
200
+ */
201
+ export async function getPendingL1ToL2MessageLogs(
202
+ publicClient: PublicClient,
203
+ inboxAddress: EthAddress,
204
+ fromBlock: bigint,
205
+ ): Promise<Log<bigint, number, undefined, true, typeof InboxAbi, 'MessageAdded'>[]> {
206
+ const abiItem = getAbiItem({
207
+ abi: InboxAbi,
208
+ name: 'MessageAdded',
209
+ });
210
+ return await publicClient.getLogs({
211
+ address: getAddress(inboxAddress.toString()),
212
+ event: abiItem,
213
+ fromBlock,
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Get relevant `L1ToL2MessageCancelled` logs emitted by Inbox on chain when pending messages are cancelled
219
+ * @param publicClient - The viem public client to use for transaction retrieval.
220
+ * @param inboxAddress - The address of the inbox contract.
221
+ * @param fromBlock - First block to get logs from (inclusive).
222
+ * @returns An array of `L1ToL2MessageCancelled` logs.
223
+ */
224
+ export async function getL1ToL2MessageCancelledLogs(
225
+ publicClient: PublicClient,
226
+ inboxAddress: EthAddress,
227
+ fromBlock: bigint,
228
+ ): Promise<Log<bigint, number, undefined, true, typeof InboxAbi, 'L1ToL2MessageCancelled'>[]> {
229
+ const abiItem = getAbiItem({
230
+ abi: InboxAbi,
231
+ name: 'L1ToL2MessageCancelled',
232
+ });
233
+ return await publicClient.getLogs({
234
+ address: getAddress(inboxAddress.toString()),
235
+ event: abiItem,
236
+ fromBlock,
237
+ });
238
+ }
@@ -0,0 +1,2 @@
1
+ export * from './archiver.js';
2
+ export * from './config.js';
@@ -0,0 +1,97 @@
1
+ import { Fr } from '@aztec/foundation/fields';
2
+ import { L1ToL2MessageStore, PendingL1ToL2MessageStore } from './l1_to_l2_message_store.js';
3
+ import { L1Actor, L1ToL2Message, L2Actor } from '@aztec/types';
4
+
5
+ describe('l1_to_l2_message_store', () => {
6
+ let store: L1ToL2MessageStore;
7
+ let entryKey: Fr;
8
+ let msg: L1ToL2Message;
9
+
10
+ beforeEach(() => {
11
+ // already adds a message to the store
12
+ store = new L1ToL2MessageStore();
13
+ entryKey = Fr.random();
14
+ msg = L1ToL2Message.random();
15
+ });
16
+
17
+ it('addMessage adds a message', () => {
18
+ store.addMessage(entryKey, msg);
19
+ expect(store.getMessage(entryKey)).toEqual(msg);
20
+ });
21
+
22
+ it('addMessage increments the count if the message is already in the store', () => {
23
+ store.addMessage(entryKey, msg);
24
+ store.addMessage(entryKey, msg);
25
+ expect(store.getMessageAndCount(entryKey)).toEqual({ message: msg, count: 2 });
26
+ });
27
+ });
28
+
29
+ describe('pending_l1_to_l2_message_store', () => {
30
+ let store: PendingL1ToL2MessageStore;
31
+ let entryKey: Fr;
32
+ let msg: L1ToL2Message;
33
+
34
+ beforeEach(() => {
35
+ // already adds a message to the store
36
+ store = new PendingL1ToL2MessageStore();
37
+ entryKey = Fr.random();
38
+ msg = L1ToL2Message.random();
39
+ });
40
+
41
+ it('removeMessage removes the message if the count is 1', () => {
42
+ store.addMessage(entryKey, msg);
43
+ store.removeMessage(entryKey);
44
+ expect(store.getMessage(entryKey)).toBeUndefined();
45
+ });
46
+
47
+ it("handles case when removing a message that doesn't exist", () => {
48
+ expect(() => store.removeMessage(new Fr(0))).not.toThrow();
49
+ const one = new Fr(1);
50
+ expect(() => store.removeMessage(one)).toThrow(`Message with key ${one.value} not found in store`);
51
+ });
52
+
53
+ it('removeMessage decrements the count if the message is already in the store', () => {
54
+ store.addMessage(entryKey, msg);
55
+ store.addMessage(entryKey, msg);
56
+ store.addMessage(entryKey, msg);
57
+ store.removeMessage(entryKey);
58
+ expect(store.getMessageAndCount(entryKey)).toEqual({ message: msg, count: 2 });
59
+ });
60
+
61
+ it('get messages for an empty store', () => {
62
+ expect(store.getMessageKeys(10)).toEqual([]);
63
+ });
64
+
65
+ it('getMessageKeys returns an empty array if take is 0', () => {
66
+ store.addMessage(entryKey, msg);
67
+ expect(store.getMessageKeys(0)).toEqual([]);
68
+ });
69
+
70
+ it('get messages for a non-empty store when take > number of messages in store', () => {
71
+ const entryKeys = [1, 2, 3, 4, 5].map(x => new Fr(x));
72
+ entryKeys.forEach(entryKey => {
73
+ store.addMessage(entryKey, L1ToL2Message.random());
74
+ });
75
+ expect(store.getMessageKeys(10).length).toEqual(5);
76
+ });
77
+
78
+ it('get messages returns messages sorted by fees and also includes multiple of the same message', () => {
79
+ const entryKeys = [1, 2, 3, 3, 3, 4].map(x => new Fr(x));
80
+ entryKeys.forEach(entryKey => {
81
+ // set msg.fee to entryKey to test the sort.
82
+ const msg = new L1ToL2Message(
83
+ L1Actor.random(),
84
+ L2Actor.random(),
85
+ Fr.random(),
86
+ Fr.random(),
87
+ 100,
88
+ Number(entryKey.value),
89
+ entryKey,
90
+ );
91
+ store.addMessage(entryKey, msg);
92
+ });
93
+ const expectedMessgeFees = [4n, 3n, 3n, 3n]; // the top 4.
94
+ const receivedMessageFees = store.getMessageKeys(4).map(key => key.value);
95
+ expect(receivedMessageFees).toEqual(expectedMessgeFees);
96
+ });
97
+ });
@@ -0,0 +1,88 @@
1
+ import { Fr } from '@aztec/foundation/fields';
2
+ import { L1ToL2Message } from '@aztec/types';
3
+
4
+ /**
5
+ * A simple in-memory implementation of an L1 to L2 message store
6
+ * that handles message duplication.
7
+ */
8
+ export class L1ToL2MessageStore {
9
+ /**
10
+ * A map containing the message key to the corresponding L1 to L2
11
+ * messages (and the number of times the message has been seen).
12
+ */
13
+ protected store: Map<bigint, L1ToL2MessageAndCount> = new Map();
14
+
15
+ constructor() {}
16
+
17
+ addMessage(messageKey: Fr, msg: L1ToL2Message) {
18
+ const messageKeyBigInt = messageKey.value;
19
+ const msgAndCount = this.store.get(messageKeyBigInt);
20
+ if (msgAndCount) {
21
+ msgAndCount.count++;
22
+ } else {
23
+ this.store.set(messageKeyBigInt, { message: msg, count: 1 });
24
+ }
25
+ }
26
+
27
+ getMessage(messageKey: Fr): L1ToL2Message | undefined {
28
+ return this.store.get(messageKey.value)?.message;
29
+ }
30
+
31
+ getMessageAndCount(messageKey: Fr): L1ToL2MessageAndCount | undefined {
32
+ return this.store.get(messageKey.value);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Specifically for the store that will hold pending messages
38
+ * for removing messages or fetching multiple messages.
39
+ */
40
+ export class PendingL1ToL2MessageStore extends L1ToL2MessageStore {
41
+ getMessageKeys(take: number): Fr[] {
42
+ if (take < 1) {
43
+ return [];
44
+ }
45
+ // fetch `take` number of messages from the store with the highest fee.
46
+ // Note the store has multiple of the same message. So if a message has count 2, include both of them in the result:
47
+ const messages: Fr[] = [];
48
+ const sortedMessages = Array.from(this.store.values()).sort((a, b) => b.message.fee - a.message.fee);
49
+ for (const messageAndCount of sortedMessages) {
50
+ for (let i = 0; i < messageAndCount.count; i++) {
51
+ messages.push(messageAndCount.message.entryKey!);
52
+ if (messages.length === take) {
53
+ return messages;
54
+ }
55
+ }
56
+ }
57
+ return messages;
58
+ }
59
+
60
+ removeMessage(messageKey: Fr) {
61
+ // ignore 0 - messageKey is a hash, so a 0 can probabilistically never occur. It is best to skip it.
62
+ if (messageKey.equals(Fr.ZERO)) return;
63
+ const messageKeyBigInt = messageKey.value;
64
+ const msgAndCount = this.store.get(messageKeyBigInt);
65
+ if (!msgAndCount) {
66
+ throw new Error(`Message with key ${messageKeyBigInt} not found in store`);
67
+ }
68
+ if (msgAndCount.count > 1) {
69
+ msgAndCount.count--;
70
+ } else {
71
+ this.store.delete(messageKeyBigInt);
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Useful to keep track of the number of times a message has been seen.
78
+ */
79
+ type L1ToL2MessageAndCount = {
80
+ /**
81
+ * The message.
82
+ */
83
+ message: L1ToL2Message;
84
+ /**
85
+ * The number of times the message has been seen.
86
+ */
87
+ count: number;
88
+ };
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { createPublicClient, http } from 'viem';
3
+ import { localhost } from 'viem/chains';
4
+ import { Archiver, getConfigEnvVars } from './archiver/index.js';
5
+ import { MemoryArchiverStore } from './archiver/archiver_store.js';
6
+ import { createLogger } from '@aztec/foundation/log';
7
+
8
+ export * from './archiver/index.js';
9
+
10
+ const log = createLogger('aztec:archiver_init');
11
+
12
+ /**
13
+ * A function which instantiates and starts Archiver.
14
+ */
15
+ // eslint-disable-next-line require-await
16
+ async function main() {
17
+ const config = getConfigEnvVars();
18
+ const { rpcUrl, rollupContract, inboxContract, contractDeploymentEmitterContract, searchStartBlock } = config;
19
+
20
+ const publicClient = createPublicClient({
21
+ chain: localhost,
22
+ transport: http(rpcUrl),
23
+ });
24
+
25
+ const archiverStore = new MemoryArchiverStore();
26
+
27
+ const archiver = new Archiver(
28
+ publicClient,
29
+ rollupContract,
30
+ inboxContract,
31
+ contractDeploymentEmitterContract,
32
+ searchStartBlock,
33
+ archiverStore,
34
+ );
35
+
36
+ const shutdown = async () => {
37
+ await archiver.stop();
38
+ process.exit(0);
39
+ };
40
+ process.once('SIGINT', shutdown);
41
+ process.once('SIGTERM', shutdown);
42
+ }
43
+
44
+ // See https://twitter.com/Rich_Harris/status/1355289863130673153
45
+ if (process.argv[1] === fileURLToPath(import.meta.url).replace(/\/index\.js$/, '')) {
46
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
47
+ main().catch(err => {
48
+ log(err);
49
+ process.exit(1);
50
+ });
51
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "..",
3
+ "compilerOptions": {
4
+ "outDir": "dest",
5
+ "rootDir": "src",
6
+ "tsBuildInfoFile": ".tsbuildinfo"
7
+ },
8
+ "references": [
9
+ {
10
+ "path": "../ethereum"
11
+ },
12
+ {
13
+ "path": "../foundation"
14
+ },
15
+ {
16
+ "path": "../l1-artifacts"
17
+ },
18
+ {
19
+ "path": "../types"
20
+ }
21
+ ],
22
+ "include": ["src"]
23
+ }