Skip to content

Commit e875a44

Browse files
author
AztecBot
committed
Merge branch 'next' into merge-train/barretenberg
2 parents 9609818 + 3c724b0 commit e875a44

File tree

12 files changed

+443
-68
lines changed

12 files changed

+443
-68
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { isErrorClass } from './index.js';
2+
3+
describe('isErrorClass', () => {
4+
class CustomError extends Error {
5+
constructor() {
6+
super('This is a custom error');
7+
this.name = 'CustomError';
8+
}
9+
}
10+
11+
class AnotherError extends Error {
12+
constructor() {
13+
super('This is a another error');
14+
this.name = 'AnotherError';
15+
}
16+
}
17+
18+
it('should identify instances of the specified error class', () => {
19+
const error = new CustomError();
20+
expect(isErrorClass(error, CustomError)).toBe(true);
21+
expect(isErrorClass(error, AnotherError)).toBe(false);
22+
});
23+
24+
it('should handle non-error values correctly', () => {
25+
expect(isErrorClass({}, CustomError)).toBe(false);
26+
expect(isErrorClass(null, CustomError)).toBe(false);
27+
expect(isErrorClass(undefined, CustomError)).toBe(false);
28+
expect(isErrorClass('error', CustomError)).toBe(false);
29+
});
30+
31+
it('should identify built-in Error instances', () => {
32+
const error = new Error('Built-in error');
33+
expect(isErrorClass(error, Error)).toBe(true);
34+
expect(isErrorClass(error, CustomError)).toBe(false);
35+
});
36+
});

yarn-project/foundation/src/types/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export function isDefined<T>(value: T | undefined): value is T {
2424
return value !== undefined;
2525
}
2626

27+
/** Type guard for error classes */
28+
export function isErrorClass<T extends Error>(value: unknown, errorClass: new (...args: any[]) => T): value is T {
29+
return value instanceof errorClass || (value instanceof Error && value.name === errorClass.name);
30+
}
31+
2732
/** Resolves a record-like type. Lifted from viem. */
2833
export type Prettify<T> = {
2934
[K in keyof T]: T[K];

yarn-project/sequencer-client/src/sequencer/checkpoint_builder.test.ts renamed to yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Fr } from '@aztec/foundation/curves/bn254';
55
import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
66
import { ProtocolContractsList, protocolContractsHash } from '@aztec/protocol-contracts';
77
import { computeFeePayerBalanceLeafSlot } from '@aztec/protocol-contracts/fee-juice';
8-
import { LightweightCheckpointBuilder } from '@aztec/prover-client/light';
98
import { PublicDataWrite } from '@aztec/stdlib/avm';
109
import { AztecAddress } from '@aztec/stdlib/aztec-address';
1110
import { EthAddress } from '@aztec/stdlib/block';
@@ -20,7 +19,9 @@ import { NativeWorldStateService } from '@aztec/world-state/native';
2019

2120
import { afterEach, beforeEach, describe, expect, it } from '@jest/globals';
2221

