Skip to content

Commit

Permalink
feat: Expose P2P service API and clean up logs (#10552)
Browse files Browse the repository at this point in the history
Adds a public API to inspect the p2p service, returning attestations,
epoch quotes, pending txs, and list of peers. Also cleans up logging on
the p2p package (still a bit more work pending to do).

Unrelated: this PR also defaults pretty logging to be single line.

Fixes #10299
  • Loading branch information
spalladino authored Dec 10, 2024
1 parent 79e49c9 commit 98cea58
Show file tree
Hide file tree
Showing 30 changed files with 466 additions and 153 deletions.
11 changes: 8 additions & 3 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ export class AztecNodeService implements AztecNode {
return this.blockSource;
}

public getP2P(): P2P {
return this.p2pClient;
}

/**
* Method to return the currently deployed L1 contract addresses.
* @returns - The currently deployed L1 contract addresses.
Expand Down Expand Up @@ -427,11 +431,12 @@ export class AztecNodeService implements AztecNode {
* @returns - The pending txs.
*/
public getPendingTxs() {
return Promise.resolve(this.p2pClient!.getTxs('pending'));
return this.p2pClient!.getPendingTxs();
}

public getPendingTxCount() {
return Promise.resolve(this.p2pClient!.getTxs('pending').length);
public async getPendingTxCount() {
const pendingTxs = await this.getPendingTxs();
return pendingTxs.length;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function injectAztecCommands(program: Command, userLog: LogFn, debugLogge
await startArchiver(options, signalHandlers, services);
} else if (options.p2pBootstrap) {
const { startP2PBootstrap } = await import('./cmds/start_p2p_bootstrap.js');
await startP2PBootstrap(options, userLog, debugLogger);
await startP2PBootstrap(options, signalHandlers, services, userLog);
} else if (options.proverAgent) {
const { startProverAgent } = await import('./cmds/start_prover_agent.js');
await startProverAgent(options, signalHandlers, services, userLog);
Expand Down
5 changes: 3 additions & 2 deletions yarn-project/aztec/src/cli/cmds/start_node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { aztecNodeConfigMappings } from '@aztec/aztec-node';
import { AztecNodeApiSchema, type PXE } from '@aztec/circuit-types';
import { AztecNodeApiSchema, P2PApiSchema, type PXE } from '@aztec/circuit-types';
import { NULL_KEY } from '@aztec/ethereum';
import { type NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server';
import { type LogFn } from '@aztec/foundation/log';
Expand Down Expand Up @@ -93,8 +93,9 @@ export async function startNode(
// Create and start Aztec Node
const node = await createAztecNode(nodeConfig, telemetryClient);

// Add node to services list
// Add node and p2p to services list
services.node = [node, AztecNodeApiSchema];
services.p2p = [node.getP2P(), P2PApiSchema];

// Add node stop function to signal handlers
signalHandlers.push(node.stop.bind(node));
Expand Down
25 changes: 17 additions & 8 deletions yarn-project/aztec/src/cli/cmds/start_p2p_bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { type Logger } from '@aztec/aztec.js';
import { type LogFn } from '@aztec/foundation/log';
import { type BootnodeConfig, bootnodeConfigMappings } from '@aztec/p2p';
import runBootstrapNode from '@aztec/p2p-bootstrap';
import { P2PBootstrapApiSchema } from '@aztec/circuit-types';
import { type NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server';
import { type LogFn, createLogger } from '@aztec/foundation/log';
import { createStore } from '@aztec/kv-store/lmdb';
import { type BootnodeConfig, BootstrapNode, bootnodeConfigMappings } from '@aztec/p2p';
import {
createAndStartTelemetryClient,
getConfigEnvVars as getTelemetryClientConfig,
} from '@aztec/telemetry-client/start';

import { extractRelevantOptions } from '../util.js';

export const startP2PBootstrap = async (options: any, userLog: LogFn, debugLogger: Logger) => {
export async function startP2PBootstrap(
options: any,
signalHandlers: (() => Promise<void>)[],
services: NamespacedApiHandlers,
userLog: LogFn,
) {
// Start a P2P bootstrap node.
const config = extractRelevantOptions<BootnodeConfig>(options, bootnodeConfigMappings, 'p2p');
const telemetryClient = await createAndStartTelemetryClient(getTelemetryClientConfig());

await runBootstrapNode(config, telemetryClient, debugLogger);
const store = await createStore('p2p-bootstrap', config, createLogger('p2p:bootstrap:store'));
const node = new BootstrapNode(store, telemetryClient);
await node.start(config);
signalHandlers.push(() => node.stop());
services.bootstrap = [node, P2PBootstrapApiSchema];
userLog(`P2P bootstrap node started on ${config.udpListenAddress}`);
};
}
8 changes: 6 additions & 2 deletions yarn-project/aztec/src/cli/cmds/start_prover_node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ProverNodeApiSchema, type ProvingJobBroker, createAztecNodeClient } from '@aztec/circuit-types';
import { P2PApiSchema, ProverNodeApiSchema, type ProvingJobBroker, createAztecNodeClient } from '@aztec/circuit-types';
import { NULL_KEY } from '@aztec/ethereum';
import { type NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server';
import { type LogFn } from '@aztec/foundation/log';
Expand Down Expand Up @@ -81,12 +81,16 @@ export async function startProverNode(
const proverNode = await createProverNode(proverConfig, { telemetry, broker });
services.proverNode = [proverNode, ProverNodeApiSchema];

const p2p = proverNode.getP2P();
if (p2p) {
services.p2p = [proverNode.getP2P(), P2PApiSchema];
}

if (!proverConfig.proverBrokerUrl) {
services.provingJobSource = [proverNode.getProver().getProvingJobSource(), ProvingJobConsumerSchema];
}

signalHandlers.push(proverNode.stop.bind(proverNode));

// Automatically start proving unproven blocks
await proverNode.start();
}
2 changes: 2 additions & 0 deletions yarn-project/circuit-types/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export * from './service.js';
export * from './sync-status.js';
export * from './world_state.js';
export * from './prover-broker.js';
export * from './p2p.js';
export * from './p2p-bootstrap.js';
21 changes: 21 additions & 0 deletions yarn-project/circuit-types/src/interfaces/p2p-bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type ApiSchemaFor } from '@aztec/foundation/schemas';

import { z } from 'zod';

/** Exposed API to the P2P bootstrap node. */
export interface P2PBootstrapApi {
/**
* Returns the ENR for this node.
*/
getEncodedEnr(): Promise<string>;

/**
* Returns ENRs for all nodes in the routing table.
*/
getRoutingTable(): Promise<string[]>;
}

export const P2PBootstrapApiSchema: ApiSchemaFor<P2PBootstrapApi> = {
getEncodedEnr: z.function().returns(z.string()),
getRoutingTable: z.function().returns(z.array(z.string())),
};
88 changes: 88 additions & 0 deletions yarn-project/circuit-types/src/interfaces/p2p.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { type JsonRpcTestContext, createJsonRpcTestSetup } from '@aztec/foundation/json-rpc/test';

import { BlockAttestation } from '../p2p/block_attestation.js';
import { EpochProofQuote } from '../prover_coordination/epoch_proof_quote.js';
import { Tx } from '../tx/tx.js';
import { type P2PApi, P2PApiSchema, type PeerInfo } from './p2p.js';

describe('P2PApiSchema', () => {
let handler: MockP2P;
let context: JsonRpcTestContext<P2PApi>;

const tested = new Set<string>();

beforeEach(async () => {
handler = new MockP2P();
context = await createJsonRpcTestSetup<P2PApi>(handler, P2PApiSchema);
});

afterEach(() => {
tested.add(/^P2PApiSchema\s+([^(]+)/.exec(expect.getState().currentTestName!)![1]);
context.httpServer.close();
});

afterAll(() => {
const all = Object.keys(P2PApiSchema);
expect([...tested].sort()).toEqual(all.sort());
});

it('getAttestationsForSlot', async () => {
const attestations = await context.client.getAttestationsForSlot(BigInt(1), 'proposalId');
expect(attestations).toEqual([BlockAttestation.empty()]);
expect(attestations[0]).toBeInstanceOf(BlockAttestation);
});

it('getEpochProofQuotes', async () => {
const quotes = await context.client.getEpochProofQuotes(BigInt(1));
expect(quotes).toEqual([EpochProofQuote.empty()]);
expect(quotes[0]).toBeInstanceOf(EpochProofQuote);
});

it('getPendingTxs', async () => {
const txs = await context.client.getPendingTxs();
expect(txs[0]).toBeInstanceOf(Tx);
});

it('getEncodedEnr', async () => {
const enr = await context.client.getEncodedEnr();
expect(enr).toEqual('enr');
});

it('getPeers', async () => {
const peers = await context.client.getPeers();
expect(peers).toEqual(peers);
});

it('getPeers(true)', async () => {
const peers = await context.client.getPeers(true);
expect(peers).toEqual(peers);
});
});

const peers: PeerInfo[] = [
{ status: 'connected', score: 1, id: 'id' },
{ status: 'dialing', dialStatus: 'dialStatus', id: 'id', addresses: ['address'] },
{ status: 'cached', id: 'id', addresses: ['address'], enr: 'enr', dialAttempts: 1 },
];

class MockP2P implements P2PApi {
getAttestationsForSlot(slot: bigint, proposalId?: string | undefined): Promise<BlockAttestation[]> {
expect(slot).toEqual(1n);
expect(proposalId).toEqual('proposalId');
return Promise.resolve([BlockAttestation.empty()]);
}
getEpochProofQuotes(epoch: bigint): Promise<EpochProofQuote[]> {
expect(epoch).toEqual(1n);
return Promise.resolve([EpochProofQuote.empty()]);
}
getPendingTxs(): Promise<Tx[]> {
return Promise.resolve([Tx.random()]);
}
getEncodedEnr(): Promise<string | undefined> {
return Promise.resolve('enr');
}
getPeers(includePending?: boolean): Promise<PeerInfo[]> {
expect(includePending === undefined || includePending === true).toBeTruthy();
return Promise.resolve(peers);
}
}
71 changes: 71 additions & 0 deletions yarn-project/circuit-types/src/interfaces/p2p.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { type ApiSchemaFor, optional, schemas } from '@aztec/foundation/schemas';

import { z } from 'zod';

import { BlockAttestation } from '../p2p/block_attestation.js';
import { EpochProofQuote } from '../prover_coordination/epoch_proof_quote.js';
import { Tx } from '../tx/tx.js';

export type PeerInfo =
| { status: 'connected'; score: number; id: string }
| { status: 'dialing'; dialStatus: string; id: string; addresses: string[] }
| { status: 'cached'; id: string; addresses: string[]; enr: string; dialAttempts: number };

const PeerInfoSchema = z.discriminatedUnion('status', [
z.object({ status: z.literal('connected'), score: z.number(), id: z.string() }),
z.object({ status: z.literal('dialing'), dialStatus: z.string(), id: z.string(), addresses: z.array(z.string()) }),
z.object({
status: z.literal('cached'),
id: z.string(),
addresses: z.array(z.string()),
enr: z.string(),
dialAttempts: z.number(),
}),
]);

/** Exposed API to the P2P module. */
export interface P2PApi {
/**
* Queries the Attestation pool for attestations for the given slot
*
* @param slot - the slot to query
* @param proposalId - the proposal id to query, or undefined to query all proposals for the slot
* @returns BlockAttestations
*/
getAttestationsForSlot(slot: bigint, proposalId?: string): Promise<BlockAttestation[]>;

/**
* Queries the EpochProofQuote pool for quotes for the given epoch
*
* @param epoch - the epoch to query
* @returns EpochProofQuotes
*/
getEpochProofQuotes(epoch: bigint): Promise<EpochProofQuote[]>;

/**
* Returns all pending transactions in the transaction pool.
* @returns An array of Txs.
*/
getPendingTxs(): Promise<Tx[]>;

/**
* Returns the ENR for this node, if any.
*/
getEncodedEnr(): Promise<string | undefined>;

/**
* Returns info for all connected, dialing, and cached peers.
*/
getPeers(includePending?: boolean): Promise<PeerInfo[]>;
}

export const P2PApiSchema: ApiSchemaFor<P2PApi> = {
getAttestationsForSlot: z
.function()
.args(schemas.BigInt, optional(z.string()))
.returns(z.array(BlockAttestation.schema)),
getEpochProofQuotes: z.function().args(schemas.BigInt).returns(z.array(EpochProofQuote.schema)),
getPendingTxs: z.function().returns(z.array(Tx.schema)),
getEncodedEnr: z.function().returns(z.string().optional()),
getPeers: z.function().args(optional(z.boolean())).returns(z.array(PeerInfoSchema)),
};
12 changes: 12 additions & 0 deletions yarn-project/circuit-types/src/p2p/block_attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { keccak256, recoverAddress } from '@aztec/foundation/crypto';
import { type EthAddress } from '@aztec/foundation/eth-address';
import { Signature } from '@aztec/foundation/eth-signature';
import { type Fr } from '@aztec/foundation/fields';
import { type ZodFor } from '@aztec/foundation/schemas';
import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize';

import { z } from 'zod';

import { ConsensusPayload } from './consensus_payload.js';
import { Gossipable } from './gossipable.js';
import { SignatureDomainSeperator, getHashedSignaturePayloadEthSignedMessage } from './signature_utils.js';
Expand Down Expand Up @@ -37,6 +40,15 @@ export class BlockAttestation extends Gossipable {
super();
}

static get schema(): ZodFor<BlockAttestation> {
return z
.object({
payload: ConsensusPayload.schema,
signature: Signature.schema,
})
.transform(obj => new BlockAttestation(obj.payload, obj.signature));
}

override p2pMessageIdentifier(): Buffer32 {
return new BlockAttestationHash(keccak256(this.signature.toBuffer()));
}
Expand Down
11 changes: 11 additions & 0 deletions yarn-project/circuit-types/src/p2p/consensus_payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { hexToBuffer } from '@aztec/foundation/string';
import { type FieldsOf } from '@aztec/foundation/types';

import { encodeAbiParameters, parseAbiParameters } from 'viem';
import { z } from 'zod';

import { TxHash } from '../tx/tx_hash.js';
import { type Signable, type SignatureDomainSeperator } from './signature_utils.js';
Expand All @@ -21,6 +22,16 @@ export class ConsensusPayload implements Signable {
public readonly txHashes: TxHash[],
) {}

static get schema() {
return z
.object({
header: BlockHeader.schema,
archive: Fr.schema,
txHashes: z.array(TxHash.schema),
})
.transform(obj => new ConsensusPayload(obj.header, obj.archive, obj.txHashes));
}

static getFields(fields: FieldsOf<ConsensusPayload>) {
return [fields.header, fields.archive, fields.txHashes] as const;
}
Expand Down
10 changes: 10 additions & 0 deletions yarn-project/foundation/src/collection/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,13 @@ export function areArraysEqual<T>(a: T[], b: T[], eq: (a: T, b: T) => boolean =
export function maxBy<T>(arr: T[], fn: (x: T) => number): T | undefined {
return arr.reduce((max, x) => (fn(x) > fn(max) ? x : max), arr[0]);
}

/** Computes the median of a numeric array. Returns undefined if array is empty. */
export function median(arr: number[]) {
if (arr.length === 0) {
return undefined;
}
const sorted = [...arr].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}
1 change: 1 addition & 0 deletions yarn-project/foundation/src/config/env_var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type EnvVar =
| 'L2_QUEUE_SIZE'
| 'LOG_ELAPSED_TIME'
| 'LOG_JSON'
| 'LOG_MULTILINE'
| 'LOG_LEVEL'
| 'MNEMONIC'
| 'NETWORK_NAME'
Expand Down
1 change: 1 addition & 0 deletions yarn-project/foundation/src/log/pino-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const pinoPrettyOpts = {
customLevels: 'fatal:60,error:50,warn:40,info:30,verbose:25,debug:20,trace:10',
customColors: 'fatal:bgRed,error:red,warn:yellow,info:green,verbose:magenta,debug:blue,trace:gray',
minimumLevel: 'trace' as const,
singleLine: !['1', 'true'].includes(process.env.LOG_MULTILINE ?? ''),
};

const prettyTransport: pino.TransportSingleOptions = {
Expand Down
Loading

0 comments on commit 98cea58

Please sign in to comment.