@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,1247 @@
1
+ # kernel-zig
2
+
3
+ The skein kernel in Zig: the block store, the input log, the index (IPLD
4
+ maps in the store and one state record), the scheduler, the kernel's four
5
+ tables (objects, heads with their owner, the dispatch table, the address
6
+ book), the filesystem over the store, the WASI preview1 imports and the
7
+ `skein` imports, and the same as a WASI 0.2 world for components. It runs
8
+ wasm programs through wasmtime's C API, and builds for
9
+ `wasm32-freestanding` for the browser host. It is the one kernel
10
+ implementation: the same log gives the same entries, records, CIDs, derived
11
+ state and fuel on every replay, and the equivalence suite checks that Zig
12
+ against Zig.
13
+
14
+ The kernel at log format 10 (#146: the BEEF envelope beside the pointer
15
+ record — "Format 10" below). Format 9 (#143): routes without senders,
16
+ filters before anything is recorded, roles and the grants head, root
17
+ instead of an owner. Format 8: one dispatch table routes (rows for boxes, HTTP
18
+ paths, libp2p topics and protocols), and the kernel matches every
19
+ transport's packages against it, first match wins (#115, `dispatch.zig`:
20
+ the front door is handed the matched row and only verifies who the package
21
+ is from, BRC-103/104 or GossipSub's signature, then runs the row's
22
+ handler); the admin operations (`objects`,
23
+ `head`, `dispatch`, `peers`) are the kernel's own, on messages from the
24
+ owner or a delegate, and so is the claim (#89: an image's genesis names no
25
+ owner; a message at its claim row writes its sender's admin rows and
26
+ removes the row, #127); a program advances only heads in its write scope (an
27
+ app's `<app>/…`; a genesis-wired program's genesis `scopes`); every package
28
+ a transport carries in is an entry and the front door is stepped on it; a
29
+ step's way out is `emit` (a message to a key the address book
30
+ names — unsigned, its transport proves its sender, #126 step 4 — or an event: an intention the runtime answers, a broadcast, a
31
+ subscription, a beacon, a `deliver` to a messagebox the host carries over
32
+ BRC-104 — #157: the in-step `authfetch` is gone), and the answer is an entry; a deadline or a shell's sleep
33
+ is a `deadline` event the host's waker keeps (#126). The host
34
+ (`src/host/`, TypeScript) drives it over one channel per kernel; the host
35
+ is transports + providers + store + signer and routes nothing. History: the
36
+ format sections below say what each format changed.
37
+
38
+ ## Build
39
+
40
+ Zig 0.16.0 is pinned by the repo's `mise.toml` (`mise install`, then prefix
41
+ commands with `mise exec --` or activate mise).
42
+
43
+ - **wasmtime**: the C API release, not vendored. Download
44
+ `wasmtime-v49.0.1-aarch64-linux-c-api.tar.xz` (or your platform's) from
45
+ https://github.com/bytecodealliance/wasmtime/releases and unpack it under
46
+ `~/.local/wasmtime-c-api/`. `build.zig` takes the directory from
47
+ `-Dwasmtime=<dir>`, else `$WASMTIME_C_API`, else
48
+ `~/.local/wasmtime-c-api/wasmtime-v49.0.1-aarch64-linux-c-api`. The static
49
+ `libwasmtime.a` is linked: the binary needs nothing of it at run time.
50
+ - **SQLite**: the system `libsqlite3` (3.38 or later, for JSON1).
51
+ - **LMDB** (#96): vendored and compiled in, `vendor/lmdb/` — OpenLDAP's
52
+ liblmdb at the tag `LMDB_0.9.36` (commit 028f61b6, github.com/LMDB/lmdb:
53
+ `mdb.c`, `midl.c`, `lmdb.h`, `midl.h`), under the OpenLDAP Public License
54
+ 2.8 (`vendor/lmdb/LICENSE`, `COPYRIGHT`). No system library: the rack's
55
+ `zig build --release -Dcpu=baseline` needs nothing new there.
56
+ - **lmdb-js** for the host's reader of an LMDB environment (`lmdb` 3.5.7 in
57
+ package.json): its prebuilt binaries link LMDB 0.9.90 (data format 2,
58
+ LMDB's 1.0 line), which cannot open a 0.9.x environment (it crashes on
59
+ one); its **data-v1 build** links 0.9.29 and reads the kernel's
60
+ environment, beside running kernels. Build it once after `npm ci`:
61
+ `LMDB_DATA_V1=true npm rebuild lmdb --build-from-source` (node-gyp, a C
62
+ compiler). Only an LMDB store loads it (`src/host/stores.ts`, which
63
+ refuses another build with this message); SQLite hosts never do.
64
+ - **RocksDB** (#158, the one-process kernel's store, `rocksdb_store.zig`): the
65
+ system `librocksdb` through its C API (`rocksdb/c.h`), linked as sqlite3 is
66
+ (`linkSystemLibrary("rocksdb")`). Built and tested here against 11.1.1
67
+ (Arch's `rocksdb`: `/usr/include/rocksdb/c.h`, `/usr/lib/librocksdb.so`). The
68
+ binary links the shared library, so it runs only where that same major
69
+ version is installed: **a host's binary is built on that host** (below).
70
+ The C functions used were chosen from those long in the C API (column
71
+ families, write batches, pinned gets, iterators with an upper bound, LRU
72
+ cache, bloom filter, `flush_wal`); not checked against 10.1.3's header. Not
73
+ in the browser build.
74
+ - **skein-sdk** (by tag URL and hash in `build.zig.zon`): the codecs (`cid`,
75
+ `cbor`, `mst`, `secp`) and the `chain` module (as `chainlib`: the door's
76
+ BEEF parser and SPV verifier, #121, in both builds; natively also the host
77
+ chain's tracker, #156), which brings bsvz (the
78
+ SDK's lazy dependency, fetched by URL and hash). `-Dwallet=false`: the
79
+ wallet module is never built.
80
+
81
+ ```
82
+ cd kernel-zig
83
+ zig build --release # zig-out/bin/skein-kernel (ReleaseSafe)
84
+ zig build test # unit tests, incl. test/fixtures.json made by src/runtime's formats
85
+ ```
86
+
87
+ wasmtime's compilation cache (`~/.cache/wasmtime`) is on: a cache of machine
88
+ code for module bytes, not state. `SKEIN_WASMTIME_CACHE=0` turns it off.
89
+
90
+ ### Building on the rack (RocksDB 10.1.3) — UNVERIFIED
91
+
92
+ The rack has its own RocksDB (10.1.3, `/usr/include/rocksdb/c.h`), mise's zig
93
+ 0.16.0, sqlite3.h and the wasmtime C API under
94
+ `~/.local/wasmtime-c-api/wasmtime-v49.0.1-x86_64-linux-c-api`, and no zig of
95
+ the system's. The binary built on jr links jr's `librocksdb.so.11` and will not
96
+ load there: build it on the rack. The recipe, **not yet run on the rack**
97
+ (#158's builder had no access; nothing below is a reported result):
98
+
99
+ ```
100
+ ssh rack
101
+ cd ~/Code/skein && git pull # the commit with #158
102
+ cd kernel-zig
103
+ export WASMTIME_C_API=$HOME/.local/wasmtime-c-api/wasmtime-v49.0.1-x86_64-linux-c-api
104
+ mise exec -- zig build --release -Dcpu=baseline # baseline: a host-tuned build SIGILLs there
105
+ ldd zig-out/bin/skein-kernel | grep -E 'rocksdb|sqlite' # expect the rack's librocksdb.so.10, libsqlite3
106
+ mise exec -- zig build test --summary all # the unit tests (rocksdb_store.zig, host.zig among them)
107
+ ```
108
+
109
+ Then a host that should run one kernel process sets `SKEIN_STORE_ROCKSDB` in
110
+ its host.env (see "One process for the host" below). If the C API of 10.1.3
111
+ lacks a function used here, the build fails at link time naming it; nothing
112
+ in `rocksdb_store.zig` needs a newer one as far as its headers go, but that is
113
+ unchecked.
114
+
115
+ ## Run
116
+
117
+ ```
118
+ skein-kernel serve # the kernel process the host spawns
119
+ skein-kernel replay <source.db> <out.db> # the log alone into a fresh store, no wallet
120
+ skein-kernel shell <store.db> [<program>] < cases.json # shell cases as equiv/shell.ts passes them (the shell program by CID, else the shell app's: head shell/app)
121
+ skein-kernel dump <store.db> # the derived state read through the index, as JSON (either format, read only)
122
+ skein-kernel fuel <store.db> [--since n] # fuel per thread and in total, from the steps whose input is entry n or later
123
+ ```
124
+
125
+ `serve` is started by the host (`src/host/kernel.ts`) with `SKEIN_DB`,
126
+ `SKEIN_HANDLE` and `SKEIN_HOME`, installs the pinned modules from `wasm/`
127
+ (the boundary programs and the wallet; #83: no shell) into a store that lacks them, speaks frames on stdin/stdout and writes its
128
+ log lines — among them `skein runtime <identity> (<handle>@<domain>) · pid …
129
+ · db …` once running — on stderr; it stops at EOF on stdin or on
130
+ SIGINT/SIGTERM. Since #30 the store file holds blocks and the state
131
+ pointer only: the explorer and
132
+ `skein-dev` open a store file through `src/runtime/index-store.ts`, which
133
+ reads the maps when the file has the state pointer and the old tables when
134
+ it does not — see "The index" below.
135
+
136
+ ## Fuel (issue #5)
137
+
138
+ wasmtime's fuel is on for every instance the kernel runs (`engine.zig`:
139
+ `wasmtime_config_consume_fuel_set`). Fuel is an instruction count, not a
140
+ clock: the same module and input burn the same fuel on every machine, so it
141
+ is recorded and replayed like any other field.
142
+
143
+ - **Per step, one budget** (`engine.Meter`): a handler program's step, or a
144
+ shell step — the shell and every child it spawns (`skein.spawn`, pipes),
145
+ nested instances drawing on one pool. The meter settles the running
146
+ instance before a child starts and after it ends, so a step's fuel is the
147
+ sum over everything that ran in it. A shell thread's steps are its
148
+ segments: from a `running` update to the `waiting`/`finished`/`errored`
149
+ that ends it (a sleep ends one; the wake starts the next with a full
150
+ budget). Re-executing a thread from its origin on wake burns the same fuel
151
+ again, and every recomputed update — its fuel included — is verified
152
+ against the chain.
153
+ - **The field**: every update that ends a step (`finished`, `errored`,
154
+ `waiting`) has `fuel` (an integer); `running` updates have none, nor do
155
+ heads and dispatch chains. A step that failed before running wasm
156
+ records `fuel: 0`.
157
+ - **The limit**: `fuelPerStep` in the genesis's `defaults` (a decimal
158
+ string, as every default is; `src/runtime/log.ts` `DEFAULTS` gives
159
+ `"1000000000000"`, 10^12), read at the start of each step; a genesis
160
+ without it gets the same 10^12 (`scheduler.FUEL_PER_STEP`). Out of fuel
161
+ (wasmtime's out-of-fuel trap, in whichever instance) the step ends
162
+ `errored` with `error: {kind: "cant-do", message: "fuel exhausted"}` and
163
+ `fuel` = the limit — stable, never retried. An errored step's update
164
+ lists the recorded calls it made before failing, so a replay has their
165
+ answers. (wasmtime checks fuel at function entries and loop headers; a run
166
+ may overshoot its budget by the few instructions after its last check,
167
+ in which case its `fuel` is the limit without a trap.)
168
+ - **Fuel accounting is a query over the log**: `skein-kernel fuel <db> [--since n]`
169
+ sums the updates' fuel per thread (in thread order, with the program's
170
+ name) and in total, over the steps whose input entry is n or later;
171
+ `skein-kernel dump` lists each update's fuel and the per-thread sums.
172
+ Anyone holding the log can replay it and get the same numbers.
173
+ - **Cost**: replaying the generated corpus takes 0–9% longer with fuel on
174
+ (best of 5 per store, e.g. `gen-run` 1110 → 1210 ms, `gen-kurt` 594 →
175
+ 593 ms; wasmtime's fuel instrumentation, plus a settle per nested
176
+ instance).
177
+ - **Existing state**: update CIDs changed. The state record carries
178
+ `format: 1`; a store with a log whose state record has no `format` (the
179
+ kernel before #5), or in the older tables (sqlite.ts's, host-signed entries), is refused for running
180
+ (`serve`, `shell`: "a store written before fuel metering (issue #5) …
181
+ refused (start a new store: re-genesis)"). Read-only uses still open it:
182
+ `dump`, `fuel`, and `replay` as a source (a log is a log — the replay
183
+ recomputes every update, with fuel, into a fresh store).
184
+ - Not metered: host time outside the VM (the peers, the signer), memory
185
+ (a module's own max), per-tree depth and step counts (countable from the
186
+ chain; enforced from config later).
187
+
188
+ ## The in-step clock runs on fuel (issue #38)
189
+
190
+ `clock_time_get` inside a step (realtime and monotonic alike) returns
191
+ `max(last + 1, stamp + fuel × 1 ns)` (`syscalls.ThreadClock`): `stamp` is the
192
+ time of the entry driving the step, `fuel` the step's `engine.Meter.used()`
193
+ so far — one meter for a handler step, one for a shell and every child it
194
+ spawns, so they read one clock — and `last` the previous read (the +1 ns
195
+ tie-break: no two reads equal). **1 fuel unit = 1 ns** (`NS_PER_FUEL`), fixed.
196
+ Deterministic because fuel is; nothing is recorded. A busy-wait on the clock
197
+ ends on its own: `equiv/serve.ts`'s clock scenario waits 50 ms on qjs
198
+ `Date.now()` and on python `time.monotonic()` and the shell's step burns
199
+ ~3.6·10^8 fuel, under a 10^9 limit. A shell sleep's deadline is this clock
200
+ plus the sleep; the waker's answer drives the next segment's stamp and the
201
+ meter's new segment counts from zero. Outside a thread (`skein-kernel
202
+ shell`, no meter) the clock is `shell.Fixed`, as before.
203
+
204
+ Changed by it: an update's `until` (a sleep's deadline) moves by the fuel
205
+ burnt before the sleep; the Go programs of the time (run-handler, loop; Zig
206
+ since #54) burnt ~1.2k more fuel per step (their runtime read the clock);
207
+ any printed mid-step time.
208
+
209
+ ## For the wallet (issue #29)
210
+
211
+ What this kernel adds for the wallet in the VM (`programs/wallet` over the SDK's wallet and chain libraries, docs/WALLET.md) and the chain app (shruggr/skein-chain), which since #79 holds the chain state the wallet reads.
212
+
213
+ - **Bitcoin codecs** (`cid.zig`, `bitcoin.zig`, #42): `putblock` accepts
214
+ `bitcoin-tx` (0xb1) and `bitcoin-block` (0xb0, 80 bytes) with
215
+ `dbl-sha2-256` (0x56), raw bytes, hash-checked, so a transaction's CID is
216
+ its txid, a header's its block hash and a merkle node's its merkle hash
217
+ (the digest in internal byte order); `get` returns the bytes as stored.
218
+ The kernel **decodes them the IPLD way** (after IPLD's bitcoin codecs,
219
+ bitcoind's field names): a header → `{version, previousblockhash → link,
220
+ merkleroot → link, time, bits, nonce}`; a transaction → `{version, vin:
221
+ [{txid → link, vout, script, sequence} | {coinbase, sequence}], vout:
222
+ [{value, script}], locktime}`; **exactly 64 bytes under `bitcoin-tx` is a
223
+ merkle node** `[left → link, right → link]` (IPLD's convention; a 64-byte
224
+ transaction is malformed by convention, so the length decides). The
225
+ header's merkle root names the root node, each node its two children
226
+ (nodes, or at the bottom transactions), so a block's transaction tree is a
227
+ DAG in the store — sparse, holding only the paths a wallet needs
228
+ (docs/WALLET.md "Proofs"). (The unregistered 0xb3 `bitcoin-merkle` of
229
+ #29 is gone.)
230
+ - **Edges from kept bitcoin transactions** (`index.zig`, `bitcoin.zig`
231
+ `edgesOf`, #42): a transaction a step keeps adds its inputs to the `edges`
232
+ map, from the block itself at seq 0 — each input `spends` (locator = its
233
+ vout, an integer). Kept headers and merkle nodes add **no edges** (decided
234
+ 2026-09-30: nothing asks the reverse questions; a proof reads down from
235
+ the root); their forward links (`bitcoin.zig` `links`) stay what the
236
+ explorer and packets follow. Keep is the trigger, not `putblock`: the map
237
+ stays a function of the chains, and a block kept again (by any thread)
238
+ adds nothing.
239
+ - **`edges(to, to_len, rel, rel_len, out, cap)`** (WIT `edges: func(to:
240
+ cid, rel: option<string>) -> result<list<u8>, string>`): the edges into
241
+ `to`, `rel` only if given, as dag-cbor `[{from, seq, rel, locator}]` in key
242
+ order (from, seq, ord) — who spent txid:vout is the `spends` edges into
243
+ the transaction's CID with that locator. A read: the index (a function of
244
+ the log) plus the inputs of the bitcoin transactions the step kept so far
245
+ (the edges its update will add); a kernel `call` reads the index only. The
246
+ wallet's `spenders` map folded into it.
247
+ - **Plain entries** (`log.zig`, `scheduler.zig` `processEvent`):
248
+ `{kind: "log", n, prev, time, box, event}`, the `event` kind of the
249
+ entry encoding (genesis | mail | event+box | request+transport; none
250
+ signed). The host admits one per event from its wiring (a header from a
251
+ feed, a proof from the broadcaster; `admitEvent` in src/host/router.ts), with the record
252
+ `event` names already put. Statuses are not events: they are signed
253
+ messages from the status provider.
254
+ Processing: the thread whose tip awaits the record's `subject` (a CID; a
255
+ transaction's is its txid) steps with `input.event = {event, box,
256
+ subject}`; else the first `mailbox` dispatch row from anyone on the
257
+ entry's box launches its program with `args = {event, box}`.
258
+ - **`await` on a record**: besides a message the step emitted, a step may
259
+ await any record in the store — the subject of a plain entry to come.
260
+ - **`emit(msg, len, out, cap)`** (#70, #67): a dag-cbor `{to: bytes(33),
261
+ box, body: bytes, subject?: <cid>}` → the CID of the message record
262
+ `{kind: "mail", op: "put", sender, recipient, box, body: <cid>, subject?,
263
+ nonce: bytes(16)}` (`scheduler.zig` hEmit/emitMessage). `to`
264
+ must be in the address book (`addressbook.zig`, the head `peers`); no
265
+ signature (#126 step 4: the recipient's front door verifies the sender's
266
+ BRC-104 session, or libp2p's peer; the recipient keeps the same record).
267
+ A log written before signed every emit through the signer (BRC-169's `[2,
268
+ "metanet handles envelope"]`, key `send`, counterparty anyone, over the
269
+ record without `signature`; a recorded call, `signer.zig`): replay serves
270
+ that call when the log holds it for that very record (`oldEnvelope`) —
271
+ read, never written. `nonce` is sha256(thread ‖ step ‖ index)[0..16]. The update lists it in `emitted`; it goes out only
272
+ if the step did not error: a `mailbox` recipient's by a delivery thread
273
+ the kernel launches (the messagebox program, args `{message, transport:
274
+ "mailbox"}`), a `local` or `libp2p` recipient's handed to the host after
275
+ commit (the serve notice `emit {message, body, transport, address}`). At
276
+ start the kernel offers again what a waiting thread still awaits. A
277
+ delivery thread that errors steps the awaiting thread with `undelivered:
278
+ {message, error}`. Refused in a kernel `call`. docs/VM.md ("emit") and
279
+ docs/MESSAGES.md ("Outbound") have the contract.
280
+ **An event** instead (#65, #119; `emitEvent`): `{event: "broadcast", tx,
281
+ beef?}` → `{kind: "broadcast", tx, beef?}` (re-offered at a start while
282
+ awaited), or `{event: <any other name>, …fields}` → `{kind: "event",
283
+ event, app?, …fields}` (`log.zig` `eventRecord`), `app` the emitting
284
+ program record's when that record is installed (`installedAs`, K1) and
285
+ absent otherwise; an emit naming `kind` or `app` is refused. `subscribe
286
+ {topic, program, fn, filter?}` / `unsubscribe {topic}` are checked as they
287
+ are emitted (`subscriptions.zig` `problem`) and are the subscriptions
288
+ (#119): their fold in log order (`subscriptions.zig` `fold`, kept in
289
+ memory as `Runtime.subs` and folded again after a step that emits one —
290
+ derived, never written) delivers a libp2p topic no row is at (`matchOf`). Listed in
291
+ `emitted`, unsigned, handed to the host after commit as transport
292
+ `event`, address the name (`handOverEvent`), once; the host wires a name
293
+ or ignores it. The kernel keeps nothing else for it: a host that needs it
294
+ after a restart reads the log.
295
+ - **`deadline(until_ms)`** (#126): the update carries `until`; when the
296
+ step ends waiting the kernel records the event `{kind: "event", event:
297
+ "deadline", at, thread, step, app?}` (log.zig `deadlineRecord`), lists it
298
+ in `emitted` and awaits it — no signer call, no lookup. A shell's sleep is
299
+ the same event on its waiting update. The answer is the waker's signed
300
+ message naming the event and carrying the instance's signed request for
301
+ it (the host signs it; the scheduler's `intentionAnswer` checks both); it
302
+ steps the thread with `input.woke`. A `fetch` event (`sk.fetch`) is the
303
+ same kind of intention (the kernel adds `thread` and `step`), answered by
304
+ the host's HTTP proxy with `input.reply`. `beacon`/`unbeacon` events are
305
+ checked as they are emitted (`subscriptions.zig` `beaconProblem`).
306
+ - **`authfetch(req)`** (#126, `authfetch.zig`): the BRC-103/104 client in
307
+ the kernel — the session per server base URL (in memory), the request
308
+ signed and the answer checked through the signer, the bytes moved by the
309
+ host (`Peers.http`: the serve frame `http`). One recorded call, `{kind:
310
+ "authfetch", thread, step, i, request, answer | error, calls}`, served
311
+ from the witness on replay.
312
+
313
+ There is no plain `http` or `libp2p` import (format 6 removed them, and the
314
+ host's attestations with them): plain HTTP is an intention the host's proxy
315
+ answers, libp2p a service a step emits to (`src/host/providers.ts`).
316
+
317
+ ## For the bootstrap loader (issue #4)
318
+
319
+ The loader itself is host code (`src/host/boot.ts`, `packet.ts`;
320
+ docs/BOOTSTRAP.md). The kernel adds only this:
321
+
322
+ - **genesis `tree?`** (`log.zig`): a git-raw CID, the system tree the loader
323
+ pre-filled. Processing the genesis (`scheduler.zig`) sets the head `main` to
324
+ it. The head update has no `thread` (`heads.By.thread` is optional), and the
325
+ genesis is refused if the tree is not in the store.
326
+ - **genesis `heads?`** (#141, `log.zig`): `{<head name>: <record CID>}`, the
327
+ heads of an image's installed apps (`<app>/app`, `reads`), their records
328
+ pre-filled by the loader. Processing the genesis advances each, after the
329
+ dispatch rows and `main`, with no `thread`; refused if a record is not in
330
+ the store, and a genesis naming `main`, `claim` or `billing` there is
331
+ malformed. No format bump: a genesis without it reads as before.
332
+ - **genesis `owner?`** (#89, `log.zig`): an image names no owner. Its
333
+ `claim` row is the kernel's `claim` operation (`scheduler.zig` claim):
334
+ the owner is the message's sender (#127; a body's `owner` is not read),
335
+ the sender's admin rows added, the claim row removed, the head `claim` at
336
+ `{owner: <the sender>, messagebox?, handle?, domain?}`; the step input
337
+ `owner` is then that key (`ownerOf`). A claim is the one message that may
338
+ name no recipient (`log.zig` isMail: signed before the instance existed,
339
+ forwarded into it by the host). No format
340
+ bump: a store that names its owner reads as before. Since #121 the claim
341
+ also writes the explorer row with the owner's key (every sender is a key:
342
+ the image carries no explorer row) — unless the table holds a row for
343
+ #115's `owner` symbol (an image written before #121), which is read as
344
+ before: the matcher's `owner` sender is the step's owner.
345
+ - **`serve` frames**:
346
+ - `has` answers whether any block is present, whatever its codec.
347
+ - `putblock {cid, bytes}` stores a block minted elsewhere, hash-checked like
348
+ the `putblock` import.
349
+ - `restore <state>` (`SqliteStore.restore`) is for a checkpoint whose blocks
350
+ are already put. The state pointer moves onto that record and the index is
351
+ read from it. It works only on a store with no log, and only for this
352
+ kernel's format.
353
+ - **replay** (`replay.zig` copyLog): copies the genesis tree's objects, and
354
+ (#141) the records its `heads` name with every record they link, so a
355
+ booted store replays to itself.
356
+
357
+ ## Components (issue #34)
358
+
359
+ The kernel runs **WASI 0.2 components** as well as preview1 core modules,
360
+ wherever it runs a module: a handler program's step, or a program the shell
361
+ spawns. `runner.zig` tells them apart by the binary's preamble.
362
+
363
+ A component targets `skein:kernel/handler` in the SDK's `wit/skein.wit`
364
+ (shruggr/skein-sdk, a Zig package dependency by URL+hash, #75; see its
365
+ `wit/README.md`):
366
+
367
+ - WASI 0.2.12: `cli`, `clocks`, `filesystem`, `io` and `random`;
368
+ - the interface `skein:kernel/skein`: the preview1 `skein` calls, including
369
+ `emit`, `deadline` and `authfetch` (there is no `wasi:http`: #70, below);
370
+ - the export `wasi:cli/run`.
371
+
372
+ Imports the kernel does not answer, such as sockets, link as traps.
373
+
374
+ ### Toolchain (pinned)
375
+
376
+ | what | version | where |
377
+ |---|---|---|
378
+ | wasmtime C API | v49.0.1 (its component API: `wasmtime/component/*.h`) | `~/.local/wasmtime-c-api/…` (unchanged) |
379
+ | WASI | 0.2.12 (the WIT is vendored in the SDK's `wit/deps`, from wasmtime v49.0.1's `crates/wasi/src/p2/wit/deps`) | |
380
+ | preview1 adapter | wasmtime v49.0.1's `wasi_snapshot_preview1.command.wasm` (sha256 `86c88319…9f76`) | `~/.local/wasi-adapter-v49.0.1/` (`$SKEIN_WASI_ADAPTER`) |
381
+ | wasm-tools | 1.259.0 | `~/.local/wasm-tools-1.259.0-aarch64-linux/`, linked from `~/.local/bin` |
382
+ | wit-bindgen | 0.62.0 (`wit-bindgen c`) | `~/.local/wit-bindgen-0.62.0-aarch64-linux/`, linked from `~/.local/bin` |
383
+ | wasi-sdk | 34 (for the unit test's C program) | `~/.local/wasi-sdk-34.0` |
384
+
385
+ To install them: `gh release download` the tools from
386
+ bytecodealliance/wasm-tools and bytecodealliance/wit-bindgen, and the adapter
387
+ from wasmtime's v49.0.1 release. The kernel build needs none of these. The
388
+ component fixtures the unit tests use are committed.
389
+
390
+ ### The host side
391
+
392
+ `component.zig` is written by hand against `wasmtime_component_*`.
393
+ wit-bindgen has no C host bindings, so the host uses dynamic values: wasmtime
394
+ lifts and lowers through the canonical ABI, and the host reads
395
+ `wasmtime_component_val_t`. Every descriptor, stream, pollable and error is a
396
+ host resource.
397
+
398
+ To give **one behaviour**, the 0.2 filesystem, io and cli calls are answered
399
+ by the preview1 implementation itself (`wasi.Process.dispatch`), run against
400
+ a scratch memory:
401
+
402
+ - a `descriptor` is a process fd;
403
+ - a file stream has its own description at its offset;
404
+ - reads, writes, stats, readdir, opens, renames and links are the preview1
405
+ calls.
406
+
407
+ So the tree, the errors and the inode numbers are the preview1 ones. The
408
+ other imports:
409
+
410
+ - **Clocks**: the run's clock, which is the entry's stamp.
411
+ - **Random**: the run's entry-derived stream. `insecure` and
412
+ `insecure-seed` draw on it too.
413
+ - **cli**: the process's args and env. There is no terminal, and
414
+ `initial-cwd` is none.
415
+ - **poll**: follows `poll_oneoff`'s rule. Clocks alone are a sleep, and every
416
+ subscription fires.
417
+ - **skein**: calls `program.Host` exactly as the preview1 imports do. Its
418
+ errors are the `result`'s string.
419
+
420
+ ### Components and emit (issues #70, #67)
421
+
422
+ A component has no network import: there is no `wasi:http` and no
423
+ `skein:kernel/libp2p` (format 6 removed them, as it removed the preview1
424
+ `http` and `libp2p` imports). It reaches the world as a preview1 program
425
+ does: `skein:kernel/skein.emit(message) -> result<cid, string>` (the
426
+ component host's `sk_emit`, over `program.Host.emit`, the scheduler's
427
+ `hEmit`) — a message to a provider or a peer — and awaits the
428
+ answer, which steps it again. `test/components/fetch.wasm`
429
+ (`../programs/test/fetch`) is the fixture: it emits `{method: "GET", url}` in box
430
+ `fetch` to the address book's `fetch` provider, ends awaiting, and writes
431
+ the answer's body on its next step (`equiv/fetch.ts`).
432
+ `test/components/p2p.wasm` (`../programs/test/p2p-component`) publishes the
433
+ same way, by an emit to the `libp2p` provider.
434
+
435
+ Fuel is per store, so a component draws on the step's `engine.Meter` like a
436
+ module. If fuel is 0 when the call fails, the step ran out of fuel. Other
437
+ traps are read from wasmtime's message and reported in V8's wording, as for
438
+ modules.
439
+
440
+ `SKEIN_COMPONENT_TRACE=1` prints every component call and the preview1 call
441
+ behind it.
442
+
443
+ Three variables exist for the equivalence suite only; live instances do not
444
+ use them:
445
+
446
+ - `SKEIN_EXTRA_MODULES=<file>:…` makes `serve` and `replay` install modules
447
+ that are not pinned, under their raw CIDs.
448
+ - `SKEIN_REPLAY_MODULE=<cid>=<file>` makes `replay` run `<file>` wherever the
449
+ log runs module `<cid>`.
450
+ - `SKEIN_SHELL_COMPONENTS=<dir>` makes `shell` run the tools found there as
451
+ `<name>.wasm`.
452
+
453
+ ### Building a component handler
454
+
455
+ The wallet (`../programs/wallet`) is the example: `zig build component` gives
456
+ `zig-out/bin/wallet.component.wasm`.
457
+
458
+ 1. **Generate the C bindings.**
459
+ `wit-bindgen c wit --world program --out-dir wit/bindings/c` gives
460
+ `program.h`, `program.c` and `program_component_type.o`. The last one
461
+ carries the world.
462
+ 2. **Compile the program.** Build it for `wasm32-wasi` with `program.c` and
463
+ the `.o`. In Zig, `@cImport(@cInclude("program.h"))`
464
+ (the SDK's `skein_wit` module, `wit/zig/skein_wit.zig`).
465
+ 3. **Keep wasi-libc out** if the program must draw the same random bytes as
466
+ its preview1 build. Linking wasi-libc switches Zig's random (`std.Io`'s
467
+ `randomSecure`) to `arc4random`. The SDK's `cabi` module (`wit/zig/cabi.zig`) provides the few libc
468
+ functions `program.c` needs.
469
+ 4. **Make the component.**
470
+ `wasm-tools component new core.wasm --adapt wasi_snapshot_preview1=<adapter> -o out.wasm`.
471
+ The adapter turns the program's preview1 WASI into 0.2.
472
+
473
+ A program that uses 0.2 directly needs no adapter: for example, C built with
474
+ wasi-sdk's `wasm32-wasip2`, as in the unit test.
475
+
476
+ The wallet's two builds share one source (`program.zig`, where `sk` is the
477
+ preview1 imports or `skein_wit.zig`). Both broadcast the same way: a
478
+ broadcast event (`emit`, #65), the same record from either build. The preview1 build stays byte-identical to the pinned
479
+ `wasm/wallet.wasm`, and the pin stays on preview1. `../programs/test/fetch` is a
480
+ second, minimal example (an emit to the `fetch` provider, its URL from argv
481
+ or the step's input).
482
+
483
+ ### What is not the same
484
+
485
+ - **Fuel.** The adapter and the canonical-ABI glue execute instructions, so a
486
+ component's fuel is not the module's. Each ABI replays itself exactly.
487
+ Across the two ABIs, every update matches except `fuel` and `prev`
488
+ (`equiv/abi.ts`).
489
+ - **Exit statuses.** `wasi:cli/exit` is ok/err. The v49 preview1 adapter and
490
+ wasi-libc's 0.2 `exit` both report any non-zero status as 1. The kernel
491
+ implements the unstable `exit-with-code`, but neither of them calls it.
492
+ - **Writes through the adapter** arrive in 4096-byte chunks. A pipe that
493
+ reaches its limit (stdout's 1 MiB for a handler) keeps the chunks that fit.
494
+ Under preview1, the whole write is refused.
495
+ - **Stdin of a handler step.** Through the adapter it has type `unknown`;
496
+ preview1 calls it a character device.
497
+ - **Costs.** Values cross as dynamic values, so a `list<u8>` is one value per
498
+ byte on the host side. A component is linked per compile, with its own
499
+ linker.
500
+
501
+ ## The index (issue #30)
502
+
503
+ The store is a key→bytes map: SQLite's `blocks` table and a `pointers` table
504
+ with one row, `state` → the CID of the **state record**
505
+ `{kind: "skein-state", log: <log tip>|null, cursor, heads: <root>|null, index: {<map>: <root>|null}}`.
506
+ What SQLite's `chains`/`updates`/`edges`/`entries`/`meta` held is now
507
+ **Merkle search trees** (`mst.zig`) whose nodes are blocks beside every
508
+ other record, one per query shape (`index.zig`):
509
+
510
+ | map | key → value | answers |
511
+ |---|---|---|
512
+ | `log` | n → entry | `logFrom(n)` (a range), genesis |
513
+ | `unique` | message (its mail record's CID), or a libp2p `p2p` event record's CID (#42/#51: a redelivery is refused) → entry | `byEnvelope` (the name kept), duplicate admission |
514
+ | `chains` | origin → {tip, seq, kind} | `chainTip`, `chainAppend` |
515
+ | `updates` | origin ‖ seq → update | `chainUpdates` (a prefix) |
516
+ | `threads` | at ‖ origin | every thread (the witness) |
517
+ | `resting` | at ‖ origin | threads not finished, in resume order |
518
+ | `sleepers` | until ‖ origin | waiting threads by deadline |
519
+ | `awaits` | record ‖ at ‖ origin | the threads awaiting a reply to a message they sent, or a subject (a prefix) |
520
+ | `edges` | to ‖ from ‖ seq ‖ ord → [rel, locator] | what points at a record (a prefix) |
521
+ | `heads` | name → tree | `head` |
522
+
523
+ Keys: numbers 8-byte big-endian with the sign bit flipped; binary CIDs
524
+ (prefix-free, so concatenations split); strings uvarint-length-prefixed. A
525
+ node is the dag-cbor array `[left, [[key, value, right]…]]` (CIDv1,
526
+ dag-cbor, sha2-256, as every record). A key's level is the leading zero
527
+ 5-bit groups of sha2-256(key), so nodes hold ~32 keys. The tree is
528
+ **canonical** — one tree per set of pairs, whatever the insertion or
529
+ deletion order (unit-tested with shuffled puts and interleaved deletes) —
530
+ so a map's root is a function of its contents and the state record is a
531
+ function of the log. The derivation is `sqlite.ts`'s, key for key.
532
+
533
+ **Commits.** Changes go to a working state; new nodes wait in memory. At each
534
+ log append the log maps' nodes and a state record are written (the entry is
535
+ durable before the provider hears back; the other maps as last committed);
536
+ after each processed entry, and at the end of `start` and of a drain,
537
+ every map's new nodes, the cursor and the state record are written in one
538
+ transaction and the pointer moves. Only the nodes the new roots reach are
539
+ written (intermediate versions within an entry are dropped). A crash loses
540
+ at most the unprocessed entry's derived changes, which reprocessing
541
+ recomputes (same CIDs).
542
+
543
+ **Per-entry cost** (measured over every replay in the suite): 2.5–7.5 new
544
+ index nodes per log entry (0.6–4.2 KiB), plus two state records (~0.5 KiB
545
+ each: one at admission, one after processing). A put writes at most the
546
+ nodes on one root-to-leaf path: log₃₂(n)+1 levels, ~5 at 2M keys. Replay
547
+ time is unchanged within noise (e.g. the generated `run` store: 1.36 s
548
+ before, 1.21–1.35 s after; `kurt/runtime.db.pre-replies` 0.57 s → 0.59–0.62
549
+ s): the wasm runs dominate.
550
+
551
+ **Older stores.** A file in the format before #30 (this kernel's before #30,
552
+ or the deleted TS runtime's) has no state record: it is imported from its tables —
553
+ the log from `entries`, each chain from its origin and `updates`, the cursor
554
+ from `meta` — through the same derivation, so the import and the
555
+ incremental path reach the same state CID (the suite checks it on every
556
+ replay). Opened for writing, the state record is written and the old tables
557
+ are renamed `legacy_*`; opened read only (a replay's source, `dump`), the
558
+ import stays in memory. A store whose old index is stale is refused (nothing
559
+ rebuilds it any more: `skein-dev rebuild` went with the TS runtime, #55).
560
+
561
+ **The shared host store** (#96, #133; `lmdb_store.zig`). Beside one SQLite
562
+ file per instance (the default, unchanged), a host may keep every instance in
563
+ **one LMDB environment**, a directory (`$SKEIN_HOME/store`: `data.mdb`,
564
+ `lock.mdb`) opened by each `skein-kernel serve` of the host and read by the
565
+ host directly (lmdb-js). Three tables: `blocks` (cid → bytes, each block once,
566
+ never overwritten), `members` (`<set>‖0x00‖cid` → ∅) and `pointers`
567
+ (`<set>‖0x00‖name` → cid). A **set** is an instance's handle; its members are
568
+ its **read gate**: `has`/`bytes` answer through the set, then through the
569
+ reserved set `host/chain` (the host's chain service writes it, #156; nothing
570
+ does yet) — a block only another set holds is absent. A put writes the block
571
+ if it is new and adds the membership; a new membership adds the block's
572
+ length to the set's `size` pointer (u64 BE: #130's measure, the set's share).
573
+ The index above it is the same: the same records, the same state CIDs (the
574
+ format number unchanged). A commit is one write transaction (the index's
575
+ begin … commit); a put outside one is its own. Flags: `MDB_NOTLS`,
576
+ `MDB_NOMETASYNC` (one flush per commit, the meta page's deferred: a system
577
+ crash may undo the last commit, never corrupt), map size 1 TiB (address space;
578
+ nothing on disk until used), 4096 reader slots. One writer at a time across
579
+ the environment: the instances' commits serialize there.
580
+
581
+ - `serve`: `SKEIN_STORE=<env dir>` → the set named by its handle
582
+ (`SKEIN_HANDLE` before `@`); unset → the SQLite file as before.
583
+ - The tools (`replay`, `dump`, `fuel`, `shell`) take a path, or
584
+ `lmdb:<env dir>#<set>` (`any_store.zig`).
585
+ - `skein-kernel migrate --from <runtime.db> --into <env dir> --set <handle>`:
586
+ every block, then the state pointer (in batches; the pointer last, so a
587
+ half-copied set has no state); a set with a state already, or a source in
588
+ an older format, refused. `skein-host migrate-store <handle>` runs it with
589
+ the host stopped, renames the file `runtime.db.migrated-<date>` (kept) and
590
+ sets the row's `store` to `lmdb:<dir>`.
591
+ - The host: `SKEIN_STORE` in host.env makes each **new** instance a set
592
+ (host.db `store` = `lmdb:<env dir>`, the set its handle); its kernel gets
593
+ `SKEIN_STORE`, a file's kernel never does. Every host-side read
594
+ (`openStoreFile`, `storeExists` in `src/host/stores.ts`; `storeSpec` in
595
+ `index-store.ts`, which touches no disk itself) takes
596
+ `lmdb:<env dir>#<set>` and reads through the same gate. The host writes no
597
+ instance store: a reclaimed LMDB instance's set is left in the environment.
598
+
599
+ **From TypeScript** (`src/runtime/index-store.ts`): a read-only `Store` over
600
+ the maps (the pointer re-read on every call, so it follows a live kernel),
601
+ used by the explorer and `skein-dev` (`rebuild` is refused on such a store:
602
+ the kernel keeps its index); and `buildIndex`, the same maps built
603
+ canonically in TypeScript — equiv checks it reaches the kernel's state CID,
604
+ and that every explorer page renders the same over the TS and the Zig
605
+ replays' files. The wallet (the SDK's `wallet`, #29) uses `mst.zig` as the SDK module `mst`
606
+ of its own for its index maps (inside the VM). **Not yet:** pruning old
607
+ spine nodes and state records; a cache bound (decoded nodes are dropped at
608
+ every commit).
609
+
610
+ ### One process for the host (issue #158)
611
+
612
+ ```
613
+ skein-host run (router.ts) ── one KernelHost ──▶ skein-kernel host (every instance of the host)
614
+ HostedKernel per instance I/O thread · workers (SKEIN_WORKERS) · run queue
615
+ (the Kernel surface, frames tagged h) one RocksDB database ($SKEIN_STORE_ROCKSDB) · one wasmtime engine
616
+ ▲ │ host-read socket <db dir>/kernel.sock (stores.ts)
617
+ └── frames ────┘
618
+ ```
619
+
620
+ `skein-kernel host` (`host.zig`) holds every instance of a host whose stores
621
+ are sets of one RocksDB database (`rocksdb_store.zig`). The router starts it
622
+ once (`KernelHost`, `src/host/kernel.ts`) when `SKEIN_STORE_ROCKSDB` is set and
623
+ speaks `serve`'s frames to it, each instance-scoped frame tagged `h` (the
624
+ handle), through a per-instance `HostedKernel` with the `Kernel` surface (the
625
+ router's calls are unchanged). Host-level frames: `open {h, v: {domain, db}}`
626
+ (hydrate: the set opened, the pinned modules installed, the runtime built;
627
+ idempotent), `close {h}` (dehydrate, after its inbox drains; the router's idle
628
+ policy sends it), `instances`, `stop`, and the host's reads `get`/`has` (a
629
+ CID; through a set's gate if the frame names `set`, else any block),
630
+ `members {set}` and `state {h}` (the set's committed state pointer).
631
+
632
+ - **Threads.** The I/O thread reads stdin and routes: a frame with `h` into
633
+ that instance's inbox (the instance becomes runnable), a `call` into the
634
+ read queue, an answer (`re`) to the worker waiting on it. It never runs a
635
+ step or a call. Workers (default the CPU count) take a runnable instance,
636
+ run its inbox to empty (each frame as `serve` handles it: `admit` steps
637
+ there and then), and put it down; an instance is on one worker at a time,
638
+ so each instance stays single-threaded and its steps are what `serve`'s
639
+ are. An idle instance holds no thread.
640
+ - **Calls beside steps.** A `call` (#40) runs on any worker over a *view* of
641
+ the set: a read-only index at the state pointer as committed when the view
642
+ is made (`rocksdb_store.zig` `View`), never the instance's working state —
643
+ so a read runs while its instance steps, and sees the state before that
644
+ step (the log appends committed so far, their entries `pending`).
645
+ `Runtime.callOver` is the call over such a view; the scheduler is
646
+ otherwise unchanged.
647
+ - **The kernel's requests.** A step's `wallet` or `http` request is a frame
648
+ tagged `h`; its worker waits for the answer (no nesting: the I/O thread
649
+ serves everything else meanwhile, and a frame for that instance waits in
650
+ its inbox behind the step). `answer` waits (#66) are kept per instance;
651
+ their bounds are the I/O thread's poll timeout.
652
+ - **The store.** One database per host: column families `blocks`, `members`
653
+ (`<set>‖0x00‖cid`), `pointers` (`<set>‖0x00‖name`), the same shape and gate
654
+ as the LMDB environment (`host/chain` the one shared set, #133). A commit
655
+ is one write batch written with `sync = false` (the WAL written, not
656
+ fsynced: an OS crash may lose the last commits, never corrupt); the WAL is
657
+ synced and the memtables flushed at a clean stop. Bloom filters on `blocks`
658
+ and `members`, one 64 MiB block cache, `IncreaseParallelism(cores)`,
659
+ `max_background_jobs` ≥ 4.
660
+ - **One process per database.** RocksDB's lock lets one process open it. The
661
+ tools (`replay`, `dump`, `fuel`, `shell`, `migrate`) open
662
+ `rocksdb:<db dir>#<set>` themselves, so they run while the host is down;
663
+ so do the one-shot commands that boot (`skein-host add --image`, `init`).
664
+ The host's own reads of such a set (the router's folds, the CLI's
665
+ `peers`, `roster`, `pack`, a client's `--instance`) go to the running
666
+ process over its host-read socket, `<db dir>/kernel.sock` (the index reader
667
+ is synchronous: `src/host/stores.ts` speaks the socket from a worker thread
668
+ it waits on); with no process there they are refused, saying so.
669
+ - **Crash.** If the process dies, every instance's kernel is gone (their
670
+ `exited`), and the next hydration starts it again and opens the instance
671
+ (replay at hydrate, as for `serve`).
672
+ - `skein-host migrate-store <handle>` with `SKEIN_STORE_ROCKSDB` set copies a
673
+ file (or an LMDB set) into the database (`skein-kernel migrate --into
674
+ rocksdb:<db dir>`), host stopped.
675
+
676
+ ### The host chain (issue #156)
677
+
678
+ The header chain is the host's, held once and kept by the kernel process
679
+ (`host_chain.zig`, over skein-sdk's `chain.host`: `Chain.add` with its checks
680
+ and the heavier-branch rule, `image.loadMaps`). `skein-kernel host` writes it
681
+ into the set `host/chain` of its database (every instance reads it through
682
+ the chain exception, #133); `serve` writes it into its one instance's own
683
+ store (SQLite; an LMDB serve instance's own set). Its record, under the
684
+ pointer `host/chain`:
685
+
686
+ ```
687
+ {kind: "host-chain", network, maps: {headers, heights}, tip: {height, hash}}
688
+ ```
689
+
690
+ Three host-level frames (no `h`; on `serve`'s stdin too), run in order on the
691
+ host process's own chain thread (an import of a whole chain takes seconds):
692
+
693
+ | op | v | answers |
694
+ |---|---|---|
695
+ | `headers` | `{raws: [bytes]}`, parents first | `{root, height, hash, added, replaced}`; nothing new: nothing written, the record as it stands; a run that does not connect: refused |
696
+ | `chain-import` | `{set, root}`: an image tree the instance `set` holds | the same; refused `NotEmpty` when the host chain has a tip, `NoChain` when the tree has no `chain/headers` |
697
+ | `chain` | — | `{root, height, hash}` or null; also on the host-read socket (`skein-host chain`) |
698
+
699
+ The network is `SKEIN_NETWORK` (default main; the router passes the host's).
700
+ Writes serialize on one mutex; reads are by CID. The router (`feeds.ts`,
701
+ `router.ts`) sends a `headers` frame per feed event (the backfill in runs of
702
+ 2016) and, on a changed record, the event `{kind: "chain", root, height}` in
703
+ box `chain` to each subscribed running instance; once at each start and when
704
+ an app taking box `chain` is installed; and `chain-import` from the
705
+ instance's tree (its head `main`) when the host chain is empty, before its
706
+ first event. The chain app (shruggr/skein-chain 0.7.0) points its state's
707
+ `headers` and `heights` at the record (`State.pointAt`). `skein-kernel
708
+ replay` copies, beside a `chain` event, the record and the blocks it reaches
709
+ into the fresh store.
710
+
711
+ ### How serve is put together (issue #33)
712
+
713
+ ```
714
+ skein-host run (the host, src/host/router.ts) ──spawn on demand──▶ skein-kernel serve (one per instance)
715
+ transports · providers · signer · feeds · fuel ledger store · log · scheduler · programs (wasmtime)
716
+ ▲ │
717
+ └──────── frames ─────┘ length-prefixed dag-cbor on the kernel's stdin/stdout; its log lines on stderr
718
+ ```
719
+
720
+ The host speaks to the kernel through `src/host/kernel.ts`: it asks `tip`,
721
+ `get`, `put`, `has`, `putblock`, `restore`, `append` (K24: the genesis
722
+ entry only, as the log's first; every other entry is admitted), `genesis`, `programs`
723
+ (the pinned program records, for a new genesis), `head`, `boxes`,
724
+ `dispatch` (#77: `{tip, rows}`, the dispatch table as it stands — the
725
+ host's libp2p node follows it), `byEnvelope`, `admit` (`{entry, request? | event?}`: the one call in that writes — a
726
+ request as received, #68, its record beside the entry (#121: the kernel's
727
+ door puts it, as it hands it back; a host may still put it first), a
728
+ feed's or a proof's event; never a `mail` entry, K2; it answers the CID
729
+ of the entry written, the admission or the refusal. #163: `entry` is the
730
+ arrival's body and its stamp, `{time, request+transport | event+box}` —
731
+ `time` the host's clock at admission; the kernel appends it, `prev` and `n`
732
+ its own tip's and `time` raised to the tip's stamp when the host's clock is
733
+ behind; a frame naming `kind`, `prev` or `n` is refused, so no admit is
734
+ ever refused as out of order and the host reads no tip. The genesis is the
735
+ host's, through `append`), `answer` (#66, below),
736
+ `call` (#40: host-side reads, below), `idle`, `start`, `running`; the kernel
737
+ asks `wallet` (a BRC-100 wire frame: the host answers from the instance's
738
+ ProtoWallet, the signer) and `http` (#126: one exchange for authfetch,
739
+ `{method, url, headers, body, timeoutMs?}` → `{status, headers, body}` |
740
+ `{error}`), and tells `emit` (#70: `{message, body, transport, address}`, a
741
+ committed message for a `local` or `libp2p` recipient, which the host's
742
+ providers carry out — or, transport `event`, an event: a broadcast for the
743
+ host's broadcaster, #65; a `deadline` or `fetch` intention for its waker or
744
+ HTTP proxy, #126; a subscription or a beacon for its libp2p node; an event
745
+ also carries `place: {n, seq, i, thread}` beside the record, #148: the entry
746
+ its step processed, the update's seq, its index in `emitted`, the thread —
747
+ the record itself is unchanged) and
748
+ `stop`. There is no `send` and no
749
+ `resolve` (#40): an instance delivers its mailbox messages itself, by its
750
+ delivery threads. The host starts the process when a message or a
751
+ read needs the instance and closes its stdin when it has been idle; the
752
+ kernel stops at EOF. A wallet call cut off because the host went away
753
+ aborts the step unrecorded (it runs again at the next hydration), unlike a
754
+ deterministic error, which is recorded.
755
+
756
+ **`serve` is re-entrant.** While a step waits for the host's answer to a
757
+ `wallet` request, `serve` keeps reading frames: `call`, `admit` and `answer`
758
+ are handled mid-step (an admit is processed when the running drain loops
759
+ again; an admit served while another admit's door waits on the host is
760
+ written first, and the waiting one extends it, #163: the tip is read right
761
+ before the append), and answers to other requests are kept until asked for. A request
762
+ appended mid-step is processed after that step. Since #67 a step never waits
763
+ on the network: sending is an `emit`, and the answer an entry.
764
+
765
+ ### The door (issue #121)
766
+
767
+ `admit` of a request runs the door before the entry is written
768
+ (`scheduler.zig` `door`), read-only: the kernel matches the row (`matchOf`,
769
+ #115); the transport's middleware verifies the sender — a kernel `call` of
770
+ its fn `verify` with `{request: <the record>, transport, match?, refused?}`
771
+ (the front door: BRC-104 for http on a row that is not open, GossipSub's
772
+ signature for a topic message, the message signature for `local`; it
773
+ answers `{ok, verified?}` or `{refused: {status, code?, reason}}`; a
774
+ middleware with no fn `verify`, a front door pinned before #121, gets the
775
+ package as received and no filter runs); then the row's `filter`
776
+ (`door.zig`; for a `local` package, the row of the message it carries —
777
+ `mailFilter`). The entry written is the admission, `door: {verified?,
778
+ filter?, beefs?, bodies?}` and the package as the door hands it back, or a
779
+ refusal, `refused: {stage: "middleware" | "filter", reason, status,
780
+ code?}`, which runs nothing (`processRequest`); `answer` answers a refusal
781
+ at once (`{state: "refused", refused}`). The front door's step trusts
782
+ `door.verified` and verifies only an entry written before the door.
783
+
784
+ The `beef` filter (`door.zig`, `beef.zig`): every byte string in the
785
+ package that starts with a BEEF pattern (`beef.patterns`: V1 `0100beef`,
786
+ V2 `0200beef`, Atomic `01010101`+txid, Outpoint `16a7beef`+txid+vout,
787
+ Subject `5709beef`+txid+BEEF V2) is
788
+ decoded and proven by skein-sdk's verifier (`chain/src/spv.zig`
789
+ `verify`, over the BEEF alone: every BUMP's root the header's in
790
+ `chain/state`, read only — none: refused, "no chain state"; every
791
+ transaction no BUMP proves spending only transactions before it in the
792
+ BEEF, its scripts verified; no txid-only entry; any error refused 400
793
+ `ERR_BEEF`, the error's name first); each transaction stored once as its
794
+ `bitcoin-tx` block, each BUMP as the raw block of its bytes and its merkle
795
+ nodes; the bytes replaced by the envelope `{form, beef: <pointer record>,
796
+ subject?, vout?}` (#146), the pointer record the BEEF alone
797
+ `{kind: "beef", version, txs, marks, bumps: [{height, path, block,
798
+ proves}]}`. `beef.wire` (and skein-sdk's `chain.record.wireOf`, and
799
+ `src/runtime/beef.ts` `wireOf`) give the exact bytes back;
800
+ `door.restore` puts them back in a package. `replay` copies what the door
801
+ put beside each entry (`replay.zig` `copyDoor`).
802
+
803
+ ### Requests and `answer` (issues #68, #66)
804
+
805
+ A request entry (`{request: <record>, transport}`, `log.zig`) launches the
806
+ transport's middleware as the request's thread (`scheduler.zig`
807
+ `processRequest`, `middlewareOf`: the genesis's `middleware[transport]`,
808
+ else its front door for `http`, `libp2p` and `local`), origin `{kind: "thread",
809
+ program, args: {request, transport}, launchedBy: <record>, input: <entry>,
810
+ at}` — so `requestThread(entry)` finds it again. Its steps run on
811
+ `callFuelLimit`, read `match` — the dispatch row the kernel matched for the request (#115, `putMatch`:
812
+ http by `route` and the claimed `x-bsv-auth-identity-key`, exact then longest prefix; libp2p by topic or
813
+ protocol (exact) and the peer's key, else, for a topic no row is at, the app's subscription, #119), or for http `refused` (`path` | `session` | `sender`) — (and, first,
814
+ `seen`; a genesis before #115, which carries `reads`, also `dispatch` and `reads`), and what their
815
+ stdout lists in `admit` is routed after the step (`routeAdmits`; a message
816
+ or `p2p` event once: `markUnique`, the `unique` map). A step may `await` a
817
+ thread's origin; `rested()` steps its awaiters with `resolved`.
818
+
819
+ `answer {entry, wait: ms}` → `{thread, state: "finished", answer: bytes}` |
820
+ `{thread, state: "errored", error}` when the request's thread comes to rest
821
+ for good — at once if it has; else the frame is parked, and the scheduler's
822
+ `on_answer` (called the moment the thread's last update is written, before
823
+ what its step routed runs) replies — or, `wait` ms on, `{thread?, state}` as
824
+ it stands (`waiting`, or `pending`: not processed yet). `serve`'s poll sleeps
825
+ until the earliest parked bound. The host holds a synchronous client on it
826
+ (`src/host/frontdoor.ts`).
827
+
828
+ ### Calls (issue #40)
829
+
830
+ `call {program: <CID or a genesis program's name>, fn, arg: bytes, caller?,
831
+ now?}` → `{ok, result | error, fuel}` runs a program as a function over the
832
+ current state (`scheduler.zig` `call`): its normal entry, with `input()` =
833
+ `{kind: "call", fn, arg, caller?, now, self: {handle, domain, identity},
834
+ owner, programs, peers, defaults, names, reads (a genesis before #115), dispatch,
835
+ pending, state}`; the result is its stdout, a non-zero exit the error (the
836
+ last stderr line). Since #68 no request is a call: calls are host-side
837
+ reads (the front door's fn `read`, the answer of a route that reads live
838
+ state, after its request's thread; the broadcaster's questions later, #65).
839
+ No entry and no writes: `put`/`putblock` go to the call's write cache,
840
+ dropped with it, `keep`/`launch`/`await`/`advance`/
841
+ `emit`/`deadline`/`authfetch` are refused (a call sends nothing), `wallet`
842
+ is answered by the host and not recorded, random is real. Fuel is limited by
843
+ `defaults.callFuelLimit` (default 10^10) and reported. The `call` import
844
+ (preview1 `call(prog, prog_len, fn, fn_len, arg, arg_len, out, cap)`, WIT
845
+ `call: func(program, fn, arg)`) is the same from inside a program: from a
846
+ call it reads only; from a step the callee is part of the step (its host and
847
+ meter: its recorded calls, records and head moves are the step's, and replay
848
+ re-runs it), and a step reads its own pending head moves. Depth at most 8,
849
+ output at most 64 MiB. `test/call/probe.wasm`
850
+ (`test/call/probe.zig`) is the probe `src/host/call.test.ts` drives. The
851
+ docs: `docs/VM.md`, "Calls".
852
+
853
+ ### Format 10 (issue #146)
854
+
855
+ The BEEF envelope beside the pointer record, not in it. The door puts
856
+ `{form, beef: <pointer record CID>, subject?, vout?}` where a BEEF's bytes
857
+ were; the pointer record is the BEEF alone (`version`, `txs`, `marks`,
858
+ `bumps`), so two envelopes over the same BEEF share one record. Subject
859
+ BEEF (BRC-233: `57 09 be ef`, the subject txid, a BEEF V2; the subject in
860
+ it, the rest any transactions) is one more form. A store in format 9 is
861
+ refused (re-genesis; no migration).
862
+
863
+ ### Format 9 (issue #143)
864
+
865
+ Routes, filters, roles. A route has no `sender`: its key is (transport,
866
+ address, prefix), it names `filters` (`kernel.brc104`, `kernel.beef`,
867
+ `<app>.<filter>`) run by the door before anything is recorded, and an http
868
+ route with no `program` is a read route (its filters answer). Events are
869
+ their own transport (`event`). A filter's rejection or answer, and the
870
+ gate's refusal, write no entry (`admit` answers `{answered: {…}}`; there are
871
+ no `refused` entries). The head `grants` ({kind: "grants", roles: {<role>:
872
+ [<key>…]}}) is the kernel's: the genesis's `root`, the claim (root to the
873
+ claimant) and the admin operation `grant` write it; the gate (grants.zig)
874
+ checks the principal the filters yielded against the roles that gate the
875
+ handler's function — `root` passes anything, `user` any principal, an app
876
+ role (`<app>.<role>`, declared in its app record's `roles`) its holders; no
877
+ principal fails closed. The genesis names `root` (no `owner`, no `reads`)
878
+ and `roles` (the genesis programs' gating, `{root: ["frontdoor.explore"]}`);
879
+ the steps' and calls' input has no `owner`. A store in format 8 is refused
880
+ (re-genesis; no migration). docs/VM.md "The route table".
881
+
882
+ ### Format 8 (issue #77)
883
+
884
+ The state record's `format` is 8 (`index.FORMAT`); a store in an older
885
+ format is refused for running ("before format 8": start a new store:
886
+ re-genesis); read-only uses still open it. Format 8: the kernel is the
887
+ machine, four tables and the signer.
888
+
889
+ - **The dispatch table** (`dispatch.zig`): one chain, origin `{kind:
890
+ "dispatch"}`, updates `{op: "add" | "remove", row, thread?, input, at}`,
891
+ rows `{transport: mailbox | http | libp2p | local, address, prefix? (http only),
892
+ sender: "*" | "event" | "session" | bytes(33), program: <cid> | "kernel", fn?, …}`
893
+ folded in order by key (transport, address, prefix, sender). It replaces
894
+ the subscriptions chain, the genesis's `routes` and the head `routes`:
895
+ `processMail` routes by `forMail` (a kernel row → `kernelOp`, else the
896
+ row's program launched), `processEvent` by `forEvent` (a row from `event`
897
+ or `*`; `forMail` never takes an `event` row, #79); a request thread's
898
+ input and a call's context carry the rows as `dispatch`. The genesis
899
+ carries `dispatch` (its seed) and `scopes`; one naming `subscriptions` or
900
+ `routes` is refused (`log.isGenesis`).
901
+ - **The admin operations** (`scheduler.zig` `kernelOp`): a message at a row
902
+ whose program is `kernel` is one of `objects` (`{records, root?}`),
903
+ `head` (`{name, tree}`), `dispatch` (`{op, row}`), `peers` (`{op, key,
904
+ …}`) — validated whole, then written under the entry (chain updates with
905
+ `thread: null`); a refusal is a log line and nothing written. No program
906
+ runs: `objects-handler`, `head-handler`, `subscribe-handler` and the
907
+ front door's `routes` box are gone, as is the `subscribe` import
908
+ (preview1 and WIT; skein-sdk 0.3.0). No reply. No default row admits the
909
+ instance's own key to an admin box (#87: #79's delegate row `{peers,
910
+ $self, kernel}` and its `adminDone` answer are gone), so a program's
911
+ admin message finds no row: recorded, nothing runs.
912
+ - **The loopback** (#79, `routeTo`): `emit` to the instance's own identity
913
+ needs no address book entry; the message goes to the host as transport
914
+ `local`, address `self`, and the host appends it back as a `local`
915
+ request (src/host/providers.ts), unsigned: the front door admits it as
916
+ the instance's own emit, its record in the store (#126 step 4). A key the address book does not name
917
+ routes to the `mailbox` middleware when the genesis has one (#87): the
918
+ messagebox's delivery thread reads the resolve program's record of it
919
+ (`resolve/peers`) or fails "no route".
920
+ - **Write scope** (`hAdvance`, `inScope`): `advance(name)` only when the
921
+ running program (the thread's, or the in-VM callee's: `StepState.progs`)
922
+ may write it — its record's `app` makes `<app>/…` its scope; a program
923
+ with no `app` has the genesis's `scopes[<its name>]` (exact names, or
924
+ prefixes ending in `/`); nothing else (#79: the `heads` grants on
925
+ program records are gone).
926
+ A head update carries `owner` (`heads.ownerOf`: the name's text before
927
+ the first `/`), and `dump` lists `[name, tree, owner]`.
928
+ - **The address book's `peers` operation** is `addressbook.write` (the
929
+ resolve program's writePeer, moved into the kernel), source `admin`, on
930
+ the owner's messages only. Since #87 the resolve program keeps what it
931
+ finds under its own head `resolve/peers` (genesis scope `resolve/`).
932
+ Sessions are the head `frontdoor/sessions`.
933
+
934
+ ### Earlier formats
935
+
936
+ Each format change refuses older stores for running ("before format N":
937
+ start a new store); read-only uses (`dump`, `fuel`, `replay` as a source)
938
+ still open them.
939
+
940
+ | format | issue | what changed |
941
+ |---|---|---|
942
+ | 7 | #65, #69 | `emitted` may list a broadcast event `{kind: "broadcast", tx, beef?}`; no `wake` entry (a deadline and a shell's sleep are wake-me messages to the waker); a genesis naming `jobs` refused; the kernel keeps no sleepers |
943
+ | 6 | #70, #67 | `emit` is the one way out; no `http` or `libp2p` imports and no attestations; the only recorded call is the signer's (`{kind: "oracle", thread, step, i, request, result}`); the genesis seeds the address book (`addressBook`); mail records may carry `subject`, `nonce`, `signature`; updates list `emitted`; delivery threads |
944
+ | 5 | #68 | a `request` entry per package a transport carries in; sessions as records; the `unique` map holds what request threads routed |
945
+ | 4 | #62 | recorded `http`/`libp2p` calls attested by a host key (removed by format 6) |
946
+ | 3 | #40 | messages as mail records (`{kind: "mail", op: "put", …}`), their CID the message id; no envelope or outcome entries |
947
+ | 2 | #33 | unsigned entries; identity keys and signatures as byte strings |
948
+
949
+ ## The browser build (issue #35)
950
+
951
+ The same kernel compiled to `wasm32-freestanding`, with its engine and its
952
+ store behind a small JS shim. Nothing else changes: log, scheduler, index/MST,
953
+ cbor/cid, vfs, syscalls, wasi and the programs compile as they are.
954
+
955
+ ```
956
+ cd kernel-zig && mise exec -- zig build web # → zig-out/web/skein-kernel.wasm (2.4 MB, stripped)
957
+ mise exec -- zig build -Dtarget=wasm32-freestanding # the same (any wasm target builds only the web kernel)
958
+ node --experimental-strip-types web/kernel/serve.ts # the proof page on :4401 (builds web/kernel/dist)
959
+ ```
960
+
961
+ **The two seams.** `engine.zig` is now the interface; it picks
962
+ `engine_wasmtime.zig` natively and `engine_v8.zig` when the kernel is itself
963
+ wasm. `store.zig`'s key→bytes backend is `sqlite_store.zig` natively and
964
+ `web_store.zig` in the browser. `component_web.zig` stands in for the
965
+ component host: the browser runs **preview1 modules only** for now (a
966
+ component is refused at compile with a clear message). Two lines outside the
967
+ backends: `wasi.zig`'s memory accessors call `engine.touch` (a no-op
968
+ natively), and `runner.zig` picks the component backend by target.
969
+
970
+ **The shim** (`kernel-zig/web/`, dependency-free ES modules):
971
+
972
+ | file | what |
973
+ |---|---|
974
+ | `kernel.js` | the kernel's imports and exports as a class: `skein_engine` (V8 as the engine), `skein_store` (stores by id: `MemoryStore`), `skein_peer` (the host) |
975
+ | `store.js` | `IdbStore`: IndexedDB (`blocks`: CID → bytes, `pointers`: name → CID), loaded whole at open (the kernel reads synchronously), each committed batch written behind in its own readwrite transaction (IndexedDB runs them in order, so what is durable is a prefix of what was committed); `flushed()` before an admission is acknowledged |
976
+ | `worker.js` | the kernel in a module Worker: programs are compiled and run synchronously, and the kernel's requests (wallet) block on a SharedArrayBuffer (`Atomics.wait`) while the page answers — so the page must be cross-origin isolated (COOP/COEP) |
977
+ | `client.js` | the page's side: calls as promises, notifications, requests answered into the shared buffer |
978
+
979
+ **The ABI** (`src/web.zig`). Results are left in a result buffer
980
+ (`skein_result()`), the call returning its length; a failure returns
981
+ −(length + 1) with its message there. Values are dag-cbor.
982
+
983
+ | export | what (serve's op) |
984
+ |---|---|
985
+ | `skein_alloc(n)`, `skein_free(p, n)`, `skein_result()`, `skein_args()` | buffers; `skein_args` is 16 × i64 for a host call's arguments and result |
986
+ | `skein_open(store)` | open the shim's store `store` and a runtime over it (an older-format store is refused) |
987
+ | `skein_modules()` | `[{name, file, cid}]`: the pinned modules and files to install |
988
+ | `skein_put_block(cid, bytes)`, `skein_get_block(cid)` | a block, checked against its CID on put |
989
+ | `skein_admit({entry, request?})` | `admit`: the one call in that writes (a request through the door, #121; `entry` the body and its stamp, the kernel appends it, #163); admitted, not yet processed |
990
+ | `skein_start()`, `skein_drain()` | `start`; the step loop over everything admitted (`kick`) |
991
+ | `skein_state()` | `{state, log, cursor}` |
992
+ | `skein_call({op, v})` | the other serve ops: `tip get put append programs head genesis boxes byEnvelope state`, and `call` (#40; the browser requires `now`) |
993
+ | `skein_replay(src, dst)` | `skein-kernel replay` between two shim stores, the same JSON report |
994
+ | `skein_host_call(instance, index, nargs, memlen)` | a program's import (the shim's import functions call it) |
995
+
996
+ Imports: `skein_engine` (`compile`, `instantiate`, `run`, `release`,
997
+ `fuel_get`/`fuel_set`, `mem_read`/`mem_write`, `error_len`/`error_take`),
998
+ `skein_store` (`get`/`take`, `has`, `put`, `begin`/`commit`/`rollback`,
999
+ `pointer_get`/`pointer_set`), `skein_peer` (`request(op, v)` → the answer,
1000
+ blocking, for `wallet` and `http` (#126: authfetch's bytes, the page's fetch); `notify(op, v)` for `emit` (an intention among them),
1001
+ `say`, `panic`; `take`).
1002
+
1003
+ **Programs on V8.** The kernel hands the shim a program's bytes; the shim
1004
+ compiles and instantiates them with an import object whose every function
1005
+ writes its arguments into `skein_args` and calls `skein_host_call` — the same
1006
+ host callback, by import index, as wasmtime's. An abort (exit, a park, a fatal
1007
+ host error) is a JS exception thrown through the program's frames; a trap
1008
+ comes back by V8's message (the `Trap` table). A sleeping shell re-executes on
1009
+ wake, as natively: same records. The program's memory is not the kernel's:
1010
+ during a host call the callback's `mem` is a mirror in the kernel's memory,
1011
+ whose 4 KiB pages are fetched on first touch and written back when the call
1012
+ returns (nothing else runs in the program meanwhile, and nested instances each
1013
+ have their own mirror).
1014
+
1015
+ **Fuel by instrumentation** (`src/wasm_fuel.zig`). V8 has no fuel, so the
1016
+ kernel rewrites each module before handing it over to count its own fuel in
1017
+ an exported i64 global, exactly as wasmtime v49's `consume_fuel` counts it
1018
+ (read from `crates/cranelift/src/func_environ.rs` and wasmtime-environ's
1019
+ `tunables.rs`): 1 per operator except `nop drop block loop unreachable return
1020
+ else end`; 1 per function entry; the bulk operators 1 per unit
1021
+ (memory.copy/fill/init per byte, memory.grow per page, table.* per element),
1022
+ charged on success, or up front for a small constant count (≤ 128); costs
1023
+ buffered and added to a local at loop/if/br/br_if/br_table/end/else and the
1024
+ calls, returns and traps; the local saved to the global before every call,
1025
+ return, unreachable and at the exit, reloaded after calls; checked (≥ 0 → out
1026
+ of fuel) at function entries, loop headers and after a bulk operator with a
1027
+ runtime or large count. A start function runs through a wrapper that costs
1028
+ what wasmtime's start trampoline costs (2). The kernel's `Meter` then works
1029
+ over that counter exactly as over wasmtime's fuel, so `fuel`, the in-step
1030
+ clock (#38) and exhaustion are the same numbers. **Identical to wasmtime** on
1031
+ everything checked: `wasm_fuel_test.zig` runs a probe module both ways under
1032
+ wasmtime (the fuel read at every host call, at the end, and the exhaustion
1033
+ point for every limit), `SKEIN_FUEL_MODE=instrument` runs the native kernel
1034
+ with instrumented modules (the 15 corpus stores replay to identical reports),
1035
+ and the corpus replays identically in Chrome. Not modelled, and absent from
1036
+ every pinned program: a statically out-of-bounds load/store (wasmtime makes
1037
+ what follows unreachable), and a bulk count that is a constant carried in a
1038
+ local across a control-flow merge (wasmtime may see a constant there; the
1039
+ total on success is the same, only a failed small grow or the exhaustion
1040
+ point could differ). Exceptions, GC, typed function references and memory64
1041
+ are refused.
1042
+
1043
+ **The host in the browser** (`web/kernel/host.ts`, #16): one instance whose
1044
+ identity is the connected wallet's; the page is its host. `wallet` → the
1045
+ page's BRC-100 wallet over the wire (Yours via @1sat/connect; in tests a
1046
+ wallet in the tab over a test key, @1sat/wallet-browser's `createWebWallet`,
1047
+ as the 1sat-sdk test-app boots one); `emit` → the page's own providers (#70: `fetch`, `waker`, keys from
1048
+ a page-local master), by which the instance's own programs deliver its
1049
+ messages (the messagebox's delivery threads, a BRC-104 client through the
1050
+ `fetch` provider) and resolve handles (the resolve program). The call in: the page's own messages (a chat, an install: the user's, on
1051
+ the page wallet's BRC-104 session with its instance — `http` requests to its
1052
+ front door, `frontFetch`, #126 step 4; K2: `admit` takes no mail entry), the
1053
+ providers' signed answers (`local` requests), and a timer for wakes. The
1054
+ page polls this identity's mailbox instance on the host (registered by the
1055
+ page; `listMessages` on a BRC-104 session) and shows every message there —
1056
+ what the instance sends its owner, a peer's mail to the user — and admits
1057
+ none into the instance: the mailbox is the user's (#126 step 4). An intermittent host: nothing runs while the tab is closed; inbound
1058
+ waits in the mailbox instance, wakes fire late at the next open.
1059
+
1060
+ **Proof** (`equiv/run.sh`; `SKEIN_EQUIV_BROWSER=0` skips the Chrome parts):
1061
+ `equiv/browser.ts` replays every corpus store in headless Chrome (Playwright,
1062
+ `/usr/bin/chromium`) into IndexedDB, reads it back in a fresh worker and
1063
+ compares the report and `skein-kernel dump` of what IndexedDB kept with the
1064
+ native replay — 12/12 identical (entries, updates with fuel, state CID; about
1065
+ 0.2–1 s a store in the worker). The 3 stores with the shell app installed
1066
+ (gen-run, gen-chat, gen-fuel: ~680 MB each, #83) are skipped with a note:
1067
+ above `SKEIN_BROWSER_MAX_MB` (256) a tab crashes holding the bundle, the blocks
1068
+ and the kernel's memory at once, so the shell in the browser build is no
1069
+ longer replayed (native and instrumented replays still cover them). `equiv/browser-live.ts` runs the page against a
1070
+ host on a scratch port with a scripted inference peer (on its own mailbox
1071
+ instance): the page registers its identity's mailbox instance; the chat app
1072
+ is installed and the page chats, both on the page wallet's session with its
1073
+ instance; its instance asks inference through its delivery thread (the
1074
+ kernel's authfetch, the page moving the bytes); the peer's answer lands in
1075
+ the page identity's mailbox and the page shows it, admitting nothing into
1076
+ the instance (#126 step 4: the mailbox is the user's; the agent half of
1077
+ this equiv, a reply admitted from the mailbox, went with it); the store the
1078
+ browser wrote then replays natively to the same state with no DIVERGED.
1079
+
1080
+ **Not yet:** components in the browser (the shim would need jco-style glue);
1081
+ the JSPI path (not needed: re-execution on wake); a store too big to hold in
1082
+ memory (IdbStore loads everything).
1083
+
1084
+ ## Equivalence
1085
+
1086
+ Since #5 the Zig kernel is its own reference (the TS runtime did not
1087
+ meter; it is deleted, #55). `equiv/run.sh` runs all of it (it builds first):
1088
+
1089
+ | check | what | expected result |
1090
+ |---|---|---|
1091
+ | `zig build test` | format 3's records (mail, genesis routes/reads) and, kept from format 2, genesis keys as bytes and unsigned entries, against vectors the host's TypeScript made: `test/format2.ts`); dag-cbor encodings and CIDs, canonical re-encoding, strict decoding, program-record CIDs, "anyone" signatures, JCS, the entropy stream — against fixtures src/runtime's formats make (`test/fixtures.ts`); traps in V8's words; the Merkle search tree; the index; **fuel** (`fuel_test.zig`): the same module burns the same fuel, in proportion to the work; a spinning module traps out of fuel at the limit every time, with `used` = the limit; nested instances share one budget (a step's fuel is their sum; a child or the parent runs out); a segment starts a full budget; **components** (`component_test.zig`, #34): one C program as a preview1 module, through the adapter and as a native `wasm32-wasip2` component gives the same stdout, stderr and tree, exit statuses as the ABIs carry them, and a component's fuel is repeatable, metered per step and runs out at the limit, and the in-step clock (#38) advances by the same fuel on every ABI ; the dispatch table's fold and match against `test/dispatch-cases.json` (libp2p rows exact, #119); the event record (`log.zig`, #119); the subscriptions' fold, delivery row and checks (`subscriptions.zig`, #119) | 45/45 tests pass |
1092
+ | `equiv/shell.ts` | host-go's 64 shell cases plus 7 for the script runtimes (#25; `equiv/shell-cases.ts`) through `skein-kernel shell`, against the results recorded from the TypeScript shell before it was deleted (`equiv/shell-expected.json`, #55): stdout, stderr, exit code, tree CID (none of which carries fuel). Then (#34) the same cases with every plain preview1 tool (coreutils, find, diff/cmp, jq, grep, tree, awk, sed, qjs/node, python/python3) made a component with the preview1 adapter, against the modules | 71/71 identical; as components 66/71 identical, the other 5 differing only in a printed exit status above 1 (the adapter's ok/err) |
1093
+ | `equiv/git.ts` | git (the shell app's `bin/git.wasm`, #83) in the shell on this kernel, 15 verbs over one tree; a second run gives identical trees and output | all ok |
1094
+ | `equiv/replays.ts` over `equiv/corpus.ts` | 15 logs: 9 agents (#83: each with the shell app and/or the chat app installed by the owner's messages first (#124) — the installs are in the logs; the kernel's objects/head/dispatch operations (#77), the run handler, the shell with writes, cwd, failures, sleeps and wakes, the script runtimes, the loop with bash and message tools, a `bash` call with no shell app, replies, resolutions, a failed delivery, a refused infer, a stranger's chat on a row the owner added, two agents, and `gen-fuel`: `fuelPerStep` 10^9, which the installs fit and a spinning shell does not) and the 6 owner's mailbox instances they delivered into (`<name>-david`), each replayed by the Zig kernel into z1, and z1's log replayed into z2. Since #40 the corpus is written by the Zig kernel as the host drives it (format 6 since #70/#67: every message out an `emit`, signed through the recorded signer; on a script clock, the owner over raw BRC-33 on BRC-104 sessions, the inference peer on its own mailbox instance, the instances delivering by their own delivery threads through the host's `fetch` provider, whose signed answers are `local` requests in the log), so each source is also reproduced exactly by its replay | 15/15 identical, sources reproduced |
1095
+ | `equiv/wallet.ts` (#29, #79) | on a test host with a fake Arcade behind its broadcaster and a fake SSE header feed, the chain app (shruggr/skein-chain) installed into two instances and the wallet program on the owner's `wallet` box: the feed's headers reach the chain app (the wallet takes none); a BRC-29 payment internalized (SPV against `chain/state`) and ingested; a spend signed through the signer whose step emits one `ingest` message to the instance itself and no broadcast — the chain app broadcasts it (Extended Format, the host's token), Arcade's RECEIVED answers the wallet's thread `accepted`, MINED `proven`; the payee internalizes it and is answered the same; a rejected broadcast gives the balance back; a draft signed by `signAction`; settlement at the chain app (A rejected, B input-rejected, both threads answered, the coins read back); a reorg run re-broadcast by the chain app and the change listed unproven; the heads `wallet/state` and `chain/state`; both stores replayed | all ok; both reproduced exactly |
1096
+ | `equiv/boot.ts` (#4) | `skein-host system` → a system tree (one handler as .wasm bytes, a SOUL.md, a config default); `add --boot <dir>` and `add --packet` of a mined ordfs-form packet of it (`--proofs`); a wrong `--scope` refused; both genesis name the tree, `main` is it, programs from bin/ (no shell, #83); the apps installed on top by the owner's messages (alpha the chat app, beta the chat app and the shell app); each chatted with (the loop reads SOUL.md from main), beta run over main; the wallet's component build (#34) as `bin/wallet.wasm` answering a `list`; `pack --checkpoint` restored on a second host (same master key): `dump` identical, still answering; both booted stores replayed Zig against Zig | all ok; both reproduced exactly |
1097
+ | `equiv/wallet.ts` (#34 part) | after the preview1 scenario: `equiv/abi.ts` replays its log with the wallet's component build in the module's place (`SKEIN_REPLAY_MODULE`), and the scenario runs again through the host with the component as the wallet program | no DIVERGED; the same chains and log lines; every update identical but for `fuel` and `prev` (the CID of the update before); the component's replay reproduces itself; the component run reports exactly what the module's did, and its store replays to itself exactly. Since #65 both builds broadcast by emitting the same event, so this also shows the two ABIs emit byte-identical records |
1098
+ | `equiv/fetch.ts` (#15, #70) | the `fetch` component as a box's handler through the host: it emits a GET to the `fetch` provider (the host's http handler standing in for the network) and writes the answer's body on its next step; then `replays.ts` with no host | the body on stdout; the update lists the emitted message; the provider's signed answer is a `local` request in the log; a 404 comes back to the program; the store replays exactly with no host to ask |
1099
+ | `wasm_fuel_test.zig` (#35) | fuel by instrumentation against wasmtime's on a probe module (bulk operators, grows that fail, branches to the function's label, call_indirect, a trap, a start function): the same reading at every host call and at the end, and the same exhaustion for every limit; every pinned program instruments to a valid module | ok |
1100
+ | instrumented fuel (#35) | the corpus replayed natively with `SKEIN_FUEL_MODE=instrument` (every module metered by its own counter) | 15/15 identical reports |
1101
+ | `equiv/browser.ts` (#35) | the corpus replayed by the wasm kernel in headless Chrome into IndexedDB, read back, against the native replay: report and dump; a store above `SKEIN_BROWSER_MAX_MB` skipped with a note (#83: the 3 with the shell app) | 12/12 identical, 3 skipped |
1102
+ | `equiv/browser-live.ts` (#35, #126 step 4) | an instance in Chrome, the chat app installed into it by the install client's plan sent by the page as its owner on its session with its instance (#83), chatted the same way; its inference request delivered by its own delivery thread; the answer shown from the page identity's mailbox, nothing admitted; its store replayed natively | all ok |
1103
+ | older-format refusal | a store in sqlite.ts's tables with a host-signed genesis (`equiv/old-store.ts`: format 1, no fuel) opened for running (`skein-kernel shell`) | refused, with the message |
1104
+ | `equiv/overlay.ts` (#36, #40, #79) | overlay nodes booted from system trees carrying skein-overlay's and skein-chain's modules and their #77 rows: @bsv/sdk's `TopicBroadcaster` and `LookupResolver` at the host-name origin; a submission ingested at the chain app and admitted on its first `accepted`/`proven` answer; GossipSub the same state; lookups, dupes, refusals, listings; a spend rejected at the chain app unwound by the overlay's watch; proofs as IPLD nodes; the busy gate; no Arcade (admitted at the proof); the three-host gossip (#74); all replayed | all ok; reproduced exactly |
1105
+ | `equiv/emit-events.ts` (#119) | two apps (app-demo's module as `evt-a`, no libp2p row, and `evt-b`, an exact row `demo_mine`) installed on a router with libp2p; refused as emitted: a subscribe with no program, a program not the app's, no fn, a `/protocol`, an unsubscribe of nothing the app has; evt-a emits `subscribe {topic: demo_abc, program: "demo", fn: "topic"}`: the update lists it with `app: "evt-a"`, the node subscribes it, a message published from a second router is delivered by the subscription to evt-a's fn (its head `evt-a/seen` moves); evt-b's subscribe of the same topic is its own (the next message is still evt-a's); evt-a unsubscribes: the next is evt-b's; evt-b unsubscribes: the node leaves it, a second unsubscribe refused; a row wins (evt-a's subscription to demo_mine: the message is evt-b's, by its row); an event nobody wires is a record and a log line; the fold read from the log; a new router on the same host subscribes the same topics and its kernel delivers by the subscription; replayed | all ok; reproduced exactly |
1106
+ | `equiv/install-overlay.ts` (#72, #79) | skein-chain and skein-overlay 0.3.0 installed by the owner's messages (#124; the overlay refused without the chain app: `requires chain/1`); its derived rows (its box from `event` and `$self`, /submit, /lookup, the libp2p topics) and no grants; a gossiped token admitted once the chain app answers; two overlay apps on one instance (the same tree as `overlay2`), each writing only its own heads over one `chain/state`; reinstall, uninstall; replayed | all ok; reproduced exactly |
1107
+ | `equiv/host.ts` (#90) | a host as `skein-host run` runs it (the signer over a master secret): `skein-host init` makes the host skein from the default image, the instance manager in its address book, and the operator claims it from the operator's wallet (#127); the onboarding app (shruggr/skein-onboard at a pinned commit, or `$SKEIN_ONBOARD_DIR`) installed in it; a client's BRC-104 session POSTs `onboard.create` and is answered `{handle, identity, url}` on the same connection; the child claimed by the client's own signed claim, forwarded (its first entry after its genesis; none or another key's refused), serving at its url, its book without the manager; the client installs app-demo in it and calls it; the host skein's `onboard/instances/alice` → the manager's answer; a taken handle and a bad one refused (409), no session refused; both stores replayed | all ok; both reproduced exactly |
1108
+ | `equiv/git-clone.ts` (#91) | deploy by hash: the git app (shruggr/skein-git at a pinned commit, or `$SKEIN_GIT_DIR`) installed; a local repository (app-demo, and a commit without its manifest) served by `git http-backend` under node:http, the fetch provider's network real (`fetchHttp`); the owner's `git.clone {url, hash}` → the advertisement and one shallow pack (protocol v2, `maxBytes` on both), the commit and its tree kept, no head moved, `{tree, app}` answered in the owner's mailbox; the manifest read out of the stored tree by CID; `readStoredApp` + `planInstall` rebuild the same record CID (and an install of the checkout would write it too), nothing to send by `objects`; `head` + `dispatch` + `start`, the app's tick and a call answered; a hash not held (`not-found`), a server sending another commit's pack (`mismatch`), a non-http URL and a short hash (`bad-args`), no repository (`not-git`), no manifest (`no-manifest`); the clone again carries the installed app's state; a stranger refused; replayed | all ok; reproduced exactly |
1109
+ | `equiv/site.ts` (#92) | the management site (shruggr/skein-site, an app since #125: installed in the host skein with the owner's root row, its page at `/site/`) in headless Chrome, served by the host skein of a host as `skein-host run` runs it, with a wallet in the tab over a test key (`?key=`) funded through a stand-in for the 1sat services (one block's header, a broadcast that accepts — the only stand-in): create alice from the page (the onboarding app's route), nothing written into the wallet, the page opens alice's view (she serves no page: #125); the git app installed from the tree her image carries, then app-demo by hash (a local repository over `git http-backend`) — the clone's answer read from its thread, the record rebuilt in the page, the prompt approved, head + dispatch + start — and app-demo answers; the explorer renders her log (0 the genesis, 1 the claim) and dispatch table, another key's read refused; Your skeins is the host's answer (`GET /skeins?key=`, from the skeins' grants): the host skein and alice; the host skein's page lists alice; (#103) a handle registered from the page — the host's certificate kept in the wallet (`acquireCertificate`, direct), listed by `listCertificates`, "Your handles" resolving it, the Inbox prefilled with its messagebox; (#104) its profile set from the page (signed by the wallet, written to the mailbox instance), served by resolve, shown as signed by your key, found by "Find a handle"; (#99) a BRC-169 delivery listed and synced into the wallet; the stores replayed | all ok; every store reproduced exactly |
1110
+ | `equiv/chain.ts` (#78) | the chain module, shruggr/skein-chain at a pinned commit (or `$SKEIN_CHAIN_DIR`), installed by the owner's messages on a host with a fake Arcade (its broadcaster and `$status` provider) and called by its owner (#79: its rows take `event`, `$self` and `$owner`; a stranger is refused): the feed's headers; `ingest` proven → answered at once; `ingest` unproven → broadcast (the Arcade gets it) → `accepted` (the status provider's RECEIVED) → `proven` (Arcade's MINED proof, its header first), each an answer to the same request, a second watcher told too; refused by Arcade → `rejected`; `status`/`proof` reads by message and as a kernel call; then the same app at boot from a system tree (the default files, `bin/chain.wasm`, its rows, the default scope `chain/`); both stores replayed | all ok; both reproduced exactly |
1111
+ | `equiv/image-chain.ts` (#132, #156) | the image carries the header chain: a fake chaintracks (history by height and a tip stream) with N regtest headers before the host starts and M more on its stream; the host builds the default image's chain part from genesis and grows it per header; a skein created from the image holds `chain/headers` and `chain/tip`; the empty host chain imports them at her first start (`chain-import`; `skein-host chain` shows the image's tip); the chain app (shruggr/skein-chain 0.7.0) installed by the owner: the install's `chain` event gives her chain state every header from genesis, no header fed, its maps the host chain's; the next header moves it on; `skein-host add --image default` from the same image; replayed; on SQLite and on RocksDB | all ok; reproduced exactly |
1112
+ | `equiv/hostchain.ts` (#156, #133) | two instances on one RocksDB host take three header runs: their `chain/state` `headers`/`heights` roots are the host chain record's, the tip read off them the host's; a proof on A verifies; a heavier branch replacing two headers reverts it (unproven, broadcast again) and leaves B on the new record; the gate (a host chain block through every set, A's record not through B's); a serve (SQLite) instance taking the same runs has the same `chain/state` CID and host chain record; every store replayed after the processes stopped; timings: one block's `headers` frame and its event stepped on 8 instances, `chain-import` of an image of `HOSTCHAIN_IMPORT` headers (default 20 160) on RocksDB and SQLite | all ok; reproduced exactly |
1113
+ | both backends (#96) | every store `replays.ts` checks also replayed into a set of a fresh LMDB environment (the same report, state CID and dump — the same CIDs) and, a SQLite one, migrated (`skein-kernel migrate`: the same dump; its replay the same state); the corpus, `serve.ts`, `chain.ts`, `overlay.ts` and `sync.ts` run again with every host's instances sets of one environment (`SKEIN_EQUIV_STORE=lmdb`), their sets replayed the same way; run.sh prints the corpus's wall time on each | all identical |
1114
+ | RocksDB, one process (#158) | every store `replays.ts` checks also replayed into a set of a fresh RocksDB database (the same report, state CID and dump) and, a Zig one, migrated into one (the same dump; its replay the same state); the corpus, `serve.ts`, `chain.ts`, `overlay.ts`, `sync.ts`, `claim.ts`, `host.ts` and `git-clone.ts` run again with every host's instances sets of one database inside one `skein-kernel host` (`SKEIN_EQUIV_STORE=rocksdb`), their sets replayed after it stopped | all identical |
1115
+ | `equiv/pool.ts` (#158) | 8 instances × 500 entries submitted interleaved: each final state CID equals a serial run's and SQLite's; the same with `SKEIN_WORKERS=1`; a `call` on an instance whose step waits on a `wallet` request answered late comes back before the step ends, with the state as committed (the entry pending); a block only bob put is absent for alice (her kernel, her call, the host's reader), a block in `host/chain` readable by both; each set replayed after the process stopped, the same state CID; timings (1 vs N workers, commit cost on RocksDB and SQLite) | all ok; timings printed |
1116
+ | `equiv/lmdb.ts` (#96, #133) | two instances in one environment: a record and a program only bob put are absent for alice (her kernel's `has`/`get`, her `call` of the program; the host's lmdb-js reader too); a block both put in both sets; a block under `host/chain` (written straight into the environment, `equiv/lmdb-env.ts`) readable by both kernels and the reader; each set's size the sum of its own members' bytes; 8 instances on one router writing N entries and N blocks each at once, SQLite against LMDB (`SKEIN_EQUIV_BENCH_DIR` for a disk) | all ok; the timing printed |
1117
+ | `equiv/serve.ts` | `serve` as the host drives it, the owner (raw BRC-33, and a signed `/account/register`) and the inference peer on BRC-104 sessions with the instances' front doors and their mailbox instances: genesis, the shell app and the chat app installed (#83), a run, a chat through the inference peer, an idle stop mid-sleep and the waker's hydration that finishes it, a host restart mid-sleep, mail surviving it; (#38) 50 ms busy-waits on the in-step clock (qjs, python) ending on their own under a 10^9 fuel limit; a second instance with `SKEIN_FUEL_PER_STEP=10^9` where `while :; do :; done` runs out (run-handler replies `fuel exhausted`; `skein-kernel fuel` shows the shell's step at exactly the limit); then both stores replayed Zig against Zig | all ok; both stores reproduced exactly by their replays |
1118
+
1119
+ A replay comparison (`equiv/replays.ts`) requires z1 and z2 to be the
1120
+ same in: the replay reports (the runtime's log lines, the log tip, the final state record — which covers every index
1121
+ node); everything `skein-kernel dump` derives (entries, chains and tips,
1122
+ every update with its fuel, threads, resting threads in resume order,
1123
+ sleepers, awaits, edges, heads, the cursor, fuel per thread and in total,
1124
+ every record's CID); `skein-kernel fuel`'s output; and every explorer page
1125
+ rendered over the two files (through `src/runtime/index-store.ts`). It also
1126
+ requires every update that ends a step to carry fuel (only `running` ones
1127
+ lack it), every `fuel exhausted` update to be `errored`/`cant-do` with fuel
1128
+ = the genesis's limit (and at least one in a `*fuel*` store), no `DIVERGED`
1129
+ or `cannot run` line, and — for a source the Zig kernel wrote itself (every
1130
+ store now: the corpus and `serve.ts`) — the source's dump to equal z1's: the
1131
+ running kernel and the replay agree exactly. Sources are copied
1132
+ (with their `-wal`) before anything opens them.
1133
+
1134
+ Dropped with #5: the comparisons with the TS runtime's replay (row-by-row
1135
+ tables, TS-built state CIDs, "vs the source" tips) and the replays of copies
1136
+ of the stores under `~/.skein` (they predate fuel; the live instances
1137
+ re-genesis when they move to this build).
1138
+
1139
+ ## The machine and the native host (the settled split, 2026-10-31)
1140
+
1141
+ The **machine** is the step machine for ONE instance — the log, the door,
1142
+ dispatch, the scheduler, program execution, the signer import, content
1143
+ addressing: everything below but the native host's files. It builds two
1144
+ ways from the same source:
1145
+
1146
+ - **`zig build web`** → `zig-out/web/skein-kernel.wasm` (`web.zig` the
1147
+ exported ABI, `engine_v8.zig` the engine through the host's wasm runtime,
1148
+ `web_store.zig` the host's store by id): **the node host loads it**, one
1149
+ per active instance on a worker thread, its storage the host's own (one
1150
+ SQLite database for the host, `src/host/store-sqlite.ts`; the host chain
1151
+ held by one more machine over the `host/chain` set,
1152
+ `skein_open_chain`) — what the npm package ships (`src/host/wasm-kernel.ts`);
1153
+ the browser host loads the same file.
1154
+ - **`zig build`** → `zig-out/bin/skein-kernel`: the **native host**, the
1155
+ hosted version, a peer of the node host kept here beside the machine:
1156
+ `serve.zig` + `ipc.zig` (one instance per process over pipes), `host.zig`
1157
+ + `rocksdb_store.zig` (#158: one process, a worker pool over a RocksDB
1158
+ database), the store backends `sqlite_store.zig`, `lmdb_store.zig`,
1159
+ `any_store.zig`, `migrate.zig`, `vendor/`, the engine
1160
+ `engine_wasmtime.zig`, and the tools (`replay.zig`, `dump.zig`,
1161
+ `fuel.zig`, `cmd_shell.zig`, `main.zig`). The node host still runs it for
1162
+ a store spec that is a file, an LMDB set or a RocksDB set (the rack's
1163
+ hosted shape); `skein-kernel replay` is the native reference a wasm
1164
+ host's store is checked against.
1165
+
1166
+ ## Layout
1167
+
1168
+ | file | what (the TypeScript it was ported from; the machine's parts were deleted in #55) |
1169
+ |---|---|
1170
+ | `cbor.zig`, `cid.zig` | dag-cbor as @ipld/dag-cbor + cborg do it; CIDs (`cid.ts`) |
1171
+ | `json.zig` | JSON.stringify quoting, Number::toString, RFC 8785 (`envelope.ts jcs`) |
1172
+ | `secp.zig` | BRC-42 "anyone" keys, ECDSA verify as @bsv/sdk (`identity.ts`) |
1173
+ | `store.zig`, `sqlite.zig`, `sqlite_store.zig` | the store interface; the SQLite file as blocks + the state pointer, and the import of the old format (`store.ts`, `sqlite.ts`; `index-store.ts` reads this format) |
1174
+ | `lmdb_store.zig`, `any_store.zig`, `migrate.zig`, `vendor/lmdb/` | the shared host store (#96, #133): a set of one LMDB environment, its member gate and `host/chain`; either backend for serve and the tools; `skein-kernel migrate`; LMDB 0.9.36 |
1175
+ | `host_chain.zig` | the host chain's tracker (#156): the SDK's `chain.host` over a kernel backend (`get`, `put`, `putBlock`), the frames `headers`, `chain-import`, `chain` |
1176
+ | `rocksdb_store.zig`, `host.zig` | the one-process kernel (#158): the RocksDB backend (a set of one database, its gate, a call's committed `View`); `skein-kernel host` (the I/O thread, the worker pool and run queue, the host-read socket) |
1177
+ | `index.zig`, `mst.zig`, `dump.zig` | the index as maps in the store and the state record (#30); the Merkle search tree; `skein-kernel dump` |
1178
+ | `log.zig`, `heads.zig`, `dispatch.zig`, `programs.zig`, `syscalls.zig` | `log.ts`, `records.ts`, `heads.ts`, the dispatch table (#77; `src/runtime/dispatch.ts` reads it), `programs.ts`, `syscalls.ts` |
1179
+ | `engine.zig`, `engine_wasmtime.zig` | the engine interface; wasmtime behind compile/run/one host callback; traps as V8 names them; the fuel meter (#5) |
1180
+ | `engine_v8.zig`, `web_store.zig`, `component_web.zig`, `web.zig`, `wasm_fuel.zig`, `web/` | the browser build (#35): V8 through the shim, the shim's stores, preview1 only, the exported ABI, fuel by instrumentation, the JS shim |
1181
+ | `wasi.zig`, `vfs.zig`, `tree.zig` | the WASI host and vfs (deleted); `tree.ts` |
1182
+ | `objects.zig` | none: the synthetic `.git/objects` (issue #2, docs/VM.md), loose-object framing over git-raw records |
1183
+ | `runner.zig`, `shell.zig`, `program.zig` | module cache and `runModule`; the shell; one program step and the `skein` imports (deleted) |
1184
+ | `scheduler.zig` | the scheduler (deleted) |
1185
+ | `serve.zig`, `ipc.zig` | the kernel as the host drives it (`src/host/kernel.ts`, issue #33) |
1186
+ | `replay.zig`, `cmd_shell.zig` | `skein-kernel replay` (was `skein-dev replay`); the shell test driver |
1187
+ | `fuel.zig`, `fuel_test.zig` | `skein-kernel fuel` (fuel accounting as a query over the log); the fuel unit tests (issue #5) |
1188
+ | `component.zig`, `component_test.zig`, `test/components/` | WASI 0.2 components (issue #34): the standard worlds and `skein:kernel/skein` over the preview1 implementation; the unit tests' C program in three builds and the `fetch` component (`build.sh`) |
1189
+ | `addressbook.zig` | the address book (#70, #77): the head `peers`, lookup by key, the genesis's seed, the kernel's `peers` operation (`write`) |
1190
+ | `authfetch.zig` | BRC-104's SimplifiedFetchTransport framing (the payloads a request and a response sign), which the door checks; the in-step client it was (#126) is gone (#157) |
1191
+ | `beef.zig`, `door.zig`, `door_test.zig` | the door (#121): BEEF's pattern table, decoder and encoder, the BUMP's nodes and root; the row filters (`beef`) over a package, against `chain/state`'s headers; `restore` |
1192
+ | `signer.zig` | the kernel's own BRC-100 frames (#70, #126): `createSignature` (an emitted message), `createHmac` and `verifySignature` (the door's BRC-104 checks), the answer |
1193
+
1194
+ ## What is not the same, or not here
1195
+
1196
+ - **The synthetic object directory** (issue #2) is only here. In a tree,
1197
+ `.git/objects/xx/yyyy…` is a gitlink naming the git-raw record; to
1198
+ programs it is a read-only file of zlib bytes; a loose object that git
1199
+ writes is hash-checked and kept as the record; `.git/objects/pack` takes
1200
+ nothing new (docs/VM.md). The TypeScript runtime treated those paths as
1201
+ plain files and gitlinks as empty directories.
1202
+
1203
+ - **Sleeping shells** are not parked mid-instance (no JSPI): the run is
1204
+ abandoned at the sleep after writing `waiting`, and the wake re-executes the
1205
+ thread from its origin, verifying every update against its chain — the
1206
+ restart path — and carries on under the waker's answer (#69, #126: the
1207
+ sleep is a `deadline` event). Same records; a re-execution per wake.
1208
+ - **Messages that only exist as JavaScript text**: trap messages are mapped
1209
+ to V8's wording for the common traps (unreachable, out-of-bounds memory,
1210
+ division, conversion, indirect calls; stack overflow as V8's RangeError);
1211
+ wasm compile errors, cborg decode errors ("CBOR decode error") and
1212
+ multiformats CID decode errors are approximated. They reach records only
1213
+ when a program prints what an import told it or a step fails that way.
1214
+ - **Engine limits**: wasmtime's default wasm stack (512 KiB) and V8's differ,
1215
+ so a deep enough recursion overflows at a different depth.
1216
+ - **The drain's own crash lines** (`runtime: <stack>`) are not stack traces.
1217
+ - **The store file** (#30): blocks and the state pointer, not sqlite.ts's
1218
+ tables; the explorer, `skein-dev` and `skein-host` read it
1219
+ (`src/runtime/index-store.ts`, which ignores the state record's `format`).
1220
+ - **Not ported**: the index rebuild of a stale older store (`edges.rebuild`;
1221
+ such a store is refused), `putMessage` and
1222
+ the `messages` table (the runtime does not write them), `handles`, the
1223
+ `waitersOn`/`waitingFrom`/`due` queries (the kernel does not ask them; the
1224
+ maps would be `launched ‖ origin` and `identity ‖ origin`, and `due` is a
1225
+ range of `sleepers`).
1226
+ - **Components in the browser build**: the browser build runs preview1
1227
+ modules only ("The browser build" above). The
1228
+ seams are `engine.zig` and `component.zig` (the wasm engine) and `index.Backend` (a key→bytes
1229
+ map with one pointer: blocks by CID, a transaction, the state pointer);
1230
+ the scheduler, the index, the WASI and skein imports, the vfs and the
1231
+ codecs touch no OS. `serve.zig` and `replay.zig` are the native front ends.
1232
+
1233
+ ## Script runtimes (issue #25)
1234
+
1235
+ The shell's `qjs`/`node` and `python`/`python3` run here as in `shell.ts`.
1236
+ The shell program record names them (#83: the shell app's install writes it
1237
+ from its manifest, docs/APPS.md §6b): `modules` maps every command to a raw
1238
+ module CID (`node` and `qjs` one module, `python3` and `python` another),
1239
+ and `support.python`/`support.python3` name the stdlib (`python314.zip`, a
1240
+ raw block of the app's tree) and the env defaults; `shell.zig loadModules`
1241
+ reads both from the record, cached by its CID. `shell.zig` dispatches a `#!` script to its interpreter when
1242
+ that is an extra program, adds a program's env defaults (`PYTHONHOME`,
1243
+ `PYTHONDONTWRITEBYTECODE`) unless the caller set them, and mounts its support
1244
+ files read-only for that process only (`wasi.zig` `Services.mounts`, a
1245
+ preopen at fd 4 over in-memory nodes outside the tree; a write open there is
1246
+ EROFS). Outside a thread, a sleep moves the run's clock to its deadline
1247
+ (`shell.Fixed.sleep`), as `runShell` does without `clock`/`sleep`.