@decentnetwork/peer 0.1.29 → 0.1.31
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/dist/peer.js +105 -32
- 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.
|
|
@@ -582,7 +586,18 @@ export class Peer {
|
|
|
582
586
|
const routes = await this.#discoverFriendRoutes(friendAddress.publicKey);
|
|
583
587
|
this.#debugLog(`friend route discovery done routes=${routes.length}`);
|
|
584
588
|
if (routes.length === 0) {
|
|
585
|
-
|
|
589
|
+
// The direct net_crypto send needs a known endpoint for the target; to
|
|
590
|
+
// a peer we've never connected to (a dora, an exit) it throws. The old
|
|
591
|
+
// code `await`ed it bare, so that throw skipped the express push below
|
|
592
|
+
// and the friend-request never went out by ANY channel — the exact
|
|
593
|
+
// reason callpass never reached dora-mac. Make it best-effort so the
|
|
594
|
+
// express fallback always runs.
|
|
595
|
+
try {
|
|
596
|
+
await this.#sendDirectCryptoFriendRequest(friendAddress.publicKey, friendReqPayload);
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
this.#debugLog(`direct crypto friend-request to ${friendId} failed: ${error.message}`);
|
|
600
|
+
}
|
|
586
601
|
this.#lastFriendRequestDispatch = {
|
|
587
602
|
transport: "direct",
|
|
588
603
|
routes: 0,
|
|
@@ -2077,46 +2092,67 @@ export class Peer {
|
|
|
2077
2092
|
}
|
|
2078
2093
|
this.#lastSelfAnnounceMs = now;
|
|
2079
2094
|
const storedNodes = [];
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2095
|
+
// An onion announce only STORES on the nodes whose key is closest to OURS,
|
|
2096
|
+
// and toxcore reaches them by following the "here are closer nodes" lists
|
|
2097
|
+
// each node returns — a converging walk. The old code instead sorted by
|
|
2098
|
+
// reliability score and did a single pass, so it announced to far-but-
|
|
2099
|
+
// reliable nodes that just replied isStored=0; it never iterated toward the
|
|
2100
|
+
// closest nodes, so nothing ever stored us (selfAnnounceStoredOn=0) and we
|
|
2101
|
+
// were unreachable in real time. Walk a distance-ordered queue toward our
|
|
2102
|
+
// own key, feeding back the closer nodes each wave discovers.
|
|
2103
|
+
const selfPk = this.#keyPair.publicKey;
|
|
2084
2104
|
const zeroPing = new Uint8Array(32);
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2105
|
+
const visited = new Set();
|
|
2106
|
+
const queue = [];
|
|
2107
|
+
const enqueueNodes = (nodes) => {
|
|
2108
|
+
for (const node of nodes) {
|
|
2109
|
+
if (!node.pk)
|
|
2110
|
+
continue;
|
|
2111
|
+
const id = `${node.host}:${node.port}`;
|
|
2112
|
+
if (visited.has(id) || this.#isNodeBlacklisted(id))
|
|
2113
|
+
continue;
|
|
2114
|
+
if (queue.some((q) => `${q.node.host}:${q.node.port}` === id))
|
|
2115
|
+
continue;
|
|
2116
|
+
let nodePk;
|
|
2117
|
+
try {
|
|
2118
|
+
nodePk = base58ToBytes(node.pk);
|
|
2119
|
+
}
|
|
2120
|
+
catch {
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
if (nodePk.length !== 32)
|
|
2124
|
+
continue;
|
|
2125
|
+
queue.push({ node, nodePk, sendBack: randomBytes(8) });
|
|
2095
2126
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
}
|
|
2100
|
-
|
|
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.
|
|
2127
|
+
// Closest-to-self first, so each wave hits the nodes most likely to store
|
|
2128
|
+
// us or to point at even-closer ones.
|
|
2129
|
+
queue.sort((x, y) => xorCloser(selfPk, x.nodePk, y.nodePk));
|
|
2130
|
+
};
|
|
2131
|
+
enqueueNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes);
|
|
2106
2132
|
// Reset counter at start so dhtHealth can distinguish "never ran" (-1)
|
|
2107
|
-
// from "ran but stored on 0 nodes" (0).
|
|
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.
|
|
2133
|
+
// from "ran but stored on 0 nodes" (0).
|
|
2110
2134
|
this.#lastSelfAnnounceStoredCount = 0;
|
|
2111
|
-
|
|
2135
|
+
const STORE_TARGET = 4;
|
|
2136
|
+
let waves = 0;
|
|
2137
|
+
while (queue.length > 0 && storedNodes.length < STORE_TARGET && waves < 16) {
|
|
2112
2138
|
if (Date.now() >= deadlineMs) {
|
|
2113
2139
|
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
2140
|
this.#lastSelfAnnounceStoredCount = storedNodes.length;
|
|
2117
2141
|
return storedNodes;
|
|
2118
2142
|
}
|
|
2119
|
-
|
|
2143
|
+
waves += 1;
|
|
2144
|
+
// Pull the closest unvisited candidates for this wave.
|
|
2145
|
+
const wave = [];
|
|
2146
|
+
while (queue.length > 0 && wave.length < SELF_ANNOUNCE_BATCH_SIZE) {
|
|
2147
|
+
const c = queue.shift();
|
|
2148
|
+
const id = `${c.node.host}:${c.node.port}`;
|
|
2149
|
+
if (visited.has(id))
|
|
2150
|
+
continue;
|
|
2151
|
+
visited.add(id);
|
|
2152
|
+
wave.push(c);
|
|
2153
|
+
}
|
|
2154
|
+
if (wave.length === 0)
|
|
2155
|
+
break;
|
|
2120
2156
|
// Fire all step1 requests in parallel.
|
|
2121
2157
|
const step1Settled = await Promise.allSettled(wave.map((c) => this.#sendAnnounceAndWait({
|
|
2122
2158
|
node: c.node,
|
|
@@ -2184,6 +2220,9 @@ export class Peer {
|
|
|
2184
2220
|
const discovered = parsePackedNodes(final.nodes);
|
|
2185
2221
|
if (discovered.length > 0) {
|
|
2186
2222
|
this.#knownNodes = dedupeNodes([...this.#knownNodes, ...discovered]);
|
|
2223
|
+
// Feed the closer nodes this node pointed us at back into the walk —
|
|
2224
|
+
// this is the convergence that actually reaches a storing node.
|
|
2225
|
+
enqueueNodes(discovered);
|
|
2187
2226
|
}
|
|
2188
2227
|
}
|
|
2189
2228
|
}
|
|
@@ -2482,6 +2521,22 @@ export class Peer {
|
|
|
2482
2521
|
}
|
|
2483
2522
|
catch { /* skip malformed pubkey */ }
|
|
2484
2523
|
}
|
|
2524
|
+
// Re-SEND the friend-request to a friend we've never had accepted
|
|
2525
|
+
// (still "requested"). The original can be lost — the friend was
|
|
2526
|
+
// offline/restarting, a relay blipped, or onion had no return path.
|
|
2527
|
+
// When WE'RE online and they still haven't accepted, re-offer over the
|
|
2528
|
+
// live path (onion, then express fallback) on a cooldown. This is how
|
|
2529
|
+
// an always-online server (a dora, an exit) finally gets our request
|
|
2530
|
+
// after either side restarted, without anyone re-running a command.
|
|
2531
|
+
if (!friend.acceptedAt && friend.address) {
|
|
2532
|
+
const lastFr = this.#friendRequestResendCooldown.get(friendId) ?? 0;
|
|
2533
|
+
if (now - lastFr > 60_000) {
|
|
2534
|
+
this.#friendRequestResendCooldown.set(friendId, now);
|
|
2535
|
+
void this.sendFriendRequest(friend.address, friend.hello).catch((error) => {
|
|
2536
|
+
this.#debugLog(`re-send friend-request to ${friendId} failed: ${error.message}`);
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2485
2540
|
const dhtPk = session?.friendDhtPublicKey;
|
|
2486
2541
|
if (dhtPk) {
|
|
2487
2542
|
const found = await this.#discoverAndCacheFriendEndpoint(friendId, dhtPk).catch(() => false);
|
|
@@ -4123,6 +4178,24 @@ function bytesEqual(a, b) {
|
|
|
4123
4178
|
function createEphemeralKeyPair() {
|
|
4124
4179
|
return nacl.box.keyPair();
|
|
4125
4180
|
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Toxcore DHT XOR-distance comparator: returns <0 if node pubkey `a` is CLOSER
|
|
4183
|
+
* to `target` than `b` (so an ascending sort puts the closest nodes first).
|
|
4184
|
+
* Onion announces only store on the nodes closest to your own key, and onion
|
|
4185
|
+
* lookups converge toward the nodes closest to the searched key — so candidate
|
|
4186
|
+
* selection MUST be by XOR distance, not by reliability score. Compares the
|
|
4187
|
+
* 32-byte keys big-endian-wise (most significant differing byte decides).
|
|
4188
|
+
*/
|
|
4189
|
+
function xorCloser(target, a, b) {
|
|
4190
|
+
const n = Math.min(target.length, a.length, b.length);
|
|
4191
|
+
for (let i = 0; i < n; i++) {
|
|
4192
|
+
const da = (a[i] ^ target[i]) & 0xff;
|
|
4193
|
+
const db = (b[i] ^ target[i]) & 0xff;
|
|
4194
|
+
if (da !== db)
|
|
4195
|
+
return da - db;
|
|
4196
|
+
}
|
|
4197
|
+
return 0;
|
|
4198
|
+
}
|
|
4126
4199
|
function isAllZero(bytes) {
|
|
4127
4200
|
for (let i = 0; i < bytes.length; i++) {
|
|
4128
4201
|
if (bytes[i] !== 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
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",
|