@decentnetwork/peer 0.1.98 → 0.1.100

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.
@@ -108,6 +108,8 @@ const LAN_SAMPLE_MAX_GAIN = 1.25;
108
108
  const LAN_STARTUP_MAX_GAIN = 1.15;
109
109
  const LAN_STARTUP_DELIVERY_GAIN = 1.5; // never probe above 1.5x recently delivered goodput
110
110
  const LAN_STARTUP_QUEUE_MS = 500; // unmistakable raw queue spike; smaller app-level RTT outliers are normal on Windows
111
+ const LAN_STARTUP_HARD_QUEUE_MS = 2000; // one multi-second queue sample is unsafe
112
+ const LAN_STARTUP_QUEUE_ROUNDS = 2; // tolerate one scheduler/session-handover outlier on LAN
111
113
  const LAN_STARTUP_KEEPUP = 0.70; // delivered goodput below 70% of pace means the receiver is no longer keeping up
112
114
  const LAN_STARTUP_SLOW_ROUNDS = 5; // require sustained saturation across Windows scheduler/ACK jitter
113
115
  // FILE ACKs are coalesced and handled by the remote JS event loop. On Windows
@@ -250,7 +252,7 @@ export class FileTransferManager {
250
252
  cwnd: CWND_INIT_CHUNKS, stalls: 0, parityHighBlock: -1, lastLossMs: 0, lossEwma: 0,
251
253
  minRttMs: Infinity, srttMs: 0, probeOffset: -1, probeSentMs: 0,
252
254
  paceRateBps: PACE_INIT_BPS, paceTokens: 0, lastPaceMs: nowMs(),
253
- btlBwBps: 0, bwSamples: [], fullBwRounds: 0, bbrPhase: 0, roundCount: 0,
255
+ btlBwBps: 0, bwSamples: [], fullBwRounds: 0, startupQueueRounds: 0, bbrPhase: 0, roundCount: 0,
254
256
  rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
255
257
  lanPath: this.isLanPath(friendId),
256
258
  };
@@ -787,6 +789,7 @@ export class FileTransferManager {
787
789
  // physical-LAN endpoint. Restart STARTUP from the measured rate.
788
790
  st.bbrPhase = 0;
789
791
  st.fullBwRounds = 0;
792
+ st.startupQueueRounds = 0;
790
793
  }
791
794
  st.lanPath = lanNow;
