@decentnetwork/peer 0.1.28 → 0.1.30

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 +94 -32
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -193,6 +193,10 @@ export class Peer {
193
193
  // unconnected friend (the "accepted friend that never connects" wedge —
194
194
  // requestRoute only fired once at startup and was never retried).
195
195
  #routeRequestCooldown = new Map();
196
+ // Per-friend cooldown for RE-SENDING the friend-request to a friend that's
197
+ // never been accepted (still "requested"). On reconnect / restart we must
198
+ // re-offer — the original request can be lost while the friend was offline.
199
+ #friendRequestResendCooldown = new Map();
196
200
  // Per-friend consecutive "no routes available" count for DHT-PK so the
197
201
  // backoff grows if a friend stays unreachable, instead of retrying every
198
202
  // 25s forever for stale persisted entries.
@@ -652,7 +656,7 @@ export class Peer {
652
656
  throw new Error(`friend request dispatch failed: ${errors.join("; ")}`);
653
657
  }
654
658
  this.#lastFriendRequestDispatch = {
655
- transport: sent > 0 ? "onion" : "express",
659
+ transport: sent > 0 ? "onion" : "direct",
656
660
  routes: routes.length,
657
661
  targets: sent
658
662
  };
@@ -2077,46 +2081,67 @@ export class Peer {
2077
2081
  }
2078
2082
  this.#lastSelfAnnounceMs = now;
2079
2083
  const storedNodes = [];
