@aztec/archiver 2.1.0-rc.9 → 3.0.0-devnet.2
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.
- package/README.md +4 -4
- package/dest/archiver/archiver.d.ts +12 -0
- package/dest/archiver/archiver.d.ts.map +1 -1
- package/dest/archiver/archiver.js +35 -4
- package/dest/archiver/archiver_store.d.ts +20 -0
- package/dest/archiver/archiver_store.d.ts.map +1 -1
- package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
- package/dest/archiver/archiver_store_test_suite.js +93 -3
- package/dest/archiver/config.js +1 -1
- package/dest/archiver/data_retrieval.d.ts +6 -5
- package/dest/archiver/data_retrieval.d.ts.map +1 -1
- package/dest/archiver/data_retrieval.js +30 -22
- package/dest/archiver/instrumentation.d.ts.map +1 -1
- package/dest/archiver/instrumentation.js +3 -0
- package/dest/archiver/kv_archiver_store/block_store.d.ts +26 -1
- package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/block_store.js +66 -4
- package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +5 -1
- package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/kv_archiver_store.js +13 -0
- package/dest/archiver/validation.d.ts.map +1 -1
- package/dest/archiver/validation.js +37 -42
- package/dest/factory.js +1 -1
- package/dest/test/mock_l2_block_source.d.ts +8 -1
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +56 -3
- package/package.json +14 -14
- package/src/archiver/archiver.ts +37 -3
- package/src/archiver/archiver_store.ts +24 -0
- package/src/archiver/archiver_store_test_suite.ts +104 -3
- package/src/archiver/config.ts +1 -1
- package/src/archiver/data_retrieval.ts +38 -26
- package/src/archiver/instrumentation.ts +3 -0
- package/src/archiver/kv_archiver_store/block_store.ts +80 -3
- package/src/archiver/kv_archiver_store/kv_archiver_store.ts +17 -1
- package/src/archiver/validation.ts +38 -45
- package/src/factory.ts +1 -1
- package/src/test/mock_l2_block_source.ts +61 -4
|
@@ -61,6 +61,18 @@ export interface ArchiverDataStore {
|
|
|
61
61
|
*/
|
|
62
62
|
getPublishedBlock(number: number): Promise<PublishedL2Block | undefined>;
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Returns the block for the given hash, or undefined if not exists.
|
|
66
|
+
* @param blockHash - The block hash to return.
|
|
67
|
+
*/
|
|
68
|
+
getPublishedBlockByHash(blockHash: Fr): Promise<PublishedL2Block | undefined>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the block for the given archive root, or undefined if not exists.
|
|
72
|
+
* @param archive - The archive root to return.
|
|
73
|
+
*/
|
|
74
|
+
getPublishedBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined>;
|
|
75
|
+
|
|
64
76
|
/**
|
|
65
77
|
* Gets up to `limit` amount of published L2 blocks starting from `from`.
|
|
66
78
|
* @param from - Number of the first block to return (inclusive).
|
|
@@ -77,6 +89,18 @@ export interface ArchiverDataStore {
|
|
|
77
89
|
*/
|
|
78
90
|
getBlockHeaders(from: number, limit: number): Promise<BlockHeader[]>;
|
|
79
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Returns the block header for the given hash, or undefined if not exists.
|
|
94
|
+
* @param blockHash - The block hash to return.
|
|
95
|
+
*/
|
|
96
|
+
getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the block header for the given archive root, or undefined if not exists.
|
|
100
|
+
* @param archive - The archive root to return.
|
|
101
|
+
*/
|
|
102
|
+
getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined>;
|
|
103
|
+
|
|
80
104
|
/**
|
|
81
105
|
* Gets a tx effect.
|
|
82
106
|
* @param txHash - The hash of the tx corresponding to the tx effect.
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
INITIAL_L2_BLOCK_NUM,
|
|
3
3
|
NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
|
|
4
4
|
PRIVATE_LOG_SIZE_IN_FIELDS,
|
|
5
|
-
PUBLIC_LOG_SIZE_IN_FIELDS,
|
|
6
5
|
} from '@aztec/constants';
|
|
7
6
|
import { makeTuple } from '@aztec/foundation/array';
|
|
8
7
|
import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
|
|
@@ -143,6 +142,28 @@ export function describeArchiverDataStore(
|
|
|
143
142
|
await store.addBlocks(blocks);
|
|
144
143
|
await expect(store.unwindBlocks(5, 1)).rejects.toThrow(/can only unwind blocks from the tip/i);
|
|
145
144
|
});
|
|
145
|
+
|
|
146
|
+
it('unwound blocks and headers cannot be retrieved by hash or archive', async () => {
|
|
147
|
+
await store.addBlocks(blocks);
|
|
148
|
+
const lastBlock = blocks[blocks.length - 1];
|
|
149
|
+
const blockHash = await lastBlock.block.hash();
|
|
150
|
+
const archive = lastBlock.block.archive.root;
|
|
151
|
+
|
|
152
|
+
// Verify block and header exist before unwinding
|
|
153
|
+
expect(await store.getPublishedBlockByHash(blockHash)).toBeDefined();
|
|
154
|
+
expect(await store.getPublishedBlockByArchive(archive)).toBeDefined();
|
|
155
|
+
expect(await store.getBlockHeaderByHash(blockHash)).toBeDefined();
|
|
156
|
+
expect(await store.getBlockHeaderByArchive(archive)).toBeDefined();
|
|
157
|
+
|
|
158
|
+
// Unwind the block
|
|
159
|
+
await store.unwindBlocks(lastBlock.block.number, 1);
|
|
160
|
+
|
|
161
|
+
// Verify neither block nor header can be retrieved after unwinding
|
|
162
|
+
expect(await store.getPublishedBlockByHash(blockHash)).toBeUndefined();
|
|
163
|
+
expect(await store.getPublishedBlockByArchive(archive)).toBeUndefined();
|
|
164
|
+
expect(await store.getBlockHeaderByHash(blockHash)).toBeUndefined();
|
|
165
|
+
expect(await store.getBlockHeaderByArchive(archive)).toBeUndefined();
|
|
166
|
+
});
|
|
146
167
|
});
|
|
147
168
|
|
|
148
169
|
describe('getBlocks', () => {
|
|
@@ -180,6 +201,86 @@ export function describeArchiverDataStore(
|
|
|
180
201
|
});
|
|
181
202
|
});
|
|
182
203
|
|
|
204
|
+
describe('getPublishedBlockByHash', () => {
|
|
205
|
+
beforeEach(async () => {
|
|
206
|
+
await store.addBlocks(blocks);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('retrieves a block by its hash', async () => {
|
|
210
|
+
const expectedBlock = blocks[5];
|
|
211
|
+
const blockHash = await expectedBlock.block.hash();
|
|
212
|
+
const retrievedBlock = await store.getPublishedBlockByHash(blockHash);
|
|
213
|
+
|
|
214
|
+
expect(retrievedBlock).toBeDefined();
|
|
215
|
+
expectBlocksEqual([retrievedBlock!], [expectedBlock]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('returns undefined for non-existent block hash', async () => {
|
|
219
|
+
const nonExistentHash = Fr.random();
|
|
220
|
+
await expect(store.getPublishedBlockByHash(nonExistentHash)).resolves.toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('getPublishedBlockByArchive', () => {
|
|
225
|
+
beforeEach(async () => {
|
|
226
|
+
await store.addBlocks(blocks);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('retrieves a block by its archive root', async () => {
|
|
230
|
+
const expectedBlock = blocks[3];
|
|
231
|
+
const archive = expectedBlock.block.archive.root;
|
|
232
|
+
const retrievedBlock = await store.getPublishedBlockByArchive(archive);
|
|
233
|
+
|
|
234
|
+
expect(retrievedBlock).toBeDefined();
|
|
235
|
+
expectBlocksEqual([retrievedBlock!], [expectedBlock]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('returns undefined for non-existent archive root', async () => {
|
|
239
|
+
const nonExistentArchive = Fr.random();
|
|
240
|
+
await expect(store.getPublishedBlockByArchive(nonExistentArchive)).resolves.toBeUndefined();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('getBlockHeaderByHash', () => {
|
|
245
|
+
beforeEach(async () => {
|
|
246
|
+
await store.addBlocks(blocks);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('retrieves a block header by its hash', async () => {
|
|
250
|
+
const expectedBlock = blocks[7];
|
|
251
|
+
const blockHash = await expectedBlock.block.hash();
|
|
252
|
+
const retrievedHeader = await store.getBlockHeaderByHash(blockHash);
|
|
253
|
+
|
|
254
|
+
expect(retrievedHeader).toBeDefined();
|
|
255
|
+
expect(retrievedHeader!.equals(expectedBlock.block.getBlockHeader())).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('returns undefined for non-existent block hash', async () => {
|
|
259
|
+
const nonExistentHash = Fr.random();
|
|
260
|
+
await expect(store.getBlockHeaderByHash(nonExistentHash)).resolves.toBeUndefined();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('getBlockHeaderByArchive', () => {
|
|
265
|
+
beforeEach(async () => {
|
|
266
|
+
await store.addBlocks(blocks);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('retrieves a block header by its archive root', async () => {
|
|
270
|
+
const expectedBlock = blocks[2];
|
|
271
|
+
const archive = expectedBlock.block.archive.root;
|
|
272
|
+
const retrievedHeader = await store.getBlockHeaderByArchive(archive);
|
|
273
|
+
|
|
274
|
+
expect(retrievedHeader).toBeDefined();
|
|
275
|
+
expect(retrievedHeader!.equals(expectedBlock.block.getBlockHeader())).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns undefined for non-existent archive root', async () => {
|
|
279
|
+
const nonExistentArchive = Fr.random();
|
|
280
|
+
await expect(store.getBlockHeaderByArchive(nonExistentArchive)).resolves.toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
183
284
|
describe('getSyncedL2BlockNumber', () => {
|
|
184
285
|
it('returns the block number before INITIAL_L2_BLOCK_NUM if no blocks have been added', async () => {
|
|
185
286
|
await expect(store.getSynchedL2BlockNumber()).resolves.toEqual(INITIAL_L2_BLOCK_NUM - 1);
|
|
@@ -737,8 +838,8 @@ export function describeArchiverDataStore(
|
|
|
737
838
|
const makePublicLog = (tag: Fr) =>
|
|
738
839
|
PublicLog.from({
|
|
739
840
|
contractAddress: AztecAddress.fromNumber(1),
|
|
740
|
-
|
|
741
|
-
|
|
841
|
+
// Arbitrary length
|
|
842
|
+
fields: new Array(10).fill(null).map((_, i) => (!i ? tag : new Fr(tag.toNumber() + i))),
|
|
742
843
|
});
|
|
743
844
|
|
|
744
845
|
const mockPrivateLogs = (blockNumber: number, txIndex: number) => {
|
package/src/archiver/config.ts
CHANGED
|
@@ -44,7 +44,7 @@ export const archiverConfigMappings: ConfigMappingsType<ArchiverConfig> = {
|
|
|
44
44
|
archiverStoreMapSizeKb: {
|
|
45
45
|
env: 'ARCHIVER_STORE_MAP_SIZE_KB',
|
|
46
46
|
parseEnv: (val: string | undefined) => (val ? +val : undefined),
|
|
47
|
-
description: 'The maximum possible size of the archiver DB in KB. Overwrites the general
|
|
47
|
+
description: 'The maximum possible size of the archiver DB in KB. Overwrites the general dataStoreMapSizeKb.',
|
|
48
48
|
},
|
|
49
49
|
skipValidateBlockAttestations: {
|
|
50
50
|
description: 'Whether to skip validating block attestations (use only for testing).',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BlobDeserializationError, SpongeBlob, getBlobFieldsInCheckpoint } from '@aztec/blob-lib';
|
|
2
2
|
import type { BlobSinkClientInterface } from '@aztec/blob-sink/client';
|
|
3
3
|
import type {
|
|
4
4
|
EpochProofPublicInputArgs,
|
|
@@ -15,10 +15,11 @@ import type { ViemSignature } from '@aztec/foundation/eth-signature';
|
|
|
15
15
|
import { Fr } from '@aztec/foundation/fields';
|
|
16
16
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
17
17
|
import { type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
18
|
-
import { Body, CommitteeAttestation, L2Block, PublishedL2Block } from '@aztec/stdlib/block';
|
|
18
|
+
import { Body, CommitteeAttestation, L2Block, L2BlockHeader, PublishedL2Block } from '@aztec/stdlib/block';
|
|
19
19
|
import { Proof } from '@aztec/stdlib/proofs';
|
|
20
|
+
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
20
21
|
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
|
|
21
|
-
import {
|
|
22
|
+
import { GlobalVariables, StateReference } from '@aztec/stdlib/tx';
|
|
22
23
|
|
|
23
24
|
import {
|
|
24
25
|
type GetContractEventsReturnType,
|
|
@@ -39,21 +40,21 @@ export type RetrievedL2Block = {
|
|
|
39
40
|
l2BlockNumber: number;
|
|
40
41
|
archiveRoot: Fr;
|
|
41
42
|
stateReference: StateReference;
|
|
42
|
-
header:
|
|
43
|
-
|
|
43
|
+
header: CheckpointHeader;
|
|
44
|
+
blobFields: Fr[];
|
|
44
45
|
l1: L1PublishedData;
|
|
45
46
|
chainId: Fr;
|
|
46
47
|
version: Fr;
|
|
47
48
|
attestations: CommitteeAttestation[];
|
|
48
49
|
};
|
|
49
50
|
|
|
50
|
-
export function retrievedBlockToPublishedL2Block(retrievedBlock: RetrievedL2Block): PublishedL2Block {
|
|
51
|
+
export async function retrievedBlockToPublishedL2Block(retrievedBlock: RetrievedL2Block): Promise<PublishedL2Block> {
|
|
51
52
|
const {
|
|
52
53
|
l2BlockNumber,
|
|
53
54
|
archiveRoot,
|
|
54
55
|
stateReference,
|
|
55
|
-
header:
|
|
56
|
-
|
|
56
|
+
header: checkpointHeader,
|
|
57
|
+
blobFields,
|
|
57
58
|
l1,
|
|
58
59
|
chainId,
|
|
59
60
|
version,
|
|
@@ -69,20 +70,32 @@ export function retrievedBlockToPublishedL2Block(retrievedBlock: RetrievedL2Bloc
|
|
|
69
70
|
chainId,
|
|
70
71
|
version,
|
|
71
72
|
blockNumber: l2BlockNumber,
|
|
72
|
-
slotNumber:
|
|
73
|
-
timestamp:
|
|
74
|
-
coinbase:
|
|
75
|
-
feeRecipient:
|
|
76
|
-
gasFees:
|
|
73
|
+
slotNumber: checkpointHeader.slotNumber,
|
|
74
|
+
timestamp: checkpointHeader.timestamp,
|
|
75
|
+
coinbase: checkpointHeader.coinbase,
|
|
76
|
+
feeRecipient: checkpointHeader.feeRecipient,
|
|
77
|
+
gasFees: checkpointHeader.gasFees,
|
|
77
78
|
});
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// TODO(#17027)
|
|
81
|
+
// This works when there's only one block in the checkpoint.
|
|
82
|
+
// If there's more than one block, we need to build the spongeBlob from the endSpongeBlob of the previous block.
|
|
83
|
+
const spongeBlob = await SpongeBlob.init(blobFields.length);
|
|
84
|
+
// Skip the first field which is the checkpoint prefix indicating the number of total blob fields in a checkpoint.
|
|
85
|
+
const blockBlobFields = blobFields.slice(1);
|
|
86
|
+
await spongeBlob.absorb(blockBlobFields);
|
|
87
|
+
const spongeBlobHash = await spongeBlob.squeeze();
|
|
88
|
+
|
|
89
|
+
const body = Body.fromBlobFields(blockBlobFields);
|
|
90
|
+
|
|
91
|
+
const header = L2BlockHeader.from({
|
|
92
|
+
lastArchive: new AppendOnlyTreeSnapshot(checkpointHeader.lastArchiveRoot, l2BlockNumber),
|
|
93
|
+
contentCommitment: checkpointHeader.contentCommitment,
|
|
82
94
|
state: stateReference,
|
|
83
95
|
globalVariables,
|
|
84
96
|
totalFees: body.txEffects.reduce((accum, txEffect) => accum.add(txEffect.transactionFee), Fr.ZERO),
|
|
85
|
-
totalManaUsed:
|
|
97
|
+
totalManaUsed: checkpointHeader.totalManaUsed,
|
|
98
|
+
spongeBlobHash,
|
|
86
99
|
});
|
|
87
100
|
|
|
88
101
|
const block = new L2Block(archive, header, body);
|
|
@@ -337,17 +350,19 @@ async function getBlockFromRollupTx(
|
|
|
337
350
|
targetCommitteeSize,
|
|
338
351
|
});
|
|
339
352
|
|
|
340
|
-
|
|
341
|
-
// This is likely going to be a footgun
|
|
342
|
-
const header = ProposedBlockHeader.fromViem(decodedArgs.header);
|
|
353
|
+
const header = CheckpointHeader.fromViem(decodedArgs.header);
|
|
343
354
|
const blobBodies = await blobSinkClient.getBlobSidecar(blockHash, blobHashes);
|
|
344
355
|
if (blobBodies.length === 0) {
|
|
345
356
|
throw new NoBlobBodiesFoundError(l2BlockNumber);
|
|
346
357
|
}
|
|
347
358
|
|
|
348
|
-
let
|
|
359
|
+
let blobFields: Fr[];
|
|
349
360
|
try {
|
|
350
|
-
|
|
361
|
+
// Get the fields that were actually added in the checkpoint. And check the encoding of the fields.
|
|
362
|
+
blobFields = getBlobFieldsInCheckpoint(
|
|
363
|
+
blobBodies.map(b => b.blob),
|
|
364
|
+
true /* checkEncoding */,
|
|
365
|
+
);
|
|
351
366
|
} catch (err: any) {
|
|
352
367
|
if (err instanceof BlobDeserializationError) {
|
|
353
368
|
logger.fatal(err.message);
|
|
@@ -357,9 +372,6 @@ async function getBlockFromRollupTx(
|
|
|
357
372
|
throw err;
|
|
358
373
|
}
|
|
359
374
|
|
|
360
|
-
// The blob source gives us blockFields, and we must construct the body from them:
|
|
361
|
-
const body = Body.fromBlobFields(blockFields);
|
|
362
|
-
|
|
363
375
|
const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
|
|
364
376
|
|
|
365
377
|
const stateReference = StateReference.fromViem(decodedArgs.stateReference);
|
|
@@ -369,7 +381,7 @@ async function getBlockFromRollupTx(
|
|
|
369
381
|
archiveRoot,
|
|
370
382
|
stateReference,
|
|
371
383
|
header,
|
|
372
|
-
|
|
384
|
+
blobFields,
|
|
373
385
|
attestations,
|
|
374
386
|
};
|
|
375
387
|
}
|
|
@@ -152,6 +152,9 @@ export class ArchiverInstrumentation {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
public processNewMessages(count: number, syncPerMessageMs: number) {
|
|
155
|
+
if (count === 0) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
155
158
|
this.syncMessageCount.add(count);
|
|
156
159
|
this.syncDurationPerMessage.record(Math.ceil(syncPerMessageMs));
|
|
157
160
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
PublishedL2Block,
|
|
15
15
|
type ValidateBlockResult,
|
|
16
16
|
} from '@aztec/stdlib/block';
|
|
17
|
-
import { deserializeValidateBlockResult, serializeValidateBlockResult } from '@aztec/stdlib/block';
|
|
17
|
+
import { L2BlockHeader, deserializeValidateBlockResult, serializeValidateBlockResult } from '@aztec/stdlib/block';
|
|
18
18
|
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
|
|
19
19
|
import {
|
|
20
20
|
BlockHeader,
|
|
@@ -66,6 +66,12 @@ export class BlockStore {
|
|
|
66
66
|
/** Index mapping a contract's address (as a string) to its location in a block */
|
|
67
67
|
#contractIndex: AztecAsyncMap<string, BlockIndexValue>;
|
|
68
68
|
|
|
69
|
+
/** Index mapping block hash to block number */
|
|
70
|
+
#blockHashIndex: AztecAsyncMap<string, number>;
|
|
71
|
+
|
|
72
|
+
/** Index mapping block archive to block number */
|
|
73
|
+
#blockArchiveIndex: AztecAsyncMap<string, number>;
|
|
74
|
+
|
|
69
75
|
#log = createLogger('archiver:block_store');
|
|
70
76
|
|
|
71
77
|
constructor(private db: AztecAsyncKVStore) {
|
|
@@ -73,6 +79,8 @@ export class BlockStore {
|
|
|
73
79
|
this.#blockTxs = db.openMap('archiver_block_txs');
|
|
74
80
|
this.#txEffects = db.openMap('archiver_tx_effects');
|
|
75
81
|
this.#contractIndex = db.openMap('archiver_contract_index');
|
|
82
|
+
this.#blockHashIndex = db.openMap('archiver_block_hash_index');
|
|
83
|
+
this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
|
|
76
84
|
this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
|
|
77
85
|
this.#lastProvenL2Block = db.openSingleton('archiver_last_proven_l2_block');
|
|
78
86
|
this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
|
|
@@ -132,6 +140,10 @@ export class BlockStore {
|
|
|
132
140
|
blockHash.toString(),
|
|
133
141
|
Buffer.concat(block.block.body.txEffects.map(tx => tx.txHash.toBuffer())),
|
|
134
142
|
);
|
|
143
|
+
|
|
144
|
+
// Update indices for block hash and archive
|
|
145
|
+
await this.#blockHashIndex.set(blockHash.toString(), block.block.number);
|
|
146
|
+
await this.#blockArchiveIndex.set(block.block.archive.root.toString(), block.block.number);
|
|
135
147
|
}
|
|
136
148
|
|
|
137
149
|
await this.#lastSynchedL1Block.set(blocks[blocks.length - 1].l1.blockNumber);
|
|
@@ -170,6 +182,11 @@ export class BlockStore {
|
|
|
170
182
|
await Promise.all(block.block.body.txEffects.map(tx => this.#txEffects.delete(tx.txHash.toString())));
|
|
171
183
|
const blockHash = (await block.block.hash()).toString();
|
|
172
184
|
await this.#blockTxs.delete(blockHash);
|
|
185
|
+
|
|
186
|
+
// Clean up indices
|
|
187
|
+
await this.#blockHashIndex.delete(blockHash);
|
|
188
|
+
await this.#blockArchiveIndex.delete(block.block.archive.root.toString());
|
|
189
|
+
|
|
173
190
|
this.#log.debug(`Unwound block ${blockNumber} ${blockHash}`);
|
|
174
191
|
}
|
|
175
192
|
|
|
@@ -205,6 +222,66 @@ export class BlockStore {
|
|
|
205
222
|
return this.getBlockFromBlockStorage(blockNumber, blockStorage);
|
|
206
223
|
}
|
|
207
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Gets an L2 block by its hash.
|
|
227
|
+
* @param blockHash - The hash of the block to return.
|
|
228
|
+
* @returns The requested L2 block.
|
|
229
|
+
*/
|
|
230
|
+
async getBlockByHash(blockHash: L2BlockHash): Promise<PublishedL2Block | undefined> {
|
|
231
|
+
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
|
|
232
|
+
if (blockNumber === undefined) {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
return this.getBlock(blockNumber);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Gets an L2 block by its archive root.
|
|
240
|
+
* @param archive - The archive root of the block to return.
|
|
241
|
+
* @returns The requested L2 block.
|
|
242
|
+
*/
|
|
243
|
+
async getBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined> {
|
|
244
|
+
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
|
|
245
|
+
if (blockNumber === undefined) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
return this.getBlock(blockNumber);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Gets a block header by its hash.
|
|
253
|
+
* @param blockHash - The hash of the block to return.
|
|
254
|
+
* @returns The requested block header.
|
|
255
|
+
*/
|
|
256
|
+
async getBlockHeaderByHash(blockHash: L2BlockHash): Promise<BlockHeader | undefined> {
|
|
257
|
+
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
|
|
258
|
+
if (blockNumber === undefined) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const blockStorage = await this.#blocks.getAsync(blockNumber);
|
|
262
|
+
if (!blockStorage || !blockStorage.header) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
return L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Gets a block header by its archive root.
|
|
270
|
+
* @param archive - The archive root of the block to return.
|
|
271
|
+
* @returns The requested block header.
|
|
272
|
+
*/
|
|
273
|
+
async getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
|
|
274
|
+
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
|
|
275
|
+
if (blockNumber === undefined) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
const blockStorage = await this.#blocks.getAsync(blockNumber);
|
|
279
|
+
if (!blockStorage || !blockStorage.header) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
return L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader();
|
|
283
|
+
}
|
|
284
|
+
|
|
208
285
|
/**
|
|
209
286
|
* Gets the headers for a sequence of L2 blocks.
|
|
210
287
|
* @param start - Number of the first block to return (inclusive).
|
|
@@ -213,7 +290,7 @@ export class BlockStore {
|
|
|
213
290
|
*/
|
|
214
291
|
async *getBlockHeaders(start: number, limit: number): AsyncIterableIterator<BlockHeader> {
|
|
215
292
|
for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
|
|
216
|
-
const header =
|
|
293
|
+
const header = L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader();
|
|
217
294
|
if (header.getBlockNumber() !== blockNumber) {
|
|
218
295
|
throw new Error(
|
|
219
296
|
`Block number mismatch when retrieving block header from archive (expected ${blockNumber} but got ${header.getBlockNumber()})`,
|
|
@@ -240,7 +317,7 @@ export class BlockStore {
|
|
|
240
317
|
blockNumber: number,
|
|
241
318
|
blockStorage: BlockStorage,
|
|
242
319
|
): Promise<PublishedL2Block | undefined> {
|
|
243
|
-
const header =
|
|
320
|
+
const header = L2BlockHeader.fromBuffer(blockStorage.header);
|
|
244
321
|
const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive);
|
|
245
322
|
const blockHash = blockStorage.blockHash;
|
|
246
323
|
const blockHashString = bufferToHex(blockHash);
|
|
@@ -5,7 +5,7 @@ import { createLogger } from '@aztec/foundation/log';
|
|
|
5
5
|
import type { AztecAsyncKVStore, CustomRange, StoreSize } from '@aztec/kv-store';
|
|
6
6
|
import { FunctionSelector } from '@aztec/stdlib/abi';
|
|
7
7
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
8
|
-
import type
|
|
8
|
+
import { type L2Block, L2BlockHash, type ValidateBlockResult } from '@aztec/stdlib/block';
|
|
9
9
|
import type {
|
|
10
10
|
ContractClassPublic,
|
|
11
11
|
ContractDataSource,
|
|
@@ -204,6 +204,14 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc
|
|
|
204
204
|
return this.#blockStore.getBlock(number);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
getPublishedBlockByHash(blockHash: Fr): Promise<PublishedL2Block | undefined> {
|
|
208
|
+
return this.#blockStore.getBlockByHash(L2BlockHash.fromField(blockHash));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getPublishedBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined> {
|
|
212
|
+
return this.#blockStore.getBlockByArchive(archive);
|
|
213
|
+
}
|
|
214
|
+
|
|
207
215
|
/**
|
|
208
216
|
* Gets up to `limit` amount of L2 blocks starting from `from`.
|
|
209
217
|
*
|
|
@@ -226,6 +234,14 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc
|
|
|
226
234
|
return toArray(this.#blockStore.getBlockHeaders(start, limit));
|
|
227
235
|
}
|
|
228
236
|
|
|
237
|
+
getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined> {
|
|
238
|
+
return this.#blockStore.getBlockHeaderByHash(L2BlockHash.fromField(blockHash));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
|
|
242
|
+
return this.#blockStore.getBlockHeaderByArchive(archive);
|
|
243
|
+
}
|
|
244
|
+
|
|
229
245
|
/**
|
|
230
246
|
* Gets a tx effect.
|
|
231
247
|
* @param txHash - The hash of the tx corresponding to the tx effect.
|
|
@@ -3,8 +3,9 @@ import { compactArray } from '@aztec/foundation/collection';
|
|
|
3
3
|
import type { Logger } from '@aztec/foundation/log';
|
|
4
4
|
import {
|
|
5
5
|
type PublishedL2Block,
|
|
6
|
+
type ValidateBlockNegativeResult,
|
|
6
7
|
type ValidateBlockResult,
|
|
7
|
-
|
|
8
|
+
getAttestationInfoFromPublishedL2Block,
|
|
8
9
|
} from '@aztec/stdlib/block';
|
|
9
10
|
import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
10
11
|
|
|
@@ -20,8 +21,8 @@ export async function validateBlockAttestations(
|
|
|
20
21
|
constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
21
22
|
logger?: Logger,
|
|
22
23
|
): Promise<ValidateBlockResult> {
|
|
23
|
-
const
|
|
24
|
-
const attestors = compactArray(
|
|
24
|
+
const attestorInfos = getAttestationInfoFromPublishedL2Block(publishedBlock);
|
|
25
|
+
const attestors = compactArray(attestorInfos.map(info => ('address' in info ? info.address : undefined)));
|
|
25
26
|
const { block } = publishedBlock;
|
|
26
27
|
const blockHash = await block.hash().then(hash => hash.toString());
|
|
27
28
|
const archiveRoot = block.archive.root.toString();
|
|
@@ -32,10 +33,7 @@ export async function validateBlockAttestations(
|
|
|
32
33
|
|
|
33
34
|
logger?.debug(`Validating attestations for block ${block.number} at slot ${slot} in epoch ${epoch}`, {
|
|
34
35
|
committee: (committee ?? []).map(member => member.toString()),
|
|
35
|
-
recoveredAttestors:
|
|
36
|
-
invalidAttestations: attestations
|
|
37
|
-
.filter(a => a !== undefined && a.tryGetSender() === undefined)
|
|
38
|
-
.map(a => a?.toInspect()),
|
|
36
|
+
recoveredAttestors: attestorInfos,
|
|
39
37
|
postedAttestations: publishedBlock.attestations.map(a => (a.address.isZero() ? a.signature : a.address).toString()),
|
|
40
38
|
...logData,
|
|
41
39
|
});
|
|
@@ -48,57 +46,52 @@ export async function validateBlockAttestations(
|
|
|
48
46
|
const committeeSet = new Set(committee.map(member => member.toString()));
|
|
49
47
|
const requiredAttestationCount = Math.floor((committee.length * 2) / 3) + 1;
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
});
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
60
|
+
for (let i = 0; i < attestorInfos.length; i++) {
|
|
61
|
+
const info = attestorInfos[i];
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
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 };
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
84
|
}
|
|
69
|
-
|
|
70
|
-
const reason = 'invalid-attestation';
|
|
71
|
-
return {
|
|
72
|
-
valid: false,
|
|
73
|
-
reason,
|
|
74
|
-
invalidIndex: i,
|
|
75
|
-
block: publishedBlock.block.toBlockInfo(),
|
|
76
|
-
committee,
|
|
77
|
-
seed,
|
|
78
|
-
epoch,
|
|
79
|
-
attestors,
|
|
80
|
-
attestations: publishedBlock.attestations,
|
|
81
|
-
};
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
const validAttestationCount =
|
|
87
|
+
const validAttestationCount = attestorInfos.filter(info => info.status === 'recovered-from-signature').length;
|
|
85
88
|
if (validAttestationCount < requiredAttestationCount) {
|
|
86
89
|
logger?.warn(`Insufficient attestations for block at slot ${slot}`, {
|
|
87
90
|
requiredAttestations: requiredAttestationCount,
|
|
88
91
|
actualAttestations: validAttestationCount,
|
|
89
92
|
...logData,
|
|
90
93
|
});
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
valid: false,
|
|
94
|
-
reason,
|
|
95
|
-
block: publishedBlock.block.toBlockInfo(),
|
|
96
|
-
committee,
|
|
97
|
-
seed,
|
|
98
|
-
epoch,
|
|
99
|
-
attestors,
|
|
100
|
-
attestations: publishedBlock.attestations,
|
|
101
|
-
};
|
|
94
|
+
return failedValidationResult('insufficient-attestations');
|
|
102
95
|
}
|
|
103
96
|
|
|
104
97
|
logger?.debug(`Block attestations validated successfully for block ${block.number} at slot ${slot}`, logData);
|
package/src/factory.ts
CHANGED
|
@@ -20,7 +20,7 @@ export async function createArchiverStore(
|
|
|
20
20
|
) {
|
|
21
21
|
const config = {
|
|
22
22
|
...userConfig,
|
|
23
|
-
|
|
23
|
+
dataStoreMapSizeKb: userConfig.archiverStoreMapSizeKb ?? userConfig.dataStoreMapSizeKb,
|
|
24
24
|
};
|
|
25
25
|
const store = await createStore(ARCHIVER_STORE_NAME, ARCHIVER_DB_VERSION, config, createLogger('archiver:lmdb'));
|
|
26
26
|
return new KVArchiverDataStore(store, config.maxLogs);
|