@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.
- package/dist/peer.js +94 -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.
|
|
@@ -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" : "
|
|
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
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
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
|
-
|
|
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.
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|