Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ import { getAddressFromPrivateKey } from '@aztec/ethereum/account';
import { getL1ContractsConfigEnvVars } from '@aztec/ethereum/config';
import { RollupContract } from '@aztec/ethereum/contracts';
import type { DeployAztecL1ContractsReturnType } from '@aztec/ethereum/deploy-aztec-l1-contracts';
import { BlockNumber } from '@aztec/foundation/branded-types';
import { SecretValue } from '@aztec/foundation/config';
import { retryUntil } from '@aztec/foundation/retry';
import { type EthPrivateKey, KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
import { StatefulTestContractArtifact } from '@aztec/noir-test-contracts.js/StatefulTest';
import type { Sequencer, SequencerClient, SequencerPublisherFactory } from '@aztec/sequencer-client';
import type { TestSequencer, TestSequencerClient } from '@aztec/sequencer-client/test';
import type { BlockProposalOptions } from '@aztec/stdlib/p2p';
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
import type { Tx } from '@aztec/stdlib/tx';
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
import { NodeKeystoreAdapter, ValidatorClient } from '@aztec/validator-client';

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

const originalCreateProposal = validatorClient.createBlockProposal.bind(validatorClient);
const createBlockProposal = (
blockNumber: BlockNumber,
header: CheckpointHeader,
blockHeader: BlockHeader,
indexWithinCheckpoint: number,
inHash: Fr,
archive: Fr,
txs: Tx[],
proposerAddress: EthAddress | undefined,
Expand All @@ -371,15 +370,15 @@ describe('e2e_multi_validator_node', () => {
if (proposerAddress) {
requestedCoinbaseAddresses.set(
proposerAddress.toString().toLowerCase(),
header.coinbase.toString().toLowerCase(),
blockHeader.globalVariables.coinbase.toString().toLowerCase(),
);
requestedFeeRecipientAddresses.set(
proposerAddress.toString().toLowerCase(),
header.feeRecipient.toString().toLowerCase(),
blockHeader.globalVariables.feeRecipient.toString().toLowerCase(),
);
}

return originalCreateProposal(blockNumber, header, archive, txs, proposerAddress, options);
return originalCreateProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options);
};
validatorClient.createBlockProposal = jest.fn(createBlockProposal);

Expand Down
15 changes: 8 additions & 7 deletions yarn-project/end-to-end/src/e2e_p2p/reex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { unfreeze } from '@aztec/foundation/types';
import type { LibP2PService, P2PClient } from '@aztec/p2p';
import type { BlockBuilder } from '@aztec/sequencer-client';
import type { CppPublicTxSimulator, PublicTxResult } from '@aztec/simulator/server';
import { BlockProposal, SignatureDomainSeparator, getHashedSignaturePayload } from '@aztec/stdlib/p2p';
import { BlockProposal } from '@aztec/stdlib/p2p';
import { ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError } from '@aztec/stdlib/validators';
import type { ValidatorClient, ValidatorKeyStore } from '@aztec/validator-client';

Expand Down Expand Up @@ -141,13 +141,14 @@ describe('e2e_p2p_reex', () => {
// We sign over the proposal using the node's signing key
const signer = (node as any).sequencer.sequencer.validatorClient.validationService
.keyStore as ValidatorKeyStore;
const newProposal = new BlockProposal(
proposal.payload,
await signer.signMessageWithAddress(
proposerAddress!,
getHashedSignaturePayload(proposal.payload, SignatureDomainSeparator.blockProposal),
),
const newProposal = await BlockProposal.createProposalFromSigner(
proposal.blockHeader,
proposal.indexWithinCheckpoint,
proposal.inHash,
proposal.archiveRoot,
proposal.txHashes,
undefined,
payload => signer.signMessageWithAddress(proposerAddress!, payload),
);

const p2pService = (p2pClient as any).p2pService as LibP2PService;
Expand Down
28 changes: 26 additions & 2 deletions yarn-project/p2p/src/client/interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { EthAddress, L2BlockId } from '@aztec/stdlib/block';
import type { P2PApiFull } from '@aztec/stdlib/interfaces/server';
import type { BlockAttestation, BlockProposal, P2PClientType } from '@aztec/stdlib/p2p';
import type {
BlockAttestation,
BlockProposal,
CheckpointAttestation,
CheckpointProposal,
P2PClientType,
} from '@aztec/stdlib/p2p';
import type { Tx, TxHash } from '@aztec/stdlib/tx';

import type { PeerId } from '@libp2p/interface';
Expand All @@ -13,7 +19,7 @@ import type {
ReqRespSubProtocolHandler,
ReqRespSubProtocolValidators,
} from '../services/reqresp/interface.js';
import type { P2PBlockReceivedCallback } from '../services/service.js';
import type { P2PBlockReceivedCallback, P2PCheckpointReceivedCallback } from '../services/service.js';

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

/**
* Broadcasts a checkpoint proposal (last block in a checkpoint) to other peers.
*
* @param proposal - the checkpoint proposal
*/
broadcastCheckpointProposal(proposal: CheckpointProposal): Promise<void>;

/** Broadcasts block attestations to other peers. */
broadcastAttestations(attestations: BlockAttestation[]): Promise<void>;

/** Broadcasts checkpoint attestations to other peers. */
broadcastCheckpointAttestations(attestations: CheckpointAttestation[]): Promise<void>;

/**
* Registers a callback from the validator client that determines how to behave when
* foreign block proposals are received
Expand All @@ -63,6 +79,14 @@ export type P2P<T extends P2PClientType = P2PClientType.Full> = P2PApiFull<T> &
// ^ This pattern is not my favorite (md)
registerBlockProposalHandler(callback: P2PBlockReceivedCallback): void;

/**
* Registers a callback from the validator client that determines how to behave when
* foreign checkpoint proposals are received
*
* @param handler - A function taking a received checkpoint proposal and producing attestations
*/
registerCheckpointProposalHandler(callback: P2PCheckpointReceivedCallback): void;

/**
* Request a list of transactions from another peer by their tx hashes.
* @param txHashes - Hashes of the txs to query.
Expand Down
38 changes: 32 additions & 6 deletions yarn-project/p2p/src/client/p2p_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ import type {
import type { ContractDataSource } from '@aztec/stdlib/contract';
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
import { type PeerInfo, tryStop } from '@aztec/stdlib/interfaces/server';
import { BlockAttestation, type BlockProposal, type P2PClientType } from '@aztec/stdlib/p2p';
import {
BlockAttestation,
type BlockProposal,
CheckpointAttestation,
type CheckpointProposal,
type P2PClientType,
} from '@aztec/stdlib/p2p';
import type { Tx, TxHash } from '@aztec/stdlib/tx';
import {
Attributes,
Expand All @@ -40,7 +46,7 @@ import {
type ReqRespSubProtocolValidators,
} from '../services/reqresp/interface.js';
import { chunkTxHashesRequest } from '../services/reqresp/protocols/tx.js';
import type { P2PBlockReceivedCallback, P2PService } from '../services/service.js';
import type { P2PBlockReceivedCallback, P2PCheckpointReceivedCallback, P2PService } from '../services/service.js';
import { TxCollection } from '../services/tx_collection/tx_collection.js';
import { TxProvider } from '../services/tx_provider.js';
import { type P2P, P2PClientState, type P2PSyncState } from './interface.js';
Expand Down Expand Up @@ -114,7 +120,8 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
);

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

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

@trackSpan('p2pClient.broadcastCheckpointProposal', async proposal => ({
[Attributes.SLOT_NUMBER]: proposal.slotNumber,
[Attributes.BLOCK_ARCHIVE]: proposal.archive.toString(),
[Attributes.P2P_ID]: (await proposal.p2pMessageLoggingIdentifier()).toString(),
}))
public broadcastCheckpointProposal(proposal: CheckpointProposal): Promise<void> {
this.log.verbose(`Broadcasting checkpoint proposal for slot ${proposal.slotNumber} to peers`);
return this.p2pService.propagate(proposal);
}

public async broadcastAttestations(attestations: BlockAttestation[]): Promise<void> {
this.log.verbose(`Broadcasting ${attestations.length} attestations to peers`);
await Promise.all(attestations.map(att => this.p2pService.propagate(att)));
}

public async broadcastCheckpointAttestations(attestations: CheckpointAttestation[]): Promise<void> {
this.log.verbose(`Broadcasting ${attestations.length} checkpoint attestations to peers`);
await Promise.all(attestations.map(att => this.p2pService.propagate(att)));
}

public async getAttestationsForSlot(slot: SlotNumber, proposalId?: string): Promise<BlockAttestation[]> {
return await (proposalId
? this.attestationPool.getAttestationsForSlotAndProposal(slot, proposalId)
Expand All @@ -405,6 +427,10 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
this.p2pService.registerBlockReceivedCallback(handler);
}

public registerCheckpointProposalHandler(handler: P2PCheckpointReceivedCallback): void {
this.p2pService.registerCheckpointReceivedCallback(handler);
}

/**
* Uses the batched Request Response protocol to request a set of transactions from the network.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ describe('p2p client integration block txs protocol ', () => {
const createBlockProposal = (blockNumber: BlockNumber, blockHash: any, txHashes: any[]) => {
return makeBlockProposal({
signer: Secp256k1Signer.random(),
header: makeL2BlockHeader(1, blockNumber),
archive: blockHash,
blockHeader: makeL2BlockHeader(1, blockNumber),
archiveRoot: blockHash,
txHashes,
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ describe('p2p client integration message propagation', () => {
expect(hashes[0].toString()).toEqual(hashes[1].toString());
expect(hashes[0].toString()).toEqual(hashes[2].toString());

expect(messages[2].payload.toString()).toEqual(blockProposal.payload.toString());
expect(messages[3].payload.toString()).toEqual(blockProposal.payload.toString());
expect(messages[2].archive.toString()).toEqual(blockProposal.archive.toString());
expect(messages[3].archive.toString()).toEqual(blockProposal.archive.toString());
expect(messages[4].payload.toString()).toEqual(attestation.payload.toString());
expect(messages[5].payload.toString()).toEqual(attestation.payload.toString());
}
Expand Down Expand Up @@ -371,7 +371,7 @@ describe('p2p client integration message propagation', () => {

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

// We expect that no messages were received by client 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@ import { SlotNumber } from '@aztec/foundation/branded-types';
import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer';
import { Fr } from '@aztec/foundation/curves/bn254';
import type { BlockAttestation, BlockProposal } from '@aztec/stdlib/p2p';
import {
BlockProposal as BlockProposalClass,
ConsensusPayload,
SignatureDomainSeparator,
getHashedSignaturePayloadEthSignedMessage,
} from '@aztec/stdlib/p2p';
import { makeL2BlockHeader } from '@aztec/stdlib/testing';
import { TxHash } from '@aztec/stdlib/tx';
import { makeBlockProposal, makeL2BlockHeader } from '@aztec/stdlib/testing';

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

const mockBlockProposal = (signer: Secp256k1Signer, slotNumber: number, archive: Fr = Fr.random()): BlockProposal => {
const mockBlockProposalForPool = (
signer: Secp256k1Signer,
slotNumber: number,
archive: Fr = Fr.random(),
): BlockProposal => {
const header = makeL2BlockHeader(1, 2, slotNumber);
const payload = new ConsensusPayload(header.toCheckpointHeader(), archive);

const hash = getHashedSignaturePayloadEthSignedMessage(payload, SignatureDomainSeparator.blockProposal);
const signature = signer.sign(hash);

const txHashes = [TxHash.random(), TxHash.random()]; // Mock tx hashes

return new BlockProposalClass(payload, signature, txHashes);
return makeBlockProposal({
signer,
blockHeader: header,
archiveRoot: archive,
});
};

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

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

await ap.addBlockProposal(proposal1);

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

await ap.addBlockProposal(proposal2);

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

it('should handle block proposals with different slots and same archive', async () => {
const archive = Fr.random();
const proposal1 = mockBlockProposal(signers[0], 100, archive);
const proposal2 = mockBlockProposal(signers[1], 200, archive);
const proposal1 = mockBlockProposalForPool(signers[0], 100, archive);
const proposal2 = mockBlockProposalForPool(signers[1], 200, archive);
const proposalId = archive.toString();

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

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

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

// Add proposal first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ describe('KV Attestation Pool', () => {
const header = makeL2BlockHeader(1, 2, slotNumber);

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

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

// Adding one more unique proposal for same slot should throw (exceeds cap)
const overflow = makeBlockProposal({ header, archive: Fr.random() });
const overflow = makeBlockProposal({ blockHeader: header, archiveRoot: Fr.random() });
await expect(kvAttestationPool.addBlockProposal(overflow)).rejects.toBeInstanceOf(ProposalSlotCapExceededError);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export class KvAttestationPool implements AttestationPool {
}

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

Expand Down
Loading