@@ -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,68 @@ 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+ const badBlock2RollupTx = await makeRollupTx ( badBlock2 , times ( 3 , Secp256k1Signer . random ) ) ;
401+ const badBlock2BlobHashes = await makeVersionedBlobHashes ( badBlock2 ) ;
402+ const badBlock2Blobs = await makeBlobsFromBlock ( badBlock2 ) ;
403+
404+ // During the first archiver loop, we fetch block 1 and the block 2 with bad attestations
405+ publicClient . getBlockNumber . mockResolvedValue ( 85n ) ;
406+ makeL2BlockProposedEvent ( 70n , 1n , blocks [ 0 ] . archive . root . toString ( ) , blobHashes [ 0 ] ) ;
407+ makeL2BlockProposedEvent ( 80n , 2n , badBlock2 . archive . root . toString ( ) , badBlock2BlobHashes ) ;
408+ mockRollup . read . status . mockResolvedValue ( [ 0n , GENESIS_ROOT , 2n , badBlock2 . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
409+ publicClient . getTransaction . mockResolvedValueOnce ( rollupTxs [ 0 ] ) . mockResolvedValueOnce ( badBlock2RollupTx ) ;
410+ blobSinkClient . getBlobSidecar . mockResolvedValueOnce ( blobsFromBlocks [ 0 ] ) . mockResolvedValueOnce ( badBlock2Blobs ) ;
411+
412+ // Start archiver, the bad block 2 should not be synced
413+ await archiver . start ( true ) ;
414+ latestBlockNum = await archiver . getBlockNumber ( ) ;
415+ expect ( latestBlockNum ) . toEqual ( 1 ) ;
416+
417+ // Now we go for another loop, where a proper block 2 is proposed with correct attestations
418+ // IRL there would be an "Invalidated" event, but we are not currently relying on it
419+ logger . warn ( `Adding new block 2 with correct attestations and a block 3` ) ;
420+ publicClient . getBlockNumber . mockResolvedValue ( 100n ) ;
421+ makeL2BlockProposedEvent ( 90n , 2n , blocks [ 1 ] . archive . root . toString ( ) , blobHashes [ 1 ] ) ;
422+ makeL2BlockProposedEvent ( 95n , 3n , blocks [ 2 ] . archive . root . toString ( ) , blobHashes [ 2 ] ) ;
423+ mockRollup . read . status . mockResolvedValue ( [
424+ 0n ,
425+ GENESIS_ROOT ,
426+ 3n ,
427+ blocks [ 2 ] . archive . root . toString ( ) ,
428+ blocks [ 0 ] . archive . root . toString ( ) ,
429+ ] ) ;
430+ publicClient . getTransaction . mockResolvedValueOnce ( rollupTxs [ 1 ] ) . mockResolvedValueOnce ( rollupTxs [ 2 ] ) ;
431+ blobSinkClient . getBlobSidecar . mockResolvedValueOnce ( blobsFromBlocks [ 1 ] ) . mockResolvedValueOnce ( blobsFromBlocks [ 2 ] ) ;
432+
433+ // Now we should move to block 3
434+ await waitUntilArchiverBlock ( 3 ) ;
435+ latestBlockNum = await archiver . getBlockNumber ( ) ;
436+ expect ( latestBlockNum ) . toEqual ( 3 ) ;
437+
438+ // And block 2 should return the proper one
439+ const [ block2 ] = await archiver . getPublishedBlocks ( 2 , 1 ) ;
440+ expect ( block2 . block . number ) . toEqual ( 2 ) ;
441+ expect ( block2 . block . archive . root . toString ( ) ) . toEqual ( blocks [ 1 ] . archive . root . toString ( ) ) ;
442+ expect ( block2 . attestations . length ) . toEqual ( 3 ) ;
443+ } , 10_000 ) ;
444+
371445 it ( 'skip event search if no changes found' , async ( ) => {
372446 const loggerSpy = jest . spyOn ( ( archiver as any ) . log , 'debug' ) ;
373447
@@ -376,7 +450,7 @@ describe('Archiver', () => {
376450
377451 const numL2BlocksInTest = 2 ;
378452
379- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
453+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
380454 const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
381455
382456 mockL1BlockNumbers ( 50n , 100n ) ;
@@ -414,7 +488,7 @@ describe('Archiver', () => {
414488
415489 const numL2BlocksInTest = 2 ;
416490
417- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
491+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
418492 const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
419493
420494 let mockedBlockNum = 0n ;
@@ -538,7 +612,7 @@ describe('Archiver', () => {
538612 blocks = [ l2Block ] ;
539613 const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
540614
541- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
615+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
542616 publicClient . getBlockNumber . mockResolvedValue ( l1BlockForL2Block ) ;
543617 mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
544618 makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -570,7 +644,7 @@ describe('Archiver', () => {
570644 blocks = [ l2Block ] ;
571645 const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
572646
573- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
647+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
574648 publicClient . getBlockNumber . mockResolvedValue ( l1BlockForL2Block ) ;
575649 mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
576650 makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -630,7 +704,7 @@ describe('Archiver', () => {
630704 blocks = [ l2Block ] ;
631705 const blobHashes = await makeVersionedBlobHashes ( l2Block ) ;
632706
633- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
707+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
634708 publicClient . getBlockNumber . mockResolvedValue ( lastL1BlockForEpoch ) ;
635709 mockRollup . read . status . mockResolvedValueOnce ( [ 0n , GENESIS_ROOT , 1n , l2Block . archive . root . toString ( ) , GENESIS_ROOT ] ) ;
636710 makeL2BlockProposedEvent ( l1BlockForL2Block , 1n , l2Block . archive . root . toString ( ) , blobHashes ) ;
@@ -660,7 +734,7 @@ describe('Archiver', () => {
660734 it ( 'handles a block gap due to a spurious L2 prune' , async ( ) => {
661735 expect ( await archiver . getBlockNumber ( ) ) . toEqual ( 0 ) ;
662736
663- const rollupTxs = await Promise . all ( blocks . map ( makeRollupTx ) ) ;
737+ const rollupTxs = await Promise . all ( blocks . map ( b => makeRollupTx ( b ) ) ) ;
664738 const blobHashes = await Promise . all ( blocks . map ( makeVersionedBlobHashes ) ) ;
665739 const blobsFromBlocks = await Promise . all ( blocks . map ( b => makeBlobsFromBlock ( b ) ) ) ;
666740
@@ -805,7 +879,11 @@ describe('Archiver', () => {
805879 * @param block - The L2Block.
806880 * @returns A fake tx with calldata that corresponds to calling process in the Rollup contract.
807881 */
808- async function makeRollupTx ( l2Block : L2Block ) {
882+ async function makeRollupTx ( l2Block : L2Block , signers : Secp256k1Signer [ ] = [ ] ) {
883+ const attestations = signers
884+ . map ( signer => makeBlockAttestationFromBlock ( l2Block , signer ) )
885+ . map ( blockAttestation => CommitteeAttestation . fromSignature ( blockAttestation . signature ) )
886+ . map ( committeeAttestation => committeeAttestation . toViem ( ) ) ;
809887 const header = l2Block . header . toPropose ( ) . toViem ( ) ;
810888 const blobInput = Blob . getPrefixedEthBlobCommitments ( await Blob . getBlobsPerBlock ( l2Block . body . toBlobFields ( ) ) ) ;
811889 const archive = toHex ( l2Block . archive . root . toBuffer ( ) ) ;
@@ -820,7 +898,7 @@ async function makeRollupTx(l2Block: L2Block) {
820898 stateReference,
821899 oracleInput : { feeAssetPriceModifier : 0n } ,
822900 } ,
823- RollupContract . packAttestations ( [ ] ) ,
901+ RollupContract . packAttestations ( attestations ) ,
824902 blobInput ,
825903 ] ,
826904 } ) ;
0 commit comments