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
2 changes: 1 addition & 1 deletion yarn-project/.claude/rules/typescript-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Type Safety

- Avoid `as Type` casts; prefer type guards
- Never use `as any`
- Never use `as any`; if a subclass need access to private members, change the visibility to `protected` instead of doing `this as any`
- Use branded types for common domain types (`SlotNumber`, `BlockNumber`, `EpochNumber`, etc.)
- Type guard functions follow `is<TypeName>` naming convention:
```typescript
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/archiver/src/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
public readonly events: ArchiverEmitter;

/** A loop in which we will be continually fetching new checkpoints. */
private runningPromise: RunningPromise;
protected runningPromise: RunningPromise;

/** L1 synchronizer that handles fetching checkpoints and messages from L1. */
private readonly synchronizer: ArchiverL1Synchronizer;
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/archiver/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ export async function createArchiver(
return archiver;
}

async function registerProtocolContracts(store: KVArchiverDataStore) {
/** Registers protocol contracts in the archiver store. */
export async function registerProtocolContracts(store: KVArchiverDataStore) {
const blockNumber = 0;
for (const name of protocolContractNames) {
const provider = new BundledProtocolContractsProvider();
Expand Down
1 change: 1 addition & 0 deletions yarn-project/archiver/src/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './mock_structs.js';
export * from './mock_l2_block_source.js';
export * from './mock_l1_to_l2_message_source.js';
export * from './mock_archiver.js';
export * from './noop_l1_archiver.js';
28 changes: 22 additions & 6 deletions yarn-project/archiver/src/test/mock_structs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,40 @@ export function makeInboxMessage(
}

export function makeInboxMessages(
count: number,
totalCount: number,
opts: {
initialHash?: Buffer16;
initialCheckpointNumber?: CheckpointNumber;
messagesPerCheckpoint?: number;
overrideFn?: (msg: InboxMessage, index: number) => InboxMessage;
} = {},
): InboxMessage[] {
const { initialHash = Buffer16.ZERO, overrideFn = msg => msg, initialCheckpointNumber = 1 } = opts;
const {
initialHash = Buffer16.ZERO,
overrideFn = msg => msg,
initialCheckpointNumber = CheckpointNumber(1),
messagesPerCheckpoint = 1,
} = opts;

const messages: InboxMessage[] = [];
let rollingHash = initialHash;
for (let i = 0; i < count; i++) {
for (let i = 0; i < totalCount; i++) {
const msgIndex = i % messagesPerCheckpoint;
const checkpointNumber = CheckpointNumber.fromBigInt(
BigInt(initialCheckpointNumber) + BigInt(i) / BigInt(messagesPerCheckpoint),
);
const leaf = Fr.random();
const checkpointNumber = CheckpointNumber(i + initialCheckpointNumber);
const message = overrideFn(makeInboxMessage(rollingHash, { leaf, checkpointNumber }), i);
const message = overrideFn(
makeInboxMessage(rollingHash, {
leaf,
checkpointNumber,
index: InboxLeaf.smallestIndexForCheckpoint(checkpointNumber) + BigInt(msgIndex),
}),
i,
);
rollingHash = message.rollingHash;
messages.push(message);
}

return messages;
}

Expand Down
109 changes: 109 additions & 0 deletions yarn-project/archiver/src/test/noop_l1_archiver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { BlobClientInterface } from '@aztec/blob-client/client';
import type { RollupContract } from '@aztec/ethereum/contracts';
import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
import { Buffer32 } from '@aztec/foundation/buffer';
import { Fr } from '@aztec/foundation/curves/bn254';
import { EthAddress } from '@aztec/foundation/eth-address';
import type { FunctionsOf } from '@aztec/foundation/types';
import type { ArchiverEmitter } from '@aztec/stdlib/block';
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';

import { mock } from 'jest-mock-extended';
import { EventEmitter } from 'node:events';

import { Archiver } from '../archiver.js';
import { ArchiverInstrumentation } from '../modules/instrumentation.js';
import type { ArchiverL1Synchronizer } from '../modules/l1_synchronizer.js';
import type { KVArchiverDataStore } from '../store/kv_archiver_store.js';

/** Noop L1 synchronizer for testing without L1 connectivity. */
class NoopL1Synchronizer implements FunctionsOf<ArchiverL1Synchronizer> {
public readonly tracer: Tracer;

constructor(tracer: Tracer) {
this.tracer = tracer;
}

setConfig(_config: unknown) {}
getL1BlockNumber(): bigint | undefined {
return 0n;
}
getL1Timestamp(): bigint | undefined {
return 0n;
}
testEthereumNodeSynced(): Promise<void> {
return Promise.resolve();
}
syncFromL1(_initialSyncComplete: boolean): Promise<void> {
return Promise.resolve();
}
}

/**
* Archiver with mocked L1 connectivity for testing.
* Uses mock L1 clients and a noop synchronizer, enabling tests that
* don't require real Ethereum connectivity.
*/
export class NoopL1Archiver extends Archiver {
constructor(
dataStore: KVArchiverDataStore,
l1Constants: L1RollupConstants & { genesisArchiveRoot: Fr },
instrumentation: ArchiverInstrumentation,
) {
// Create mocks for L1 clients
const publicClient = mock<ViemPublicClient>();
const debugClient = mock<ViemPublicDebugClient>();
const rollup = mock<RollupContract>();
const blobClient = mock<BlobClientInterface>();

// Mock methods called during start()
blobClient.testSources.mockResolvedValue();
publicClient.getBlockNumber.mockResolvedValue(1n);

const events = new EventEmitter() as ArchiverEmitter;
const synchronizer = new NoopL1Synchronizer(instrumentation.tracer);

super(
publicClient,
debugClient,
rollup,
{
registryAddress: EthAddress.ZERO,
governanceProposerAddress: EthAddress.ZERO,
slashFactoryAddress: EthAddress.ZERO,
slashingProposerAddress: EthAddress.ZERO,
},
dataStore,
{
pollingIntervalMs: 1000,
batchSize: 100,
skipValidateCheckpointAttestations: true,
maxAllowedEthClientDriftSeconds: 300,
ethereumAllowNoDebugHosts: true, // Skip trace validation
},
blobClient,
instrumentation,
{ ...l1Constants, l1StartBlockHash: Buffer32.random() },
synchronizer as ArchiverL1Synchronizer,
events,
);
}

/** Override start to skip L1 validation checks. */
public override start(_blockUntilSynced?: boolean): Promise<void> {
// Just start the running promise without L1 checks
this.runningPromise.start();
return Promise.resolve();
}
}

/** Creates an archiver with mocked L1 connectivity for testing. */
export async function createNoopL1Archiver(
dataStore: KVArchiverDataStore,
l1Constants: L1RollupConstants & { genesisArchiveRoot: Fr },
telemetry: TelemetryClient = getTelemetryClient(),
): Promise<NoopL1Archiver> {
const instrumentation = await ArchiverInstrumentation.new(telemetry, () => dataStore.estimateSize());
return new NoopL1Archiver(dataStore, l1Constants, instrumentation);
}
1 change: 1 addition & 0 deletions yarn-project/epoch-cache/src/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './test_epoch_cache.js';
169 changes: 169 additions & 0 deletions yarn-project/epoch-cache/src/test/test_epoch_cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { EthAddress } from '@aztec/foundation/eth-address';
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
import { getEpochAtSlot, getSlotAtTimestamp, getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers';

import type { EpochAndSlot, EpochCacheInterface, EpochCommitteeInfo, SlotTag } from '../epoch_cache.js';

/** Default L1 constants for testing. */
const DEFAULT_L1_CONSTANTS: L1RollupConstants = {
l1StartBlock: 0n,
l1GenesisTime: 0n,
slotDuration: 24,
epochDuration: 16,
ethereumSlotDuration: 12,
proofSubmissionEpochs: 2,
};

/**
* A test implementation of EpochCacheInterface that allows manual configuration
* of committee, proposer, slot, and escape hatch state for use in tests.
*
* Unlike the real EpochCache, this class doesn't require any RPC connections
* or mock setup. Simply use the setter methods to configure the test state.
*/
export class TestEpochCache implements EpochCacheInterface {
private committee: EthAddress[] = [];
private proposerAddress: EthAddress | undefined;
private currentSlot: SlotNumber = SlotNumber(0);
private escapeHatchOpen: boolean = false;
private seed: bigint = 0n;
private registeredValidators: EthAddress[] = [];
private l1Constants: L1RollupConstants;

constructor(l1Constants: Partial<L1RollupConstants> = {}) {
this.l1Constants = { ...DEFAULT_L1_CONSTANTS, ...l1Constants };
}

/**
* Sets the committee members. Used in validation and attestation flows.
* @param committee - Array of committee member addresses.
*/
setCommittee(committee: EthAddress[]): this {
this.committee = committee;
return this;
}

/**
* Sets the proposer address returned by getProposerAttesterAddressInSlot.
* @param proposer - The address of the current proposer.
*/
setProposer(proposer: EthAddress | undefined): this {
this.proposerAddress = proposer;
return this;
}

/**
* Sets the current slot number.
* @param slot - The slot number to set.
*/
setCurrentSlot(slot: SlotNumber): this {
this.currentSlot = slot;
return this;
}

/**
* Sets whether the escape hatch is open.
* @param open - True if escape hatch should be open.
*/
setEscapeHatchOpen(open: boolean): this {
this.escapeHatchOpen = open;
return this;
}

/**
* Sets the randomness seed used for proposer selection.
* @param seed - The seed value.
*/
setSeed(seed: bigint): this {
this.seed = seed;
return this;
}

/**
* Sets the list of registered validators (all validators, not just committee).
* @param validators - Array of validator addresses.
*/
setRegisteredValidators(validators: EthAddress[]): this {
this.registeredValidators = validators;
return this;
}

/**
* Sets the L1 constants used for epoch/slot calculations.
* @param constants - Partial constants to override defaults.
*/
setL1Constants(constants: Partial<L1RollupConstants>): this {
this.l1Constants = { ...this.l1Constants, ...constants };
return this;
}

getL1Constants(): L1RollupConstants {
return this.l1Constants;
}

getCommittee(_slot?: SlotTag): Promise<EpochCommitteeInfo> {
const epoch = getEpochAtSlot(this.currentSlot, this.l1Constants);
return Promise.resolve({
committee: this.committee,
epoch,
seed: this.seed,
isEscapeHatchOpen: this.escapeHatchOpen,
});
}

getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint } {
const epoch = getEpochAtSlot(this.currentSlot, this.l1Constants);
const ts = getTimestampRangeForEpoch(epoch, this.l1Constants)[0];
return { epoch, slot: this.currentSlot, ts, nowMs: ts * 1000n };
}

getEpochAndSlotInNextL1Slot(): EpochAndSlot & { now: bigint } {
const now = getTimestampRangeForEpoch(getEpochAtSlot(this.currentSlot, this.l1Constants), this.l1Constants)[0];
const nextSlotTs = now + BigInt(this.l1Constants.ethereumSlotDuration);
const nextSlot = getSlotAtTimestamp(nextSlotTs, this.l1Constants);
const epoch = getEpochAtSlot(nextSlot, this.l1Constants);
const ts = getTimestampRangeForEpoch(epoch, this.l1Constants)[0];
return { epoch, slot: nextSlot, ts, now };
}

getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}` {
// Simple encoding for testing purposes
return `0x${epoch.toString(16).padStart(64, '0')}${slot.toString(16).padStart(64, '0')}${seed.toString(16).padStart(64, '0')}`;
}

