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
5 changes: 5 additions & 0 deletions .test_patterns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ tests:
error_regex: "combined_a.get_value() > fq_ct::modulus"
owners:
- *suyash
# http://ci.aztec-labs.com/1593f7c89e22b51b
- regex: stdlib_primitives_tests stdlibBiggroupSecp256k1/1.WnafSecp256k1StaggerOutOfRangeFails
error_regex: "biggroup_nafs: stagger fragment is not in range"
owners:
- *luke

# noir
# Something to do with how I run the tests now. Think these are fine in nextest.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Secp256k1Signer } from '@aztec/foundation/crypto';
import { Fr } from '@aztec/foundation/fields';
import { PeerErrorSeverity } from '@aztec/stdlib/p2p';
import { makeBlockProposal, makeL2BlockHeader } from '@aztec/stdlib/testing';
import { TxHash } from '@aztec/stdlib/tx';

import { mock } from 'jest-mock-extended';

Expand All @@ -14,7 +15,7 @@ describe('BlockProposalValidator', () => {

beforeEach(() => {
epochCache = mock<EpochCache>();
validator = new BlockProposalValidator(epochCache);
validator = new BlockProposalValidator(epochCache, { txsPermitted: true });
});

it('returns high tolerance error if slot number is not current or next slot', async () => {
Expand Down Expand Up @@ -146,4 +147,75 @@ describe('BlockProposalValidator', () => {
const result = await validator.validate(mockProposal);
expect(result).toBeUndefined();
});

describe('transaction permission validation', () => {
it('returns mid tolerance error if txs not permitted and proposal contains txHashes', async () => {
const currentProposer = Secp256k1Signer.random();
const validatorWithTxsDisabled = new BlockProposalValidator(epochCache, { txsPermitted: false });

// Create a block proposal with transaction hashes
const mockProposal = makeBlockProposal({
header: makeL2BlockHeader(1, 100, 100),
signer: currentProposer,
txHashes: [TxHash.random(), TxHash.random()], // Include some tx hashes
});

// Mock epoch cache to return valid proposer (so only tx permission check fails)
(epochCache.getProposerAttesterAddressInCurrentOrNextSlot as jest.Mock).mockResolvedValue({
currentSlot: 100n,
nextSlot: 101n,
currentProposer: currentProposer.address,
nextProposer: Fr.random(),
});

const result = await validatorWithTxsDisabled.validate(mockProposal);
expect(result).toBe(PeerErrorSeverity.MidToleranceError);
});

it('returns undefined if txs not permitted but proposal has no txHashes', async () => {
const currentProposer = Secp256k1Signer.random();
const validatorWithTxsDisabled = new BlockProposalValidator(epochCache, { txsPermitted: false });

// Create a block proposal without transaction hashes
const mockProposal = makeBlockProposal({
header: makeL2BlockHeader(1, 100, 100),
signer: currentProposer,
txHashes: [], // Empty tx hashes array
});

// Mock epoch cache for valid case
(epochCache.getProposerAttesterAddressInCurrentOrNextSlot as jest.Mock).mockResolvedValue({
currentSlot: 100n,
nextSlot: 101n,
currentProposer: currentProposer.address,
nextProposer: Fr.random(),
});

const result = await validatorWithTxsDisabled.validate(mockProposal);
expect(result).toBeUndefined();
});

it('returns undefined if txs permitted and proposal contains txHashes', async () => {
const currentProposer = Secp256k1Signer.random();
// validator already created with txsPermitted = true in beforeEach

// Create a block proposal with transaction hashes
const mockProposal = makeBlockProposal({
header: makeL2BlockHeader(1, 100, 100),
signer: currentProposer,
txHashes: [TxHash.random(), TxHash.random()], // Include some tx hashes
});

// Mock epoch cache for valid case
(epochCache.getProposerAttesterAddressInCurrentOrNextSlot as jest.Mock).mockResolvedValue({
currentSlot: 100n,
nextSlot: 101n,
currentProposer: currentProposer.address,
nextProposer: Fr.random(),
});

const result = await validator.validate(mockProposal);
expect(result).toBeUndefined();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { type BlockProposal, type P2PValidator, PeerErrorSeverity } from '@aztec
export class BlockProposalValidator implements P2PValidator<BlockProposal> {
private epochCache: EpochCacheInterface;
private logger: Logger;
private txsPermitted: boolean;

constructor(epochCache: EpochCacheInterface) {
constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean }) {
this.epochCache = epochCache;
this.txsPermitted = opts.txsPermitted;
this.logger = createLogger('p2p:block_proposal_validator');
}

Expand All @@ -21,6 +23,14 @@ export class BlockProposalValidator implements P2PValidator<BlockProposal> {
return PeerErrorSeverity.MidToleranceError;
}

// Check if transactions are permitted when the proposal contains transaction hashes
if (!this.txsPermitted && block.txHashes.length > 0) {
this.logger.debug(
`Penalizing peer for block proposal with ${block.txHashes.length} transaction(s) when transactions are not permitted`,
);
return PeerErrorSeverity.MidToleranceError;
}

const { currentProposer, nextProposer, currentSlot, nextSlot } =
await this.epochCache.getProposerAttesterAddressInCurrentOrNextSlot();

Expand Down
2 changes: 1 addition & 1 deletion yarn-project/p2p/src/services/libp2p/libp2p_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
);

this.attestationValidator = new AttestationValidator(epochCache);
this.blockProposalValidator = new BlockProposalValidator(epochCache);
this.blockProposalValidator = new BlockProposalValidator(epochCache, { txsPermitted: !config.disableTransactions });

this.gossipSubEventHandler = this.handleGossipSubEvent.bind(this);

Expand Down
5 changes: 3 additions & 2 deletions yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type Offense, OffenseType, type SlashPayloadRound } from '../slashing/i
import { type AztecNodeAdmin, AztecNodeAdminApiSchema } from './aztec-node-admin.js';
import type { SequencerConfig } from './configs.js';
import type { ProverConfig } from './prover-client.js';
import type { ValidatorClientConfig } from './server.js';
import type { ValidatorClientFullConfig } from './server.js';
import type { SlasherConfig } from './slasher.js';

describe('AztecNodeAdminApiSchema', () => {
Expand Down Expand Up @@ -126,7 +126,7 @@ class MockAztecNodeAdmin implements AztecNodeAdmin {
]);
}
getConfig(): Promise<
ValidatorClientConfig & SequencerConfig & ProverConfig & SlasherConfig & { maxTxPoolSize: number }
ValidatorClientFullConfig & SequencerConfig & ProverConfig & SlasherConfig & { maxTxPoolSize: number }
> {
return Promise.resolve({
realProofs: false,
Expand Down Expand Up @@ -164,6 +164,7 @@ class MockAztecNodeAdmin implements AztecNodeAdmin {
attestationPollingIntervalMs: 1000,
validatorReexecute: true,
validatorReexecuteDeadlineMs: 1000,
disableTransactions: false,
});
}
startSnapshotUpload(_location: string): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/stdlib/src/interfaces/aztec-node-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { type ArchiverSpecificConfig, ArchiverSpecificConfigSchema } from './arc
import { type SequencerConfig, SequencerConfigSchema } from './configs.js';
import { type ProverConfig, ProverConfigSchema } from './prover-client.js';
import { type SlasherConfig, SlasherConfigSchema } from './slasher.js';
import { ValidatorClientConfigSchema, type ValidatorClientFullConfig } from './validator.js';
import { type ValidatorClientFullConfig, ValidatorClientFullConfigSchema } from './validator.js';

/**
* Aztec node admin API.
Expand Down Expand Up @@ -62,7 +62,7 @@ export type AztecNodeAdminConfig = ValidatorClientFullConfig &

export const AztecNodeAdminConfigSchema = SequencerConfigSchema.merge(ProverConfigSchema)
.merge(SlasherConfigSchema)
.merge(ValidatorClientConfigSchema)
.merge(ValidatorClientFullConfigSchema)
.merge(
ArchiverSpecificConfigSchema.pick({
archiverPollingIntervalMS: true,
Expand Down
16 changes: 15 additions & 1 deletion yarn-project/stdlib/src/interfaces/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { z } from 'zod';

import type { CommitteeAttestationsAndSigners } from '../block/index.js';
import type { CheckpointHeader } from '../rollup/checkpoint_header.js';
import { AllowedElementSchema } from './allowed_element.js';

/**
* Validator client configuration
Expand Down Expand Up @@ -44,7 +45,13 @@ export interface ValidatorClientConfig {

export type ValidatorClientFullConfig = ValidatorClientConfig &
Pick<SequencerConfig, 'txPublicSetupAllowList' | 'broadcastInvalidBlockProposal'> &
Pick<SlasherConfig, 'slashBroadcastedInvalidBlockPenalty'>;
Pick<SlasherConfig, 'slashBroadcastedInvalidBlockPenalty'> & {
/**
* Whether transactions are disabled for this node
* @remarks This should match the property in P2PConfig. It's not picked from there to avoid circular dependencies.
*/
disableTransactions?: boolean;
};

export const ValidatorClientConfigSchema = z.object({
validatorAddresses: z.array(schemas.EthAddress).optional(),
Expand All @@ -56,6 +63,13 @@ export const ValidatorClientConfigSchema = z.object({
alwaysReexecuteBlockProposals: z.boolean().optional(),
}) satisfies ZodFor<Omit<ValidatorClientConfig, 'validatorPrivateKeys'>>;

export const ValidatorClientFullConfigSchema = ValidatorClientConfigSchema.extend({
txPublicSetupAllowList: z.array(AllowedElementSchema).optional(),
broadcastInvalidBlockProposal: z.boolean().optional(),
slashBroadcastedInvalidBlockPenalty: schemas.BigInt,
disableTransactions: z.boolean().optional(),
}) satisfies ZodFor<Omit<ValidatorClientFullConfig, 'validatorPrivateKeys'>>;

export interface Validator {
start(): Promise<void>;
updateConfig(config: Partial<ValidatorClientFullConfig>): void;
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/validator-client/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export function createBlockProposalHandler(
},
) {
const metrics = new ValidatorMetrics(deps.telemetry);
const blockProposalValidator = new BlockProposalValidator(deps.epochCache);
const blockProposalValidator = new BlockProposalValidator(deps.epochCache, {
txsPermitted: !config.disableTransactions,
});
return new BlockProposalHandler(
deps.blockBuilder,
deps.blockSource,
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/validator-client/src/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ import { type ValidatorClientConfig, validatorClientConfigMappings } from './con
import { ValidatorClient } from './validator.js';

describe('ValidatorClient', () => {
let config: ValidatorClientConfig & Pick<SlasherConfig, 'slashBroadcastedInvalidBlockPenalty'>;
let config: ValidatorClientConfig &
Pick<SlasherConfig, 'slashBroadcastedInvalidBlockPenalty'> & { disableTransactions: boolean };
let validatorClient: ValidatorClient;
let p2pClient: MockProxy<P2P>;
let blockSource: MockProxy<L2BlockSource>;
Expand Down Expand Up @@ -75,6 +76,7 @@ describe('ValidatorClient', () => {
validatorReexecute: false,
validatorReexecuteDeadlineMs: 6000,
slashBroadcastedInvalidBlockPenalty: 1n,
disableTransactions: false,
};

const keyStore: KeyStore = {
Expand Down
15 changes: 5 additions & 10 deletions yarn-project/validator-client/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@ import { DateProvider } from '@aztec/foundation/timer';
import type { KeystoreManager } from '@aztec/node-keystore';
import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
import {
OffenseType,
type SlasherConfig,
WANT_TO_SLASH_EVENT,
type Watcher,
type WatcherEmitter,
} from '@aztec/slasher';
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
import type { CommitteeAttestationsAndSigners, L2BlockSource } from '@aztec/stdlib/block';
import type { IFullNodeBlockBuilder, Validator, ValidatorClientFullConfig } from '@aztec/stdlib/interfaces/server';
Expand All @@ -30,7 +24,6 @@ import { EventEmitter } from 'events';
import type { TypedDataDefinition } from 'viem';

import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
import type { ValidatorClientConfig } from './config.js';
import { ValidationService } from './duties/validation_service.js';
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
import { ValidatorMetrics } from './metrics.js';
Expand Down Expand Up @@ -140,7 +133,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
}

static new(
config: ValidatorClientConfig & Pick<SlasherConfig, 'slashBroadcastedInvalidBlockPenalty'>,
config: ValidatorClientFullConfig,
blockBuilder: IFullNodeBlockBuilder,
epochCache: EpochCache,
p2pClient: P2P,
Expand All @@ -152,7 +145,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
telemetry: TelemetryClient = getTelemetryClient(),
) {
const metrics = new ValidatorMetrics(telemetry);
const blockProposalValidator = new BlockProposalValidator(epochCache);
const blockProposalValidator = new BlockProposalValidator(epochCache, {
txsPermitted: !config.disableTransactions,
});
const blockProposalHandler = new BlockProposalHandler(
blockBuilder,
blockSource,
Expand Down
Loading