Skip to content

Commit eaede92

Browse files
authored
feat: Proposer invalidates previous block if needed (#16067)
If the previous pending block on L1 has invalid attestations, the proposer for the next slot invalidates it as part of its multicall. Main changes involved: - Archiver's `validateBlockAttestations` returns an object with the validation errors that can be used for constructing the invalidate request. - Archiver also stores the validation result for the last block it has fetched, so sequencer can check it to know if it needs to invalidate. - Rollup contract wrapper can construct a state override to execute simulations as if the pending block was different, which is used to simulate that the invalid block has already been removed when constructing a block proposal. - Sequencer enqueues a call to invalidate that is executed as part of its multicall before the block proposal lands.
1 parent 12dfe78 commit eaede92

File tree

26 files changed

+1015
-225
lines changed

26 files changed

+1015
-225
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ describe('Archiver', () => {
402402
const badBlock2BlobHashes = await makeVersionedBlobHashes(badBlock2);
403403
const badBlock2Blobs = await makeBlobsFromBlock(badBlock2);
404404

405-
// Return the archive root for the bad block 2 when queried
405+
// Return the archive root for the bad block 2 when L1 is queried
406406
mockRollupRead.archiveAt.mockImplementation((args: readonly [bigint]) =>
407407
Promise.resolve((args[0] === 2n ? badBlock2 : blocks[Number(args[0] - 1n)]).archive.root.toString()),
408408
);
@@ -423,6 +423,14 @@ describe('Archiver', () => {
423423
await archiver.start(true);
424424
latestBlockNum = await archiver.getBlockNumber();
425425
expect(latestBlockNum).toEqual(1);
426+
expect(await archiver.getPendingChainValidationStatus()).toEqual(
427+
expect.objectContaining({
428+
valid: false,
429+
reason: 'invalid-attestation',
430+
invalidIndex: 0,
431+
committee,
432+
}),
433+
);
426434

427435
// Now we go for another loop, where a proper block 2 is proposed with correct attestations
428436
// IRL there would be an "Invalidated" event, but we are not currently relying on it
@@ -453,6 +461,9 @@ describe('Archiver', () => {
453461
expect(block2.block.number).toEqual(2);
454462
expect(block2.block.archive.root.toString()).toEqual(blocks[1].archive.root.toString());
455463
expect(block2.attestations.length).toEqual(3);
464+
465+
// With a valid pending chain validation status
466+
expect(await archiver.getPendingChainValidationStatus()).toEqual(expect.objectContaining({ valid: true }));
456467
}, 10_000);
457468

458469
it('skip event search if no changes found', async () => {

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@aztec/ethereum';
1111
import { maxBigint } from '@aztec/foundation/bigint';
1212
import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
13+
import { pick } from '@aztec/foundation/collection';
1314
import type { EthAddress } from '@aztec/foundation/eth-address';
1415
import { Fr } from '@aztec/foundation/fields';
1516
import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -87,7 +88,7 @@ import { InitialBlockNumberNotSequentialError, NoBlobBodiesFoundError } from './
8788
import { ArchiverInstrumentation } from './instrumentation.js';
8889
import type { InboxMessage } from './structs/inbox_message.js';
8990
import type { PublishedL2Block } from './structs/published.js';
90-
import { validateBlockAttestations } from './validation.js';
91+
import { type ValidateBlockResult, validateBlockAttestations } from './validation.js';
9192

9293
/**
9394
* Helper interface to combine all sources this archiver implementation provides.
@@ -119,6 +120,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
119120

120121
private l1BlockNumber: bigint | undefined;
121122
private l1Timestamp: bigint | undefined;
123+
private pendingChainValidationStatus: ValidateBlockResult = { valid: true };
122124
private initialSyncComplete: boolean = false;
123125

124126
public readonly tracer: Tracer;
@@ -356,10 +358,11 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
356358
// We only do this if rollup cant prune on the next submission. Otherwise we will end up
357359
// re-syncing the blocks we have just unwound above. We also dont do this if the last block is invalid,
358360
// since the archiver will rightfully refuse to sync up to it.
359-
if (!rollupCanPrune && !rollupStatus.lastBlockIsInvalid) {
361+
if (!rollupCanPrune && rollupStatus.lastBlockValidationResult.valid) {
360362
await this.checkForNewBlocksBeforeL1SyncPoint(rollupStatus, blocksSynchedTo, currentL1BlockNumber);
361363
}
362364

365+
this.pendingChainValidationStatus = rollupStatus.lastBlockValidationResult;
363366
this.instrumentation.updateL1BlockHeight(currentL1BlockNumber);
364367
}
365368

@@ -617,7 +620,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
617620
provenArchive,
618621
pendingBlockNumber: Number(pendingBlockNumber),
619622
pendingArchive,
620-
lastBlockIsInvalid: false,
623+
lastBlockValidationResult: { valid: true } as ValidateBlockResult,
621624
};
622625
this.log.trace(`Retrieved rollup status at current L1 block ${currentL1BlockNumber}.`, {
623626
localPendingBlockNumber,
@@ -793,16 +796,19 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
793796

794797
for (const block of publishedBlocks) {
795798
const isProven = block.block.number <= provenBlockNumber;
796-
if (!isProven && !(await validateBlockAttestations(block, this.epochCache, this.l1constants, this.log))) {
799+
rollupStatus.lastBlockValidationResult = isProven
800+
? { valid: true }
801+
: await validateBlockAttestations(block, this.epochCache, this.l1constants, this.log);
802+
803+
if (!rollupStatus.lastBlockValidationResult.valid) {
797804
this.log.warn(`Skipping block ${block.block.number} due to invalid attestations`, {
798805
blockHash: block.block.hash(),
799806
l1BlockNumber: block.l1.blockNumber,
807+
...pick(rollupStatus.lastBlockValidationResult, 'reason'),
800808
});
801-
rollupStatus.lastBlockIsInvalid = true;
802809
continue;
803810
}
804811

805-
rollupStatus.lastBlockIsInvalid = false;
806812
validBlocks.push(block);
807813
this.log.debug(`Ingesting new L2 block ${block.block.number} with ${block.block.body.txEffects.length} txs`, {
808814
blockHash: block.block.hash(),
@@ -1200,6 +1206,14 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
12001206
return this.store.getDebugFunctionName(address, selector);
12011207
}
12021208

1209+
getPendingChainValidationStatus(): Promise<ValidateBlockResult> {
1210+
return Promise.resolve(this.pendingChainValidationStatus);
1211+
}
1212+
1213+
isPendingChainInvalid(): Promise<boolean> {
1214+
return Promise.resolve(this.pendingChainValidationStatus.valid === false);
1215+
}
1216+
12031217
async getL2Tips(): Promise<L2Tips> {
12041218
const [latestBlockNumber, provenBlockNumber] = await Promise.all([
12051219
this.getBlockNumber(),

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,17 @@ describe('validateBlockAttestations', () => {
4545
const block = await makeBlock([], []);
4646
const result = await validateBlockAttestations(block, epochCache, constants, logger);
4747

48-
expect(result).toBe(true);
48+
expect(result.valid).toBe(true);
49+
expect(result.block).toBe(block);
4950
expect(epochCache.getCommitteeForEpoch).toHaveBeenCalledWith(0n);
5051
});
5152

5253
it('validates a block with no attestations if no committee is found', async () => {
5354
const block = await makeBlock(signers, committee);
5455
const result = await validateBlockAttestations(block, epochCache, constants, logger);
5556

56-
expect(result).toBe(true);
57+
expect(result.valid).toBe(true);
58+
expect(result.block).toBe(block);
5759
expect(epochCache.getCommitteeForEpoch).toHaveBeenCalledWith(0n);
5860
});
5961
});
@@ -73,19 +75,33 @@ describe('validateBlockAttestations', () => {
7375
const badSigner = Secp256k1Signer.random();
7476
const block = await makeBlock([...signers, badSigner], [...committee, badSigner.address]);
7577
const result = await validateBlockAttestations(block, epochCache, constants, logger);
76-
expect(result).toBe(false);
78+
expect(result.valid).toBe(false);
79+
if (!result.valid) {
80+
expect(result.reason).toBe('invalid-attestation');
81+
expect(result.block).toBe(block);
82+
expect(result.committee).toEqual(committee);
83+
if (result.reason === 'invalid-attestation') {
84+
expect(result.invalidIndex).toBe(5); // The bad signer is at index 5
85+
}
86+
}
7787
});
7888

7989
it('returns false if insufficient attestations', async () => {
8090
const block = await makeBlock(signers.slice(0, 2), committee);
8191
const result = await validateBlockAttestations(block, epochCache, constants, logger);
82-
expect(result).toBe(false);
92+
expect(result.valid).toBe(false);
93+
if (!result.valid) {
94+
expect(result.reason).toBe('insufficient-attestations');
95+
expect(result.block).toBe(block);
96+
expect(result.committee).toEqual(committee);
97+
}
8398
});
8499

85100
it('returns true if all attestations are valid and sufficient', async () => {
86101
const block = await makeBlock(signers.slice(0, 4), committee);
87102
const result = await validateBlockAttestations(block, epochCache, constants, logger);
88-
expect(result).toBe(true);
103+
expect(result.valid).toBe(true);
104+
expect(result.block).toBe(block);
89105
});
90106
});
91107
});

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import type { EpochCache } from '@aztec/epoch-cache';
22
import type { Logger } from '@aztec/foundation/log';
3-
import { type PublishedL2Block, getAttestationsFromPublishedL2Block } from '@aztec/stdlib/block';
3+
import {
4+
type PublishedL2Block,
5+
type ValidateBlockResult,
6+
getAttestationsFromPublishedL2Block,
7+
} from '@aztec/stdlib/block';
48
import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
59

10+
export type { ValidateBlockResult };
11+
612
/**
713
* Validates the attestations submitted for the given block.
814
* Returns true if the attestations are valid and sufficient, false otherwise.
915
*/
1016
export async function validateBlockAttestations(
11-
publishedBlock: Pick<PublishedL2Block, 'attestations' | 'block'>,
17+
publishedBlock: PublishedL2Block,
1218
epochCache: EpochCache,
1319
constants: Pick<L1RollupConstants, 'epochDuration'>,
1420
logger?: Logger,
15-
): Promise<boolean> {
21+
): Promise<ValidateBlockResult> {
1622
const attestations = getAttestationsFromPublishedL2Block(publishedBlock);
1723
const { block } = publishedBlock;
1824
const blockHash = await block.hash().then(hash => hash.toString());
@@ -33,17 +39,18 @@ export async function validateBlockAttestations(
3339
if (!committee || committee.length === 0) {
3440
// Q: Should we accept blocks with no committee?
3541
logger?.warn(`No committee found for epoch ${epoch} at slot ${slot}. Accepting block without validation.`, logData);
36-
return true;
42+
return { valid: true, block: publishedBlock };
3743
}
3844

3945
const committeeSet = new Set(committee.map(member => member.toString()));
4046
const requiredAttestationCount = Math.floor((committee.length * 2) / 3) + 1;
4147

42-
for (const attestation of attestations) {
48+
for (let i = 0; i < attestations.length; i++) {
49+
const attestation = attestations[i];
4350
const signer = attestation.getSender().toString();
4451
if (!committeeSet.has(signer)) {
4552
logger?.warn(`Attestation from non-committee member ${signer} at slot ${slot}`, { committee });
46-
return false;
53+
return { valid: false, reason: 'invalid-attestation', invalidIndex: i, block: publishedBlock, committee };
4754
}
4855
}
4956

@@ -53,9 +60,9 @@ export async function validateBlockAttestations(
5360
actualAttestations: attestations.length,
5461
...logData,
5562
});
56-
return false;
63+
return { valid: false, reason: 'insufficient-attestations', block: publishedBlock, committee };
5764
}
5865

5966
logger?.debug(`Block attestations validated successfully for block ${block.number} at slot ${slot}`, logData);
60-
return true;
67+
return { valid: true, block: publishedBlock };
6168
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Fr } from '@aztec/foundation/fields';
55
import { createLogger } from '@aztec/foundation/log';
66
import type { FunctionSelector } from '@aztec/stdlib/abi';
77
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
8-
import { L2Block, L2BlockHash, type L2BlockSource, type L2Tips } from '@aztec/stdlib/block';
8+
import { L2Block, L2BlockHash, type L2BlockSource, type L2Tips, type ValidateBlockResult } from '@aztec/stdlib/block';
99
import type { ContractClassPublic, ContractDataSource, ContractInstanceWithAddress } from '@aztec/stdlib/contract';
1010
import { EmptyL1RollupConstants, type L1RollupConstants, getSlotRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
1111
import { type BlockHeader, TxHash, TxReceipt, TxStatus } from '@aztec/stdlib/tx';
@@ -271,4 +271,12 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource {
271271
syncImmediate(): Promise<void> {
272272
return Promise.resolve();
273273
}
274+
275+
isPendingChainInvalid(): Promise<boolean> {
276+
return Promise.resolve(false);
277+
}
278+
279+
getPendingChainValidationStatus(): Promise<ValidateBlockResult> {
280+
return Promise.resolve({ valid: true });
281+
}
274282
}

0 commit comments

Comments
 (0)