@dcasegr/skein 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +525 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +782 -0
- package/dist/host/router.js +2411 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Store } from "../runtime/store.ts";
|
|
2
|
+
import { type BootSource } from "./boot.ts";
|
|
3
|
+
import type { HostDb } from "./instances.ts";
|
|
4
|
+
/** Headers per block (skein-sdk `image.per_block`). */
|
|
5
|
+
export declare const PER_BLOCK = 2016;
|
|
6
|
+
export declare const blockName: (first: number) => string;
|
|
7
|
+
/** Why a header (80 bytes) is not one the chain app takes on its own: an unusable target, or a hash that misses it. */
|
|
8
|
+
export declare function headerProblem(raw: Uint8Array): string | undefined;
|
|
9
|
+
export interface ImageChainOptions {
|
|
10
|
+
db: HostDb;
|
|
11
|
+
/** The chaintracks headers endpoint (`…/chaintracks/v2/headers`): history by height. */
|
|
12
|
+
history?: string;
|
|
13
|
+
/** Headers per history request (default 100 800: 50 blocks). */
|
|
14
|
+
page?: number;
|
|
15
|
+
/** The static part (default: the repo's images/default). */
|
|
16
|
+
dir?: string;
|
|
17
|
+
/** The host image's own part (#142, default: the repo's images/host), merged over the static part. */
|
|
18
|
+
hostDir?: string;
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
log?(line: string): void;
|
|
21
|
+
}
|
|
22
|
+
/** The history endpoint beside a chaintracks tip stream (`…/tip/stream` → `…/headers`), if the URL is one. */
|
|
23
|
+
export declare function historyOf(feed: string | undefined): string | undefined;
|
|
24
|
+
/** The image names (#152): `default`, and `host` (images/host merged over it, #142). */
|
|
25
|
+
export declare const IMAGE_NAMES: readonly ["default", "host"];
|
|
26
|
+
export declare class ImageChain {
|
|
27
|
+
readonly o: ImageChainOptions;
|
|
28
|
+
/** chain/headers' blocks, in height order. */
|
|
29
|
+
private blocks;
|
|
30
|
+
/** The last block's headers (raw). */
|
|
31
|
+
private last;
|
|
32
|
+
private queue;
|
|
33
|
+
private stat?;
|
|
34
|
+
/** #142, #152: the overlay images' static parts (images/host, a directory with no `bin/`), each merged over the default image once. */
|
|
35
|
+
private merged;
|
|
36
|
+
/** Settles once the first sync (start) is done: what a creation waits for. */
|
|
37
|
+
ready: Promise<void>;
|
|
38
|
+
constructor(o: ImageChainOptions);
|
|
39
|
+
private say;
|
|
40
|
+
private object;
|
|
41
|
+
/** A block's headers (the one read last kept: a search below the tip reads one block many times). */
|
|
42
|
+
private blob;
|
|
43
|
+
private read?;
|
|
44
|
+
/** The tip's height (-1: no chain part). */
|
|
45
|
+
height(): number;
|
|
46
|
+
tip(): {
|
|
47
|
+
height: number;
|
|
48
|
+
hash: string;
|
|
49
|
+
} | undefined;
|
|
50
|
+
/** The header at a height (raw), from its block. */
|
|
51
|
+
private at;
|
|
52
|
+
/** One at a time: every change to the chain part goes through here. */
|
|
53
|
+
private serial;
|
|
54
|
+
/**
|
|
55
|
+
* Put `raws` at `height` (at most the tip + 1): the headers held from there
|
|
56
|
+
* that differ are replaced (a reorg), the ones that match are kept, the
|
|
57
|
+
* rest appended; a new tree. Whether anything changed. Refused: a gap, a
|
|
58
|
+
* header that does not link to the one before it, one the chain app would
|
|
59
|
+
* refuse on its own.
|
|
60
|
+
*/
|
|
61
|
+
put(height: number, raws: Uint8Array[]): Promise<boolean>;
|
|
62
|
+
private write;
|
|
63
|
+
/** The static part: the image directory, scanned once (boot.ts imageDir: the apps it names beside it too). */
|
|
64
|
+
private static;
|
|
65
|
+
/**
|
|
66
|
+
* #142, #152: an overlay image's static part — its directory (images/host, or one with no `bin/`) merged
|
|
67
|
+
* over the default image (boot.ts overlayImage, mergeImages), once per directory.
|
|
68
|
+
*/
|
|
69
|
+
private overlayStatic;
|
|
70
|
+
/** The image's root: the static part's entries, `chain` among them. */
|
|
71
|
+
private rootWith;
|
|
72
|
+
/**
|
|
73
|
+
* The current image as a boot source (#89, #132): the static part with the
|
|
74
|
+
* chain part at `chain/`; with no chain part, the static part alone (the
|
|
75
|
+
* repo's images/default, as before). `host` / `image: "host"`: the host
|
|
76
|
+
* image; `overlay` (#152): a directory with no `bin/`, merged over the
|
|
77
|
+
* default image as the host image is, with the same chain part. Its
|
|
78
|
+
* objects: host.db's, the scan's, the repo's pinned modules.
|
|
79
|
+
*/
|
|
80
|
+
source(o?: {
|
|
81
|
+
host?: boolean;
|
|
82
|
+
image?: string;
|
|
83
|
+
overlay?: string;
|
|
84
|
+
}): Promise<BootSource>;
|
|
85
|
+
/**
|
|
86
|
+
* Headers from the stream (feeds.ts, in order): each appended at the tip
|
|
87
|
+
* when it links there; one that links to a header a little below the tip
|
|
88
|
+
* replaces from there (a reorg); otherwise the history fills in (a gap,
|
|
89
|
+
* a deeper reorg), or, with none, it is dropped.
|
|
90
|
+
*/
|
|
91
|
+
headers(raws: Uint8Array[]): Promise<void>;
|
|
92
|
+
/** Fill in from the history (#132): from genesis once, then from a little below the tip. */
|
|
93
|
+
sync(): Promise<void>;
|
|
94
|
+
private syncNow;
|
|
95
|
+
/**
|
|
96
|
+
* #141: the headers a skein whose tip is `tip` lacks, from the image's chain, in order — what the
|
|
97
|
+
* host pushes it on (re)subscribing, before the live stream. From the header after its tip when
|
|
98
|
+
* the image holds that tip; a tip the image does not hold at its height (a fork) from a little
|
|
99
|
+
* below it; none when the skein is at or past the image's tip, or has no tip (no chain part).
|
|
100
|
+
* Run in turn with the chain part's writes: every header the feed brought before this call is in.
|
|
101
|
+
*/
|
|
102
|
+
after(tip: {
|
|
103
|
+
height: number;
|
|
104
|
+
hash: string;
|
|
105
|
+
} | undefined): Promise<Uint8Array[]>;
|
|
106
|
+
/** Start: the first sync, which creations wait for (`ready`). */
|
|
107
|
+
start(): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* shruggr/skein#150: the chain app's best-chain header at `height` (the head `chain/state`, map
|
|
111
|
+
* `headers`: the MST height (u32 big-endian) → header): its hash (display hex) and merkle root
|
|
112
|
+
* (display hex); undefined when the chain state holds none there.
|
|
113
|
+
*/
|
|
114
|
+
export declare function chainHeader(store: Store, height: number): Promise<{
|
|
115
|
+
hash: string;
|
|
116
|
+
root: string;
|
|
117
|
+
} | undefined>;
|
|
118
|
+
/**
|
|
119
|
+
* #141: a skein's chain tip as its store holds it: the chain app's state (the head `chain/state`,
|
|
120
|
+
* {kind: "chain-state", maps: {headers}} — the MST height (u32 big-endian) → header, skein-sdk
|
|
121
|
+
* chain.zig; its last key is the tip), else the chain part of the tree it was born from (`main`'s
|
|
122
|
+
* `chain/tip`, image.zig's format), else none.
|
|
123
|
+
*/
|
|
124
|
+
export declare function skeinTip(store: Store): Promise<{
|
|
125
|
+
height: number;
|
|
126
|
+
hash: string;
|
|
127
|
+
} | undefined>;
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// The default image's chain part (#132): the whole header chain, from the
|
|
2
|
+
// network's genesis header to the last header the host received, as blocks
|
|
3
|
+
// in the image tree — so a skein made at any moment is born with the chain up
|
|
4
|
+
// to the current tip, and its chain app starts from it (skein-sdk
|
|
5
|
+
// `chain.image`). Headers are pushed, never pulled by a skein: the host grows
|
|
6
|
+
// the image, and from its creation on a skein gets each new tip as an event,
|
|
7
|
+
// as before.
|
|
8
|
+
//
|
|
9
|
+
// The layout (skein-sdk chain/src/image.zig, the same bytes):
|
|
10
|
+
//
|
|
11
|
+
// chain/headers/<first> the raw 80-byte headers, concatenated in height order, 2016 per block,
|
|
12
|
+
// <first> the first one's height in 8 digits (00000000, 00002016, …);
|
|
13
|
+
// every block but the last is full; the last holds the rest
|
|
14
|
+
// chain/tip {"height":<n>,"hash":"<display hex>"} and a newline
|
|
15
|
+
//
|
|
16
|
+
// The image tree is the static part — the repo's images/default, scanned —
|
|
17
|
+
// with `chain` added at its root. **A new tree per header**: each header the
|
|
18
|
+
// host takes rewrites the last block (at most 2016 × 80 bytes), the tip, the
|
|
19
|
+
// two trees above them and the root; every full block stays as it is. The
|
|
20
|
+
// chain part's objects live in host.db (`image_blocks`: only the current ones;
|
|
21
|
+
// a replaced object is deleted with the write that replaces it), the current
|
|
22
|
+
// root and `chain` tree in `host_settings` (`image`, `image_chain`).
|
|
23
|
+
//
|
|
24
|
+
// Where the headers come from: the host's headers feed (SKEIN_HEADERS_URL,
|
|
25
|
+
// feeds.ts), each one appended as it arrives; and its history, from the same
|
|
26
|
+
// chaintracks service (`/headers?height=&count=`, 80 bytes a header, next to
|
|
27
|
+
// `/tip/stream`): at start the host fills the chain from genesis once, then
|
|
28
|
+
// catches up from a little below its tip (a restart, a gap, a reorg deeper
|
|
29
|
+
// than the last block). A header that links to none the host holds near its
|
|
30
|
+
// tip is taken through the history; with no history service it is dropped,
|
|
31
|
+
// logged. The host checks what it writes as the chain app will: each header
|
|
32
|
+
// links to the one before, its target is usable and its hash meets it. It does
|
|
33
|
+
// not judge the network: the chain app refuses an image whose first header is
|
|
34
|
+
// not its network's genesis.
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
import { CID as Cid } from "multiformats/cid";
|
|
37
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
38
|
+
import { headTree } from "../runtime/heads.js";
|
|
39
|
+
import { hashBlob, hashTree, parseTree, readBlob, readTree } from "../runtime/tree.js";
|
|
40
|
+
import { anyOf, DEFAULT_IMAGE, HOST_IMAGE, imageDir, MemBlocks, overlayImage, WASM_DIR, wasmDirObjects } from "./boot.js";
|
|
41
|
+
/** Headers per block (skein-sdk `image.per_block`). */
|
|
42
|
+
export const PER_BLOCK = 2016;
|
|
43
|
+
const SIZE = 80;
|
|
44
|
+
export const blockName = (first) => String(first).padStart(8, "0");
|
|
45
|
+
const sha256d = (b) => createHash("sha256").update(createHash("sha256").update(b).digest()).digest();
|
|
46
|
+
const display = (h) => Buffer.from(h).reverse().toString("hex");
|
|
47
|
+
/** The target a compact `bits` encodes, or undefined when it is not usable (skein-sdk header.zig `target`). */
|
|
48
|
+
function target(bits) {
|
|
49
|
+
const mantissa = BigInt(bits & 0x007fffff);
|
|
50
|
+
const negative = (bits & 0x00800000) !== 0;
|
|
51
|
+
const exponent = bits >>> 24;
|
|
52
|
+
let t;
|
|
53
|
+
if (exponent <= 3)
|
|
54
|
+
t = mantissa >> BigInt(8 * (3 - exponent));
|
|
55
|
+
else {
|
|
56
|
+
const shift = 8 * (exponent - 3);
|
|
57
|
+
if (shift >= 256)
|
|
58
|
+
return undefined;
|
|
59
|
+
t = mantissa << BigInt(shift);
|
|
60
|
+
if (t >> 256n !== 0n)
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return t === 0n || negative ? undefined : t;
|
|
64
|
+
}
|
|
65
|
+
/** Why a header (80 bytes) is not one the chain app takes on its own: an unusable target, or a hash that misses it. */
|
|
66
|
+
export function headerProblem(raw) {
|
|
67
|
+
if (raw.length !== SIZE)
|
|
68
|
+
return "not 80 bytes";
|
|
69
|
+
const t = target(Buffer.from(raw).readUInt32LE(72));
|
|
70
|
+
if (t === undefined)
|
|
71
|
+
return "an unusable target";
|
|
72
|
+
return BigInt(`0x${display(sha256d(raw))}`) <= t ? undefined : "its hash misses its target";
|
|
73
|
+
}
|
|
74
|
+
const prevOf = (raw) => Buffer.from(raw.subarray(4, 36));
|
|
75
|
+
/** The history endpoint beside a chaintracks tip stream (`…/tip/stream` → `…/headers`), if the URL is one. */
|
|
76
|
+
export function historyOf(feed) {
|
|
77
|
+
if (!feed)
|
|
78
|
+
return undefined;
|
|
79
|
+
const u = new URL(feed);
|
|
80
|
+
if (!/\/tip\/stream\/?$/.test(u.pathname))
|
|
81
|
+
return undefined;
|
|
82
|
+
u.pathname = u.pathname.replace(/\/tip\/stream\/?$/, "/headers");
|
|
83
|
+
u.search = "";
|
|
84
|
+
return u.toString();
|
|
85
|
+
}
|
|
86
|
+
/** The image names (#152): `default`, and `host` (images/host merged over it, #142). */
|
|
87
|
+
export const IMAGE_NAMES = ["default", "host"];
|
|
88
|
+
export class ImageChain {
|
|
89
|
+
o;
|
|
90
|
+
/** chain/headers' blocks, in height order. */
|
|
91
|
+
blocks = [];
|
|
92
|
+
/** The last block's headers (raw). */
|
|
93
|
+
last = Buffer.alloc(0);
|
|
94
|
+
queue = Promise.resolve();
|
|
95
|
+
stat;
|
|
96
|
+
/** #142, #152: the overlay images' static parts (images/host, a directory with no `bin/`), each merged over the default image once. */
|
|
97
|
+
merged = new Map();
|
|
98
|
+
/** Settles once the first sync (start) is done: what a creation waits for. */
|
|
99
|
+
ready = Promise.resolve();
|
|
100
|
+
constructor(o) {
|
|
101
|
+
this.o = o;
|
|
102
|
+
const chain = o.db.setting("image_chain");
|
|
103
|
+
if (chain) {
|
|
104
|
+
const ch = parseTree(this.object(Cid.parse(chain)));
|
|
105
|
+
const hd = ch.find((e) => e.name === "headers");
|
|
106
|
+
if (hd) {
|
|
107
|
+
this.blocks = parseTree(this.object(hd.cid)).map((e) => e.cid);
|
|
108
|
+
if (this.blocks.length)
|
|
109
|
+
this.last = this.blob(this.blocks.at(-1));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
say(line) { this.o.log?.(line); }
|
|
114
|
+
object(cid) {
|
|
115
|
+
const b = this.o.db.imageBlock(cid.toString());
|
|
116
|
+
if (!b)
|
|
117
|
+
throw new Error(`the image's ${cid} is not in host.db`);
|
|
118
|
+
return Buffer.from(b);
|
|
119
|
+
}
|
|
120
|
+
/** A block's headers (the one read last kept: a search below the tip reads one block many times). */
|
|
121
|
+
blob(cid) {
|
|
122
|
+
if (this.read?.cid.equals(cid))
|
|
123
|
+
return this.read.bytes;
|
|
124
|
+
const o = this.object(cid);
|
|
125
|
+
this.read = { cid, bytes: o.subarray(o.indexOf(0) + 1) };
|
|
126
|
+
return this.read.bytes;
|
|
127
|
+
}
|
|
128
|
+
read;
|
|
129
|
+
/** The tip's height (-1: no chain part). */
|
|
130
|
+
height() { return this.blocks.length ? (this.blocks.length - 1) * PER_BLOCK + this.last.length / SIZE - 1 : -1; }
|
|
131
|
+
tip() {
|
|
132
|
+
const h = this.height();
|
|
133
|
+
return h < 0 ? undefined : { height: h, hash: display(sha256d(this.last.subarray(this.last.length - SIZE))) };
|
|
134
|
+
}
|
|
135
|
+
/** The header at a height (raw), from its block. */
|
|
136
|
+
at(height) {
|
|
137
|
+
const b = Math.floor(height / PER_BLOCK);
|
|
138
|
+
const bytes = b === this.blocks.length - 1 ? this.last : this.blob(this.blocks[b]);
|
|
139
|
+
const o = (height - b * PER_BLOCK) * SIZE;
|
|
140
|
+
return bytes.subarray(o, o + SIZE);
|
|
141
|
+
}
|
|
142
|
+
/** One at a time: every change to the chain part goes through here. */
|
|
143
|
+
serial(f) {
|
|
144
|
+
const p = this.queue.then(f, f);
|
|
145
|
+
this.queue = p.catch(() => { });
|
|
146
|
+
return p;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Put `raws` at `height` (at most the tip + 1): the headers held from there
|
|
150
|
+
* that differ are replaced (a reorg), the ones that match are kept, the
|
|
151
|
+
* rest appended; a new tree. Whether anything changed. Refused: a gap, a
|
|
152
|
+
* header that does not link to the one before it, one the chain app would
|
|
153
|
+
* refuse on its own.
|
|
154
|
+
*/
|
|
155
|
+
put(height, raws) { return this.serial(() => this.write(height, raws)); }
|
|
156
|
+
async write(height, raws) {
|
|
157
|
+
const tipH = this.height();
|
|
158
|
+
if (height > tipH + 1)
|
|
159
|
+
throw new Error(`a gap: header ${height} after the tip ${tipH}`);
|
|
160
|
+
let prev = height === 0 ? Buffer.alloc(32) : sha256d(this.at(height - 1));
|
|
161
|
+
for (const [i, raw] of raws.entries()) {
|
|
162
|
+
const why = headerProblem(raw);
|
|
163
|
+
if (why)
|
|
164
|
+
throw new Error(`header ${height + i}: ${why}`);
|
|
165
|
+
if (!prevOf(raw).equals(prev))
|
|
166
|
+
throw new Error(`header ${height + i} does not link to header ${height + i - 1}`);
|
|
167
|
+
prev = sha256d(raw);
|
|
168
|
+
}
|
|
169
|
+
// Skip what is held already.
|
|
170
|
+
let i = 0;
|
|
171
|
+
while (i < raws.length && height + i <= tipH && this.at(height + i).equals(raws[i]))
|
|
172
|
+
i++;
|
|
173
|
+
if (i === raws.length)
|
|
174
|
+
return false;
|
|
175
|
+
const h0 = height + i;
|
|
176
|
+
const b0 = Math.floor(h0 / PER_BLOCK);
|
|
177
|
+
const base = b0 < this.blocks.length ? (b0 === this.blocks.length - 1 ? this.last : this.blob(this.blocks[b0])) : Buffer.alloc(0);
|
|
178
|
+
const rest = Buffer.concat([base.subarray(0, (h0 - b0 * PER_BLOCK) * SIZE), ...raws.slice(i)]);
|
|
179
|
+
const puts = new Map();
|
|
180
|
+
const drops = new Set(this.blocks.slice(b0).map(String));
|
|
181
|
+
const blocks = this.blocks.slice(0, b0);
|
|
182
|
+
for (let o = 0; o < rest.length; o += PER_BLOCK * SIZE) {
|
|
183
|
+
const { cid, object } = hashBlob(rest.subarray(o, o + PER_BLOCK * SIZE));
|
|
184
|
+
blocks.push(cid);
|
|
185
|
+
puts.set(cid.toString(), object);
|
|
186
|
+
}
|
|
187
|
+
const last = rest.subarray(Math.floor((rest.length - 1) / (PER_BLOCK * SIZE)) * PER_BLOCK * SIZE);
|
|
188
|
+
const tipHeight = blocks.length ? (blocks.length - 1) * PER_BLOCK + last.length / SIZE - 1 : -1;
|
|
189
|
+
const tipText = `{"height":${tipHeight},"hash":"${display(sha256d(last.subarray(last.length - SIZE)))}"}\n`;
|
|
190
|
+
const headers = hashTree(blocks.map((cid, k) => ({ mode: "100644", name: blockName(k * PER_BLOCK), cid })));
|
|
191
|
+
const tip = hashBlob(Buffer.from(tipText));
|
|
192
|
+
const chain = hashTree([{ mode: "40000", name: "headers", cid: headers.cid }, { mode: "100644", name: "tip", cid: tip.cid }]);
|
|
193
|
+
const root = await this.rootWith(chain.cid, await this.static());
|
|
194
|
+
for (const x of [headers, tip, chain, root])
|
|
195
|
+
puts.set(x.cid.toString(), x.object);
|
|
196
|
+
// The objects this tree no longer names.
|
|
197
|
+
const old = this.o.db.setting("image_chain");
|
|
198
|
+
if (old) {
|
|
199
|
+
const ch = parseTree(this.object(Cid.parse(old)));
|
|
200
|
+
drops.add(old);
|
|
201
|
+
for (const e of ch)
|
|
202
|
+
drops.add(e.cid.toString());
|
|
203
|
+
}
|
|
204
|
+
const oldRoot = this.o.db.setting("image");
|
|
205
|
+
if (oldRoot)
|
|
206
|
+
drops.add(oldRoot);
|
|
207
|
+
for (const k of puts.keys())
|
|
208
|
+
drops.delete(k);
|
|
209
|
+
this.o.db.writeImage(puts, drops, { image: root.cid.toString(), image_chain: chain.cid.toString() });
|
|
210
|
+
this.blocks = blocks;
|
|
211
|
+
this.last = Buffer.from(last);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
/** The static part: the image directory, scanned once (boot.ts imageDir: the apps it names beside it too). */
|
|
215
|
+
static() {
|
|
216
|
+
this.stat ??= (async () => {
|
|
217
|
+
const d = await imageDir(this.o.dir ?? DEFAULT_IMAGE);
|
|
218
|
+
return { root: d.root, entries: parseTree(await d.objects.bytes(d.root)), objects: d.objects, apps: d.apps };
|
|
219
|
+
})();
|
|
220
|
+
return this.stat;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* #142, #152: an overlay image's static part — its directory (images/host, or one with no `bin/`) merged
|
|
224
|
+
* over the default image (boot.ts overlayImage, mergeImages), once per directory.
|
|
225
|
+
*/
|
|
226
|
+
overlayStatic(dir) {
|
|
227
|
+
let p = this.merged.get(dir);
|
|
228
|
+
if (!p) {
|
|
229
|
+
p = (async () => {
|
|
230
|
+
const m = await overlayImage(await this.static(), dir);
|
|
231
|
+
return { ...m, entries: parseTree((await m.objects.get(m.root))) };
|
|
232
|
+
})();
|
|
233
|
+
this.merged.set(dir, p);
|
|
234
|
+
}
|
|
235
|
+
return p;
|
|
236
|
+
}
|
|
237
|
+
/** The image's root: the static part's entries, `chain` among them. */
|
|
238
|
+
async rootWith(chain, s) {
|
|
239
|
+
return hashTree([...s.entries.filter((e) => e.name !== "chain"), { mode: "40000", name: "chain", cid: chain }]);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The current image as a boot source (#89, #132): the static part with the
|
|
243
|
+
* chain part at `chain/`; with no chain part, the static part alone (the
|
|
244
|
+
* repo's images/default, as before). `host` / `image: "host"`: the host
|
|
245
|
+
* image; `overlay` (#152): a directory with no `bin/`, merged over the
|
|
246
|
+
* default image as the host image is, with the same chain part. Its
|
|
247
|
+
* objects: host.db's, the scan's, the repo's pinned modules.
|
|
248
|
+
*/
|
|
249
|
+
async source(o = {}) {
|
|
250
|
+
if (o.image !== undefined && !IMAGE_NAMES.includes(o.image))
|
|
251
|
+
throw new Error(`image ${JSON.stringify(o.image)}: this host has ${IMAGE_NAMES.join(", ")}`);
|
|
252
|
+
const over = o.overlay ?? (o.host || o.image === "host" ? this.o.hostDir ?? HOST_IMAGE : undefined);
|
|
253
|
+
const named = over !== undefined;
|
|
254
|
+
const s = named ? await this.overlayStatic(over) : await this.static();
|
|
255
|
+
const chainObjects = { get: async (cid) => this.o.db.imageBlock(cid.toString()) };
|
|
256
|
+
const chain = this.o.db.setting("image_chain");
|
|
257
|
+
if (named) {
|
|
258
|
+
// An overlay image (#142 the host image, #152 a directory with no bin/): its root is not kept in host.db; the chain part's objects are the same.
|
|
259
|
+
const root = chain ? await this.rootWith(Cid.parse(chain), s) : undefined;
|
|
260
|
+
const mem = new MemBlocks();
|
|
261
|
+
if (root)
|
|
262
|
+
await mem.putBlock(root.cid, root.object);
|
|
263
|
+
return { kind: "tree", root: root?.cid ?? s.root, objects: anyOf(mem, s.objects, wasmDirObjects(WASM_DIR), chainObjects), apps: s.apps };
|
|
264
|
+
}
|
|
265
|
+
const objects = anyOf(s.objects, wasmDirObjects(WASM_DIR), chainObjects);
|
|
266
|
+
if (!chain)
|
|
267
|
+
return { kind: "tree", root: s.root, objects, apps: s.apps };
|
|
268
|
+
const root = await this.rootWith(Cid.parse(chain), s);
|
|
269
|
+
// The static part changed since the last write (a new checkout): this root is the current one.
|
|
270
|
+
if (this.o.db.setting("image") !== root.cid.toString())
|
|
271
|
+
this.o.db.writeImage(new Map([[root.cid.toString(), root.object]]), new Set([this.o.db.setting("image") ?? ""]), { image: root.cid.toString() });
|
|
272
|
+
return { kind: "tree", root: root.cid, objects, apps: s.apps };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Headers from the stream (feeds.ts, in order): each appended at the tip
|
|
276
|
+
* when it links there; one that links to a header a little below the tip
|
|
277
|
+
* replaces from there (a reorg); otherwise the history fills in (a gap,
|
|
278
|
+
* a deeper reorg), or, with none, it is dropped.
|
|
279
|
+
*/
|
|
280
|
+
headers(raws) {
|
|
281
|
+
return this.serial(async () => {
|
|
282
|
+
for (const raw of raws) {
|
|
283
|
+
try {
|
|
284
|
+
const prev = prevOf(raw);
|
|
285
|
+
const tipH = this.height();
|
|
286
|
+
let at;
|
|
287
|
+
if (tipH < 0)
|
|
288
|
+
at = prev.equals(Buffer.alloc(32)) ? 0 : undefined;
|
|
289
|
+
else
|
|
290
|
+
for (let h = tipH; h >= Math.max(0, tipH - 2 * PER_BLOCK); h--)
|
|
291
|
+
if (sha256d(this.at(h)).equals(prev)) {
|
|
292
|
+
at = h + 1;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
if (at !== undefined) {
|
|
296
|
+
await this.write(at, [raw]);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (!this.o.history) {
|
|
300
|
+
this.say(`header ${display(sha256d(raw))}: links to no header near the tip ${tipH}, and no history to fill in from: dropped`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
await this.syncNow();
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
this.say(`header ${display(sha256d(raw))} not taken: ${e.message}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/** Fill in from the history (#132): from genesis once, then from a little below the tip. */
|
|
312
|
+
sync() { return this.serial(() => this.syncNow()); }
|
|
313
|
+
async syncNow() {
|
|
314
|
+
const url = this.o.history;
|
|
315
|
+
if (!url)
|
|
316
|
+
return;
|
|
317
|
+
const page = this.o.page ?? 50 * PER_BLOCK;
|
|
318
|
+
const t0 = Date.now();
|
|
319
|
+
const start = this.height();
|
|
320
|
+
let from = Math.max(0, start - 6);
|
|
321
|
+
for (let back = 0;;) {
|
|
322
|
+
const res = await (this.o.fetch ?? fetch)(`${url}?height=${from}&count=${page}`);
|
|
323
|
+
if (!res.ok)
|
|
324
|
+
throw new Error(`${url}: HTTP ${res.status}`);
|
|
325
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
326
|
+
if (bytes.length % SIZE)
|
|
327
|
+
throw new Error(`${url}: ${bytes.length} bytes, not whole headers`);
|
|
328
|
+
const raws = [];
|
|
329
|
+
for (let o = 0; o < bytes.length; o += SIZE)
|
|
330
|
+
raws.push(bytes.subarray(o, o + SIZE));
|
|
331
|
+
if (!raws.length)
|
|
332
|
+
break;
|
|
333
|
+
if (from > 0 && from <= this.height() + 1 && !prevOf(raws[0]).equals(sha256d(this.at(from - 1)))) {
|
|
334
|
+
// A reorg below where we asked from: further back.
|
|
335
|
+
if (++back > 8)
|
|
336
|
+
throw new Error(`${url}: header ${from} links to none held, ${back} pages back`);
|
|
337
|
+
from = Math.max(0, from - PER_BLOCK * back);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
await this.write(from, raws);
|
|
341
|
+
from += raws.length;
|
|
342
|
+
}
|
|
343
|
+
const tip = this.tip();
|
|
344
|
+
if (tip && tip.height !== start)
|
|
345
|
+
this.say(`the image's chain: ${start < 0 ? "built from genesis" : `from ${start}`} to ${tip.height} (${tip.hash}) in ${Date.now() - t0} ms`);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* #141: the headers a skein whose tip is `tip` lacks, from the image's chain, in order — what the
|
|
349
|
+
* host pushes it on (re)subscribing, before the live stream. From the header after its tip when
|
|
350
|
+
* the image holds that tip; a tip the image does not hold at its height (a fork) from a little
|
|
351
|
+
* below it; none when the skein is at or past the image's tip, or has no tip (no chain part).
|
|
352
|
+
* Run in turn with the chain part's writes: every header the feed brought before this call is in.
|
|
353
|
+
*/
|
|
354
|
+
after(tip) {
|
|
355
|
+
return this.serial(() => {
|
|
356
|
+
const top = this.height();
|
|
357
|
+
if (!tip || tip.height >= top)
|
|
358
|
+
return [];
|
|
359
|
+
const from = display(sha256d(this.at(tip.height))) === tip.hash ? tip.height + 1 : Math.max(0, tip.height - 6);
|
|
360
|
+
const out = [];
|
|
361
|
+
for (let h = from; h <= top; h++)
|
|
362
|
+
out.push(new Uint8Array(this.at(h)));
|
|
363
|
+
return out;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/** Start: the first sync, which creations wait for (`ready`). */
|
|
367
|
+
start() {
|
|
368
|
+
this.ready = this.sync().catch((e) => this.say(`the image's chain not synced: ${e.message}`));
|
|
369
|
+
return this.ready;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* shruggr/skein#150: the chain app's best-chain header at `height` (the head `chain/state`, map
|
|
374
|
+
* `headers`: the MST height (u32 big-endian) → header): its hash (display hex) and merkle root
|
|
375
|
+
* (display hex); undefined when the chain state holds none there.
|
|
376
|
+
*/
|
|
377
|
+
export async function chainHeader(store, height) {
|
|
378
|
+
const state = await headTree(store, "chain/state");
|
|
379
|
+
if (!state)
|
|
380
|
+
return undefined;
|
|
381
|
+
const v = await store.get(state).catch(() => undefined);
|
|
382
|
+
const key = Buffer.alloc(4);
|
|
383
|
+
key.writeUInt32BE(height);
|
|
384
|
+
let node = v?.kind === "chain-state" ? v.maps?.headers ?? undefined : undefined;
|
|
385
|
+
while (node) {
|
|
386
|
+
const [left, es] = dagCbor.decode(await store.bytes(node));
|
|
387
|
+
let next = left;
|
|
388
|
+
let found;
|
|
389
|
+
for (const [k, val, right] of es) {
|
|
390
|
+
const c = Buffer.compare(Buffer.from(k), key);
|
|
391
|
+
if (c === 0) {
|
|
392
|
+
found = val;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if (c > 0)
|
|
396
|
+
break;
|
|
397
|
+
next = right;
|
|
398
|
+
}
|
|
399
|
+
if (found) {
|
|
400
|
+
const raw = await store.bytes(found);
|
|
401
|
+
return { hash: display(sha256d(raw)), root: Buffer.from(raw.subarray(36, 68)).reverse().toString("hex") };
|
|
402
|
+
}
|
|
403
|
+
node = next ?? undefined;
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* #141: a skein's chain tip as its store holds it: the chain app's state (the head `chain/state`,
|
|
409
|
+
* {kind: "chain-state", maps: {headers}} — the MST height (u32 big-endian) → header, skein-sdk
|
|
410
|
+
* chain.zig; its last key is the tip), else the chain part of the tree it was born from (`main`'s
|
|
411
|
+
* `chain/tip`, image.zig's format), else none.
|
|
412
|
+
*/
|
|
413
|
+
export async function skeinTip(store) {
|
|
414
|
+
const state = await headTree(store, "chain/state");
|
|
415
|
+
if (state) {
|
|
416
|
+
const v = await store.get(state).catch(() => undefined);
|
|
417
|
+
let node = v?.kind === "chain-state" ? v.maps?.headers ?? undefined : undefined;
|
|
418
|
+
let last;
|
|
419
|
+
while (node) {
|
|
420
|
+
const [, es] = dagCbor.decode(await store.bytes(node));
|
|
421
|
+
if (!es.length)
|
|
422
|
+
break;
|
|
423
|
+
last = es[es.length - 1];
|
|
424
|
+
node = last[2] ?? undefined;
|
|
425
|
+
}
|
|
426
|
+
if (last) {
|
|
427
|
+
const raw = await store.bytes(last[1]);
|
|
428
|
+
return { height: Buffer.from(last[0]).readUInt32BE(0), hash: display(sha256d(raw)) };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const main = await headTree(store, "main");
|
|
432
|
+
if (!main)
|
|
433
|
+
return undefined;
|
|
434
|
+
const chain = (await readTree(store, main).catch(() => [])).find((e) => e.name === "chain" && e.mode === "40000");
|
|
435
|
+
const tip = chain && (await readTree(store, chain.cid)).find((e) => e.name === "tip");
|
|
436
|
+
if (!tip)
|
|
437
|
+
return undefined;
|
|
438
|
+
const t = JSON.parse(Buffer.from(await readBlob(store, tip.cid)).toString("utf8"));
|
|
439
|
+
return typeof t.height === "number" && typeof t.hash === "string" ? { height: t.height, hash: t.hash } : undefined;
|
|
440
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
import type { Store } from "../runtime/store.ts";
|
|
3
|
+
import { type AppRecord, type AppTree, type InstanceView } from "./plan.ts";
|
|
4
|
+
export * from "./plan.ts";
|
|
5
|
+
/** What a tree carries that is not the app: build output, dependencies, VCS. */
|
|
6
|
+
export declare function appIgnore(rel: string): boolean;
|
|
7
|
+
/** Read and check the app in `dir`. */
|
|
8
|
+
export declare function readApp(dir: string): Promise<AppTree>;
|
|
9
|
+
/**
|
|
10
|
+
* Where the app is: a directory, or a git repository URL (`<url>` or
|
|
11
|
+
* `<url>#<rev>`) cloned into a temporary directory.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fetchApp(spec: string): string;
|
|
14
|
+
/** The view of an instance's store file (the CLI's, the tests'). */
|
|
15
|
+
export declare function instanceView(store: Store): Promise<InstanceView>;
|
|
16
|
+
/** The root holders (hex) as the head `grants` names them (#143; none: an unclaimed image). */
|
|
17
|
+
export declare function rootOf(store: Store): Promise<string[]>;
|
|
18
|
+
/** The app record a head's root is, if it is one. */
|
|
19
|
+
export declare function appRecordOf(store: Store, name: string): Promise<{
|
|
20
|
+
cid: CID;
|
|
21
|
+
record: AppRecord;
|
|
22
|
+
} | undefined>;
|