@aztec/p2p 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/service/tx_messages.d.ts.map +1 -1
- package/dest/service/tx_messages.js +3 -3
- package/package.json +5 -5
- package/src/bootstrap/bootstrap.ts +116 -0
- package/src/client/index.ts +20 -0
- package/src/client/mocks.ts +87 -0
- package/src/client/p2p_client.ts +334 -0
- package/src/config.ts +113 -0
- package/src/index.ts +5 -0
- package/src/service/dummy_service.ts +36 -0
- package/src/service/index.ts +2 -0
- package/src/service/known_txs.ts +56 -0
- package/src/service/libp2p_service.ts +397 -0
- package/src/service/service.ts +30 -0
- package/src/service/tx_messages.ts +216 -0
- package/src/tx_pool/aztec_kv_tx_pool.ts +99 -0
- package/src/tx_pool/index.ts +3 -0
- package/src/tx_pool/memory_tx_pool.ts +86 -0
- package/src/tx_pool/tx_pool.ts +44 -0
- package/src/tx_pool/tx_pool_test_suite.ts +59 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2P client configuration values.
|
|
3
|
+
*/
|
|
4
|
+
export interface P2PConfig {
|
|
5
|
+
/**
|
|
6
|
+
* A flag dictating whether the P2P subsystem should be enabled.
|
|
7
|
+
*/
|
|
8
|
+
p2pEnabled: boolean;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The frequency in which to check.
|
|
12
|
+
*/
|
|
13
|
+
p2pBlockCheckIntervalMS: number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Size of queue of L2 blocks to store.
|
|
17
|
+
*/
|
|
18
|
+
p2pL2QueueSize: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The tcp port on which the P2P service should listen for connections.
|
|
22
|
+
*/
|
|
23
|
+
tcpListenPort: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The tcp IP on which the P2P service should listen for connections.
|
|
27
|
+
*/
|
|
28
|
+
tcpListenIp: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An optional peer id private key. If blank, will generate a random key.
|
|
32
|
+
*/
|
|
33
|
+
peerIdPrivateKey?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A list of bootstrap peers to connect to.
|
|
37
|
+
*/
|
|
38
|
+
bootstrapNodes: string[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Protocol identifier for transaction gossiping.
|
|
42
|
+
*/
|
|
43
|
+
transactionProtocol: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hostname to announce.
|
|
47
|
+
*/
|
|
48
|
+
announceHostname?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Port to announce.
|
|
52
|
+
*/
|
|
53
|
+
announcePort?: number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional specification to run as a client in the Kademlia routing protocol.
|
|
57
|
+
*/
|
|
58
|
+
clientKADRouting: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether to enable NAT from libp2p (ignored for bootstrap node).
|
|
62
|
+
*/
|
|
63
|
+
enableNat?: boolean;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The minimum number of peers (a peer count below this will cause the node to look for more peers)
|
|
67
|
+
*/
|
|
68
|
+
minPeerCount: number;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The maximum number of peers (a peer count above this will cause the node to refuse connection attempts)
|
|
72
|
+
*/
|
|
73
|
+
maxPeerCount: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets the config values for p2p client from environment variables.
|
|
78
|
+
* @returns The config values for p2p client.
|
|
79
|
+
*/
|
|
80
|
+
export function getP2PConfigEnvVars(): P2PConfig {
|
|
81
|
+
const {
|
|
82
|
+
P2P_ENABLED,
|
|
83
|
+
P2P_BLOCK_CHECK_INTERVAL_MS,
|
|
84
|
+
P2P_L2_BLOCK_QUEUE_SIZE,
|
|
85
|
+
P2P_TCP_LISTEN_PORT,
|
|
86
|
+
P2P_TCP_LISTEN_IP,
|
|
87
|
+
PEER_ID_PRIVATE_KEY,
|
|
88
|
+
BOOTSTRAP_NODES,
|
|
89
|
+
P2P_ANNOUNCE_HOSTNAME,
|
|
90
|
+
P2P_ANNOUNCE_PORT,
|
|
91
|
+
P2P_KAD_CLIENT,
|
|
92
|
+
P2P_NAT_ENABLED,
|
|
93
|
+
P2P_MIN_PEERS,
|
|
94
|
+
P2P_MAX_PEERS,
|
|
95
|
+
} = process.env;
|
|
96
|
+
const envVars: P2PConfig = {
|
|
97
|
+
p2pEnabled: P2P_ENABLED === 'true',
|
|
98
|
+
p2pBlockCheckIntervalMS: P2P_BLOCK_CHECK_INTERVAL_MS ? +P2P_BLOCK_CHECK_INTERVAL_MS : 100,
|
|
99
|
+
p2pL2QueueSize: P2P_L2_BLOCK_QUEUE_SIZE ? +P2P_L2_BLOCK_QUEUE_SIZE : 1000,
|
|
100
|
+
tcpListenPort: P2P_TCP_LISTEN_PORT ? +P2P_TCP_LISTEN_PORT : 40400,
|
|
101
|
+
tcpListenIp: P2P_TCP_LISTEN_IP ? P2P_TCP_LISTEN_IP : '0.0.0.0',
|
|
102
|
+
peerIdPrivateKey: PEER_ID_PRIVATE_KEY,
|
|
103
|
+
bootstrapNodes: BOOTSTRAP_NODES ? BOOTSTRAP_NODES.split(',') : [],
|
|
104
|
+
transactionProtocol: '',
|
|
105
|
+
announceHostname: P2P_ANNOUNCE_HOSTNAME,
|
|
106
|
+
announcePort: P2P_ANNOUNCE_PORT ? +P2P_ANNOUNCE_PORT : undefined,
|
|
107
|
+
clientKADRouting: P2P_KAD_CLIENT === 'true',
|
|
108
|
+
enableNat: P2P_NAT_ENABLED === 'true',
|
|
109
|
+
minPeerCount: P2P_MIN_PEERS ? +P2P_MIN_PEERS : 10,
|
|
110
|
+
maxPeerCount: P2P_MAX_PEERS ? +P2P_MAX_PEERS : 100,
|
|
111
|
+
};
|
|
112
|
+
return envVars;
|
|
113
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Tx, TxHash } from '@aztec/circuit-types';
|
|
2
|
+
|
|
3
|
+
import { P2PService } from './service.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A dummy implementation of the P2P Service.
|
|
7
|
+
*/
|
|
8
|
+
export class DummyP2PService implements P2PService {
|
|
9
|
+
/**
|
|
10
|
+
* Starts the dummy implementation.
|
|
11
|
+
* @returns A resolved promise.
|
|
12
|
+
*/
|
|
13
|
+
public start() {
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stops the dummy implementation.
|
|
19
|
+
* @returns A resolved promise.
|
|
20
|
+
*/
|
|
21
|
+
public stop() {
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Called to have the given transaction propagated through the P2P network.
|
|
27
|
+
* @param _ - The transaction to be propagated.
|
|
28
|
+
*/
|
|
29
|
+
public propagateTx(_: Tx) {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Called upon receipt of settled transactions.
|
|
33
|
+
* @param _ - The hashes of the settled transactions.
|
|
34
|
+
*/
|
|
35
|
+
public settledTxs(_: TxHash[]) {}
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PeerId } from '@libp2p/interface-peer-id';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps a record of which Peers have 'seen' which transactions.
|
|
5
|
+
*/
|
|
6
|
+
export class KnownTxLookup {
|
|
7
|
+
private lookup: { [key: string]: { [key: string]: boolean } } = {};
|
|
8
|
+
|
|
9
|
+
constructor() {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Inform this lookup that a peer has 'seen' a transaction.
|
|
13
|
+
* @param peerId - The peerId of the peer that has 'seen' the transaction.
|
|
14
|
+
* @param txHash - The thHash of the 'seen' transaction.
|
|
15
|
+
*/
|
|
16
|
+
public addPeerForTx(peerId: PeerId, txHash: string) {
|
|
17
|
+
const peerIdAsString = peerId.toString();
|
|
18
|
+
const existingLookup = this.lookup[txHash];
|
|
19
|
+
if (existingLookup === undefined) {
|
|
20
|
+
const newLookup: { [key: string]: boolean } = {};
|
|
21
|
+
newLookup[peerIdAsString] = true;
|
|
22
|
+
this.lookup[txHash] = newLookup;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
existingLookup[peerIdAsString] = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Determine if a peer has 'seen' a transaction.
|
|
30
|
+
* @param peerId - The peerId of the peer.
|
|
31
|
+
* @param txHash - The thHash of the transaction.
|
|
32
|
+
* @returns A boolean indicating if the transaction has been 'seen' by the peer.
|
|
33
|
+
*/
|
|
34
|
+
public hasPeerSeenTx(peerId: PeerId, txHash: string) {
|
|
35
|
+
const existingLookup = this.lookup[txHash];
|
|
36
|
+
if (existingLookup === undefined) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const peerIdAsString = peerId.toString();
|
|
40
|
+
return !!existingLookup[peerIdAsString];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Updates the lookup from the result of settled txs
|
|
45
|
+
* These txs will be cleared out of the lookup.
|
|
46
|
+
* It is possible that some txs could still be gossiped for a
|
|
47
|
+
* short period of time meaning they come back into this lookup
|
|
48
|
+
* but this should be infrequent and cause no undesirable effects
|
|
49
|
+
* @param txHashes - The hashes of the newly settled transactions
|
|
50
|
+
*/
|
|
51
|
+
public handleSettledTxs(txHashes: string[]) {
|
|
52
|
+
for (const txHash of txHashes) {
|
|
53
|
+
delete this.lookup[txHash];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { Tx, TxHash } from '@aztec/circuit-types';
|
|
2
|
+
import { SerialQueue } from '@aztec/foundation/fifo';
|
|
3
|
+
import { createDebugLogger } from '@aztec/foundation/log';
|
|
4
|
+
|
|
5
|
+
import { noise } from '@chainsafe/libp2p-noise';
|
|
6
|
+
import { yamux } from '@chainsafe/libp2p-yamux';
|
|
7
|
+
import { bootstrap } from '@libp2p/bootstrap';
|
|
8
|
+
import type { ServiceMap } from '@libp2p/interface-libp2p';
|
|
9
|
+
import { PeerId } from '@libp2p/interface-peer-id';
|
|
10
|
+
import { IncomingStreamData } from '@libp2p/interface/stream-handler';
|
|
11
|
+
import { DualKadDHT, kadDHT } from '@libp2p/kad-dht';
|
|
12
|
+
import { mplex } from '@libp2p/mplex';
|
|
13
|
+
import { createFromJSON, createSecp256k1PeerId, exportToProtobuf } from '@libp2p/peer-id-factory';
|
|
14
|
+
import { tcp } from '@libp2p/tcp';
|
|
15
|
+
import { pipe } from 'it-pipe';
|
|
16
|
+
import { Libp2p, Libp2pOptions, ServiceFactoryMap, createLibp2p } from 'libp2p';
|
|
17
|
+
import { identifyService } from 'libp2p/identify';
|
|
18
|
+
|
|
19
|
+
import { P2PConfig } from '../config.js';
|
|
20
|
+
import { TxPool } from '../tx_pool/index.js';
|
|
21
|
+
import { KnownTxLookup } from './known_txs.js';
|
|
22
|
+
import { P2PService } from './service.js';
|
|
23
|
+
import {
|
|
24
|
+
Messages,
|
|
25
|
+
createGetTransactionsRequestMessage,
|
|
26
|
+
createTransactionHashesMessage,
|
|
27
|
+
createTransactionsMessage,
|
|
28
|
+
decodeGetTransactionsRequestMessage,
|
|
29
|
+
decodeTransactionHashesMessage,
|
|
30
|
+
decodeTransactionsMessage,
|
|
31
|
+
getEncodedMessage,
|
|
32
|
+
} from './tx_messages.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a libp2p peer ID from the private key if provided, otherwise creates a new random ID.
|
|
36
|
+
* @param privateKey - Optional peer ID private key as hex string
|
|
37
|
+
* @returns The peer ID.
|
|
38
|
+
*/
|
|
39
|
+
export async function createLibP2PPeerId(privateKey?: string) {
|
|
40
|
+
if (!privateKey?.length) {
|
|
41
|
+
return await createSecp256k1PeerId();
|
|
42
|
+
}
|
|
43
|
+
const base64 = Buffer.from(privateKey, 'hex').toString('base64');
|
|
44
|
+
return await createFromJSON({
|
|
45
|
+
id: '',
|
|
46
|
+
privKey: base64,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Exports a given peer id to a string representation.
|
|
52
|
+
* @param peerId - The peerId instance to be converted.
|
|
53
|
+
* @returns The peer id as a string.
|
|
54
|
+
*/
|
|
55
|
+
export function exportLibP2PPeerIdToString(peerId: PeerId) {
|
|
56
|
+
return Buffer.from(exportToProtobuf(peerId)).toString('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lib P2P implementation of the P2PService interface.
|
|
61
|
+
*/
|
|
62
|
+
export class LibP2PService implements P2PService {
|
|
63
|
+
private jobQueue: SerialQueue = new SerialQueue();
|
|
64
|
+
private knownTxLookup: KnownTxLookup = new KnownTxLookup();
|
|
65
|
+
constructor(
|
|
66
|
+
private config: P2PConfig,
|
|
67
|
+
private node: Libp2p,
|
|
68
|
+
private protocolId: string,
|
|
69
|
+
private txPool: TxPool,
|
|
70
|
+
private logger = createDebugLogger('aztec:libp2p_service'),
|
|
71
|
+
) {}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Starts the LibP2P service.
|
|
75
|
+
* @returns An empty promise.
|
|
76
|
+
*/
|
|
77
|
+
public async start() {
|
|
78
|
+
if (this.node.isStarted()) {
|
|
79
|
+
throw new Error('P2P service already started');
|
|
80
|
+
}
|
|
81
|
+
const { enableNat, tcpListenIp, tcpListenPort, announceHostname, announcePort } = this.config;
|
|
82
|
+
this.logger(`Starting P2P node on ${tcpListenIp}:${tcpListenPort}`);
|
|
83
|
+
if (announceHostname) {
|
|
84
|
+
this.logger(`Announcing at ${announceHostname}/tcp/${announcePort ?? tcpListenPort}`);
|
|
85
|
+
}
|
|
86
|
+
if (enableNat) {
|
|
87
|
+
this.logger(`Enabling NAT in libp2p module`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.node.addEventListener('peer:discovery', evt => {
|
|
91
|
+
const peerId = evt.detail.id;
|
|
92
|
+
if (this.isBootstrapPeer(peerId)) {
|
|
93
|
+
this.logger(`Discovered bootstrap peer ${peerId.toString()}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.node.addEventListener('peer:connect', evt => {
|
|
98
|
+
const peerId = evt.detail;
|
|
99
|
+
this.handleNewConnection(peerId);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.node.addEventListener('peer:disconnect', evt => {
|
|
103
|
+
const peerId = evt.detail;
|
|
104
|
+
if (this.isBootstrapPeer(peerId)) {
|
|
105
|
+
this.logger(`Disconnect from bootstrap peer ${peerId.toString()}`);
|
|
106
|
+
} else {
|
|
107
|
+
this.logger(`Disconnected from transaction peer ${peerId.toString()}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.jobQueue.start();
|
|
112
|
+
await this.node.start();
|
|
113
|
+
await this.node.handle(this.protocolId, (incoming: IncomingStreamData) =>
|
|
114
|
+
this.jobQueue.put(() => Promise.resolve(this.handleProtocolDial(incoming))),
|
|
115
|
+
);
|
|
116
|
+
const dht = this.node.services['kadDHT'] as DualKadDHT;
|
|
117
|
+
this.logger(`Started P2P client as ${await dht.getMode()} with Peer ID ${this.node.peerId.toString()}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Stops the LibP2P service.
|
|
122
|
+
* @returns An empty promise.
|
|
123
|
+
*/
|
|
124
|
+
public async stop() {
|
|
125
|
+
this.logger('Stopping job queue...');
|
|
126
|
+
await this.jobQueue.end();
|
|
127
|
+
this.logger('Stopping LibP2P...');
|
|
128
|
+
await this.node.stop();
|
|
129
|
+
this.logger('LibP2P service stopped');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Creates an instance of the LibP2P service.
|
|
134
|
+
* @param config - The configuration to use when creating the service.
|
|
135
|
+
* @param txPool - The transaction pool to be accessed by the service.
|
|
136
|
+
* @returns The new service.
|
|
137
|
+
*/
|
|
138
|
+
public static async new(config: P2PConfig, txPool: TxPool) {
|
|
139
|
+
const {
|
|
140
|
+
tcpListenIp,
|
|
141
|
+
tcpListenPort,
|
|
142
|
+
announceHostname,
|
|
143
|
+
announcePort,
|
|
144
|
+
clientKADRouting,
|
|
145
|
+
minPeerCount,
|
|
146
|
+
maxPeerCount,
|
|
147
|
+
peerIdPrivateKey,
|
|
148
|
+
} = config;
|
|
149
|
+
const peerId = await createLibP2PPeerId(peerIdPrivateKey);
|
|
150
|
+
|
|
151
|
+
const opts: Libp2pOptions<ServiceMap> = {
|
|
152
|
+
start: false,
|
|
153
|
+
peerId,
|
|
154
|
+
addresses: {
|
|
155
|
+
listen: [`/ip4/${tcpListenIp}/tcp/${tcpListenPort}`],
|
|
156
|
+
announce: announceHostname ? [`${announceHostname}/tcp/${announcePort ?? tcpListenPort}`] : [],
|
|
157
|
+
},
|
|
158
|
+
transports: [tcp()],
|
|
159
|
+
streamMuxers: [yamux(), mplex()],
|
|
160
|
+
connectionEncryption: [noise()],
|
|
161
|
+
connectionManager: {
|
|
162
|
+
minConnections: minPeerCount,
|
|
163
|
+
maxConnections: maxPeerCount,
|
|
164
|
+
},
|
|
165
|
+
peerDiscovery: [
|
|
166
|
+
bootstrap({
|
|
167
|
+
list: config.bootstrapNodes,
|
|
168
|
+
}),
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const services: ServiceFactoryMap = {
|
|
173
|
+
identify: identifyService({
|
|
174
|
+
protocolPrefix: 'aztec',
|
|
175
|
+
}),
|
|
176
|
+
kadDHT: kadDHT({
|
|
177
|
+
protocolPrefix: 'aztec',
|
|
178
|
+
clientMode: clientKADRouting,
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// The autonat service seems quite problematic in that using it seems to cause a lot of attempts
|
|
183
|
+
// to dial ephemeral ports. I suspect that it works better if you can get the uPNPnat service to
|
|
184
|
+
// work as then you would have a permanent port to be dialled.
|
|
185
|
+
// Alas, I struggled to get this to work reliably either. I find there is a race between the
|
|
186
|
+
// service that reads our listener addresses and the uPnP service.
|
|
187
|
+
// The result being the uPnP service can't find an address to use for the port forward.
|
|
188
|
+
// Need to investigate further.
|
|
189
|
+
// if (enableNat) {
|
|
190
|
+
// services.autoNAT = autoNATService({
|
|
191
|
+
// protocolPrefix: 'aztec',
|
|
192
|
+
// });
|
|
193
|
+
// services.uPnPNAT = uPnPNATService();
|
|
194
|
+
// }
|
|
195
|
+
|
|
196
|
+
const node = await createLibp2p({
|
|
197
|
+
...opts,
|
|
198
|
+
services,
|
|
199
|
+
});
|
|
200
|
+
const protocolId = config.transactionProtocol;
|
|
201
|
+
return new LibP2PService(config, node, protocolId, txPool);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Propagates the provided transaction to peers.
|
|
206
|
+
* @param tx - The transaction to propagate.
|
|
207
|
+
*/
|
|
208
|
+
public propagateTx(tx: Tx): void {
|
|
209
|
+
void this.jobQueue.put(() => Promise.resolve(this.sendTxToPeers(tx)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handles the settling of a new batch of transactions.
|
|
214
|
+
* @param txHashes - The hashes of the newly settled transactions.
|
|
215
|
+
*/
|
|
216
|
+
public settledTxs(txHashes: TxHash[]): void {
|
|
217
|
+
this.knownTxLookup.handleSettledTxs(txHashes.map(x => x.toString()));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async handleProtocolDial(incomingStreamData: IncomingStreamData) {
|
|
221
|
+
try {
|
|
222
|
+
const { message, peer } = await this.consumeInboundStream(incomingStreamData);
|
|
223
|
+
if (!message.length) {
|
|
224
|
+
this.logger(`Ignoring 0 byte message from peer${peer.toString()}`);
|
|
225
|
+
}
|
|
226
|
+
await this.processMessage(message, peer);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
this.logger.error(
|
|
229
|
+
`Failed to handle received message from peer ${incomingStreamData.connection.remotePeer.toString()}`,
|
|
230
|
+
err,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async consumeInboundStream(incomingStreamData: IncomingStreamData) {
|
|
236
|
+
let buffer = Buffer.alloc(0);
|
|
237
|
+
await pipe(incomingStreamData.stream, async source => {
|
|
238
|
+
for await (const msg of source) {
|
|
239
|
+
const payload = msg.subarray();
|
|
240
|
+
buffer = Buffer.concat([buffer, Buffer.from(payload)]);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
await incomingStreamData.stream.close();
|
|
244
|
+
return { message: buffer, peer: incomingStreamData.connection.remotePeer };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private handleNewConnection(peerId: PeerId) {
|
|
248
|
+
if (this.isBootstrapPeer(peerId)) {
|
|
249
|
+
this.logger(`Connected to bootstrap peer ${peerId.toString()}`);
|
|
250
|
+
} else {
|
|
251
|
+
this.logger(`Connected to transaction peer ${peerId.toString()}`);
|
|
252
|
+
// send the peer our current pooled transaction hashes
|
|
253
|
+
void this.jobQueue.put(async () => {
|
|
254
|
+
await this.sendTxHashesMessageToPeer(peerId);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async processMessage(message: Buffer, peerId: PeerId) {
|
|
260
|
+
const type = message.readUInt32BE(0);
|
|
261
|
+
const encodedMessage = getEncodedMessage(message);
|
|
262
|
+
switch (type) {
|
|
263
|
+
case Messages.POOLED_TRANSACTIONS:
|
|
264
|
+
await this.processReceivedTxs(encodedMessage, peerId);
|
|
265
|
+
return;
|
|
266
|
+
case Messages.POOLED_TRANSACTION_HASHES:
|
|
267
|
+
await this.processReceivedTxHashes(encodedMessage, peerId);
|
|
268
|
+
return;
|
|
269
|
+
case Messages.GET_TRANSACTIONS:
|
|
270
|
+
await this.processReceivedGetTransactionsRequest(encodedMessage, peerId);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
throw new Error(`Unknown message type ${type}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async processReceivedTxHashes(encodedMessage: Buffer, peerId: PeerId) {
|
|
277
|
+
try {
|
|
278
|
+
const txHashes = decodeTransactionHashesMessage(encodedMessage);
|
|
279
|
+
this.logger(`Received tx hash messages from ${peerId.toString()}`);
|
|
280
|
+
// we send a message requesting the transactions that we don't have from the set of received hashes
|
|
281
|
+
const requiredHashes = txHashes.filter(hash => !this.txPool.hasTx(hash));
|
|
282
|
+
if (!requiredHashes.length) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await this.sendGetTransactionsMessageToPeer(txHashes, peerId);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
this.logger.error(`Failed to process received tx hashes`, err);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async processReceivedGetTransactionsRequest(encodedMessage: Buffer, peerId: PeerId) {
|
|
292
|
+
try {
|
|
293
|
+
this.logger(`Received get txs messages from ${peerId.toString()}`);
|
|
294
|
+
// get the transactions in the list that we have and return them
|
|
295
|
+
const removeUndefined = <S>(value: S | undefined): value is S => value != undefined;
|
|
296
|
+
const txHashes = decodeGetTransactionsRequestMessage(encodedMessage);
|
|
297
|
+
const txs = txHashes.map(x => this.txPool.getTxByHash(x)).filter(removeUndefined);
|
|
298
|
+
if (!txs.length) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
await this.sendTransactionsMessageToPeer(txs, peerId);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
this.logger.error(`Failed to process get txs request`, err);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private async processReceivedTxs(encodedMessage: Buffer, peerId: PeerId) {
|
|
308
|
+
try {
|
|
309
|
+
const txs = decodeTransactionsMessage(encodedMessage);
|
|
310
|
+
// Could optimize here and process all txs at once
|
|
311
|
+
// Propagation would need to filter and send custom tx set per peer
|
|
312
|
+
for (const tx of txs) {
|
|
313
|
+
await this.processTxFromPeer(tx, peerId);
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
this.logger.error(`Failed to process pooled transactions message`, err);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async processTxFromPeer(tx: Tx, peerId: PeerId): Promise<void> {
|
|
321
|
+
const txHash = await tx.getTxHash();
|
|
322
|
+
const txHashString = txHash.toString();
|
|
323
|
+
this.knownTxLookup.addPeerForTx(peerId, txHashString);
|
|
324
|
+
this.logger(`Received tx ${txHashString} from peer ${peerId.toString()}`);
|
|
325
|
+
await this.txPool.addTxs([tx]);
|
|
326
|
+
this.propagateTx(tx);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async sendTxToPeers(tx: Tx) {
|
|
330
|
+
const txs = createTransactionsMessage([tx]);
|
|
331
|
+
const payload = new Uint8Array(txs);
|
|
332
|
+
const peers = this.getTxPeers();
|
|
333
|
+
const txHash = await tx.getTxHash();
|
|
334
|
+
const txHashString = txHash.toString();
|
|
335
|
+
for (const peer of peers) {
|
|
336
|
+
try {
|
|
337
|
+
if (this.knownTxLookup.hasPeerSeenTx(peer, txHashString)) {
|
|
338
|
+
this.logger(`Not sending tx ${txHashString} to peer ${peer.toString()} as they have already seen it`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
this.logger(`Sending tx ${txHashString} to peer ${peer.toString()}`);
|
|
342
|
+
await this.sendRawMessageToPeer(payload, peer);
|
|
343
|
+
this.knownTxLookup.addPeerForTx(peer, txHashString);
|
|
344
|
+
} catch (err) {
|
|
345
|
+
this.logger.error(`Failed to send txs to peer ${peer.toString()}`, err);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async sendTxHashesMessageToPeer(peer: PeerId) {
|
|
352
|
+
try {
|
|
353
|
+
const hashes = this.txPool.getAllTxHashes();
|
|
354
|
+
if (!hashes.length) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const message = createTransactionHashesMessage(hashes);
|
|
358
|
+
await this.sendRawMessageToPeer(new Uint8Array(message), peer);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
this.logger.error(`Failed to send tx hashes to peer ${peer.toString()}`, err);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async sendGetTransactionsMessageToPeer(hashes: TxHash[], peer: PeerId) {
|
|
365
|
+
try {
|
|
366
|
+
const message = createGetTransactionsRequestMessage(hashes);
|
|
367
|
+
await this.sendRawMessageToPeer(new Uint8Array(message), peer);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
this.logger.error(`Failed to send tx request to peer ${peer.toString()}`, err);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private async sendTransactionsMessageToPeer(txs: Tx[], peer: PeerId) {
|
|
374
|
+
// don't filter out any transactions based on what we think the peer has seen,
|
|
375
|
+
// we have been explicitly asked for these transactions
|
|
376
|
+
const message = createTransactionsMessage(txs);
|
|
377
|
+
await this.sendRawMessageToPeer(message, peer);
|
|
378
|
+
for (const tx of txs) {
|
|
379
|
+
const hash = await tx.getTxHash();
|
|
380
|
+
this.knownTxLookup.addPeerForTx(peer, hash.toString());
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private async sendRawMessageToPeer(message: Uint8Array, peer: PeerId) {
|
|
385
|
+
const stream = await this.node.dialProtocol(peer, this.protocolId);
|
|
386
|
+
await pipe([message], stream);
|
|
387
|
+
await stream.close();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private getTxPeers() {
|
|
391
|
+
return this.node.getPeers().filter(peer => !this.isBootstrapPeer(peer));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private isBootstrapPeer(peer: PeerId) {
|
|
395
|
+
return this.config.bootstrapNodes.findIndex(bootstrap => bootstrap.includes(peer.toString())) != -1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Tx, TxHash } from '@aztec/circuit-types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The interface for a P2P service implementation.
|
|
5
|
+
*/
|
|
6
|
+
export interface P2PService {
|
|
7
|
+
/**
|
|
8
|
+
* Starts the service.
|
|
9
|
+
* @returns An empty promise.
|
|
10
|
+
*/
|
|
11
|
+
start(): Promise<void>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stops the service.
|
|
15
|
+
* @returns An empty promise.
|
|
16
|
+
*/
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Called to have the given transaction propagated through the P2P network.
|
|
21
|
+
* @param tx - The transaction to be propagated.
|
|
22
|
+
*/
|
|
23
|
+
propagateTx(tx: Tx): void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Called upon receipt of settled transactions.
|
|
27
|
+
* @param txHashes - The hashes of the settled transactions.
|
|
28
|
+
*/
|
|
29
|
+
settledTxs(txHashes: TxHash[]): void;
|
|
30
|
+
}
|