Skip to content

Commit 941beed

Browse files
committed
feat: Archiver does not sync blocks with invalid attestations
1 parent 71e376a commit 941beed

File tree

24 files changed

+411
-110
lines changed

24 files changed

+411
-110
lines changed

yarn-project/archiver/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"@aztec/blob-lib": "workspace:^",
7070
"@aztec/blob-sink": "workspace:^",
7171
"@aztec/constants": "workspace:^",
72+
"@aztec/epoch-cache": "workspace:^",
7273
"@aztec/ethereum": "workspace:^",
7374
"@aztec/foundation": "workspace:^",
7475
"@aztec/kv-store": "workspace:^",

yarn-project/archiver/src/archiver/archiver.test.ts

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { Blob } from '@aztec/blob-lib';
22
import type { BlobSinkClientInterface } from '@aztec/blob-sink/client';
33
import { BlobWithIndex } from '@aztec/blob-sink/types';
44
import { GENESIS_ARCHIVE_ROOT } from '@aztec/constants';
5+
import type { EpochCache, EpochCommitteeInfo } from '@aztec/epoch-cache';
56
import { DefaultL1ContractsConfig, InboxContract, RollupContract, type ViemPublicClient } from '@aztec/ethereum';
67
import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
78
import { times } from '@aztec/foundation/collection';
9+
import { Secp256k1Signer } from '@aztec/foundation/crypto';
810
import { EthAddress } from '@aztec/foundation/eth-address';
911
import { Fr } from '@aztec/foundation/fields';
1012
import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -13,10 +15,11 @@ import { sleep } from '@aztec/foundation/sleep';
1315
import { bufferToHex, withoutHexPrefix } from '@aztec/foundation/string';
1416
import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
1517
import { type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
16-
import { L2Block } from '@aztec/stdlib/block';
18+
import { CommitteeAttestation, L2Block } from '@aztec/stdlib/block';
1719
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
1820
import { PrivateLog } from '@aztec/stdlib/logs';
1921
import { InboxLeaf } from '@aztec/stdlib/messaging';
22+
import { makeBlockAttestationFromBlock } from '@aztec/stdlib/testing';
2023
import { getTelemetryClient } from '@aztec/telemetry-client';
2124

2225
import { jest } from '@jest/globals';
@@ -30,6 +33,8 @@ import { KVArchiverDataStore } from './kv_archiver_store/kv_archiver_store.js';
3033
import { updateRollingHash } from './structs/inbox_message.js';
3134

3235
interface 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

Comments
 (0)