@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.
Files changed (176) hide show
  1. package/README.md +522 -2
  2. package/bin/skein-host.js +22 -0
  3. package/bin/skein.js +8 -0
  4. package/dist/client/admin-cli.d.ts +10 -0
  5. package/dist/client/admin-cli.js +370 -0
  6. package/dist/client/admin.d.ts +139 -0
  7. package/dist/client/admin.js +256 -0
  8. package/dist/client/bundle.d.ts +9 -0
  9. package/dist/client/bundle.js +34 -0
  10. package/dist/client/cli.d.ts +44 -0
  11. package/dist/client/cli.js +306 -0
  12. package/dist/client/client.d.ts +161 -0
  13. package/dist/client/client.js +237 -0
  14. package/dist/client/config.d.ts +18 -0
  15. package/dist/client/config.js +31 -0
  16. package/dist/client/conversation.d.ts +62 -0
  17. package/dist/client/conversation.js +113 -0
  18. package/dist/client/raw.d.ts +94 -0
  19. package/dist/client/raw.js +154 -0
  20. package/dist/client/target.d.ts +34 -0
  21. package/dist/client/target.js +158 -0
  22. package/dist/dev/cli.d.ts +8 -0
  23. package/dist/dev/cli.js +248 -0
  24. package/dist/dev/scan.d.ts +15 -0
  25. package/dist/dev/scan.js +173 -0
  26. package/dist/envelope-cbor.d.ts +110 -0
  27. package/dist/envelope-cbor.js +160 -0
  28. package/dist/envelope.d.ts +38 -0
  29. package/dist/envelope.js +101 -0
  30. package/dist/host/arc.d.ts +131 -0
  31. package/dist/host/arc.js +425 -0
  32. package/dist/host/billing.d.ts +102 -0
  33. package/dist/host/billing.js +167 -0
  34. package/dist/host/boot.d.ts +246 -0
  35. package/dist/host/boot.js +643 -0
  36. package/dist/host/cli.d.ts +57 -0
  37. package/dist/host/cli.js +1253 -0
  38. package/dist/host/clock.d.ts +3 -0
  39. package/dist/host/clock.js +8 -0
  40. package/dist/host/control.d.ts +45 -0
  41. package/dist/host/control.js +170 -0
  42. package/dist/host/cron.d.ts +99 -0
  43. package/dist/host/cron.js +196 -0
  44. package/dist/host/deliver.d.ts +53 -0
  45. package/dist/host/deliver.js +251 -0
  46. package/dist/host/deploy.d.ts +29 -0
  47. package/dist/host/deploy.js +55 -0
  48. package/dist/host/door-answer.d.ts +24 -0
  49. package/dist/host/door-answer.js +21 -0
  50. package/dist/host/events.d.ts +89 -0
  51. package/dist/host/events.js +222 -0
  52. package/dist/host/fake-arcade.d.ts +40 -0
  53. package/dist/host/fake-arcade.js +149 -0
  54. package/dist/host/fake-discovery.d.ts +4 -0
  55. package/dist/host/fake-discovery.js +58 -0
  56. package/dist/host/feeds.d.ts +127 -0
  57. package/dist/host/feeds.js +422 -0
  58. package/dist/host/frontdoor.d.ts +92 -0
  59. package/dist/host/frontdoor.js +172 -0
  60. package/dist/host/genesis.d.ts +259 -0
  61. package/dist/host/genesis.js +353 -0
  62. package/dist/host/handles.d.ts +40 -0
  63. package/dist/host/handles.js +88 -0
  64. package/dist/host/hostenv.d.ts +30 -0
  65. package/dist/host/hostenv.js +83 -0
  66. package/dist/host/image-chain.d.ts +127 -0
  67. package/dist/host/image-chain.js +440 -0
  68. package/dist/host/install.d.ts +22 -0
  69. package/dist/host/install.js +139 -0
  70. package/dist/host/instances.d.ts +227 -0
  71. package/dist/host/instances.js +430 -0
  72. package/dist/host/kernel.d.ts +246 -0
  73. package/dist/host/kernel.js +603 -0
  74. package/dist/host/liveness.d.ts +65 -0
  75. package/dist/host/liveness.js +155 -0
  76. package/dist/host/manifest.d.ts +160 -0
  77. package/dist/host/manifest.js +597 -0
  78. package/dist/host/p2p.d.ts +337 -0
  79. package/dist/host/p2p.js +1002 -0
  80. package/dist/host/packet.d.ts +121 -0
  81. package/dist/host/packet.js +562 -0
  82. package/dist/host/plan.d.ts +154 -0
  83. package/dist/host/plan.js +418 -0
  84. package/dist/host/providers.d.ts +238 -0
  85. package/dist/host/providers.js +565 -0
  86. package/dist/host/roster.d.ts +72 -0
  87. package/dist/host/roster.js +172 -0
  88. package/dist/host/router.d.ts +773 -0
  89. package/dist/host/router.js +2389 -0
  90. package/dist/host/signer.d.ts +49 -0
  91. package/dist/host/signer.js +167 -0
  92. package/dist/host/store-sqlite.d.ts +39 -0
  93. package/dist/host/store-sqlite.js +170 -0
  94. package/dist/host/stores.d.ts +43 -0
  95. package/dist/host/stores.js +342 -0
  96. package/dist/host/sync.d.ts +121 -0
  97. package/dist/host/sync.js +467 -0
  98. package/dist/host/testbeef.d.ts +21 -0
  99. package/dist/host/testbeef.js +37 -0
  100. package/dist/host/testhost.d.ts +149 -0
  101. package/dist/host/testhost.js +211 -0
  102. package/dist/host/vcdiff.d.ts +9 -0
  103. package/dist/host/vcdiff.js +210 -0
  104. package/dist/host/wasm-kernel.d.ts +87 -0
  105. package/dist/host/wasm-kernel.js +296 -0
  106. package/dist/host/wasm-worker.d.ts +1 -0
  107. package/dist/host/wasm-worker.js +186 -0
  108. package/dist/peers/cron.d.ts +33 -0
  109. package/dist/peers/cron.js +75 -0
  110. package/dist/peers/infer-main.d.ts +2 -0
  111. package/dist/peers/infer-main.js +74 -0
  112. package/dist/peers/infer.d.ts +163 -0
  113. package/dist/peers/infer.js +367 -0
  114. package/dist/runtime/beef.d.ts +28 -0
  115. package/dist/runtime/beef.js +107 -0
  116. package/dist/runtime/bitcoin.d.ts +36 -0
  117. package/dist/runtime/bitcoin.js +141 -0
  118. package/dist/runtime/cid.d.ts +14 -0
  119. package/dist/runtime/cid.js +51 -0
  120. package/dist/runtime/dispatch.d.ts +92 -0
  121. package/dist/runtime/dispatch.js +142 -0
  122. package/dist/runtime/envelope.d.ts +60 -0
  123. package/dist/runtime/envelope.js +101 -0
  124. package/dist/runtime/heads.d.ts +25 -0
  125. package/dist/runtime/heads.js +37 -0
  126. package/dist/runtime/identity.d.ts +28 -0
  127. package/dist/runtime/identity.js +67 -0
  128. package/dist/runtime/index-store.d.ts +152 -0
  129. package/dist/runtime/index-store.js +715 -0
  130. package/dist/runtime/log.d.ts +62 -0
  131. package/dist/runtime/log.js +104 -0
  132. package/dist/runtime/programs.d.ts +30 -0
  133. package/dist/runtime/programs.js +61 -0
  134. package/dist/runtime/records.d.ts +135 -0
  135. package/dist/runtime/records.js +149 -0
  136. package/dist/runtime/sqlite.d.ts +33 -0
  137. package/dist/runtime/sqlite.js +604 -0
  138. package/dist/runtime/store.d.ts +141 -0
  139. package/dist/runtime/store.js +24 -0
  140. package/dist/runtime/syscalls.d.ts +9 -0
  141. package/dist/runtime/syscalls.js +41 -0
  142. package/dist/runtime/tree.d.ts +60 -0
  143. package/dist/runtime/tree.js +196 -0
  144. package/dist/runtime/types.d.ts +100 -0
  145. package/dist/runtime/types.js +7 -0
  146. package/dist/testapps.d.ts +101 -0
  147. package/dist/testapps.js +275 -0
  148. package/dist/testkit.d.ts +25 -0
  149. package/dist/testkit.js +48 -0
  150. package/dist/wallet.d.ts +35 -0
  151. package/dist/wallet.js +87 -0
  152. package/images/README.md +68 -0
  153. package/images/default/README.md +38 -0
  154. package/images/default/bin/frontdoor.cid +1 -0
  155. package/images/default/bin/frontdoor.json +5 -0
  156. package/images/default/bin/messagebox.cid +1 -0
  157. package/images/default/bin/messagebox.json +8 -0
  158. package/images/default/bin/wallet.cid +1 -0
  159. package/images/default/bin/wallet.json +14 -0
  160. package/images/default/etc/config.json +1 -0
  161. package/images/default/etc/dispatch.json +82 -0
  162. package/images/host/etc/apps.json +8 -0
  163. package/kernel-zig/README.md +1247 -0
  164. package/kernel-zig/web/client.js +65 -0
  165. package/kernel-zig/web/kernel.d.ts +59 -0
  166. package/kernel-zig/web/kernel.js +329 -0
  167. package/kernel-zig/web/store.js +67 -0
  168. package/kernel-zig/web/worker.js +79 -0
  169. package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
  170. package/package.json +77 -4
  171. package/wasm/README.md +35 -0
  172. package/wasm/frontdoor.wasm +0 -0
  173. package/wasm/messagebox.wasm +0 -0
  174. package/wasm/resolve.wasm +0 -0
  175. package/wasm/wallet.wasm +0 -0
  176. package/wasm/wire-probe.wasm +0 -0
