@dcasegr/skein 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +522 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +773 -0
- package/dist/host/router.js +2389 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
|
@@ -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`.
|