23-
describe('CheckpointBuilder', () => {
22+
import { LightweightCheckpointBuilder } from './lightweight_checkpoint_builder.js';
23+
24+
describe('LightweightCheckpointBuilder', () => {
2425
let worldState: NativeWorldStateService;
2526
let feePayer: AztecAddress;
2627
let feePayerBalance: Fr;

yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ export class LightweightCheckpointBuilder {
143143
return builder;
144144
}
145145

146+
/** Returns how many blocks have been added to this checkpoint so far */
147+
public getBlockCount() {
148+
return this.blocks.length;
149+
}
150+
146151
/**
147152
* Adds a new block to the checkpoint. The tx effects must have already been inserted into the db if
148153
* this is called after tx processing, if that's not the case, then set `insertTxsEffects` to true.

yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts

Lines changed: 160 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,23 @@ import type { TypedEventEmitter } from '@aztec/foundation/types';
2323
import { type P2P, P2PClientState } from '@aztec/p2p';
2424
import type { SlasherClientInterface } from '@aztec/slasher';
2525
import { AztecAddress } from '@aztec/stdlib/aztec-address';
26-
import { CommitteeAttestation, type L2BlockSink, type L2BlockSource } from '@aztec/stdlib/block';
26+
import { CommitteeAttestation, L2Block, type L2BlockSink, type L2BlockSource } from '@aztec/stdlib/block';
2727
import { Checkpoint } from '@aztec/stdlib/checkpoint';
2828
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
2929
import { GasFees } from '@aztec/stdlib/gas';
30-
import type {
31-
MerkleTreeWriteOperations,
32-
ResolvedSequencerConfig,
33-
WorldStateSynchronizer,
30+
import {
31+
type BuildBlockInCheckpointResult,
32+
type MerkleTreeWriteOperations,
33+
NoValidTxsError,
34+
type ResolvedSequencerConfig,
35+
type WorldStateSynchronizer,
3436
} from '@aztec/stdlib/interfaces/server';
3537
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
3638
import { BlockProposal, CheckpointProposal } from '@aztec/stdlib/p2p';
37-
import { GlobalVariables, type Tx } from '@aztec/stdlib/tx';
39+
import { type FailedTx, GlobalVariables, type Tx } from '@aztec/stdlib/tx';
3840
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
3941
import { getTelemetryClient } from '@aztec/telemetry-client';
40-
import type { FullNodeCheckpointsBuilder, ValidatorClient } from '@aztec/validator-client';
42+
import { CheckpointBuilder, type FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
4143
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
4244
import { DutyType } from '@aztec/validator-ha-signer/types';
4345

@@ -458,7 +460,7 @@ describe('CheckpointProposalJob', () => {
458460
const txs = await Promise.all(Array.from({ length: totalTxs }, (_, i) => makeTx(i + 1, chainId)));
459461

460462
// Set up p2p mocks
461-
p2p.getPendingTxCount.mockResolvedValue(10);
463+
p2p.getPendingTxCount.mockResolvedValue(txs.length);
462464
p2p.iteratePendingTxs.mockImplementation(() => mockTxIterator(Promise.resolve(txs)));
463465

464466
// Create blocks with incrementing block numbers
@@ -585,6 +587,92 @@ describe('CheckpointProposalJob', () => {
585587
expect(waitSpy.mock.calls[0][0]).toEqual(10);
586588
});
587589

590+
it('builds a single empty block when no txs are available and no min txs required', async () => {
591+
// Mock timetable to have two sub-slots
592+
jest
593+
.spyOn(job.getTimetable(), 'canStartNextBlock')
594+
.mockReturnValueOnce({ canStart: true, deadline: 2, isLastBlock: false })
595+
.mockReturnValueOnce({ canStart: true, deadline: 4, isLastBlock: true })
596+
.mockReturnValue({ canStart: false, deadline: undefined, isLastBlock: false });
597+
598+
// Set up test data for an empty block
599+
const { lastBlock } = await setupMultipleBlocks(1, [0]);
600+
validatorClient.collectAttestations.mockResolvedValue(getAttestations(lastBlock));
601+
602+
// Install spy on waitUntilTimeInSlot to verify it's called with expected deadlines
603+
const waitSpy = jest.spyOn(job, 'waitUntilTimeInSlot');
604+
605+
job.updateConfig({ minTxsPerBlock: 0 });
606+
const checkpoint = await job.execute();
607+
608+
expect(checkpoint).toBeDefined();
609+
expect(checkpointBuilder.buildBlockCalls).toHaveLength(1);
610+
expect(validatorClient.collectAttestations).toHaveBeenCalledTimes(1);
611+
expect(publisher.enqueueProposeCheckpoint).toHaveBeenCalledTimes(1);
612+
613+
// Verify waitUntilTimeInSlot was called between blocks
614+
expect(waitSpy).toHaveBeenCalledTimes(1);
615+
// The wait time is until the next block deadline
616+
expect(waitSpy.mock.calls[0][0]).toEqual(2);
617+
});
618+
619+
it('builds a single block when not enough txs are available but we build empty checkpoints', async () => {
620+
// Mock timetable to have two sub-slots
621+
jest
622+
.spyOn(job.getTimetable(), 'canStartNextBlock')
623+
.mockReturnValueOnce({ canStart: true, deadline: 2, isLastBlock: false })
624+
.mockReturnValueOnce({ canStart: true, deadline: 4, isLastBlock: true })
625+
.mockReturnValue({ canStart: false, deadline: undefined, isLastBlock: false });
626+
627+
// Set up test data for a block with only 2 txs, note that min txs is 5
628+
const { lastBlock } = await setupMultipleBlocks(1, [2]);
629+
validatorClient.collectAttestations.mockResolvedValue(getAttestations(lastBlock));
630+
631+
// Install spy on waitUntilTimeInSlot to verify it's called with expected deadlines
632+
const waitSpy = jest.spyOn(job, 'waitUntilTimeInSlot');
633+
634+
job.updateConfig({ minTxsPerBlock: 5, buildCheckpointIfEmpty: true });
635+
const checkpoint = await job.execute();
636+
637+
expect(checkpoint).toBeDefined();
638+
expect(checkpointBuilder.buildBlockCalls).toHaveLength(1);
639+
expect(validatorClient.collectAttestations).toHaveBeenCalledTimes(1);
640+
expect(publisher.enqueueProposeCheckpoint).toHaveBeenCalledTimes(1);
641+
642+
// Verify waitUntilTimeInSlot was called between blocks
643+
expect(waitSpy).toHaveBeenCalledTimes(1);
644+
// The wait time is until the next block deadline
645+
expect(waitSpy.mock.calls[0][0]).toEqual(2);
646+
});
647+
648+
it('does not build anything if not enough txs and we do not build empty checkpoints', async () => {
649+
// Mock timetable to have two sub-slots
650+
jest
651+
.spyOn(job.getTimetable(), 'canStartNextBlock')
652+
.mockReturnValueOnce({ canStart: true, deadline: 2, isLastBlock: false })
653+
.mockReturnValueOnce({ canStart: true, deadline: 4, isLastBlock: true })
654+
.mockReturnValue({ canStart: false, deadline: undefined, isLastBlock: false });
655+
656+
// Not enough txs to build a block
657+
p2p.getPendingTxCount.mockResolvedValue(2);
658+
659+
// Install spy on waitUntilTimeInSlot to verify it's called with expected deadlines
660+
const waitSpy = jest.spyOn(job, 'waitUntilTimeInSlot');
661+
662+
job.updateConfig({ minTxsPerBlock: 5, buildCheckpointIfEmpty: false });
663+
const checkpoint = await job.execute();
664+
665+
expect(checkpoint).toBeUndefined();
666+
expect(checkpointBuilder.buildBlockCalls).toHaveLength(0);
667+
expect(validatorClient.collectAttestations).toHaveBeenCalledTimes(0);
668+
expect(publisher.enqueueProposeCheckpoint).toHaveBeenCalledTimes(0);
669+
670+
// Verify waitUntilTimeInSlot was called between blocks
671+
expect(waitSpy).toHaveBeenCalledTimes(1);
672+
// The wait time is until the next block deadline
673+
expect(waitSpy.mock.calls[0][0]).toEqual(2);
674+
});
675+
588676
it('stops building when canStartNextBlock returns false', async () => {
589677
// Mock timetable to stop after 1 block (simulating time running out)
590678
jest
@@ -720,6 +808,53 @@ describe('CheckpointProposalJob', () => {
720808
});
721809
});
722810

811+
describe('build single block', () => {
812+
it('does not build a block if not enough valid txs are collected', async () => {
813+
// We have enough txs, but not enough valid ones
814+
job.updateConfig({ minTxsPerBlock: 3, minValidTxsPerBlock: 2 });
815+
const txs = await timesAsync(3, i => makeTx(i + 1, chainId));
816+
mockPendingTxs(p2p, txs);
817+
818+
const checkpointBuilder = mock<CheckpointBuilder>();
819+
const failedTxs: FailedTx[] = txs.slice(1).map(tx => ({ tx, error: new Error('Invalid tx') }));
820+
checkpointBuilder.buildBlock.mockResolvedValue({ failedTxs, numTxs: 1 } as BuildBlockInCheckpointResult);
821+
822+
const checkpoint = await job.buildSingleBlock(checkpointBuilder, {
823+
blockNumber: newBlockNumber,
824+
indexWithinCheckpoint: IndexWithinCheckpoint(1),
825+
buildDeadline: undefined,
826+
blockTimestamp: 0n,
827+
remainingBlobFields: 1,
828+
txHashesAlreadyIncluded: new Set<string>(),
829+
});
830+
831+
expect(checkpoint).toBeUndefined();
832+
expect(p2p.deleteTxs).toHaveBeenCalledWith(failedTxs.map(ftx => ftx.tx.txHash));
833+
});
834+
835+
it('does not build a block if checkpoint builder fails with invalid txs', async () => {
836+
job.updateConfig({ minTxsPerBlock: 3 });
837+
const txs = await timesAsync(3, i => makeTx(i + 1, chainId));
838+
mockPendingTxs(p2p, txs);
839+
840+
const checkpointBuilder = mock<CheckpointBuilder>();
841+
const failedTxs: FailedTx[] = txs.slice(1).map(tx => ({ tx, error: new Error('Invalid tx') }));
842+
checkpointBuilder.buildBlock.mockRejectedValue(new NoValidTxsError(failedTxs));
843+
844+
const checkpoint = await job.buildSingleBlock(checkpointBuilder, {
845+
blockNumber: newBlockNumber,
846+
indexWithinCheckpoint: IndexWithinCheckpoint(1),
847+
buildDeadline: undefined,
848+
blockTimestamp: 0n,
849+
remainingBlobFields: 1,
850+
txHashesAlreadyIncluded: new Set<string>(),
851+
});
852+
853+
expect(checkpoint).toBeUndefined();
854+
expect(p2p.deleteTxs).toHaveBeenCalledWith(failedTxs.map(ftx => ftx.tx.txHash));
855+
});
856+
});
857+
723858
describe('timing edge cases', () => {
724859
it('handles insufficient time remaining in slot', async () => {
725860
// Mock canStartNextBlock to return false (not enough time)
@@ -855,7 +990,7 @@ describe('CheckpointProposalJob', () => {
855990
});
856991
});
857992

858-
describe('HA error handling during block building', () => {
993+
describe('high-availability error handling during block building', () => {
859994
it('should stop checkpoint building when block proposal throws DutyAlreadySignedError on first block', async () => {
860995
// Set up test data for 3 blocks (to verify it stops even with multiple blocks configured)
861996
const { lastBlock } = await setupMultipleBlocks(3, 1);
@@ -961,4 +1096,20 @@ class TestCheckpointProposalJob extends CheckpointProposalJob {
9611096
public getTimetable(): SequencerTimetable {
9621097
return this.timetable;
9631098
}
1099+
1100+
/** Expose internal buildSingleBlock method */
1101+
public override buildSingleBlock(
1102+
checkpointBuilder: CheckpointBuilder,
1103+
opts: {
1104+
forceCreate?: boolean;
1105+
blockTimestamp: bigint;
1106+
blockNumber: BlockNumber;
1107+
indexWithinCheckpoint: IndexWithinCheckpoint;
1108+
buildDeadline: Date | undefined;
1109+
txHashesAlreadyIncluded: Set<string>;
1110+
remainingBlobFields: number;
1111+
},
1112+
): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
1113+
return super.buildSingleBlock(checkpointBuilder, opts);
1114+
}
9641115
}

0 commit comments

Comments
 (0)