@@ -2,9 +2,11 @@ import { Blob } from '@aztec/blob-lib';
2
2
import type { BlobSinkClientInterface } from '@aztec/blob-sink/client' ;
3
3
import { BlobWithIndex } from '@aztec/blob-sink/types' ;
4
4
import { GENESIS_ARCHIVE_ROOT } from '@aztec/constants' ;
5
+ import type { EpochCache , EpochCommitteeInfo } from '@aztec/epoch-cache' ;
5
6
import { DefaultL1ContractsConfig , InboxContract , RollupContract , type ViemPublicClient } from '@aztec/ethereum' ;
6
7
import { Buffer16 , Buffer32 } from '@aztec/foundation/buffer' ;
7
8
import { times } from '@aztec/foundation/collection' ;
9
+ import { Secp256k1Signer } from '@aztec/foundation/crypto' ;
8
10
import { EthAddress } from '@aztec/foundation/eth-address' ;
9
11
import { Fr } from '@aztec/foundation/fields' ;
10
12
import { type Logger , createLogger } from '@aztec/foundation/log' ;
@@ -13,10 +15,11 @@ import { sleep } from '@aztec/foundation/sleep';
13
15
import { bufferToHex , withoutHexPrefix } from '@aztec/foundation/string' ;
14
16
import { openTmpStore } from '@aztec/kv-store/lmdb-v2' ;
15
17
import { type InboxAbi , RollupAbi } from '@aztec/l1-artifacts' ;
16
- import { L2Block } from '@aztec/stdlib/block' ;
18
+ import { CommitteeAttestation , L2Block } from '@aztec/stdlib/block' ;
17
19
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers' ;
18
20
import { PrivateLog } from '@aztec/stdlib/logs' ;
19
21
import { InboxLeaf } from '@aztec/stdlib/messaging' ;
22
+ import { makeBlockAttestationFromBlock } from '@aztec/stdlib/testing' ;
20
23
import { getTelemetryClient } from '@aztec/telemetry-client' ;
21
24
22
25
import { jest } from '@jest/globals' ;
@@ -30,6 +33,8 @@ import { KVArchiverDataStore } from './kv_archiver_store/kv_archiver_store.js';
30
33
import { updateRollingHash } from './structs/inbox_message.js' ;
31
34
32
35
interface MockRollupContractRead {
36
+ /** Returns the target committee size */
37
+ getTargetCommitteeSize : ( ) => Promise < bigint > ;
33
38
/** Returns the rollup version. */
34
39
getVersion : ( ) => Promise < bigint > ;
35
40
/** Given an L2 block number, returns the archive. */
@@ -81,9 +86,19 @@ describe('Archiver', () => {
81
86
publicClient . getBlockNumber . mockResolvedValue ( nums . at ( - 1 ) ! ) ;
82
87
} ;
83
88
89
+ const makeBlock = async ( blockNumber : number ) => {
90
+ const block = await L2Block . random ( blockNumber , txsPerBlock , blockNumber + 1 , 2 ) ;
91
+ block . header . globalVariables . timestamp = BigInt ( now + Number ( ETHEREUM_SLOT_DURATION ) * ( blockNumber + 1 ) ) ;
92
+ block . body . txEffects . forEach ( ( txEffect , i ) => {
93
+ txEffect . privateLogs = times ( getNumPrivateLogsForTx ( block . number , i ) , ( ) => PrivateLog . random ( ) ) ;
94
+ } ) ;
95
+ return block ;
96
+ } ;
97
+
84
98
let publicClient : MockProxy < ViemPublicClient > ;
85
99
let instrumentation : MockProxy < ArchiverInstrumentation > ;
86
100
let blobSinkClient : MockProxy < BlobSinkClientInterface > ;
101
+ let epochCache : MockProxy < EpochCache > ;
87
102
let archiverStore : ArchiverDataStore ;
88
103
let l1Constants : L1RollupConstants & { l1StartBlockHash : Buffer32 } ;
89
104
let now : number ;
@@ -132,6 +147,8 @@ describe('Archiver', () => {
132
147
} ) as any ) ;
133
148
134
149
blobSinkClient = mock < BlobSinkClientInterface > ( ) ;
150
+ epochCache = mock < EpochCache > ( ) ;
151
+ epochCache . getCommitteeForEpoch . mockResolvedValue ( { committee : [ ] as EthAddress [ ] } as EpochCommitteeInfo ) ;
135
152
136
153
const tracer = getTelemetryClient ( ) . getTracer ( '' ) ;
137
154
instrumentation = mock < ArchiverInstrumentation > ( { isEnabled : ( ) => true , tracer } ) ;
@@ -152,17 +169,12 @@ describe('Archiver', () => {
152
169
archiverStore ,
153
170
{ pollingIntervalMs : 1000 , batchSize : 1000 } ,
154
171
blobSinkClient ,
172
+ epochCache ,
155
173
instrumentation ,
156
174
l1Constants ,
157
175
) ;
158
176
159
- blocks = await Promise . all ( blockNumbers . map ( x => L2Block . random ( x , txsPerBlock , x + 1 , 2 ) ) ) ;
160
- blocks . forEach ( ( block , i ) => {
161
- block . header . globalVariables . timestamp = BigInt ( now + Number ( ETHEREUM_SLOT_DURATION ) * ( i + 1 ) ) ;
162
- block . body . txEffects . forEach ( ( txEffect , i ) => {
163
- txEffect . privateLogs = times ( getNumPrivateLogsForTx ( block . number , i ) , ( ) => PrivateLog . random ( ) ) ;
164
- } ) ;
165
- } ) ;
177
+ blocks = await Promise . all ( blockNumbers . map ( makeBlock ) ) ;
166
178
167
179
// TODO(palla/archiver) Instead of guessing the archiver requests with mockResolvedValueOnce,
168
180
// we should use a mock implementation that returns the expected value based on the input.
@@ -171,7 +183,7 @@ describe('Archiver', () => {
171
183
// blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b)));
172
184
// blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs));
173
185
174
- // rollupTxs = await Promise.all(blocks.map(makeRollupTx));
186
+ // rollupTxs = await Promise.all(blocks.map(b => makeRollupTx(b) ));
175
187
// publicClient.getTransaction.mockImplementation((args: { hash?: `0x${string}` }) => {
176
188
// const index = parseInt(withoutHexPrefix(args.hash!));
177
189
// if (index > blocks.length) {
@@ -252,7 +264,7 @@ describe('Archiver', () => {
252
264
let latestBlockNum = await archiver . getBlockNumber ( ) ;
253
265
expect ( latestBlockNum ) . toEqual ( 0 ) ;
254
266
255
- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
267
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
256
268
const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
257
269
258
270
mockL1BlockNumbers ( 2500n , 2510n , 2520n ) ;
@@ -334,7 +346,7 @@ describe('Archiver', () => {
334
346
335
347
const numL2BlocksInTest = 2 ;
336
348
337
- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
349
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
338
350
const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
339
351
340
352
// Here we set the current L1 block number to 102. L1 to L2 messages after this should not be read.
@@ -368,6 +380,81 @@ describe('Archiver', () => {
368
380
} ) ;
369
381
} , 10_000 ) ;
370
382
383
+ it ( 'ignores block 2 because it had invalid attestations' , async ( ) => {
384
+ let latestBlockNum = await archiver . getBlockNumber ( ) ;
385
+ expect ( latestBlockNum ) . toEqual ( 0 ) ;
386
+
387
+ // Setup a committee of 3 signers
388
+ mockRollupRead . getTargetCommitteeSize . mockResolvedValue ( 3n ) ;
389
+ const signers = times ( 3 , Secp256k1Signer . random ) ;
390
+ const committee = signers . map ( signer => signer . address ) ;
391
+ epochCache . getCommitteeForEpoch . mockResolvedValue ( { committee } as EpochCommitteeInfo ) ;
392
+
393
+ // Add the attestations from the signers to all 3 blocks
394
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b , signers ) ) ) ;
395
+ const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
396
+ const blobsFromBlocks = await Promise . all ( blocks . map ( b => makeBlobsFromBlock ( b ) ) ) ;
397
+
398
+ // And define a bad block 2 with attestations from random signers
399
+ const badBlock2 = await makeBlock ( 2 ) ;
400
+ badBlock2 . archive . root = new Fr ( 0x1002 ) ;
401
+ const badBlock2RollupTx = await makeRollupTx ( badBlock2 , times ( 3 , Secp256k1Signer . random ) ) ;
402
+ const badBlock2BlobHashes = await makeVersionedBlobHashes ( badBlock2 ) ;
403
+ const badBlock2Blobs = await makeBlobsFromBlock ( badBlock2 ) ;
404
+
405
+ // Return the archive root for the bad block 2 when queried
406
+ mockRollupRead . archiveAt . mockImplementation ( ( args : readonly [ bigint ] ) =>
407
+ Promise . resolve ( ( args [ 0 ] === 2n ? badBlock2 : blocks [ Number ( args [ 0 ] - 1n ) ] ) . archive . root . toString ( ) ) ,
408
+ ) ;
409
+
410
+ logger . warn ( `Created 3 valid blocks` ) ;
411
+ blocks . forEach ( block => logger . warn ( `Block ${ block . number } with root ${ block . archive . root . toString ( ) } ` ) ) ;
412
+ logger . warn ( `Created invalid block 2 with root ${ badBlock2 . archive . root . toString ( ) } ` ) ;
413
+
414
+ // During the first archiver loop, we fetch block 1 and the block 2 with bad attestations
415
+ publicClient . getBlockNumber . mockResolvedValue ( 85n ) ;
416
+ makeL2BlockProposedEvent ( 70n , 1n , blocks [ 0 ] . archive . root . toString ( ) , blobHashes [ 0 ] ) ;
417
+ makeL2BlockProposedEvent ( 80n , 2n , badBlock2 . archive . root . toString ( ) , badBlock2BlobHashes ) ;
418
+ mockRollup . read . status . mockResolvedValue ( [ 0n , GENESIS_ROOT , 2n , badBlock2 . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
419
+ publicClient . getTransaction . mockResolvedValueOnce ( rollupTxs [ 0 ] ) . mockResolvedValueOnce ( badBlock2RollupTx ) ;
420
+ blobSinkClient . getBlobSidecar . mockResolvedValueOnce ( blobsFromBlocks [ 0 ] ) . mockResolvedValueOnce ( badBlock2Blobs ) ;
421
+
422
+ // Start archiver, the bad block 2 should not be synced
423
+ await archiver . start ( true ) ;
424
+ latestBlockNum = await archiver . getBlockNumber ( ) ;
425
+ expect ( latestBlockNum ) . toEqual ( 1 ) ;
426
+
427
+ // Now we go for another loop, where a proper block 2 is proposed with correct attestations
428
+ // IRL there would be an "Invalidated" event, but we are not currently relying on it
429
+ logger . warn ( `Adding new block 2 with correct attestations and a block 3` ) ;
430
+ publicClient . getBlockNumber . mockResolvedValue ( 100n ) ;
431
+ makeL2BlockProposedEvent ( 90n , 2n , blocks [ 1 ] . archive . root . toString ( ) , blobHashes [ 1 ] ) ;
432
+ makeL2BlockProposedEvent ( 95n , 3n , blocks [ 2 ] . archive . root . toString ( ) , blobHashes [ 2 ] ) ;
433
+ mockRollup . read . status . mockResolvedValue ( [
434
+ 0n ,
435
+ GENESIS_ROOT ,
436
+ 3n ,
437
+ blocks [ 2 ] . archive . root . toString ( ) ,
438
+ blocks [ 0 ] . archive . root . toString ( ) ,
439
+ ] ) ;
440
+ publicClient . getTransaction . mockResolvedValueOnce ( rollupTxs [ 1 ] ) . mockResolvedValueOnce ( rollupTxs [ 2 ] ) ;
441
+ blobSinkClient . getBlobSidecar . mockResolvedValueOnce ( blobsFromBlocks [ 1 ] ) . mockResolvedValueOnce ( blobsFromBlocks [ 2 ] ) ;
442
+ mockRollupRead . archiveAt . mockImplementation ( ( args : readonly [ bigint ] ) =>
443
+ Promise . resolve ( blocks [ Number ( args [ 0 ] - 1n ) ] . archive . root . toString ( ) ) ,
444
+ ) ;
445
+
446
+ // Now we should move to block 3
447
+ await waitUntilArchiverBlock ( 3 ) ;
448
+ latestBlockNum = await archiver . getBlockNumber ( ) ;
449
+ expect ( latestBlockNum ) . toEqual ( 3 ) ;
450
+
451
+ // And block 2 should return the proper one
452
+ const [ block2 ] = await archiver . getPublishedBlocks ( 2 , 1 ) ;
453
+ expect ( block2 . block . number ) . toEqual ( 2 ) ;
454
+ expect ( block2 . block . archive . root . toString ( ) ) . toEqual ( blocks [ 1 ] . archive . root . toString ( ) ) ;
455
+ expect ( block2 . attestations . length ) . toEqual ( 3 ) ;
456
+ } , 10_000 ) ;
457
+
371
458
it ( 'skip event search if no changes found' , async ( ) => {
372
459
const loggerSpy = jest . spyOn ( ( archiver as any ) . log , 'debug' ) ;
373
460
@@ -376,7 +463,7 @@ describe('Archiver', () => {
376
463
377
464
const numL2BlocksInTest = 2 ;
378
465
379
- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
466
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
380
467
const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
381
468
382
469
mockL1BlockNumbers ( 50n , 100n ) ;
@@ -414,7 +501,7 @@ describe('Archiver', () => {
414
501
415
502
const numL2BlocksInTest = 2 ;
416
503
417
- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
504
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
418
505
const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
419
506
420
507
let mockedBlockNum = 0n ;
@@ -460,7 +547,7 @@ describe('Archiver', () => {
460
547
// Lets take a look to see if we can find re-org stuff!
461
548
await sleep ( 2000 ) ;
462
549
463
- expect ( loggerSpy ) . toHaveBeenCalledWith ( `L2 prune has been detected.` ) ;
550
+ expect ( loggerSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( `L2 prune has been detected` ) , expect . anything ( ) ) ;
464
551
465
552
// Should also see the block number be reduced
466
553
latestBlockNum = await archiver . getBlockNumber ( ) ;
@@ -538,7 +625,7 @@ describe('Archiver', () => {
538
625
blocks = [ l2Block ] ;
539
626
const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
540
627
541
- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
628
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
542
629
publicClient . getBlockNumber . mockResolvedValue ( l1BlockForL2Block ) ;
543
630
mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
544
631
makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -570,7 +657,7 @@ describe('Archiver', () => {
570
657
blocks = [ l2Block ] ;
571
658
const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
572
659
573
- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
660
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
574
661
publicClient . getBlockNumber . mockResolvedValue ( l1BlockForL2Block ) ;
575
662
mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
576
663
makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -630,7 +717,7 @@ describe('Archiver', () => {
630
717
blocks = [ l2Block ] ;
631
718
const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
632
719
633
- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
720
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
634
721
publicClient . getBlockNumber . mockResolvedValue ( lastL1BlockForEpoch ) ;
635
722
mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
636
723
makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -660,7 +747,7 @@ describe('Archiver', () => {
660
747
it ( 'handles a block gap due to a spurious L2 prune' , async ( ) => {
661
748
expect ( await archiver . getBlockNumber ( ) ) . toEqual ( 0 ) ;
662
749
663
- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
750
+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
664
751
const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
665
752
const blobsFromBlocks = await Promise . all ( blocks . map ( b => makeBlobsFromBlock ( b ) ) ) ;
666
753
@@ -805,7 +892,11 @@ describe('Archiver', () => {
805
892
* @param block - The L2Block.
806
893
* @returns A fake tx with calldata that corresponds to calling process in the Rollup contract.
807
894
*/
808
- async function makeRollupTx ( l2Block : L2Block ) {
895
+ async function makeRollupTx ( l2Block : L2Block , signers : Secp256k1Signer [ ] = [ ] ) {
896
+ const attestations = signers
897
+ . map ( signer => makeBlockAttestationFromBlock ( l2Block , signer ) )
898
+ . map ( blockAttestation => CommitteeAttestation . fromSignature ( blockAttestation . signature ) )
899
+ . map ( committeeAttestation => committeeAttestation . toViem ( ) ) ;
809
900
const header = l2Block . header . toPropose ( ) . toViem ( ) ;
810
901
const blobInput = Blob . getPrefixedEthBlobCommitments ( await Blob . getBlobsPerBlock ( l2Block . body . toBlobFields ( ) ) ) ;
811
902
const archive = toHex ( l2Block . archive . root . toBuffer ( ) ) ;
@@ -820,8 +911,8 @@ async function makeRollupTx(l2Block: L2Block) {
820
911
stateReference,
821
912
oracleInput : { feeAssetPriceModifier : 0n } ,
822
913
} ,
823
- RollupContract . packAttestations ( [ ] ) ,
824
- [ ] ,
914
+ RollupContract . packAttestations ( attestations ) ,
915
+ signers . map ( signer => signer . address . toString ( ) ) ,
825
916
blobInput ,
826
917
] ,
827
918
} ) ;
0 commit comments