Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion yarn-project/archiver/src/archiver/archiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ describe('Archiver', () => {
const badBlock2BlobHashes = await makeVersionedBlobHashes(badBlock2);
const badBlock2Blobs = await makeBlobsFromBlock(badBlock2);

// Return the archive root for the bad block 2 when queried
// Return the archive root for the bad block 2 when L1 is queried
mockRollupRead.archiveAt.mockImplementation((args: readonly [bigint]) =>
Promise.resolve((args[0] === 2n ? badBlock2 : blocks[Number(args[0] - 1n)]).archive.root.toString()),
);
Expand All @@ -423,6 +423,14 @@ describe('Archiver', () => {
await archiver.start(true);
latestBlockNum = await archiver.getBlockNumber();
expect(latestBlockNum).toEqual(1);
expect(await archiver.getPendingChainValidationStatus()).toEqual(
expect.objectContaining({
valid: false,
reason: 'invalid-attestation',
invalidIndex: 0,
committee,
}),
);

// Now we go for another loop, where a proper block 2 is proposed with correct attestations
// IRL there would be an "Invalidated" event, but we are not currently relying on it
Expand Down Expand Up @@ -453,6 +461,9 @@ describe('Archiver', () => {
expect(block2.block.number).toEqual(2);
expect(block2.block.archive.root.toString()).toEqual(blocks[1].archive.root.toString());
expect(block2.attestations.length).toEqual(3);

// With a valid pending chain validation status
expect(await archiver.getPendingChainValidationStatus()).toEqual(expect.objectContaining({ valid: true }));
}, 10_000);

it('skip event search if no changes found', async () => {
Expand Down
26 changes: 20 additions & 6 deletions yarn-project/archiver/src/archiver/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@aztec/ethereum';
import { maxBigint } from '@aztec/foundation/bigint';
import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
import { pick } from '@aztec/foundation/collection';
import type { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { type Logger, createLogger } from '@aztec/foundation/log';
Expand Down Expand Up @@ -87,7 +88,7 @@ import { InitialBlockNumberNotSequentialError, NoBlobBodiesFoundError } from './
import { ArchiverInstrumentation } from './instrumentation.js';
import type { InboxMessage } from './structs/inbox_message.js';
import type { PublishedL2Block } from './structs/published.js';
import { validateBlockAttestations } from './validation.js';
import { type ValidateBlockResult, validateBlockAttestations } from './validation.js';

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

private l1BlockNumber: bigint | undefined;
private l1Timestamp: bigint | undefined;
private pendingChainValidationStatus: ValidateBlockResult = { valid: true };
private initialSyncComplete: boolean = false;

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

this.pendingChainValidationStatus = rollupStatus.lastBlockValidationResult;
this.instrumentation.updateL1BlockHeight(currentL1BlockNumber);
}

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

for (const block of publishedBlocks) {
const isProven = block.block.number <= provenBlockNumber;
if (!isProven && !(await validateBlockAttestations(block, this.epochCache, this.l1constants, this.log))) {
rollupStatus.lastBlockValidationResult = isProven
? { valid: true }
: await validateBlockAttestations(block, this.epochCache, this.l1constants, this.log);

if (!rollupStatus.lastBlockValidationResult.valid) {
this.log.warn(`Skipping block ${block.block.number} due to invalid attestations`, {
blockHash: block.block.hash(),
l1BlockNumber: block.l1.blockNumber,
...pick(rollupStatus.lastBlockValidationResult, 'reason'),
});
rollupStatus.lastBlockIsInvalid = true;
continue;
}

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

getPendingChainValidationStatus(): Promise<ValidateBlockResult> {
return Promise.resolve(this.pendingChainValidationStatus);
}

isPendingChainInvalid(): Promise<boolean> {
return Promise.resolve(this.pendingChainValidationStatus.valid === false);
}

async getL2Tips(): Promise<L2Tips> {
const [latestBlockNumber, provenBlockNumber] = await Promise.all([
this.getBlockNumber(),
Expand Down
26 changes: 21 additions & 5 deletions yarn-project/archiver/src/archiver/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,17 @@ describe('validateBlockAttestations', () => {
const block = await makeBlock([], []);
const result = await validateBlockAttestations(block, epochCache, constants, logger);

expect(result).toBe(true);
expect(result.valid).toBe(true);
expect(result.block).toBe(block);
expect(epochCache.getCommitteeForEpoch).toHaveBeenCalledWith(0n);
});

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

expect(result).toBe(true);
expect(result.valid).toBe(true);
expect(result.block).toBe(block);
expect(epochCache.getCommitteeForEpoch).toHaveBeenCalledWith(0n);
});
});
Expand All @@ -73,19 +75,33 @@ describe('validateBlockAttestations', () => {
const badSigner = Secp256k1Signer.random();
const block = await makeBlock([...signers, badSigner], [...committee, badSigner.address]);
const result = await validateBlockAttestations(block, epochCache, constants, logger);
expect(result).toBe(false);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe('invalid-attestation');
expect(result.block).toBe(block);
expect(result.committee).toEqual(committee);
if (result.reason === 'invalid-attestation') {
expect(result.invalidIndex).toBe(5); // The bad signer is at index 5
}
}
});

it('returns false if insufficient attestations', async () => {
const block = await makeBlock(signers.slice(0, 2), committee);
const result = await validateBlockAttestations(block, epochCache, constants, logger);
expect(result).toBe(false);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe('insufficient-attestations');
expect(result.block).toBe(block);
expect(result.committee).toEqual(committee);
}
});

it('returns true if all attestations are valid and sufficient', async () => {
const block = await makeBlock(signers.slice(0, 4), committee);
const result = await validateBlockAttestations(block, epochCache, constants, logger);
expect(result).toBe(true);
expect(result.valid).toBe(true);
expect(result.block).toBe(block);
});
});
});
23 changes: 15 additions & 8 deletions yarn-project/archiver/src/archiver/validation.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import type { EpochCache } from '@aztec/epoch-cache';
import type { Logger } from '@aztec/foundation/log';
import { type PublishedL2Block, getAttestationsFromPublishedL2Block } from '@aztec/stdlib/block';
import {
type PublishedL2Block,
type ValidateBlockResult,
getAttestationsFromPublishedL2Block,
} from '@aztec/stdlib/block';
import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';

export type { ValidateBlockResult };

/**
* Validates the attestations submitted for the given block.
* Returns true if the attestations are valid and sufficient, false otherwise.
*/
export async function validateBlockAttestations(
publishedBlock: Pick<PublishedL2Block, 'attestations' | 'block'>,
publishedBlock: PublishedL2Block,
epochCache: EpochCache,
constants: Pick<L1RollupConstants, 'epochDuration'>,
logger?: Logger,
): Promise<boolean> {
): Promise<ValidateBlockResult> {
const attestations = getAttestationsFromPublishedL2Block(publishedBlock);
const { block } = publishedBlock;
const blockHash = await block.hash().then(hash => hash.toString());
Expand All @@ -33,17 +39,18 @@ export async function validateBlockAttestations(
if (!committee || committee.length === 0) {
// Q: Should we accept blocks with no committee?
logger?.warn(`No committee found for epoch ${epoch} at slot ${slot}. Accepting block without validation.`, logData);
return true;
return { valid: true, block: publishedBlock };
}

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

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

Expand All @@ -53,9 +60,9 @@ export async function validateBlockAttestations(
actualAttestations: attestations.length,
...logData,
});
return false;
return { valid: false, reason: 'insufficient-attestations', block: publishedBlock, committee };
}

logger?.debug(`Block attestations validated successfully for block ${block.number} at slot ${slot}`, logData);
return true;
return { valid: true, block: publishedBlock };
}
10 changes: 9 additions & 1 deletion yarn-project/archiver/src/test/mock_l2_block_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Fr } from '@aztec/foundation/fields';
import { createLogger } from '@aztec/foundation/log';
import type { FunctionSelector } from '@aztec/stdlib/abi';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
import { L2Block, L2BlockHash, type L2BlockSource, type L2Tips } from '@aztec/stdlib/block';
import { L2Block, L2BlockHash, type L2BlockSource, type L2Tips, type ValidateBlockResult } from '@aztec/stdlib/block';
import type { ContractClassPublic, ContractDataSource, ContractInstanceWithAddress } from '@aztec/stdlib/contract';
import { EmptyL1RollupConstants, type L1RollupConstants, getSlotRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
import { type BlockHeader, TxHash, TxReceipt, TxStatus } from '@aztec/stdlib/tx';
Expand Down Expand Up @@ -271,4 +271,12 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource {
syncImmediate(): Promise<void> {
return Promise.resolve();
}

isPendingChainInvalid(): Promise<boolean> {
return Promise.resolve(false);
}

getPendingChainValidationStatus(): Promise<ValidateBlockResult> {
return Promise.resolve({ valid: true });
}
}
Loading
Loading