Skip to content

Commit 80a4090

Browse files
committed
feat: Proposer invalidates previous block if needed
If the previous pending block on L1 has invalid attestations, the proposer for the next slot invalidates it as part of its multicall. Next step is for the prover to invalidate the last block in the epoch it wants to prove.
1 parent dd1e4ee commit 80a4090

File tree

16 files changed

+560
-93
lines changed

16 files changed

+560
-93
lines changed

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
}

yarn-project/ethereum/src/contracts/empire_base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { EthAddress } from '@aztec/foundation/eth-address';
12
import { Signature } from '@aztec/foundation/eth-signature';
23
import { EmpireBaseAbi } from '@aztec/l1-artifacts/EmpireBaseAbi';
34

@@ -6,6 +7,7 @@ import { type Hex, type TypedDataDefinition, encodeFunctionData } from 'viem';
67
import type { L1TxRequest } from '../l1_tx_utils.js';
78

89
export interface IEmpireBase {
10+
get address(): EthAddress;
911
getRoundInfo(
1012
rollupAddress: Hex,
1113
round: bigint,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { getPublicClient } from '@aztec/ethereum';
2+
import { EthAddress } from '@aztec/foundation/eth-address';
3+
import { Fr } from '@aztec/foundation/fields';
4+
import { type Logger, createLogger } from '@aztec/foundation/log';
5+
import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
6+
7+
import type { Anvil } from '@viem/anvil';
8+
import type { Abi } from 'viem';
9+
import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts';
10+
import { foundry } from 'viem/chains';
11+
12+
import { DefaultL1ContractsConfig } from '../config.js';
13+
import { deployL1Contracts } from '../deploy_l1_contracts.js';
14+
import { EthCheatCodes } from '../test/eth_cheat_codes.js';
15+
import { startAnvil } from '../test/start_anvil.js';
16+
import type { ViemClient } from '../types.js';
17+
import { RollupContract } from './rollup.js';
18+
19+
describe('Rollup', () => {
20+
let anvil: Anvil;
21+
let rpcUrl: string;
22+
let privateKey: PrivateKeyAccount;
23+
let logger: Logger;
24+
let publicClient: ViemClient;
25+
let cheatCodes: EthCheatCodes;
26+
27+
let vkTreeRoot: Fr;
28+
let protocolContractTreeRoot: Fr;
29+
let rollupAddress: `0x${string}`;
30+
let rollup: RollupContract;
31+
32+
beforeAll(async () => {
33+
logger = createLogger('ethereum:test:rollup');
34+
// this is the 6th address that gets funded by the junk mnemonic
35+
privateKey = privateKeyToAccount('0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba');
36+
vkTreeRoot = Fr.random();
37+
protocolContractTreeRoot = Fr.random();
38+
39+
({ anvil, rpcUrl } = await startAnvil());
40+
41+
publicClient = getPublicClient({ l1RpcUrls: [rpcUrl], l1ChainId: 31337 });
42+
cheatCodes = new EthCheatCodes([rpcUrl]);
43+
44+
const deployed = await deployL1Contracts([rpcUrl], privateKey, foundry, logger, {
45+
...DefaultL1ContractsConfig,
46+
salt: undefined,
47+
vkTreeRoot,
48+
protocolContractTreeRoot,
49+
genesisArchiveRoot: Fr.random(),
50+
realVerifier: false,
51+
});
52+
53+
rollupAddress = deployed.l1ContractAddresses.rollupAddress.toString();
54+
rollup = new RollupContract(publicClient, rollupAddress);
55+
});
56+
57+
afterAll(async () => {
58+
await cheatCodes.setIntervalMining(0);
59+
await anvil?.stop().catch(err => createLogger('cleanup').error(err));
60+
});
61+
62+
describe('makePendingBlockNumberOverride', () => {
63+
it('creates state override that correctly overrides pending block number', async () => {
64+
const testProvenBlockNumber = 42n;
65+
const testPendingBlockNumber = 100n;
66+
const newPendingBlockNumber = 150;
67+
68+
// Set storage directly using cheat codes
69+
// The storage slot stores both values: pending (high 128 bits) | proven (low 128 bits)
70+
const storageSlot = RollupContract.stfStorageSlot;
71+
const packedValue = (testPendingBlockNumber << 128n) | testProvenBlockNumber;
72+
await cheatCodes.store(EthAddress.fromString(rollupAddress), BigInt(storageSlot), packedValue);
73+
74+
// Verify the values were set correctly by calling the getters directly
75+
const provenBlockNumber = await rollup.getProvenBlockNumber();
76+
const pendingBlockNumber = await rollup.getBlockNumber();
77+
78+
expect(provenBlockNumber).toBe(testProvenBlockNumber);
79+
expect(pendingBlockNumber).toBe(testPendingBlockNumber);
80+
81+
// Create the override
82+
const stateOverride = await rollup.makePendingBlockNumberOverride(newPendingBlockNumber);
83+
84+
// Test the override using simulateContract
85+
const { result: overriddenPendingBlockNumber } = await publicClient.simulateContract({
86+
address: rollupAddress,
87+
abi: RollupAbi as Abi,
88+
functionName: 'getPendingBlockNumber',
89+
stateOverride,
90+
});
91+
92+
// The overridden value should be the new pending block number
93+
expect(overriddenPendingBlockNumber).toBe(BigInt(newPendingBlockNumber));
94+
95+
// Verify that the proven block number is preserved in the override
96+
const { result: overriddenProvenBlockNumber } = await publicClient.simulateContract({
97+
address: rollupAddress,
98+
abi: RollupAbi as Abi,
99+
functionName: 'getProvenBlockNumber',
100+
stateOverride,
101+
});
102+
103+
expect(overriddenProvenBlockNumber).toBe(testProvenBlockNumber);
104+
105+
// Verify the actual storage hasn't changed
106+
const actualPendingBlockNumber = await rollup.getBlockNumber();
107+
expect(actualPendingBlockNumber).toBe(testPendingBlockNumber);
108+
});
109+
});
110+
});

0 commit comments

Comments
 (0)