Skip to content

Commit c1a00d2

Browse files
committed
feat: Handle multiple blocks per slot in validators
1 parent 4d20c57 commit c1a00d2

38 files changed

+1330
-397
lines changed

yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,14 @@ import { getAddressFromPrivateKey } from '@aztec/ethereum/account';
99
import { getL1ContractsConfigEnvVars } from '@aztec/ethereum/config';
1010
import { RollupContract } from '@aztec/ethereum/contracts';
1111
import type { DeployAztecL1ContractsReturnType } from '@aztec/ethereum/deploy-aztec-l1-contracts';
12-
import { BlockNumber } from '@aztec/foundation/branded-types';
1312
import { SecretValue } from '@aztec/foundation/config';
1413
import { retryUntil } from '@aztec/foundation/retry';
1514
import { type EthPrivateKey, KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
1615
import { StatefulTestContractArtifact } from '@aztec/noir-test-contracts.js/StatefulTest';
1716
import type { Sequencer, SequencerClient, SequencerPublisherFactory } from '@aztec/sequencer-client';
1817
import type { TestSequencer, TestSequencerClient } from '@aztec/sequencer-client/test';
1918
import type { BlockProposalOptions } from '@aztec/stdlib/p2p';
20-
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
21-
import type { Tx } from '@aztec/stdlib/tx';
19+
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
2220
import { NodeKeystoreAdapter, ValidatorClient } from '@aztec/validator-client';
2321

2422
import { jest } from '@jest/globals';
@@ -361,8 +359,9 @@ describe('e2e_multi_validator_node', () => {
361359

362360
const originalCreateProposal = validatorClient.createBlockProposal.bind(validatorClient);
363361
const createBlockProposal = (
364-
blockNumber: BlockNumber,
365-
header: CheckpointHeader,
362+
blockHeader: BlockHeader,
363+
indexWithinCheckpoint: number,
364+
inHash: Fr,
366365
archive: Fr,
367366
txs: Tx[],
368367
proposerAddress: EthAddress | undefined,
@@ -371,15 +370,15 @@ describe('e2e_multi_validator_node', () => {
371370
if (proposerAddress) {
372371
requestedCoinbaseAddresses.set(
373372
proposerAddress.toString().toLowerCase(),
374-
header.coinbase.toString().toLowerCase(),
373+
blockHeader.globalVariables.coinbase.toString().toLowerCase(),
375374
);
376375
requestedFeeRecipientAddresses.set(
377376
proposerAddress.toString().toLowerCase(),
378-
header.feeRecipient.toString().toLowerCase(),
377+
blockHeader.globalVariables.feeRecipient.toString().toLowerCase(),
379378
);
380379
}
381380

382-
return originalCreateProposal(blockNumber, header, archive, txs, proposerAddress, options);
381+
return originalCreateProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options);
383382
};
384383
validatorClient.createBlockProposal = jest.fn(createBlockProposal);
385384

yarn-project/end-to-end/src/e2e_p2p/reex.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { unfreeze } from '@aztec/foundation/types';
88
import type { LibP2PService, P2PClient } from '@aztec/p2p';
99
import type { BlockBuilder } from '@aztec/sequencer-client';
1010
import type { CppPublicTxSimulator, PublicTxResult } from '@aztec/simulator/server';
11-
import { BlockProposal, SignatureDomainSeparator, getHashedSignaturePayload } from '@aztec/stdlib/p2p';
11+
import { BlockProposal } from '@aztec/stdlib/p2p';
1212
import { ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError } from '@aztec/stdlib/validators';
1313
import type { ValidatorClient, ValidatorKeyStore } from '@aztec/validator-client';
1414

@@ -141,13 +141,14 @@ describe('e2e_p2p_reex', () => {
141141
// We sign over the proposal using the node's signing key
142142
const signer = (node as any).sequencer.sequencer.validatorClient.validationService
143143
.keyStore as ValidatorKeyStore;
144-
const newProposal = new BlockProposal(
145-
proposal.payload,
146-
await signer.signMessageWithAddress(
147-
proposerAddress!,
148-
getHashedSignaturePayload(proposal.payload, SignatureDomainSeparator.blockProposal),
149-
),
144+
const newProposal = await BlockProposal.createProposalFromSigner(
145+
proposal.blockHeader,
146+
proposal.indexWithinCheckpoint,
147+
proposal.inHash,
148+
proposal.archiveRoot,
150149
proposal.txHashes,
150+
undefined,
151+
payload => signer.signMessageWithAddress(proposerAddress!, payload),
151152
);
152153

153154
const p2pService = (p2pClient as any).p2pService as LibP2PService;

yarn-project/p2p/src/client/interface.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { EthAddress, L2BlockId } from '@aztec/stdlib/block';
22
import type { P2PApiFull } from '@aztec/stdlib/interfaces/server';
3-
import type { BlockAttestation, BlockProposal, P2PClientType } from '@aztec/stdlib/p2p';
3+
import type {
4+
BlockAttestation,
5+
BlockProposal,
6+
CheckpointAttestation,
7+
CheckpointProposal,
8+
P2PClientType,
9+
} from '@aztec/stdlib/p2p';
410
import type { Tx, TxHash } from '@aztec/stdlib/tx';
511

612
import type { PeerId } from '@libp2p/interface';
@@ -13,7 +19,7 @@ import type {
1319
ReqRespSubProtocolHandler,
1420
ReqRespSubProtocolValidators,
1521
} from '../services/reqresp/interface.js';
16-
import type { P2PBlockReceivedCallback } from '../services/service.js';
22+
import type { P2PBlockReceivedCallback, P2PCheckpointReceivedCallback } from '../services/service.js';
1723

1824
/**
1925
* Enum defining the possible states of the p2p client.
@@ -50,9 +56,19 @@ export type P2P<T extends P2PClientType = P2PClientType.Full> = P2PApiFull<T> &
5056
*/
5157
broadcastProposal(proposal: BlockProposal): Promise<void>;
5258

59+
/**
60+
* Broadcasts a checkpoint proposal (last block in a checkpoint) to other peers.
61+
*
62+
* @param proposal - the checkpoint proposal
63+
*/
64+
broadcastCheckpointProposal(proposal: CheckpointProposal): Promise<void>;
65+
5366
/** Broadcasts block attestations to other peers. */
5467
broadcastAttestations(attestations: BlockAttestation[]): Promise<void>;
5568

69+
/** Broadcasts checkpoint attestations to other peers. */
70+
broadcastCheckpointAttestations(attestations: CheckpointAttestation[]): Promise<void>;
71+
5672
/**
5773
* Registers a callback from the validator client that determines how to behave when
5874
* foreign block proposals are received
@@ -63,6 +79,14 @@ export type P2P<T extends P2PClientType = P2PClientType.Full> = P2PApiFull<T> &
6379
// ^ This pattern is not my favorite (md)
6480
registerBlockProposalHandler(callback: P2PBlockReceivedCallback): void;
6581

82+
/**
83+
* Registers a callback from the validator client that determines how to behave when
84+
* foreign checkpoint proposals are received
85+
*
86+
* @param handler - A function taking a received checkpoint proposal and producing attestations
87+
*/
88+
registerCheckpointProposalHandler(callback: P2PCheckpointReceivedCallback): void;
89+
6690
/**
6791
* Request a list of transactions from another peer by their tx hashes.
6892
* @param txHashes - Hashes of the txs to query.

yarn-project/p2p/src/client/p2p_client.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ import type {
1515
import type { ContractDataSource } from '@aztec/stdlib/contract';
1616
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
1717
import { type PeerInfo, tryStop } from '@aztec/stdlib/interfaces/server';
18-
import { BlockAttestation, type BlockProposal, type P2PClientType } from '@aztec/stdlib/p2p';
18+
import {
19+
BlockAttestation,
20+
type BlockProposal,
21+
CheckpointAttestation,
22+
type CheckpointProposal,
23+
type P2PClientType,
24+
} from '@aztec/stdlib/p2p';
1925
import type { Tx, TxHash } from '@aztec/stdlib/tx';
2026
import {
2127
Attributes,
@@ -40,7 +46,7 @@ import {
4046
type ReqRespSubProtocolValidators,
4147
} from '../services/reqresp/interface.js';
4248
import { chunkTxHashesRequest } from '../services/reqresp/protocols/tx.js';
43-
import type { P2PBlockReceivedCallback, P2PService } from '../services/service.js';
49+
import type { P2PBlockReceivedCallback, P2PCheckpointReceivedCallback, P2PService } from '../services/service.js';
4450
import { TxCollection } from '../services/tx_collection/tx_collection.js';
4551
import { TxProvider } from '../services/tx_provider.js';
4652
import { type P2P, P2PClientState, type P2PSyncState } from './interface.js';
@@ -114,7 +120,8 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
114120
);
115121

116122
// Default to collecting all txs when we see a valid proposal
117-
// This can be overridden by the validator client to attest, and it will call getTxsForBlockProposal on its own
123+
// This can be overridden by the validator client to validate, and it will call getTxsForBlockProposal on its own
124+
// Note: Validators do NOT attest to individual blocks - attestations are only for checkpoint proposals.
118125
// TODO(palla/txs): We should not trigger a request for txs on a proposal before fully validating it. We need to bring
119126
// validator-client code into here so we can validate a proposal is reasonable.
120127
this.registerBlockProposalHandler(async (block, sender) => {
@@ -123,14 +130,14 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
123130
const constants = this.txCollection.getConstants();
124131
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(block.slotNumber + 1), constants));
125132
const deadline = new Date(nextSlotTimestampSeconds * 1000);
126-
const parentBlock = await this.l2BlockSource.getBlockHeaderByArchive(block.payload.header.lastArchiveRoot);
133+
const parentBlock = await this.l2BlockSource.getBlockHeaderByArchive(block.blockHeader.lastArchive.root);
127134
if (!parentBlock) {
128135
this.log.debug(`Cannot collect txs for proposal as parent block not found`);
129-
return;
136+
return false;
130137
}
131138
const blockNumber = BlockNumber(parentBlock.getBlockNumber() + 1);
132139
await this.txProvider.getTxsForBlockProposal(block, blockNumber, { pinnedPeer: sender, deadline });
133-
return undefined;
140+
return true;
134141
});
135142

136143
// REFACTOR: Try replacing these with an L2TipsStore
@@ -380,11 +387,26 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
380387
return this.p2pService.propagate(proposal);
381388
}
382389

390+
@trackSpan('p2pClient.broadcastCheckpointProposal', async proposal => ({
391+
[Attributes.SLOT_NUMBER]: proposal.slotNumber,
392+
[Attributes.BLOCK_ARCHIVE]: proposal.archive.toString(),
393+
[Attributes.P2P_ID]: (await proposal.p2pMessageLoggingIdentifier()).toString(),
394+
}))
395+
public broadcastCheckpointProposal(proposal: CheckpointProposal): Promise<void> {
396+
this.log.verbose(`Broadcasting checkpoint proposal for slot ${proposal.slotNumber} to peers`);
397+
return this.p2pService.propagate(proposal);
398+
}
399+
383400
public async broadcastAttestations(attestations: BlockAttestation[]): Promise<void> {
384401
this.log.verbose(`Broadcasting ${attestations.length} attestations to peers`);
385402
await Promise.all(attestations.map(att => this.p2pService.propagate(att)));
386403
}
387404

405+
public async broadcastCheckpointAttestations(attestations: CheckpointAttestation[]): Promise<void> {
406+
this.log.verbose(`Broadcasting ${attestations.length} checkpoint attestations to peers`);
407+
await Promise.all(attestations.map(att => this.p2pService.propagate(att)));
408+
}
409+
388410
public async getAttestationsForSlot(slot: SlotNumber, proposalId?: string): Promise<BlockAttestation[]> {
389411
return await (proposalId
390412
? this.attestationPool.getAttestationsForSlotAndProposal(slot, proposalId)
@@ -405,6 +427,10 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
405427
this.p2pService.registerBlockReceivedCallback(handler);
406428
}
407429

430+
public registerCheckpointProposalHandler(handler: P2PCheckpointReceivedCallback): void {
431+
this.p2pService.registerCheckpointReceivedCallback(handler);
432+
}
433+
408434
/**
409435
* Uses the batched Request Response protocol to request a set of transactions from the network.
410436
*/

yarn-project/p2p/src/client/test/p2p_client.integration_block_txs.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ describe('p2p client integration block txs protocol ', () => {
121121
const createBlockProposal = (blockNumber: BlockNumber, blockHash: any, txHashes: any[]) => {
122122
return makeBlockProposal({
123123
signer: Secp256k1Signer.random(),
124-
header: makeL2BlockHeader(1, blockNumber),
125-
archive: blockHash,
124+
blockHeader: makeL2BlockHeader(1, blockNumber),
125+
archiveRoot: blockHash,
126126
txHashes,
127127
});
128128
};

yarn-project/p2p/src/client/test/p2p_client.integration_message_propagation.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,8 @@ describe('p2p client integration message propagation', () => {
217217
expect(hashes[0].toString()).toEqual(hashes[1].toString());
218218
expect(hashes[0].toString()).toEqual(hashes[2].toString());
219219

220-
expect(messages[2].payload.toString()).toEqual(blockProposal.payload.toString());
221-
expect(messages[3].payload.toString()).toEqual(blockProposal.payload.toString());
220+
expect(messages[2].archive.toString()).toEqual(blockProposal.archive.toString());
221+
expect(messages[3].archive.toString()).toEqual(blockProposal.archive.toString());
222222
expect(messages[4].payload.toString()).toEqual(attestation.payload.toString());
223223
expect(messages[5].payload.toString()).toEqual(attestation.payload.toString());
224224
}
@@ -371,7 +371,7 @@ describe('p2p client integration message propagation', () => {
371371

372372
const hashes = await Promise.all([tx, client2Messages![0]].map(tx => tx!.getTxHash()));
373373
expect(hashes[0].toString()).toEqual(hashes[1].toString());
374-
expect(client2Messages![1].payload.toString()).toEqual(blockProposal.payload.toString());
374+
expect(client2Messages![1].archive.toString()).toEqual(blockProposal.archive.toString());
375375
expect(client2Messages![2].payload.toString()).toEqual(attestation.payload.toString());
376376

377377
// We expect that no messages were received by client 3

yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@ import { SlotNumber } from '@aztec/foundation/branded-types';
22
import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer';
33
import { Fr } from '@aztec/foundation/curves/bn254';
44
import type { BlockAttestation, BlockProposal } from '@aztec/stdlib/p2p';
5-
import {
6-
BlockProposal as BlockProposalClass,
7-
ConsensusPayload,
8-
SignatureDomainSeparator,
9-
getHashedSignaturePayloadEthSignedMessage,
10-
} from '@aztec/stdlib/p2p';
11-
import { makeL2BlockHeader } from '@aztec/stdlib/testing';
12-
import { TxHash } from '@aztec/stdlib/tx';
5+
import { makeBlockProposal, makeL2BlockHeader } from '@aztec/stdlib/testing';
136

147
import { jest } from '@jest/globals';
158
import { type MockProxy, mock } from 'jest-mock-extended';
@@ -41,16 +34,17 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
4134
return signers.map(signer => mockAttestation(signer, slotNumber, archive));
4235
};
4336

44-
const mockBlockProposal = (signer: Secp256k1Signer, slotNumber: number, archive: Fr = Fr.random()): BlockProposal => {
37+
const mockBlockProposalForPool = (
38+
signer: Secp256k1Signer,
39+
slotNumber: number,
40+
archive: Fr = Fr.random(),
41+
): BlockProposal => {
4542
const header = makeL2BlockHeader(1, 2, slotNumber);
46-
const payload = new ConsensusPayload(header.toCheckpointHeader(), archive);
47-
48-
const hash = getHashedSignaturePayloadEthSignedMessage(payload, SignatureDomainSeparator.blockProposal);
49-
const signature = signer.sign(hash);
50-
51-
const txHashes = [TxHash.random(), TxHash.random()]; // Mock tx hashes
52-
53-
return new BlockProposalClass(payload, signature, txHashes);
43+
return makeBlockProposal({
44+
signer,
45+
blockHeader: header,
46+
archiveRoot: archive,
47+
});
5448
};
5549

5650
// We compare buffers as the objects can have cached values attached to them which are not serialised
@@ -290,7 +284,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
290284
it('should add and retrieve block proposal', async () => {
291285
const slotNumber = 420;
292286
const archive = Fr.random();
293-
const proposal = mockBlockProposal(signers[0], slotNumber, archive);
287+
const proposal = mockBlockProposalForPool(signers[0], slotNumber, archive);
294288
const proposalId = proposal.archive.toString();
295289

296290
await ap.addBlockProposal(proposal);
@@ -317,13 +311,13 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
317311
it('should update block proposal if added twice with same id', async () => {
318312
const slotNumber = 420;
319313
const archive = Fr.random();
320-
const proposal1 = mockBlockProposal(signers[0], slotNumber, archive);
314+
const proposal1 = mockBlockProposalForPool(signers[0], slotNumber, archive);
321315
const proposalId = proposal1.archive.toString();
322316

323317
await ap.addBlockProposal(proposal1);
324318

325319
// Create a new proposal with same archive but different signer
326-
const proposal2 = mockBlockProposal(signers[1], slotNumber, archive);
320+
const proposal2 = mockBlockProposalForPool(signers[1], slotNumber, archive);
327321

328322
await ap.addBlockProposal(proposal2);
329323

@@ -336,8 +330,8 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
336330

337331
it('should handle block proposals with different slots and same archive', async () => {
338332
const archive = Fr.random();
339-
const proposal1 = mockBlockProposal(signers[0], 100, archive);
340-
const proposal2 = mockBlockProposal(signers[1], 200, archive);
333+
const proposal1 = mockBlockProposalForPool(signers[0], 100, archive);
334+
const proposal2 = mockBlockProposalForPool(signers[1], 200, archive);
341335
const proposalId = archive.toString();
342336

343337
await ap.addBlockProposal(proposal1);
@@ -353,7 +347,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
353347
it('should delete block proposal when deleting attestations for slot and proposal', async () => {
354348
const slotNumber = 420;
355349
const archive = Fr.random();
356-
const proposal = mockBlockProposal(signers[0], slotNumber, archive);
350+
const proposal = mockBlockProposalForPool(signers[0], slotNumber, archive);
357351
const proposalId = proposal.archive.toString();
358352

359353
// Add proposal and some attestations
@@ -378,7 +372,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
378372
it('should delete block proposal when deleting attestations for slot', async () => {
379373
const slotNumber = 420;
380374
const archive = Fr.random();
381-
const proposal = mockBlockProposal(signers[0], slotNumber, archive);
375+
const proposal = mockBlockProposalForPool(signers[0], slotNumber, archive);
382376
const proposalId = proposal.archive.toString();
383377

384378
// Add proposal
@@ -401,7 +395,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
401395
it('should be able to fetch both block proposal and attestations', async () => {
402396
const slotNumber = 420;
403397
const archive = Fr.random();
404-
const proposal = mockBlockProposal(signers[0], slotNumber, archive);
398+
const proposal = mockBlockProposalForPool(signers[0], slotNumber, archive);
405399
const proposalId = proposal.archive.toString();
406400

407401
// Add proposal first

yarn-project/p2p/src/mem_pools/attestation_pool/kv_attestation_pool.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,18 @@ describe('KV Attestation Pool', () => {
2929
const header = makeL2BlockHeader(1, 2, slotNumber);
3030

3131
// Add 1 proposal and re-add it (duplicate) → should not count against cap and not throw
32-
const p0 = makeBlockProposal({ header, archive: Fr.random() });
32+
const p0 = makeBlockProposal({ blockHeader: header, archiveRoot: Fr.random() });
3333
await kvAttestationPool.addBlockProposal(p0);
3434
await kvAttestationPool.addBlockProposal(p0); // idempotent
3535

3636
// Add up to the cap: add (MAX_PROPOSALS_PER_SLOT - 1) more unique proposals
3737
for (let i = 0; i < MAX_PROPOSALS_PER_SLOT - 1; i++) {
38-
const p = makeBlockProposal({ header, archive: Fr.random() });
38+
const p = makeBlockProposal({ blockHeader: header, archiveRoot: Fr.random() });
3939
await kvAttestationPool.addBlockProposal(p);
4040
}
4141

4242
// Adding one more unique proposal for same slot should throw (exceeds cap)
43-
const overflow = makeBlockProposal({ header, archive: Fr.random() });
43+
const overflow = makeBlockProposal({ blockHeader: header, archiveRoot: Fr.random() });
4444
await expect(kvAttestationPool.addBlockProposal(overflow)).rejects.toBeInstanceOf(ProposalSlotCapExceededError);
4545
});
4646
});

yarn-project/p2p/src/mem_pools/attestation_pool/kv_attestation_pool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export class KvAttestationPool implements AttestationPool {
248248
}
249249

250250
public async hasBlockProposal(idOrProposal: string | BlockProposal): Promise<boolean> {
251-
const id = typeof idOrProposal === 'string' ? idOrProposal : idOrProposal.payload.archive.toString();
251+
const id = typeof idOrProposal === 'string' ? idOrProposal : idOrProposal.archive.toString();
252252
return await this.proposals.hasAsync(id);
253253
}
254254

0 commit comments

Comments
 (0)