Skip to content

Commit 98cea58

Browse files
authored
feat: Expose P2P service API and clean up logs (#10552)
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
1 parent 79e49c9 commit 98cea58

30 files changed

+466
-153
lines changed

yarn-project/aztec-node/src/aztec-node/server.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ export class AztecNodeService implements AztecNode {
218218
return this.blockSource;
219219
}
220220

221+
public getP2P(): P2P {
222+
return this.p2pClient;
223+
}
224+
221225
/**
222226
* Method to return the currently deployed L1 contract addresses.
223227
* @returns - The currently deployed L1 contract addresses.
@@ -427,11 +431,12 @@ export class AztecNodeService implements AztecNode {
427431
* @returns - The pending txs.
428432
*/
429433
public getPendingTxs() {
430-
return Promise.resolve(this.p2pClient!.getTxs('pending'));
434+
return this.p2pClient!.getPendingTxs();
431435
}
432436

433-
public getPendingTxCount() {
434-
return Promise.resolve(this.p2pClient!.getTxs('pending').length);
437+
public async getPendingTxCount() {
438+
const pendingTxs = await this.getPendingTxs();
439+
return pendingTxs.length;
435440
}
436441

437442
/**

yarn-project/aztec/src/cli/cli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export function injectAztecCommands(program: Command, userLog: LogFn, debugLogge
9595
await startArchiver(options, signalHandlers, services);
9696
} else if (options.p2pBootstrap) {
9797
const { startP2PBootstrap } = await import('./cmds/start_p2p_bootstrap.js');
98-
await startP2PBootstrap(options, userLog, debugLogger);
98+
await startP2PBootstrap(options, signalHandlers, services, userLog);
9999
} else if (options.proverAgent) {
100100
const { startProverAgent } = await import('./cmds/start_prover_agent.js');
101101
await startProverAgent(options, signalHandlers, services, userLog);

yarn-project/aztec/src/cli/cmds/start_node.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { aztecNodeConfigMappings } from '@aztec/aztec-node';
2-
import { AztecNodeApiSchema, type PXE } from '@aztec/circuit-types';
2+
import { AztecNodeApiSchema, P2PApiSchema, type PXE } from '@aztec/circuit-types';
33
import { NULL_KEY } from '@aztec/ethereum';
44
import { type NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server';
55
import { type LogFn } from '@aztec/foundation/log';
@@ -93,8 +93,9 @@ export async function startNode(
9393
// Create and start Aztec Node
9494
const node = await createAztecNode(nodeConfig, telemetryClient);
9595

96-
// Add node to services list
96+
// Add node and p2p to services list
9797
services.node = [node, AztecNodeApiSchema];
98+
services.p2p = [node.getP2P(), P2PApiSchema];
9899

99100
// Add node stop function to signal handlers
100101
signalHandlers.push(node.stop.bind(node));
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1-
import { type Logger } from '@aztec/aztec.js';
2-
import { type LogFn } from '@aztec/foundation/log';
3-
import { type BootnodeConfig, bootnodeConfigMappings } from '@aztec/p2p';
4-
import runBootstrapNode from '@aztec/p2p-bootstrap';
1+
import { P2PBootstrapApiSchema } from '@aztec/circuit-types';
2+
import { type NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server';
3+
import { type LogFn, createLogger } from '@aztec/foundation/log';
4+
import { createStore } from '@aztec/kv-store/lmdb';
5+
import { type BootnodeConfig, BootstrapNode, bootnodeConfigMappings } from '@aztec/p2p';
56
import {
67
createAndStartTelemetryClient,
78
getConfigEnvVars as getTelemetryClientConfig,
89
} from '@aztec/telemetry-client/start';
910

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

12-
export const startP2PBootstrap = async (options: any, userLog: LogFn, debugLogger: Logger) => {
13+
export async function startP2PBootstrap(
14+
options: any,
15+
signalHandlers: (() => Promise<void>)[],
16+
services: NamespacedApiHandlers,
17+
userLog: LogFn,
18+
) {
1319
// Start a P2P bootstrap node.
1420
const config = extractRelevantOptions<BootnodeConfig>(options, bootnodeConfigMappings, 'p2p');
1521
const telemetryClient = await createAndStartTelemetryClient(getTelemetryClientConfig());
16-
17-
await runBootstrapNode(config, telemetryClient, debugLogger);
22+
const store = await createStore('p2p-bootstrap', config, createLogger('p2p:bootstrap:store'));
23+
const node = new BootstrapNode(store, telemetryClient);
24+
await node.start(config);
25+
signalHandlers.push(() => node.stop());
26+
services.bootstrap = [node, P2PBootstrapApiSchema];
1827
userLog(`P2P bootstrap node started on ${config.udpListenAddress}`);
19-
};
28+
}

yarn-project/aztec/src/cli/cmds/start_prover_node.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ProverNodeApiSchema, type ProvingJobBroker, createAztecNodeClient } from '@aztec/circuit-types';
1+
import { P2PApiSchema, ProverNodeApiSchema, type ProvingJobBroker, createAztecNodeClient } from '@aztec/circuit-types';
22
import { NULL_KEY } from '@aztec/ethereum';
33
import { type NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server';
44
import { type LogFn } from '@aztec/foundation/log';
@@ -81,12 +81,16 @@ export async function startProverNode(
8181
const proverNode = await createProverNode(proverConfig, { telemetry, broker });
8282
services.proverNode = [proverNode, ProverNodeApiSchema];
8383

84+
const p2p = proverNode.getP2P();
85+
if (p2p) {
86+
services.p2p = [proverNode.getP2P(), P2PApiSchema];
87+
}
88+
8489
if (!proverConfig.proverBrokerUrl) {
8590
services.provingJobSource = [proverNode.getProver().getProvingJobSource(), ProvingJobConsumerSchema];
8691
}
8792

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

90-
// Automatically start proving unproven blocks
9195
await proverNode.start();
9296
}

yarn-project/circuit-types/src/interfaces/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ export * from './service.js';
2121
export * from './sync-status.js';
2222
export * from './world_state.js';
2323
export * from './prover-broker.js';
24+
export * from './p2p.js';
25+
export * from './p2p-bootstrap.js';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { type ApiSchemaFor } from '@aztec/foundation/schemas';
2+
3+
import { z } from 'zod';
4+
5+
/** Exposed API to the P2P bootstrap node. */
6+
export interface P2PBootstrapApi {
7+
/**
8+
* Returns the ENR for this node.
9+
*/
10+
getEncodedEnr(): Promise<string>;
11+
12+
/**
13+
* Returns ENRs for all nodes in the routing table.
14+
*/
15+
getRoutingTable(): Promise<string[]>;
16+
}
17+
18+
export const P2PBootstrapApiSchema: ApiSchemaFor<P2PBootstrapApi> = {
19+
getEncodedEnr: z.function().returns(z.string()),
20+
getRoutingTable: z.function().returns(z.array(z.string())),
21+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { type JsonRpcTestContext, createJsonRpcTestSetup } from '@aztec/foundation/json-rpc/test';
2+
3+
import { BlockAttestation } from '../p2p/block_attestation.js';
4+
import { EpochProofQuote } from '../prover_coordination/epoch_proof_quote.js';
5+
import { Tx } from '../tx/tx.js';
6+
import { type P2PApi, P2PApiSchema, type PeerInfo } from './p2p.js';
7+
8+
describe('P2PApiSchema', () => {
9+
let handler: MockP2P;
10+
let context: JsonRpcTestContext<P2PApi>;
11+
12+
const tested = new Set<string>();
13+
14+
beforeEach(async () => {
15+
handler = new MockP2P();
16+
context = await createJsonRpcTestSetup<P2PApi>(handler, P2PApiSchema);
17+
});
18+
19+
afterEach(() => {
20+
tested.add(/^P2PApiSchema\s+([^(]+)/.exec(expect.getState().currentTestName!)![1]);
21+
context.httpServer.close();
22+
});
23+
24+
afterAll(() => {
25+
const all = Object.keys(P2PApiSchema);
26+
expect([...tested].sort()).toEqual(all.sort());
27+
});
28+
29+
it('getAttestationsForSlot', async () => {
30+
const attestations = await context.client.getAttestationsForSlot(BigInt(1), 'proposalId');
31+
expect(attestations).toEqual([BlockAttestation.empty()]);
32+
expect(attestations[0]).toBeInstanceOf(BlockAttestation);
33+
});
34+
35+
it('getEpochProofQuotes', async () => {
36+
const quotes = await context.client.getEpochProofQuotes(BigInt(1));
37+
expect(quotes).toEqual([EpochProofQuote.empty()]);
38+
expect(quotes[0]).toBeInstanceOf(EpochProofQuote);
39+
});
40+
41+
it('getPendingTxs', async () => {
42+
const txs = await context.client.getPendingTxs();
43+
expect(txs[0]).toBeInstanceOf(Tx);
44+
});
45+
46+
it('getEncodedEnr', async () => {
47+
const enr = await context.client.getEncodedEnr();
48+
expect(enr).toEqual('enr');
49+
});
50+
51+
it('getPeers', async () => {
52+
const peers = await context.client.getPeers();
53+
expect(peers).toEqual(peers);
54+
});
55+
56+
it('getPeers(true)', async () => {
57+
const peers = await context.client.getPeers(true);
58+
expect(peers).toEqual(peers);
59+
});
60+
});
61+
62+
const peers: PeerInfo[] = [
63+
{ status: 'connected', score: 1, id: 'id' },
64+
{ status: 'dialing', dialStatus: 'dialStatus', id: 'id', addresses: ['address'] },
65+
{ status: 'cached', id: 'id', addresses: ['address'], enr: 'enr', dialAttempts: 1 },
66+
];
67+
68+
class MockP2P implements P2PApi {
69+
getAttestationsForSlot(slot: bigint, proposalId?: string | undefined): Promise<BlockAttestation[]> {
70+
expect(slot).toEqual(1n);
71+
expect(proposalId).toEqual('proposalId');
72+
return Promise.resolve([BlockAttestation.empty()]);
73+
}
74+
getEpochProofQuotes(epoch: bigint): Promise<EpochProofQuote[]> {
75+
expect(epoch).toEqual(1n);
76+
return Promise.resolve([EpochProofQuote.empty()]);
77+
}
78+
getPendingTxs(): Promise<Tx[]> {
79+
return Promise.resolve([Tx.random()]);
80+
}
81+
getEncodedEnr(): Promise<string | undefined> {
82+
return Promise.resolve('enr');
83+
}
84+
getPeers(includePending?: boolean): Promise<PeerInfo[]> {
85+
expect(includePending === undefined || includePending === true).toBeTruthy();
86+
return Promise.resolve(peers);
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { type ApiSchemaFor, optional, schemas } from '@aztec/foundation/schemas';
2+
3+
import { z } from 'zod';
4+
5+
import { BlockAttestation } from '../p2p/block_attestation.js';
6+
import { EpochProofQuote } from '../prover_coordination/epoch_proof_quote.js';
7+
import { Tx } from '../tx/tx.js';
8+
9+
export type PeerInfo =
10+
| { status: 'connected'; score: number; id: string }
11+
| { status: 'dialing'; dialStatus: string; id: string; addresses: string[] }
12+
| { status: 'cached'; id: string; addresses: string[]; enr: string; dialAttempts: number };
13+
14+
const PeerInfoSchema = z.discriminatedUnion('status', [
15+
z.object({ status: z.literal('connected'), score: z.number(), id: z.string() }),
16+
z.object({ status: z.literal('dialing'), dialStatus: z.string(), id: z.string(), addresses: z.array(z.string()) }),
17+
z.object({
18+
status: z.literal('cached'),
19+
id: z.string(),
20+
addresses: z.array(z.string()),
21+
enr: z.string(),
22+
dialAttempts: z.number(),
23+
}),
24+
]);
25+
26+
/** Exposed API to the P2P module. */
27+
export interface P2PApi {
28+
/**
29+
* Queries the Attestation pool for attestations for the given slot
30+
*
31+
* @param slot - the slot to query
32+
* @param proposalId - the proposal id to query, or undefined to query all proposals for the slot
33+
* @returns BlockAttestations
34+
*/
35+
getAttestationsForSlot(slot: bigint, proposalId?: string): Promise<BlockAttestation[]>;
36+
37+
/**
38+
* Queries the EpochProofQuote pool for quotes for the given epoch
39+
*
40+
* @param epoch - the epoch to query
41+
* @returns EpochProofQuotes
42+
*/
43+
getEpochProofQuotes(epoch: bigint): Promise<EpochProofQuote[]>;
44+
45+
/**
46+
* Returns all pending transactions in the transaction pool.
47+
* @returns An array of Txs.
48+
*/
49+
getPendingTxs(): Promise<Tx[]>;
50+
51+
/**
52+
* Returns the ENR for this node, if any.
53+
*/
54+
getEncodedEnr(): Promise<string | undefined>;
55+
56+
/**
57+
* Returns info for all connected, dialing, and cached peers.
58+
*/
59+
getPeers(includePending?: boolean): Promise<PeerInfo[]>;
60+
}
61+
62+
export const P2PApiSchema: ApiSchemaFor<P2PApi> = {
63+
getAttestationsForSlot: z
64+
.function()
65+
.args(schemas.BigInt, optional(z.string()))
66+
.returns(z.array(BlockAttestation.schema)),
67+
getEpochProofQuotes: z.function().args(schemas.BigInt).returns(z.array(EpochProofQuote.schema)),
68+
getPendingTxs: z.function().returns(z.array(Tx.schema)),
69+
getEncodedEnr: z.function().returns(z.string().optional()),
70+
getPeers: z.function().args(optional(z.boolean())).returns(z.array(PeerInfoSchema)),
71+
};

yarn-project/circuit-types/src/p2p/block_attestation.ts

+12
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { keccak256, recoverAddress } from '@aztec/foundation/crypto';
33
import { type EthAddress } from '@aztec/foundation/eth-address';
44
import { Signature } from '@aztec/foundation/eth-signature';
55
import { type Fr } from '@aztec/foundation/fields';
6+
import { type ZodFor } from '@aztec/foundation/schemas';
67
import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize';
78

9+
import { z } from 'zod';
10+
811
import { ConsensusPayload } from './consensus_payload.js';
912
import { Gossipable } from './gossipable.js';
1013
import { SignatureDomainSeperator, getHashedSignaturePayloadEthSignedMessage } from './signature_utils.js';
@@ -37,6 +40,15 @@ export class BlockAttestation extends Gossipable {
3740
super();
3841
}
3942

43+
static get schema(): ZodFor<BlockAttestation> {
44+
return z
45+
.object({
46+
payload: ConsensusPayload.schema,
47+
signature: Signature.schema,
48+
})
49+
.transform(obj => new BlockAttestation(obj.payload, obj.signature));
50+
}
51+
4052
override p2pMessageIdentifier(): Buffer32 {
4153
return new BlockAttestationHash(keccak256(this.signature.toBuffer()));
4254
}

yarn-project/circuit-types/src/p2p/consensus_payload.ts

+11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { hexToBuffer } from '@aztec/foundation/string';
55
import { type FieldsOf } from '@aztec/foundation/types';
66

77
import { encodeAbiParameters, parseAbiParameters } from 'viem';
8+
import { z } from 'zod';
89

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

25+
static get schema() {
26+
return z
27+
.object({
28+
header: BlockHeader.schema,
29+
archive: Fr.schema,
30+
txHashes: z.array(TxHash.schema),
31+
})
32+
.transform(obj => new ConsensusPayload(obj.header, obj.archive, obj.txHashes));
33+
}
34+
2435
static getFields(fields: FieldsOf<ConsensusPayload>) {
2536
return [fields.header, fields.archive, fields.txHashes] as const;
2637
}

yarn-project/foundation/src/collection/array.ts

+10
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,13 @@ export function areArraysEqual<T>(a: T[], b: T[], eq: (a: T, b: T) => boolean =
145145
export function maxBy<T>(arr: T[], fn: (x: T) => number): T | undefined {
146146
return arr.reduce((max, x) => (fn(x) > fn(max) ? x : max), arr[0]);
147147
}
148+
149+
/** Computes the median of a numeric array. Returns undefined if array is empty. */
150+
export function median(arr: number[]) {
151+
if (arr.length === 0) {
152+
return undefined;
153+
}
154+
const sorted = [...arr].sort((a, b) => a - b);
155+
const mid = Math.floor(sorted.length / 2);
156+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
157+
}

yarn-project/foundation/src/config/env_var.ts

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export type EnvVar =
5656
| 'L2_QUEUE_SIZE'
5757
| 'LOG_ELAPSED_TIME'
5858
| 'LOG_JSON'
59+
| 'LOG_MULTILINE'
5960
| 'LOG_LEVEL'
6061
| 'MNEMONIC'
6162
| 'NETWORK_NAME'

yarn-project/foundation/src/log/pino-logger.ts

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export const pinoPrettyOpts = {
7676
customLevels: 'fatal:60,error:50,warn:40,info:30,verbose:25,debug:20,trace:10',
7777
customColors: 'fatal:bgRed,error:red,warn:yellow,info:green,verbose:magenta,debug:blue,trace:gray',
7878
minimumLevel: 'trace' as const,
79+
singleLine: !['1', 'true'].includes(process.env.LOG_MULTILINE ?? ''),
7980
};
8081

8182
const prettyTransport: pino.TransportSingleOptions = {

0 commit comments

Comments
 (0)