@decentnetwork/peer 0.1.123 → 0.1.124

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.
Files changed (2) hide show
  1. package/dist/peer.js +100 -6
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -109,6 +109,15 @@ const FRIEND_PING_INTERVAL_MS = readEnvInt("DECENT_FRIEND_PING_INTERVAL_MS", 400
109
109
  // middle of toxcore's reactive range — fast enough to keep responsiveness,
110
110
  // slow enough not to burn CPU on idle peers. Tunable via env.
111
111
  const FRIEND_CONNECTION_LOOP_MS = readEnvInt("DECENT_FRIEND_CONNECTION_LOOP_MS", 250);
112
+ /** Minimum gap between onion DHT-PK announces to the SAME friend.
113
+ *
114
+ * One announce fans out to several nodes and costs three X25519 scalar mults
115
+ * per node (one per onion layer), so this budget is the difference between an
116
+ * idle node and a busy core. Two call sites in #doFriendConnections need it —
117
+ * they previously each spelled the interval out, drifted apart (25s vs the
118
+ * enclosing 3s re-punch cadence), and the cheap one silently set the pace for
119
+ * the expensive one. Shared here so they cannot diverge again. */
120
+ const DHT_PK_ANNOUNCE_COOLDOWN_MS = readEnvInt("DECENT_DHT_PK_ANNOUNCE_COOLDOWN_MS", 25_000);
112
121
  const FRIEND_TIMEOUT_MS = readEnvInt("DECENT_FRIEND_TIMEOUT_MS", 32000);
113
122
  // A session whose keys we've PROVEN (decrypted ≥1 real packet from the peer) is
114
123
  // NOT torn down at FRIEND_TIMEOUT_MS. A transient all-transport blackout — e.g.
@@ -379,6 +388,14 @@ export class Peer {
379
388
  // session entry exists yet so the connection loop does not flood DHT-PK
380
389
  // requests when route discovery keeps failing.
381
390
  #dhtPkSendCooldown = new Map();
391
+ /** Last ONION-routed endpoint lookup per friend. Separate from
392
+ * #dhtPkSendCooldown: they are two different expensive operations for the
393
+ * same friend, and sharing one timestamp would let whichever ran first
394
+ * suppress the other for a full budget. */
395
+ #onionLookupCooldown = new Map();
396
+ /** Consecutive failed endpoint lookups per (friend,target) — drives how wide
397
+ * the next fan-out goes. Cleared on success. */
398
+ #onionLookupMisses = new Map();
382
399
  // Per-friend cooldown for re-asserting the TCP-relay route toward an
383
400
  // unconnected friend (the "accepted friend that never connects" wedge —
384
401
  // requestRoute only fired once at startup and was never retried).
@@ -3060,12 +3077,53 @@ export class Peer {
3060
3077
  return false;
3061
3078
  }
3062
3079
  const targetId = carrierIdFromPublicKey(searchPublicKey);
3080
+ // Per-(friend, target) budget, enforced HERE rather than at the call sites.
3081
+ //
3082
+ // One run of this fans out to up to 24 nodes and each onion request costs
3083
+ // three X25519 scalar mults (one per layer) — up to ~72 per call, tens of
3084
+ // ms of pure CPU. There are four call sites and gating them one at a time
3085
+ // just moves the cost to whichever one was missed: after gating only the
3086
+ // re-punch path, this function was STILL the top crypto entry on
3087
+ // smtp.gfax.cn. A choke point inside the function is the only version that
3088
+ // cannot be bypassed by a call site added later.
3089
+ //
3090
+ // Keyed by target, not just friend, so the dhtPk lookup and the
3091
+ // onion-routed fallback for the same friend keep independent budgets —
3092
+ // throttling repeats of the SAME search, never the first try of a
3093
+ // different one.
3094
+ const budgetKey = `${friendId}:${targetId}`;
3095
+ const lastLookup = this.#onionLookupCooldown.get(budgetKey) ?? 0;
3096
+ const sinceLast = Date.now() - lastLookup;
3097
+ if (sinceLast < DHT_PK_ANNOUNCE_COOLDOWN_MS) {
3098
+ this.#debugLog(`endpoint lookup for ${friendId} skipped: ${Math.round(sinceLast / 1000)}s since last ` +
3099
+ `(budget ${DHT_PK_ANNOUNCE_COOLDOWN_MS / 1000}s)`);
3100
+ return false;
3101
+ }
3102
+ this.#onionLookupCooldown.set(budgetKey, Date.now());
3103
+ // Adaptive fan-out. Each node in this queue costs TWO announce round trips
3104
+ // (ping id, then the real query) and every onion request is three X25519
3105
+ // scalar mults — so a full 24-node sweep is ~144 scalar mults, and the loop
3106
+ // only exits early on SUCCESS. In steady state the lookups that repeat are
3107
+ // exactly the ones that keep failing, so the old fixed width paid the
3108
+ // maximum price precisely when it was least likely to pay off: measured
3109
+ // ~10-23 announce sends/sec on smtp.gfax.cn, ~28% of a core.
3110
+ //
3111
+ // First attempt keeps the full width — that is the one most likely to find
3112
+ // the peer, and discovery reliability is not what we are trading away.
3113
+ // Repeats narrow to a probe, and every 8th attempt goes wide again so a
3114
+ // peer that only just came back is still found without waiting for a
3115
+ // restart. Widening beats a permanently narrow sweep: the failure we must
3116
+ // not introduce is "never discovers", not "discovers a bit later".
3117
+ const misses = this.#onionLookupMisses.get(budgetKey) ?? 0;
3118
+ const fullWidth = Math.max(8, Math.min(24, MAX_FRIEND_ROUTE_ATTEMPTS));
3119
+ const width = misses === 0 || misses % 8 === 0 ? fullWidth : 4;
3063
3120
  const queue = dedupeNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes)
