@dcasegr/skein 0.0.0-stage → 0.1.1
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/README.md +522 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +773 -0
- package/dist/host/router.js +2389 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
package/dist/host/p2p.js
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
// The router's libp2p host (#43, #51): a router component, like the HTTP
|
|
2
|
+
// proxy and the feeds. The runtime has no network; this is where the sockets
|
|
3
|
+
// are. One js-libp2p node per peer KEY (#160) for the instances that declare
|
|
4
|
+
// libp2p — in their config (the genesis's `libp2p`, from etc/config.json), or
|
|
5
|
+
// by a `libp2p` row in the kernel's dispatch table (#72, #77: a topic, or a
|
|
6
|
+
// `/<protocol>`) — all inside the router process: a master-key host derives a
|
|
7
|
+
// key per instance, so one node per instance; a one-key host's instances share
|
|
8
|
+
// the one peer key, so one node carries them all ("the host" below):
|
|
9
|
+
//
|
|
10
|
+
// peer key a secp256k1 key derived from the INSTANCE's root key with BRC-42/43
|
|
11
|
+
// (#129: [2, "skein instance"], key ID `libp2p:<handle>`, self — signer.ts
|
|
12
|
+
// peerKey): the key the instance's signer answers for getPublicKey with
|
|
13
|
+
// those arguments, so a program computes its own peer ID; a child, not
|
|
14
|
+
// the root. The peer ID is the identity
|
|
15
|
+
// multihash of the key's protobuf (type secp256k1, the 33-byte
|
|
16
|
+
// compressed key), so the key reads straight out of the peer ID.
|
|
17
|
+
// node GossipSub (StrictSign: every message is signed by its publisher's
|
|
18
|
+
// peer key), noise + yamux, TCP and WebSocket listeners (WSS with a
|
|
19
|
+
// certificate), Kademlia DHT off | client | server, mDNS on/off,
|
|
20
|
+
// bootstrap peers kept connected, circuit relays.
|
|
21
|
+
// topics the instance's `libp2p.topics`, its dispatch table's libp2p
|
|
22
|
+
// rows' topics (libp2pConfig) and the topics its apps subscribed
|
|
23
|
+
// (#119: `subscribe {topic, program, fn}` / `unsubscribe {topic}`,
|
|
24
|
+
// emitted by an app's installed program; the subscriptions folded
|
|
25
|
+
// from the log, subscriptionsOf, then followed live): subscribed; each message is judged
|
|
26
|
+
// by the async topic validator, which makes one front-door call
|
|
27
|
+
// (`inbound`) and returns its verdict to GossipSub (accept: admit +
|
|
28
|
+
// forward; reject: drop + penalise the delivering peer; ignore: drop).
|
|
29
|
+
// A redelivered message already admitted (its `p2p` event record is
|
|
30
|
+
// in the kernel's `unique` map) is refused at admit, nothing written:
|
|
31
|
+
// ignore.
|
|
32
|
+
// A topic only an app's liveness takes (#138, liveness.ts) is
|
|
33
|
+
// subscribed too, but its messages are never admitted: each is
|
|
34
|
+
// judged by the liveness tool alone (a beat whose instance
|
|
35
|
+
// signature verifies is kept and accepted, a bad one rejected).
|
|
36
|
+
// With the DHT on, each topic name is a rendezvous: provide the
|
|
37
|
+
// CID v1 raw sha2-256 of the name and dial the providers found
|
|
38
|
+
// (go-libp2p's RoutingDiscovery, as go-p2p-message-bus uses it).
|
|
39
|
+
// protocols the instance's `libp2p.protocols` and its dispatch table's
|
|
40
|
+
// libp2p rows' protocols (`/…`): for each inbound stream the
|
|
41
|
+
// router reads length-prefixed frames (unsigned varint), makes one
|
|
42
|
+
// front-door call per frame and writes the answer's body back.
|
|
43
|
+
// Every node also serves /skein/message/1.0.0 (#70): a frame there is
|
|
44
|
+
// a signed message to the instance, its package {message, body}; the
|
|
45
|
+
// front door checks and routes it, no route needed.
|
|
46
|
+
// changes `declare` again with the instance's config as it now stands (the
|
|
47
|
+
// router does when the dispatch table changed: an install or an
|
|
48
|
+
// uninstall added or removed libp2p rows, #72/#77): new topics are
|
|
49
|
+
// subscribed and new protocols handled; gone ones unsubscribed and
|
|
50
|
+
// unhandled; no topic or protocol left: the node stops. No restart.
|
|
51
|
+
// outbound the `libp2p` provider's work (#70, providers.ts): publish, dial,
|
|
52
|
+
// send, close for the messages an instance emits to it, and each frame
|
|
53
|
+
// read from a dialled stream handed back (`frames`) to become a
|
|
54
|
+
// message to the instance — the next frame arrives as an entry.
|
|
55
|
+
//
|
|
56
|
+
// Host-wide settings (hostP2PConfig): SKEIN_LIBP2P_LISTEN, _BOOTSTRAP, _RELAYS
|
|
57
|
+
// (comma-separated multiaddrs), _DHT (off | client | server), _MDNS (on | off),
|
|
58
|
+
// _TLS_CERT / _TLS_KEY (PEM files: the WebSocket listener serves TLS, so
|
|
59
|
+
// browsers can dial /tls/ws); from the environment, else $SKEIN_HOME/host.env.
|
|
60
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
61
|
+
import { join } from "node:path";
|
|
62
|
+
import { noise } from "@chainsafe/libp2p-noise";
|
|
63
|
+
import { yamux } from "@chainsafe/libp2p-yamux";
|
|
64
|
+
import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
|
|
65
|
+
import { privateKeyFromRaw } from "@libp2p/crypto/keys";
|
|
66
|
+
import { gossipsub, TopicValidatorResult } from "@libp2p/gossipsub";
|
|
67
|
+
import { identify } from "@libp2p/identify";
|
|
68
|
+
import { kadDHT, passthroughMapper, removePrivateAddressesMapper } from "@libp2p/kad-dht";
|
|
69
|
+
import { mdns } from "@libp2p/mdns";
|
|
70
|
+
import { peerIdFromPrivateKey, peerIdFromString } from "@libp2p/peer-id";
|
|
71
|
+
import { ping } from "@libp2p/ping";
|
|
72
|
+
import { tcp } from "@libp2p/tcp";
|
|
73
|
+
import { isPrivate, lpStream } from "@libp2p/utils";
|
|
74
|
+
import { webSockets } from "@libp2p/websockets";
|
|
75
|
+
import { multiaddr } from "@multiformats/multiaddr";
|
|
76
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
77
|
+
import { createLibp2p } from "libp2p";
|
|
78
|
+
import { CID } from "multiformats/cid";
|
|
79
|
+
import { sha256 } from "multiformats/hashes/sha2";
|
|
80
|
+
import { MESSAGE_KEY_ID, MESSAGE_PROTOCOL as SIGN_PROTOCOL } from "./providers.js";
|
|
81
|
+
import { verifyAnyone } from "../runtime/identity.js";
|
|
82
|
+
import { Liveness } from "./liveness.js";
|
|
83
|
+
/** The default listeners: loopback TCP and WebSocket, ports chosen by the OS (plain WS on loopback: dev only). */
|
|
84
|
+
export const DEFAULT_LISTEN = ["/ip4/127.0.0.1/tcp/0", "/ip4/127.0.0.1/tcp/0/ws"];
|
|
85
|
+
/** The stream protocol a signed message travels on to an instance (#70). */
|
|
86
|
+
export const MESSAGE_PROTOCOL = "/skein/message/1.0.0";
|
|
87
|
+
/**
|
|
88
|
+
* The overlay's receive row (skein-overlay etc/app.json; the settled sync design, David 2026-10-30):
|
|
89
|
+
* the host's OWN entry for the BEEFs it fetched (sync.ts `deliver` → Router.p2pInbound), never served
|
|
90
|
+
* to peers ("not a good door to have": a node takes BEEFs only from paths it chose). Whatever rows an
|
|
91
|
+
* instance declares, no node handles it inbound.
|
|
92
|
+
*/
|
|
93
|
+
export const RECEIVE_PROTOCOL = "/skein/overlay/beef/1.0.0";
|
|
94
|
+
/** Whether a node handles `protocol` inbound for its handles' rows: not the receive row, not the host's own. */
|
|
95
|
+
const inboundRow = (protocol, host) => protocol !== MESSAGE_PROTOCOL && protocol !== RECEIVE_PROTOCOL && !(protocol in host);
|
|
96
|
+
// ---------------------------------------------------------------- keys and IDs
|
|
97
|
+
/** A bsv secp256k1 key as a libp2p private key. */
|
|
98
|
+
export function libp2pKey(k) {
|
|
99
|
+
return privateKeyFromRaw(Uint8Array.from(k.toArray("be", 32)));
|
|
100
|
+
}
|
|
101
|
+
/** The peer ID of a key: the identity multihash of its protobuf (secp256k1, compressed). */
|
|
102
|
+
export function peerIdOf(k) {
|
|
103
|
+
return peerIdFromPrivateKey(libp2pKey(k));
|
|
104
|
+
}
|
|
105
|
+
/** The compressed secp256k1 key a peer ID embeds (hex), or undefined for another kind. */
|
|
106
|
+
export function keyOfPeerId(id) {
|
|
107
|
+
const b = typeof id === "string" ? peerIdFromString(id).toMultihash().bytes : id;
|
|
108
|
+
// identity multihash (0x00, 0x25) over protobuf {1: KeyType = 2 (secp256k1), 2: 33 bytes}
|
|
109
|
+
if (b.length !== 39 || b[0] !== 0x00 || b[1] !== 0x25 || b[2] !== 0x08 || b[3] !== 0x02 || b[4] !== 0x12 || b[5] !== 0x21)
|
|
110
|
+
return undefined;
|
|
111
|
+
return Buffer.from(b.subarray(6)).toString("hex");
|
|
112
|
+
}
|
|
113
|
+
/** A topic name's rendezvous CID: v1 raw over sha2-256 of the name (go-libp2p's nsToCid). */
|
|
114
|
+
export async function topicCid(topic) {
|
|
115
|
+
return CID.createV1(0x55, await sha256.digest(new TextEncoder().encode(topic)));
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------- config
|
|
118
|
+
const list = (s) => (s ?? "").split(/[,\s]+/).map((x) => x.trim()).filter(Boolean);
|
|
119
|
+
/** The host-wide settings: the environment's SKEIN_LIBP2P_*, else $SKEIN_HOME/host.env's. */
|
|
120
|
+
export function hostP2PConfig(vars, home) {
|
|
121
|
+
const file = {};
|
|
122
|
+
const envFile = home ? join(home, "host.env") : undefined;
|
|
123
|
+
if (envFile && existsSync(envFile)) {
|
|
124
|
+
for (const line of readFileSync(envFile, "utf8").split("\n")) {
|
|
125
|
+
const m = /^\s*(?:export\s+)?(SKEIN_LIBP2P_[A-Z_]+)=(.*)$/.exec(line);
|
|
126
|
+
if (m)
|
|
127
|
+
file[m[1]] = m[2].trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const get = (k) => vars[k] ?? file[k];
|
|
131
|
+
const dht = (get("SKEIN_LIBP2P_DHT") || "off").toLowerCase();
|
|
132
|
+
if (dht !== "off" && dht !== "client" && dht !== "server")
|
|
133
|
+
throw new Error(`SKEIN_LIBP2P_DHT: off | client | server, not ${dht}`);
|
|
134
|
+
const cert = get("SKEIN_LIBP2P_TLS_CERT"), key = get("SKEIN_LIBP2P_TLS_KEY");
|
|
135
|
+
const listen = list(get("SKEIN_LIBP2P_LISTEN"));
|
|
136
|
+
return {
|
|
137
|
+
listen: listen.length ? listen : DEFAULT_LISTEN,
|
|
138
|
+
bootstrap: list(get("SKEIN_LIBP2P_BOOTSTRAP")),
|
|
139
|
+
dht,
|
|
140
|
+
relays: list(get("SKEIN_LIBP2P_RELAYS")),
|
|
141
|
+
mdns: /^(on|1|true|yes)$/i.test(get("SKEIN_LIBP2P_MDNS") ?? ""),
|
|
142
|
+
...(cert && key ? { tls: { cert: readFileSync(cert, "utf8"), key: readFileSync(key, "utf8") } } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** The libp2p config a genesis carries (`libp2p: {topics?, protocols?, listen?}`), or undefined: no node. */
|
|
146
|
+
export function libp2pOf(g) {
|
|
147
|
+
const c = g?.libp2p;
|
|
148
|
+
if (!c || typeof c !== "object")
|
|
149
|
+
return undefined;
|
|
150
|
+
const strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
151
|
+
const listen = strs(c.listen);
|
|
152
|
+
return { topics: strs(c.topics), protocols: strs(c.protocols), ...(listen.length ? { listen } : {}) };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* An instance's libp2p config as it stands (#72, #77): the genesis's
|
|
156
|
+
* (libp2pOf: a tree names what its node subscribes in `config.libp2p`),
|
|
157
|
+
* plus the topics and protocols named by the dispatch table's `libp2p`
|
|
158
|
+
* rows added since the genesis (an app's install; address: a topic) — a
|
|
159
|
+
* genesis topic row alone subscribes nothing, as a route alone did not — and
|
|
160
|
+
* every stream protocol a `libp2p` row names (`/<protocol>`), whatever
|
|
161
|
+
* installed it, the genesis included (#155: an image-born instance serves its
|
|
162
|
+
* sync row; the overlay's receive row stays unserved, P2PHost), plus the
|
|
163
|
+
* topics the apps subscribed (#119, `subscribed`).
|
|
164
|
+
* `beacons` (#126): an app's beacon (or its liveness, #138: P2PHost.liveness
|
|
165
|
+
* subscribes those topics itself) needs the node, and adds no topic.
|
|
166
|
+
* Undefined: no node — the genesis declares none and nothing needs one.
|
|
167
|
+
*/
|
|
168
|
+
export function libp2pConfig(g, rows, subscribed = [], beacons = false) {
|
|
169
|
+
const base = libp2pOf(g);
|
|
170
|
+
const seeded = new Set((g?.dispatch ?? []).filter((r) => r.transport === "libp2p").map((r) => String(r.address)));
|
|
171
|
+
// #155: a stream protocol is declared whatever installed its row; a topic row from the genesis alone subscribes nothing.
|
|
172
|
+
const names = rows.filter((r) => r.transport === "libp2p" && (String(r.address).startsWith("/") || !seeded.has(String(r.address)))).map((r) => r.address).filter((p) => typeof p === "string" && p.length > 0);
|
|
173
|
+
// #126: a beacon needs the node (to publish), not a subscription of its topic.
|
|
174
|
+
if (!base && !names.length && !subscribed.length && !beacons)
|
|
175
|
+
return undefined;
|
|
176
|
+
const add = (xs, ys) => [...new Set([...xs, ...ys])];
|
|
177
|
+
return {
|
|
178
|
+
topics: add(add(base?.topics ?? [], names.filter((n) => !n.startsWith("/"))), subscribed),
|
|
179
|
+
protocols: add(base?.protocols ?? [], names.filter((n) => n.startsWith("/"))),
|
|
180
|
+
...(base?.listen ? { listen: base.listen } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export const isTopic = (t) => typeof t === "string" && t.length > 0 && !/[\s\0]/.test(t) && !t.startsWith("/");
|
|
184
|
+
/** A `subscribe` / `unsubscribe` event record, read (subscriptions.zig eventOf); or why it is not one this host follows. */
|
|
185
|
+
export function subscriptionEvent(rec) {
|
|
186
|
+
const event = rec.event;
|
|
187
|
+
if (rec.kind !== "event" || (event !== "subscribe" && event !== "unsubscribe"))
|
|
188
|
+
return { refused: "not a subscribe or unsubscribe event" };
|
|
189
|
+
if (typeof rec.app !== "string" || !rec.app)
|
|
190
|
+
return { refused: "it names no app (its program record is not installed): ignored" };
|
|
191
|
+
if (!isTopic(rec.topic))
|
|
192
|
+
return { refused: `${JSON.stringify(rec.topic)} is not a topic (text, no space; a /protocol is not subscribed)` };
|
|
193
|
+
if (event === "unsubscribe")
|
|
194
|
+
return { event, app: rec.app, topic: rec.topic };
|
|
195
|
+
if (typeof rec.program !== "string" || !rec.program || typeof rec.fn !== "string" || !rec.fn)
|
|
196
|
+
return { refused: "a subscribe names its program and fn" };
|
|
197
|
+
return { event, sub: { app: rec.app, topic: rec.topic, program: rec.program, fn: rec.fn, ...(typeof rec.filter === "string" ? { filter: rec.filter } : {}) } };
|
|
198
|
+
}
|
|
199
|
+
/** Fold subscription events in order (subscriptions.zig apply). */
|
|
200
|
+
export function foldSubscriptions(events, into = []) {
|
|
201
|
+
for (const e of events) {
|
|
202
|
+
const [app, topic] = e.event === "subscribe" ? [e.sub.app, e.sub.topic] : [e.app, e.topic];
|
|
203
|
+
const i = into.findIndex((s) => s.app === app && s.topic === topic);
|
|
204
|
+
if (e.event === "subscribe") {
|
|
205
|
+
if (i >= 0)
|
|
206
|
+
into[i] = e.sub;
|
|
207
|
+
else
|
|
208
|
+
into.push(e.sub);
|
|
209
|
+
}
|
|
210
|
+
else if (i >= 0)
|
|
211
|
+
into.splice(i, 1);
|
|
212
|
+
}
|
|
213
|
+
return into;
|
|
214
|
+
}
|
|
215
|
+
/** The topics the subscriptions take (each once): what the node subscribes for the apps. */
|
|
216
|
+
export const subscribedTopics = (subs) => [...new Set(subs.map((s) => s.topic))];
|
|
217
|
+
/**
|
|
218
|
+
* The subscriptions, folded from the store's log (#119, the host's recovery
|
|
219
|
+
* at hydrate; subscriptions.zig fold): the `subscribe` / `unsubscribe`
|
|
220
|
+
* records each step's update lists in `emitted`, in log order — by the entry
|
|
221
|
+
* the step processed (`input`'s `n`), the step's time, its thread's start,
|
|
222
|
+
* the thread's CID, its place in the thread and in `emitted`. Read only.
|
|
223
|
+
*/
|
|
224
|
+
export async function subscriptionsOf(store) {
|
|
225
|
+
return foldSubscriptions(await emittedEvents(store, (rec) => { const e = subscriptionEvent(rec); return "refused" in e ? undefined : e; }));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The beacons standing (#126), folded from the store's log as the
|
|
229
|
+
* subscriptions are: the `beacon` / `unbeacon` records the steps' updates
|
|
230
|
+
* list, in log order, keyed by (app, topic). Read only.
|
|
231
|
+
*/
|
|
232
|
+
export async function beaconsOf(store) {
|
|
233
|
+
return foldBeacons(await emittedEvents(store, (rec) => { const e = beaconEvent(rec); return "refused" in e ? undefined : e; }));
|
|
234
|
+
}
|
|
235
|
+
/** The event records the steps' updates list in `emitted` that `read` takes, in log order (the kernel's fold order, subscriptions.zig); `read` gets each one's place too. */
|
|
236
|
+
export async function emittedEvents(store, read) {
|
|
237
|
+
const ns = new Map();
|
|
238
|
+
const nOf = async (input) => {
|
|
239
|
+
if (!isCid(input))
|
|
240
|
+
return -1;
|
|
241
|
+
const k = input.toString();
|
|
242
|
+
let n = ns.get(k);
|
|
243
|
+
if (n === undefined) {
|
|
244
|
+
const e = await store.get(input).catch(() => undefined);
|
|
245
|
+
n = typeof e?.n === "number" ? e.n : -1;
|
|
246
|
+
ns.set(k, n);
|
|
247
|
+
}
|
|
248
|
+
return n;
|
|
249
|
+
};
|
|
250
|
+
const found = [];
|
|
251
|
+
for await (const origin of store.edges.query({ kind: "thread" })) {
|
|
252
|
+
const o = await store.get(origin);
|
|
253
|
+
const thread = origin, threadBytes = origin.bytes;
|
|
254
|
+
let seq = 0;
|
|
255
|
+
for await (const c of store.chains.history(origin)) {
|
|
256
|
+
if (c.equals(origin))
|
|
257
|
+
continue;
|
|
258
|
+
seq++;
|
|
259
|
+
const u = await store.get(c);
|
|
260
|
+
if (!Array.isArray(u.emitted) || !u.emitted.length)
|
|
261
|
+
continue;
|
|
262
|
+
for (const [i, x] of u.emitted.entries()) {
|
|
263
|
+
if (!isCid(x))
|
|
264
|
+
continue;
|
|
265
|
+
const rec = await store.get(x).catch(() => undefined);
|
|
266
|
+
if (rec?.kind !== "event")
|
|
267
|
+
continue;
|
|
268
|
+
const n = await nOf(u.input), at = Number(u.at ?? 0);
|
|
269
|
+
const e = read(rec, { n, at, thread, seq, i, record: x });
|
|
270
|
+
if (e === undefined)
|
|
271
|
+
continue;
|
|
272
|
+
found.push({ key: [n, at, Number(o.at ?? 0), seq, i], thread: threadBytes, e });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const cmp = (x, y) => {
|
|
277
|
+
for (let i = 0; i < 3; i++)
|
|
278
|
+
if (x.key[i] !== y.key[i])
|
|
279
|
+
return x.key[i] - y.key[i];
|
|
280
|
+
const t = Buffer.compare(x.thread, y.thread);
|
|
281
|
+
if (t)
|
|
282
|
+
return t;
|
|
283
|
+
for (let i = 3; i < x.key.length; i++)
|
|
284
|
+
if (x.key[i] !== y.key[i])
|
|
285
|
+
return x.key[i] - y.key[i];
|
|
286
|
+
return 0;
|
|
287
|
+
};
|
|
288
|
+
found.sort(cmp);
|
|
289
|
+
return found.map((f) => f.e);
|
|
290
|
+
}
|
|
291
|
+
/** What a beat's signature covers. */
|
|
292
|
+
export function beaconPreimage(topic, b) {
|
|
293
|
+
return dagCbor.encode({ kind: "beacon", topic, body: b.body, at: b.at, sender: b.sender });
|
|
294
|
+
}
|
|
295
|
+
/** A beat's frame for `topic`, signed by `sign` (the instance's signer). */
|
|
296
|
+
export async function beaconFrame(topic, body, at, sender, sign) {
|
|
297
|
+
const signature = await sign(beaconPreimage(topic, { body, at, sender }));
|
|
298
|
+
return dagCbor.encode({ body, at, sender, signature });
|
|
299
|
+
}
|
|
300
|
+
/** A beat read from a frame on `topic`, its signature checked against its sender; or why it is not one. */
|
|
301
|
+
export function beaconBeat(topic, frame) {
|
|
302
|
+
let v;
|
|
303
|
+
try {
|
|
304
|
+
v = dagCbor.decode(frame);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return { problem: "not dag-cbor" };
|
|
308
|
+
}
|
|
309
|
+
if (!v || typeof v !== "object")
|
|
310
|
+
return { problem: "not a map" };
|
|
311
|
+
const { body, at, sender, signature } = v;
|
|
312
|
+
if (!(body instanceof Uint8Array) || typeof at !== "number" || !(sender instanceof Uint8Array) || sender.length !== 33 || !(signature instanceof Uint8Array))
|
|
313
|
+
return { problem: "want {body: bytes, at: int, sender: bytes(33), signature: bytes}" };
|
|
314
|
+
const b = { body, at, sender, signature };
|
|
315
|
+
if (!verifyAnyone(Buffer.from(sender).toString("hex"), SIGN_PROTOCOL, MESSAGE_KEY_ID, beaconPreimage(topic, b), signature))
|
|
316
|
+
return { problem: "the signature is not the sender's" };
|
|
317
|
+
return b;
|
|
318
|
+
}
|
|
319
|
+
/** A `beacon` / `unbeacon` event record, read; or why it is not one this host follows. */
|
|
320
|
+
export function beaconEvent(rec) {
|
|
321
|
+
const event = rec.event;
|
|
322
|
+
if (rec.kind !== "event" || (event !== "beacon" && event !== "unbeacon"))
|
|
323
|
+
return { refused: "not a beacon or unbeacon event" };
|
|
324
|
+
if (typeof rec.app !== "string" || !rec.app)
|
|
325
|
+
return { refused: "it names no app (its program record is not installed): ignored" };
|
|
326
|
+
if (!isTopic(rec.topic))
|
|
327
|
+
return { refused: `${JSON.stringify(rec.topic)} is not a topic` };
|
|
328
|
+
if (event === "unbeacon")
|
|
329
|
+
return { event, app: rec.app, topic: rec.topic };
|
|
330
|
+
const every = typeof rec.every === "number" ? rec.every : typeof rec.every === "bigint" ? Number(rec.every) : NaN;
|
|
331
|
+
if (!Number.isFinite(every) || every < 1000)
|
|
332
|
+
return { refused: "a beacon beats every second or slower (`every`, ms)" };
|
|
333
|
+
if (!(rec.body instanceof Uint8Array))
|
|
334
|
+
return { refused: "a beacon has a body (bytes)" };
|
|
335
|
+
return { event, beacon: { app: rec.app, topic: rec.topic, every, body: rec.body } };
|
|
336
|
+
}
|
|
337
|
+
/** Fold beacon events in order: a beacon replaces the app's own on the topic, an unbeacon removes it. */
|
|
338
|
+
export function foldBeacons(events, into = []) {
|
|
339
|
+
for (const e of events) {
|
|
340
|
+
const [app, topic] = e.event === "beacon" ? [e.beacon.app, e.beacon.topic] : [e.app, e.topic];
|
|
341
|
+
const i = into.findIndex((b) => b.app === app && b.topic === topic);
|
|
342
|
+
if (e.event === "beacon") {
|
|
343
|
+
if (i >= 0)
|
|
344
|
+
into[i] = e.beacon;
|
|
345
|
+
else
|
|
346
|
+
into.push(e.beacon);
|
|
347
|
+
}
|
|
348
|
+
else if (i >= 0)
|
|
349
|
+
into.splice(i, 1);
|
|
350
|
+
}
|
|
351
|
+
return into;
|
|
352
|
+
}
|
|
353
|
+
const isCid = (x) => CID.asCID(x) !== null;
|
|
354
|
+
/** Stream IDs unique across restarts (a thread's recorded stream never names a later stream). */
|
|
355
|
+
let nextStream = Date.now() * 1000;
|
|
356
|
+
const u64 = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, n, false); return b; };
|
|
357
|
+
/** The answer to a stream whose first frame names no topic where several instances declare its protocol (#160). */
|
|
358
|
+
export const TOPIC_REQUIRED = "topic required";
|
|
359
|
+
export class P2PHost {
|
|
360
|
+
o;
|
|
361
|
+
/** The liveness tool's state (#138): the apps' liveness and the beats kept, in memory. */
|
|
362
|
+
live = new Liveness();
|
|
363
|
+
/** The nodes, by peer ID (#160: one per key). */
|
|
364
|
+
nodes = new Map();
|
|
365
|
+
/** Each handle's node. */
|
|
366
|
+
byHandle = new Map();
|
|
367
|
+
/** Each handle's peer ID, as derived (keyOf). */
|
|
368
|
+
ids = new Map();
|
|
369
|
+
/** Each key's declares, in order (one at a time). */
|
|
370
|
+
starting = new Map();
|
|
371
|
+
stopped = false;
|
|
372
|
+
constructor(o) {
|
|
373
|
+
this.o = o;
|
|
374
|
+
if (o.beat)
|
|
375
|
+
this.live.kept = (h, topic, b) => { if (b.from !== this.peerId(h))
|
|
376
|
+
o.beat(h, topic, b); };
|
|
377
|
+
}
|
|
378
|
+
say(source, line) { this.o.log?.(source, line); }
|
|
379
|
+
/** A node's name in a log line: its one handle (as before #160), else its first. */
|
|
380
|
+
nameOf(n) { return n.handles.keys().next().value ?? n.id; }
|
|
381
|
+
/** The node of an instance, if it has one. */
|
|
382
|
+
node(handle) { return this.byHandle.get(handle)?.node; }
|
|
383
|
+
/** An instance's peer ID (derived: whether or not its node runs). */
|
|
384
|
+
peerId(handle) {
|
|
385
|
+
let id = this.ids.get(handle);
|
|
386
|
+
if (!id)
|
|
387
|
+
this.ids.set(handle, (id = peerIdOf(this.o.keyOf(handle)).toString()));
|
|
388
|
+
return id;
|
|
389
|
+
}
|
|
390
|
+
/** The handles on `handle`'s node (#160: one in a master-key host; every one sharing its key in a one-key host). */
|
|
391
|
+
handlesOn(handle) { return [...(this.byHandle.get(handle)?.handles.keys() ?? [])]; }
|
|
392
|
+
/** How many nodes run (#160: one per key). */
|
|
393
|
+
nodeCount() { return this.nodes.size; }
|
|
394
|
+
/** Where an instance's node listens (with /p2p/<id>). */
|
|
395
|
+
addrs(handle) { return this.node(handle)?.getMultiaddrs().map((m) => m.toString()) ?? []; }
|
|
396
|
+
/**
|
|
397
|
+
* `handle`'s libp2p config is this: its key's node started (once) with its peer key, the handle's
|
|
398
|
+
* topics subscribed with the validator, its protocols handled. Declared again, the handle is brought
|
|
399
|
+
* to the new config. No config: the handle leaves its node; the node stops with its last handle.
|
|
400
|
+
*/
|
|
401
|
+
async declare(handle, config) {
|
|
402
|
+
if (this.stopped)
|
|
403
|
+
return;
|
|
404
|
+
const key = this.byHandle.get(handle)?.id ?? this.peerId(handle);
|
|
405
|
+
const prev = this.starting.get(key) ?? Promise.resolve();
|
|
406
|
+
// The previous declare's failure went to its own caller (it awaited it); this one runs regardless.
|
|
407
|
+
const next = prev.catch(() => { }).then(async () => {
|
|
408
|
+
if (this.stopped)
|
|
409
|
+
return;
|
|
410
|
+
if (!config)
|
|
411
|
+
return this.drop(handle);
|
|
412
|
+
const n = this.byHandle.get(handle) ?? this.nodes.get(key);
|
|
413
|
+
if (n)
|
|
414
|
+
return this.reconfigure(n, handle, config);
|
|
415
|
+
await this.start(key, handle, config);
|
|
416
|
+
});
|
|
417
|
+
this.starting.set(key, next);
|
|
418
|
+
// Only clears the slot: the error reaches this call's caller through `await next` below.
|
|
419
|
+
void next.catch(() => { }).finally(() => { if (this.starting.get(key) === next)
|
|
420
|
+
this.starting.delete(key); });
|
|
421
|
+
await next;
|
|
422
|
+
}
|
|
423
|
+
/** The topics and protocols an instance's node serves for it now. */
|
|
424
|
+
served(handle) {
|
|
425
|
+
const h = this.byHandle.get(handle)?.handles.get(handle);
|
|
426
|
+
return h && { topics: [...h.config.topics], protocols: [...h.config.protocols] };
|
|
427
|
+
}
|
|
428
|
+
/** Bring `handle` on a running node to `config` (joining it if it is new there): subscribe and handle what is new, unsubscribe and unhandle what no handle takes any more. */
|
|
429
|
+
async reconfigure(n, handle, config) {
|
|
430
|
+
let h = n.handles.get(handle);
|
|
431
|
+
const joined = !h;
|
|
432
|
+
if (!h) {
|
|
433
|
+
n.handles.set(handle, (h = { config: { topics: [], protocols: [] }, streams: new Map(), beats: new Map() }));
|
|
434
|
+
this.byHandle.set(handle, n);
|
|
435
|
+
if (config.listen?.length)
|
|
436
|
+
this.say(handle, `libp2p: its listen ${config.listen.join(" ")} not honoured: its key's node (${n.id}) carries ${n.handles.size} instances and listens on ${n.node.getMultiaddrs().map(String).join(" ") || "(nothing)"} (#160)`);
|
|
437
|
+
}
|
|
438
|
+
const was = h.config;
|
|
439
|
+
const added = config.topics.filter((t) => !was.topics.includes(t));
|
|
440
|
+
const removed = was.topics.filter((t) => !config.topics.includes(t));
|
|
441
|
+
const addedP = config.protocols.filter((p) => !was.protocols.includes(p) && !this.hostServed(p));
|
|
442
|
+
const removedP = was.protocols.filter((p) => !config.protocols.includes(p) && !this.hostServed(p));
|
|
443
|
+
h.config = { ...config, listen: joined ? undefined : was.listen };
|
|
444
|
+
if (!joined && !added.length && !removed.length && !addedP.length && !removedP.length)
|
|
445
|
+
return;
|
|
446
|
+
const sub = this.resubscribe(n);
|
|
447
|
+
await this.rehandle(n);
|
|
448
|
+
const list = (xs) => xs.join(", ") || "none";
|
|
449
|
+
if (joined)
|
|
450
|
+
this.say(handle, `libp2p: on the node of its key ${n.id} (${n.handles.size} instances); topics ${list(config.topics)}; protocols ${list(config.protocols)}`);
|
|
451
|
+
else
|
|
452
|
+
this.say(handle, `libp2p: topics ${list(config.topics)} (+${list(added)} −${list(removed)}); protocols ${list(config.protocols)} (+${list(addedP)} −${list(removedP)})`);
|
|
453
|
+
if (added.length || sub.added.length)
|
|
454
|
+
setTimeout(() => void this.discover(n).catch((e) => this.say(this.nameOf(n), `libp2p: discovery: ${e.message}`)), 200);
|
|
455
|
+
}
|
|
456
|
+
subscribe(n, topic) {
|
|
457
|
+
const ps = this.pubsub(n);
|
|
458
|
+
ps.topicValidators.set(topic, (from, msg) => this.validate(n, topic, from, msg));
|
|
459
|
+
ps.subscribe(topic);
|
|
460
|
+
n.subscribed.add(topic);
|
|
461
|
+
}
|
|
462
|
+
/** The topics `handle` takes: its config's and its apps' liveness topics (#138). */
|
|
463
|
+
takes(handle, h) { return [...h.config.topics, ...this.live.topics(handle)]; }
|
|
464
|
+
/** Subscribe what the handles' configs and liveness tools (#138) take, unsubscribe the rest: the topics added and removed. */
|
|
465
|
+
resubscribe(n) {
|
|
466
|
+
const want = new Set([...n.handles].flatMap(([handle, h]) => this.takes(handle, h)));
|
|
467
|
+
const ps = this.pubsub(n);
|
|
468
|
+
const removed = [...n.subscribed].filter((t) => !want.has(t));
|
|
469
|
+
const added = [...want].filter((t) => !n.subscribed.has(t));
|
|
470
|
+
for (const t of removed) {
|
|
471
|
+
ps.unsubscribe(t);
|
|
472
|
+
ps.topicValidators.delete(t);
|
|
473
|
+
n.subscribed.delete(t);
|
|
474
|
+
}
|
|
475
|
+
for (const t of added)
|
|
476
|
+
this.subscribe(n, t);
|
|
477
|
+
return { added, removed };
|
|
478
|
+
}
|
|
479
|
+
/** Handle the protocols the handles declare, unhandle those none declares any more (the host's own aside). */
|
|
480
|
+
async rehandle(n) {
|
|
481
|
+
const host = this.o.hostProtocols ?? {};
|
|
482
|
+
const want = new Set([...n.handles.values()].flatMap((h) => h.config.protocols).filter((p) => inboundRow(p, host)));
|
|
483
|
+
for (const p of [...n.handled])
|
|
484
|
+
if (!want.has(p)) {
|
|
485
|
+
n.handled.delete(p);
|
|
486
|
+
await n.node.unhandle(p);
|
|
487
|
+
}
|
|
488
|
+
for (const p of want)
|
|
489
|
+
if (!n.handled.has(p)) {
|
|
490
|
+
n.handled.add(p);
|
|
491
|
+
await this.handleProtocol(n, p);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/** The topics `handle`'s node subscribes for it now: its config's and its apps' liveness topics (#138). */
|
|
495
|
+
listening(handle) {
|
|
496
|
+
const n = this.byHandle.get(handle);
|
|
497
|
+
const h = n?.handles.get(handle);
|
|
498
|
+
return n && h ? [...new Set(this.takes(handle, h))].filter((t) => n.subscribed.has(t)) : [];
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* The liveness `handle`'s apps keep (#138, liveness.ts): the tool keeps it, and the node subscribes
|
|
502
|
+
* each topic (not admitting its messages) and leaves those no liveness nor config takes. One line
|
|
503
|
+
* when a topic is taken or left. No node: kept for when it starts.
|
|
504
|
+
*/
|
|
505
|
+
liveness(handle, lives) {
|
|
506
|
+
this.live.set(handle, lives);
|
|
507
|
+
const n = this.byHandle.get(handle);
|
|
508
|
+
if (!n)
|
|
509
|
+
return;
|
|
510
|
+
const { added, removed } = this.resubscribe(n);
|
|
511
|
+
for (const t of added)
|
|
512
|
+
this.say(handle, `libp2p: liveness ${t} (app ${lives.filter((l) => l.topic === t).map((l) => l.app).join(", ")})`);
|
|
513
|
+
for (const t of removed)
|
|
514
|
+
this.say(handle, `libp2p: ${t} left`);
|
|
515
|
+
if (added.length)
|
|
516
|
+
setTimeout(() => void this.discover(n).catch((e) => this.say(this.nameOf(n), `libp2p: discovery: ${e.message}`)), 200);
|
|
517
|
+
}
|
|
518
|
+
/** Whether the host serves `protocol` on every node itself (/skein/message/1.0.0, `hostProtocols`). */
|
|
519
|
+
hostServed(protocol) { return protocol === MESSAGE_PROTOCOL || protocol in (this.o.hostProtocols ?? {}); }
|
|
520
|
+
async handleHostProtocol(n, protocol, f) {
|
|
521
|
+
await n.node.handle(protocol, (stream, connection) => {
|
|
522
|
+
void (async () => {
|
|
523
|
+
const lp = lpStream(stream);
|
|
524
|
+
try {
|
|
525
|
+
for (;;) {
|
|
526
|
+
let frame;
|
|
527
|
+
try {
|
|
528
|
+
frame = (await lp.read()).subarray();
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
// The router names no host protocol (#154); a node's first handle answers for it.
|
|
534
|
+
await lp.write(await f(this.nameOf(n), connection.remotePeer, frame));
|
|
535
|
+
}
|
|
536
|
+
await stream.close().catch(() => { });
|
|
537
|
+
}
|
|
538
|
+
catch (e) {
|
|
539
|
+
this.say(this.nameOf(n), `libp2p: ${protocol} from ${connection.remotePeer}: ${e.message}`);
|
|
540
|
+
stream.abort(e);
|
|
541
|
+
}
|
|
542
|
+
})();
|
|
543
|
+
}, { runOnLimitedConnection: true });
|
|
544
|
+
}
|
|
545
|
+
async handleProtocol(n, protocol) {
|
|
546
|
+
await n.node.handle(protocol, (stream, connection) => { void this.serve(n, protocol, stream, connection.remotePeer); }, { runOnLimitedConnection: true });
|
|
547
|
+
}
|
|
548
|
+
async start(key, handle, config) {
|
|
549
|
+
const h = this.o.host;
|
|
550
|
+
const n = { id: key, node: undefined, handles: new Map([[handle, { config, streams: new Map(), beats: new Map() }]]), seqnos: new Map(), subscribed: new Set(), handled: new Set() };
|
|
551
|
+
const listen = [...(config.listen ?? h.listen), ...h.relays.map((r) => `${r.replace(/\/+$/, "")}/p2p-circuit`)];
|
|
552
|
+
// An address that does not parse counts as private here; start() reports it when it is dialled or listened on.
|
|
553
|
+
const privateDev = [...h.bootstrap, ...listen].every((a) => { try {
|
|
554
|
+
return isPrivateAddr(multiaddr(a));
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return true;
|
|
558
|
+
} });
|
|
559
|
+
const services = {
|
|
560
|
+
identify: identify(),
|
|
561
|
+
ping: ping(),
|
|
562
|
+
pubsub: gossipsub({
|
|
563
|
+
globalSignaturePolicy: "StrictSign",
|
|
564
|
+
allowPublishToZeroTopicPeers: true,
|
|
565
|
+
emitSelf: false,
|
|
566
|
+
// The default id (the publisher's key ‖ seqno), and publish's seqno caught by the data object.
|
|
567
|
+
msgIdFn: async (msg) => {
|
|
568
|
+
if (msg.type !== "signed")
|
|
569
|
+
throw new Error("unsigned message");
|
|
570
|
+
if (n.seqnos.has(msg.data))
|
|
571
|
+
n.seqnos.set(msg.data, msg.sequenceNumber);
|
|
572
|
+
const key = (msg.from.publicKey ?? msg.key);
|
|
573
|
+
const { publicKeyToProtobuf } = await import("@libp2p/crypto/keys");
|
|
574
|
+
const k = publicKeyToProtobuf(key);
|
|
575
|
+
const out = new Uint8Array(k.length + 8);
|
|
576
|
+
out.set(k, 0);
|
|
577
|
+
out.set(u64(msg.sequenceNumber), k.length);
|
|
578
|
+
return out;
|
|
579
|
+
},
|
|
580
|
+
}),
|
|
581
|
+
};
|
|
582
|
+
if (h.dht !== "off")
|
|
583
|
+
services.dht = kadDHT({ clientMode: h.dht === "client", peerInfoMapper: privateDev ? passthroughMapper : removePrivateAddressesMapper });
|
|
584
|
+
const node = await createLibp2p({
|
|
585
|
+
privateKey: libp2pKey(this.o.keyOf(handle)),
|
|
586
|
+
addresses: { listen },
|
|
587
|
+
transports: [tcp(), webSockets(h.tls ? { https: { cert: h.tls.cert, key: h.tls.key } } : {}), ...(h.relays.length ? [circuitRelayTransport()] : [])],
|
|
588
|
+
connectionEncrypters: [noise()],
|
|
589
|
+
streamMuxers: [yamux()],
|
|
590
|
+
peerDiscovery: h.mdns ? [mdns()] : [],
|
|
591
|
+
services: services,
|
|
592
|
+
});
|
|
593
|
+
n.node = node;
|
|
594
|
+
if (this.stopped) {
|
|
595
|
+
await node.stop();
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
this.nodes.set(key, n);
|
|
599
|
+
this.byHandle.set(handle, n);
|
|
600
|
+
if (h.mdns)
|
|
601
|
+
node.addEventListener("peer:discovery", (e) => { void node.dial(e.detail.id).catch((err) => this.say(this.nameOf(n), `libp2p: mDNS peer ${e.detail.id}: ${err.message}`)); });
|
|
602
|
+
this.resubscribe(n);
|
|
603
|
+
const host = this.o.hostProtocols ?? {};
|
|
604
|
+
for (const protocol of config.protocols.filter((p) => inboundRow(p, host))) {
|
|
605
|
+
n.handled.add(protocol);
|
|
606
|
+
await this.handleProtocol(n, protocol);
|
|
607
|
+
}
|
|
608
|
+
await this.handleProtocol(n, MESSAGE_PROTOCOL);
|
|
609
|
+
for (const [protocol, f] of Object.entries(host))
|
|
610
|
+
await this.handleHostProtocol(n, protocol, f);
|
|
611
|
+
this.say(handle, `libp2p: peer ${node.peerId} listening on ${node.getMultiaddrs().map(String).join(" ") || "(nothing)"}; topics ${config.topics.join(", ") || "none"}; protocols ${config.protocols.join(", ") || "none"}`);
|
|
612
|
+
const discover = () => void this.discover(n).catch((e) => this.say(this.nameOf(n), `libp2p: discovery: ${e.message}`));
|
|
613
|
+
setTimeout(discover, 200);
|
|
614
|
+
n.timer = setInterval(discover, this.o.discoveryMs ?? 30_000);
|
|
615
|
+
return n;
|
|
616
|
+
}
|
|
617
|
+
// ---------------------------------------------------------------- discovery
|
|
618
|
+
/** Keep the bootstrap peers connected; with the DHT on, advertise each topic and dial its providers. */
|
|
619
|
+
async discover(n) {
|
|
620
|
+
const node = n.node;
|
|
621
|
+
const name = this.nameOf(n);
|
|
622
|
+
const connected = new Set(node.getConnections().map((c) => c.remotePeer.toString()));
|
|
623
|
+
for (const a of this.o.host.bootstrap) {
|
|
624
|
+
let ma;
|
|
625
|
+
try {
|
|
626
|
+
ma = multiaddr(a);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
this.say(name, `libp2p: bad bootstrap address ${a}`);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const id = ma.getComponents().find((c) => c.name === "p2p")?.value;
|
|
633
|
+
if (id === node.peerId.toString() || (id && connected.has(id)))
|
|
634
|
+
continue;
|
|
635
|
+
await node.dial(ma).catch((e) => this.say(name, `libp2p: bootstrap ${a}: ${e.message}`));
|
|
636
|
+
}
|
|
637
|
+
if (this.o.host.dht === "off" || !node.services.dht)
|
|
638
|
+
return;
|
|
639
|
+
for (const topic of [...n.subscribed]) {
|
|
640
|
+
const cid = await topicCid(topic);
|
|
641
|
+
await node.contentRouting.provide(cid, { signal: AbortSignal.timeout(20_000) }).catch((e) => { if (!timedOut(e))
|
|
642
|
+
this.say(name, `libp2p: provide ${topic}: ${e.message}`); });
|
|
643
|
+
try {
|
|
644
|
+
for await (const p of node.contentRouting.findProviders(cid, { signal: AbortSignal.timeout(20_000) })) {
|
|
645
|
+
if (p.id.equals(node.peerId) || node.getConnections(p.id).length)
|
|
646
|
+
continue;
|
|
647
|
+
await node.dial(p.multiaddrs.length ? p.multiaddrs : p.id).catch((e) => this.say(name, `libp2p: ${topic} provider ${p.id}: ${e.message}`));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
if (!timedOut(e))
|
|
652
|
+
this.say(name, `libp2p: find providers ${topic}: ${e.message}`);
|
|
653
|
+
} // timed out: next round
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// ---------------------------------------------------------------- inbound
|
|
657
|
+
pubsub(n) {
|
|
658
|
+
return n.node.services.pubsub;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* The topic validator: for every handle on the node that takes the topic, one front-door call (or,
|
|
662
|
+
* for a topic only its apps' liveness takes, #138, the liveness tool's verdict alone: nothing
|
|
663
|
+
* admitted); GossipSub's verdict (async: forwarding waits on it) is Accept if any handle accepts,
|
|
664
|
+
* Reject only if every one rejects, else Ignore (#160).
|
|
665
|
+
*/
|
|
666
|
+
async validate(n, topic, _source, msg) {
|
|
667
|
+
if (msg.type !== "signed")
|
|
668
|
+
return TopicValidatorResult.Reject;
|
|
669
|
+
const takers = [...n.handles].filter(([handle, h]) => h.config.topics.includes(topic) || this.live.takes(handle, topic));
|
|
670
|
+
if (!takers.length)
|
|
671
|
+
return TopicValidatorResult.Ignore;
|
|
672
|
+
const verdicts = await Promise.all(takers.map(([handle, h]) => this.validateFor(handle, h, topic, msg)));
|
|
673
|
+
if (verdicts.includes(TopicValidatorResult.Accept))
|
|
674
|
+
return TopicValidatorResult.Accept;
|
|
675
|
+
if (verdicts.every((v) => v === TopicValidatorResult.Reject))
|
|
676
|
+
return TopicValidatorResult.Reject;
|
|
677
|
+
return TopicValidatorResult.Ignore;
|
|
678
|
+
}
|
|
679
|
+
/** One handle's verdict on a topic message (the validator as it was per node before #160). */
|
|
680
|
+
async validateFor(handle, h, topic, msg) {
|
|
681
|
+
const live = this.live.takes(handle, topic) ? this.live.observe(handle, topic, msg.data, msg.from.toString()) : undefined;
|
|
682
|
+
if (!h.config.topics.includes(topic))
|
|
683
|
+
return live === "accept" ? TopicValidatorResult.Accept : live === "reject" ? TopicValidatorResult.Reject : TopicValidatorResult.Ignore;
|
|
684
|
+
try {
|
|
685
|
+
const a = await this.o.inbound(handle, {
|
|
686
|
+
transport: "libp2p", topic, from: msg.from.toMultihash().bytes, seqno: u64(msg.sequenceNumber), signature: msg.signature, body: msg.data,
|
|
687
|
+
});
|
|
688
|
+
return a.verdict === "accept" ? TopicValidatorResult.Accept : a.verdict === "reject" ? TopicValidatorResult.Reject : TopicValidatorResult.Ignore;
|
|
689
|
+
}
|
|
690
|
+
catch (e) {
|
|
691
|
+
// The host failing is not the message's fault: no penalty.
|
|
692
|
+
this.say(handle, `libp2p: ${topic}: validation failed: ${e.message}`);
|
|
693
|
+
return TopicValidatorResult.Ignore;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Which handle an inbound stream on `protocol` is for (#160), by its first frame: the handles that
|
|
698
|
+
* declare the protocol (every handle declares /skein/message/1.0.0); the frame's `topic` (dag-cbor
|
|
699
|
+
* {topic, …}) picks the one among them that takes it; a frame naming none, or a topic none of them
|
|
700
|
+
* takes, goes to the one handle that declares the protocol; with several, a problem.
|
|
701
|
+
*/
|
|
702
|
+
streamHandle(n, protocol, first) {
|
|
703
|
+
const declaring = [...n.handles].filter(([, h]) => protocol === MESSAGE_PROTOCOL || h.config.protocols.includes(protocol)).map(([handle]) => handle);
|
|
704
|
+
if (declaring.length === 1)
|
|
705
|
+
return { handle: declaring[0] };
|
|
706
|
+
if (!declaring.length)
|
|
707
|
+
return { problem: `no instance here declares ${protocol}` };
|
|
708
|
+
let topic;
|
|
709
|
+
try {
|
|
710
|
+
const v = dagCbor.decode(first);
|
|
711
|
+
topic = v && typeof v === "object" ? v.topic : undefined;
|
|
712
|
+
}
|
|
713
|
+
catch { /* not dag-cbor: it names no topic */ }
|
|
714
|
+
if (typeof topic !== "string" || !topic)
|
|
715
|
+
return { problem: TOPIC_REQUIRED };
|
|
716
|
+
// A topic several declaring instances take (a discovery instance beside a token instance, #137): the first declared of them.
|
|
717
|
+
const taker = declaring.find((handle) => n.handles.get(handle).config.topics.includes(topic));
|
|
718
|
+
return taker ? { handle: taker } : { problem: `no instance here takes ${topic}` };
|
|
719
|
+
}
|
|
720
|
+
/** An inbound stream on a handled protocol: its first frame picks the handle (#160); each frame one front-door call, its answer's body written back. */
|
|
721
|
+
async serve(n, protocol, stream, remote) {
|
|
722
|
+
const lp = lpStream(stream);
|
|
723
|
+
let handle = this.nameOf(n);
|
|
724
|
+
try {
|
|
725
|
+
let first;
|
|
726
|
+
try {
|
|
727
|
+
first = (await lp.read()).subarray();
|
|
728
|
+
}
|
|
729
|
+
catch { /* the remote closed before a frame: nothing owed */ }
|
|
730
|
+
if (first) {
|
|
731
|
+
const to = this.streamHandle(n, protocol, first);
|
|
732
|
+
if ("problem" in to) {
|
|
733
|
+
this.say(handle, `libp2p: ${protocol} from ${remote}: ${to.problem}: answered and closed`);
|
|
734
|
+
await lp.write(dagCbor.encode({ kind: "error", message: to.problem }));
|
|
735
|
+
await stream.close().catch(() => { });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
handle = to.handle;
|
|
739
|
+
for (let frame = first;;) {
|
|
740
|
+
const a = await this.o.inbound(handle, { transport: "libp2p", protocol, from: remote.toMultihash().bytes, body: frame });
|
|
741
|
+
if (a.body)
|
|
742
|
+
await lp.write(a.body);
|
|
743
|
+
if (a.close || a.verdict === "reject")
|
|
744
|
+
break;
|
|
745
|
+
try {
|
|
746
|
+
frame = (await lp.read()).subarray();
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
break;
|
|
750
|
+
} // the remote closed or reset its side: the stream is over either way, and nothing here is owed an answer
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
await stream.close().catch(() => { }); // a stream the remote already closed or reset cannot be closed again; every frame was answered
|
|
754
|
+
}
|
|
755
|
+
catch (e) {
|
|
756
|
+
this.say(handle, `libp2p: ${protocol} from ${remote}: ${e.message}`);
|
|
757
|
+
stream.abort(e);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ---------------------------------------------------------------- outbound (the libp2p provider's, #70)
|
|
761
|
+
nodeOf(handle) {
|
|
762
|
+
const n = this.byHandle.get(handle);
|
|
763
|
+
if (!n)
|
|
764
|
+
throw new Error("this instance has no libp2p node (its config names no libp2p)");
|
|
765
|
+
return n;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* The beacons `handle`'s node beats (#126): each a new frame (beaconFrame:
|
|
769
|
+
* the body, the beat's time, the instance's signature) published on its topic
|
|
770
|
+
* every `every` ms — the topic not subscribed; nothing logged per beat (a
|
|
771
|
+
* failed beat is dropped: the next one is the retry). The FIRST beacon on a
|
|
772
|
+
* topic publishes at once. A beacon REPLACING one (the same app and topic, a
|
|
773
|
+
* new body and/or `every`; the settled sync design, David 2026-10-30) swaps
|
|
774
|
+
* what the next beat carries and leaves the timer alone — no publish now, no
|
|
775
|
+
* reset — unless `every` changed, when the timer is reset to the new
|
|
776
|
+
* interval (still no publish now). One line when a beacon starts or stops.
|
|
777
|
+
* No node: nothing beats.
|
|
778
|
+
*/
|
|
779
|
+
beacons(handle, beacons) {
|
|
780
|
+
const n = this.byHandle.get(handle);
|
|
781
|
+
const hs = n?.handles.get(handle);
|
|
782
|
+
if (!n || !hs)
|
|
783
|
+
return;
|
|
784
|
+
const want = new Map(beacons.map((b) => [`${b.app} ${b.topic}`, b]));
|
|
785
|
+
// Each beat a new frame: the entry's body as it stands then, now, signed by the instance's signer (beaconFrame).
|
|
786
|
+
const beatOf = (k) => () => {
|
|
787
|
+
const x = hs.beats.get(k);
|
|
788
|
+
if (!x || this.byHandle.get(handle) !== n || n.handles.get(handle) !== hs)
|
|
789
|
+
return;
|
|
790
|
+
const b = x.beacon;
|
|
791
|
+
const { sign, identity } = this.o;
|
|
792
|
+
if (!sign || !identity)
|
|
793
|
+
return;
|
|
794
|
+
void (async () => {
|
|
795
|
+
const sender = await identity(handle);
|
|
796
|
+
if (!sender)
|
|
797
|
+
return;
|
|
798
|
+
const frame = await beaconFrame(b.topic, b.body, Date.now(), sender, (d) => sign(handle, d));
|
|
799
|
+
await this.publish(handle, b.topic, frame);
|
|
800
|
+
// #138: the node's own beat into its liveness set (gossip does not echo it).
|
|
801
|
+
if (this.live.takes(handle, b.topic))
|
|
802
|
+
this.live.observe(handle, b.topic, frame, n.node.peerId.toString());
|
|
803
|
+
})().catch(() => { });
|
|
804
|
+
};
|
|
805
|
+
for (const [k, x] of hs.beats) {
|
|
806
|
+
const b = want.get(k);
|
|
807
|
+
if (b) {
|
|
808
|
+
if (b.every !== x.beacon.every) {
|
|
809
|
+
clearInterval(x.timer);
|
|
810
|
+
x.timer = setInterval(beatOf(k), b.every);
|
|
811
|
+
}
|
|
812
|
+
x.beacon = b; // the next beat carries it
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
clearInterval(x.timer);
|
|
816
|
+
hs.beats.delete(k);
|
|
817
|
+
this.say(handle, `libp2p: beacon ${x.beacon.topic} (app ${x.beacon.app}) stopped`);
|
|
818
|
+
}
|
|
819
|
+
for (const [k, b] of want) {
|
|
820
|
+
if (hs.beats.has(k))
|
|
821
|
+
continue;
|
|
822
|
+
hs.beats.set(k, { beacon: b, timer: setInterval(beatOf(k), b.every) });
|
|
823
|
+
beatOf(k)();
|
|
824
|
+
this.say(handle, `libp2p: beacon ${b.topic} (app ${b.app}) every ${b.every} ms, ${b.body.length} bytes`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/** Publish `body` on `topic` from `handle`'s node (signed with its peer key): the message's seqno, and how many peers took it. */
|
|
828
|
+
async publish(handle, topic, body) {
|
|
829
|
+
const n = this.nodeOf(handle);
|
|
830
|
+
const data = Uint8Array.from(body); // a fresh object: the key msgIdFn catches the seqno by
|
|
831
|
+
n.seqnos.set(data, -1n);
|
|
832
|
+
try {
|
|
833
|
+
const r = await this.pubsub(n).publish(topic, data);
|
|
834
|
+
const seq = n.seqnos.get(data);
|
|
835
|
+
if (seq < 0n)
|
|
836
|
+
throw new Error("publish: no seqno");
|
|
837
|
+
return { seqno: u64(seq), recipients: r.recipients.length };
|
|
838
|
+
}
|
|
839
|
+
finally {
|
|
840
|
+
n.seqnos.delete(data);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/** Open a stream from `handle`'s node to a peer (a peer ID, or a multiaddr with /p2p/<id>): its id; each frame read from it, and its end, to `frames`. */
|
|
844
|
+
async dial(handle, peer, protocol, frames) {
|
|
845
|
+
const n = this.nodeOf(handle);
|
|
846
|
+
const hs = n.handles.get(handle);
|
|
847
|
+
const target = peer.startsWith("/") ? multiaddr(peer) : peerIdFromString(peer);
|
|
848
|
+
const stream = await n.node.dialProtocol(target, protocol, { signal: AbortSignal.timeout(15_000) });
|
|
849
|
+
const id = ++nextStream;
|
|
850
|
+
const s = { stream, lp: lpStream(stream) };
|
|
851
|
+
hs.streams.set(id, s);
|
|
852
|
+
void (async () => {
|
|
853
|
+
for (;;) {
|
|
854
|
+
try {
|
|
855
|
+
frames({ body: (await s.lp.read()).subarray() });
|
|
856
|
+
}
|
|
857
|
+
catch (e) {
|
|
858
|
+
const name = e.name;
|
|
859
|
+
const clean = name === "UnexpectedEOFError" || name === "StreamClosedError" || name === "StreamResetError";
|
|
860
|
+
if (hs.streams.get(id) === s)
|
|
861
|
+
frames({ closed: true, ...(clean ? {} : { error: e.message }) });
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
})();
|
|
866
|
+
return id;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* One request to a peer on `protocol` (a host protocol, shruggr/skein#150): a stream opened, one
|
|
870
|
+
* frame written, one frame read back, the stream closed. → the answer's bytes.
|
|
871
|
+
*/
|
|
872
|
+
async request(handle, peer, protocol, body, timeoutMs = 15_000) {
|
|
873
|
+
const n = this.nodeOf(handle);
|
|
874
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
875
|
+
const stream = await n.node.dialProtocol(peerIdFromString(peer), protocol, { signal });
|
|
876
|
+
try {
|
|
877
|
+
const lp = lpStream(stream);
|
|
878
|
+
await lp.write(body, { signal });
|
|
879
|
+
const answer = (await lp.read({ signal })).subarray();
|
|
880
|
+
await stream.close().catch(() => { });
|
|
881
|
+
return answer;
|
|
882
|
+
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
stream.abort(e);
|
|
885
|
+
throw e;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/** The protocols `handle`'s node handles inbound now (the libp2p node's own registry: its rows', the host's, /skein/message/1.0.0). */
|
|
889
|
+
handledProtocols(handle) { return this.byHandle.get(handle)?.node.getProtocols() ?? []; }
|
|
890
|
+
/** The addresses `handle`'s node knows for `peer` (its peer store), as multiaddr strings. */
|
|
891
|
+
async peerAddrs(handle, peer) {
|
|
892
|
+
const n = this.byHandle.get(handle);
|
|
893
|
+
if (!n)
|
|
894
|
+
return [];
|
|
895
|
+
try {
|
|
896
|
+
return (await n.node.peerStore.get(peerIdFromString(peer))).addresses.map((a) => a.multiaddr.toString());
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/** Dial `peer` from `handle`'s node, at `addrs` when given (a start's persisted peers), else as the peer store knows it. */
|
|
903
|
+
async connect(handle, peer, addrs = []) {
|
|
904
|
+
const n = this.nodeOf(handle);
|
|
905
|
+
const id = peerIdFromString(peer);
|
|
906
|
+
if (n.node.getConnections(id).length)
|
|
907
|
+
return;
|
|
908
|
+
const mas = addrs.flatMap((a) => { try {
|
|
909
|
+
const m = multiaddr(a);
|
|
910
|
+
return [m.getComponents().some((c) => c.name === "p2p") ? m : m.encapsulate(`/p2p/${peer}`)];
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
return [];
|
|
914
|
+
} });
|
|
915
|
+
await n.node.dial(mas.length ? mas : id, { signal: AbortSignal.timeout(15_000) });
|
|
916
|
+
}
|
|
917
|
+
/** The peers `handle`'s node knows subscribe `topic` (GossipSub's view), as peer ID strings. */
|
|
918
|
+
subscribers(handle, topic) {
|
|
919
|
+
const n = this.byHandle.get(handle);
|
|
920
|
+
return n ? this.pubsub(n).getSubscribers(topic).map((p) => p.toString()) : [];
|
|
921
|
+
}
|
|
922
|
+
/** Write one length-prefixed frame on a dialled stream. */
|
|
923
|
+
async send(handle, id, body) {
|
|
924
|
+
const s = this.nodeOf(handle).handles.get(handle).streams.get(id);
|
|
925
|
+
if (!s)
|
|
926
|
+
throw new Error(`send: no stream ${id}`);
|
|
927
|
+
await s.lp.write(body);
|
|
928
|
+
}
|
|
929
|
+
/** Close a dialled stream (our side), and forget it. */
|
|
930
|
+
async close(handle, id) {
|
|
931
|
+
const hs = this.nodeOf(handle).handles.get(handle);
|
|
932
|
+
const s = hs.streams.get(id);
|
|
933
|
+
if (!s)
|
|
934
|
+
throw new Error(`close: no stream ${id}`);
|
|
935
|
+
hs.streams.delete(id);
|
|
936
|
+
await s.stream.close().catch(() => s.stream.abort(new Error("closed")));
|
|
937
|
+
}
|
|
938
|
+
// ---------------------------------------------------------------- lifecycle
|
|
939
|
+
/** `handle` leaves its node (its beats stopped, its liveness forgotten); the node stops with its last handle, else follows the handles left. */
|
|
940
|
+
async drop(handle) {
|
|
941
|
+
const n = this.byHandle.get(handle);
|
|
942
|
+
if (!n)
|
|
943
|
+
return;
|
|
944
|
+
const hs = n.handles.get(handle);
|
|
945
|
+
n.handles.delete(handle);
|
|
946
|
+
this.byHandle.delete(handle);
|
|
947
|
+
for (const b of hs?.beats.values() ?? [])
|
|
948
|
+
clearInterval(b.timer);
|
|
949
|
+
hs?.beats.clear();
|
|
950
|
+
this.live.drop(handle);
|
|
951
|
+
if (n.handles.size) {
|
|
952
|
+
this.resubscribe(n);
|
|
953
|
+
await this.rehandle(n).catch((e) => this.say(handle, `libp2p: leaving its key's node: ${e.message}`));
|
|
954
|
+
this.say(handle, `libp2p: left the node of its key ${n.id} (${n.handles.size} instances on it)`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
this.nodes.delete(n.id);
|
|
958
|
+
clearInterval(n.timer);
|
|
959
|
+
try {
|
|
960
|
+
await n.node.stop();
|
|
961
|
+
}
|
|
962
|
+
catch (e) {
|
|
963
|
+
this.say(handle, `libp2p: stop: ${e.message}`);
|
|
964
|
+
} // dropped either way
|
|
965
|
+
}
|
|
966
|
+
async stop() {
|
|
967
|
+
this.stopped = true;
|
|
968
|
+
await Promise.all([...this.starting.values()].map((p) => p.catch(() => { }))); // each declare's failure went to its caller: this waits for them to settle
|
|
969
|
+
// Every node stopped at once, its handles with it (nothing to follow on a node that stops).
|
|
970
|
+
await Promise.all([...this.nodes.values()].map(async (n) => {
|
|
971
|
+
for (const [handle, hs] of n.handles) {
|
|
972
|
+
for (const b of hs.beats.values())
|
|
973
|
+
clearInterval(b.timer);
|
|
974
|
+
this.byHandle.delete(handle);
|
|
975
|
+
this.live.drop(handle);
|
|
976
|
+
}
|
|
977
|
+
n.handles.clear();
|
|
978
|
+
this.nodes.delete(n.id);
|
|
979
|
+
clearInterval(n.timer);
|
|
980
|
+
try {
|
|
981
|
+
await n.node.stop();
|
|
982
|
+
}
|
|
983
|
+
catch (e) {
|
|
984
|
+
this.say(n.id, `libp2p: stop: ${e.message}`);
|
|
985
|
+
} // dropped either way
|
|
986
|
+
}));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/** A multiaddr on a loopback or private network (dev: the DHT keeps such addresses). */
|
|
990
|
+
function isPrivateAddr(ma) {
|
|
991
|
+
const c = ma.getComponents().find((x) => x.name.startsWith("dns"));
|
|
992
|
+
if (c?.value)
|
|
993
|
+
return c.value === "localhost" || c.value.endsWith(".localhost");
|
|
994
|
+
try {
|
|
995
|
+
return isPrivate(ma);
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
return true;
|
|
999
|
+
} // an address isPrivate cannot classify (no IP component) counts as private, as a dev address
|
|
1000
|
+
}
|
|
1001
|
+
/** An AbortSignal.timeout's abort: the DHT's 20-second rounds end this way, and the next round tries again. */
|
|
1002
|
+
const timedOut = (e) => e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError");
|