Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(p2p): DiscV5 Peer Discovery #5652

Merged
merged 19 commits into from
Apr 22, 2024
Merged
8 changes: 7 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"accum",
"acir",
"acvm",
"addrs",
"archiver",
"assignement",
"asyncify",
Expand Down Expand Up @@ -70,12 +71,14 @@
"devs",
"diffie",
"direnv",
"discv5",
"dockerfiles",
"dockerhub",
"dockerized",
"doesnt",
"dont",
"elif",
"enrs",
"entrypoints",
"erc",
"falsey",
Expand Down Expand Up @@ -137,7 +140,10 @@
"mplex",
"msgpack",
"muldiv",
"multiaddr",
"multiaddrs",
"multiarch",
"multiformats",
"multivalue",
"muxers",
"nada",
Expand Down Expand Up @@ -281,4 +287,4 @@
"flagWords": [
"anonymous"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,14 @@ Variables like `DEPLOY_AZTEC_CONTRACTS` & `AZTEC_NODE_PORT` are valid here as de
# Configuration variables for connecting a Node to the Aztec Node P2P network. You'll need a running P2P-Bootstrap node to connect to.
P2P_ENABLED='false' # A flag to enable P2P networking for this node. (default: false)
P2P_BLOCK_CHECK_INTERVAL_MS=100 # The frequency in which to check for new L2 blocks.
P2P_PEER_CHECK_INTERVAL_MS=1000 # The frequency in which to check for peers.
P2P_L2_BLOCK_QUEUE_SIZE=1000 # Size of queue of L2 blocks to store.
P2P_TCP_LISTEN_PORT=40400 # The tcp port on which the P2P service should listen for connections.
P2P_TCP_LISTEN_IP= #The tcp IP on which the P2P service should listen for connections.
PEER_ID_PRIVATE_KEY='' # An optional peer id private key. If blank, will generate a random key.
BOOTSTRAP_NODES='' # A list of bootstrap peers to connect to, separated by commas
P2P_ANNOUNCE_HOSTNAME='' # Hostname to announce to the p2p network
P2P_ANNOUNCE_PORT='' # Port to announce to the p2p network
P2P_KAD_CLIENT='false' # Optional specification to run as a client in the Kademlia routing protocol.
P2P_NAT_ENABLED='false' # Whether to enable NAT from libp2p
P2P_MIN_PEERS=10 # The minimum number of peers (a peer count below this will cause the node to look for more peers)
P2P_MAX_PEERS=100 # The maximum number of peers (a peer count above this will cause the node to refuse connection attempts)
Expand Down
10 changes: 9 additions & 1 deletion yarn-project/aztec-node/terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,15 @@ resource "aws_ecs_task_definition" "aztec-node" {
{
"name": "P2P_MAX_PEERS",
"value": "${var.P2P_MAX_PEERS}"
}
},
{
"name": "P2P_BLOCK_CHECK_INTERVAL_MS",
"value": "1000"
},
{
"name": "P2P_PEER_CHECK_INTERVAL_MS",
"value": "2000"
},
],
"mountPoints": [
{
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec/src/cli/texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ const contractAddresses =
'availabilityOracleAddress:AVAILABILITY_ORACLE_CONTRACT_ADDRESS - string - The deployed L1 availability oracle contract address.\n';
const p2pOptions =
'p2pBlockCheckIntervalMS:P2P_BLOCK_CHECK_INTERVAL_MS - number - The frequency in which to check for blocks. Default: 100\n' +
'p2pPeerCheckIntervalMS:P2P_PEER_CHECK_INTERVAL_MS - number - The frequency in which to check for peers. Default: 1000\n' +
'p2pL2QueueSize:P2P_L2_QUEUE_SIZE - number - Size of queue of L2 blocks to store. Default: 1000\n' +
'tcpListenPort:TCP_LISTEN_PORT - number - The tcp port on which the P2P service should listen for connections. Default: 40400\n' +
'tcpListenIp:TCP_LISTEN_IP - string - The tcp IP on which the P2P service should listen for connections. Default: 0.0.0.0\n' +
'peerIdPrivateKey:PEER_ID_PRIVATE_KEY - string - An optional peer id private key. If blank, will generate a random key.\n' +
'bootstrapNodes:BOOTSTRAP_NODES - string - A list of bootstrap peers to connect to.\n' +
'announceHostname:P2P_ANNOUNCE_HOSTNAME - string - P2P Hostname to announce.\n' +
'announcePort:P2P_ANNOUNCE_PORT - number - P2P Port to announce.\n' +
'clientKADRouting:P2P_KAD_CLIENT - boolean - Optional specification to run as a client in the Kademlia routing protocol. Default: false\n' +
'enableNat:P2P_NAT_ENABLED - boolean - Whether to enable NAT from libp2p (ignored for bootstrap node). Default: false\n' +
'minPeerCount:P2P_MIN_PEERS - number - The minimum number of peers to connect to. Default: 10\n' +
'maxPeerCount:P2P_MAX_PEERS - number - The maximum number of peers to connect to. Default: 100\n';
Expand Down
38 changes: 18 additions & 20 deletions yarn-project/end-to-end/src/flakey_e2e_p2p_network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type SentTx,
TxStatus,
} from '@aztec/aztec.js';
import { BootstrapNode, type P2PConfig, createLibP2PPeerId } from '@aztec/p2p';
import { type BootNodeConfig, BootstrapNode, createLibP2PPeerId } from '@aztec/p2p';
import { type PXEService, createPXEService, getPXEServiceConfig as getRpcConfig } from '@aztec/pxe';

import { mnemonicToAccount } from 'viem/accounts';
Expand Down Expand Up @@ -44,16 +44,17 @@ describe('e2e_p2p_network', () => {
it('should rollup txs from all peers', async () => {
// create the bootstrap node for the network
const bootstrapNode = await createBootstrapNode();
const bootstrapNodeAddress = `/ip4/127.0.0.1/tcp/${BOOT_NODE_TCP_PORT}/p2p/${bootstrapNode
.getPeerId()!
.toString()}`;
const bootstrapNodeEnr = bootstrapNode.getENR();
if (!bootstrapNodeEnr) {
throw new Error('Bootstrap node ENR is not available');
}
// create our network of nodes and submit txs into each of them
// the number of txs per node and the number of txs per rollup
// should be set so that the only way for rollups to be built
// is if the txs are successfully gossiped around the nodes.
const contexts: NodeContext[] = [];
for (let i = 0; i < NUM_NODES; i++) {
const node = await createNode(i + 1 + BOOT_NODE_TCP_PORT, bootstrapNodeAddress, i);
const node = await createNode(i + 1 + BOOT_NODE_TCP_PORT, bootstrapNodeEnr?.encodeTxt(), i);
const context = await createPXEServiceAndSubmitTransactions(node, NUM_TXS_PER_NODE);
contexts.push(context);
}
Expand All @@ -71,23 +72,16 @@ describe('e2e_p2p_network', () => {

const createBootstrapNode = async () => {
const peerId = await createLibP2PPeerId();
const bootstrapNode = new BootstrapNode(logger);
const config: P2PConfig = {
p2pEnabled: true,
tcpListenPort: BOOT_NODE_TCP_PORT,
tcpListenIp: '0.0.0.0',
announceHostname: '/tcp/127.0.0.1',
const bootstrapNode = new BootstrapNode();
const config: BootNodeConfig = {
udpListenPort: BOOT_NODE_TCP_PORT,
udpListenIp: '0.0.0.0',
announceHostname: '/ip4/127.0.0.1',
announcePort: BOOT_NODE_TCP_PORT,
peerIdPrivateKey: Buffer.from(peerId.privateKey!).toString('hex'),
clientKADRouting: false,
minPeerCount: 10,
maxPeerCount: 100,

// TODO: the following config options are not applicable to bootstrap nodes
p2pBlockCheckIntervalMS: 1000,
p2pL2QueueSize: 1,
transactionProtocol: '',
bootstrapNodes: [''],
p2pPeerCheckIntervalMS: 100,
};
await bootstrapNode.start(config);

Expand All @@ -105,13 +99,17 @@ describe('e2e_p2p_network', () => {
const newConfig: AztecNodeConfig = {
...config,
tcpListenPort,
udpListenPort: tcpListenPort,
tcpListenIp: '0.0.0.0',
enableNat: false,
udpListenIp: '0.0.0.0',
announceHostname: '/ip4/127.0.0.1',
bootstrapNodes: [bootstrapNode],
minTxsPerBlock: NUM_TXS_PER_BLOCK,
maxTxsPerBlock: NUM_TXS_PER_BLOCK,
p2pEnabled: true,
clientKADRouting: false,
p2pBlockCheckIntervalMS: 1000,
p2pL2QueueSize: 1,
transactionProtocol: '',
};
return await AztecNodeService.createAndSync(newConfig);
};
Expand Down
22 changes: 13 additions & 9 deletions yarn-project/p2p/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,23 @@
"@aztec/circuits.js": "workspace:^",
"@aztec/foundation": "workspace:^",
"@aztec/kv-store": "workspace:^",
"@chainsafe/libp2p-noise": "^13.0.0",
"@chainsafe/libp2p-yamux": "^5.0.0",
"@chainsafe/discv5": "^9.0.0",
"@chainsafe/enr": "^3.0.0",
"@chainsafe/libp2p-noise": "^15.0.0",
"@chainsafe/libp2p-yamux": "^6.0.2",
"@libp2p/bootstrap": "^9.0.4",
"@libp2p/interface": "^0.1.2",
"@libp2p/crypto": "^4.0.3",
"@libp2p/identify": "^1.0.15",
"@libp2p/interface": "^1.1.4",
"@libp2p/interface-libp2p": "^3.2.0",
"@libp2p/interface-peer-id": "^2.0.2",
"@libp2p/kad-dht": "^10.0.4",
"@libp2p/mplex": "^9.0.4",
"@libp2p/peer-id": "^3.0.2",
"@libp2p/peer-id-factory": "^3.0.3",
"@libp2p/tcp": "^8.0.4",
"@libp2p/mplex": "^10.0.16",
"@libp2p/peer-id": "^4.0.7",
"@libp2p/peer-id-factory": "^4.0.7",
"@libp2p/tcp": "^9.0.16",
"@multiformats/multiaddr": "^12.1.14",
"it-pipe": "^3.0.1",
"libp2p": "^0.46.6",
"libp2p": "^1.2.4",
"sha3": "^2.1.4",
"tslib": "^2.4.0"
},
Expand Down
122 changes: 52 additions & 70 deletions yarn-project/p2p/src/bootstrap/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,71 @@
import { createDebugLogger } from '@aztec/foundation/log';

import { noise } from '@chainsafe/libp2p-noise';
import { yamux } from '@chainsafe/libp2p-yamux';
import type { ServiceMap } from '@libp2p/interface-libp2p';
import { kadDHT } from '@libp2p/kad-dht';
import { mplex } from '@libp2p/mplex';
import { tcp } from '@libp2p/tcp';
import { type Libp2p, type Libp2pOptions, type ServiceFactoryMap, createLibp2p } from 'libp2p';
import { identifyService } from 'libp2p/identify';
import { format } from 'util';
import { Discv5, type Discv5EventEmitter } from '@chainsafe/discv5';
import { SignableENR } from '@chainsafe/enr';
import type { PeerId } from '@libp2p/interface';
import { type Multiaddr, multiaddr } from '@multiformats/multiaddr';

import { type P2PConfig } from '../config.js';
import { createLibP2PPeerId } from '../service/index.js';

/**
* Required P2P config values for a bootstrap node.
*/
export type BootNodeConfig = Partial<P2PConfig> &
Pick<P2PConfig, 'announceHostname' | 'announcePort'> &
Required<Pick<P2PConfig, 'udpListenIp' | 'udpListenPort'>>;

/**
* Encapsulates a 'Bootstrap' node, used for the purpose of assisting new joiners in acquiring peers.
*/
export class BootstrapNode {
private node?: Libp2p = undefined;
private node?: Discv5 = undefined;
private peerId?: PeerId;

constructor(private logger = createDebugLogger('aztec:p2p_bootstrap')) {}

/**
* Starts the bootstrap node.
* @param config - The P2P configuration.
* @param config - A partial P2P configuration. No need for TCP values as well as aztec node specific values.
* @returns An empty promise.
*/
public async start(config: P2PConfig) {
const { peerIdPrivateKey, tcpListenIp, tcpListenPort, announceHostname, announcePort, minPeerCount, maxPeerCount } =
config;
public async start(config: BootNodeConfig) {
const { peerIdPrivateKey, udpListenIp, udpListenPort, announceHostname, announcePort } = config;
const peerId = await createLibP2PPeerId(peerIdPrivateKey);
this.logger.info(
`Starting bootstrap node ${peerId} on ${tcpListenIp}:${tcpListenPort} announced at ${announceHostname}:${
announcePort ?? tcpListenPort
}`,
);
this.peerId = peerId;
const enr = SignableENR.createFromPeerId(peerId);

const opts: Libp2pOptions<ServiceMap> = {
start: false,
peerId,
addresses: {
listen: [`/ip4/${tcpListenIp}/tcp/${tcpListenPort}`],
announce: announceHostname ? [`${announceHostname}/tcp/${announcePort ?? tcpListenPort}`] : [],
},
transports: [tcp()],
streamMuxers: [yamux(), mplex()],
connectionEncryption: [noise()],
connectionManager: {
minConnections: minPeerCount,
maxConnections: maxPeerCount,
},
};
const listenAddrUdp = multiaddr(`/ip4/${udpListenIp}/udp/${udpListenPort}`);
const publicAddr = multiaddr(`${announceHostname}/udp/${announcePort}`);
enr.setLocationMultiaddr(publicAddr);

const services: ServiceFactoryMap = {
identify: identifyService({
protocolPrefix: 'aztec',
}),
kadDHT: kadDHT({
protocolPrefix: 'aztec',
clientMode: false,
}),
// The autonat service seems quite problematic in that using it seems to cause a lot of attempts
// to dial ephemeral ports. I suspect that it works better if you can get the uPNPnat service to
// work as then you would have a permanent port to be dialled.
// Alas, I struggled to get this to work reliably either.
// autoNAT: autoNATService({
// protocolPrefix: 'aztec',
// }),
};
this.logger.info(`Starting bootstrap node ${peerId}, listening on ${listenAddrUdp.toString()}`);

this.node = await createLibp2p({
...opts,
services,
this.node = Discv5.create({
enr,
peerId,
bindAddrs: { ip4: listenAddrUdp },
config: {
lookupTimeout: 2000,
},
});

await this.node.start();
this.logger.debug(`lib p2p has started`);

// print out listening addresses
this.logger.info('Listening on addresses:');
this.node.getMultiaddrs().forEach(addr => {
this.logger.info(addr.toString());
(this.node as Discv5EventEmitter).on('multiaddrUpdated', (addr: Multiaddr) => {
this.logger.info('Advertised socket address updated', { addr: addr.toString() });
});

this.node.addEventListener('peer:discovery', evt => {
this.logger.verbose(format('Discovered %s', evt.detail.id.toString())); // Log discovered peer
(this.node as Discv5EventEmitter).on('discovered', async (enr: SignableENR) => {
const addr = await enr.getFullMultiaddr('udp');
this.logger.verbose(`Discovered new peer, enr: ${enr.encodeTxt()}, addr: ${addr?.toString()}`);
});

this.node.addEventListener('peer:connect', evt => {
this.logger.verbose(format('Connected to %s', evt.detail.toString())); // Log connected peer
});
try {
await this.node.start();
this.logger.info('Discv5 started');
} catch (e) {
this.logger.error('Error starting Discv5', e);
}

this.node.addEventListener('peer:disconnect', evt => {
this.logger.verbose(format('Disconnected from %s', evt.detail.toString())); // Log connected peer
});
this.logger.info(`ENR: ${this.node?.enr.encodeTxt()}`);
}

/**
Expand All @@ -111,6 +83,16 @@ export class BootstrapNode {
* @returns The node's peer Id
*/
public getPeerId() {
return this.node?.peerId;
if (!this.peerId) {
throw new Error('Node not started');
}
return this.peerId;
}

public getENR() {
if (!this.node) {
throw new Error('Node not started');
}
return this.node?.enr.toENR();
}
}
17 changes: 14 additions & 3 deletions yarn-project/p2p/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { type AztecKVStore } from '@aztec/kv-store';

import { P2PClient } from '../client/p2p_client.js';
import { type P2PConfig } from '../config.js';
import { DummyP2PService } from '../service/dummy_service.js';
import { LibP2PService } from '../service/index.js';
import { DiscV5Service } from '../service/discV5_service.js';
import { DummyP2PService, DummyPeerDiscoveryService } from '../service/dummy_service.js';
import { LibP2PService, createLibP2PPeerId } from '../service/index.js';
import { type TxPool } from '../tx_pool/index.js';

export * from './p2p_client.js';
Expand All @@ -15,6 +16,16 @@ export const createP2PClient = async (
txPool: TxPool,
l2BlockSource: L2BlockSource,
) => {
const p2pService = config.p2pEnabled ? await LibP2PService.new(config, txPool) : new DummyP2PService();
let discv5Service;
let p2pService;
if (config.p2pEnabled) {
// Create peer discovery service]
const peerId = await createLibP2PPeerId(config.peerIdPrivateKey);
discv5Service = new DiscV5Service(peerId, config);
p2pService = await LibP2PService.new(config, discv5Service, peerId, txPool, store);
} else {
p2pService = new DummyP2PService();
discv5Service = new DummyPeerDiscoveryService();
}
return new P2PClient(store, l2BlockSource, txPool, p2pService);
};
Loading
Loading