792
795
  if (!lanNow) {
@@ -817,7 +820,19 @@ export class FileTransferManager {
817
820
  st.fullBwRounds++;
818
821
  }
819
822
  const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
820
- const startupOvershot = rawQueue > LAN_STARTUP_QUEUE_MS;
823
+ if (rawQueue > LAN_STARTUP_QUEUE_MS) {
824
+ st.startupQueueRounds++;
825
+ }
826
+ else {
827
+ st.startupQueueRounds = 0;
828
+ }
829
+ // File acceptance, an event-loop pause, or a relay-to-LAN
830
+ // handover can produce one 500-1000 ms ACK on Windows/macOS.
831
+ // One such sample is not evidence that a paced LAN is full.
832
+ // Require persistence, while retaining an immediate brake for
833
+ // a genuinely dangerous multi-second queue.
834
+ const startupOvershot = rawQueue > LAN_STARTUP_HARD_QUEUE_MS ||
835
+ st.startupQueueRounds >= LAN_STARTUP_QUEUE_ROUNDS;
821
836
  const startupUnderDelivering = st.fullBwRounds >= LAN_STARTUP_SLOW_ROUNDS;
822
837
  if (startupUnderDelivering || startupOvershot) {
823
838
  st.bbrPhase = 2;
@@ -828,6 +843,7 @@ export class FileTransferManager {
828
843
  // the sender continue flooding for ~2 s and collapse cwnd.
829
844
  st.bwSamples = measuredRate > 0 ? [measuredRate] : [];
830
845
  st.btlBwBps = measuredRate > 0 ? measuredRate : PACE_INIT_BPS;
846
+ st.startupQueueRounds = 0;
831
847
  st.paceRateBps = Math.max(PACE_INIT_BPS, Math.min(st.paceRateBps * 0.8, st.btlBwBps * 1.1));
832
848
  }
833
849
  else if (st.bbrPhase === 0) {
package/dist/peer.js CHANGED
@@ -1893,10 +1893,33 @@ export class Peer {
1893
1893
  // breaker" churned because it accepted new keys with NO such evidence).
1894
1894
  const peerReKeyed = state.undecryptableRecvMs !== undefined &&
1895
1895
  state.undecryptableRecvMs > (state.lastPingRecvMs ?? 0);
1896
- if (sinceEstablished > 1000 && currentSessionAlive && !peerReKeyed) {
1897
- this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live established session, ${sinceEstablished}ms after establish)`);
1896
+ const now = Date.now();
1897
+ const sameRehsKey = state.rehsSessionPublicKey !== undefined &&
1898
+ bytesEqual(state.rehsSessionPublicKey, hs.sessionPublicKey);
1899
+ const rehsWindowExpired = state.rehsLastMs === undefined ||
1900
+ now - state.rehsLastMs > REHS_WINDOW_MS;
1901
+ if (!sameRehsKey || rehsWindowExpired) {
1902
+ state.rehsSessionPublicKey = hs.sessionPublicKey.slice();
1903
+ state.rehsCount = 1;
1904
+ state.rehsFirstMs = now;
1905
+ }
1906
+ else {
1907
+ state.rehsCount = (state.rehsCount ?? 0) + 1;
1908
+ }
1909
+ state.rehsLastMs = now;
1910
+ const rehsElapsed = now - (state.rehsFirstMs ?? now);
1911
+ const persistentRehs = (state.rehsCount ?? 0) >= REHS_ACCEPT_COUNT &&
1912
+ rehsElapsed >= REHS_ACCEPT_MS &&
1913
+ rehsElapsed <= REHS_WINDOW_MS;
1914
+ if (sinceEstablished > 1000 && currentSessionAlive && !peerReKeyed && !persistentRehs) {
1915
+ this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live established session, ` +
1916
+ `${sinceEstablished}ms after establish, attempts=${state.rehsCount ?? 0})`);
1898
1917
  return;
1899
1918
  }
1919
+ if (persistentRehs) {
1920
+ this.#debugLog(`hs_recv accepting persistent re-handshake friend=${friendId} ` +
1921
+ `(attempts=${state.rehsCount}, elapsed=${rehsElapsed}ms)`);
1922
+ }
1900
1923
  if (peerReKeyed) {
1901
1924
  this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (peer re-keyed: undecryptable data since last good packet)`);
1902
1925
  }
@@ -1908,6 +1931,10 @@ export class Peer {
1908
1931
  state.sessionSharedKey = nacl.box.before(hs.sessionPublicKey, state.ourSessionKeyPair.secretKey);
1909
1932
  state.established = true;
1910
1933
  state.sessionEstablishedAtMs = Date.now();
1934
+ state.rehsCount = undefined;
1935
+ state.rehsFirstMs = undefined;
1936
+ state.rehsLastMs = undefined;
1937
+ state.rehsSessionPublicKey = undefined;
1911
1938
  // Fresh session up — reset the higher-pubkey defer window for the
1912
1939
  // next outage (see #initiateSession).
1913
1940
  this.#initiateDeferSinceMs.delete(friendId);
@@ -1936,7 +1963,13 @@ export class Peer {
1936
1963
  state.sendPacketNumber ??= 0;
1937
1964
  state.receiveBufferStart ??= 0;
1938
1965
  }
1939
- state.lastPingRecvMs = Date.now();
1966
+ // A handshake authenticates identities and proposes keys, but it does not
1967
+ // prove that CRYPTO_DATA encrypted under those keys works in both
1968
+ // directions. Keep a fresh session "unproven" until the first packet is
1969
+ // actually decrypted; otherwise a half-open session receives the 180s
1970
+ // proven-session blackout grace instead of the 32s recovery deadline.
1971
+ if (isNewSession)
1972
+ state.lastPingRecvMs = undefined;
1940
1973
  state.pendingEcho = undefined;
1941
1974
  state.pendingCookiePeerDhtPublicKey = undefined;
1942
1975
  this.#debugLog(`hs_recv friend=${friendId} initiated=${weInitiated ? 1 : 0} remote=${remote.address}:${remote.port}`);
@@ -2084,6 +2117,13 @@ export class Peer {
2084
2117
  // stays unset → udpFresh false → bulk data (files) never ride the LAN
2085
2118
  // even when LAN UDP is flowing both ways (the snoopy slow-file bug).
2086
2119
  state.lastPingRecvMs = Date.now();
2120
+ // Any successfully decrypted packet proves the current session is
2121
+ // healthy. Forget rejected new-key proposals so late/replayed
2122
+ // handshakes can never accumulate across healthy traffic.
2123
+ state.rehsCount = undefined;
2124
+ state.rehsFirstMs = undefined;
2125
+ state.rehsLastMs = undefined;
2126
+ state.rehsSessionPublicKey = undefined;
2087
2127
  if (this.#remoteIsTcp(remote)) {
2088
2128
  state.hasTcpRoute = true;
2089
2129
  }
@@ -2932,6 +2972,18 @@ export class Peer {
2932
2972
  // sessions a long grace so a blackout doesn't churn them via re-handshake;
2933
2973
  // unproven sessions still time out fast to self-heal a wedged handshake.
2934
2974
  const proven = session.lastPingRecvMs !== undefined;
2975
+ // A proven session normally gets a long blackout grace: if the network
2976
+ // disappears, keeping the known-good keys avoids unnecessary churn.
2977
+ // A restarted peer is different. Once our last good decrypt goes quiet
2978
+ // while fresh packets from that same friend keep failing authentication,
2979
+ // the transport is alive but the keys are not. Waiting the full proven
2980
+ // timeout turns every peer restart into a multi-minute wedge. Require a
2981
+ // sustained good-packet silence plus recent desync evidence so a single
2982
+ // late relay duplicate can never tear down a healthy session.
2983
+ const recentUndecryptable = session.undecryptableRecvMs !== undefined &&
2984
+ session.undecryptableRecvMs > lastAlive &&
2985
+ now - session.undecryptableRecvMs <= REINIT_STUCK_MS;
2986
+ const rekeyStuck = recentUndecryptable && silentFor > REINIT_ON_DESYNC_MS;
2935
2987
  // A relay-only session (synthetic tcp: remote, no UDP path) whose
2936
2988
  // relay route is gone has NO transport at all — nothing can arrive
2937
2989
  // and nothing we send leaves. The proven-session blackout grace
@@ -2942,7 +2994,23 @@ export class Peer {
2942
2994
  const relayOnly = !session.remote || session.remote.host.startsWith("tcp:");
2943
2995
  const pathless = relayOnly && !session.hasTcpRoute;
2944
2996
  const deadline = proven && !pathless ? PROVEN_SESSION_HARD_TIMEOUT_MS : FRIEND_TIMEOUT_MS;
2945
- if (proven && !pathless && silentFor > FRIEND_TIMEOUT_MS && silentFor <= deadline) {
2997
+ if (rekeyStuck) {
2998
+ this.#debugLog(`session re-key desync for ${friendId} (no good decrypt in ${silentFor}ms; ` +
2999
+ `undecryptable packets still arriving) — tearing down to re-handshake`);
3000
+ this.#friendSessions.delete(friendId);
3001
+ this.#setFriendOffline(friendId);
3002
+ try {
3003
+ const pk = base58ToBytes(friend.pubkey);
3004
+ if (pk.length === 32)
3005
+ this.#tcpRelays?.requestRoute(pk);
3006
+ const dhtPk = this.#friendDhtKeys.get(friendId);
3007
+ if (dhtPk && dhtPk.length === 32)
3008
+ this.#tcpRelays?.requestRoute(dhtPk);
3009
+ }
3010
+ catch { /* skip malformed */ }
3011
+ continue;
3012
+ }
3013
+ else if (proven && !pathless && silentFor > FRIEND_TIMEOUT_MS && silentFor <= deadline) {
2946
3014
  // Blackout grace: keep the proven session and keep probing (the
2947
3015
  // keepalive + UDP re-punch + relay-keepalive below all still run). Do
2948
3016
  // NOT delete or re-handshake — that's what desyncs and churns. The peer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.98",
3
+ "version": "0.1.100",
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",