diff --git a/yarn-project/aztec-node/package.json b/yarn-project/aztec-node/package.json index 6ace5e54c53..4ab993ebe60 100644 --- a/yarn-project/aztec-node/package.json +++ b/yarn-project/aztec-node/package.json @@ -71,6 +71,7 @@ "@aztec/bb-prover": "workspace:^", "@aztec/circuit-types": "workspace:^", "@aztec/circuits.js": "workspace:^", + "@aztec/epoch-cache": "workspace:^", "@aztec/ethereum": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/kv-store": "workspace:^", diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 59ba35913fe..68d89f05dae 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -54,6 +54,7 @@ import { type PublicDataTreeLeafPreimage, } from '@aztec/circuits.js'; import { computePublicDataTreeLeafSlot } from '@aztec/circuits.js/hash'; +import { EpochCache } from '@aztec/epoch-cache'; import { type L1ContractAddresses, createEthereumChain } from '@aztec/ethereum'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { padArrayEnd } from '@aztec/foundation/collection'; @@ -166,6 +167,8 @@ export class AztecNodeService implements AztecNode, Traceable { log.warn(`Aztec node is accepting fake proofs`); } + const epochCache = await EpochCache.create(config.l1Contracts.rollupAddress, config, { dateProvider }); + // create the tx pool and the p2p client, which will need the l2 block source const p2pClient = await createP2PClient( P2PClientType.Full, @@ -173,13 +176,14 @@ export class AztecNodeService implements AztecNode, Traceable { archiver, proofVerifier, worldStateSynchronizer, + epochCache, telemetry, ); // start both and wait for them to sync from the block source await Promise.all([p2pClient.start(), worldStateSynchronizer.start()]); - const validatorClient = await createValidatorClient(config, rollupAddress, { p2pClient, telemetry, dateProvider }); + const validatorClient = createValidatorClient(config, { p2pClient, telemetry, dateProvider, epochCache }); // now create the sequencer const sequencer = config.disableValidator diff --git a/yarn-project/aztec-node/tsconfig.json b/yarn-project/aztec-node/tsconfig.json index e179f91e9e9..2f5e8c847ce 100644 --- a/yarn-project/aztec-node/tsconfig.json +++ b/yarn-project/aztec-node/tsconfig.json @@ -18,6 +18,9 @@ { "path": "../circuits.js" }, + { + "path": "../epoch-cache" + }, { "path": "../ethereum" }, diff --git a/yarn-project/circuit-types/src/p2p/index.ts b/yarn-project/circuit-types/src/p2p/index.ts index 972988c8b7f..24ee7781f65 100644 --- a/yarn-project/circuit-types/src/p2p/index.ts +++ b/yarn-project/circuit-types/src/p2p/index.ts @@ -6,3 +6,6 @@ export * from './interface.js'; export * from './signature_utils.js'; export * from './topic_type.js'; export * from './client_type.js'; +export * from './message_validator.js'; +export * from './peer_error.js'; +export * from './mocks.js'; diff --git a/yarn-project/circuit-types/src/p2p/message_validator.ts b/yarn-project/circuit-types/src/p2p/message_validator.ts new file mode 100644 index 00000000000..ae132ad8676 --- /dev/null +++ b/yarn-project/circuit-types/src/p2p/message_validator.ts @@ -0,0 +1,10 @@ +import { type PeerErrorSeverity } from './peer_error.js'; + +/** + * P2PValidator + * + * A validator for P2P messages, which returns a severity of error to be applied to the peer + */ +export interface P2PValidator { + validate(message: T): Promise; +} diff --git a/yarn-project/circuit-types/src/p2p/peer_error.ts b/yarn-project/circuit-types/src/p2p/peer_error.ts new file mode 100644 index 00000000000..73be41ebada --- /dev/null +++ b/yarn-project/circuit-types/src/p2p/peer_error.ts @@ -0,0 +1,17 @@ +export enum PeerErrorSeverity { + /** + * Not malicious action, but it must not be tolerated + * ~2 occurrences will get the peer banned + */ + LowToleranceError = 'LowToleranceError', + /** + * Negative action that can be tolerated only sometimes + * ~10 occurrences will get the peer banned + */ + MidToleranceError = 'MidToleranceError', + /** + * Some error that can be tolerated multiple times + * ~50 occurrences will get the peer banned + */ + HighToleranceError = 'HighToleranceError', +} diff --git a/yarn-project/p2p/package.json b/yarn-project/p2p/package.json index 5be467e88f8..be4d0b372a6 100644 --- a/yarn-project/p2p/package.json +++ b/yarn-project/p2p/package.json @@ -6,7 +6,8 @@ ".": "./dest/index.js", "./mocks": "./dest/mocks/index.js", "./bootstrap": "./dest/bootstrap/bootstrap.js", - "./config": "./dest/config.js" + "./config": "./dest/config.js", + "./msg_validators": "./dest/msg_validators/index.js" }, "typedocOptions": { "entryPoints": [ @@ -69,6 +70,7 @@ "dependencies": { "@aztec/circuit-types": "workspace:^", "@aztec/circuits.js": "workspace:^", + "@aztec/epoch-cache": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/kv-store": "workspace:^", "@aztec/telemetry-client": "workspace:^", diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts new file mode 100644 index 00000000000..926286c19ba --- /dev/null +++ b/yarn-project/p2p/src/client/factory.ts @@ -0,0 +1,97 @@ +import { + type ClientProtocolCircuitVerifier, + type L2BlockSource, + P2PClientType, + type WorldStateSynchronizer, +} from '@aztec/circuit-types'; +import { type EpochCache } from '@aztec/epoch-cache'; +import { createLogger } from '@aztec/foundation/log'; +import { type AztecKVStore } from '@aztec/kv-store'; +import { type DataStoreConfig } from '@aztec/kv-store/config'; +import { createStore } from '@aztec/kv-store/lmdb'; +import { type TelemetryClient } from '@aztec/telemetry-client'; +import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; + +import { P2PClient } from '../client/p2p_client.js'; +import { type P2PConfig } from '../config.js'; +import { type AttestationPool } from '../mem_pools/attestation_pool/attestation_pool.js'; +import { InMemoryAttestationPool } from '../mem_pools/attestation_pool/memory_attestation_pool.js'; +import { type EpochProofQuotePool } from '../mem_pools/epoch_proof_quote_pool/epoch_proof_quote_pool.js'; +import { MemoryEpochProofQuotePool } from '../mem_pools/epoch_proof_quote_pool/memory_epoch_proof_quote_pool.js'; +import { type MemPools } from '../mem_pools/interface.js'; +import { AztecKVTxPool, type TxPool } from '../mem_pools/tx_pool/index.js'; +import { DiscV5Service } from '../services/discv5/discV5_service.js'; +import { DummyP2PService } from '../services/dummy_service.js'; +import { LibP2PService } from '../services/index.js'; +import { configureP2PClientAddresses, createLibP2PPeerIdFromPrivateKey, getPeerIdPrivateKey } from '../util.js'; + +type P2PClientDeps = { + txPool?: TxPool; + store?: AztecKVStore; + attestationPool?: T extends P2PClientType.Full ? AttestationPool : undefined; + epochProofQuotePool?: EpochProofQuotePool; +}; + +export const createP2PClient = async ( + clientType: T, + _config: P2PConfig & DataStoreConfig, + l2BlockSource: L2BlockSource, + proofVerifier: ClientProtocolCircuitVerifier, + worldStateSynchronizer: WorldStateSynchronizer, + epochCache: EpochCache, + telemetry: TelemetryClient = new NoopTelemetryClient(), + deps: P2PClientDeps = {}, +) => { + let config = { ..._config }; + const logger = createLogger('p2p'); + const store = deps.store ?? (await createStore('p2p', config, createLogger('p2p:lmdb'))); + + const mempools: MemPools = { + txPool: deps.txPool ?? new AztecKVTxPool(store, telemetry), + epochProofQuotePool: deps.epochProofQuotePool ?? new MemoryEpochProofQuotePool(telemetry), + attestationPool: + clientType === P2PClientType.Full + ? ((deps.attestationPool ?? new InMemoryAttestationPool(telemetry)) as T extends P2PClientType.Full + ? AttestationPool + : undefined) + : undefined, + }; + + let p2pService; + + if (_config.p2pEnabled) { + logger.verbose('P2P is enabled. Using LibP2P service.'); + config = await configureP2PClientAddresses(_config); + + // Create peer discovery service + const peerIdPrivateKey = await getPeerIdPrivateKey(config, store); + const peerId = await createLibP2PPeerIdFromPrivateKey(peerIdPrivateKey); + const discoveryService = new DiscV5Service(peerId, config, telemetry); + + p2pService = await LibP2PService.new( + clientType, + config, + discoveryService, + peerId, + mempools, + l2BlockSource, + epochCache, + proofVerifier, + worldStateSynchronizer, + store, + telemetry, + ); + } else { + logger.verbose('P2P is disabled. Using dummy P2P service'); + p2pService = new DummyP2PService(); + } + return new P2PClient( + clientType, + store, + l2BlockSource, + mempools, + p2pService, + config.keepProvenTxsInPoolFor, + telemetry, + ); +}; diff --git a/yarn-project/p2p/src/client/index.ts b/yarn-project/p2p/src/client/index.ts index 9c4cca388db..c0f55cf9015 100644 --- a/yarn-project/p2p/src/client/index.ts +++ b/yarn-project/p2p/src/client/index.ts @@ -1,96 +1,2 @@ -import { - type ClientProtocolCircuitVerifier, - type L2BlockSource, - P2PClientType, - type WorldStateSynchronizer, -} from '@aztec/circuit-types'; -import { createLogger } from '@aztec/foundation/log'; -import { type AztecKVStore } from '@aztec/kv-store'; -import { type DataStoreConfig } from '@aztec/kv-store/config'; -import { createStore } from '@aztec/kv-store/lmdb'; -import { type TelemetryClient } from '@aztec/telemetry-client'; -import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; - -import { P2PClient } from '../client/p2p_client.js'; -import { type P2PConfig } from '../config.js'; -import { type AttestationPool } from '../mem_pools/attestation_pool/attestation_pool.js'; -import { KvAttestationPool } from '../mem_pools/attestation_pool/kv_attestation_pool.js'; -import { type EpochProofQuotePool } from '../mem_pools/epoch_proof_quote_pool/epoch_proof_quote_pool.js'; -import { MemoryEpochProofQuotePool } from '../mem_pools/epoch_proof_quote_pool/memory_epoch_proof_quote_pool.js'; -import { type MemPools } from '../mem_pools/interface.js'; -import { AztecKVTxPool, type TxPool } from '../mem_pools/tx_pool/index.js'; -import { DiscV5Service } from '../services/discv5/discV5_service.js'; -import { DummyP2PService } from '../services/dummy_service.js'; -import { LibP2PService } from '../services/index.js'; -import { configureP2PClientAddresses, createLibP2PPeerIdFromPrivateKey, getPeerIdPrivateKey } from '../util.js'; - export * from './p2p_client.js'; - -type P2PClientDeps = { - txPool?: TxPool; - store?: AztecKVStore; - attestationPool?: T extends P2PClientType.Full ? AttestationPool : undefined; - epochProofQuotePool?: EpochProofQuotePool; -}; - -export const createP2PClient = async ( - clientType: T, - _config: P2PConfig & DataStoreConfig, - l2BlockSource: L2BlockSource, - proofVerifier: ClientProtocolCircuitVerifier, - worldStateSynchronizer: WorldStateSynchronizer, - telemetry: TelemetryClient = new NoopTelemetryClient(), - deps: P2PClientDeps = {}, -) => { - let config = { ..._config }; - const logger = createLogger('p2p'); - const store = deps.store ?? (await createStore('p2p', config, createLogger('p2p:lmdb'))); - - const mempools: MemPools = { - txPool: deps.txPool ?? new AztecKVTxPool(store, telemetry), - epochProofQuotePool: deps.epochProofQuotePool ?? new MemoryEpochProofQuotePool(telemetry), - attestationPool: - clientType === P2PClientType.Full - ? ((deps.attestationPool ?? new KvAttestationPool(store, telemetry)) as T extends P2PClientType.Full - ? AttestationPool - : undefined) - : undefined, - }; - - let p2pService; - - if (_config.p2pEnabled) { - logger.verbose('P2P is enabled. Using LibP2P service.'); - config = await configureP2PClientAddresses(_config); - - // Create peer discovery service - const peerIdPrivateKey = await getPeerIdPrivateKey(config, store); - const peerId = await createLibP2PPeerIdFromPrivateKey(peerIdPrivateKey); - const discoveryService = new DiscV5Service(peerId, config, telemetry); - - p2pService = await LibP2PService.new( - clientType, - config, - discoveryService, - peerId, - mempools, - l2BlockSource, - proofVerifier, - worldStateSynchronizer, - store, - telemetry, - ); - } else { - logger.verbose('P2P is disabled. Using dummy P2P service'); - p2pService = new DummyP2PService(); - } - return new P2PClient( - clientType, - store, - l2BlockSource, - mempools, - p2pService, - config.keepProvenTxsInPoolFor, - telemetry, - ); -}; +export * from './factory.js'; diff --git a/yarn-project/p2p/src/index.ts b/yarn-project/p2p/src/index.ts index f8510cf8552..f6f67f3afdb 100644 --- a/yarn-project/p2p/src/index.ts +++ b/yarn-project/p2p/src/index.ts @@ -5,4 +5,4 @@ export * from './config.js'; export * from './mem_pools/epoch_proof_quote_pool/index.js'; export * from './services/index.js'; export * from './mem_pools/tx_pool/index.js'; -export * from './tx_validator/index.js'; +export * from './msg_validators/index.js'; diff --git a/yarn-project/p2p/src/mocks/index.ts b/yarn-project/p2p/src/mocks/index.ts index 56a98c4462d..ff5996d0908 100644 --- a/yarn-project/p2p/src/mocks/index.ts +++ b/yarn-project/p2p/src/mocks/index.ts @@ -5,6 +5,7 @@ import { type Tx, type WorldStateSynchronizer, } from '@aztec/circuit-types'; +import { type EpochCache } from '@aztec/epoch-cache'; import { type DataStoreConfig } from '@aztec/kv-store/config'; import { openTmpStore } from '@aztec/kv-store/lmdb'; import { type TelemetryClient } from '@aztec/telemetry-client'; @@ -101,6 +102,7 @@ export async function createTestLibP2PService( boostrapAddrs: string[] = [], l2BlockSource: L2BlockSource, worldStateSynchronizer: WorldStateSynchronizer, + epochCache: EpochCache, mempools: MemPools, telemetry: TelemetryClient, port: number = 0, @@ -132,6 +134,7 @@ export async function createTestLibP2PService( discoveryService, mempools, l2BlockSource, + epochCache, proofVerifier, worldStateSynchronizer, telemetry, diff --git a/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.test.ts b/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.test.ts new file mode 100644 index 00000000000..59fed59ac05 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.test.ts @@ -0,0 +1,86 @@ +import { PeerErrorSeverity, makeBlockAttestation } from '@aztec/circuit-types'; +import { makeHeader } from '@aztec/circuits.js/testing'; +import { type EpochCache } from '@aztec/epoch-cache'; + +import { mock } from 'jest-mock-extended'; + +import { AttestationValidator } from './attestation_validator.js'; + +describe('AttestationValidator', () => { + let epochCache: EpochCache; + let validator: AttestationValidator; + + beforeEach(() => { + epochCache = mock(); + validator = new AttestationValidator(epochCache); + }); + + it('returns high tolerance error if slot number is not current or next slot', async () => { + // Create an attestation for slot 97 + const header = makeHeader(1, 97, 97); + const mockAttestation = makeBlockAttestation({ + header, + }); + + // Mock epoch cache to return different slot numbers + (epochCache.getProposerInCurrentOrNextSlot as jest.Mock).mockResolvedValue({ + currentSlot: 98n, + nextSlot: 99n, + }); + (epochCache.isInCommittee as jest.Mock).mockResolvedValue(true); + + const result = await validator.validate(mockAttestation); + expect(result).toBe(PeerErrorSeverity.HighToleranceError); + }); + + it('returns high tolerance error if attester is not in committee', async () => { + // The slot is correct, but the attester is not in the committee + const mockAttestation = makeBlockAttestation({ + header: makeHeader(1, 100, 100), + }); + + // Mock epoch cache to return matching slot number but invalid committee membership + (epochCache.getProposerInCurrentOrNextSlot as jest.Mock).mockResolvedValue({ + currentSlot: 100n, + nextSlot: 101n, + }); + (epochCache.isInCommittee as jest.Mock).mockResolvedValue(false); + + const result = await validator.validate(mockAttestation); + expect(result).toBe(PeerErrorSeverity.HighToleranceError); + }); + + it('returns undefined if attestation is valid (current slot)', async () => { + // Create an attestation for slot 100 + const mockAttestation = makeBlockAttestation({ + header: makeHeader(1, 100, 100), + }); + + // Mock epoch cache for valid case with current slot + (epochCache.getProposerInCurrentOrNextSlot as jest.Mock).mockResolvedValue({ + currentSlot: 100n, + nextSlot: 101n, + }); + (epochCache.isInCommittee as jest.Mock).mockResolvedValue(true); + + const result = await validator.validate(mockAttestation); + expect(result).toBeUndefined(); + }); + + it('returns undefined if attestation is valid (next slot)', async () => { + // Setup attestation for next slot + const mockAttestation = makeBlockAttestation({ + header: makeHeader(1, 101, 101), + }); + + // Mock epoch cache for valid case with next slot + (epochCache.getProposerInCurrentOrNextSlot as jest.Mock).mockResolvedValue({ + currentSlot: 100n, + nextSlot: 101n, + }); + (epochCache.isInCommittee as jest.Mock).mockResolvedValue(true); + + const result = await validator.validate(mockAttestation); + expect(result).toBeUndefined(); + }); +}); diff --git a/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.ts b/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.ts new file mode 100644 index 00000000000..ab07e9862ac --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.ts @@ -0,0 +1,26 @@ +import { type BlockAttestation, type P2PValidator, PeerErrorSeverity } from '@aztec/circuit-types'; +import { type EpochCache } from '@aztec/epoch-cache'; + +export class AttestationValidator implements P2PValidator { + private epochCache: EpochCache; + + constructor(epochCache: EpochCache) { + this.epochCache = epochCache; + } + + async validate(message: BlockAttestation): Promise { + const { currentSlot, nextSlot } = await this.epochCache.getProposerInCurrentOrNextSlot(); + + const slotNumberBigInt = message.payload.header.globalVariables.slotNumber.toBigInt(); + if (slotNumberBigInt !== currentSlot && slotNumberBigInt !== nextSlot) { + return PeerErrorSeverity.HighToleranceError; + } + + const attester = message.getSender(); + if (!(await this.epochCache.isInCommittee(attester))) { + return PeerErrorSeverity.HighToleranceError; + } + + return undefined; + } +} diff --git a/yarn-project/p2p/src/msg_validators/attestation_validator/index.ts b/yarn-project/p2p/src/msg_validators/attestation_validator/index.ts new file mode 100644 index 00000000000..611cc23df61 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/attestation_validator/index.ts @@ -0,0 +1 @@ +export * from './attestation_validator.js'; diff --git a/yarn-project/p2p/src/msg_validators/block_proposal_validator/block_proposal_validator.test.ts b/yarn-project/p2p/src/msg_validators/block_proposal_validator/block_proposal_validator.test.ts new file mode 100644 index 00000000000..9ed0f37f928 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/block_proposal_validator/block_proposal_validator.test.ts @@ -0,0 +1,104 @@ +import { PeerErrorSeverity, makeBlockProposal } from '@aztec/circuit-types'; +import { Fr } from '@aztec/circuits.js'; +import { makeHeader } from '@aztec/circuits.js/testing'; +import { type EpochCache } from '@aztec/epoch-cache'; +import { Secp256k1Signer } from '@aztec/foundation/crypto'; + +import { mock } from 'jest-mock-extended'; + +import { BlockProposalValidator } from './block_proposal_validator.js'; + +describe('BlockProposalValidator', () => { + let epochCache: EpochCache; + let validator: BlockProposalValidator; + + beforeEach(() => { + epochCache = mock(); + validator = new BlockProposalValidator(epochCache); + }); + + it('returns high tolerance error if slot number is not current or next slot', async () => { + // Create a block proposal for slot 97 + const mockProposal = makeBlockProposal({ + header: makeHeader(1, 97, 97), + }); + + // Mock epoch cache to return different slot numbers + (epochCache.getProposerInCurrentOrNextSlot as jest.Mock).mockResolvedValue({ + currentSlot: 98n, + nextSlot: 99n, + currentProposer: Fr.random(), + nextProposer: Fr.random(), + }); + + const result = await validator.validate(mockProposal); + expect(result).toBe(PeerErrorSeverity.HighToleranceError); + }); + + it('returns high tolerance error if proposer is not current or next proposer', async () => { + const currentProposer = Secp256k1Signer.random(); + const nextProposer = Secp256k1Signer.random(); + const invalidProposer = Secp256k1Signer.random(); + + // Create a block proposal with correct slot but wrong proposer + const mockProposal = makeBlockProposal({ + header: makeHeader(1, 100, 100), + signer: invalidProposer, + }); + + // Mock epoch cache to return valid slots but different proposers + (epochCache.getProposerInCurrentOrNextSlot as jest.Mock).mockResolvedValue({ + currentSlot: 100n, + nextSlot: 101n, + currentProposer: currentProposer.address, + nextProposer: nextProposer.address, + }); + + const result = await validator.validate(mockProposal); + expect(result).toBe(PeerErrorSeverity.HighToleranceError); + }); + + it('returns undefined if proposal is valid for current slot and proposer', async () => { + const currentProposer = Secp256k1Signer.random(); + const nextProposer = Secp256k1Signer.random(); + + // Create a block proposal for current slot with correct proposer + const mockProposal = makeBlockProposal({ + header: makeHeader(1, 100, 100), + signer: currentProposer, + }); + + // Mock epoch cache for valid case + (epochCache.getProposerInCurrentOrNextSlot as jest.Mock).mockResolvedValue({ + currentSlot: 100n, + nextSlot: 101n, + currentProposer: currentProposer.address, + nextProposer: nextProposer.address, + }); + + const result = await validator.validate(mockProposal); + expect(result).toBeUndefined(); + }); + + it('returns undefined if proposal is valid for next slot and proposer', async () => { + const currentProposer = Secp256k1Signer.random(); + const nextProposer = Secp256k1Signer.random(); + + // Create a block proposal for next slot with correct proposer + const mockProposal = makeBlockProposal({ + header: makeHeader(1, 101, 101), + signer: nextProposer, + }); + + // Mock epoch cache for valid case + (epochCache.getProposerInCurrentOrNextSlot as jest.Mock).mockResolvedValue({ + currentSlot: 100n, + nextSlot: 101n, + currentProposer: currentProposer.address, + nextProposer: nextProposer.address, + }); + + const result = await validator.validate(mockProposal); + expect(result).toBeUndefined(); + }); +}); diff --git a/yarn-project/p2p/src/msg_validators/block_proposal_validator/block_proposal_validator.ts b/yarn-project/p2p/src/msg_validators/block_proposal_validator/block_proposal_validator.ts new file mode 100644 index 00000000000..34063408d09 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/block_proposal_validator/block_proposal_validator.ts @@ -0,0 +1,29 @@ +import { type BlockProposal, type P2PValidator, PeerErrorSeverity } from '@aztec/circuit-types'; +import { type EpochCache } from '@aztec/epoch-cache'; + +export class BlockProposalValidator implements P2PValidator { + private epochCache: EpochCache; + + constructor(epochCache: EpochCache) { + this.epochCache = epochCache; + } + + async validate(block: BlockProposal): Promise { + const { currentProposer, nextProposer, currentSlot, nextSlot } = + await this.epochCache.getProposerInCurrentOrNextSlot(); + + // Check that the attestation is for the current or next slot + const slotNumberBigInt = block.payload.header.globalVariables.slotNumber.toBigInt(); + if (slotNumberBigInt !== currentSlot && slotNumberBigInt !== nextSlot) { + return PeerErrorSeverity.HighToleranceError; + } + + // Check that the block proposal is from the current or next proposer + const proposer = block.getSender(); + if (!proposer.equals(currentProposer) && !proposer.equals(nextProposer)) { + return PeerErrorSeverity.HighToleranceError; + } + + return undefined; + } +} diff --git a/yarn-project/p2p/src/msg_validators/block_proposal_validator/index.ts b/yarn-project/p2p/src/msg_validators/block_proposal_validator/index.ts new file mode 100644 index 00000000000..7098ac47d19 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/block_proposal_validator/index.ts @@ -0,0 +1 @@ +export * from './block_proposal_validator.js'; diff --git a/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.test.ts b/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.test.ts new file mode 100644 index 00000000000..de90b81f9c3 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.test.ts @@ -0,0 +1,68 @@ +import { EpochProofQuote, EpochProofQuotePayload, PeerErrorSeverity } from '@aztec/circuit-types'; +import { type EpochCache } from '@aztec/epoch-cache'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Signature } from '@aztec/foundation/eth-signature'; + +import { mock } from 'jest-mock-extended'; + +import { EpochProofQuoteValidator } from './epoch_proof_quote_validator.js'; + +describe('EpochProofQuoteValidator', () => { + let epochCache: EpochCache; + let validator: EpochProofQuoteValidator; + + beforeEach(() => { + epochCache = mock(); + validator = new EpochProofQuoteValidator(epochCache); + }); + + const makeEpochProofQuote = (epochToProve: bigint) => { + const payload = EpochProofQuotePayload.from({ + basisPointFee: 5000, + bondAmount: 1000000000000000000n, + epochToProve, + prover: EthAddress.random(), + validUntilSlot: 100n, + }); + return new EpochProofQuote(payload, Signature.random()); + }; + + it('returns high tolerance error if epoch to prove is not current or previous epoch', async () => { + // Create an epoch proof quote for epoch 5 + const mockQuote = makeEpochProofQuote(5n); + + // Mock epoch cache to return different epoch + (epochCache.getEpochAndSlotNow as jest.Mock).mockReturnValue({ + epoch: 7n, + }); + + const result = await validator.validate(mockQuote); + expect(result).toBe(PeerErrorSeverity.HighToleranceError); + }); + + it('returns no error if epoch to prove is current epoch', async () => { + // Create an epoch proof quote for current epoch + const mockQuote = makeEpochProofQuote(7n); + + // Mock epoch cache to return matching epoch + (epochCache.getEpochAndSlotNow as jest.Mock).mockReturnValue({ + epoch: 7n, + }); + + const result = await validator.validate(mockQuote); + expect(result).toBeUndefined(); + }); + + it('returns no error if epoch to prove is previous epoch', async () => { + // Create an epoch proof quote for previous epoch + const mockQuote = makeEpochProofQuote(6n); + + // Mock epoch cache to return current epoch + (epochCache.getEpochAndSlotNow as jest.Mock).mockReturnValue({ + epoch: 7n, + }); + + const result = await validator.validate(mockQuote); + expect(result).toBeUndefined(); + }); +}); diff --git a/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.ts b/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.ts new file mode 100644 index 00000000000..99eec5be73d --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.ts @@ -0,0 +1,22 @@ +import { type EpochProofQuote, type P2PValidator, PeerErrorSeverity } from '@aztec/circuit-types'; +import { type EpochCache } from '@aztec/epoch-cache'; + +export class EpochProofQuoteValidator implements P2PValidator { + private epochCache: EpochCache; + + constructor(epochCache: EpochCache) { + this.epochCache = epochCache; + } + + validate(message: EpochProofQuote): Promise { + const { epoch } = this.epochCache.getEpochAndSlotNow(); + + // Check that the epoch proof quote is for the current epoch + const epochToProve = message.payload.epochToProve; + if (epochToProve !== epoch && epochToProve !== epoch - 1n) { + return Promise.resolve(PeerErrorSeverity.HighToleranceError); + } + + return Promise.resolve(undefined); + } +} diff --git a/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/index.ts b/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/index.ts new file mode 100644 index 00000000000..fc8ab006e7e --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/epoch_proof_quote_validator/index.ts @@ -0,0 +1 @@ +export { EpochProofQuoteValidator } from './epoch_proof_quote_validator.js'; diff --git a/yarn-project/p2p/src/msg_validators/index.ts b/yarn-project/p2p/src/msg_validators/index.ts new file mode 100644 index 00000000000..a9f709d3dc1 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/index.ts @@ -0,0 +1,3 @@ +export * from './tx_validator/index.js'; +export * from './block_proposal_validator/index.js'; +export * from './attestation_validator/index.js'; diff --git a/yarn-project/p2p/src/tx_validator/aggregate_tx_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/aggregate_tx_validator.test.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/aggregate_tx_validator.test.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/aggregate_tx_validator.test.ts diff --git a/yarn-project/p2p/src/tx_validator/aggregate_tx_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/aggregate_tx_validator.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/aggregate_tx_validator.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/aggregate_tx_validator.ts diff --git a/yarn-project/p2p/src/tx_validator/data_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/data_validator.test.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts diff --git a/yarn-project/p2p/src/tx_validator/data_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/data_validator.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/data_validator.ts diff --git a/yarn-project/p2p/src/tx_validator/double_spend_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/double_spend_validator.test.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/double_spend_validator.test.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/double_spend_validator.test.ts diff --git a/yarn-project/p2p/src/tx_validator/double_spend_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/double_spend_validator.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/double_spend_validator.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/double_spend_validator.ts diff --git a/yarn-project/p2p/src/tx_validator/index.ts b/yarn-project/p2p/src/msg_validators/tx_validator/index.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/index.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/index.ts diff --git a/yarn-project/p2p/src/tx_validator/metadata_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/metadata_validator.test.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/metadata_validator.test.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/metadata_validator.test.ts diff --git a/yarn-project/p2p/src/tx_validator/metadata_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/metadata_validator.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/metadata_validator.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/metadata_validator.ts diff --git a/yarn-project/p2p/src/tx_validator/tx_proof_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/tx_proof_validator.ts similarity index 100% rename from yarn-project/p2p/src/tx_validator/tx_proof_validator.ts rename to yarn-project/p2p/src/msg_validators/tx_validator/tx_proof_validator.ts diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index bfbf5b7df20..2056cf80f28 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -6,6 +6,7 @@ import { type Gossipable, type L2BlockSource, MerkleTreeId, + PeerErrorSeverity, type PeerInfo, type RawGossipMessage, TopicTypeMap, @@ -17,6 +18,7 @@ import { } from '@aztec/circuit-types'; import { P2PClientType } from '@aztec/circuit-types'; import { Fr } from '@aztec/circuits.js'; +import { type EpochCache } from '@aztec/epoch-cache'; import { createLogger } from '@aztec/foundation/log'; import { SerialQueue } from '@aztec/foundation/queue'; import { RunningPromise } from '@aztec/foundation/running-promise'; @@ -37,16 +39,17 @@ import { createLibp2p } from 'libp2p'; import { type P2PConfig } from '../../config.js'; import { type MemPools } from '../../mem_pools/interface.js'; +import { EpochProofQuoteValidator } from '../../msg_validators/epoch_proof_quote_validator/index.js'; +import { AttestationValidator, BlockProposalValidator } from '../../msg_validators/index.js'; import { DataTxValidator, DoubleSpendTxValidator, MetadataTxValidator, TxProofValidator, -} from '../../tx_validator/index.js'; +} from '../../msg_validators/tx_validator/index.js'; import { type PubSubLibp2p, convertToMultiaddr } from '../../util.js'; import { AztecDatastore } from '../data_store.js'; import { SnappyTransform, fastMsgIdFn, getMsgIdFn, msgIdToStrFn } from '../encoding.js'; -import { PeerErrorSeverity } from '../peer-scoring/peer_scoring.js'; import { PeerManager } from '../peer_manager.js'; import { pingHandler, statusHandler } from '../reqresp/handlers.js'; import { @@ -85,6 +88,11 @@ export class LibP2PService extends WithTracer implement private peerManager: PeerManager; private discoveryRunningPromise?: RunningPromise; + // Message validators + private attestationValidator: AttestationValidator; + private blockProposalValidator: BlockProposalValidator; + private epochProofQuoteValidator: EpochProofQuoteValidator; + // Request and response sub service public reqresp: ReqResp; @@ -102,6 +110,7 @@ export class LibP2PService extends WithTracer implement private peerDiscoveryService: PeerDiscoveryService, private mempools: MemPools, private l2BlockSource: L2BlockSource, + private epochCache: EpochCache, private proofVerifier: ClientProtocolCircuitVerifier, private worldStateSynchronizer: WorldStateSynchronizer, private telemetry: TelemetryClient, @@ -117,6 +126,10 @@ export class LibP2PService extends WithTracer implement this.node.services.pubsub.score.params.appSpecificWeight = 10; this.reqresp = new ReqResp(config, node, this.peerManager); + this.attestationValidator = new AttestationValidator(epochCache); + this.blockProposalValidator = new BlockProposalValidator(epochCache); + this.epochProofQuoteValidator = new EpochProofQuoteValidator(epochCache); + this.blockReceivedCallback = (block: BlockProposal): Promise => { this.logger.verbose( `[WARNING] handler not yet registered: Block received callback not set. Received block ${block.p2pMessageIdentifier()} from peer.`, @@ -153,7 +166,17 @@ export class LibP2PService extends WithTracer implement } // Add p2p topic validators - this.node.services.pubsub.topicValidators.set(Tx.p2pTopic, this.validatePropagatedTxFromMessage.bind(this)); + // As they are stored within a kv pair, there is no need to register them conditionally + // based on the client type + const topicValidators = { + [Tx.p2pTopic]: this.validatePropagatedTxFromMessage.bind(this), + [BlockAttestation.p2pTopic]: this.validatePropagatedAttestationFromMessage.bind(this), + [BlockProposal.p2pTopic]: this.validatePropagatedBlockFromMessage.bind(this), + [EpochProofQuote.p2pTopic]: this.validatePropagatedEpochProofQuoteFromMessage.bind(this), + }; + for (const [topic, validator] of Object.entries(topicValidators)) { + this.node.services.pubsub.topicValidators.set(topic, validator); + } // add GossipSub listener this.node.services.pubsub.addEventListener('gossipsub:message', async e => { @@ -215,6 +238,7 @@ export class LibP2PService extends WithTracer implement peerId: PeerId, mempools: MemPools, l2BlockSource: L2BlockSource, + epochCache: EpochCache, proofVerifier: ClientProtocolCircuitVerifier, worldStateSynchronizer: WorldStateSynchronizer, store: AztecKVStore, @@ -329,6 +353,7 @@ export class LibP2PService extends WithTracer implement peerDiscoveryService, mempools, l2BlockSource, + epochCache, proofVerifier, worldStateSynchronizer, telemetry, @@ -537,6 +562,12 @@ export class LibP2PService extends WithTracer implement return true; } + /** + * Validate a tx from a peer. + * @param propagationSource - The peer ID of the peer that sent the tx. + * @param msg - The tx message. + * @returns True if the tx is valid, false otherwise. + */ private async validatePropagatedTxFromMessage( propagationSource: PeerId, msg: Message, @@ -551,11 +582,62 @@ export class LibP2PService extends WithTracer implement } /** - * Validate a tx that has been propagated from a peer. - * @param tx - The tx to validate. - * @param peerId - The peer ID of the peer that sent the tx. - * @returns True if the tx is valid, false otherwise. + * Validate an attestation from a peer. + * @param propagationSource - The peer ID of the peer that sent the attestation. + * @param msg - The attestation message. + * @returns True if the attestation is valid, false otherwise. + */ + private async validatePropagatedAttestationFromMessage( + propagationSource: PeerId, + msg: Message, + ): Promise { + const attestation = BlockAttestation.fromBuffer(Buffer.from(msg.data)); + const isValid = await this.validateAttestation(propagationSource, attestation); + this.logger.trace(`validatePropagatedAttestation: ${isValid}`, { + [Attributes.SLOT_NUMBER]: attestation.payload.header.globalVariables.slotNumber.toString(), + [Attributes.P2P_ID]: propagationSource.toString(), + }); + return isValid ? TopicValidatorResult.Accept : TopicValidatorResult.Reject; + } + + /** + * Validate a block proposal from a peer. + * @param propagationSource - The peer ID of the peer that sent the block. + * @param msg - The block proposal message. + * @returns True if the block proposal is valid, false otherwise. */ + private async validatePropagatedBlockFromMessage( + propagationSource: PeerId, + msg: Message, + ): Promise { + const block = BlockProposal.fromBuffer(Buffer.from(msg.data)); + const isValid = await this.validateBlockProposal(propagationSource, block); + this.logger.trace(`validatePropagatedBlock: ${isValid}`, { + [Attributes.SLOT_NUMBER]: block.payload.header.globalVariables.slotNumber.toString(), + [Attributes.P2P_ID]: propagationSource.toString(), + }); + return isValid ? TopicValidatorResult.Accept : TopicValidatorResult.Reject; + } + + /** + * Validate an epoch proof quote from a peer. + * @param propagationSource - The peer ID of the peer that sent the epoch proof quote. + * @param msg - The epoch proof quote message. + * @returns True if the epoch proof quote is valid, false otherwise. + */ + private async validatePropagatedEpochProofQuoteFromMessage( + propagationSource: PeerId, + msg: Message, + ): Promise { + const epochProofQuote = EpochProofQuote.fromBuffer(Buffer.from(msg.data)); + const isValid = await this.validateEpochProofQuote(propagationSource, epochProofQuote); + this.logger.trace(`validatePropagatedEpochProofQuote: ${isValid}`, { + [Attributes.EPOCH_NUMBER]: epochProofQuote.payload.epochToProve.toString(), + [Attributes.P2P_ID]: propagationSource.toString(), + }); + return isValid ? TopicValidatorResult.Accept : TopicValidatorResult.Reject; + } + @trackSpan('Libp2pService.validatePropagatedTx', tx => ({ [Attributes.TX_HASH]: tx.getTxHash().toString(), })) @@ -687,6 +769,63 @@ export class LibP2PService extends WithTracer implement return true; } + /** + * Validate an attestation. + * + * @param attestation - The attestation to validate. + * @returns True if the attestation is valid, false otherwise. + */ + @trackSpan('Libp2pService.validateAttestation', (_peerId, attestation) => ({ + [Attributes.SLOT_NUMBER]: attestation.payload.header.globalVariables.slotNumber.toString(), + })) + public async validateAttestation(peerId: PeerId, attestation: BlockAttestation): Promise { + const severity = await this.attestationValidator.validate(attestation); + if (severity) { + this.peerManager.penalizePeer(peerId, severity); + return false; + } + + return true; + } + + /** + * Validate a block proposal. + * + * @param block - The block proposal to validate. + * @returns True if the block proposal is valid, false otherwise. + */ + @trackSpan('Libp2pService.validateBlockProposal', (_peerId, block) => ({ + [Attributes.SLOT_NUMBER]: block.payload.header.globalVariables.slotNumber.toString(), + })) + public async validateBlockProposal(peerId: PeerId, block: BlockProposal): Promise { + const severity = await this.blockProposalValidator.validate(block); + if (severity) { + this.peerManager.penalizePeer(peerId, severity); + return false; + } + + return true; + } + + /** + * Validate an epoch proof quote. + * + * @param epochProofQuote - The epoch proof quote to validate. + * @returns True if the epoch proof quote is valid, false otherwise. + */ + @trackSpan('Libp2pService.validateEpochProofQuote', (_peerId, epochProofQuote) => ({ + [Attributes.EPOCH_NUMBER]: epochProofQuote.payload.epochToProve.toString(), + })) + public async validateEpochProofQuote(peerId: PeerId, epochProofQuote: EpochProofQuote): Promise { + const severity = await this.epochProofQuoteValidator.validate(epochProofQuote); + if (severity) { + this.peerManager.penalizePeer(peerId, severity); + return false; + } + + return true; + } + public getPeerScore(peerId: PeerId): number { return this.node.services.pubsub.score.score(peerId.toString()); } diff --git a/yarn-project/p2p/src/services/peer-scoring/peer_scoring.test.ts b/yarn-project/p2p/src/services/peer-scoring/peer_scoring.test.ts index 92592d9a026..a8e38b6e42b 100644 --- a/yarn-project/p2p/src/services/peer-scoring/peer_scoring.test.ts +++ b/yarn-project/p2p/src/services/peer-scoring/peer_scoring.test.ts @@ -1,7 +1,9 @@ +import { PeerErrorSeverity } from '@aztec/circuit-types'; + import { jest } from '@jest/globals'; import { getP2PDefaultConfig } from '../../config.js'; -import { PeerErrorSeverity, PeerScoring } from './peer_scoring.js'; +import { PeerScoring } from './peer_scoring.js'; describe('PeerScoring', () => { let peerScoring: PeerScoring; diff --git a/yarn-project/p2p/src/services/peer-scoring/peer_scoring.ts b/yarn-project/p2p/src/services/peer-scoring/peer_scoring.ts index 93a62645e0e..f16c21f3bf7 100644 --- a/yarn-project/p2p/src/services/peer-scoring/peer_scoring.ts +++ b/yarn-project/p2p/src/services/peer-scoring/peer_scoring.ts @@ -1,25 +1,8 @@ +import { PeerErrorSeverity } from '@aztec/circuit-types'; import { median } from '@aztec/foundation/collection'; import { type P2PConfig } from '../../config.js'; -export enum PeerErrorSeverity { - /** - * Not malicious action, but it must not be tolerated - * ~2 occurrences will get the peer banned - */ - LowToleranceError = 'LowToleranceError', - /** - * Negative action that can be tolerated only sometimes - * ~10 occurrences will get the peer banned - */ - MidToleranceError = 'MidToleranceError', - /** - * Some error that can be tolerated multiple times - * ~50 occurrences will get the peer banned - */ - HighToleranceError = 'HighToleranceError', -} - const DefaultPeerPenalties = { [PeerErrorSeverity.LowToleranceError]: 2, [PeerErrorSeverity.MidToleranceError]: 10, diff --git a/yarn-project/p2p/src/services/peer_manager.ts b/yarn-project/p2p/src/services/peer_manager.ts index f078b2924d4..0e39c71b0c7 100644 --- a/yarn-project/p2p/src/services/peer_manager.ts +++ b/yarn-project/p2p/src/services/peer_manager.ts @@ -1,4 +1,4 @@ -import { type PeerInfo } from '@aztec/circuit-types'; +import { type PeerErrorSeverity, type PeerInfo } from '@aztec/circuit-types'; import { createLogger } from '@aztec/foundation/log'; import { type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client'; @@ -9,7 +9,7 @@ import { inspect } from 'util'; import { type P2PConfig } from '../config.js'; import { type PubSubLibp2p } from '../util.js'; -import { type PeerErrorSeverity, PeerScoring } from './peer-scoring/peer_scoring.js'; +import { PeerScoring } from './peer-scoring/peer_scoring.js'; import { type PeerDiscoveryService } from './service.js'; const MAX_DIAL_ATTEMPTS = 3; diff --git a/yarn-project/p2p/src/services/reqresp/rate_limiter/rate_limiter.test.ts b/yarn-project/p2p/src/services/reqresp/rate_limiter/rate_limiter.test.ts index 019d0a7183b..9e8502eed2b 100644 --- a/yarn-project/p2p/src/services/reqresp/rate_limiter/rate_limiter.test.ts +++ b/yarn-project/p2p/src/services/reqresp/rate_limiter/rate_limiter.test.ts @@ -1,8 +1,9 @@ +import { PeerErrorSeverity } from '@aztec/circuit-types'; + import { jest } from '@jest/globals'; import { type PeerId } from '@libp2p/interface'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { PeerErrorSeverity } from '../../peer-scoring/peer_scoring.js'; import { type PeerManager } from '../../peer_manager.js'; import { PING_PROTOCOL, type ReqRespSubProtocolRateLimits, TX_REQ_PROTOCOL } from '../interface.js'; import { RequestResponseRateLimiter } from './rate_limiter.js'; diff --git a/yarn-project/p2p/src/services/reqresp/rate_limiter/rate_limiter.ts b/yarn-project/p2p/src/services/reqresp/rate_limiter/rate_limiter.ts index be92f06ec2f..495aea8bac2 100644 --- a/yarn-project/p2p/src/services/reqresp/rate_limiter/rate_limiter.ts +++ b/yarn-project/p2p/src/services/reqresp/rate_limiter/rate_limiter.ts @@ -3,9 +3,10 @@ * Rationale is that if it was good enough for them, then it should be good enough for us. * https://github.com/ChainSafe/lodestar */ +import { PeerErrorSeverity } from '@aztec/circuit-types'; + import { type PeerId } from '@libp2p/interface'; -import { PeerErrorSeverity } from '../../peer-scoring/peer_scoring.js'; import { type PeerManager } from '../../peer_manager.js'; import { type ReqRespSubProtocol, type ReqRespSubProtocolRateLimits } from '../interface.js'; import { DEFAULT_RATE_LIMITS } from './rate_limits.js'; diff --git a/yarn-project/p2p/src/services/reqresp/reqresp.integration.test.ts b/yarn-project/p2p/src/services/reqresp/reqresp.integration.test.ts index d94075e288c..d930341268c 100644 --- a/yarn-project/p2p/src/services/reqresp/reqresp.integration.test.ts +++ b/yarn-project/p2p/src/services/reqresp/reqresp.integration.test.ts @@ -3,9 +3,12 @@ import { MockL2BlockSource } from '@aztec/archiver/test'; import { type ClientProtocolCircuitVerifier, P2PClientType, + PeerErrorSeverity, + type Tx, type WorldStateSynchronizer, mockTx, } from '@aztec/circuit-types'; +import { type EpochCache } from '@aztec/epoch-cache'; import { createLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; import { type AztecKVStore } from '@aztec/kv-store'; @@ -16,6 +19,7 @@ import { SignableENR } from '@chainsafe/enr'; import { describe, expect, it, jest } from '@jest/globals'; import { multiaddr } from '@multiformats/multiaddr'; import getPort from 'get-port'; +import { type MockProxy, mock } from 'jest-mock-extended'; import { generatePrivateKey } from 'viem/accounts'; import { createP2PClient } from '../../client/index.js'; @@ -27,14 +31,6 @@ import { type TxPool } from '../../mem_pools/tx_pool/index.js'; import { AlwaysFalseCircuitVerifier, AlwaysTrueCircuitVerifier } from '../../mocks/index.js'; import { convertToMultiaddr, createLibP2PPeerIdFromPrivateKey } from '../../util.js'; import { AZTEC_ENR_KEY, AZTEC_NET } from '../discv5/discV5_service.js'; -import { PeerErrorSeverity } from '../peer-scoring/peer_scoring.js'; - -/** - * Mockify helper for testing purposes. - */ -type Mockify = { - [P in keyof T]: ReturnType; -}; const TEST_TIMEOUT = 80000; @@ -49,41 +45,11 @@ function generatePeerIdPrivateKeys(numberOfPeers: number): string[] { const NUMBER_OF_PEERS = 2; -// Mock the mempools -const makeMockPools = () => { - return { - txPool: { - addTxs: jest.fn(() => {}), - getTxByHash: jest.fn().mockReturnValue(undefined), - deleteTxs: jest.fn(), - getAllTxs: jest.fn().mockReturnValue([]), - getAllTxHashes: jest.fn().mockReturnValue([]), - getMinedTxHashes: jest.fn().mockReturnValue([]), - getPendingTxHashes: jest.fn().mockReturnValue([]), - getTxStatus: jest.fn().mockReturnValue(undefined), - markAsMined: jest.fn(), - markMinedAsPending: jest.fn(), - }, - attestationPool: { - addAttestations: jest.fn(), - deleteAttestations: jest.fn(), - deleteAttestationsForSlot: jest.fn(), - deleteAttestationsOlderThan: jest.fn(), - deleteAttestationsForSlotAndProposal: jest.fn(), - getAttestationsForSlot: jest.fn().mockReturnValue(undefined), - }, - epochProofQuotePool: { - addQuote: jest.fn(), - getQuotes: jest.fn().mockReturnValue([]), - deleteQuotesToEpoch: jest.fn(), - }, - }; -}; - describe('Req Resp p2p client integration', () => { - let txPool: Mockify; - let attestationPool: Mockify; - let epochProofQuotePool: Mockify; + let txPool: MockProxy; + let attestationPool: MockProxy; + let epochProofQuotePool: MockProxy; + let epochCache: MockProxy; let l2BlockSource: MockL2BlockSource; let kvStore: AztecKVStore; let worldState: WorldStateSynchronizer; @@ -91,7 +57,14 @@ describe('Req Resp p2p client integration', () => { const logger = createLogger('p2p:test:client-integration'); beforeEach(() => { - ({ txPool, attestationPool, epochProofQuotePool } = makeMockPools()); + txPool = mock(); + attestationPool = mock(); + epochProofQuotePool = mock(); + epochCache = mock(); + + txPool.getAllTxs.mockImplementation(() => { + return [] as Tx[]; + }); }); const getPorts = (numberOfPeers: number) => Promise.all(Array.from({ length: numberOfPeers }, () => getPort())); @@ -160,6 +133,7 @@ describe('Req Resp p2p client integration', () => { l2BlockSource, proofVerifier, worldState, + epochCache, undefined, deps, ); diff --git a/yarn-project/p2p/src/services/reqresp/reqresp.test.ts b/yarn-project/p2p/src/services/reqresp/reqresp.test.ts index 499ebd63564..11a951d9e33 100644 --- a/yarn-project/p2p/src/services/reqresp/reqresp.test.ts +++ b/yarn-project/p2p/src/services/reqresp/reqresp.test.ts @@ -1,4 +1,4 @@ -import { TxHash, mockTx } from '@aztec/circuit-types'; +import { PeerErrorSeverity, TxHash, mockTx } from '@aztec/circuit-types'; import { sleep } from '@aztec/foundation/sleep'; import { describe, expect, it, jest } from '@jest/globals'; @@ -14,7 +14,6 @@ import { startNodes, stopNodes, } from '../../mocks/index.js'; -import { PeerErrorSeverity } from '../peer-scoring/peer_scoring.js'; import { type PeerManager } from '../peer_manager.js'; import { PING_PROTOCOL, RequestableBuffer, TX_REQ_PROTOCOL } from './interface.js'; diff --git a/yarn-project/p2p/src/services/reqresp/reqresp.ts b/yarn-project/p2p/src/services/reqresp/reqresp.ts index 261c2d09fd2..f7fb2efebb9 100644 --- a/yarn-project/p2p/src/services/reqresp/reqresp.ts +++ b/yarn-project/p2p/src/services/reqresp/reqresp.ts @@ -1,4 +1,5 @@ // @attribution: lodestar impl for inspiration +import { PeerErrorSeverity } from '@aztec/circuit-types'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { executeTimeoutWithCustomError } from '@aztec/foundation/timer'; @@ -13,7 +14,6 @@ import { InvalidResponseError, } from '../../errors/reqresp.error.js'; import { SnappyTransform } from '../encoding.js'; -import { PeerErrorSeverity } from '../peer-scoring/peer_scoring.js'; import { type PeerManager } from '../peer_manager.js'; import { type P2PReqRespConfig } from './config.js'; import { diff --git a/yarn-project/p2p/tsconfig.json b/yarn-project/p2p/tsconfig.json index dbffd3db046..e24bd0fd165 100644 --- a/yarn-project/p2p/tsconfig.json +++ b/yarn-project/p2p/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../circuits.js" }, + { + "path": "../epoch-cache" + }, { "path": "../foundation" }, diff --git a/yarn-project/prover-node/package.json b/yarn-project/prover-node/package.json index 4d0fc44fd24..39235ab951d 100644 --- a/yarn-project/prover-node/package.json +++ b/yarn-project/prover-node/package.json @@ -61,6 +61,7 @@ "@aztec/bb-prover": "workspace:^", "@aztec/circuit-types": "workspace:^", "@aztec/circuits.js": "workspace:^", + "@aztec/epoch-cache": "workspace:^", "@aztec/ethereum": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/kv-store": "workspace:^", diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index c1a5f27026a..f7f777626fb 100644 --- a/yarn-project/prover-node/src/factory.ts +++ b/yarn-project/prover-node/src/factory.ts @@ -1,5 +1,6 @@ import { type Archiver, createArchiver } from '@aztec/archiver'; import { type ProverCoordination, type ProvingJobBroker } from '@aztec/circuit-types'; +import { EpochCache } from '@aztec/epoch-cache'; import { createEthereumChain } from '@aztec/ethereum'; import { Buffer32 } from '@aztec/foundation/buffer'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -51,12 +52,15 @@ export async function createProverNode( // REFACTOR: Move publisher out of sequencer package and into an L1-related package const publisher = deps.publisher ?? new L1Publisher(config, telemetry); + const epochCache = await EpochCache.create(config.l1Contracts.rollupAddress, config); + // If config.p2pEnabled is true, createProverCoordination will create a p2p client where quotes will be shared and tx's requested // If config.p2pEnabled is false, createProverCoordination request information from the AztecNode const proverCoordination = await createProverCoordination(config, { aztecNodeTxProvider: deps.aztecNodeTxProvider, worldStateSynchronizer, archiver, + epochCache, telemetry, }); diff --git a/yarn-project/prover-node/src/prover-coordination/factory.ts b/yarn-project/prover-node/src/prover-coordination/factory.ts index 48194d44c47..88731deec2a 100644 --- a/yarn-project/prover-node/src/prover-coordination/factory.ts +++ b/yarn-project/prover-node/src/prover-coordination/factory.ts @@ -6,6 +6,7 @@ import { type WorldStateSynchronizer, createAztecNodeClient, } from '@aztec/circuit-types'; +import { type EpochCache } from '@aztec/epoch-cache'; import { createLogger } from '@aztec/foundation/log'; import { type DataStoreConfig } from '@aztec/kv-store/config'; import { createP2PClient } from '@aztec/p2p'; @@ -19,6 +20,7 @@ type ProverCoordinationDeps = { worldStateSynchronizer?: WorldStateSynchronizer; archiver?: Archiver | ArchiveSource; telemetry?: TelemetryClient; + epochCache?: EpochCache; }; /** @@ -41,7 +43,7 @@ export async function createProverCoordination( if (config.p2pEnabled) { log.info('Using prover coordination via p2p'); - if (!deps.archiver || !deps.worldStateSynchronizer || !deps.telemetry) { + if (!deps.archiver || !deps.worldStateSynchronizer || !deps.telemetry || !deps.epochCache) { throw new Error('Missing dependencies for p2p prover coordination'); } @@ -52,6 +54,7 @@ export async function createProverCoordination( deps.archiver, proofVerifier, deps.worldStateSynchronizer, + deps.epochCache, deps.telemetry, ); await p2pClient.start(); diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index f3fce627a36..bb73e84bfe1 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -13,6 +13,7 @@ import { type WorldStateSynchronizer, } from '@aztec/circuit-types'; import { type ContractDataSource, EthAddress, Fr } from '@aztec/circuits.js'; +import { type EpochCache } from '@aztec/epoch-cache'; import { times } from '@aztec/foundation/collection'; import { Signature } from '@aztec/foundation/eth-signature'; import { makeBackoff, retry } from '@aztec/foundation/retry'; @@ -310,6 +311,7 @@ describe('prover-node', () => { // - The prover node can get the it is missing via p2p, or it has them in it's mempool describe('Using a p2p coordination', () => { let bootnode: BootstrapNode; + let epochCache: MockProxy; let p2pClient: P2PClient; let otherP2PClient: P2PClient; @@ -318,11 +320,13 @@ describe('prover-node', () => { txPool: new InMemoryTxPool(telemetryClient), epochProofQuotePool: new MemoryEpochProofQuotePool(telemetryClient), }; + epochCache = mock(); const libp2pService = await createTestLibP2PService( P2PClientType.Prover, [bootnodeAddr], l2BlockSource, worldState, + epochCache, mempools, telemetryClient, port, @@ -370,14 +374,21 @@ describe('prover-node', () => { }); it('Should send a proof quote via p2p to another node', async () => { + const epochNumber = 10n; + epochCache.getEpochAndSlotNow.mockReturnValue({ + epoch: epochNumber, + slot: epochNumber * 2n, + ts: BigInt(Date.now()), + }); + // Check that the p2p client receives the quote (casted as any to access private property) const p2pEpochReceivedSpy = jest.spyOn((otherP2PClient as any).p2pService, 'processEpochProofQuoteFromPeer'); // Check the other node's pool has no quotes yet - const peerInitialState = await otherP2PClient.getEpochProofQuotes(10n); + const peerInitialState = await otherP2PClient.getEpochProofQuotes(epochNumber); expect(peerInitialState.length).toEqual(0); - await proverNode.handleEpochCompleted(10n); + await proverNode.handleEpochCompleted(epochNumber); // Wait for message to be propagated await retry( @@ -391,8 +402,8 @@ describe('prover-node', () => { ); // We should be able to retreive the quote from the other node - const peerFinalStateQuotes = await otherP2PClient.getEpochProofQuotes(10n); - expect(peerFinalStateQuotes[0]).toEqual(toExpectedQuote(10n)); + const peerFinalStateQuotes = await otherP2PClient.getEpochProofQuotes(epochNumber); + expect(peerFinalStateQuotes[0]).toEqual(toExpectedQuote(epochNumber)); }); }); }); diff --git a/yarn-project/prover-node/tsconfig.json b/yarn-project/prover-node/tsconfig.json index 93d30fb1eb8..b4f4776b00d 100644 --- a/yarn-project/prover-node/tsconfig.json +++ b/yarn-project/prover-node/tsconfig.json @@ -18,6 +18,9 @@ { "path": "../circuits.js" }, + { + "path": "../epoch-cache" + }, { "path": "../ethereum" }, diff --git a/yarn-project/validator-client/src/factory.ts b/yarn-project/validator-client/src/factory.ts index 3382163f408..3e4bf64acf7 100644 --- a/yarn-project/validator-client/src/factory.ts +++ b/yarn-project/validator-client/src/factory.ts @@ -1,5 +1,4 @@ -import { EpochCache, type EpochCacheConfig } from '@aztec/epoch-cache'; -import { type EthAddress } from '@aztec/foundation/eth-address'; +import { type EpochCache } from '@aztec/epoch-cache'; import { type DateProvider } from '@aztec/foundation/timer'; import { type P2P } from '@aztec/p2p'; import { type TelemetryClient } from '@aztec/telemetry-client'; @@ -9,13 +8,13 @@ import { generatePrivateKey } from 'viem/accounts'; import { type ValidatorClientConfig } from './config.js'; import { ValidatorClient } from './validator.js'; -export async function createValidatorClient( - config: ValidatorClientConfig & EpochCacheConfig, - rollupAddress: EthAddress, +export function createValidatorClient( + config: ValidatorClientConfig, deps: { p2pClient: P2P; telemetry: TelemetryClient; dateProvider: DateProvider; + epochCache: EpochCache; }, ) { if (config.disableValidator) { @@ -25,8 +24,5 @@ export async function createValidatorClient( config.validatorPrivateKey = generatePrivateKey(); } - // Create the epoch cache - const epochCache = await EpochCache.create(rollupAddress, config, deps); - - return ValidatorClient.new(config, epochCache, deps.p2pClient, deps.telemetry); + return ValidatorClient.new(config, deps.epochCache, deps.p2pClient, deps.telemetry); } diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts index 42e673058b9..cc4c10226dd 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -15,6 +15,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise'; import { sleep } from '@aztec/foundation/sleep'; import { type Timer } from '@aztec/foundation/timer'; import { type P2P } from '@aztec/p2p'; +import { BlockProposalValidator } from '@aztec/p2p/msg_validators'; import { type TelemetryClient, WithTracer } from '@aztec/telemetry-client'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; @@ -71,6 +72,8 @@ export class ValidatorClient extends WithTracer implements Validator { private epochCacheUpdateLoop: RunningPromise; + private blockProposalValidator: BlockProposalValidator; + constructor( private keyStore: ValidatorKeyStore, private epochCache: EpochCache, @@ -85,6 +88,8 @@ export class ValidatorClient extends WithTracer implements Validator { this.validationService = new ValidationService(keyStore); + this.blockProposalValidator = new BlockProposalValidator(epochCache); + // Refresh epoch cache every second to trigger commiteeChanged event this.epochCacheUpdateLoop = new RunningPromise( () => @@ -180,18 +185,9 @@ export class ValidatorClient extends WithTracer implements Validator { } // Check that the proposal is from the current proposer, or the next proposer. - const proposalSender = proposal.getSender(); - const { currentProposer, nextProposer, currentSlot, nextSlot } = - await this.epochCache.getProposerInCurrentOrNextSlot(); - if (!proposalSender.equals(currentProposer) && !proposalSender.equals(nextProposer)) { - this.log.verbose(`Not the current or next proposer, skipping attestation`); - return undefined; - } - - // Check that the proposal is for the current or next slot - const slotNumberBigInt = proposal.slotNumber.toBigInt(); - if (slotNumberBigInt !== currentSlot && slotNumberBigInt !== nextSlot) { - this.log.verbose(`Not the current or next slot, skipping attestation`); + const invalidProposal = await this.blockProposalValidator.validate(proposal); + if (invalidProposal) { + this.log.verbose(`Proposal is not valid, skipping attestation`); return undefined; } diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 3bc9084e506..e511df70a0e 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -152,6 +152,7 @@ __metadata: "@aztec/bb-prover": "workspace:^" "@aztec/circuit-types": "workspace:^" "@aztec/circuits.js": "workspace:^" + "@aztec/epoch-cache": "workspace:^" "@aztec/ethereum": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^" @@ -928,6 +929,7 @@ __metadata: "@aztec/archiver": "workspace:^" "@aztec/circuit-types": "workspace:^" "@aztec/circuits.js": "workspace:^" + "@aztec/epoch-cache": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^" "@aztec/telemetry-client": "workspace:^" @@ -1058,6 +1060,7 @@ __metadata: "@aztec/bb-prover": "workspace:^" "@aztec/circuit-types": "workspace:^" "@aztec/circuits.js": "workspace:^" + "@aztec/epoch-cache": "workspace:^" "@aztec/ethereum": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^"