2080
- const targets = dedupeNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes)
2081
- .filter((n) => !this.#isNodeBlacklisted(`${n.host}:${n.port}`))
2082
- .sort((a, b) => this.#nodeScore(`${b.host}:${b.port}`) - this.#nodeScore(`${a.host}:${a.port}`))
2083
- .slice(0, MAX_SELF_ANNOUNCE_TARGETS);
2084
+ // An onion announce only STORES on the nodes whose key is closest to OURS,
2085
+ // and toxcore reaches them by following the "here are closer nodes" lists
2086
+ // each node returns a converging walk. The old code instead sorted by
2087
+ // reliability score and did a single pass, so it announced to far-but-
2088
+ // reliable nodes that just replied isStored=0; it never iterated toward the
2089
+ // closest nodes, so nothing ever stored us (selfAnnounceStoredOn=0) and we
2090
+ // were unreachable in real time. Walk a distance-ordered queue toward our
2091
+ // own key, feeding back the closer nodes each wave discovers.
2092
+ const selfPk = this.#keyPair.publicKey;
2084
2093
  const zeroPing = new Uint8Array(32);
2085
- const candidates = [];
2086
- for (const node of targets) {
2087
- if (!node.pk)
2088
- continue;
2089
- let nodePk;
2090
- try {
2091
- nodePk = base58ToBytes(node.pk);
2092
- }
2093
- catch {
2094
- continue;
2094
+ const visited = new Set();
2095
+ const queue = [];
2096
+ const enqueueNodes = (nodes) => {
2097
+ for (const node of nodes) {
2098
+ if (!node.pk)
2099
+ continue;
2100
+ const id = `${node.host}:${node.port}`;
2101
+ if (visited.has(id) || this.#isNodeBlacklisted(id))
2102
+ continue;
2103
+ if (queue.some((q) => `${q.node.host}:${q.node.port}` === id))
2104
+ continue;
2105
+ let nodePk;
2106
+ try {
2107
+ nodePk = base58ToBytes(node.pk);
2108
+ }
2109
+ catch {
2110
+ continue;
2111
+ }
2112
+ if (nodePk.length !== 32)
2113
+ continue;
2114
+ queue.push({ node, nodePk, sendBack: randomBytes(8) });
2095
2115
  }
2096
- if (nodePk.length !== 32)
2097
- continue;
2098
- candidates.push({ node, nodePk, sendBack: randomBytes(8) });
2099
- }
2100
- // Walk in waves fire SELF_ANNOUNCE_BATCH_SIZE step1+step2 sequences
2101
- // in parallel per wave, then process. Toxcore's onion_client.c does
2102
- // up to MAX_ONION_CLIENTS_ANNOUNCE = 12 simultaneously, which lets it
2103
- // stabilize self-announce in ~10s on a healthy network. Old serial
2104
- // walk took (per-node-timeout × N targets × 2 steps) ≈ 16 × 5s × 2
2105
- // = 160s worst case before a single isStored=2 response.
2116
+ // Closest-to-self first, so each wave hits the nodes most likely to store
2117
+ // us or to point at even-closer ones.
2118
+ queue.sort((x, y) => xorCloser(selfPk, x.nodePk, y.nodePk));
2119
+ };
2120
+ enqueueNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes);
2106
2121
  // Reset counter at start so dhtHealth can distinguish "never ran" (-1)
2107
- // from "ran but stored on 0 nodes" (0). The latter means our UDP
2108
- // path is reaching bootstrap nodes but they're refusing to store
2109
- // us — that's a wire-level problem (auth, network) we want to see.
2122
+ // from "ran but stored on 0 nodes" (0).
2110
2123
  this.#lastSelfAnnounceStoredCount = 0;
2111
- for (let i = 0; i < candidates.length; i += SELF_ANNOUNCE_BATCH_SIZE) {
2124
+ const STORE_TARGET = 4;
2125
+ let waves = 0;
2126
+ while (queue.length > 0 && storedNodes.length < STORE_TARGET && waves < 16) {
2112
2127
  if (Date.now() >= deadlineMs) {
2113
2128
  this.#debugLog("self announce stopped at deadline");
2114
- // Even on early return, the count is live: storedNodes is what
2115
- // we got so far.
2116
2129
  this.#lastSelfAnnounceStoredCount = storedNodes.length;
2117
2130
  return storedNodes;
2118
2131
  }
2119
- const wave = candidates.slice(i, i + SELF_ANNOUNCE_BATCH_SIZE);
2132
+ waves += 1;
2133
+ // Pull the closest unvisited candidates for this wave.
2134
+ const wave = [];
2135
+ while (queue.length > 0 && wave.length < SELF_ANNOUNCE_BATCH_SIZE) {
2136
+ const c = queue.shift();
2137
+ const id = `${c.node.host}:${c.node.port}`;
2138
+ if (visited.has(id))
2139
+ continue;
2140
+ visited.add(id);
2141
+ wave.push(c);
2142
+ }
2143
+ if (wave.length === 0)
2144
+ break;
2120
2145
  // Fire all step1 requests in parallel.
2121
2146
  const step1Settled = await Promise.allSettled(wave.map((c) => this.#sendAnnounceAndWait({
2122
2147
  node: c.node,
@@ -2184,6 +2209,9 @@ export class Peer {
2184
2209
  const discovered = parsePackedNodes(final.nodes);
2185
2210
  if (discovered.length > 0) {
2186
2211
  this.#knownNodes = dedupeNodes([...this.#knownNodes, ...discovered]);
2212
+ // Feed the closer nodes this node pointed us at back into the walk —
2213
+ // this is the convergence that actually reaches a storing node.
2214
+ enqueueNodes(discovered);
2187
2215
  }
2188
2216
  }
2189
2217
  }
@@ -2482,6 +2510,22 @@ export class Peer {
2482
2510
  }
2483
2511
  catch { /* skip malformed pubkey */ }
2484
2512
  }
2513
+ // Re-SEND the friend-request to a friend we've never had accepted
2514
+ // (still "requested"). The original can be lost — the friend was
2515
+ // offline/restarting, a relay blipped, or onion had no return path.
2516
+ // When WE'RE online and they still haven't accepted, re-offer over the
2517
+ // live path (onion, then express fallback) on a cooldown. This is how
2518
+ // an always-online server (a dora, an exit) finally gets our request
2519
+ // after either side restarted, without anyone re-running a command.
2520
+ if (!friend.acceptedAt && friend.address) {
2521
+ const lastFr = this.#friendRequestResendCooldown.get(friendId) ?? 0;
2522
+ if (now - lastFr > 60_000) {
2523
+ this.#friendRequestResendCooldown.set(friendId, now);
2524
+ void this.sendFriendRequest(friend.address, friend.hello).catch((error) => {
2525
+ this.#debugLog(`re-send friend-request to ${friendId} failed: ${error.message}`);
2526
+ });
2527
+ }
2528
+ }
2485
2529
  const dhtPk = session?.friendDhtPublicKey;
2486
2530
  if (dhtPk) {
2487
2531
  const found = await this.#discoverAndCacheFriendEndpoint(friendId, dhtPk).catch(() => false);
@@ -4123,6 +4167,24 @@ function bytesEqual(a, b) {
4123
4167
  function createEphemeralKeyPair() {
4124
4168
  return nacl.box.keyPair();
4125
4169
  }
4170
+ /**
4171
+ * Toxcore DHT XOR-distance comparator: returns <0 if node pubkey `a` is CLOSER
4172
+ * to `target` than `b` (so an ascending sort puts the closest nodes first).
4173
+ * Onion announces only store on the nodes closest to your own key, and onion
4174
+ * lookups converge toward the nodes closest to the searched key — so candidate
4175
+ * selection MUST be by XOR distance, not by reliability score. Compares the
4176
+ * 32-byte keys big-endian-wise (most significant differing byte decides).
4177
+ */
4178
+ function xorCloser(target, a, b) {
4179
+ const n = Math.min(target.length, a.length, b.length);
4180
+ for (let i = 0; i < n; i++) {
4181
+ const da = (a[i] ^ target[i]) & 0xff;
4182
+ const db = (b[i] ^ target[i]) & 0xff;
4183
+ if (da !== db)
4184
+ return da - db;
4185
+ }
4186
+ return 0;
4187
+ }
4126
4188
  function isAllZero(bytes) {
4127
4189
  for (let i = 0; i < bytes.length; i++) {
4128
4190
  if (bytes[i] !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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",