computeProposerIndex(slot: SlotNumber, _epoch: EpochNumber, _seed: bigint, size: bigint): bigint {
if (size === 0n) {
return 0n;
}
return BigInt(slot) % size;
}

getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber } {
return {
currentSlot: this.currentSlot,
nextSlot: SlotNumber(this.currentSlot + 1),
};
}

getProposerAttesterAddressInSlot(_slot: SlotNumber): Promise<EthAddress | undefined> {
return Promise.resolve(this.proposerAddress);
}

getRegisteredValidators(): Promise<EthAddress[]> {
return Promise.resolve(this.registeredValidators);
}

isInCommittee(_slot: SlotTag, validator: EthAddress): Promise<boolean> {
return Promise.resolve(this.committee.some(v => v.equals(validator)));
}

filterInCommittee(_slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]> {
const committeeSet = new Set(this.committee.map(v => v.toString()));
return Promise.resolve(validators.filter(v => committeeSet.has(v.toString())));
}

isEscapeHatchOpenAtSlot(_slot?: SlotTag): Promise<boolean> {
return Promise.resolve(this.escapeHatchOpen);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,24 @@ export abstract class ProposalValidator<TProposal extends BlockProposal | Checkp
if (slotNumber !== currentSlot && slotNumber !== nextSlot) {
// Check if message is for previous slot and within clock tolerance
if (!isWithinClockTolerance(slotNumber, currentSlot, this.epochCache)) {
this.logger.debug(`Penalizing peer for invalid slot number ${slotNumber}`, { currentSlot, nextSlot });
this.logger.warn(`Penalizing peer for invalid slot number ${slotNumber}`, { currentSlot, nextSlot });
return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError };
}
this.logger.debug(`Ignoring proposal for previous slot ${slotNumber} within clock tolerance`);
this.logger.verbose(`Ignoring proposal for previous slot ${slotNumber} within clock tolerance`);
return { result: 'ignore' };
}

// Signature validity
const proposer = proposal.getSender();
if (!proposer) {
this.logger.debug(`Penalizing peer for proposal with invalid signature`);
this.logger.warn(`Penalizing peer for proposal with invalid signature`);
return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
}

// Transactions permitted check
const embeddedTxCount = proposal.txs?.length ?? 0;
if (!this.txsPermitted && (proposal.txHashes.length > 0 || embeddedTxCount > 0)) {
this.logger.debug(
this.logger.warn(
`Penalizing peer for proposal with ${proposal.txHashes.length} transaction(s) when transactions are not permitted`,
);
return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
Expand All @@ -65,7 +65,7 @@ export abstract class ProposalValidator<TProposal extends BlockProposal | Checkp
// Proposer check
const expectedProposer = await this.epochCache.getProposerAttesterAddressInSlot(slotNumber);
if (expectedProposer !== undefined && !proposer.equals(expectedProposer)) {
this.logger.debug(`Penalizing peer for invalid proposer for current slot ${slotNumber}`, {
this.logger.warn(`Penalizing peer for invalid proposer for current slot ${slotNumber}`, {
expectedProposer,
proposer: proposer.toString(),
});
Expand Down
Loading
Loading