Skip to content

Commit

Permalink
feat(p2p): DiscV5 Peer Discovery (#5652)
Browse files Browse the repository at this point in the history
  • Loading branch information
spypsy authored Apr 22, 2024
1 parent f77636f commit 0e81642
Show file tree
Hide file tree
Showing 19 changed files with 2,133 additions and 1,689 deletions.
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

0 comments on commit 0e81642

Please sign in to comment.