@@ -0,0 +1,467 @@
1
+ // Overlay sync (shruggr/skein#150; the settled design, David 2026-10-30,
2
+ // shruggr/skein-overlay 0.20.0): the host half of keeping an overlay topic's
3
+ // admitted set equal across peers. The overlay engine keeps, per topic,
4
+ // BRC-136's anchors and answers the sync protocol's frames; its beacon on
5
+ // `<topic>-live` carries `{tipHeight, tipAnchor, tailDigest}`. No timer, no
6
+ // retry on a clock, no backoff anywhere here ("a non-starter").
7
+ //
8
+ // announce, then a peer's `admit {txid, topics}` on `<topic>` (router.ts
9
+ // fetch p2pInbound takes it, never delivers it to the instance):
10
+ // unless the instance holds the txid (the engine's `holds`
11
+ // read, by kernel call) or a fetch of it is under way, the
12
+ // host asks that peer `{kind: "tx", topic, txid}` and hands
13
+ // the BEEF to the overlay's receive row
14
+ // /skein/overlay/beef/1.0.0 as from that peer. Per (handle,
15
+ // txid) it keeps the SET of peers whose admit named it:
16
+ // fetch from one; on a failure (no answer, no `beef`, the
17
+ // delivery not taken) the next; the set exhausted, it stops
18
+ // and waits for a new admit to add a peer (then one more
19
+ // try, from that peer). A delivery taken (accepted, or the
20
+ // transaction now held) drops the entry. Nothing logged in
21
+ // the instance but the delivered BEEF.
22
+ // the beat a beat newly kept from another peer on `<topic>-live`
23
+ // starts a reconcile from it — at most one per (handle,
24
+ // topic, peer) at a time, and a new one only when that
25
+ // peer's LATEST beat body differs from the one that started
26
+ // (or, at its end, was the latest for) the last one.
27
+ // mined history the reconcile's first half: when its tip anchor is not
28
+ // ours, the locator on the direct stream
29
+ // /skein/overlay/sync/1.0.0 (our anchors at tip, tip-1,
30
+ // tip-2, tip-4 … genesis; the peer answers the highest height
31
+ // where it agrees, its genesis and its tip), then the
32
+ // peer's admitted lists above the agreed height, by ranges
33
+ // of `rangeHeights` spread across the peers whose last beat
34
+ // has the same tip anchor; each block checked against our
35
+ // headers (its hash, the BUMP's root for every txid) before
36
+ // any transaction is fetched (a height we hold no header for
37
+ // yet ends the range: the next changed beat resumes it); each
38
+ // txid we lack fetched from the peer that listed it into the
39
+ // receive row, block by block in height order. A peer whose
40
+ // list fails the check is dropped for the rest of the host's
41
+ // run.
42
+ // the tail the reconcile's second half: when the mined side is equal
43
+ // (its tip anchor is ours at its height, or neither has one)
44
+ // and its `tailDigest` is not ours (our own beacon's body),
45
+ // `{kind: "tail", topic}` to that peer, and each txid we do
46
+ // not hold fetched as an announced one (the per-txid set,
47
+ // that peer).
48
+ // a start `reconnect`: the peers a topic's beats came from (host.db,
49
+ // router.ts) asked at once — the locator and the tail —
50
+ // without waiting for a beat.
51
+ // answering not the host's (#154): the engine answers the stream
52
+ // through the front door, on the libp2p row an app declares.
53
+ // our own reads the locator, "what we lack" and "do we hold it" read our
54
+ // own overlay's state by a kernel call of its engine (fn
55
+ // `anchors`, `admitted`, `holds`; router.ts `overlayRead`).
56
+ //
57
+ // The wire, one dag-cbor frame each way:
58
+ //
59
+ // {kind: "locator", topic, locator: [{height, anchor: bytes(32)}]}
60
+ // → {kind: "locator", topic, genesisHeight?, agreedHeight?, tipHeight?}
61
+ // {kind: "admitted", topic, fromHeight, toHeight, limit}
62
+ // → {kind: "admitted", topic, blocks: [{height, blockHash, txids, bump}], nextHeight} (hex, as /admitted)
63
+ // {kind: "tx", topic, txid}
64
+ // → {kind: "tx", txid, beef?: bytes} (absent: not held)
65
+ // {kind: "tail", topic}
66
+ // → {kind: "tail", topic, txids: [hex]} the admitted, not mined, in admission order
67
+ // anything else → {kind: "error", message}
68
+ import * as dagCbor from "@ipld/dag-cbor";
69
+ import { MerklePath } from "@bsv/sdk";
70
+ import { peerIdFromMultihash } from "@libp2p/peer-id";
71
+ import * as Digest from "multiformats/hashes/digest";
72
+ import { RECEIVE_PROTOCOL } from "./p2p.js";
73
+ export const SYNC_PROTOCOL = "/skein/overlay/sync/1.0.0";
74
+ /** The overlay's receive row (skein-overlay etc/app.json): the host's own entry, one Atomic BEEF per frame; never served to peers. */
75
+ export const BEEF_PROTOCOL = RECEIVE_PROTOCOL;
76
+ const LIVE = "-live";
77
+ const hex = (b) => Buffer.from(b).toString("hex");
78
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
79
+ const HEX64 = /^[0-9a-f]{64}$/;
80
+ /** A peer ID's string from its multihash bytes. */
81
+ export const peerOf = (mh) => peerIdFromMultihash(Digest.decode(mh)).toString();
82
+ /**
83
+ * An `admit` announcement on `<topic>` (skein-overlay gossip.zig: dag-cbor `{kind: "admit", txid,
84
+ * topics: {<topic>: {outputsToAdmit, coinsToRetain}}}`) → its txid, or undefined when the body is
85
+ * not one naming `topic`.
86
+ */
87
+ export function admitOf(topic, body) {
88
+ let v;
89
+ try {
90
+ v = dagCbor.decode(body);
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ if (!v || typeof v !== "object")
96
+ return undefined;
97
+ const m = v;
98
+ if (m.kind !== "admit" || typeof m.txid !== "string" || !HEX64.test(m.txid))
99
+ return undefined;
100
+ if (!m.topics || typeof m.topics !== "object" || !(topic in m.topics))
101
+ return undefined;
102
+ return m.txid;
103
+ }
104
+ export class OverlaySync {
105
+ o;
106
+ /** `${handle} ${txid}` → the announced transaction, while it is not taken. */
107
+ announced = new Map();
108
+ /** Reconciles running: `${handle} ${topic} ${peer}`. */
109
+ running = new Set();
110
+ /** The beat body (hex) that started, or ended, the last reconcile per `${handle} ${topic} ${peer}`. */
111
+ seen = new Map();
112
+ /** Peers caught serving a list that does not check: per handle. */
113
+ dropped = new Map();
114
+ inflight = new Set();
115
+ stopped = false;
116
+ constructor(o) { this.o = o; }
117
+ say(handle, line) { this.o.log?.(handle, `sync: ${line}`); }
118
+ track(p) {
119
+ const q = p.catch(() => { });
120
+ this.inflight.add(q);
121
+ void q.finally(() => this.inflight.delete(q));
122
+ }
123
+ /** Whether anything is under way (a reconcile, a fetch). */
124
+ busy() { return this.inflight.size > 0; }
125
+ async idle() { while (this.inflight.size)
126
+ await Promise.all([...this.inflight]); }
127
+ stop() { this.stopped = true; this.announced.clear(); }
128
+ /** The announced transactions not taken yet, per handle (tests). */
129
+ pending(handle) {
130
+ return [...this.announced].filter(([k]) => k.startsWith(`${handle} `)).map(([k, e]) => ({ txid: k.slice(handle.length + 1), peers: [...e.peers], fetching: e.fetching }));
131
+ }
132
+ isDropped(handle, peer) { return this.dropped.get(handle)?.has(peer) ?? false; }
133
+ // ---------------------------------------------------------------- announce, then fetch
134
+ /**
135
+ * A peer's `admit` for `txid` on `topic` (its announcement), at `handle` whose overlay `app` runs
136
+ * the topic: fetched from that peer unless the instance holds it or a fetch of it is under way;
137
+ * the peer joins the transaction's set either way.
138
+ */
139
+ admit(handle, app, topic, txid, peer) {
140
+ const p = this.announce(handle, app, topic, txid, peer);
141
+ if (p)
142
+ this.track(p);
143
+ }
144
+ /** `admit`'s work: the fetch it started, if any (the tail awaits it). */
145
+ announce(handle, app, topic, txid, peer) {
146
+ if (this.stopped || peer === this.o.self(handle))
147
+ return undefined;
148
+ const k = `${handle} ${txid}`;
149
+ const e = this.announced.get(k);
150
+ if (e) {
151
+ if (!e.peers.includes(peer))
152
+ e.peers.push(peer);
153
+ return e.fetching ? undefined : this.pursue(handle, k, e);
154
+ }
155
+ const fresh = { app, topic, peers: [peer], tried: new Set(), fetching: true };
156
+ this.announced.set(k, fresh);
157
+ return (async () => {
158
+ if (await this.holds(handle, app, txid)) {
159
+ this.announced.delete(k);
160
+ fresh.fetching = false;
161
+ return;
162
+ }
163
+ await this.pursue(handle, k, fresh);
164
+ })();
165
+ }
166
+ /** Whether `handle`'s overlay state reaches `txid` (its engine's `holds`). */
167
+ async holds(handle, app, txid) {
168
+ try {
169
+ const h = await this.o.local(handle, app, "holds", { txid });
170
+ return typeof h?.txid === "string";
171
+ }
172
+ catch (e) {
173
+ this.say(handle, `holds ${txid}: ${e.message}`);
174
+ return false;
175
+ }
176
+ }
177
+ /** Fetch an announced transaction from its peers, one after another, until one delivery is taken or none is left. */
178
+ async pursue(handle, k, e) {
179
+ e.fetching = true;
180
+ try {
181
+ for (;;) {
182
+ if (this.stopped || this.announced.get(k) !== e)
183
+ return;
184
+ const peer = e.peers.find((p) => !e.tried.has(p));
185
+ if (!peer)
186
+ return; // exhausted: a new admit adds a peer
187
+ e.tried.add(peer);
188
+ const txid = k.slice(handle.length + 1);
189
+ const taken = await this.fetchInto(handle, peer, e.topic, txid).catch((x) => { this.say(handle, `${e.topic}: ${txid} from ${peer}: ${x.message}`); return false; });
190
+ if (taken || await this.holds(handle, e.app, txid)) {
191
+ this.announced.delete(k);
192
+ return;
193
+ }
194
+ }
195
+ }
196
+ finally {
197
+ e.fetching = false;
198
+ }
199
+ }
200
+ // ---------------------------------------------------------------- the beat and the reconcile
201
+ /** A beat newly kept on `liveTopic` from another peer: a reconcile from it when its body changed since the last. */
202
+ beat(handle, liveTopic, b) {
203
+ if (this.stopped || !liveTopic.endsWith(LIVE))
204
+ return;
205
+ const app = this.o.overlayOf(handle, liveTopic);
206
+ if (!app || this.isDropped(handle, b.from) || b.from === this.o.self(handle))
207
+ return;
208
+ this.start(handle, app, liveTopic.slice(0, -LIVE.length), b.from, b.body);
209
+ }
210
+ /**
211
+ * A start (router.ts, the persisted peers): each peer that beat on the topic before asked at once,
212
+ * the locator and the tail, without a beat. One reconcile per peer, as for a beat.
213
+ */
214
+ reconnect(handle, app, topic, peer) {
215
+ if (this.stopped || this.isDropped(handle, peer) || peer === this.o.self(handle))
216
+ return;
217
+ this.start(handle, app, topic, peer, undefined);
218
+ }
219
+ start(handle, app, topic, peer, body) {
220
+ const k = `${handle} ${topic} ${peer}`;
221
+ if (this.running.has(k))
222
+ return; // its end looks at the peer's latest beat
223
+ if (body && this.seen.get(k) === hex(body))
224
+ return;
225
+ if (body)
226
+ this.seen.set(k, hex(body));
227
+ this.running.add(k);
228
+ this.track(this.reconcile(handle, app, topic, peer, body)
229
+ .catch((e) => this.say(handle, `${topic} from ${peer}: ${e.message}`))
230
+ .finally(() => {
231
+ this.running.delete(k);
232
+ // A beat that came while it ran: the latest, if it is not the one already reconciled from.
233
+ const latest = this.o.beats(handle, `${topic}${LIVE}`).find((x) => x.from === peer);
234
+ if (latest && !this.stopped && this.seen.get(k) !== hex(latest.body))
235
+ this.start(handle, app, topic, peer, latest.body);
236
+ }));
237
+ }
238
+ async anchors(handle, app, topic, heights) {
239
+ return await this.o.local(handle, app, "anchors", { topic, ...(heights ? { heights: heights.join(",") } : {}) });
240
+ }
241
+ async ask(handle, peer, req) {
242
+ const a = dagCbor.decode(await this.o.request(handle, peer, dagCbor.encode(req)));
243
+ if (a.kind === "error")
244
+ throw new Error(`${peer}: ${String(a.message)}`);
245
+ if (a.kind !== req.kind)
246
+ throw new Error(`${peer}: answered ${String(a.kind)} to ${String(req.kind)}`);
247
+ return a;
248
+ }
249
+ /** The beat body's sync fields, or undefined when it does not decode. */
250
+ static fields(body) {
251
+ if (!body)
252
+ return undefined;
253
+ try {
254
+ return dagCbor.decode(body);
255
+ }
256
+ catch {
257
+ return undefined;
258
+ }
259
+ }
260
+ /**
261
+ * One reconcile with `peer` on `topic`: its mined history (the locator and the pulls) when its
262
+ * tip anchor is not ours, then its unmined tail when the mined side is equal and its tail digest
263
+ * is not ours. No beat (a start): both asked.
264
+ */
265
+ async reconcile(handle, app, topic, peer, body) {
266
+ const theirs = OverlaySync.fields(body);
267
+ if (body && !theirs)
268
+ return;
269
+ const hasTip = (b) => typeof b?.tipHeight === "number" && b.tipAnchor instanceof Uint8Array;
270
+ if (!theirs) {
271
+ await this.catchUp(handle, app, topic, peer);
272
+ }
273
+ else if (hasTip(theirs)) {
274
+ const at = await this.anchors(handle, app, topic, [theirs.tipHeight]);
275
+ if (!at.anchors.some((x) => x.height === theirs.tipHeight && x.anchor === hex(theirs.tipAnchor))) {
276
+ const done = await this.catchUp(handle, app, topic, peer, { height: theirs.tipHeight, anchor: hex(theirs.tipAnchor) });
277
+ if (!done)
278
+ return; // a range left for a later beat: the mined side is not equal yet
279
+ }
280
+ }
281
+ if (this.stopped || this.isDropped(handle, peer))
282
+ return;
283
+ if (theirs) {
284
+ // The mined side equal: its tip anchor ours at its height (after any pull), or neither has one.
285
+ if (hasTip(theirs)) {
286
+ const at = await this.anchors(handle, app, topic, [theirs.tipHeight]);
287
+ if (!at.anchors.some((x) => x.height === theirs.tipHeight && x.anchor === hex(theirs.tipAnchor)))
288
+ return;
289
+ }
290
+ else if (typeof (await this.anchors(handle, app, topic)).tipHeight === "number")
291
+ return;
292
+ const ours = OverlaySync.fields(this.o.ownBeat(handle, `${topic}${LIVE}`));
293
+ const a = theirs.tailDigest instanceof Uint8Array ? hex(theirs.tailDigest) : "";
294
+ const b = ours?.tailDigest instanceof Uint8Array ? hex(ours.tailDigest) : "";
295
+ if (a === b)
296
+ return;
297
+ }
298
+ await this.tail(handle, app, topic, peer);
299
+ }
300
+ /** The peer's unmined tail: each txid we do not hold fetched as an announced one from that peer. */
301
+ async tail(handle, app, topic, peer) {
302
+ const t = await this.ask(handle, peer, { kind: "tail", topic });
303
+ const txids = (Array.isArray(t.txids) ? t.txids : []).filter((x) => typeof x === "string" && HEX64.test(x));
304
+ let asked = 0;
305
+ // One at a time, in its order (admission order: parents first): each through the per-txid set.
306
+ for (const txid of txids) {
307
+ if (this.stopped)
308
+ return;
309
+ const known = this.announced.has(`${handle} ${txid}`);
310
+ if (!known && await this.holds(handle, app, txid))
311
+ continue;
312
+ asked++;
313
+ await this.announce(handle, app, topic, txid, peer);
314
+ }
315
+ if (asked)
316
+ this.say(handle, `${topic}: ${peer}'s tail: ${txids.length} unmined, ${asked} asked`);
317
+ }
318
+ /**
319
+ * Catch `topic` up from `peer`: the locator, then the pulls. `tip`: the tip its beat named (the
320
+ * ranges are spread across the peers whose last beat names the same); none (a start): that peer
321
+ * alone. → whether every range was taken (a height we hold no header for, or a list that fails,
322
+ * leaves it unfinished).
323
+ */
324
+ async catchUp(handle, app, topic, peer, tip) {
325
+ const mine = await this.anchors(handle, app, topic);
326
+ const answer = await this.ask(handle, peer, {
327
+ kind: "locator", topic,
328
+ locator: mine.anchors.map((x) => ({ height: x.height, anchor: Uint8Array.from(Buffer.from(x.anchor, "hex")) })),
329
+ });
330
+ if (typeof answer.tipHeight !== "number" || typeof answer.genesisHeight !== "number")
331
+ return true;
332
+ const from = typeof answer.agreedHeight === "number" ? answer.agreedHeight + 1 : answer.genesisHeight;
333
+ const to = answer.tipHeight;
334
+ if (from > to)
335
+ return true;
336
+ // The peers whose last beat has the same tip anchor: the ranges are spread across them.
337
+ const agreeing = [peer, ...(tip ? this.o.beats(handle, `${topic}${LIVE}`).filter((b) => {
338
+ if (b.from === peer || b.from === this.o.self(handle) || this.isDropped(handle, b.from))
339
+ return false;
340
+ const t = OverlaySync.fields(b.body);
341
+ return t?.tipHeight === tip.height && t.tipAnchor instanceof Uint8Array && hex(t.tipAnchor) === tip.anchor;
342
+ }).map((b) => b.from) : [])];
343
+ this.say(handle, `${topic}: agreed with ${peer} through ${from - 1}; pulling ${from}..${to} from ${agreeing.length} peer(s)`);
344
+ const span = this.o.rangeHeights ?? 1_000;
345
+ const ranges = [];
346
+ for (let h = from, i = 0; h <= to; h += span, i++)
347
+ ranges.push({ from: h, to: Math.min(to, h + span - 1), peer: agreeing[i % agreeing.length] });
348
+ const lists = await Promise.all(ranges.map(async (r) => ({ r, blocks: await this.pullRange(handle, topic, r.peer, r.from, r.to).catch((e) => { this.say(handle, `${topic}: ${r.from}..${r.to} from ${r.peer}: ${e.message}`); return undefined; }) })));
349
+ let whole = true;
350
+ for (const { r, blocks } of lists) {
351
+ if (!blocks) {
352
+ whole = false;
353
+ continue;
354
+ }
355
+ const ok = await this.checked(handle, topic, r.peer, blocks);
356
+ if (!ok) {
357
+ whole = false;
358
+ continue;
359
+ }
360
+ const ours = new Set();
361
+ for (let h = r.from; h !== null && h <= r.to;) {
362
+ const page = await this.o.local(handle, app, "admitted", { topic, fromHeight: String(h), toHeight: String(r.to) });
363
+ for (const b of page.blocks)
364
+ for (const t of b.txids)
365
+ ours.add(t);
366
+ h = page.nextHeight;
367
+ }
368
+ for (const b of blocks) {
369
+ const lacking = b.txids.filter((t) => !ours.has(t));
370
+ for (const txid of lacking)
371
+ await this.fetchInto(handle, r.peer, topic, txid);
372
+ if (lacking.length)
373
+ await this.landed(handle, app, topic, b.height, lacking);
374
+ }
375
+ }
376
+ return whole;
377
+ }
378
+ /** The peer's admitted lists for `from..to`, every page. */
379
+ async pullRange(handle, topic, peer, from, to) {
380
+ const out = [];
381
+ for (let h = from; h !== null && h <= to;) {
382
+ const a = await this.ask(handle, peer, { kind: "admitted", topic, fromHeight: h, toHeight: to, limit: 1_000 });
383
+ const blocks = (a.blocks ?? []);
384
+ out.push(...blocks);
385
+ const next = a.nextHeight;
386
+ if (typeof next === "number" && next <= h)
387
+ throw new Error("nextHeight does not advance");
388
+ h = typeof next === "number" ? next : null;
389
+ }
390
+ return out;
391
+ }
392
+ /**
393
+ * Each block against our headers before any transaction is fetched: its hash ours at its height,
394
+ * the BUMP at that height and every listed txid in it with our merkle root. A failure drops the
395
+ * peer; a height we hold no header for yet stops the range (the next changed beat resumes it).
396
+ * → whether all checked.
397
+ */
398
+ async checked(handle, topic, peer, blocks) {
399
+ for (const b of blocks) {
400
+ if (!(await this.o.header(handle, b.height))) {
401
+ this.say(handle, `${topic}: no header of ours at ${b.height} yet: ${peer}'s range left for its next changed beat`);
402
+ return false;
403
+ }
404
+ const why = await this.problem(handle, b);
405
+ if (!why)
406
+ continue;
407
+ this.say(handle, `${topic}: ${peer}'s list at ${b.height}: ${why}; peer dropped`);
408
+ let d = this.dropped.get(handle);
409
+ if (!d)
410
+ this.dropped.set(handle, d = new Set());
411
+ d.add(peer);
412
+ return false;
413
+ }
414
+ return true;
415
+ }
416
+ async problem(handle, b) {
417
+ const h = await this.o.header(handle, b.height);
418
+ if (!h)
419
+ return "no header of ours at that height";
420
+ if (h.hash !== b.blockHash)
421
+ return `block ${b.blockHash} is not ours (${h.hash})`;
422
+ if (!b.txids.length)
423
+ return "no txids";
424
+ let mp;
425
+ try {
426
+ mp = MerklePath.fromHex(b.bump);
427
+ }
428
+ catch (e) {
429
+ return `a BUMP that does not parse: ${e.message}`;
430
+ }
431
+ if (mp.blockHeight !== b.height)
432
+ return `a BUMP at ${mp.blockHeight}`;
433
+ for (const t of b.txids) {
434
+ let root;
435
+ try {
436
+ root = mp.computeRoot(t);
437
+ }
438
+ catch {
439
+ return `${t} is not in its BUMP`;
440
+ }
441
+ if (root !== h.root)
442
+ return `${t}'s root is not our header's`;
443
+ }
444
+ return undefined;
445
+ }
446
+ /** One transaction asked of `peer` and handed to the receive row as from it. → whether it was accepted. */
447
+ async fetchInto(handle, peer, topic, txid) {
448
+ const a = await this.ask(handle, peer, { kind: "tx", topic, txid });
449
+ if (!(a.beef instanceof Uint8Array))
450
+ return false;
451
+ const r = await this.o.deliver(handle, peer, a.beef);
452
+ if (r.verdict !== "accept")
453
+ this.say(handle, `${topic}: ${txid} from ${peer}: ${r.verdict}${r.reason ? ` (${r.reason})` : ""}`);
454
+ return r.verdict === "accept";
455
+ }
456
+ /** Until the topic lists `txids` at `height` (admitted: each delivery is judged on its own thread), or 10 s. */
457
+ async landed(handle, app, topic, height, txids) {
458
+ const end = Date.now() + 10_000;
459
+ while (Date.now() < end && !this.stopped) {
460
+ const page = await this.o.local(handle, app, "admitted", { topic, fromHeight: String(height), toHeight: String(height) });
461
+ const have = new Set(page.blocks.flatMap((b) => b.txids));
462
+ if (txids.every((t) => have.has(t)))
463
+ return;
464
+ await sleep(100);
465
+ }
466
+ }
467
+ }
@@ -0,0 +1,21 @@
1
+ import type { CID } from "multiformats/cid";
2
+ export declare const HEIGHT_170 = 170;
3
+ export declare const HEADER_170: Uint8Array<ArrayBuffer>;
4
+ export declare const TX_170: Uint8Array<ArrayBuffer>;
5
+ /** The transaction's `bitcoin-tx` block CID (its txid). */
6
+ export declare const TX_170_CID: CID;
7
+ /**
8
+ * BEEF V1 of TX_170 with its BUMP (height 170, two leaves: the coinbase, the transaction). `bad`: one
9
+ * byte of the coinbase's hash flipped, so the BUMP's merkle root is not the header's.
10
+ */
11
+ export declare function beef170(o?: {
12
+ bad?: boolean;
13
+ }): Uint8Array;
14
+ /** The chain state holding block 170's header: its blocks (the header, the MST node, the record) and the record's CID. */
15
+ export declare function chainState170(): {
16
+ blocks: Array<{
17
+ cid: CID;
18
+ bytes: Uint8Array;
19
+ }>;
20
+ state: CID;
21
+ };
@@ -0,0 +1,37 @@
1
+ // A BEEF whose BUMP checks, and the chain state it checks against (#121, #164): block 170's header,
2
+ // its spend (the first person-to-person transaction) and a BUMP proving it beside the coinbase —
3
+ // kernel-zig/src/door_test.zig's fixture, for the TS tests and equiv drivers. `chainState170` is the
4
+ // chain app's record shape ({kind: "chain-state", maps: {headers: <MST root>}}, the MST height (u32
5
+ // big-endian) → the header), one header, as blocks to put (root's `objects`, or the store directly)
6
+ // and the record to point the head `chain/state` at.
7
+ import { createHash } from "node:crypto";
8
+ import { BITCOIN_BLOCK, BITCOIN_TX, hashCid } from "../runtime/bitcoin.js";
9
+ import { encode } from "../runtime/cid.js";
10
+ const unhex = (s) => Uint8Array.from(Buffer.from(s, "hex"));
11
+ const sha256d = (b) => Uint8Array.from(createHash("sha256").update(createHash("sha256").update(b).digest()).digest());
12
+ export const HEIGHT_170 = 170;
13
+ export const HEADER_170 = unhex("0100000055bd840a78798ad0da853f68974f3d183e2bd1db6a842c1feecf222a00000000ff104ccb05421ab93e63f8c3ce5c2c2e9dbb37de2764b3a3175c8166562cac7d51b96a49ffff001d283e9e70");
14
+ export const TX_170 = unhex("0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000004847304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000");
15
+ const COINBASE_170 = unhex("82501c1178fa0b222c1f3d474ec726b832013f0a532b44bb620cce8624a5feb1");
16
+ /** The transaction's `bitcoin-tx` block CID (its txid). */
17
+ export const TX_170_CID = hashCid(BITCOIN_TX, sha256d(TX_170));
18
+ /**
19
+ * BEEF V1 of TX_170 with its BUMP (height 170, two leaves: the coinbase, the transaction). `bad`: one
20
+ * byte of the coinbase's hash flipped, so the BUMP's merkle root is not the header's.
21
+ */
22
+ export function beef170(o = {}) {
23
+ const cb = Uint8Array.from(COINBASE_170);
24
+ if (o.bad)
25
+ cb[0] ^= 0xff;
26
+ const bump = [0xaa, 1, 2, 0, 0, ...cb, 1, 2, ...sha256d(TX_170)];
27
+ return Uint8Array.from([1, 0, 0xbe, 0xef, 1, ...bump, 1, ...TX_170, 1, 0]);
28
+ }
29
+ /** The chain state holding block 170's header: its blocks (the header, the MST node, the record) and the record's CID. */
30
+ export function chainState170() {
31
+ const header = { cid: hashCid(BITCOIN_BLOCK, sha256d(HEADER_170)), bytes: HEADER_170 };
32
+ const key = new Uint8Array(4);
33
+ new DataView(key.buffer).setUint32(0, HEIGHT_170);
34
+ const node = encode([null, [[key, header.cid, null]]]);
35
+ const state = encode({ kind: "chain-state", network: "main", maps: { headers: node.cid } });
36
+ return { blocks: [header, { cid: node.cid, bytes: node.bytes }, { cid: state.cid, bytes: state.bytes }], state: state.cid };
37
+ }
@@ -0,0 +1,149 @@
1
+ import { PrivateKey } from "@bsv/sdk";
2
+ import type { CID } from "multiformats/cid";
3
+ import type { Store } from "../runtime/store.ts";
4
+ import { type PinnedApp } from "../testapps.ts";
5
+ import { HostDb } from "./instances.ts";
6
+ import { Router } from "./router.ts";
7
+ export declare const until: <T>(what: string, f: () => Promise<T | undefined> | T | undefined, ms?: number) => Promise<T>;
8
+ /**
9
+ * The messages an instance took in, in log order (#68): a mail entry of its
10
+ * own (a host that admits one directly), or a message a request's thread
11
+ * routed — the `admit` of the front door's answer on that thread's updates.
12
+ */
13
+ export declare function messagesIn(store: Store): Promise<Array<Record<string, unknown>>>;
14
+ /** A host with instances (mailbox instances among them), each instance's key its own (the signer's stand-in). */
15
+ export declare function testHost(t: {
16
+ after(f: () => unknown): void;
17
+ }, o?: {
18
+ idleMs?: number;
19
+ http?: Router["o"]["http"];
20
+ infer?: string;
21
+ ownerMessagebox?: string;
22
+ ownerKey?: PrivateKey;
23
+ genesis?: Router["o"]["genesis"];
24
+ now?: Router["o"]["now"];
25
+ arc?: Router["o"]["arc"];
26
+ arcRetry?: Router["o"]["arcRetry"];
27
+ headersFeed?: string;
28
+ billing?: Router["o"]["billing"];
29
+ lmdb?: boolean;
30
+ rocks?: boolean;
31
+ sqlite?: boolean;
32
+ }): Promise<{
33
+ home: string;
34
+ db: HostDb;
35
+ router: Router;
36
+ base: string;
37
+ owner: import("@bsv/sdk").WalletInterface;
38
+ ownerKey: PrivateKey;
39
+ ownerId: string;
40
+ lines: string[];
41
+ keyOf: (h: string) => PrivateKey;
42
+ /**
43
+ * An instance's row (host.db kind `agent`: not a mailbox instance), its
44
+ * genesis at first hydration (the owner's mailbox must exist first to be
45
+ * named in it). Its identity key (hex).
46
+ */
47
+ instance(handle: string): string;
48
+ /** The same as `instance`, by its old name (for tests written before the rename). */
49
+ agent(handle: string): string;
50
+ /**
51
+ * An instance booted from an image (#89; default: the default image,
52
+ * images/default): no owner in its genesis, the claim row from anyone.
53
+ * Claim it with a message from the owner's wallet in box `claim`
54
+ * (src/testapps.ts sendPlan of planClaim, #127): its sender owns it.
55
+ */
56
+ image(handle: string, dir?: string): Promise<import("./boot.ts").Booted>;
57
+ /** A mailbox instance's row (a fixture's: its genesis at first hydration; the product path is the manager's create, Router.createInstance image `mailbox`). */
58
+ mailbox(handle: string, whose: string): void;
59
+ /**
60
+ * The host skein (#90, #113, #142): `host` from the host image (the
61
+ * default image with the onboarding app), its genesis naming the owner
62
+ * (the operator's key here), the instance manager and the certifier in
63
+ * its address book; the onboarding app's config at birth over its
64
+ * manifest's (default: domain `localhost`, origin this router's). Its own
65
+ * origin's BRC-169 requests are then the app's.
66
+ */
67
+ hostSkein(config?: Record<string, unknown>): Promise<{
68
+ handle: string;
69
+ identity: string;
70
+ url: string;
71
+ }>;
72
+ origin: (handle: string) => string;
73
+ storeSize(handle: string): number;
74
+ /**
75
+ * Install apps into an instance as the owner (#83: a genesis has no shell,
76
+ * no `run`, no `chat`): by default the shell app and the chat app, through
77
+ * the owner's messages (src/testapps.ts installApps, #124). Hydrates the instance first.
78
+ */
79
+ install(handle: string, apps?: PinnedApp[]): Promise<void>;
80
+ entries(handle: string): Promise<number>;
81
+ }>;
82
+ /**
83
+ * A small world on a test host (the explorer's and the store reader's tests):
84
+ * the owner's mailbox, the inference peer's, and instance `alpha` with the shell
85
+ * app and the chat app installed (#83); `dir` imported into alpha (objects →
86
+ * `main`); a chat whose turn makes one bash
87
+ * tool call (`ls | head -3`) and answers "README and **src**." into the
88
+ * owner's mailbox. The peer answers from a script, over its raw transport.
89
+ */
90
+ export declare function chatWorld(t: {
91
+ after(f: () => unknown): void;
92
+ }, dir: string, o?: {
93
+ sqlite?: boolean;
94
+ }): Promise<{
95
+ h: {
96
+ home: string;
97
+ db: HostDb;
98
+ router: Router;
99
+ base: string;
100
+ owner: import("@bsv/sdk").WalletInterface;
101
+ ownerKey: PrivateKey;
102
+ ownerId: string;
103
+ lines: string[];
104
+ keyOf: (h: string) => PrivateKey;
105
+ /**
106
+ * An instance's row (host.db kind `agent`: not a mailbox instance), its
107
+ * genesis at first hydration (the owner's mailbox must exist first to be
108
+ * named in it). Its identity key (hex).
109
+ */
110
+ instance(handle: string): string;
111
+ /** The same as `instance`, by its old name (for tests written before the rename). */
112
+ agent(handle: string): string;
113
+ /**
114
+ * An instance booted from an image (#89; default: the default image,
115
+ * images/default): no owner in its genesis, the claim row from anyone.
116
+ * Claim it with a message from the owner's wallet in box `claim`
117
+ * (src/testapps.ts sendPlan of planClaim, #127): its sender owns it.
118
+ */
119
+ image(handle: string, dir?: string): Promise<import("./boot.ts").Booted>;
120
+ /** A mailbox instance's row (a fixture's: its genesis at first hydration; the product path is the manager's create, Router.createInstance image `mailbox`). */
121
+ mailbox(handle: string, whose: string): void;
122
+ /**
123
+ * The host skein (#90, #113, #142): `host` from the host image (the
124
+ * default image with the onboarding app), its genesis naming the owner
125
+ * (the operator's key here), the instance manager and the certifier in
126
+ * its address book; the onboarding app's config at birth over its
127
+ * manifest's (default: domain `localhost`, origin this router's). Its own
128
+ * origin's BRC-169 requests are then the app's.
129
+ */
130
+ hostSkein(config?: Record<string, unknown>): Promise<{
131
+ handle: string;
132
+ identity: string;
133
+ url: string;
134
+ }>;
135
+ origin: (handle: string) => string;
136
+ storeSize(handle: string): number;
137
+ /**
138
+ * Install apps into an instance as the owner (#83: a genesis has no shell,
139
+ * no `run`, no `chat`): by default the shell app and the chat app, through
140
+ * the owner's messages (src/testapps.ts installApps, #124). Hydrates the instance first.
141
+ */
142
+ install(handle: string, apps?: PinnedApp[]): Promise<void>;
143
+ entries(handle: string): Promise<number>;
144
+ };
145
+ alpha: string;
146
+ root: CID<unknown, number, number, import("multiformats/cid").Version>;
147
+ chat: CID<unknown, number, number, import("multiformats/cid").Version>;
148
+ store: string;
149
+ }>;