@aztec/p2p 0.1.0-alpha11
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/.eslintrc.cjs +1 -0
- package/.tsbuildinfo +1 -0
- package/README.md +7 -0
- package/dest/bootstrap/bootstrap.d.ts +26 -0
- package/dest/bootstrap/bootstrap.d.ts.map +1 -0
- package/dest/bootstrap/bootstrap.js +92 -0
- package/dest/client/index.d.ts +5 -0
- package/dest/client/index.d.ts.map +1 -0
- package/dest/client/index.js +8 -0
- package/dest/client/mocks.d.ts +33 -0
- package/dest/client/mocks.d.ts.map +1 -0
- package/dest/client/mocks.js +52 -0
- package/dest/client/p2p_client.d.ts +170 -0
- package/dest/client/p2p_client.d.ts.map +1 -0
- package/dest/client/p2p_client.js +206 -0
- package/dest/client/p2p_client.test.d.ts +2 -0
- package/dest/client/p2p_client.test.d.ts.map +1 -0
- package/dest/client/p2p_client.test.js +58 -0
- package/dest/config.d.ts +67 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +25 -0
- package/dest/index.d.ts +6 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +6 -0
- package/dest/service/dummy_service.d.ts +28 -0
- package/dest/service/dummy_service.d.ts.map +1 -0
- package/dest/service/dummy_service.js +30 -0
- package/dest/service/index.d.ts +3 -0
- package/dest/service/index.d.ts.map +1 -0
- package/dest/service/index.js +3 -0
- package/dest/service/known_txs.d.ts +31 -0
- package/dest/service/known_txs.d.ts.map +1 -0
- package/dest/service/known_txs.js +52 -0
- package/dest/service/known_txs.test.d.ts +2 -0
- package/dest/service/known_txs.test.d.ts.map +1 -0
- package/dest/service/known_txs.test.js +34 -0
- package/dest/service/libp2p_service.d.ts +74 -0
- package/dest/service/libp2p_service.d.ts.map +1 -0
- package/dest/service/libp2p_service.js +335 -0
- package/dest/service/service.d.ts +27 -0
- package/dest/service/service.d.ts.map +1 -0
- package/dest/service/service.js +2 -0
- package/dest/service/tx_messages.d.ts +78 -0
- package/dest/service/tx_messages.d.ts.map +1 -0
- package/dest/service/tx_messages.js +191 -0
- package/dest/service/tx_messages.test.d.ts +2 -0
- package/dest/service/tx_messages.test.d.ts.map +1 -0
- package/dest/service/tx_messages.test.js +56 -0
- package/dest/tx_pool/index.d.ts +3 -0
- package/dest/tx_pool/index.d.ts.map +1 -0
- package/dest/tx_pool/index.js +3 -0
- package/dest/tx_pool/memory_tx_pool.d.ts +52 -0
- package/dest/tx_pool/memory_tx_pool.d.ts.map +1 -0
- package/dest/tx_pool/memory_tx_pool.js +70 -0
- package/dest/tx_pool/tx_pool.d.ts +39 -0
- package/dest/tx_pool/tx_pool.d.ts.map +1 -0
- package/dest/tx_pool/tx_pool.js +2 -0
- package/dest/tx_pool/tx_pool.test.d.ts +2 -0
- package/dest/tx_pool/tx_pool.test.d.ts.map +1 -0
- package/dest/tx_pool/tx_pool.test.js +20 -0
- package/package.json +24 -0
- package/src/bootstrap/bootstrap.ts +107 -0
- package/src/client/index.ts +10 -0
- package/src/client/mocks.ts +63 -0
- package/src/client/p2p_client.test.ts +79 -0
- package/src/client/p2p_client.ts +304 -0
- package/src/config.ts +113 -0
- package/src/index.ts +5 -0
- package/src/service/dummy_service.ts +35 -0
- package/src/service/index.ts +2 -0
- package/src/service/known_txs.test.ts +45 -0
- package/src/service/known_txs.ts +56 -0
- package/src/service/libp2p_service.ts +385 -0
- package/src/service/service.ts +30 -0
- package/src/service/tx_messages.test.ts +83 -0
- package/src/service/tx_messages.ts +211 -0
- package/src/tx_pool/index.ts +2 -0
- package/src/tx_pool/memory_tx_pool.ts +82 -0
- package/src/tx_pool/tx_pool.test.ts +24 -0
- package/src/tx_pool/tx_pool.ts +44 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Tx, TxHash } from '@aztec/types';
|
|
2
|
+
import { P2PService } from './service.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A dummy implementation of the P2P Service.
|
|
6
|
+
*/
|
|
7
|
+
export class DummyP2PService implements P2PService {
|
|
8
|
+
/**
|
|
9
|
+
* Starts the dummy implementation.
|
|
10
|
+
* @returns A resolved promise.
|
|
11
|
+
*/
|
|
12
|
+
public start() {
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stops the dummy imaplementation.
|
|
18
|
+
* @returns A resolved promise.
|
|
19
|
+
*/
|
|
20
|
+
public stop() {
|
|
21
|
+
return Promise.resolve();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Called to have the given transaction propagated through the P2P network.
|
|
26
|
+
* @param _ - The transaction to be propagated.
|
|
27
|
+
*/
|
|
28
|
+
public propagateTx(_: Tx) {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Called upon receipt of settled transactions.
|
|
32
|
+
* @param _ - The hashes of the settled transactions.
|
|
33
|
+
*/
|
|
34
|
+
public settledTxs(_: TxHash[]) {}
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { expect } from '@jest/globals';
|
|
2
|
+
import { mock } from 'jest-mock-extended';
|
|
3
|
+
import { PeerId, Ed25519PeerId } from '@libp2p/interface-peer-id';
|
|
4
|
+
import { KnownTxLookup } from './known_txs.js';
|
|
5
|
+
import { TxHash } from '@aztec/types';
|
|
6
|
+
import { randomBytes } from '@aztec/foundation/crypto';
|
|
7
|
+
|
|
8
|
+
const createMockPeerId = (peerId: string): PeerId => {
|
|
9
|
+
return mock<Ed25519PeerId>({
|
|
10
|
+
toString: () => peerId,
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const createTxHash = () => {
|
|
15
|
+
return new TxHash(randomBytes(32));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('Known Txs', () => {
|
|
19
|
+
it('Returns false when a peer has not seen a tx', () => {
|
|
20
|
+
const knownTxs = new KnownTxLookup();
|
|
21
|
+
|
|
22
|
+
const peer = createMockPeerId('Peer 1');
|
|
23
|
+
const txHash = createTxHash();
|
|
24
|
+
|
|
25
|
+
expect(knownTxs.hasPeerSeenTx(peer, txHash.toString())).toEqual(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('Returns true when a peer has seen a tx', () => {
|
|
29
|
+
const knownTxs = new KnownTxLookup();
|
|
30
|
+
|
|
31
|
+
const peer = createMockPeerId('Peer 1');
|
|
32
|
+
const peer2 = createMockPeerId('Peer 2');
|
|
33
|
+
const txHash = createTxHash();
|
|
34
|
+
|
|
35
|
+
knownTxs.addPeerForTx(peer, txHash.toString());
|
|
36
|
+
|
|
37
|
+
expect(knownTxs.hasPeerSeenTx(peer, txHash.toString())).toEqual(true);
|
|
38
|
+
expect(knownTxs.hasPeerSeenTx(peer2, txHash.toString())).toEqual(false);
|
|
39
|
+
|
|
40
|
+
knownTxs.addPeerForTx(peer2, txHash.toString());
|
|
41
|
+
|
|
42
|
+
expect(knownTxs.hasPeerSeenTx(peer, txHash.toString())).toEqual(true);
|
|
43
|
+
expect(knownTxs.hasPeerSeenTx(peer2, txHash.toString())).toEqual(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -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,385 @@
|
|
|
1
|
+
import { Libp2p, Libp2pOptions, ServiceFactoryMap, createLibp2p } from 'libp2p';
|
|
2
|
+
import { tcp } from '@libp2p/tcp';
|
|
3
|
+
import { noise } from '@chainsafe/libp2p-noise';
|
|
4
|
+
import { yamux } from '@chainsafe/libp2p-yamux';
|
|
5
|
+
import { mplex } from '@libp2p/mplex';
|
|
6
|
+
import { bootstrap } from '@libp2p/bootstrap';
|
|
7
|
+
import { DualKadDHT, kadDHT } from '@libp2p/kad-dht';
|
|
8
|
+
import { createEd25519PeerId, createFromProtobuf, exportToProtobuf } from '@libp2p/peer-id-factory';
|
|
9
|
+
import type { ServiceMap } from '@libp2p/interface-libp2p';
|
|
10
|
+
import { PeerId } from '@libp2p/interface-peer-id';
|
|
11
|
+
import { IncomingStreamData } from '@libp2p/interface-registrar';
|
|
12
|
+
import { P2PService } from './service.js';
|
|
13
|
+
import { createDebugLogger } from '@aztec/foundation/log';
|
|
14
|
+
import { SerialQueue } from '@aztec/foundation/fifo';
|
|
15
|
+
import { P2PConfig } from '../config.js';
|
|
16
|
+
import { Tx, TxHash } from '@aztec/types';
|
|
17
|
+
import { pipe } from 'it-pipe';
|
|
18
|
+
import {
|
|
19
|
+
Messages,
|
|
20
|
+
createGetTransactionsRequestMessage,
|
|
21
|
+
createTransactionHashesMessage,
|
|
22
|
+
createTransactionsMessage,
|
|
23
|
+
decodeGetTransactionsRequestMessage,
|
|
24
|
+
decodeTransactionHashesMessage,
|
|
25
|
+
decodeTransactionsMessage,
|
|
26
|
+
getEncodedMessage,
|
|
27
|
+
} from './tx_messages.js';
|
|
28
|
+
import { KnownTxLookup } from './known_txs.js';
|
|
29
|
+
import { TxPool } from '../index.js';
|
|
30
|
+
import { autoNATService } from 'libp2p/autonat';
|
|
31
|
+
import { identifyService } from 'libp2p/identify';
|
|
32
|
+
|
|
33
|
+
const INITIAL_PEER_REFRESH_INTERVAL = 20000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a libp2p peer ID.
|
|
37
|
+
* @returns The peer ID.
|
|
38
|
+
*/
|
|
39
|
+
export async function createLibP2PPeerId() {
|
|
40
|
+
return await createEd25519PeerId();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Exports a given peer id to a string representation.
|
|
45
|
+
* @param peerId - The peerId instance to be converted.
|
|
46
|
+
* @returns The peer id as a string.
|
|
47
|
+
*/
|
|
48
|
+
export function exportLibP2PPeerIdToString(peerId: PeerId) {
|
|
49
|
+
return Buffer.from(exportToProtobuf(peerId)).toString('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Lib P2P implementation of the P2PService interface.
|
|
54
|
+
*/
|
|
55
|
+
export class LibP2PService implements P2PService {
|
|
56
|
+
private jobQueue: SerialQueue = new SerialQueue();
|
|
57
|
+
private timeout: NodeJS.Timer | undefined = undefined;
|
|
58
|
+
private knownTxLookup: KnownTxLookup = new KnownTxLookup();
|
|
59
|
+
constructor(
|
|
60
|
+
private config: P2PConfig,
|
|
61
|
+
private node: Libp2p,
|
|
62
|
+
private protocolId: string,
|
|
63
|
+
private txPool: TxPool,
|
|
64
|
+
private logger = createDebugLogger('aztec:libp2p_service'),
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Starts the LibP2P service.
|
|
69
|
+
* @returns An empty promise.
|
|
70
|
+
*/
|
|
71
|
+
public async start() {
|
|
72
|
+
if (this.node.isStarted()) {
|
|
73
|
+
throw new Error('P2P service already started');
|
|
74
|
+
}
|
|
75
|
+
const { enableNat, tcpListenIp, tcpListenPort, announceHostname, announcePort } = this.config;
|
|
76
|
+
this.logger(`Starting P2P node on ${tcpListenIp}:${tcpListenPort}`);
|
|
77
|
+
if (announceHostname) this.logger(`Announcing at ${announceHostname}:${announcePort ?? tcpListenPort}`);
|
|
78
|
+
if (enableNat) this.logger(`Enabling NAT in libp2p module`);
|
|
79
|
+
|
|
80
|
+
this.node.addEventListener('peer:discovery', evt => {
|
|
81
|
+
const peerId = evt.detail.id;
|
|
82
|
+
if (this.isBootstrapPeer(peerId)) {
|
|
83
|
+
this.logger(`Discovered bootstrap peer ${peerId.toString()}`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.node.addEventListener('peer:connect', evt => {
|
|
88
|
+
const peerId = evt.detail;
|
|
89
|
+
this.handleNewConnection(peerId);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.node.addEventListener('peer:disconnect', evt => {
|
|
93
|
+
const peerId = evt.detail;
|
|
94
|
+
if (this.isBootstrapPeer(peerId)) {
|
|
95
|
+
this.logger(`Disconnect from bootstrap peer ${peerId.toString()}`);
|
|
96
|
+
} else {
|
|
97
|
+
this.logger(`Disconnected from transaction peer ${peerId.toString()}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.jobQueue.start();
|
|
102
|
+
await this.node.start();
|
|
103
|
+
await this.node.handle(this.protocolId, (incoming: IncomingStreamData) =>
|
|
104
|
+
this.jobQueue.put(() => Promise.resolve(this.handleProtocolDial(incoming))),
|
|
105
|
+
);
|
|
106
|
+
const dht = this.node.services['kadDHT'] as DualKadDHT;
|
|
107
|
+
this.logger(`Started P2P client as ${await dht.getMode()} with Peer ID ${this.node.peerId.toString()}`);
|
|
108
|
+
this.timeout = setTimeout(async () => {
|
|
109
|
+
this.logger(`Refreshing routing table...`);
|
|
110
|
+
await dht.refreshRoutingTable();
|
|
111
|
+
}, INITIAL_PEER_REFRESH_INTERVAL);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stops the LibP2P service.
|
|
116
|
+
* @returns An empty promise.
|
|
117
|
+
*/
|
|
118
|
+
public async stop() {
|
|
119
|
+
if (this.timeout) {
|
|
120
|
+
clearTimeout(this.timeout);
|
|
121
|
+
}
|
|
122
|
+
await this.jobQueue.end();
|
|
123
|
+
await this.node.stop();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates an instance of the LibP2P service.
|
|
128
|
+
* @param config - The configuration to use when creating the service.
|
|
129
|
+
* @param txPool - The transaction pool to be accessed by the service.
|
|
130
|
+
* @returns The new service.
|
|
131
|
+
*/
|
|
132
|
+
public static async new(config: P2PConfig, txPool: TxPool) {
|
|
133
|
+
const {
|
|
134
|
+
enableNat,
|
|
135
|
+
tcpListenIp,
|
|
136
|
+
tcpListenPort,
|
|
137
|
+
announceHostname,
|
|
138
|
+
announcePort,
|
|
139
|
+
serverMode,
|
|
140
|
+
minPeerCount,
|
|
141
|
+
maxPeerCount,
|
|
142
|
+
} = config;
|
|
143
|
+
const peerId = config.peerIdPrivateKey
|
|
144
|
+
? await createFromProtobuf(Buffer.from(config.peerIdPrivateKey, 'hex'))
|
|
145
|
+
: await createLibP2PPeerId();
|
|
146
|
+
|
|
147
|
+
const opts: Libp2pOptions<ServiceMap> = {
|
|
148
|
+
start: false,
|
|
149
|
+
peerId,
|
|
150
|
+
addresses: {
|
|
151
|
+
listen: [`/ip4/${tcpListenIp}/tcp/${tcpListenPort}`],
|
|
152
|
+
announce: announceHostname ? [`/ip4/${announceHostname}/tcp/${announcePort ?? tcpListenPort}`] : [],
|
|
153
|
+
},
|
|
154
|
+
transports: [tcp()],
|
|
155
|
+
streamMuxers: [yamux(), mplex()],
|
|
156
|
+
connectionEncryption: [noise()],
|
|
157
|
+
connectionManager: {
|
|
158
|
+
minConnections: minPeerCount,
|
|
159
|
+
maxConnections: maxPeerCount,
|
|
160
|
+
},
|
|
161
|
+
peerDiscovery: [
|
|
162
|
+
bootstrap({
|
|
163
|
+
list: config.bootstrapNodes,
|
|
164
|
+
}),
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const services: ServiceFactoryMap = {
|
|
169
|
+
identify: identifyService({
|
|
170
|
+
protocolPrefix: 'aztec',
|
|
171
|
+
}),
|
|
172
|
+
kadDHT: kadDHT({
|
|
173
|
+
protocolPrefix: 'aztec',
|
|
174
|
+
clientMode: !serverMode,
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (enableNat) {
|
|
179
|
+
services.nat = autoNATService({
|
|
180
|
+
protocolPrefix: 'aztec',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const node = await createLibp2p({
|
|
185
|
+
...opts,
|
|
186
|
+
services,
|
|
187
|
+
});
|
|
188
|
+
const protocolId = config.transactionProtocol;
|
|
189
|
+
return new LibP2PService(config, node, protocolId, txPool);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Propagates the provided transaction to peers.
|
|
194
|
+
* @param tx - The transaction to propagate.
|
|
195
|
+
*/
|
|
196
|
+
public propagateTx(tx: Tx): void {
|
|
197
|
+
void this.jobQueue.put(() => Promise.resolve(this.sendTxToPeers(tx)));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handles the settling of a new batch of transactions.
|
|
202
|
+
* @param txHashes - The hashes of the newly settled transactions.
|
|
203
|
+
*/
|
|
204
|
+
public settledTxs(txHashes: TxHash[]): void {
|
|
205
|
+
this.knownTxLookup.handleSettledTxs(txHashes.map(x => x.toString()));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async handleProtocolDial(incomingStreamData: IncomingStreamData) {
|
|
209
|
+
try {
|
|
210
|
+
const { message, peer } = await this.consumeInboundStream(incomingStreamData);
|
|
211
|
+
if (!message.length) {
|
|
212
|
+
this.logger(`Ignoring 0 byte message from peer${peer.toString()}`);
|
|
213
|
+
}
|
|
214
|
+
await this.processMessage(message, peer);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
this.logger(
|
|
217
|
+
`Failed to handle received message from peer ${incomingStreamData.connection.remotePeer.toString()}`,
|
|
218
|
+
err,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async consumeInboundStream(incomingStreamData: IncomingStreamData) {
|
|
224
|
+
let buffer = Buffer.alloc(0);
|
|
225
|
+
await pipe(incomingStreamData.stream, async source => {
|
|
226
|
+
for await (const msg of source) {
|
|
227
|
+
const payload = msg.subarray();
|
|
228
|
+
buffer = Buffer.concat([buffer, Buffer.from(payload)]);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
incomingStreamData.stream.close();
|
|
232
|
+
return { message: buffer, peer: incomingStreamData.connection.remotePeer };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private handleNewConnection(peerId: PeerId) {
|
|
236
|
+
if (this.isBootstrapPeer(peerId)) {
|
|
237
|
+
this.logger(`Connected to bootstrap peer ${peerId.toString()}`);
|
|
238
|
+
} else {
|
|
239
|
+
this.logger(`Connected to transaction peer ${peerId.toString()}`);
|
|
240
|
+
// send the peer our current pooled transaction hashes
|
|
241
|
+
void this.jobQueue.put(async () => {
|
|
242
|
+
await this.sendTxHashesMessageToPeer(peerId);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async processMessage(message: Buffer, peerId: PeerId) {
|
|
248
|
+
const type = message.readUInt32BE(0);
|
|
249
|
+
const encodedMessage = getEncodedMessage(message);
|
|
250
|
+
switch (type) {
|
|
251
|
+
case Messages.POOLED_TRANSACTIONS:
|
|
252
|
+
await this.processReceivedTxs(encodedMessage, peerId);
|
|
253
|
+
return;
|
|
254
|
+
case Messages.POOLED_TRANSACTION_HASHES:
|
|
255
|
+
await this.processReceivedTxHashes(encodedMessage, peerId);
|
|
256
|
+
return;
|
|
257
|
+
case Messages.GET_TRANSACTIONS:
|
|
258
|
+
await this.processReceivedGetTransactionsRequest(encodedMessage, peerId);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`Unknown message type ${type}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async processReceivedTxHashes(encodedMessage: Buffer, peerId: PeerId) {
|
|
265
|
+
try {
|
|
266
|
+
const txHashes = decodeTransactionHashesMessage(encodedMessage);
|
|
267
|
+
this.logger(`Received tx hash messages from ${peerId.toString()}`);
|
|
268
|
+
// we send a message requesting the transactions that we don't have from the set of received hashes
|
|
269
|
+
const requiredHashes = txHashes.filter(hash => !this.txPool.hasTx(hash));
|
|
270
|
+
if (!requiredHashes.length) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await this.sendGetTransactionsMessageToPeer(txHashes, peerId);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
this.logger(`Failed to process received tx hashes`, err);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async processReceivedGetTransactionsRequest(encodedMessage: Buffer, peerId: PeerId) {
|
|
280
|
+
try {
|
|
281
|
+
this.logger(`Received get txs messages from ${peerId.toString()}`);
|
|
282
|
+
// get the transactions in the list that we have and return them
|
|
283
|
+
const removeUndefined = <S>(value: S | undefined): value is S => value != undefined;
|
|
284
|
+
const txHashes = decodeGetTransactionsRequestMessage(encodedMessage);
|
|
285
|
+
const txs = txHashes.map(x => this.txPool.getTxByHash(x)).filter(removeUndefined);
|
|
286
|
+
if (!txs.length) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
await this.sendTransactionsMessageToPeer(txs, peerId);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
this.logger(`Failed to process get txs request`, err);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async processReceivedTxs(encodedMessage: Buffer, peerId: PeerId) {
|
|
296
|
+
try {
|
|
297
|
+
const txs = decodeTransactionsMessage(encodedMessage);
|
|
298
|
+
// Could optimise here and process all txs at once
|
|
299
|
+
// Propagation would need to filter and send custom tx set per peer
|
|
300
|
+
for (const tx of txs) {
|
|
301
|
+
await this.processTxFromPeer(tx, peerId);
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
this.logger(`Failed to process pooled transactions message`, err);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async processTxFromPeer(tx: Tx, peerId: PeerId): Promise<void> {
|
|
309
|
+
const txHash = await tx.getTxHash();
|
|
310
|
+
const txHashString = txHash.toString();
|
|
311
|
+
this.knownTxLookup.addPeerForTx(peerId, txHashString);
|
|
312
|
+
this.logger(`Received tx ${txHashString} from peer ${peerId.toString()}`);
|
|
313
|
+
await this.txPool.addTxs([tx]);
|
|
314
|
+
this.propagateTx(tx);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async sendTxToPeers(tx: Tx) {
|
|
318
|
+
const txs = createTransactionsMessage([tx]);
|
|
319
|
+
const payload = new Uint8Array(txs);
|
|
320
|
+
const peers = this.getTxPeers();
|
|
321
|
+
const txHash = await tx.getTxHash();
|
|
322
|
+
const txHashString = txHash.toString();
|
|
323
|
+
for (const peer of peers) {
|
|
324
|
+
try {
|
|
325
|
+
if (this.knownTxLookup.hasPeerSeenTx(peer, txHashString)) {
|
|
326
|
+
this.logger(`Not sending tx ${txHashString} to peer ${peer.toString()} as they have already seen it`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
this.logger(`Sending tx ${txHashString} to peer ${peer.toString()}`);
|
|
330
|
+
await this.sendRawMessageToPeer(payload, peer);
|
|
331
|
+
this.knownTxLookup.addPeerForTx(peer, txHashString);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
this.logger(`Failed to send txs to peer ${peer.toString()}`, err);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private async sendTxHashesMessageToPeer(peer: PeerId) {
|
|
340
|
+
try {
|
|
341
|
+
const hashes = this.txPool.getAllTxHashes();
|
|
342
|
+
if (!hashes.length) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const message = createTransactionHashesMessage(hashes);
|
|
346
|
+
await this.sendRawMessageToPeer(new Uint8Array(message), peer);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
this.logger(`Failed to send tx hashes to peer ${peer.toString()}`, err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private async sendGetTransactionsMessageToPeer(hashes: TxHash[], peer: PeerId) {
|
|
353
|
+
try {
|
|
354
|
+
const message = createGetTransactionsRequestMessage(hashes);
|
|
355
|
+
await this.sendRawMessageToPeer(new Uint8Array(message), peer);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
this.logger(`Failed to send tx request to peer ${peer.toString()}`, err);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private async sendTransactionsMessageToPeer(txs: Tx[], peer: PeerId) {
|
|
362
|
+
// don't filter out any transactions based on what we think the peer has seen,
|
|
363
|
+
// we have been explicitly asked for these transactions
|
|
364
|
+
const message = createTransactionsMessage(txs);
|
|
365
|
+
await this.sendRawMessageToPeer(message, peer);
|
|
366
|
+
for (const tx of txs) {
|
|
367
|
+
const hash = await tx.getTxHash();
|
|
368
|
+
this.knownTxLookup.addPeerForTx(peer, hash.toString());
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async sendRawMessageToPeer(message: Uint8Array, peer: PeerId) {
|
|
373
|
+
const stream = await this.node.dialProtocol(peer, this.protocolId);
|
|
374
|
+
await pipe([message], stream);
|
|
375
|
+
stream.close();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private getTxPeers() {
|
|
379
|
+
return this.node.getPeers().filter(peer => !this.isBootstrapPeer(peer));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private isBootstrapPeer(peer: PeerId) {
|
|
383
|
+
return this.config.bootstrapNodes.findIndex(bootstrap => bootstrap.includes(peer.toString())) != -1;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Tx, TxHash } from '@aztec/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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { KERNEL_PUBLIC_CALL_STACK_LENGTH, Proof } from '@aztec/circuits.js';
|
|
2
|
+
import { makeKernelPublicInputs, makePublicCallRequest } from '@aztec/circuits.js/factories';
|
|
3
|
+
import { EncodedContractFunction, Tx, TxHash, TxL2Logs } from '@aztec/types';
|
|
4
|
+
import { expect } from '@jest/globals';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
6
|
+
import times from 'lodash.times';
|
|
7
|
+
import {
|
|
8
|
+
Messages,
|
|
9
|
+
createGetTransactionsRequestMessage,
|
|
10
|
+
createTransactionHashesMessage,
|
|
11
|
+
createTransactionsMessage,
|
|
12
|
+
decodeGetTransactionsRequestMessage,
|
|
13
|
+
decodeMessageType,
|
|
14
|
+
decodeTransactionHashesMessage,
|
|
15
|
+
decodeTransactionsMessage,
|
|
16
|
+
fromTxMessage,
|
|
17
|
+
getEncodedMessage,
|
|
18
|
+
toTxMessage,
|
|
19
|
+
} from './tx_messages.js';
|
|
20
|
+
|
|
21
|
+
const makeTx = () => {
|
|
22
|
+
const encodedPublicFunctions = [EncodedContractFunction.random(), EncodedContractFunction.random()];
|
|
23
|
+
const enqueuedPublicFunctionCalls = times(KERNEL_PUBLIC_CALL_STACK_LENGTH, i => makePublicCallRequest(i));
|
|
24
|
+
return new Tx(
|
|
25
|
+
makeKernelPublicInputs(),
|
|
26
|
+
Proof.fromBuffer(Buffer.alloc(10, 9)),
|
|
27
|
+
TxL2Logs.random(8, 2),
|
|
28
|
+
TxL2Logs.random(8, 3),
|
|
29
|
+
encodedPublicFunctions,
|
|
30
|
+
enqueuedPublicFunctionCalls,
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const makeTxHash = () => {
|
|
35
|
+
return new TxHash(randomBytes(32));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const verifyTx = (actual: Tx, expected: Tx) => {
|
|
39
|
+
expect(actual.data!.toBuffer()).toEqual(expected.data?.toBuffer());
|
|
40
|
+
expect(actual.proof!.toBuffer()).toEqual(expected.proof!.toBuffer());
|
|
41
|
+
expect(actual.encryptedLogs!.toBuffer()).toEqual(expected.encryptedLogs?.toBuffer());
|
|
42
|
+
expect(actual.newContractPublicFunctions!.length).toEqual(expected.newContractPublicFunctions!.length);
|
|
43
|
+
for (let i = 0; i < actual.newContractPublicFunctions!.length; i++) {
|
|
44
|
+
expect(actual.newContractPublicFunctions![i].toBuffer()).toEqual(
|
|
45
|
+
expected.newContractPublicFunctions![i].toBuffer(),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe('Messages', () => {
|
|
51
|
+
it('Correctly serialises and deserialises a single private transaction', () => {
|
|
52
|
+
const transaction = makeTx();
|
|
53
|
+
const message = toTxMessage(transaction);
|
|
54
|
+
const decodedTransaction = fromTxMessage(message);
|
|
55
|
+
verifyTx(decodedTransaction, transaction);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('Correctly serialises and deserialises transactions messages', () => {
|
|
59
|
+
const privateTransactions = [makeTx(), makeTx(), makeTx()];
|
|
60
|
+
const message = createTransactionsMessage(privateTransactions);
|
|
61
|
+
expect(decodeMessageType(message)).toBe(Messages.POOLED_TRANSACTIONS);
|
|
62
|
+
const decodedTransactions = decodeTransactionsMessage(getEncodedMessage(message));
|
|
63
|
+
verifyTx(decodedTransactions[0], privateTransactions[0]);
|
|
64
|
+
verifyTx(decodedTransactions[1], privateTransactions[1]);
|
|
65
|
+
verifyTx(decodedTransactions[2], privateTransactions[2]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('Correctly serialises and deserialises transaction hashes message', () => {
|
|
69
|
+
const txHashes = [makeTxHash(), makeTxHash(), makeTxHash()];
|
|
70
|
+
const message = createTransactionHashesMessage(txHashes);
|
|
71
|
+
expect(decodeMessageType(message)).toEqual(Messages.POOLED_TRANSACTION_HASHES);
|
|
72
|
+
const decodedHashes = decodeTransactionHashesMessage(getEncodedMessage(message));
|
|
73
|
+
expect(decodedHashes.map(x => x.toString())).toEqual(txHashes.map(x => x.toString()));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('Correctly serialises and deserialises get transactions message', () => {
|
|
77
|
+
const txHashes = [makeTxHash(), makeTxHash(), makeTxHash()];
|
|
78
|
+
const message = createGetTransactionsRequestMessage(txHashes);
|
|
79
|
+
expect(decodeMessageType(message)).toEqual(Messages.GET_TRANSACTIONS);
|
|
80
|
+
const decodedHashes = decodeGetTransactionsRequestMessage(getEncodedMessage(message));
|
|
81
|
+
expect(decodedHashes.map(x => x.toString())).toEqual(txHashes.map(x => x.toString()));
|
|
82
|
+
});
|
|
83
|
+
});
|