Skip to content

Commit 2ec7c7c

Browse files
authored
chore: Persist validation status in archiver (#16869)
Adds the _pending chain validation status_ to the archiver store, so it is persisted across node restarts. The pending chain validation status is whether the last block seen in the pending chain is valid or not, as in it having valid attestations or not. Also, this PR fixes the condition in which it's updated: if a new block with the same number and still invalid is detected, the validation status is updated. This is tested in #16836. This PR also changes that shape of the validation result, so instead of storing a full L2 block, we only store the identifiers and attestations needed to invalidate it. It also adds serialization and static `random` methods to some adjacent classes.
2 parents 7c09062 + e35972e commit 2ec7c7c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+752
-276
lines changed

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,9 @@ describe('Archiver', () => {
415415
return [badBlock, badBlockRollupTx, badBlockBlobHashes, badBlockBlobs] as const;
416416
};
417417

418+
// We create two bad blocks with blocknumber 2, and one bad block with blocknumber 3
418419
const [badBlock2, badBlock2RollupTx, badBlock2BlobHashes, badBlock2Blobs] = await makeBadBlock(2);
420+
const [badBlock2b, badBlock2bRollupTx, badBlock2bBlobHashes, badBlock2bBlobs] = await makeBadBlock(2);
419421
const [badBlock3, badBlock3RollupTx, badBlock3BlobHashes, badBlock3Blobs] = await makeBadBlock(3);
420422

421423
// Return the archive root for the bad block 2 when L1 is queried
@@ -428,7 +430,7 @@ describe('Archiver', () => {
428430
logger.warn(`Created invalid block 3 with root ${badBlock3.archive.root.toString()}`);
429431

430432
// During the first archiver loop, we fetch block 1 and the block 2 with bad attestations
431-
publicClient.getBlockNumber.mockResolvedValue(85n);
433+
publicClient.getBlockNumber.mockResolvedValue(82n);
432434
makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString(), blobHashes[0]);
433435
makeL2BlockProposedEvent(80n, 2n, badBlock2.archive.root.toString(), badBlock2BlobHashes);
434436
mockRollup.read.status.mockResolvedValue([0n, GENESIS_ROOT, 2n, badBlock2.archive.root.toString(), GENESIS_ROOT]);
@@ -456,10 +458,33 @@ describe('Archiver', () => {
456458
}),
457459
);
458460

461+
// Another loop, where a proposer invalidates the invalid block 2, but proposes another invalid block 2 (2b)
462+
logger.warn(`Adding new block 2 with bad attestations`);
463+
publicClient.getBlockNumber.mockResolvedValue(85n);
464+
makeL2BlockProposedEvent(85n, 2n, badBlock2b.archive.root.toString(), badBlock2bBlobHashes);
465+
mockRollup.read.status.mockResolvedValue([
466+
0n,
467+
GENESIS_ROOT,
468+
2n,
469+
badBlock2b.archive.root.toString(),
470+
blocks[0].archive.root.toString(),
471+
]);
472+
publicClient.getTransaction.mockResolvedValueOnce(badBlock2bRollupTx);
473+
blobSinkClient.getBlobSidecar.mockResolvedValueOnce(badBlock2bBlobs);
474+
475+
// Our chain validation status should be updated to point to the new bad block 2b
476+
await archiver.syncImmediate();
477+
latestBlockNum = await archiver.getBlockNumber();
478+
expect(latestBlockNum).toEqual(1);
479+
let validationStatus = await archiver.getPendingChainValidationStatus();
480+
assert(!validationStatus.valid);
481+
expect(validationStatus.block.blockNumber).toEqual(2);
482+
expect(validationStatus.block.archive.toString()).toEqual(badBlock2b.archive.root.toString());
483+
459484
// Now another loop, where we propose a block 3 with bad attestations
460485
logger.warn(`Adding new block 3 with bad attestations`);
461486
publicClient.getBlockNumber.mockResolvedValue(90n);
462-
makeL2BlockProposedEvent(85n, 3n, badBlock3.archive.root.toString(), badBlock3BlobHashes);
487+
makeL2BlockProposedEvent(88n, 3n, badBlock3.archive.root.toString(), badBlock3BlobHashes);
463488
mockRollup.read.status.mockResolvedValue([
464489
0n,
465490
GENESIS_ROOT,
@@ -475,10 +500,10 @@ describe('Archiver', () => {
475500
await archiver.syncImmediate();
476501
latestBlockNum = await archiver.getBlockNumber();
477502
expect(latestBlockNum).toEqual(1);
478-
const validationStatus = await archiver.getPendingChainValidationStatus();
503+
validationStatus = await archiver.getPendingChainValidationStatus();
479504
assert(!validationStatus.valid);
480-
expect(validationStatus.block.block.number).toEqual(2);
481-
expect(validationStatus.block.block.archive.root.toString()).toEqual(badBlock2.archive.root.toString());
505+
expect(validationStatus.block.blockNumber).toEqual(2);
506+
expect(validationStatus.block.archive.toString()).toEqual(badBlock2.archive.root.toString());
482507

483508
// Check that InvalidBlockDetected event was also emitted for bad block 3
484509
expect(invalidBlockDetectedSpy).toHaveBeenCalledWith(

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

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
120120

121121
private l1BlockNumber: bigint | undefined;
122122
private l1Timestamp: bigint | undefined;
123-
private pendingChainValidationStatus: ValidateBlockResult = { valid: true };
124123
private initialSyncComplete: boolean = false;
125124

126125
public readonly tracer: Tracer;
@@ -342,7 +341,8 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
342341

343342
// ********** Events that are processed per L2 block **********
344343
if (currentL1BlockNumber > blocksSynchedTo) {
345-
// First we retrieve new L2 blocks
344+
// First we retrieve new L2 blocks and store them in the DB. This will also update the
345+
// pending chain validation status, proven block number, and synched L1 block number.
346346
const rollupStatus = await this.handleL2blocks(blocksSynchedTo, currentL1BlockNumber);
347347
// Then we prune the current epoch if it'd reorg on next submission.
348348
// Note that we don't do this before retrieving L2 blocks because we may need to retrieve
@@ -355,21 +355,11 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
355355
currentL1Timestamp,
356356
);
357357

358-
// Update the pending chain validation status with the last block validation result.
359-
// Again, we only update if validation status changed, so in a sequence of invalid blocks
360-
// we keep track of the first invalid block so we can invalidate that one if needed.
361-
if (
362-
rollupStatus.validationResult &&
363-
rollupStatus.validationResult?.valid !== this.pendingChainValidationStatus.valid
364-
) {
365-
this.pendingChainValidationStatus = rollupStatus.validationResult;
366-
}
367-
368358
// And lastly we check if we are missing any L2 blocks behind us due to a possible L1 reorg.
369359
// We only do this if rollup cant prune on the next submission. Otherwise we will end up
370360
// re-syncing the blocks we have just unwound above. We also dont do this if the last block is invalid,
371361
// since the archiver will rightfully refuse to sync up to it.
372-
if (!rollupCanPrune && this.pendingChainValidationStatus.valid) {
362+
if (!rollupCanPrune && rollupStatus.validationResult?.valid) {
373363
await this.checkForNewBlocksBeforeL1SyncPoint(rollupStatus, blocksSynchedTo, currentL1BlockNumber);
374364
}
375365

@@ -623,14 +613,15 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
623613

624614
private async handleL2blocks(blocksSynchedTo: bigint, currentL1BlockNumber: bigint) {
625615
const localPendingBlockNumber = await this.getBlockNumber();
616+
const initialValidationResult: ValidateBlockResult | undefined = await this.store.getPendingChainValidationStatus();
626617
const [provenBlockNumber, provenArchive, pendingBlockNumber, pendingArchive, archiveForLocalPendingBlockNumber] =
627618
await this.rollup.status(BigInt(localPendingBlockNumber), { blockNumber: currentL1BlockNumber });
628619
const rollupStatus = {
629620
provenBlockNumber: Number(provenBlockNumber),
630621
provenArchive,
631622
pendingBlockNumber: Number(pendingBlockNumber),
632623
pendingArchive,
633-
validationResult: undefined as ValidateBlockResult | undefined,
624+
validationResult: initialValidationResult,
634625
};
635626
this.log.trace(`Retrieved rollup status at current L1 block ${currentL1BlockNumber}.`, {
636627
localPendingBlockNumber,
@@ -809,7 +800,15 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
809800

810801
// Only update the validation result if it has changed, so we can keep track of the first invalid block
811802
// in case there is a sequence of more than one invalid block, as we need to invalidate the first one.
812-
if (rollupStatus.validationResult?.valid !== validationResult.valid) {
803+
// There is an exception though: if an invalid block is invalidated and replaced with another invalid block,
804+
// we need to update the validation result, since we need to be able to invalidate the new one.
805+
// See test 'chain progresses if an invalid block is invalidated with an invalid one' for more info.
806+
if (
807+
rollupStatus.validationResult?.valid !== validationResult.valid ||
808+
(!rollupStatus.validationResult.valid &&
809+
!validationResult.valid &&
810+
rollupStatus.validationResult.block.blockNumber === validationResult.block.blockNumber)
811+
) {
813812
rollupStatus.validationResult = validationResult;
814813
}
815814

@@ -828,6 +827,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
828827

829828
// We keep consuming blocks if we find an invalid one, since we do not listen for BlockInvalidated events
830829
// We just pretend the invalid ones are not there and keep consuming the next blocks
830+
// Note that this breaks if the committee ever attests to a descendant of an invalid block
831831
continue;
832832
}
833833

@@ -841,7 +841,9 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
841841
}
842842

843843
try {
844-
const [processDuration] = await elapsed(() => this.store.addBlocks(validBlocks));
844+
const updatedValidationResult =
845+
rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
846+
const [processDuration] = await elapsed(() => this.store.addBlocks(validBlocks, updatedValidationResult));
845847
this.instrumentation.processNewBlocks(
846848
processDuration / validBlocks.length,
847849
validBlocks.map(b => b.block),
@@ -1228,12 +1230,12 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
12281230
return this.store.getDebugFunctionName(address, selector);
12291231
}
12301232

1231-
getPendingChainValidationStatus(): Promise<ValidateBlockResult> {
1232-
return Promise.resolve(this.pendingChainValidationStatus);
1233+
async getPendingChainValidationStatus(): Promise<ValidateBlockResult> {
1234+
return (await this.store.getPendingChainValidationStatus()) ?? { valid: true };
12331235
}
12341236

12351237
isPendingChainInvalid(): Promise<boolean> {
1236-
return Promise.resolve(this.pendingChainValidationStatus.valid === false);
1238+
return this.getPendingChainValidationStatus().then(status => !status.valid);
12371239
}
12381240

12391241
async getL2Tips(): Promise<L2Tips> {
@@ -1351,6 +1353,7 @@ export class ArchiverStoreHelper
13511353
| 'backupTo'
13521354
| 'close'
13531355
| 'transactionAsync'
1356+
| 'addBlocks'
13541357
>
13551358
{
13561359
#log = createLogger('archiver:block-helper');
@@ -1493,13 +1496,16 @@ export class ArchiverStoreHelper
14931496
return true;
14941497
}
14951498

1496-
public addBlocks(blocks: PublishedL2Block[]): Promise<boolean> {
1499+
public addBlocks(blocks: PublishedL2Block[], pendingChainValidationStatus?: ValidateBlockResult): Promise<boolean> {
14971500
// Add the blocks to the store. Store will throw if the blocks are not in order, there are gaps,
14981501
// or if the previous block is not in the store.
14991502
return this.store.transactionAsync(async () => {
15001503
await this.store.addBlocks(blocks);
15011504

15021505
const opResults = await Promise.all([
1506+
// Update the pending chain validation status if provided
1507+
pendingChainValidationStatus && this.store.setPendingChainValidationStatus(pendingChainValidationStatus),
1508+
// Add any logs emitted during the retrieved blocks
15031509
this.store.addLogs(blocks.map(block => block.block)),
15041510
// Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them
15051511
...blocks.map(async block => {
@@ -1539,6 +1545,8 @@ export class ArchiverStoreHelper
15391545
const blocks = await this.getPublishedBlocks(from - blocksToUnwind + 1, blocksToUnwind);
15401546

15411547
const opResults = await Promise.all([
1548+
// Prune rolls back to the last proven block, which is by definition valid
1549+
this.store.setPendingChainValidationStatus({ valid: true }),
15421550
// Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them
15431551
...blocks.map(async block => {
15441552
const contractClassLogs = block.block.body.txEffects.flatMap(txEffect => txEffect.contractClassLogs);
@@ -1656,4 +1664,11 @@ export class ArchiverStoreHelper
16561664
getLastL1ToL2Message(): Promise<InboxMessage | undefined> {
16571665
return this.store.getLastL1ToL2Message();
16581666
}
1667+
getPendingChainValidationStatus(): Promise<ValidateBlockResult | undefined> {
1668+
return this.store.getPendingChainValidationStatus();
1669+
}
1670+
setPendingChainValidationStatus(status: ValidateBlockResult | undefined): Promise<void> {
1671+
this.#log.debug(`Setting pending chain validation status to valid ${status?.valid}`, status);
1672+
return this.store.setPendingChainValidationStatus(status);
1673+
}
16591674
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Fr } from '@aztec/foundation/fields';
33
import type { CustomRange } from '@aztec/kv-store';
44
import type { FunctionSelector } from '@aztec/stdlib/abi';
55
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
6-
import type { L2Block } from '@aztec/stdlib/block';
6+
import type { L2Block, ValidateBlockResult } from '@aztec/stdlib/block';
77
import type {
88
ContractClassPublic,
99
ContractInstanceUpdateWithAddress,
@@ -272,4 +272,10 @@ export interface ArchiverDataStore {
272272

273273
/** Returns the last L1 to L2 message stored. */
274274
getLastL1ToL2Message(): Promise<InboxMessage | undefined>;
275+
276+
/** Returns the last synced validation status of the pending chain. */
277+
getPendingChainValidationStatus(): Promise<ValidateBlockResult | undefined>;
278+
279+
/** Sets the last synced validation status of the pending chain. */
280+
setPendingChainValidationStatus(status: ValidateBlockResult | undefined): Promise<void>;
275281
}

0 commit comments

Comments
 (0)