3064
3121
  .filter((n) => !this.#isNodeBlacklisted(`${n.host}:${n.port}`))
3065
3122
  .sort((a, b) => this.#nodeScore(`${b.host}:${b.port}`) - this.#nodeScore(`${a.host}:${a.port}`))
3066
- .slice(0, Math.max(8, Math.min(24, MAX_FRIEND_ROUTE_ATTEMPTS)));
3123
+ .slice(0, width);
3067
3124
  const directKnown = queue.find((node) => node.pk === targetId);
3068
3125
  if (directKnown) {
3126
+ this.#onionLookupMisses.delete(budgetKey);
3069
3127
  this.#cacheFriendRemote(friendId, directKnown.host, directKnown.port);
3070
3128
  this.#debugLog(`friend endpoint matched known node for ${friendId} at ${directKnown.host}:${directKnown.port}`);
3071
3129
  return true;
@@ -3122,11 +3180,15 @@ export class Peer {
3122
3180
  }
3123
3181
  const exact = discovered.find((n) => n.pk === targetId);
3124
3182
  if (exact) {
3183
+ this.#onionLookupMisses.delete(budgetKey);
3125
3184
  this.#cacheFriendRemote(friendId, exact.host, exact.port);
3126
3185
  this.#debugLog(`friend endpoint discovered for ${friendId} at ${exact.host}:${exact.port}`);
3127
3186
  return true;
3128
3187
  }
3129
3188
  }
3189
+ // Nothing found: remember, so the next sweep for this target probes
3190
+ // narrowly instead of paying the full 24-node price again.
3191
+ this.#onionLookupMisses.set(budgetKey, misses + 1);
3130
3192
  return false;
3131
3193
  }
3132
3194
  async #announceSelfBestEffort(force = false, deadlineMs = Number.POSITIVE_INFINITY) {
@@ -3547,9 +3609,29 @@ export class Peer {
3547
3609
  if (friendRealPk && friendRealPk.length === 32) {
3548
3610
  // 1. Re-announce our DHT-PK so peer learns *our* UDP endpoint
3549
3611
  // even if their own DHT lookup against us was stale.
3550
- void this.#sendOnionDhtPk(friendRealPk).catch((error) => {
3551
- this.#debugLog(`UDP retry: dhtpk_send for ${friendId} failed: ${error.message}`);
3552
- });
3612
+ //
3613
+ // This shares #dhtPkSendCooldown with the periodic branch
3614
+ // below ON PURPOSE. The two operations in this `if` have very
3615
+ // different costs and must not share the 3s cadence: the
3616
+ // endpoint offer above is one UDP packet, while an onion
3617
+ // DHT-PK announce fans out to several nodes and does THREE
3618
+ // X25519 scalar mults per node (one per onion layer). At 3s
3619
+ // per friend that dominated the CPU of any node with a few
3620
+ // idle relay friends — measured on smtp.gfax.cn: 82% of wall
3621
+ // clock inside nacl, 83% of it under createOnionRequest0,
3622
+ // against 2% on a node with none. Note the trigger is NOT a
3623
+ // broken friend: udpConfirmed needs UDP traffic within 4s, so
3624
+ // a perfectly healthy but idle relay friend qualifies forever.
3625
+ //
3626
+ // Keep the offer at 3s (cheap, and it is what actually
3627
+ // re-punches); let the expensive announce keep its 25s budget.
3628
+ const lastAnnounce = this.#dhtPkSendCooldown.get(friendId) ?? 0;
3629
+ if (now - lastAnnounce > DHT_PK_ANNOUNCE_COOLDOWN_MS) {
3630
+ this.#dhtPkSendCooldown.set(friendId, now);
3631
+ void this.#sendOnionDhtPk(friendRealPk).catch((error) => {
3632
+ this.#debugLog(`UDP retry: dhtpk_send for ${friendId} failed: ${error.message}`);
3633
+ });
3634
+ }
3553
3635
  // 2. Try to discover the peer's UDP endpoint and kick a
