@dcasegr/skein 0.0.0-stage → 0.1.2
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 +525 -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 +782 -0
- package/dist/host/router.js +2411 -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
|
@@ -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
|
+
}>;
|