1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244 | /**
* The branches of lib/net and lib/log that net.json and log.json never reach.
*
* WHAT THIS IS, AND WHAT IT IS NOT. Every other vector in this directory was
* recorded from the TypeScript build (test/vectors/generate.mjs) and is
* therefore a CROSS-IMPLEMENTATION proof: it can tell "the port is right" from
* "the assertion is wrong". This one cannot. The TS build is retired, so this
* transcript was recorded from the ClojureScript dist and is a REGRESSION pin,
* not a proof of the original semantics.
*
* What makes it trustworthy anyway is HOW it was validated. Before it was
* recorded, this exact script was run against two dists — the one built from the
* sources before Phase 6a's refactor of lib/net and lib/log, and the one built
* after — and the 34 labelled observations were asserted deepEqual. So what is
* frozen below is the behaviour of the code as it stood BEFORE the refactor;
* the refactor is what the differential run had to survive, and this file is
* what keeps it surviving. Treat a future failure the same way as any other
* vector failure: it is a finding, never something to regenerate.
*
* WHY THESE BRANCHES. Measured against test/vectors/net.json, which is the only
* thing pinning the membership machine:
* · its transcript contains the peer states "relayed" and "direct" ONLY.
* conn-state's zero-connection branches — "disconnected" for a peer that had
* proved membership, "connecting" for one that never did — are unreached,
* and so are the two effects that ride along with them (clearing `ready`,
* tearing the outbound channels down). That is the branch a member's
* connection actually takes when it drops.
* · its three `dialProtocol` calls are three DIFFERENT peers, so send-frame's
* channel-REUSE branch never runs: every send in the vector opens a stream.
* · it drives inbound 0x01 frames but never sendBinaryTo or broadcastBinary.
* · it never calls close(), so the bye beacon, the listener removal and the
* timer clearing are unpinned — and close() is the path a consumer that
* opens and closes Nets without a page reload depends on for not leaking.
* · four of the twenty-one `_` seams (frame, relayPeerId, pubsub, and beacon
* called directly) are exposed and never called by any script.
* · after-close no-ops: beacon, dialPeer and scheduleRedial must all become
* inert, and each is a separate `_closed` guard.
* And on the log side, the defensive edges that exist precisely because the
* inputs are untrusted: a falsy entry, an entry with an empty hash, an untyped
* payload, an author reference with no signing key, a CID that throws on
* toString, an unparseable image root.
*
* STILL NOT COVERED by this file or any other, for the record: ping-relay! (no
* seam, and no fixture calls create()), install-timers!/install-topic!/
* install-discovery!/install-wake-listeners! and start() itself, and every path
* through create(). Those are reached only by the browser e2e suite.
*/
import { fakeLibp2p, fakeStack, tickMicro, withClockAsync } from "./fakes.mjs";
const RELAY = "/dns4/signal.example/tcp/443/tls/ws/p2p/12D3KooRELAY";
const T0 = 1_700_000_000_000;
const te = new TextEncoder();
export async function edgeScript({ Net, RoomLog, RoomCrypto, makeLogEncryption, priv }) {
return withClockAsync(T0, async (tick) => {
const out = [];
const say = (label, value) => out.push([label, JSON.parse(JSON.stringify(value ?? null))]);
const rcMe = await RoomCrypto.create("s".repeat(43), "chat.example/v2", "12D3KooME");
const rcPeer = await RoomCrypto.create("s".repeat(43), "chat.example/v2", "12D3KooP");
const hub = fakeLibp2p("12D3KooME");
const events = [];
const ev = {
peerState: (p, s, n) => events.push(["peerState", p, s, n ?? null]),
peerGone: (p) => events.push(["peerGone", p]),
message: (f, p) => events.push(["message", f, p]),
binary: (f, d) => events.push(["binary", f, [...d]]),
peerReady: (p) => events.push(["peerReady", p]),
status: (u) => events.push(["status", u]),
};
const net = new Net(hub.node, rcMe, ev, () => "me", RELAY);
const p = (n) => priv(net, n);
const flush = () => events.splice(0);
const snap = (label) =>
say(label, {
events: flush(),
state: net.debugState().map((r) => [r.id, r.state, r.name ?? null, !!r.ready, !!r.verified, r.conns]),
open: net.openPeers(),
calls: hub.calls.splice(0).map((c) => c.map(String)),
});
// ---- the seams every other script leaves alone -------------------------
say("frame-static", [...p("frame")(0x01, new Uint8Array([7, 8]))]);
say("relayPeerId-none", p("relayPeerId")() === null ? "null" : "something");
say("pubsub-is-the-service", p("pubsub")() === hub.node.services.pubsub);
// ---- refresh-state with ZERO connections -------------------------------
// never verified, never connected: "connecting", and nothing is emitted
const recA = p("recFor")("12D3KooA");
p("refreshState")("12D3KooA", recA, true);
say("refresh-zero-unverified", { state: recA.state, ready: !!recA.ready, out: recA.out.size, events: flush() });
// a verified peer, reached over a direct connection
hub.connect("12D3KooP", `${RELAY}/p2p-circuit/webrtc/p2p/12D3KooP`);
const beacon = await rcPeer.sealMsg({ kind: "presence", op: "beacon", name: "pal", ts: Date.now() });
await p("onTopicMessage")("12D3KooP", te.encode(JSON.stringify(beacon)));
await tickMicro();
snap("peer-verified-direct");
// Verifying it also fired a directed beacon and a fast-hello broadcast, and
// both seal through WebCrypto — which `tickMicro` does NOT drain, since it
// only turns the microtask queue. The first real send below is what settles
// them, so this row carries their traffic as well as its own.
const recP = p("recFor")("12D3KooP");
await net.sendTo("12D3KooP", { kind: "hello", n: 1 });
await tickMicro();
say("first-send-opens-one-stream", {
out: recP.out.size,
calls: hub.calls.splice(0).map((c) => c.map(String)),
});
// ... and from here every send REUSES it: three more sends, two of them
// binary, and not one new dialProtocol. net.json cannot show this — its
// three dialProtocol calls are three different peers, one send each.
await net.sendTo("12D3KooP", { kind: "hello", n: 2 });
await net.sendBinaryTo("12D3KooP", new Uint8Array([1, 2, 3]));
await net.broadcastBinary(new Uint8Array([4, 5]));
await tickMicro();
say("three-more-sends-no-new-stream", {
out: recP.out.size,
calls: hub.calls.splice(0).map((c) => c.map(String)),
});
// now lose the connection: "disconnected", ready cleared, channels torn down
hub.disconnect("12D3KooP");
p("refreshState")("12D3KooP", recP, true);
say("refresh-zero-verified", { state: recP.state, ready: !!recP.ready, out: recP.out.size, events: flush() });
// the same transition with emit? false stays silent
hub.connect("12D3KooP", `${RELAY}/p2p-circuit/p2p/12D3KooP`);
p("refreshState")("12D3KooP", recP, false);
hub.disconnect("12D3KooP");
p("refreshState")("12D3KooP", recP, false);
say("refresh-zero-verified-silent", { state: recP.state, events: flush() });
// peerReady needs a live connection, not just a proven membership
recP.ready = false;
p("maybeReady")("12D3KooP", recP);
say("maybeReady-no-conn", { ready: !!recP.ready, events: flush() });
// ---- presence edges ----------------------------------------------------
// bye from a peer that never proved membership: no peerGone
p("recFor")("12D3KooQ");
p("onPresence")("12D3KooQ", { kind: "presence", op: "bye", name: "q", ts: Date.now() });
snap("bye-unverified");
// a non-string name must not overwrite the stored one
tick(1000);
p("onPresence")("12D3KooP", { kind: "presence", op: "beacon", name: 42, ts: Date.now() });
await tickMicro();
snap("presence-nonstring-name");
tick(1000);
p("onPresence")("12D3KooP", { kind: "presence", op: "beacon", name: "renamed", ts: Date.now() });
await tickMicro();
snap("presence-rename");
// ---- both sweep verdicts, at their boundaries --------------------------
const recS = p("recFor")("12D3KooSTALE");
recS.lastBeacon = Date.now();
p("recFor")("12D3KooFRESH");
tick(20_001); // past the stranger window, not the stale one
p("sweep")();
snap("sweep-stranger-only");
tick(10_000); // ... and now past the stale one too
p("sweep")();
snap("sweep-stale");
// ---- close(), and the no-ops after it ----------------------------------
//
// Nothing in this harness runs start(), so `_windowListeners` and `_timers`
// are still the empty arrays the constructor made and asserting on them
// would pin 0 === 0. Plant what start() would have left behind, and stub the
// three globals close() reaches for, so the teardown is actually observed.
// (Checked: without this, deleting the `_windowListeners` reset from close()
// leaves every assertion here green.)
const removed = [];
const cleared = [];
const saved = {
window: globalThis.window,
clearInterval: globalThis.clearInterval,
clearTimeout: globalThis.clearTimeout,
};
globalThis.window = { removeEventListener: (evt, fn) => removed.push([evt, typeof fn]) };
globalThis.clearInterval = (id) => cleared.push(["interval", id]);
globalThis.clearTimeout = (id) => cleared.push(["timeout", id]);
net._windowListeners = [
["visibilitychange", () => {}],
["pageshow", () => {}],
["online", () => {}],
["pagehide", () => {}],
];
net._timers = [11, 12, 13];
net._redialTimer = 99;
try {
await net.close();
await tickMicro();
} finally {
globalThis.window = saved.window;
globalThis.clearInterval = saved.clearInterval;
globalThis.clearTimeout = saved.clearTimeout;
}
say("after-close", {
events: flush(),
calls: hub.calls.splice(0).map((c) => c.map(String)),
listeners: net._windowListeners?.length ?? null,
removed,
cleared,
});
say("beacon-after-close", await p("beacon")("beacon"));
say("dialPeer-after-close", await p("dialPeer")("12D3KooZ"));
say("scheduleRedial-after-close", p("scheduleRedial")() === undefined ? "undefined" : "?");
say("final-calls", hub.calls.splice(0).map((c) => c.map(String)));
// ---- log: the defensive edges ------------------------------------------
const enc = await makeLogEncryption(rcMe);
const img = await rcMe.imgCipher();
const stack = fakeStack({ entries: [], identities: {} });
const log = new RoomLog(stack.db, stack.orbitdb, stack.helia, img, enc);
const emitted = [];
log.onEntry = (e) => emitted.push(e.hash);
log.onError = (e) => emitted.push(["err", String(e?.message ?? e)]);
const lp = (n) => priv(log, n);
say("emit-falsy", await lp("emit")(null));
say("emit-no-hash", await lp("emit")({ payload: { op: "ADD", value: { t: "chat" } } }));
say("emit-empty-hash", await lp("emit")({ hash: "", payload: { op: "ADD", value: { t: "chat" } } }));
say("entryMeta-null-payload", await log.entryMeta({ hash: "z", payload: null }));
say("entryMeta-untyped", await log.entryMeta({ hash: "z", payload: { op: "ADD", value: { ts: 1 } } }));
say("authorOf-no-key", await lp("authorOf")({ identity: "idOK" }));
say("authorOf-no-ref", await lp("authorOf")({ key: "k" }));
say("identityLocal-no-ref", await lp("identityLocal")({}));
say("rawBlock-throwing-cid", await log.rawBlock({ toString() { throw new Error("boom"); } }));
say("sealedEntryBytes-missing", await log.sealedEntryBytes("nope"));
say("imageDagBlocks-unparseable", await log.imageDagBlocks("not-a-cid"));
say("myIdentityBlock-bad-hash", (() => {
const bad = new RoomLog(stack.db, { ...stack.orbitdb, identity: { hash: "!!!", bytes: new Uint8Array() } },
stack.helia, img, enc);
return bad.myIdentityBlock();
})());
say("emitted", emitted);
say("log-calls", stack.calls.map((c) => c.map(String)));
return out;
});
}
|