@@ -2,9 +2,11 @@ import { Blob } from '@aztec/blob-lib';
22import type { BlobSinkClientInterface } from '@aztec/blob-sink/client' ;
33import { BlobWithIndex } from '@aztec/blob-sink/types' ;
44import { GENESIS_ARCHIVE_ROOT } from '@aztec/constants' ;
5+ import type { EpochCache , EpochCommitteeInfo } from '@aztec/epoch-cache' ;
56import { DefaultL1ContractsConfig , InboxContract , RollupContract , type ViemPublicClient } from '@aztec/ethereum' ;
67import { Buffer16 , Buffer32 } from '@aztec/foundation/buffer' ;
78import { times } from '@aztec/foundation/collection' ;
9+ import { Secp256k1Signer } from '@aztec/foundation/crypto' ;
810import { EthAddress } from '@aztec/foundation/eth-address' ;
911import { Fr } from '@aztec/foundation/fields' ;
1012import { type Logger , createLogger } from '@aztec/foundation/log' ;
@@ -13,10 +15,11 @@ import { sleep } from '@aztec/foundation/sleep';
1315import { bufferToHex , withoutHexPrefix } from '@aztec/foundation/string' ;
1416import { openTmpStore } from '@aztec/kv-store/lmdb-v2' ;
1517import { type InboxAbi , RollupAbi } from '@aztec/l1-artifacts' ;
16- import { L2Block } from '@aztec/stdlib/block' ;
18+ import { CommitteeAttestation , L2Block } from '@aztec/stdlib/block' ;
1719import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers' ;
1820import { PrivateLog } from '@aztec/stdlib/logs' ;
1921import { InboxLeaf } from '@aztec/stdlib/messaging' ;
22+ import { makeBlockAttestationFromBlock } from '@aztec/stdlib/testing' ;
2023import { getTelemetryClient } from '@aztec/telemetry-client' ;
2124
2225import { jest } from '@jest/globals' ;
@@ -30,6 +33,8 @@ import { KVArchiverDataStore } from './kv_archiver_store/kv_archiver_store.js';
3033import { updateRollingHash } from './structs/inbox_message.js' ;
3134
3235interface MockRollupContractRead {
36+ /** Returns the target committee size */
37+ getTargetCommitteeSize : ( ) => Promise < bigint > ;
3338 /** Returns the rollup version. */
3439 getVersion : ( ) => Promise < bigint > ;
3540 /** Given an L2 block number, returns the archive. */
@@ -81,9 +86,19 @@ describe('Archiver', () => {
8186 publicClient . getBlockNumber . mockResolvedValue ( nums . at ( - 1 ) ! ) ;
8287 } ;
8388
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+
8498 let publicClient : MockProxy < ViemPublicClient > ;
8599 let instrumentation : MockProxy < ArchiverInstrumentation > ;
86100 let blobSinkClient : MockProxy < BlobSinkClientInterface > ;
101+ let epochCache : MockProxy < EpochCache > ;
87102 let archiverStore : ArchiverDataStore ;
88103 let l1Constants : L1RollupConstants & { l1StartBlockHash : Buffer32 } ;
89104 let now : number ;
@@ -132,6 +147,8 @@ describe('Archiver', () => {
132147 } ) as any ) ;
133148
134149 blobSinkClient = mock < BlobSinkClientInterface > ( ) ;
150+ epochCache = mock < EpochCache > ( ) ;
151+ epochCache . getCommitteeForEpoch . mockResolvedValue ( { committee : [ ] as EthAddress [ ] } as EpochCommitteeInfo ) ;
135152
136153 const tracer = getTelemetryClient ( ) . getTracer ( '' ) ;
137154 instrumentation = mock < ArchiverInstrumentation > ( { isEnabled : ( ) => true , tracer } ) ;
@@ -152,17 +169,12 @@ describe('Archiver', () => {
152169 archiverStore ,
153170 { pollingIntervalMs : 1000 , batchSize : 1000 } ,
154171 blobSinkClient ,
172+ epochCache ,
155173 instrumentation ,
156174 l1Constants ,
157175 ) ;
158176
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 ) ) ;
166178
167179 // TODO(palla/archiver) Instead of guessing the archiver requests with mockResolvedValueOnce,
168180 // we should use a mock implementation that returns the expected value based on the input.
@@ -171,7 +183,7 @@ describe('Archiver', () => {
171183 // blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b)));
172184 // blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs));
173185
174- // rollupTxs = await Promise.all(blocks.map(makeRollupTx));
186+ // rollupTxs = await Promise.all(blocks.map(b => makeRollupTx(b) ));
175187 // publicClient.getTransaction.mockImplementation((args: { hash?: `0x${string}` }) => {
176188 // const index = parseInt(withoutHexPrefix(args.hash!));
177189 // if (index > blocks.length) {
@@ -252,7 +264,7 @@ describe('Archiver', () => {
252264 let latestBlockNum = await archiver . getBlockNumber ( ) ;
253265 expect ( latestBlockNum ) . toEqual ( 0 ) ;
254266
255- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
267+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
256268 const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
257269
258270 mockL1BlockNumbers ( 2500n , 2510n , 2520n ) ;
@@ -334,7 +346,7 @@ describe('Archiver', () => {
334346
335347 const numL2BlocksInTest = 2 ;
336348
337- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
349+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
338350 const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
339351
340352 // 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', () => {
368380 } ) ;
369381 } , 10_000 ) ;
370382
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+
371458 it ( 'skip event search if no changes found' , async ( ) => {
372459 const loggerSpy = jest . spyOn ( ( archiver as any ) . log , 'debug' ) ;
373460
@@ -376,7 +463,7 @@ describe('Archiver', () => {
376463
377464 const numL2BlocksInTest = 2 ;
378465
379- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
466+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
380467 const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
381468
382469 mockL1BlockNumbers ( 50n , 100n ) ;
@@ -414,7 +501,7 @@ describe('Archiver', () => {
414501
415502 const numL2BlocksInTest = 2 ;
416503
417- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
504+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
418505 const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
419506
420507 let mockedBlockNum = 0n ;
@@ -460,7 +547,7 @@ describe('Archiver', () => {
460547 // Lets take a look to see if we can find re-org stuff!
461548 await sleep ( 2000 ) ;
462549
463- expect ( loggerSpy ) . toHaveBeenCalledWith ( `L2 prune has been detected.` ) ;
550+ expect ( loggerSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( `L2 prune has been detected` ) , expect . anything ( ) ) ;
464551
465552 // Should also see the block number be reduced
466553 latestBlockNum = await archiver . getBlockNumber ( ) ;
@@ -538,7 +625,7 @@ describe('Archiver', () => {
538625 blocks = [ l2Block ] ;
539626 const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
540627
541- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
628+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
542629 publicClient . getBlockNumber . mockResolvedValue ( l1BlockForL2Block ) ;
543630 mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
544631 makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -570,7 +657,7 @@ describe('Archiver', () => {
570657 blocks = [ l2Block ] ;
571658 const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
572659
573- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
660+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
574661 publicClient . getBlockNumber . mockResolvedValue ( l1BlockForL2Block ) ;
575662 mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
576663 makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -630,7 +717,7 @@ describe('Archiver', () => {
630717 blocks = [ l2Block ] ;
631718 const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
632719
633- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
720+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
634721 publicClient . getBlockNumber . mockResolvedValue ( lastL1BlockForEpoch ) ;
635722 mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
636723 makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -660,7 +747,7 @@ describe('Archiver', () => {
660747 it ( 'handles a block gap due to a spurious L2 prune' , async ( ) => {
661748 expect ( await archiver . getBlockNumber ( ) ) . toEqual ( 0 ) ;
662749
663- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
750+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
664751 const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
665752 const blobsFromBlocks = await Promise . all ( blocks . map ( b => makeBlobsFromBlock ( b ) ) ) ;
666753
@@ -805,7 +892,11 @@ describe('Archiver', () => {
805892 * @param block - The L2Block.
806893 * @returns A fake tx with calldata that corresponds to calling process in the Rollup contract.
807894 */
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 ( ) ) ;
809900 const header = l2Block . header . toPropose ( ) . toViem ( ) ;
810901 const blobInput = Blob . getPrefixedEthBlobCommitments ( await Blob . getBlobsPerBlock ( l2Block . body . toBlobFields ( ) ) ) ;
811902 const archive = toHex ( l2Block . archive . root . toBuffer ( ) ) ;
@@ -820,8 +911,8 @@ async function makeRollupTx(l2Block: L2Block) {
820911 stateReference,
821912 oracleInput : { feeAssetPriceModifier : 0n } ,
822913 } ,
823- RollupContract . packAttestations ( [ ] ) ,
824- [ ] ,
914+ RollupContract . packAttestations ( attestations ) ,
915+ signers . map ( signer => signer . address . toString ( ) ) ,
825916 blobInput ,
826917 ] ,
827918 } ) ;
0 commit comments