3554
3636
  // fresh cookie request via UDP. discoverAndCacheFriend
3555
3637
  // Endpoint accepts EITHER the DHT-PK (preferred —
@@ -3560,6 +3642,19 @@ export class Peer {
3560
3642
  // session.friendDhtPublicKey is null. Falling back
3561
3643
  // to friendRealPk keeps the retry from being a no-op
3562
3644
  // in exactly the case it was designed to fix.
3645
+ // COST NOTE: those two lookups are not the same price. With a
3646
+ // dhtPk it is a direct DHT query. Without one it is an
3647
+ // ONION-routed lookup — several nodes, three X25519 scalar
3648
+ // mults each — and the fallback fires precisely on
3649
+ // relay-only sessions, which are the long-lived ones. At the
3650
+ // 3s re-punch cadence that made it the single biggest CPU
3651
+ // consumer on a node with idle relay friends (measured on
3652
+ // smtp.gfax.cn: 31% of all crypto time entered here). So the
3653
+ // cheap direct lookup keeps the 3s cadence and only the onion
3654
+ // fallback is charged against the announce budget.
3655
+ // The per-(friend, target) budget now lives INSIDE
3656
+ // #discoverAndCacheFriendEndpoint, so this call site stays
3657
+ // simple and every other caller is covered by the same gate.
3563
3658
  const dhtPk = session.friendDhtPublicKey;
3564
3659
  const searchKey = dhtPk ?? friendRealPk;
3565
3660
  if (searchKey && searchKey.length === 32) {
@@ -3593,8 +3688,7 @@ export class Peer {
3593
3688
  // bootstrap nodes). Per-friend cooldown via #dhtPkSendCooldown so we
3594
3689
  // don't flood onion data requests every 5s loop tick.
3595
3690
  const lastDhtPkSent = this.#dhtPkSendCooldown.get(friendId) ?? 0;
3596
- const dhtPkInterval = 25_000;
3597
- if (now - lastDhtPkSent > dhtPkInterval) {
3691
+ if (now - lastDhtPkSent > DHT_PK_ANNOUNCE_COOLDOWN_MS) {
3598
3692
  let friendRealPk = session?.friendRealPublicKey;
3599
3693
  if (!friendRealPk && friend.address) {
3600
3694
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.123",
3
+ "version": "0.1.124",
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",