@decentnetwork/peer 0.1.128 → 0.1.130
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/README.md +44 -0
- package/dist/compat/tcp-relay-pool.d.ts +25 -6
- package/dist/compat/tcp-relay-pool.js +34 -7
- package/dist/peer.js +36 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,3 +95,47 @@ is your identity — back it up.
|
|
|
95
95
|
## License
|
|
96
96
|
|
|
97
97
|
GPL-3.0-or-later. Same as upstream toxcore.
|
|
98
|
+
|
|
99
|
+
## Tuning peer discovery
|
|
100
|
+
|
|
101
|
+
Discovery is the loop that finds a friend's current UDP endpoint when there is
|
|
102
|
+
no live session. It costs real CPU — every onion request is three X25519
|
|
103
|
+
scalar multiplications, one per layer — so the defaults are a compromise, and
|
|
104
|
+
a node with unusual friend counts or hardware may want to move them.
|
|
105
|
+
|
|
106
|
+
All are environment variables read at startup.
|
|
107
|
+
|
|
108
|
+
| Axis | Variable | Default | What it controls |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| **Interval** | `DECENT_DHT_PK_ANNOUNCE_COOLDOWN_MS` | `25000` | Minimum gap between lookups for the same friend |
|
|
111
|
+
| **Backoff cap** | `DECENT_ONION_LOOKUP_MAX_BACKOFF_MS` | `120000` | Ceiling once a friend keeps failing to resolve |
|
|
112
|
+
| **Breadth** | `DECENT_FRIEND_ROUTE_MAX_ATTEMPTS` | `24` | Nodes queried per sweep |
|
|
113
|
+
| **Depth** | `DECENT_ONION_DATA_ATTEMPTS` | `5` | Retries per onion data packet |
|
|
114
|
+
| Self-announce interval | `DECENT_SELF_ANNOUNCE_INTERVAL_MS` | `20000` | How often we re-announce ourselves |
|
|
115
|
+
| Self-announce breadth | `DECENT_SELF_ANNOUNCE_TARGETS` | `16` | Nodes we announce to |
|
|
116
|
+
|
|
117
|
+
### Which knob to turn
|
|
118
|
+
|
|
119
|
+
**Turn the interval, not the breadth.** Cost is breadth x frequency, so both
|
|
120
|
+
reduce CPU — but they fail differently. Narrowing breadth was tried and
|
|
121
|
+
reverted: iOS peers appear in short windows and are only caught by a wide
|
|
122
|
+
sweep, so a narrow one loses them outright rather than finding them late.
|
|
123
|
+
Lengthening the interval degrades gracefully — the worst case is simply
|
|
124
|
+
`ONION_LOOKUP_MAX_BACKOFF_MS`, the longest you can go without noticing a peer
|
|
125
|
+
that came back.
|
|
126
|
+
|
|
127
|
+
Read the backoff cap as exactly that: **the worst-case time to notice a friend
|
|
128
|
+
returning.** Raising it to save CPU is a direct trade against how quickly the
|
|
129
|
+
node reacts.
|
|
130
|
+
|
|
131
|
+
### Symptoms and responses
|
|
132
|
+
|
|
133
|
+
- **High steady CPU with offline friends.** Expected: each unreachable friend
|
|
134
|
+
is swept on its own schedule, so cost scales with how many friends are *not*
|
|
135
|
+
there. Raise `DECENT_ONION_LOOKUP_MAX_BACKOFF_MS`. Measured on a 2-core box
|
|
136
|
+
with ~5 unreachable friends, a flat 25s interval held the daemon at ~22% of
|
|
137
|
+
a core purely on lookups.
|
|
138
|
+
- **A peer is online but never seen.** Lower the interval, or raise breadth if
|
|
139
|
+
it was lowered. Do not lower breadth further.
|
|
140
|
+
- **Small or battery-powered device.** Raise the interval and the backoff cap
|
|
141
|
+
together; leave breadth alone.
|
|
@@ -70,12 +70,31 @@ export declare class TcpRelayPool extends EventEmitter {
|
|
|
70
70
|
/** Diagnostics. */
|
|
71
71
|
connectedCount(): number;
|
|
72
72
|
/**
|
|
73
|
-
* Send an onion request
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
73
|
+
* Send an onion request through up to `fanout` connected relays.
|
|
74
|
+
*
|
|
75
|
+
* Each relay forwards the packet to node B over UDP and routes the onion
|
|
76
|
+
* response back over TCP (surfaced via the pool's "onionResponse" event).
|
|
77
|
+
* Returns how many relays accepted it (0 = none connected).
|
|
78
|
+
*
|
|
79
|
+
* **What fanout buys.** The SAME request is handed to several relays, so it
|
|
80
|
+
* is redundancy, not extra reach: if one relay is unreachable, overloaded, or
|
|
81
|
+
* simply cannot get to node B, another copy still arrives. The peer sees one
|
|
82
|
+
* request either way — duplicates are dropped by the onion layer.
|
|
83
|
+
*
|
|
84
|
+
* **What it costs.** Linear in every resource this path spends: N relays
|
|
85
|
+
* means N times the TCP traffic and N times the work at the relay end. It
|
|
86
|
+
* does NOT multiply local crypto — the onion packet is built once and reused,
|
|
87
|
+
* so the X25519 cost is paid once regardless of fanout.
|
|
88
|
+
*
|
|
89
|
+
* **Choosing a value.** `1` is cheapest and correct on a stable link where
|
|
90
|
+
* the first relay is reliable; a lost request just waits for the next sweep.
|
|
91
|
+
* `2` (the default) is the useful middle: it covers a single dead relay,
|
|
92
|
+
* which is the common failure, at double the traffic on a path that is
|
|
93
|
+
* already small. Above 3 the added redundancy is mostly wasted — you are
|
|
94
|
+
* paying for the case where three independent relays fail at once, which in
|
|
95
|
+
* practice means the network is down, not the relays.
|
|
96
|
+
*
|
|
97
|
+
* Override with `DECENT_ONION_RELAY_FANOUT`.
|
|
79
98
|
*/
|
|
80
99
|
sendOnionRequest(packet: Uint8Array, fanout?: number): number;
|
|
81
100
|
/**
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
import { EventEmitter } from "node:events";
|
|
23
23
|
import { base58ToBytes } from "../utils/base58.js";
|
|
24
24
|
import { TcpRelayClient } from "./tcp-relay.js";
|
|
25
|
+
/** How many connected relays each onion request is duplicated across.
|
|
26
|
+
*
|
|
27
|
+
* Redundancy against a single dead relay, at a linear cost in TCP traffic.
|
|
28
|
+
* See {@link TcpRelayPool.sendOnionRequest} for how to choose a value. */
|
|
29
|
+
const ONION_RELAY_FANOUT = (() => {
|
|
30
|
+
const raw = Number(process.env.DECENT_ONION_RELAY_FANOUT);
|
|
31
|
+
return Number.isInteger(raw) && raw >= 1 && raw <= 8 ? raw : 2;
|
|
32
|
+
})();
|
|
25
33
|
const KEY_SIZE = 32;
|
|
26
34
|
// Connect to (up to) this many bootstrap nodes as persistent TCP relays. It was
|
|
27
35
|
// 3 — but the pool picks the FIRST N in order, so a peer only met friends whose
|
|
@@ -212,14 +220,33 @@ export class TcpRelayPool extends EventEmitter {
|
|
|
212
220
|
return this.#relays.filter((r) => r.client?.state() === "connected").length;
|
|
213
221
|
}
|
|
214
222
|
/**
|
|
215
|
-
* Send an onion request
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
223
|
+
* Send an onion request through up to `fanout` connected relays.
|
|
224
|
+
*
|
|
225
|
+
* Each relay forwards the packet to node B over UDP and routes the onion
|
|
226
|
+
* response back over TCP (surfaced via the pool's "onionResponse" event).
|
|
227
|
+
* Returns how many relays accepted it (0 = none connected).
|
|
228
|
+
*
|
|
229
|
+
* **What fanout buys.** The SAME request is handed to several relays, so it
|
|
230
|
+
* is redundancy, not extra reach: if one relay is unreachable, overloaded, or
|
|
231
|
+
* simply cannot get to node B, another copy still arrives. The peer sees one
|
|
232
|
+
* request either way — duplicates are dropped by the onion layer.
|
|
233
|
+
*
|
|
234
|
+
* **What it costs.** Linear in every resource this path spends: N relays
|
|
235
|
+
* means N times the TCP traffic and N times the work at the relay end. It
|
|
236
|
+
* does NOT multiply local crypto — the onion packet is built once and reused,
|
|
237
|
+
* so the X25519 cost is paid once regardless of fanout.
|
|
238
|
+
*
|
|
239
|
+
* **Choosing a value.** `1` is cheapest and correct on a stable link where
|
|
240
|
+
* the first relay is reliable; a lost request just waits for the next sweep.
|
|
241
|
+
* `2` (the default) is the useful middle: it covers a single dead relay,
|
|
242
|
+
* which is the common failure, at double the traffic on a path that is
|
|
243
|
+
* already small. Above 3 the added redundancy is mostly wasted — you are
|
|
244
|
+
* paying for the case where three independent relays fail at once, which in
|
|
245
|
+
* practice means the network is down, not the relays.
|
|
246
|
+
*
|
|
247
|
+
* Override with `DECENT_ONION_RELAY_FANOUT`.
|
|
221
248
|
*/
|
|
222
|
-
sendOnionRequest(packet, fanout =
|
|
249
|
+
sendOnionRequest(packet, fanout = ONION_RELAY_FANOUT) {
|
|
223
250
|
let sent = 0;
|
|
224
251
|
for (const r of this.#relays) {
|
|
225
252
|
if (sent >= fanout)
|
package/dist/peer.js
CHANGED
|
@@ -968,7 +968,7 @@ export class Peer {
|
|
|
968
968
|
uint32ToLe(friendAddress.nospam),
|
|
969
969
|
appPayload
|
|
970
970
|
]);
|
|
971
|
-
const routes = await this.#discoverFriendRoutes(friendAddress.publicKey);
|
|
971
|
+
const routes = await this.#discoverFriendRoutes(friendAddress.publicKey, true);
|
|
972
972
|
this.#debugLog(`friend route discovery done routes=${routes.length}`);
|
|
973
973
|
if (routes.length === 0) {
|
|
974
974
|
// The direct net_crypto send needs a known endpoint for the target; to
|
|
@@ -2928,10 +2928,35 @@ export class Peer {
|
|
|
2928
2928
|
// Ignore invalid offline payloads.
|
|
2929
2929
|
}
|
|
2930
2930
|
}
|
|
2931
|
-
|
|
2931
|
+
/**
|
|
2932
|
+
* @param userInitiated skips the budget. Adding a friend must not wait on a
|
|
2933
|
+
* throttle meant for background upkeep.
|
|
2934
|
+
*/
|
|
2935
|
+
async #discoverFriendRoutes(friendPublicKey, userInitiated = false) {
|
|
2932
2936
|
if (!this.#keyPair) {
|
|
2933
2937
|
throw new Error("Peer is not started");
|
|
2934
2938
|
}
|
|
2939
|
+
// Same per-friend budget as #discoverAndCacheFriendEndpoint, enforced HERE
|
|
2940
|
+
// for the same reason: this is the THIRD time today the cost simply moved
|
|
2941
|
+
// to whichever caller was not gated. #sendOnionDhtPk's call sites were
|
|
2942
|
+
// throttled, so the sweep underneath them became the top crypto entry
|
|
2943
|
+
// instead — 17.9% of wall on gfax, climbing (20% -> 27% over ten minutes)
|
|
2944
|
+
// rather than settling, because the DHT-PK path kept a FLAT 25s interval
|
|
2945
|
+
// while the endpoint path had already been given backoff.
|
|
2946
|
+
//
|
|
2947
|
+
// A gate inside the function cannot be bypassed by a call site added later.
|
|
2948
|
+
const routeKey = `routes:${carrierIdFromPublicKey(friendPublicKey)}`;
|
|
2949
|
+
if (!userInitiated) {
|
|
2950
|
+
const misses = this.#onionLookupMisses.get(routeKey) ?? 0;
|
|
2951
|
+
const budget = Math.min(DHT_PK_ANNOUNCE_COOLDOWN_MS * Math.max(1, misses), ONION_LOOKUP_MAX_BACKOFF_MS);
|
|
2952
|
+
const since = Date.now() - (this.#onionLookupCooldown.get(routeKey) ?? 0);
|
|
2953
|
+
if (since < budget) {
|
|
2954
|
+
this.#debugLog(`route discovery for ${routeKey} skipped: ${Math.round(since / 1000)}s ` +
|
|
2955
|
+
`since last (budget ${Math.round(budget / 1000)}s, misses ${misses})`);
|
|
2956
|
+
return [];
|
|
2957
|
+
}
|
|
2958
|
+
this.#onionLookupCooldown.set(routeKey, Date.now());
|
|
2959
|
+
}
|
|
2935
2960
|
const searchKey = createEphemeralKeyPair();
|
|
2936
2961
|
const routes = [];
|
|
2937
2962
|
const routeSeen = new Set();
|
|
@@ -3075,6 +3100,15 @@ export class Peer {
|
|
|
3075
3100
|
}
|
|
3076
3101
|
}
|
|
3077
3102
|
}
|
|
3103
|
+
// Miss accounting drives the backoff above. Without it the budget stays at
|
|
3104
|
+
// its 25s floor forever and the gate does nothing for the friends that
|
|
3105
|
+
// actually cost us — the ones that never resolve.
|
|
3106
|
+
if (!userInitiated) {
|
|
3107
|
+
if (routes.length > 0)
|
|
3108
|
+
this.#onionLookupMisses.delete(routeKey);
|
|
3109
|
+
else
|
|
3110
|
+
this.#onionLookupMisses.set(routeKey, (this.#onionLookupMisses.get(routeKey) ?? 0) + 1);
|
|
3111
|
+
}
|
|
3078
3112
|
return routes;
|
|
3079
3113
|
}
|
|
3080
3114
|
async #discoverAndCacheFriendEndpoint(friendId, searchPublicKey) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.130",
|
|
4
4
|
"description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|