@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,121 @@
|
|
|
1
|
+
import { type ChainTracker } from "@bsv/sdk";
|
|
2
|
+
import { CID } from "multiformats/cid";
|
|
3
|
+
import { type EntryMode } from "../runtime/tree.ts";
|
|
4
|
+
import { MemBlocks, type Objects } from "./boot.ts";
|
|
5
|
+
export declare const PACKET_KIND = "skein-packet";
|
|
6
|
+
export declare const BITCOIN_TX = 177;
|
|
7
|
+
export declare const BITCOIN_BLOCK = 176;
|
|
8
|
+
export declare class PacketError extends Error {
|
|
9
|
+
readonly code: "malformed" | "scope-mismatch" | "incomplete" | "hash-mismatch" | "unproven";
|
|
10
|
+
constructor(code: PacketError["code"], message: string);
|
|
11
|
+
}
|
|
12
|
+
export interface IndexEntry {
|
|
13
|
+
tx: CID;
|
|
14
|
+
vout: number;
|
|
15
|
+
cid: CID;
|
|
16
|
+
}
|
|
17
|
+
export interface PacketRecord {
|
|
18
|
+
kind: typeof PACKET_KIND;
|
|
19
|
+
version: 1;
|
|
20
|
+
scope: CID;
|
|
21
|
+
beef: Uint8Array;
|
|
22
|
+
index?: IndexEntry[];
|
|
23
|
+
}
|
|
24
|
+
/** A transaction's CID: bitcoin-tx, dbl-sha2-256, the digest in internal byte order (the txid reversed). */
|
|
25
|
+
export declare function txCid(txidHex: string): CID;
|
|
26
|
+
export declare function txidOf(c: CID): string;
|
|
27
|
+
/** Do `bytes` hash to `cid` (git sha1, sha2-256, dbl-sha2-256)? */
|
|
28
|
+
export declare function hashMatches(cid: CID, bytes: Uint8Array): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The CIDs a block links to: dag-cbor links, git tree entries, and a bitcoin
|
|
31
|
+
* block's typed links (#42, bitcoin.ts): a header its previous header and
|
|
32
|
+
* its merkle root, a merkle node (64-byte bitcoin-tx) its children, a
|
|
33
|
+
* transaction what its inputs spend — all `optional`: the tree and the
|
|
34
|
+
* ancestry are sparse (only the paths to held transactions, only the
|
|
35
|
+
* ancestors held), so a target that is not held is not missing. None for
|
|
36
|
+
* blobs and raw.
|
|
37
|
+
*/
|
|
38
|
+
export declare function linksOf(cid: CID, bytes: Uint8Array): Array<{
|
|
39
|
+
cid: CID;
|
|
40
|
+
tree?: boolean;
|
|
41
|
+
optional?: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* The closure a scope requires: a system tree's objects plus the modules its
|
|
45
|
+
* bin/<name>.cid name; a state record's every link. `get` returns a block,
|
|
46
|
+
* checked against its CID, or undefined (then `missing` lists it).
|
|
47
|
+
*/
|
|
48
|
+
export declare function closure(scope: CID, get: (cid: CID) => Promise<Uint8Array | undefined>): Promise<{
|
|
49
|
+
blocks: Array<{
|
|
50
|
+
cid: CID;
|
|
51
|
+
bytes: Uint8Array;
|
|
52
|
+
}>;
|
|
53
|
+
missing: string[];
|
|
54
|
+
}>;
|
|
55
|
+
/** Pushes of an `OP_FALSE OP_RETURN <type> <payload>` script, or undefined. */
|
|
56
|
+
export declare function carrierOf(script: Uint8Array): {
|
|
57
|
+
type: string;
|
|
58
|
+
payload: Uint8Array;
|
|
59
|
+
} | undefined;
|
|
60
|
+
export declare function carrierScript(type: string, payload: Uint8Array): Uint8Array;
|
|
61
|
+
export declare function encodeDir(entries: Array<{
|
|
62
|
+
name: string;
|
|
63
|
+
mode: EntryMode;
|
|
64
|
+
txid?: string;
|
|
65
|
+
vout: number;
|
|
66
|
+
}>): Uint8Array;
|
|
67
|
+
export interface ReadOptions {
|
|
68
|
+
/** The root the caller means to accept; the packet's scope must be it. */
|
|
69
|
+
scope?: CID;
|
|
70
|
+
/** Require chain inclusion: every carrier transaction's merkle path must verify against these headers. */
|
|
71
|
+
chainTracker?: ChainTracker;
|
|
72
|
+
}
|
|
73
|
+
export interface Verified {
|
|
74
|
+
scope: CID;
|
|
75
|
+
kind: "tree" | "checkpoint";
|
|
76
|
+
/** Every block the scope requires, each checked against its CID. */
|
|
77
|
+
blocks: MemBlocks;
|
|
78
|
+
list: Array<{
|
|
79
|
+
cid: CID;
|
|
80
|
+
bytes: Uint8Array;
|
|
81
|
+
}>;
|
|
82
|
+
/** Transactions objects came out of (and patch bases), by txid. */
|
|
83
|
+
carriers: string[];
|
|
84
|
+
}
|
|
85
|
+
export declare function decodePacket(bytes: Uint8Array): PacketRecord;
|
|
86
|
+
/** Read and verify a packet: the blocks its scope requires, every one checked; else a PacketError. */
|
|
87
|
+
export declare function readPacket(bytes: Uint8Array, o?: ReadOptions): Promise<Verified>;
|
|
88
|
+
export interface WriteOptions {
|
|
89
|
+
/** "ordfs" (default): trees as ordfs/dir manifests and files as bare content, as gib publishes; "git": every object as it is. */
|
|
90
|
+
form?: "ordfs" | "git";
|
|
91
|
+
/** Give every transaction a merkle path (a block at height 1, 2, …) and return the headers' roots. */
|
|
92
|
+
mined?: boolean;
|
|
93
|
+
/** Carrier outputs per transaction, and bytes per transaction, before a new one starts. */
|
|
94
|
+
maxOutputs?: number;
|
|
95
|
+
maxBytes?: number;
|
|
96
|
+
/** Leave the index out (readers then scan). */
|
|
97
|
+
noIndex?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Blocks to carry beyond the scope's closure (a checkpoint's records that no
|
|
100
|
+
* link reaches: a run's args, an emitted envelope's signed part). Listed in
|
|
101
|
+
* the index, so a reader verifies and keeps them; the index is then required.
|
|
102
|
+
*/
|
|
103
|
+
extras?: Array<{
|
|
104
|
+
cid: CID;
|
|
105
|
+
bytes: Uint8Array;
|
|
106
|
+
}>;
|
|
107
|
+
}
|
|
108
|
+
export interface Written {
|
|
109
|
+
bytes: Uint8Array;
|
|
110
|
+
packet: PacketRecord;
|
|
111
|
+
txids: string[];
|
|
112
|
+
roots: Record<number, string>;
|
|
113
|
+
objects: number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A packet of everything `scope` requires, read out of `objects`, carried in
|
|
117
|
+
* synthetic transactions (never broadcast; see the header).
|
|
118
|
+
*/
|
|
119
|
+
export declare function writePacket(scope: CID, objects: Objects, o?: WriteOptions): Promise<Written>;
|
|
120
|
+
/** A ChainTracker over a fixed set of header roots {height: root hex} (offline). */
|
|
121
|
+
export declare function rootsTracker(roots: Record<string | number, string>): ChainTracker;
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
// Bootstrap packets (issue #4, docs/BOOTSTRAP.md): on the pattern of BRC-176,
|
|
2
|
+
// a BEEF bag of the transactions that carry a system tree's objects (or a
|
|
3
|
+
// whole instance's, for a checkpoint) plus a **scope** — the root it is
|
|
4
|
+
// verified against. Reading one is an object lookup into the bag, not a
|
|
5
|
+
// chain reader; where the bag came from is nobody's concern.
|
|
6
|
+
//
|
|
7
|
+
// packet dag-cbor {kind: "skein-packet", version: 1, scope: CID, beef: bytes,
|
|
8
|
+
// index?: [{tx: CID(bitcoin-tx), vout: int, cid: CID}]}
|
|
9
|
+
// scope a git tree (a system tree: boot a new instance from it), or a
|
|
10
|
+
// skein-state record (#30: a checkpoint — restore the instance, its
|
|
11
|
+
// index read, never rebuilt)
|
|
12
|
+
// carrier an output `OP_FALSE OP_RETURN <content-type> <payload>`:
|
|
13
|
+
// ordfs/dir a gib directory manifest: resolves to the git tree of its entries
|
|
14
|
+
// ordfs/patch [0x00][36B base outpoint][vcdiff]: resolves to apply(delta, resolve(base))
|
|
15
|
+
// anything else the payload as it is
|
|
16
|
+
// A resolved payload is an object for a CID if it hashes to it —
|
|
17
|
+
// for a git blob either as the whole object or as the file's bare
|
|
18
|
+
// content (how ORDFS/gib carry files); a bitcoin-tx CID is also
|
|
19
|
+
// served by the bag's own transaction of that txid.
|
|
20
|
+
// index where each object is (optional: without it every carrier output
|
|
21
|
+
// is resolved and keyed by every CID its bytes hash to)
|
|
22
|
+
//
|
|
23
|
+
// Verification is offline. **Completeness**: every object reachable from the
|
|
24
|
+
// scope must come out of the bag — a system tree's trees and blobs (patched
|
|
25
|
+
// files walked to their bases), the modules its bin/<name>.cid files name; a
|
|
26
|
+
// checkpoint's every link (dag-cbor links, tree entries) from the state
|
|
27
|
+
// record down. **Content addressing**: every object is checked against its
|
|
28
|
+
// CID (git sha1, sha2-256, dbl-sha2-256). **Chain inclusion** only when asked
|
|
29
|
+
// (`chainTracker`): every carrier transaction must then have a merkle path
|
|
30
|
+
// in the bag that verifies against the tracker's headers.
|
|
31
|
+
//
|
|
32
|
+
// The writer (`writePacket`) makes the transactions itself: synthetic,
|
|
33
|
+
// unfunded (a made-up input), never broadcast — a test fixture and a file
|
|
34
|
+
// format, not a publisher. A real publisher (gib, 1sat inscriptions or B
|
|
35
|
+
// outputs, funded and broadcast) is out of scope; its outputs would be read
|
|
36
|
+
// the same way once their carrier shape is mapped onto (content-type, payload).
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
38
|
+
import { Beef, MerklePath, Script, Transaction } from "@bsv/sdk";
|
|
39
|
+
import { CID } from "multiformats/cid";
|
|
40
|
+
import * as Digest from "multiformats/hashes/digest";
|
|
41
|
+
import { decode, encode } from "../runtime/cid.js";
|
|
42
|
+
import { GIT_RAW, hashTree, parseTree, sha1Cid } from "../runtime/tree.js";
|
|
43
|
+
import { BIN, DAG_CBOR, gitBody, MemBlocks, RAW } from "./boot.js";
|
|
44
|
+
import { vcdiffDecode } from "./vcdiff.js";
|
|
45
|
+
import { bitcoinLinks, isBitcoin } from "../runtime/bitcoin.js";
|
|
46
|
+
export const PACKET_KIND = "skein-packet";
|
|
47
|
+
export const BITCOIN_TX = 0xb1;
|
|
48
|
+
export const BITCOIN_BLOCK = 0xb0;
|
|
49
|
+
const SHA1 = 0x11, SHA2_256 = 0x12, DBL_SHA2_256 = 0x56;
|
|
50
|
+
const OP_FALSE = 0x00, OP_RETURN = 0x6a;
|
|
51
|
+
export class PacketError extends Error {
|
|
52
|
+
code;
|
|
53
|
+
constructor(code, message) { super(`packet ${code}: ${message}`); this.code = code; }
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------- ids
|
|
56
|
+
const sha1 = (b) => createHash("sha1").update(b).digest();
|
|
57
|
+
const sha256 = (b) => createHash("sha256").update(b).digest();
|
|
58
|
+
const blobObject = (content) => Buffer.concat([Buffer.from(`blob ${content.length}\0`, "latin1"), content]);
|
|
59
|
+
/** A transaction's CID: bitcoin-tx, dbl-sha2-256, the digest in internal byte order (the txid reversed). */
|
|
60
|
+
export function txCid(txidHex) {
|
|
61
|
+
return CID.createV1(BITCOIN_TX, Digest.create(DBL_SHA2_256, Buffer.from(txidHex, "hex").reverse()));
|
|
62
|
+
}
|
|
63
|
+
export function txidOf(c) {
|
|
64
|
+
if (c.code !== BITCOIN_TX || c.multihash.code !== DBL_SHA2_256)
|
|
65
|
+
throw new PacketError("malformed", `${c} is not a bitcoin-tx CID`);
|
|
66
|
+
return Buffer.from(c.multihash.digest).reverse().toString("hex");
|
|
67
|
+
}
|
|
68
|
+
/** Do `bytes` hash to `cid` (git sha1, sha2-256, dbl-sha2-256)? */
|
|
69
|
+
export function hashMatches(cid, bytes) {
|
|
70
|
+
const d = Buffer.from(cid.multihash.digest);
|
|
71
|
+
switch (cid.multihash.code) {
|
|
72
|
+
case SHA1: return cid.code === GIT_RAW && d.equals(sha1(bytes));
|
|
73
|
+
case SHA2_256: return (cid.code === RAW || cid.code === DAG_CBOR) && d.equals(sha256(bytes));
|
|
74
|
+
case DBL_SHA2_256: return (cid.code === BITCOIN_TX || (cid.code === BITCOIN_BLOCK && bytes.length === 80)) && d.equals(sha256(sha256(bytes)));
|
|
75
|
+
default: return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The block `payload` is for `cid`, if it is one: as it is, or (a git blob) framed as a blob object. */
|
|
79
|
+
function asBlock(cid, payload) {
|
|
80
|
+
if (hashMatches(cid, payload))
|
|
81
|
+
return payload;
|
|
82
|
+
if (cid.code === GIT_RAW && cid.multihash.code === SHA1) {
|
|
83
|
+
const framed = blobObject(payload);
|
|
84
|
+
if (Buffer.from(cid.multihash.digest).equals(sha1(framed)))
|
|
85
|
+
return new Uint8Array(framed);
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------- links (completeness)
|
|
90
|
+
/**
|
|
91
|
+
* The CIDs a block links to: dag-cbor links, git tree entries, and a bitcoin
|
|
92
|
+
* block's typed links (#42, bitcoin.ts): a header its previous header and
|
|
93
|
+
* its merkle root, a merkle node (64-byte bitcoin-tx) its children, a
|
|
94
|
+
* transaction what its inputs spend — all `optional`: the tree and the
|
|
95
|
+
* ancestry are sparse (only the paths to held transactions, only the
|
|
96
|
+
* ancestors held), so a target that is not held is not missing. None for
|
|
97
|
+
* blobs and raw.
|
|
98
|
+
*/
|
|
99
|
+
export function linksOf(cid, bytes) {
|
|
100
|
+
if (isBitcoin(cid))
|
|
101
|
+
return bitcoinLinks(cid, bytes).map((l) => ({ cid: l.to, optional: true }));
|
|
102
|
+
if (cid.code === DAG_CBOR) {
|
|
103
|
+
const out = [];
|
|
104
|
+
const walk = (v) => {
|
|
105
|
+
const c = CID.asCID(v);
|
|
106
|
+
if (c) {
|
|
107
|
+
out.push({ cid: c });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (v instanceof Uint8Array || v === null || typeof v !== "object")
|
|
111
|
+
return;
|
|
112
|
+
for (const x of Array.isArray(v) ? v : Object.values(v))
|
|
113
|
+
walk(x);
|
|
114
|
+
};
|
|
115
|
+
walk(decode(bytes));
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
if (cid.code === GIT_RAW && gitBody(bytes, "tree"))
|
|
119
|
+
return parseTree(bytes, cid).map((e) => ({ cid: e.cid }));
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The closure a scope requires: a system tree's objects plus the modules its
|
|
124
|
+
* bin/<name>.cid name; a state record's every link. `get` returns a block,
|
|
125
|
+
* checked against its CID, or undefined (then `missing` lists it).
|
|
126
|
+
*/
|
|
127
|
+
export async function closure(scope, get) {
|
|
128
|
+
const blocks = [];
|
|
129
|
+
const missing = [];
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const visit = async (cid, path, optional = false) => {
|
|
132
|
+
const k = cid.toString();
|
|
133
|
+
if (seen.has(k))
|
|
134
|
+
return undefined;
|
|
135
|
+
seen.add(k);
|
|
136
|
+
const bytes = await get(cid);
|
|
137
|
+
if (!bytes) {
|
|
138
|
+
if (!optional)
|
|
139
|
+
missing.push(`${path || "/"} (${k})`);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
blocks.push({ cid, bytes });
|
|
143
|
+
for (const l of linksOf(cid, bytes))
|
|
144
|
+
await visit(l.cid, cid.code === GIT_RAW ? `${path}/${nameIn(bytes, cid, l.cid)}` : `${path}→${short(l.cid)}`, l.optional);
|
|
145
|
+
return bytes;
|
|
146
|
+
};
|
|
147
|
+
const root = await visit(scope, "");
|
|
148
|
+
if (root && scope.code === GIT_RAW) {
|
|
149
|
+
// A system tree's bin/<name>.cid: the module it names is part of the tree's closure.
|
|
150
|
+
const bin = parseTree(root, scope).find((e) => e.name === BIN && e.mode === "40000");
|
|
151
|
+
const binBytes = bin && blocks.find((b) => b.cid.equals(bin.cid))?.bytes;
|
|
152
|
+
if (bin && binBytes) {
|
|
153
|
+
for (const e of parseTree(binBytes, bin.cid)) {
|
|
154
|
+
if (!e.name.endsWith(".cid"))
|
|
155
|
+
continue;
|
|
156
|
+
const f = blocks.find((b) => b.cid.equals(e.cid))?.bytes;
|
|
157
|
+
const text = f && gitBody(f, "blob");
|
|
158
|
+
if (!text)
|
|
159
|
+
continue;
|
|
160
|
+
let m;
|
|
161
|
+
try {
|
|
162
|
+
m = CID.parse(Buffer.from(text).toString("utf8").trim());
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
} // the loader refuses it with a reason
|
|
167
|
+
await visit(m, `/${BIN}/${e.name} → module`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { blocks, missing };
|
|
172
|
+
}
|
|
173
|
+
const short = (c) => c.toString().slice(-8);
|
|
174
|
+
function nameIn(tree, cid, child) {
|
|
175
|
+
return parseTree(tree, cid).find((e) => e.cid.equals(child))?.name ?? short(child);
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------- carriers
|
|
178
|
+
/** Pushes of an `OP_FALSE OP_RETURN <type> <payload>` script, or undefined. */
|
|
179
|
+
export function carrierOf(script) {
|
|
180
|
+
if (script.length < 2 || script[0] !== OP_FALSE || script[1] !== OP_RETURN)
|
|
181
|
+
return undefined;
|
|
182
|
+
const pushes = [];
|
|
183
|
+
let i = 2;
|
|
184
|
+
while (i < script.length) {
|
|
185
|
+
const op = script[i++];
|
|
186
|
+
let n;
|
|
187
|
+
if (op >= 1 && op <= 75)
|
|
188
|
+
n = op;
|
|
189
|
+
else if (op === 0x4c)
|
|
190
|
+
n = script[i++];
|
|
191
|
+
else if (op === 0x4d) {
|
|
192
|
+
n = script[i] | (script[i + 1] << 8);
|
|
193
|
+
i += 2;
|
|
194
|
+
}
|
|
195
|
+
else if (op === 0x4e) {
|
|
196
|
+
n = (script[i] | (script[i + 1] << 8) | (script[i + 2] << 16) | (script[i + 3] << 24)) >>> 0;
|
|
197
|
+
i += 4;
|
|
198
|
+
}
|
|
199
|
+
else if (op === 0)
|
|
200
|
+
n = 0;
|
|
201
|
+
else
|
|
202
|
+
return undefined;
|
|
203
|
+
if (i + n > script.length)
|
|
204
|
+
return undefined;
|
|
205
|
+
pushes.push(script.subarray(i, i + n));
|
|
206
|
+
i += n;
|
|
207
|
+
}
|
|
208
|
+
if (pushes.length !== 2)
|
|
209
|
+
return undefined;
|
|
210
|
+
return { type: Buffer.from(pushes[0]).toString("utf8"), payload: pushes[1] };
|
|
211
|
+
}
|
|
212
|
+
function push(b) {
|
|
213
|
+
const n = b.length;
|
|
214
|
+
const head = n <= 75 ? [n] : n <= 0xff ? [0x4c, n] : n <= 0xffff ? [0x4d, n & 0xff, n >> 8] : [0x4e, n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff];
|
|
215
|
+
return Buffer.concat([Buffer.from(head), b]);
|
|
216
|
+
}
|
|
217
|
+
export function carrierScript(type, payload) {
|
|
218
|
+
return new Uint8Array(Buffer.concat([Buffer.from([OP_FALSE, OP_RETURN]), push(Buffer.from(type, "utf8")), push(payload)]));
|
|
219
|
+
}
|
|
220
|
+
/** An outpoint as ORDFS writes it: 32-byte txid (internal order) + 4-byte LE vout. */
|
|
221
|
+
const outpointBytes = (txid, vout) => { const b = Buffer.alloc(36); Buffer.from(txid, "hex").reverse().copy(b); b.writeUInt32LE(vout, 32); return b; };
|
|
222
|
+
const outpointOf = (b) => ({ txid: Buffer.from(b.subarray(0, 32)).reverse().toString("hex"), vout: Buffer.from(b).readUInt32LE(32) });
|
|
223
|
+
// ordfs/dir (ordfs-formats.html): [1B version=1][2B count][entries: flags, name len, name, target]
|
|
224
|
+
const KIND_DIR = 1, EXEC = 2, SYMLINK = 4, REFTYPE = 8;
|
|
225
|
+
export function encodeDir(entries) {
|
|
226
|
+
const sorted = [...entries].sort((a, b) => Buffer.compare(Buffer.from(a.name), Buffer.from(b.name)));
|
|
227
|
+
const parts = [Buffer.from([1, sorted.length >> 8, sorted.length & 0xff])];
|
|
228
|
+
for (const e of sorted) {
|
|
229
|
+
const flags = (e.mode === "40000" ? KIND_DIR : 0) | (e.mode === "100755" ? EXEC : 0) | (e.mode === "120000" ? SYMLINK : 0) | (e.txid ? REFTYPE : 0);
|
|
230
|
+
const name = Buffer.from(e.name, "utf8");
|
|
231
|
+
parts.push(Buffer.from([flags, name.length]), name, e.txid ? outpointBytes(e.txid, e.vout) : Buffer.from([e.vout]));
|
|
232
|
+
}
|
|
233
|
+
return new Uint8Array(Buffer.concat(parts));
|
|
234
|
+
}
|
|
235
|
+
function decodeDir(b) {
|
|
236
|
+
if (b[0] !== 1 || b.length < 3)
|
|
237
|
+
throw new PacketError("malformed", "ordfs/dir: bad version");
|
|
238
|
+
const n = (b[1] << 8) | b[2];
|
|
239
|
+
const out = [];
|
|
240
|
+
let i = 3, last;
|
|
241
|
+
for (let k = 0; k < n; k++) {
|
|
242
|
+
const flags = b[i++];
|
|
243
|
+
if (flags & 0xf0)
|
|
244
|
+
throw new PacketError("malformed", "ordfs/dir: reserved flag bits");
|
|
245
|
+
const len = b[i++];
|
|
246
|
+
const name = Buffer.from(b.subarray(i, i + len));
|
|
247
|
+
i += len;
|
|
248
|
+
if (!len || name.includes(0) || name.includes(0x2f) || (last && Buffer.compare(last, name) >= 0))
|
|
249
|
+
throw new PacketError("malformed", "ordfs/dir: bad or unsorted name");
|
|
250
|
+
last = name;
|
|
251
|
+
const mode = flags & KIND_DIR ? "40000" : flags & SYMLINK ? "120000" : flags & EXEC ? "100755" : "100644";
|
|
252
|
+
if (flags & REFTYPE) {
|
|
253
|
+
const op = outpointOf(b.subarray(i, i + 36));
|
|
254
|
+
i += 36;
|
|
255
|
+
out.push({ name: name.toString("utf8"), mode, ...op });
|
|
256
|
+
}
|
|
257
|
+
else
|
|
258
|
+
out.push({ name: name.toString("utf8"), mode, vout: b[i++] });
|
|
259
|
+
if (i > b.length)
|
|
260
|
+
throw new PacketError("malformed", "ordfs/dir: truncated");
|
|
261
|
+
}
|
|
262
|
+
if (i !== b.length)
|
|
263
|
+
throw new PacketError("malformed", "ordfs/dir: trailing bytes");
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
export function decodePacket(bytes) {
|
|
267
|
+
let p;
|
|
268
|
+
try {
|
|
269
|
+
p = decode(bytes);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
throw new PacketError("malformed", "not dag-cbor");
|
|
273
|
+
}
|
|
274
|
+
if (!p || p.kind !== PACKET_KIND || p.version !== 1)
|
|
275
|
+
throw new PacketError("malformed", `not a ${PACKET_KIND} v1`);
|
|
276
|
+
if (!CID.asCID(p.scope))
|
|
277
|
+
throw new PacketError("malformed", "no scope");
|
|
278
|
+
if (!(p.beef instanceof Uint8Array))
|
|
279
|
+
throw new PacketError("malformed", "no beef");
|
|
280
|
+
if (p.index !== undefined && (!Array.isArray(p.index) || p.index.some((e) => !CID.asCID(e?.tx) || !Number.isInteger(e?.vout) || !CID.asCID(e?.cid))))
|
|
281
|
+
throw new PacketError("malformed", "bad index");
|
|
282
|
+
return p;
|
|
283
|
+
}
|
|
284
|
+
/** Read and verify a packet: the blocks its scope requires, every one checked; else a PacketError. */
|
|
285
|
+
export async function readPacket(bytes, o = {}) {
|
|
286
|
+
const p = decodePacket(bytes);
|
|
287
|
+
const scope = CID.asCID(p.scope);
|
|
288
|
+
if (o.scope && !o.scope.equals(scope))
|
|
289
|
+
throw new PacketError("scope-mismatch", `the packet's scope is ${scope}, not ${o.scope}`);
|
|
290
|
+
if (scope.code !== GIT_RAW && scope.code !== DAG_CBOR)
|
|
291
|
+
throw new PacketError("malformed", "the scope is neither a git tree nor a state record");
|
|
292
|
+
let beef;
|
|
293
|
+
try {
|
|
294
|
+
beef = Beef.fromBinary(Array.from(p.beef));
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
throw new PacketError("malformed", `beef: ${e.message}`);
|
|
298
|
+
}
|
|
299
|
+
const txs = new Map();
|
|
300
|
+
for (const b of beef.txs) {
|
|
301
|
+
if (!b.tx)
|
|
302
|
+
continue; // txid-only (BRC-96): not a body
|
|
303
|
+
if (b.tx.id("hex") !== b.txid)
|
|
304
|
+
throw new PacketError("hash-mismatch", `beef: ${b.txid} does not match its body`);
|
|
305
|
+
txs.set(b.txid, b.tx);
|
|
306
|
+
}
|
|
307
|
+
const used = new Set();
|
|
308
|
+
const output = (txid, vout) => {
|
|
309
|
+
const tx = txs.get(txid);
|
|
310
|
+
if (!tx)
|
|
311
|
+
return undefined;
|
|
312
|
+
const out = tx.outputs[vout];
|
|
313
|
+
if (!out)
|
|
314
|
+
return undefined;
|
|
315
|
+
used.add(txid);
|
|
316
|
+
return carrierOf(Uint8Array.from(out.lockingScript.toBinary()));
|
|
317
|
+
};
|
|
318
|
+
// Resolution: an outpoint's bytes (patches applied), and the git objects a directory manifest makes.
|
|
319
|
+
const derived = new Map();
|
|
320
|
+
const resolved = new Map();
|
|
321
|
+
const resolving = new Set();
|
|
322
|
+
const resolve = (txid, vout, depth = 0) => {
|
|
323
|
+
const key = `${txid}_${vout}`;
|
|
324
|
+
if (resolved.has(key))
|
|
325
|
+
return resolved.get(key);
|
|
326
|
+
if (resolving.has(key) || depth > 1000)
|
|
327
|
+
throw new PacketError("malformed", `a patch or directory cycle at ${key}`);
|
|
328
|
+
resolving.add(key);
|
|
329
|
+
try {
|
|
330
|
+
const c = output(txid, vout);
|
|
331
|
+
if (!c)
|
|
332
|
+
return undefined;
|
|
333
|
+
let bytes;
|
|
334
|
+
if (c.type === "ordfs/patch") {
|
|
335
|
+
if (c.payload.length < 37 || c.payload[0] !== 0)
|
|
336
|
+
throw new PacketError("malformed", `ordfs/patch at ${key}`);
|
|
337
|
+
const base = outpointOf(c.payload.subarray(1, 37));
|
|
338
|
+
const b = resolve(base.txid, base.vout, depth + 1);
|
|
339
|
+
if (!b)
|
|
340
|
+
return undefined; // the base is missing: incomplete
|
|
341
|
+
bytes = vcdiffDecode(c.payload.subarray(37), b);
|
|
342
|
+
}
|
|
343
|
+
else if (c.type === "ordfs/dir") {
|
|
344
|
+
const entries = [];
|
|
345
|
+
for (const e of decodeDir(c.payload)) {
|
|
346
|
+
const at = e.txid ?? txid;
|
|
347
|
+
const child = resolve(at, e.vout, depth + 1);
|
|
348
|
+
if (!child)
|
|
349
|
+
return undefined;
|
|
350
|
+
if (e.mode === "40000")
|
|
351
|
+
entries.push({ mode: e.mode, name: e.name, cid: sha1Cid(sha1(child)) });
|
|
352
|
+
else {
|
|
353
|
+
// A file: ORDFS carries its bare content; the git blob is that content framed.
|
|
354
|
+
const obj = new Uint8Array(blobObject(child));
|
|
355
|
+
const cid = sha1Cid(sha1(obj));
|
|
356
|
+
derived.set(cid.toString(), obj);
|
|
357
|
+
entries.push({ mode: e.mode, name: e.name, cid });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const t = hashTree(entries);
|
|
361
|
+
derived.set(t.cid.toString(), t.object);
|
|
362
|
+
bytes = t.object;
|
|
363
|
+
}
|
|
364
|
+
else
|
|
365
|
+
bytes = c.payload;
|
|
366
|
+
resolved.set(key, bytes);
|
|
367
|
+
return bytes;
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
resolving.delete(key);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
// Where each CID is: the index, else every carrier output keyed by what its bytes hash to.
|
|
374
|
+
const where = new Map();
|
|
375
|
+
const at = (cid, txid, vout) => { const k = cid.toString(); where.set(k, [...(where.get(k) ?? []), { txid, vout }]); };
|
|
376
|
+
if (p.index)
|
|
377
|
+
for (const e of p.index)
|
|
378
|
+
at(CID.asCID(e.cid), txidOf(CID.asCID(e.tx)), e.vout);
|
|
379
|
+
else {
|
|
380
|
+
for (const [txid, tx] of txs)
|
|
381
|
+
tx.outputs.forEach((_, vout) => {
|
|
382
|
+
const b = resolve(txid, vout);
|
|
383
|
+
if (!b)
|
|
384
|
+
return;
|
|
385
|
+
const h1 = sha1(b), h2 = sha256(b);
|
|
386
|
+
at(sha1Cid(h1), txid, vout);
|
|
387
|
+
at(sha1Cid(sha1(blobObject(b))), txid, vout);
|
|
388
|
+
at(CID.createV1(RAW, Digest.create(SHA2_256, h2)), txid, vout);
|
|
389
|
+
at(CID.createV1(DAG_CBOR, Digest.create(SHA2_256, h2)), txid, vout);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const get = async (cid) => {
|
|
393
|
+
const d = derived.get(cid.toString());
|
|
394
|
+
if (d)
|
|
395
|
+
return d;
|
|
396
|
+
if (cid.code === BITCOIN_TX) {
|
|
397
|
+
const tx = txs.get(txidOf(cid));
|
|
398
|
+
if (tx) {
|
|
399
|
+
used.add(txidOf(cid));
|
|
400
|
+
return Uint8Array.from(tx.toBinary());
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const places = where.get(cid.toString());
|
|
404
|
+
if (!places)
|
|
405
|
+
return undefined;
|
|
406
|
+
for (const { txid, vout } of places) {
|
|
407
|
+
const b = resolve(txid, vout);
|
|
408
|
+
if (!b)
|
|
409
|
+
continue;
|
|
410
|
+
const block = asBlock(cid, b);
|
|
411
|
+
if (block)
|
|
412
|
+
return block;
|
|
413
|
+
if (p.index)
|
|
414
|
+
throw new PacketError("hash-mismatch", `${txid}:${vout} does not hash to ${cid}`);
|
|
415
|
+
}
|
|
416
|
+
return undefined;
|
|
417
|
+
};
|
|
418
|
+
const { blocks, missing } = await closure(scope, get);
|
|
419
|
+
if (missing.length)
|
|
420
|
+
throw new PacketError("incomplete", `${missing.length} object(s) the scope reaches are not in the bag: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? " …" : ""}`);
|
|
421
|
+
if (scope.code === DAG_CBOR) {
|
|
422
|
+
const s = decode(blocks[0].bytes);
|
|
423
|
+
if (s?.kind !== "skein-state")
|
|
424
|
+
throw new PacketError("malformed", "a dag-cbor scope must be a skein-state record (a checkpoint)");
|
|
425
|
+
}
|
|
426
|
+
// Extras: what the index lists beyond the closure (a checkpoint's records no link reaches) — each verified, then kept.
|
|
427
|
+
const inClosure = new Set(blocks.map((b) => b.cid.toString()));
|
|
428
|
+
for (const e of p.index ?? []) {
|
|
429
|
+
const c = CID.asCID(e.cid);
|
|
430
|
+
if (inClosure.has(c.toString()))
|
|
431
|
+
continue;
|
|
432
|
+
inClosure.add(c.toString());
|
|
433
|
+
const b = await get(c);
|
|
434
|
+
if (!b)
|
|
435
|
+
throw new PacketError("incomplete", `the index lists ${c}, which the bag does not carry`);
|
|
436
|
+
blocks.push({ cid: c, bytes: b });
|
|
437
|
+
}
|
|
438
|
+
if (o.chainTracker) {
|
|
439
|
+
for (const txid of used) {
|
|
440
|
+
const bt = beef.findTxid(txid);
|
|
441
|
+
const mp = bt?.bumpIndex !== undefined ? beef.bumps[bt.bumpIndex] : undefined;
|
|
442
|
+
if (!mp)
|
|
443
|
+
throw new PacketError("unproven", `${txid} has no merkle path in the bag`);
|
|
444
|
+
if (!(await mp.verify(txid, o.chainTracker)))
|
|
445
|
+
throw new PacketError("unproven", `${txid}'s merkle path does not verify against the headers`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const mem = new MemBlocks();
|
|
449
|
+
for (const b of blocks)
|
|
450
|
+
await mem.putBlock(b.cid, b.bytes);
|
|
451
|
+
return { scope, kind: scope.code === GIT_RAW ? "tree" : "checkpoint", blocks: mem, list: blocks, carriers: [...used] };
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* A packet of everything `scope` requires, read out of `objects`, carried in
|
|
455
|
+
* synthetic transactions (never broadcast; see the header).
|
|
456
|
+
*/
|
|
457
|
+
export async function writePacket(scope, objects, o = {}) {
|
|
458
|
+
const { blocks, missing } = await closure(scope, async (c) => {
|
|
459
|
+
const b = await objects.get(c);
|
|
460
|
+
if (b && !hashMatches(c, b))
|
|
461
|
+
throw new PacketError("hash-mismatch", `the source's ${c} does not hash to it`);
|
|
462
|
+
return b;
|
|
463
|
+
});
|
|
464
|
+
if (missing.length)
|
|
465
|
+
throw new PacketError("incomplete", `the source lacks ${missing.length} object(s): ${missing.slice(0, 5).join(", ")}`);
|
|
466
|
+
if (o.extras?.length && o.noIndex)
|
|
467
|
+
throw new PacketError("malformed", "extras need the index");
|
|
468
|
+
const have = new Set(blocks.map((b) => b.cid.toString()));
|
|
469
|
+
for (const x of o.extras ?? []) {
|
|
470
|
+
if (have.has(x.cid.toString()))
|
|
471
|
+
continue;
|
|
472
|
+
if (!hashMatches(x.cid, x.bytes))
|
|
473
|
+
throw new PacketError("hash-mismatch", `extra ${x.cid} does not hash to it`);
|
|
474
|
+
have.add(x.cid.toString());
|
|
475
|
+
blocks.push(x);
|
|
476
|
+
}
|
|
477
|
+
const form = o.form ?? "ordfs";
|
|
478
|
+
const maxOutputs = Math.min(o.maxOutputs ?? 200, 256), maxBytes = o.maxBytes ?? 1 << 20;
|
|
479
|
+
const byCid = new Map(blocks.map((b) => [b.cid.toString(), b]));
|
|
480
|
+
const done = [];
|
|
481
|
+
let cur, curBytes = 0;
|
|
482
|
+
const placed = new Map();
|
|
483
|
+
const index = [];
|
|
484
|
+
const close = () => { if (cur)
|
|
485
|
+
done.push(cur); cur = undefined; curBytes = 0; };
|
|
486
|
+
const open = () => {
|
|
487
|
+
const t = new Transaction();
|
|
488
|
+
// A made-up, unfunded input: the transaction only carries data (and is never broadcast).
|
|
489
|
+
t.addInput({ sourceTXID: sha256(Buffer.from(`skein-packet ${scope} ${done.length}`)).toString("hex"), sourceOutputIndex: 0, unlockingScript: new Script(), sequence: 0xffffffff });
|
|
490
|
+
cur = t;
|
|
491
|
+
return t;
|
|
492
|
+
};
|
|
493
|
+
const txidOfTx = (t) => t.id("hex");
|
|
494
|
+
const room = (n) => { if (cur && (cur.outputs.length >= maxOutputs || curBytes + n > maxBytes))
|
|
495
|
+
close(); return cur ?? open(); };
|
|
496
|
+
const add = (cid, type, payload) => {
|
|
497
|
+
const est = payload().length;
|
|
498
|
+
const t = room(est);
|
|
499
|
+
const data = payload(); // after `room`: a directory's same-tx references depend on which transaction it lands in
|
|
500
|
+
t.addOutput({ lockingScript: Script.fromBinary(Array.from(carrierScript(type, data))), satoshis: 0 });
|
|
501
|
+
curBytes += data.length;
|
|
502
|
+
placed.set(cid.toString(), { tx: t, vout: t.outputs.length - 1 });
|
|
503
|
+
index.push({ tx: t, vout: t.outputs.length - 1, cid });
|
|
504
|
+
};
|
|
505
|
+
// Post-order over trees (children first), then everything else.
|
|
506
|
+
const emitted = new Set();
|
|
507
|
+
const emit = (cid) => {
|
|
508
|
+
const k = cid.toString();
|
|
509
|
+
if (emitted.has(k))
|
|
510
|
+
return;
|
|
511
|
+
emitted.add(k);
|
|
512
|
+
const b = byCid.get(k);
|
|
513
|
+
if (!b)
|
|
514
|
+
return;
|
|
515
|
+
const tree = cid.code === GIT_RAW && gitBody(b.bytes, "tree") ? parseTree(b.bytes, cid) : undefined;
|
|
516
|
+
if (tree) {
|
|
517
|
+
for (const e of tree)
|
|
518
|
+
emit(e.cid);
|
|
519
|
+
if (form === "ordfs" && tree.every((e) => e.mode !== "160000" && placed.has(e.cid.toString()))) {
|
|
520
|
+
add(cid, "ordfs/dir", () => encodeDir(tree.map((e) => {
|
|
521
|
+
const at = placed.get(e.cid.toString());
|
|
522
|
+
return { name: e.name, mode: e.mode, vout: at.vout, ...(at.tx === cur ? {} : { txid: txidOfTx(at.tx) }) };
|
|
523
|
+
})));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
add(cid, "application/octet-stream", () => b.bytes);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const blob = cid.code === GIT_RAW ? gitBody(b.bytes, "blob") : undefined;
|
|
530
|
+
if (blob && form === "ordfs")
|
|
531
|
+
add(cid, "application/octet-stream", () => blob);
|
|
532
|
+
else
|
|
533
|
+
add(cid, "application/octet-stream", () => b.bytes);
|
|
534
|
+
};
|
|
535
|
+
for (const b of blocks)
|
|
536
|
+
emit(b.cid);
|
|
537
|
+
close();
|
|
538
|
+
const roots = {};
|
|
539
|
+
const beef = new Beef();
|
|
540
|
+
done.forEach((t, i) => {
|
|
541
|
+
if (o.mined) {
|
|
542
|
+
const txid = txidOfTx(t);
|
|
543
|
+
// A two-transaction block: a made-up coinbase at offset 0 (offset 0 is a coinbase to verify()), this one at 1.
|
|
544
|
+
const coinbase = sha256(Buffer.from(`skein-packet coinbase ${i + 1}`)).toString("hex");
|
|
545
|
+
t.merklePath = new MerklePath(i + 1, [[{ offset: 0, hash: coinbase }, { offset: 1, hash: txid, txid: true }]]);
|
|
546
|
+
roots[i + 1] = t.merklePath.computeRoot(txid);
|
|
547
|
+
}
|
|
548
|
+
beef.mergeTransaction(t);
|
|
549
|
+
});
|
|
550
|
+
const packet = {
|
|
551
|
+
kind: PACKET_KIND, version: 1, scope, beef: Uint8Array.from(beef.toBinary()),
|
|
552
|
+
...(o.noIndex ? {} : { index: index.map((x) => ({ tx: txCid(txidOfTx(x.tx)), vout: x.vout, cid: x.cid })) }),
|
|
553
|
+
};
|
|
554
|
+
return { bytes: encode(packet).bytes, packet, txids: done.map(txidOfTx), roots, objects: blocks.length };
|
|
555
|
+
}
|
|
556
|
+
/** A ChainTracker over a fixed set of header roots {height: root hex} (offline). */
|
|
557
|
+
export function rootsTracker(roots) {
|
|
558
|
+
return {
|
|
559
|
+
isValidRootForHeight: async (root, height) => roots[height] === root,
|
|
560
|
+
currentHeight: async () => Math.max(0, ...Object.keys(roots).map(Number)),
|
|
561
|
+
};
|
|
562
|
+
}
|