@hazae41/bobine 0.0.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/LICENSE.md ADDED
@@ -0,0 +1,19 @@
1
+ # MIT
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # Bobine
2
+
3
+ Your neighbor's chain of compute
4
+
5
+ ## HTTP API
6
+
7
+ ### POST /api/create
8
+
9
+ Creates a new module using some WebAssembly code and some unique salt
10
+
11
+ Accepts a form data with the following fields
12
+
13
+ - `code` as bytes = your WebAssembly code (your .wasm file)
14
+
15
+ - `salt` as bytes = some bytes (any length, can be 0) that will be hashed with your code to produce your module address
16
+
17
+ - `effort` as bytes = some unique 32 bytes whose sha256 `hash` validates `((2n ** 256n) / BigInt("0x" + hash.toHex())) > (code.length + salt.length)`
18
+
19
+ ### POST /api/execute
20
+
21
+ Execute some function on a module
22
+
23
+ Accepts a form data with the following fields
24
+
25
+ - `module` as string = your module address (32-bytes hex)
26
+
27
+ - `method` as string = WebAssembly function to be executed
28
+
29
+ - `params` as bytes = WebAssembly function parameters as pack-encoded bytes (see pack encoding below)
30
+
31
+ - `effort` as bytes = some unique 32 bytes whose sha256 `hash` whose result in `((2n ** 256n) / BigInt("0x" + hash.toHex()))` will be the maximum number of sparks (gas) used
32
+
33
+ ### POST /api/simulate
34
+
35
+ Simulate some function on a module (it won't write anything to storage and will execute in mode `2` so verifications such as signatures can be skipped)
36
+
37
+ Accepts a form data with the following fields
38
+
39
+ - `module` as string = your module address (32-bytes hex)
40
+
41
+ - `method` as string = WebAssembly function to be executed
42
+
43
+ - `params` as bytes = WebAssembly function parameters as pack-encoded bytes (see pack encoding below)
44
+
45
+ - `effort` as bytes = some unique 32 bytes whose sha256 `hash` whose result in `((2n ** 256n) / BigInt("0x" + hash.toHex()))` will be the maximum number of sparks (gas) used
46
+
47
+ ## WebAssembly API
48
+
49
+ ### <module>
50
+
51
+ You can use any module method by using the module address as hex
52
+
53
+ ```tsx
54
+ @external("5feeee846376f6436990aa2757bc67fbc4498bcc9993b647788e273ad6fde474", "add")
55
+ declare function add(x: u32, y: u32): u32
56
+ ```
57
+
58
+ ### blobs
59
+
60
+ You can pass bytes between modules by storing them in the blob storage and loading them via reference
61
+
62
+ - `blobs.save(offset: u32, length: u32): blobref` = save `length` bytes at `offset` of your memory to the blob storage
63
+
64
+ - `blobs.load(blob: blobref, offset: u32): void` = load some blob into your memory at `offset`
65
+
66
+ - `blobs.equals(left: blobref, right: blobref): bool` = check if two blobs are equals without loading them into memory
67
+
68
+ - `blobs.concat(left: blobref, right: blobref): blobref` = concatenate two blobs without loading them into memory
69
+
70
+ - `blob.to_hex/from_hex/to_base64/from_base64(blob: blobref): blobref` = convert blobs to/from hex/base64 without loading them into memory
71
+
72
+ ### bigints
73
+
74
+ You can work with infinite-precision bigints
75
+
76
+ - `bigints.add(left: bigintref, right: bigintref): bigintref` = add two bigints
77
+
78
+ - `bigints.sub(left: bigintref, right: bigintref): bigintref` = subtract two bigints
79
+
80
+ - `bigints.mul(left: bigintref, right: bigintref): bigintref` = multiply two bigints
81
+
82
+ - `bigints.div(left: bigintref, right: bigintref): bigintref` = divide two bigints
83
+
84
+ - `bigints.pow(left: bigintref, right: bigintref): bigintref` = left ** right
85
+
86
+ - `bigints.encode(bigint: bigintref): blobref` = convert bigint to bytes
87
+
88
+ - `bigints.decode(base16: blobref): bigintref` = convert bytes to bigint
89
+
90
+ - `bigints.to_base16(bigint: bigintref): blobref` = convert bigint to hex utf8 bytes
91
+
92
+ - `bigints.from_base16(base16: blobref): bigintref` = convert hex utf8 bytes to bigint
93
+
94
+ - `bigints.to_base10(bigint: bigintref): blobref` = convert bigint to base10 utf8 bytes
95
+
96
+ - `bigints.from_base10(base16: blobref): bigintref` = convert base10 utf8 bytes to bigint
97
+
98
+ ### packs
99
+
100
+ You can pack various arguments (numbers, refs) into a pack which can be passed between modules and/or encoded/decoded into bytes
101
+
102
+ - `packs.create(...values: any[]): packref` = create a new pack from the provided values (number, blobref, packref, null)
103
+
104
+ - `packs.encode(pack: packref): blobref` = encodes values into bytes using the following pseudocode
105
+
106
+ ```tsx
107
+ function writePack(pack: packref) {
108
+ for (const value of values) {
109
+ if (value == null) {
110
+ writeUint8(1)
111
+ continue
112
+ }
113
+
114
+ if (typeof value === "number") {
115
+ writeUint8(2)
116
+ writeFloat64(value, "little-endian")
117
+ continue
118
+ }
119
+
120
+ if (typeof value === "bigint") {
121
+ writeUint8(3)
122
+ writeUint32(value.toHex().length, "little-endian")
123
+ writeBytes(value.toHex())
124
+ continue
125
+ }
126
+
127
+ if (isBlobref(value)) {
128
+ writeUint8(4)
129
+ writeUint32(value.length, "little-endian")
130
+ writeBytes(value)
131
+ continue
132
+ }
133
+
134
+ if (isPackref(value)) {
135
+ writeUint8(5)
136
+ writePack(value)
137
+ continue
138
+ }
139
+
140
+ writeUint8(1) // anything else if encoded as null
141
+ continue
142
+ }
143
+
144
+ writeUint8(0)
145
+ }
146
+ ```
147
+
148
+ - `packs.decode(blob: blobref): packref` = decodes bytes into a pack of values using the same pseudocode but for reading
149
+
150
+ - `packs.concat(left: packref, right: packref)` = concatenate two packs into one (basically does `[...left, ...right]`)
151
+
152
+ - `packs.get<T>(pack: packref, index: u32): T` = get the value of a pack at `index` (throws if not found)
153
+
154
+ ### env
155
+
156
+ Get infos about the executing environment
157
+
158
+ - `env.mode: u32` = `1` if execution, `2` is simulation
159
+
160
+ - `env.uuid(): blobref` = get the unique uuid of this environment (similar to a chain id)
161
+
162
+ ### modules
163
+
164
+ Modules are identified by their address as a blob of bytes (pure sha256-output 32-length bytes without any encoding)
165
+
166
+ - `modules.load(module: blobref): blobref` = get the code of module as a blob
167
+
168
+ - `modules.call(module: blobref, method: blobref, params: packref): packref` = dynamically call a module method with the given params as pack and return value as a 1-length pack
169
+
170
+ - `modules.create(code: blobref, salt: blobref): blobref` = dynamically create a new module with the given code and salt, returns the module address
171
+
172
+ - `modules.self(): blobref` = get your module address as blob
173
+
174
+ ### storage
175
+
176
+ You can use a private storage (it works like storage and events at the same time)
177
+
178
+ - `storage.set(key: blobref, value: blobref): void` = set some value to storage at key
179
+
180
+ - `storage.get(key: blobref): blobref` = get the latest value from storage at key
181
+
182
+ ### sha256
183
+
184
+ Use the SHA-256 hashing algorithm
185
+
186
+ - `sha256.digest(payload: blobref): blobref` = hash the payload and returns the digest
187
+
188
+ ### ed25519
189
+
190
+ Use the Ed25519 signing algorithm
191
+
192
+ - `ed25519.verify(pubkey: blobref, signature: blobref, payload: blobref): boolean` = verify a signature
193
+
194
+ - `ed25519.sign(payload: blobref): blobref` = (experimental) sign payload using the miner's private key
195
+
196
+ ### symbols (experimental)
197
+
198
+ - `symbols.create(): symbolref` = create a unique reference that can be passed around
199
+
200
+ ### refs (experimental)
201
+
202
+ - `refs.numerize(ref: symbolref/blobref/packref): u32` = translate any reference into a unique private pointer that can be stored into data structures
203
+
204
+ - `refs.denumerize(pointer: u32): symbolref/blobref/packref` = get the exact same reference back from your private pointer
205
+
206
+ This can be useful if you want to check a reference for authenticity
207
+
208
+ ```tsx
209
+ const sessions = new Set<u32>()
210
+
211
+ export function login(password: blobref): symbolref {
212
+ const session = symbols.create()
213
+
214
+ sessions.put(symbols.numerize(session))
215
+
216
+ return session
217
+ }
218
+
219
+ export function verify(session: symbolref) {
220
+ return sessions.has(symbols.numerize(session))
221
+ }
222
+ ```
223
+
224
+ You should never accept a pointer instead of a real reference because they can be easily guessed by an attacking module
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import * as Wasm from "@hazae41/wasm";
2
+ export declare function meter(module: Wasm.Module, from: string, name: string): void;
@@ -0,0 +1,46 @@
1
+ import * as Wasm from "@hazae41/wasm";
2
+ export function meter(module, from, name) {
3
+ const wtype = module.body.sections.find(section => section.kind === Wasm.TypeSection.kind);
4
+ const wimport = module.body.sections.find(section => section.kind === Wasm.ImportSection.kind);
5
+ const wexport = module.body.sections.find(section => section.kind === Wasm.ExportSection.kind);
6
+ const wcode = module.body.sections.find(section => section.kind === Wasm.CodeSection.kind);
7
+ if (wtype == null)
8
+ throw new Error(`No type section`);
9
+ if (wimport == null)
10
+ throw new Error(`No import section`);
11
+ if (wexport == null)
12
+ throw new Error(`No export section`);
13
+ if (wcode == null)
14
+ throw new Error(`No code section`);
15
+ const wstart = module.body.sections.find(section => section.kind === Wasm.StartSection.kind);
16
+ wtype.descriptors.push({ prefix: Wasm.TypeSection.FuncType.kind, subtypes: [], body: new Wasm.TypeSection.FuncType([0x7f], []) });
17
+ wimport.descriptors.unshift({ from: new TextEncoder().encode(from), name: new TextEncoder().encode(name), body: new Wasm.ImportSection.FunctionImport(wtype.descriptors.length - 1) });
18
+ if (wstart != null)
19
+ wstart.funcidx++;
20
+ for (const body of wcode.bodies) {
21
+ const instructions = new Array();
22
+ const subinstructions = new Array();
23
+ for (const instruction of body.instructions) {
24
+ if ([0x10, 0x12, 0xd2].includes(instruction.opcode))
25
+ instruction.params[0].value++;
26
+ if ([0x03, 0x04, 0x05, 0x0B, 0x0c, 0x0D, 0x0E, 0x0F, 0xd5, 0xd6].includes(instruction.opcode)) {
27
+ subinstructions.push(instruction);
28
+ instructions.push(new Wasm.Instruction(0x41, [new Wasm.LEB128.I32(subinstructions.length)]));
29
+ instructions.push(new Wasm.Instruction(0x10, [new Wasm.LEB128.U32(0)]));
30
+ instructions.push(...subinstructions);
31
+ subinstructions.length = 0;
32
+ }
33
+ else {
34
+ subinstructions.push(instruction);
35
+ }
36
+ }
37
+ instructions.push(...subinstructions);
38
+ body.instructions = instructions;
39
+ continue;
40
+ }
41
+ for (const descriptor of wexport.descriptors) {
42
+ if (descriptor.kind !== 0x00)
43
+ continue;
44
+ descriptor.xidx++;
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ export type Nullable<T> = T | null | undefined;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { Cursor } from "@hazae41/cursor";
2
+ export declare class Pack {
3
+ readonly values: Array<Pack.Value>;
4
+ constructor(values: Array<Pack.Value>);
5
+ sizeOrThrow(): number;
6
+ writeOrThrow(cursor: Cursor): void;
7
+ }
8
+ export declare namespace Pack {
9
+ type Value = number | bigint | Uint8Array | Pack | null;
10
+ function readOrThrow(cursor: Cursor): Pack;
11
+ }
@@ -0,0 +1,98 @@
1
+ export class Pack {
2
+ values;
3
+ constructor(values) {
4
+ this.values = values;
5
+ }
6
+ sizeOrThrow() {
7
+ let size = 0;
8
+ for (const value of this.values) {
9
+ if (typeof value === "number") {
10
+ size += 1 + 4;
11
+ continue;
12
+ }
13
+ if (typeof value === "bigint") {
14
+ const text = value.toString(16);
15
+ const data = Uint8Array.fromHex(text.length % 2 === 1 ? "0" + text : text);
16
+ size += 1 + 4 + data.length;
17
+ continue;
18
+ }
19
+ if (value instanceof Uint8Array) {
20
+ size += 1 + 4 + value.length;
21
+ continue;
22
+ }
23
+ if (value instanceof Pack) {
24
+ size += 1 + value.sizeOrThrow();
25
+ continue;
26
+ }
27
+ size += 1;
28
+ continue;
29
+ }
30
+ size += 1;
31
+ return size;
32
+ }
33
+ writeOrThrow(cursor) {
34
+ for (const value of this.values) {
35
+ if (typeof value === "number") {
36
+ cursor.writeUint8OrThrow(2);
37
+ cursor.writeFloat64OrThrow(value, true);
38
+ continue;
39
+ }
40
+ if (typeof value === "bigint") {
41
+ cursor.writeUint8OrThrow(3);
42
+ const text = value.toString(16);
43
+ const data = Uint8Array.fromHex(text.length % 2 === 1 ? "0" + text : text);
44
+ cursor.writeUint32OrThrow(data.length, true);
45
+ cursor.writeOrThrow(data);
46
+ continue;
47
+ }
48
+ if (value instanceof Uint8Array) {
49
+ cursor.writeUint8OrThrow(4);
50
+ cursor.writeUint32OrThrow(value.length, true);
51
+ cursor.writeOrThrow(value);
52
+ continue;
53
+ }
54
+ if (value instanceof Pack) {
55
+ cursor.writeUint8OrThrow(5);
56
+ value.writeOrThrow(cursor);
57
+ continue;
58
+ }
59
+ cursor.writeUint8OrThrow(1);
60
+ continue;
61
+ }
62
+ cursor.writeUint8OrThrow(0);
63
+ return;
64
+ }
65
+ }
66
+ (function (Pack) {
67
+ function readOrThrow(cursor) {
68
+ const values = [];
69
+ while (true) {
70
+ const type = cursor.readUint8OrThrow();
71
+ if (type === 0)
72
+ break;
73
+ if (type === 2) {
74
+ values.push(cursor.readFloat64OrThrow(true));
75
+ continue;
76
+ }
77
+ if (type === 3) {
78
+ const size = cursor.readUint32OrThrow(true);
79
+ const data = cursor.readOrThrow(size);
80
+ values.push(BigInt("0x" + data.toHex()));
81
+ continue;
82
+ }
83
+ if (type === 4) {
84
+ const size = cursor.readUint32OrThrow(true);
85
+ values.push(cursor.readOrThrow(size));
86
+ continue;
87
+ }
88
+ if (type === 5) {
89
+ values.push(Pack.readOrThrow(cursor));
90
+ continue;
91
+ }
92
+ values.push(null);
93
+ continue;
94
+ }
95
+ return new Pack(values);
96
+ }
97
+ Pack.readOrThrow = readOrThrow;
98
+ })(Pack || (Pack = {}));
@@ -0,0 +1,3 @@
1
+ import type { Database } from "@tursodatabase/database";
2
+ export declare function runAsImmediateOrThrow<T>(database: Database, callback: (database: Database) => Promise<T>): Promise<T>;
3
+ export declare function runAsDeferredOrThrow<T>(database: Database, callback: (database: Database) => Promise<T>): Promise<T>;
@@ -0,0 +1,24 @@
1
+ export async function runAsImmediateOrThrow(database, callback) {
2
+ await database.exec("BEGIN IMMEDIATE TRANSACTION;");
3
+ try {
4
+ const result = await callback(database);
5
+ await database.exec("COMMIT;");
6
+ return result;
7
+ }
8
+ catch (e) {
9
+ await database.exec("ROLLBACK;");
10
+ throw e;
11
+ }
12
+ }
13
+ export async function runAsDeferredOrThrow(database, callback) {
14
+ await database.exec("BEGIN DEFERRED TRANSACTION;");
15
+ try {
16
+ const result = await callback(database);
17
+ await database.exec("COMMIT;");
18
+ return result;
19
+ }
20
+ catch (e) {
21
+ await database.exec("ROLLBACK;");
22
+ throw e;
23
+ }
24
+ }
package/out/mod.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./mods/mod.ts";
package/out/mod.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./mods/mod.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ import { RpcMethodNotFoundError } from "@hazae41/jsonrpc";
2
+ import { connect } from "@tursodatabase/database";
3
+ import { runAsImmediateOrThrow } from "../../libs/sql/mod.js";
4
+ const params = new URL(import.meta.url).searchParams;
5
+ const database = await connect(params.get("database"));
6
+ self.addEventListener("message", async (event) => {
7
+ try {
8
+ const request = event.data;
9
+ if (request.method === "storage_set") {
10
+ return await runAsImmediateOrThrow(database, async (database) => {
11
+ const [module, method, args, events] = event.data.params;
12
+ const moment = await database.prepare(`INSERT INTO moments (epoch, module, method, params) VALUES (0, ?, ?, ?);`).run(module, method, args);
13
+ const writer = database.prepare(`INSERT INTO events (moment, module, key, value) VALUES (?, ?, ?, ?);`);
14
+ for (const [module, key, value] of events)
15
+ await writer.run(moment.lastInsertRowid, module, key, value);
16
+ request.result[0] = 1;
17
+ request.result[1] = moment.lastInsertRowid;
18
+ Atomics.notify(request.result, 0);
19
+ return;
20
+ });
21
+ }
22
+ if (request.method === "storage_get") {
23
+ const [module, key] = request.params;
24
+ const row = await database.prepare(`SELECT value FROM events event WHERE event.module = ? AND event.key = ? ORDER BY event.nonce DESC LIMIT 1;`).get(module, key);
25
+ if (row == null) {
26
+ request.result[0] = 1;
27
+ request.result[1] = 2;
28
+ Atomics.notify(request.result, 0);
29
+ return;
30
+ }
31
+ const valueAsBytes = new Uint8Array(row.value);
32
+ request.result[0] = 1;
33
+ request.result[1] = 1;
34
+ request.result[2] = valueAsBytes.length;
35
+ request.result.buffer.grow(request.result.buffer.byteLength + valueAsBytes.length);
36
+ new Uint8Array(request.result.buffer).set(valueAsBytes, 4 + 4 + 4);
37
+ Atomics.notify(request.result, 0);
38
+ return;
39
+ }
40
+ if (request.method === "sha256_digest") {
41
+ const [payloadAsBytes] = request.params;
42
+ const digestAsBytes = new Uint8Array(await crypto.subtle.digest("SHA-256", payloadAsBytes));
43
+ request.result[0] = 1;
44
+ new Uint8Array(request.result.buffer).set(digestAsBytes, 4);
45
+ Atomics.notify(request.result, 0);
46
+ return;
47
+ }
48
+ if (request.method === "ed25519_verify") {
49
+ const [pubkeyAsBytes, signatureAsBytes, payloadAsBytes] = request.params;
50
+ const pubkeyAsKey = await crypto.subtle.importKey("raw", pubkeyAsBytes, "Ed25519", true, ["verify"]);
51
+ const verified = await crypto.subtle.verify("Ed25519", pubkeyAsKey, signatureAsBytes, payloadAsBytes);
52
+ request.result[0] = 1;
53
+ request.result[1] = verified ? 1 : 0;
54
+ Atomics.notify(request.result, 0);
55
+ return;
56
+ }
57
+ if (request.method === "ed25519_sign") {
58
+ const [privkeyAsBytes, payloadAsBytes] = request.params;
59
+ const privkeyAsKey = await crypto.subtle.importKey("pkcs8", privkeyAsBytes, "Ed25519", false, ["sign"]);
60
+ const signatureAsBytes = new Uint8Array(await crypto.subtle.sign("Ed25519", privkeyAsKey, payloadAsBytes));
61
+ request.result[0] = 1;
62
+ new Uint8Array(request.result.buffer).set(signatureAsBytes, 4);
63
+ Atomics.notify(request.result, 0);
64
+ return;
65
+ }
66
+ throw new RpcMethodNotFoundError();
67
+ }
68
+ catch (error) {
69
+ const request = event.data;
70
+ console.error(error);
71
+ request.result[0] = 2;
72
+ Atomics.notify(request.result, 0);
73
+ return;
74
+ }
75
+ });
@@ -0,0 +1 @@
1
+ export * from "./server/mod.ts";
@@ -0,0 +1 @@
1
+ export * from "./server/mod.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { readFileSync } from "node:fs";
2
+ import process from "node:process";
3
+ import { serveWithEnv } from "./mod.js";
4
+ const { PORT = process.env.PORT || "8080", CERT = process.env.CERT, KEY = process.env.KEY, } = process.env;
5
+ const port = Number(PORT);
6
+ const cert = CERT != null ? readFileSync(CERT, "utf8") : undefined;
7
+ const key = KEY != null ? readFileSync(KEY, "utf8") : undefined;
8
+ const server = await serveWithEnv();
9
+ const route = async (request) => {
10
+ if (request.method === "OPTIONS")
11
+ return new Response(null, { status: 204 });
12
+ if (request.headers.get("Upgrade") === "websocket") {
13
+ const { socket, response } = Deno.upgradeWebSocket(request);
14
+ await server.onWebSocketRequest(request, socket);
15
+ return response;
16
+ }
17
+ return await server.onHttpRequest(request);
18
+ };
19
+ const onHttpRequest = async (request) => {
20
+ try {
21
+ const response = await route(request);
22
+ if (response.status === 101)
23
+ return response;
24
+ response.headers.set("Access-Control-Allow-Origin", "*");
25
+ response.headers.set("Access-Control-Allow-Methods", "*");
26
+ response.headers.set("Access-Control-Allow-Headers", "*");
27
+ return response;
28
+ }
29
+ catch (cause) {
30
+ console.error(cause);
31
+ return new Response("Error", { status: 500 });
32
+ }
33
+ };
34
+ Deno.serve({ hostname: "0.0.0.0", port, cert, key }, onHttpRequest);
@@ -0,0 +1,8 @@
1
+ export declare function serveWithEnv(prefix?: string): Promise<{
2
+ onHttpRequest(request: Request): Promise<Response>;
3
+ onWebSocketRequest(request: Request, socket: WebSocket): Promise<void>;
4
+ }>;
5
+ export declare function serve(databaseAsPath: string, scriptsAsPath: string, ed25519PrivateKeyAsHex: string, ed25519PublicKeyAsHex: string): Promise<{
6
+ onHttpRequest(request: Request): Promise<Response>;
7
+ onWebSocketRequest(request: Request, socket: WebSocket): Promise<void>;
8
+ }>;
@@ -0,0 +1,299 @@
1
+ // deno-lint-ignore-file no-cond-assign no-unused-vars require-await
2
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
3
+ if (value !== null && value !== void 0) {
4
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
5
+ var dispose, inner;
6
+ if (async) {
7
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
8
+ dispose = value[Symbol.asyncDispose];
9
+ }
10
+ if (dispose === void 0) {
11
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
12
+ dispose = value[Symbol.dispose];
13
+ if (async) inner = dispose;
14
+ }
15
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
16
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
17
+ env.stack.push({ value: value, dispose: dispose, async: async });
18
+ }
19
+ else if (async) {
20
+ env.stack.push({ async: true });
21
+ }
22
+ return value;
23
+ };
24
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
25
+ return function (env) {
26
+ function fail(e) {
27
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
28
+ env.hasError = true;
29
+ }
30
+ var r, s = 0;
31
+ function next() {
32
+ while (r = env.stack.pop()) {
33
+ try {
34
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
35
+ if (r.dispose) {
36
+ var result = r.dispose.call(r.value);
37
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
38
+ }
39
+ else s |= 1;
40
+ }
41
+ catch (e) {
42
+ fail(e);
43
+ }
44
+ }
45
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
46
+ if (env.hasError) throw env.error;
47
+ }
48
+ return next();
49
+ };
50
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
51
+ var e = new Error(message);
52
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
53
+ });
54
+ import { Writable } from "@hazae41/binary";
55
+ import { RpcRequest, RpcResponse } from "@hazae41/jsonrpc";
56
+ import { Mutex } from "@hazae41/mutex";
57
+ import { connect } from "@tursodatabase/database";
58
+ import { existsSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
59
+ import { dirname } from "node:path";
60
+ import process from "node:process";
61
+ import { Pack } from "../../libs/packs/mod.js";
62
+ export async function serveWithEnv(prefix = "") {
63
+ const { DATABASE_PATH = process.env[prefix + "DATABASE_PATH"], SCRIPTS_PATH = process.env[prefix + "SCRIPTS_PATH"], ED25519_PRIVATE_KEY_HEX = process.env[prefix + "ED25519_PRIVATE_KEY_HEX"], ED25519_PUBLIC_KEY_HEX = process.env[prefix + "ED25519_PUBLIC_KEY_HEX"], } = {};
64
+ if (DATABASE_PATH == null)
65
+ throw new Error("DATABASE_PATH is not set");
66
+ if (SCRIPTS_PATH == null)
67
+ throw new Error("SCRIPTS_PATH is not set");
68
+ if (ED25519_PRIVATE_KEY_HEX == null)
69
+ throw new Error("ED25519_PRIVATE_KEY_HEX is not set");
70
+ if (ED25519_PUBLIC_KEY_HEX == null)
71
+ throw new Error("ED25519_PUBLIC_KEY_HEX is not set");
72
+ mkdirSync(dirname(DATABASE_PATH), { recursive: true });
73
+ return await serve(DATABASE_PATH, SCRIPTS_PATH, ED25519_PRIVATE_KEY_HEX, ED25519_PUBLIC_KEY_HEX);
74
+ }
75
+ export async function serve(databaseAsPath, scriptsAsPath, ed25519PrivateKeyAsHex, ed25519PublicKeyAsHex) {
76
+ const database = await connect(databaseAsPath);
77
+ await database.exec(`CREATE TABLE IF NOT EXISTS events (
78
+ nonce INTEGER PRIMARY KEY AUTOINCREMENT,
79
+
80
+ moment INTEGER NOT NULL,
81
+
82
+ module TEXT NOT NULL,
83
+
84
+ key BLOB NOT NULL,
85
+ value BLOB NOT NULL
86
+ );`);
87
+ await database.exec(`CREATE TABLE IF NOT EXISTS moments (
88
+ nonce INTEGER PRIMARY KEY AUTOINCREMENT,
89
+
90
+ epoch INTEGER NOT NULL,
91
+
92
+ module TEXT NOT NULL,
93
+ method TEXT NOT NULL,
94
+ params BLOB NOT NULL
95
+ );`);
96
+ await database.close();
97
+ mkdirSync(scriptsAsPath, { recursive: true });
98
+ const setOfEffortsAsHex = new Set();
99
+ const worker = new Mutex(new Worker(import.meta.resolve(`@/mods/worker/bin.ts?database=${databaseAsPath}&scripts=${scriptsAsPath}&ed25519PrivateKeyAsHex=${ed25519PrivateKeyAsHex}&ed25519PublicKeyAsHex=${ed25519PublicKeyAsHex}`), { name: "worker", type: "module" }));
100
+ const onHttpRequest = async (request) => {
101
+ let match;
102
+ if (match = new URLPattern("/api/create", request.url).exec(request.url)) {
103
+ if (request.method === "POST") {
104
+ const form = await request.formData();
105
+ const wasmAsEntry = form.get("code");
106
+ if (wasmAsEntry == null)
107
+ return Response.json(null, { status: 400 });
108
+ if (typeof wasmAsEntry === "string")
109
+ return Response.json(null, { status: 400 });
110
+ const wasmAsBytes = await wasmAsEntry.bytes();
111
+ const saltAsEntry = form.get("salt");
112
+ if (saltAsEntry == null)
113
+ return Response.json(null, { status: 400 });
114
+ if (typeof saltAsEntry === "string")
115
+ return Response.json(null, { status: 400 });
116
+ const saltAsBytes = await saltAsEntry.bytes();
117
+ const effortAsEntry = form.get("effort");
118
+ if (effortAsEntry == null)
119
+ return Response.json(null, { status: 400 });
120
+ if (typeof effortAsEntry === "string")
121
+ return Response.json(null, { status: 400 });
122
+ const effortAsBytes = await effortAsEntry.bytes();
123
+ if (effortAsBytes.length !== 32)
124
+ return Response.json(null, { status: 400 });
125
+ const effortAsHex = effortAsBytes.toHex();
126
+ if (setOfEffortsAsHex.has(effortAsHex))
127
+ return Response.json(null, { status: 402 });
128
+ setOfEffortsAsHex.add(effortAsHex);
129
+ const sparksAsBigInt = (2n ** 256n) / BigInt("0x" + new Uint8Array(await crypto.subtle.digest("SHA-256", effortAsBytes)).toHex());
130
+ if (sparksAsBigInt < (wasmAsBytes.length + saltAsBytes.length))
131
+ return Response.json(null, { status: 402 });
132
+ const packAsBytes = Writable.writeToBytesOrThrow(new Pack([wasmAsBytes, saltAsBytes]));
133
+ const digestOfWasmAsBytes = new Uint8Array(await crypto.subtle.digest("SHA-256", wasmAsBytes));
134
+ const digestOfPackAsBytes = new Uint8Array(await crypto.subtle.digest("SHA-256", packAsBytes));
135
+ const digestOfWasmAsHex = digestOfWasmAsBytes.toHex();
136
+ const digestOfPackAsHex = digestOfPackAsBytes.toHex();
137
+ if (!existsSync(`${scriptsAsPath}/${digestOfWasmAsHex}.wasm`))
138
+ writeFileSync(`${scriptsAsPath}/${digestOfWasmAsHex}.wasm`, wasmAsBytes);
139
+ if (!existsSync(`${scriptsAsPath}/${digestOfPackAsHex}.wasm`))
140
+ symlinkSync(`./${digestOfWasmAsHex}.wasm`, `${scriptsAsPath}/${digestOfPackAsHex}.wasm`);
141
+ return Response.json(digestOfPackAsHex);
142
+ }
143
+ return Response.json(null, { status: 405, headers: { "Allow": "POST" } });
144
+ }
145
+ if (match = new URLPattern("/api/execute", request.url).exec(request.url)) {
146
+ if (request.method === "POST") {
147
+ const env_1 = { stack: [], error: void 0, hasError: false };
148
+ try {
149
+ const stack = __addDisposableResource(env_1, new DisposableStack(), false);
150
+ const form = await request.formData();
151
+ const moduleAsEntry = form.get("module");
152
+ if (moduleAsEntry == null)
153
+ return Response.json(null, { status: 400 });
154
+ if (typeof moduleAsEntry !== "string")
155
+ return Response.json(null, { status: 400 });
156
+ const methodAsEntry = form.get("method");
157
+ if (methodAsEntry == null)
158
+ return Response.json(null, { status: 400 });
159
+ if (typeof methodAsEntry !== "string")
160
+ return Response.json(null, { status: 400 });
161
+ const paramsAsEntry = form.get("params");
162
+ if (paramsAsEntry == null)
163
+ return Response.json(null, { status: 400 });
164
+ if (typeof paramsAsEntry === "string")
165
+ return Response.json(null, { status: 400 });
166
+ const paramsAsBytes = await paramsAsEntry.bytes();
167
+ const effortAsEntry = form.get("effort");
168
+ if (effortAsEntry == null)
169
+ return Response.json(null, { status: 400 });
170
+ if (typeof effortAsEntry === "string")
171
+ return Response.json(null, { status: 400 });
172
+ const effortAsBytes = await effortAsEntry.bytes();
173
+ if (effortAsBytes.length !== 32)
174
+ return Response.json(null, { status: 400 });
175
+ const effortAsHex = effortAsBytes.toHex();
176
+ if (setOfEffortsAsHex.has(effortAsHex))
177
+ return Response.json(null, { status: 402 });
178
+ setOfEffortsAsHex.add(effortAsHex);
179
+ const sparksAsBigInt = (2n ** 256n) / BigInt("0x" + new Uint8Array(await crypto.subtle.digest("SHA-256", effortAsBytes)).toHex());
180
+ const future = Promise.withResolvers();
181
+ const aborter = new AbortController();
182
+ stack.defer(() => aborter.abort());
183
+ stack.use(await worker.lockOrWait());
184
+ worker.get().addEventListener("message", (event) => {
185
+ RpcResponse.from(event.data).inspectSync(future.resolve).inspectErrSync(future.reject);
186
+ }, { signal: aborter.signal });
187
+ worker.get().addEventListener("error", (event) => {
188
+ future.reject(event.error);
189
+ }, { signal: aborter.signal });
190
+ worker.get().addEventListener("messageerror", (event) => {
191
+ future.reject(event.data);
192
+ }, { signal: aborter.signal });
193
+ AbortSignal.timeout(1000).addEventListener("abort", (reason) => {
194
+ future.reject(reason);
195
+ }, { signal: aborter.signal });
196
+ worker.get().postMessage(new RpcRequest(null, "execute", [moduleAsEntry, methodAsEntry, paramsAsBytes, sparksAsBigInt]));
197
+ return new Response(await future.promise);
198
+ }
199
+ catch (e_1) {
200
+ env_1.error = e_1;
201
+ env_1.hasError = true;
202
+ }
203
+ finally {
204
+ __disposeResources(env_1);
205
+ }
206
+ }
207
+ return Response.json(null, { status: 405, headers: { "Allow": "POST" } });
208
+ }
209
+ if (match = new URLPattern("/api/simulate", request.url).exec(request.url)) {
210
+ if (request.method === "POST") {
211
+ const env_2 = { stack: [], error: void 0, hasError: false };
212
+ try {
213
+ const stack = __addDisposableResource(env_2, new DisposableStack(), false);
214
+ const form = await request.formData();
215
+ const moduleAsEntry = form.get("module");
216
+ if (moduleAsEntry == null)
217
+ return Response.json(null, { status: 400 });
218
+ if (typeof moduleAsEntry !== "string")
219
+ return Response.json(null, { status: 400 });
220
+ const methodAsEntry = form.get("method");
221
+ if (methodAsEntry == null)
222
+ return Response.json(null, { status: 400 });
223
+ if (typeof methodAsEntry !== "string")
224
+ return Response.json(null, { status: 400 });
225
+ const paramsAsEntry = form.get("params");
226
+ if (paramsAsEntry == null)
227
+ return Response.json(null, { status: 400 });
228
+ if (typeof paramsAsEntry === "string")
229
+ return Response.json(null, { status: 400 });
230
+ const paramsAsBytes = await paramsAsEntry.bytes();
231
+ const effortAsEntry = form.get("effort");
232
+ if (effortAsEntry == null)
233
+ return Response.json(null, { status: 400 });
234
+ if (typeof effortAsEntry === "string")
235
+ return Response.json(null, { status: 400 });
236
+ const effortAsBytes = await effortAsEntry.bytes();
237
+ if (effortAsBytes.length !== 32)
238
+ return Response.json(null, { status: 400 });
239
+ const effortAsHex = effortAsBytes.toHex();
240
+ if (setOfEffortsAsHex.has(effortAsHex))
241
+ return Response.json(null, { status: 402 });
242
+ setOfEffortsAsHex.add(effortAsHex);
243
+ const sparksAsBigInt = (2n ** 256n) / BigInt("0x" + new Uint8Array(await crypto.subtle.digest("SHA-256", effortAsBytes)).toHex());
244
+ const future = Promise.withResolvers();
245
+ const aborter = new AbortController();
246
+ stack.defer(() => aborter.abort());
247
+ stack.use(await worker.lockOrWait());
248
+ worker.get().addEventListener("message", (event) => {
249
+ RpcResponse.from(event.data).inspectSync(future.resolve).inspectErrSync(future.reject);
250
+ }, { signal: aborter.signal });
251
+ worker.get().addEventListener("error", (event) => {
252
+ future.reject(event.error);
253
+ }, { signal: aborter.signal });
254
+ worker.get().addEventListener("messageerror", (event) => {
255
+ future.reject(event.data);
256
+ }, { signal: aborter.signal });
257
+ AbortSignal.timeout(1000).addEventListener("abort", (reason) => {
258
+ future.reject(reason);
259
+ }, { signal: aborter.signal });
260
+ worker.get().postMessage(new RpcRequest(null, "simulate", [moduleAsEntry, methodAsEntry, paramsAsBytes, sparksAsBigInt]));
261
+ return new Response(await future.promise);
262
+ }
263
+ catch (e_2) {
264
+ env_2.error = e_2;
265
+ env_2.hasError = true;
266
+ }
267
+ finally {
268
+ __disposeResources(env_2);
269
+ }
270
+ }
271
+ return Response.json(null, { status: 405, headers: { "Allow": "POST" } });
272
+ }
273
+ return Response.json(null, { status: 404 });
274
+ };
275
+ const onWebSocketRequest = async (request, socket) => {
276
+ const routeOrThrow = (_message) => {
277
+ return;
278
+ };
279
+ const handleOrClose = async (request) => {
280
+ try {
281
+ if (!request)
282
+ return;
283
+ const response = await routeOrThrow(request);
284
+ if (response == null)
285
+ return;
286
+ socket.send(JSON.stringify(response));
287
+ }
288
+ catch {
289
+ socket.close();
290
+ }
291
+ };
292
+ socket.addEventListener("message", async (event) => {
293
+ if (typeof event.data !== "string")
294
+ return;
295
+ return await handleOrClose(event.data);
296
+ });
297
+ };
298
+ return { onHttpRequest, onWebSocketRequest };
299
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,351 @@
1
+ // deno-lint-ignore-file no-explicit-any no-unused-vars ban-unused-ignore
2
+ import { Readable, Writable } from "@hazae41/binary";
3
+ import { RpcErr, RpcError, RpcMethodNotFoundError, RpcOk } from "@hazae41/jsonrpc";
4
+ import * as Wasm from "@hazae41/wasm";
5
+ import { Buffer } from "node:buffer";
6
+ import { existsSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
7
+ import { meter } from "../../libs/metering/mod.js";
8
+ import { Pack } from "../../libs/packs/mod.js";
9
+ const params = new URL(import.meta.url).searchParams;
10
+ const scriptsAsPath = params.get("scripts");
11
+ const ed25519PrivkeyAsHex = params.get("ed25519PrivateKeyAsHex");
12
+ const ed25519PrivkeyAsBytes = Uint8Array.fromHex(ed25519PrivkeyAsHex);
13
+ const helper = new Worker(import.meta.resolve(`@/mods/helper/bin.ts?${params.toString()}`), { type: "module" });
14
+ function run(module, method, params, mode, maxsparks) {
15
+ let sparks = 0n;
16
+ const exports = {};
17
+ const caches = new Map();
18
+ const writes = new Array();
19
+ const pack_encode = (pack) => {
20
+ return Writable.writeToBytesOrThrow(pack);
21
+ };
22
+ const pack_decode = (bytes) => {
23
+ return Readable.readFromBytesOrThrow(Pack, bytes);
24
+ };
25
+ const sparks_consume = (amount) => {
26
+ sparks += amount;
27
+ if (maxsparks != null && sparks > maxsparks)
28
+ throw new Error("Out of sparks");
29
+ return;
30
+ };
31
+ const sha256_digest = (payload) => {
32
+ sparks_consume(BigInt(payload.length) * 256n);
33
+ const result = new Int32Array(new SharedArrayBuffer(4 + 32));
34
+ helper.postMessage({ method: "sha256_digest", params: [payload], result });
35
+ if (Atomics.wait(result, 0, 0) !== "ok")
36
+ throw new Error("Failed to wait");
37
+ if (result[0] === 2)
38
+ throw new Error("Internal error");
39
+ return new Uint8Array(result.buffer, 4, 32).slice();
40
+ };
41
+ const ed25519_verify = (pubkey, signature, payload) => {
42
+ sparks_consume(BigInt(payload.length) * 256n);
43
+ const result = new Int32Array(new SharedArrayBuffer(4 + 4));
44
+ helper.postMessage({ method: "ed25519_verify", params: [pubkey, signature, payload], result });
45
+ if (Atomics.wait(result, 0, 0) !== "ok")
46
+ throw new Error("Failed to wait");
47
+ if (result[0] === 2)
48
+ throw new Error("Internal error");
49
+ return result[1] === 1;
50
+ };
51
+ const ed25519_sign = (subpayload) => {
52
+ sparks_consume(BigInt(subpayload.length) * 256n);
53
+ const payload = pack_encode(new Pack([Uint8Array.fromHex(module), subpayload]));
54
+ const result = new Int32Array(new SharedArrayBuffer(4 + 64));
55
+ helper.postMessage({ method: "ed25519_sign", params: [ed25519PrivkeyAsBytes, payload], result });
56
+ if (Atomics.wait(result, 0, 0) !== "ok")
57
+ throw new Error("Failed to wait");
58
+ if (result[0] === 2)
59
+ throw new Error("Internal error");
60
+ return new Uint8Array(result.buffer, 4, 64).slice();
61
+ };
62
+ const load = (module) => {
63
+ const current = {};
64
+ caches.set(module, new Map());
65
+ const imports = {};
66
+ imports["env"] = {
67
+ mode: mode,
68
+ abort: () => {
69
+ throw new Error("Aborted");
70
+ },
71
+ uuid: () => {
72
+ return Uint8Array.fromHex("8a8f19d1de0e4fcd9ab15cd7ed5de6dd");
73
+ }
74
+ };
75
+ imports["sparks"] = {
76
+ remaining: () => {
77
+ return sparks;
78
+ },
79
+ consume: (amount) => {
80
+ sparks_consume(BigInt(amount >>> 0));
81
+ }
82
+ };
83
+ imports["console"] = {
84
+ log: (blob) => {
85
+ console.log(new TextDecoder().decode(blob));
86
+ }
87
+ };
88
+ imports["blobs"] = {
89
+ save: (offset, length) => {
90
+ const { memory } = current.instance.exports;
91
+ const view = new Uint8Array(memory.buffer, offset >>> 0, length >>> 0);
92
+ return view.slice();
93
+ },
94
+ size: (blob) => {
95
+ return blob.length;
96
+ },
97
+ load: (blob, offset) => {
98
+ const { memory } = current.instance.exports;
99
+ const view = new Uint8Array(memory.buffer, offset >>> 0, blob.length);
100
+ view.set(blob);
101
+ },
102
+ concat: (left, right) => {
103
+ const concat = new Uint8Array(left.length + right.length);
104
+ concat.set(left, 0);
105
+ concat.set(right, left.length);
106
+ return concat;
107
+ },
108
+ equals: (left, right) => {
109
+ return !Buffer.compare(left, right);
110
+ },
111
+ from_base16: (text) => {
112
+ return Uint8Array.fromHex(new TextDecoder().decode(text));
113
+ },
114
+ from_base64: (text) => {
115
+ return Uint8Array.fromBase64(new TextDecoder().decode(text));
116
+ },
117
+ to_base16: (bytes) => {
118
+ return new TextEncoder().encode(bytes.toHex());
119
+ },
120
+ to_base64: (bytes) => {
121
+ return new TextEncoder().encode(bytes.toBase64());
122
+ }
123
+ };
124
+ imports["bigints"] = {
125
+ add: (left, right) => {
126
+ return left + right;
127
+ },
128
+ sub: (left, right) => {
129
+ return left - right;
130
+ },
131
+ mul: (left, right) => {
132
+ return left * right;
133
+ },
134
+ div: (left, right) => {
135
+ return left / right;
136
+ },
137
+ pow: (left, right) => {
138
+ return left ** right;
139
+ },
140
+ encode: (bigint) => {
141
+ const text = bigint.toString(16);
142
+ const data = Uint8Array.fromHex(text.length % 2 === 1 ? "0" + text : text);
143
+ return data;
144
+ },
145
+ decode: (bytes) => {
146
+ return BigInt("0x" + bytes.toHex());
147
+ },
148
+ from_base16: (text) => {
149
+ return BigInt("0x" + new TextDecoder().decode(text));
150
+ },
151
+ to_base16: (bigint) => {
152
+ return new TextEncoder().encode(bigint.toString(16));
153
+ },
154
+ from_base10: (text) => {
155
+ return BigInt(new TextDecoder().decode(text));
156
+ },
157
+ to_base10: (bigint) => {
158
+ return new TextEncoder().encode(bigint.toString());
159
+ }
160
+ };
161
+ imports["symbols"] = {
162
+ create: () => {
163
+ return Symbol();
164
+ }
165
+ };
166
+ const refs = new Array();
167
+ const ptrs = new Map();
168
+ imports["refs"] = {
169
+ numerize: (ref) => {
170
+ const stale = ptrs.get(ref);
171
+ if (stale != null)
172
+ return stale;
173
+ const fresh = refs.push(ref) - 1;
174
+ ptrs.set(ref, fresh);
175
+ return fresh;
176
+ },
177
+ denumerize: (ptr) => {
178
+ const ref = refs.at(ptr >>> 0);
179
+ if (ref == null)
180
+ throw new Error("Not found");
181
+ return ref;
182
+ }
183
+ };
184
+ imports["modules"] = {
185
+ create: (wasmAsBytes, saltAsBytes) => {
186
+ const packAsBytes = pack_encode(new Pack([wasmAsBytes, saltAsBytes]));
187
+ const digestOfWasmAsBytes = sha256_digest(wasmAsBytes);
188
+ const digestOfPackAsBytes = sha256_digest(packAsBytes);
189
+ const digestOfWasmAsHex = digestOfWasmAsBytes.toHex();
190
+ const digestOfPackAsHex = digestOfPackAsBytes.toHex();
191
+ if (!existsSync(`${scriptsAsPath}/${digestOfWasmAsHex}.wasm`))
192
+ writeFileSync(`${scriptsAsPath}/${digestOfWasmAsHex}.wasm`, wasmAsBytes);
193
+ if (!existsSync(`${scriptsAsPath}/${digestOfPackAsHex}.wasm`))
194
+ symlinkSync(`./${digestOfWasmAsHex}.wasm`, `${scriptsAsPath}/${digestOfPackAsHex}.wasm`, "file");
195
+ return digestOfPackAsBytes;
196
+ },
197
+ call: (moduleAsBytes, methodAsBytes, paramsAsPack) => {
198
+ const moduleAsString = moduleAsBytes.toHex();
199
+ const methodAsString = new TextDecoder().decode(methodAsBytes);
200
+ if (exports[moduleAsString] == null)
201
+ load(moduleAsString);
202
+ if (typeof exports[moduleAsString][methodAsString] !== "function")
203
+ throw new Error("Not found");
204
+ return new Pack([exports[moduleAsString][methodAsString](...paramsAsPack.values)]);
205
+ },
206
+ load: (moduleAsBytes) => {
207
+ return readFileSync(`${scriptsAsPath}/${moduleAsBytes.toHex()}.wasm`);
208
+ },
209
+ self: () => {
210
+ return Uint8Array.fromHex(module);
211
+ }
212
+ };
213
+ imports["sha256"] = {
214
+ digest: (payload) => {
215
+ return sha256_digest(payload);
216
+ }
217
+ };
218
+ imports["ed25519"] = {
219
+ verify: (pubkey, signature, payload) => {
220
+ return ed25519_verify(pubkey, signature, payload);
221
+ },
222
+ sign: (payload) => {
223
+ return ed25519_sign(payload);
224
+ }
225
+ };
226
+ imports["packs"] = {
227
+ create: (...values) => {
228
+ return new Pack(values);
229
+ },
230
+ concat: (left, right) => {
231
+ return new Pack([...left.values, ...right.values]);
232
+ },
233
+ length: (pack) => {
234
+ return pack.values.length;
235
+ },
236
+ get(pack, index) {
237
+ const value = pack.values[index >>> 0];
238
+ if (value === undefined)
239
+ throw new Error("Not found");
240
+ return value;
241
+ },
242
+ encode: (pack) => {
243
+ return pack_encode(pack);
244
+ },
245
+ decode: (blob) => {
246
+ return pack_decode(blob);
247
+ }
248
+ };
249
+ imports["storage"] = {
250
+ set: (key, value) => {
251
+ const cache = caches.get(module);
252
+ cache.set(key, value);
253
+ writes.push([module, key, value]);
254
+ return;
255
+ },
256
+ get: (key) => {
257
+ const cache = caches.get(module);
258
+ const stale = cache.get(key);
259
+ if (stale != null)
260
+ return stale;
261
+ const result = new Int32Array(new SharedArrayBuffer(4 + 4 + 4, { maxByteLength: ((4 + 4 + 4) + (1024 * 1024)) }));
262
+ helper.postMessage({ method: "storage_get", params: [module, key], result });
263
+ if (Atomics.wait(result, 0, 0) !== "ok")
264
+ throw new Error("Failed to wait");
265
+ if (result[0] === 2)
266
+ throw new Error("Internal error");
267
+ if (result[1] === 2)
268
+ return null;
269
+ const fresh = new Uint8Array(result.buffer, 4 + 4 + 4, result[2]).slice();
270
+ cache.set(key, fresh);
271
+ return fresh;
272
+ }
273
+ };
274
+ let meteredWasmAsBytes;
275
+ if (!existsSync(`${scriptsAsPath}/${module}.metered.wasm`)) {
276
+ const wasmAsBytes = readFileSync(`${scriptsAsPath}/${module}.wasm`);
277
+ const wasmAsParsed = Readable.readFromBytesOrThrow(Wasm.Module, wasmAsBytes);
278
+ meter(wasmAsParsed, "sparks", "consume");
279
+ meteredWasmAsBytes = Writable.writeToBytesOrThrow(wasmAsParsed);
280
+ writeFileSync(`${scriptsAsPath}/${module}.metered.wasm`, meteredWasmAsBytes);
281
+ }
282
+ else {
283
+ meteredWasmAsBytes = readFileSync(`${scriptsAsPath}/${module}.metered.wasm`);
284
+ }
285
+ const meteredWasmAsModule = new WebAssembly.Module(meteredWasmAsBytes);
286
+ for (const descriptor of WebAssembly.Module.imports(meteredWasmAsModule)) {
287
+ if (imports[descriptor.module] != null) {
288
+ // NOOP
289
+ continue;
290
+ }
291
+ if (exports[descriptor.module] != null) {
292
+ imports[descriptor.module] = exports[descriptor.module];
293
+ continue;
294
+ }
295
+ const { instance } = load(descriptor.module);
296
+ imports[descriptor.module] = instance.exports;
297
+ continue;
298
+ }
299
+ const meteredWasmAsInstance = new WebAssembly.Instance(meteredWasmAsModule, imports);
300
+ current.instance = meteredWasmAsInstance;
301
+ current.module = meteredWasmAsModule;
302
+ exports[module] = meteredWasmAsInstance.exports;
303
+ return current;
304
+ };
305
+ const { instance } = load(module);
306
+ if (typeof instance.exports[method] !== "function")
307
+ throw new Error("Not found");
308
+ const result = pack_encode(new Pack([instance.exports[method](...pack_decode(params).values)]));
309
+ return { result, writes, sparks };
310
+ }
311
+ self.addEventListener("message", (event) => {
312
+ try {
313
+ const request = event.data;
314
+ if (request.method === "execute") {
315
+ const [module, method, params, maxsparks] = request.params;
316
+ const start = performance.now();
317
+ const { result, writes, sparks } = run(module, method, params, 1, maxsparks);
318
+ const until = performance.now();
319
+ console.log(`Evaluated ${(until - start).toFixed(2)}ms with ${sparks} sparks`);
320
+ if (writes.length) {
321
+ const result = new Int32Array(new SharedArrayBuffer(4 + 4));
322
+ helper.postMessage({ method: "storage_set", params: [module, method, params, writes], result });
323
+ if (Atomics.wait(result, 0, 0) !== "ok")
324
+ throw new Error("Failed to wait");
325
+ if (result[0] === 2)
326
+ throw new Error("Internal error");
327
+ console.log(`Wrote ${writes.length} events to storage`);
328
+ }
329
+ self.postMessage(new RpcOk(request.id, result));
330
+ return;
331
+ }
332
+ if (request.method === "simulate") {
333
+ const [module, method, params, maxsparks] = request.params;
334
+ const start = performance.now();
335
+ const { result, sparks } = run(module, method, params, 2, maxsparks);
336
+ const until = performance.now();
337
+ console.log(`Evaluated ${(until - start).toFixed(2)}ms with ${sparks} sparks`);
338
+ self.postMessage(new RpcOk(request.id, result));
339
+ return;
340
+ }
341
+ if (request.method === "verify") {
342
+ // TODO
343
+ }
344
+ throw new RpcMethodNotFoundError();
345
+ }
346
+ catch (cause) {
347
+ const request = event.data;
348
+ console.error(cause);
349
+ self.postMessage(new RpcErr(request.id, RpcError.rewrap(cause)));
350
+ }
351
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@hazae41/bobine",
4
+ "version": "0.0.2",
5
+ "description": "Your neighbor's chain of compute",
6
+ "repository": "github:hazae41/bobine",
7
+ "author": "hazae41",
8
+ "license": "MIT",
9
+ "scripts": {
10
+ "examine": "deno lint ./src && deno check ./src && deno test -R ./src",
11
+ "version": "deno -RW ./x.vsync.ts && git add deno.json",
12
+ "prepare": "deno -RW ./x.dsync.ts && deno install",
13
+ "prepack": "rm -rf ./out && tsc && tscousin",
14
+ "develop": "deno --env-file=./.env.local -A ./src/mods/server/bin.ts"
15
+ },
16
+ "files": [
17
+ "./out"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./out/mod.d.ts",
22
+ "import": "./out/mod.js"
23
+ },
24
+ "./worker": {
25
+ "types": "./out/mods/worker/bin.d.ts",
26
+ "import": "./out/mods/worker/bin.js"
27
+ },
28
+ "./helper": {
29
+ "types": "./out/mods/helper/bin.d.ts",
30
+ "import": "./out/mods/helper/bin.js"
31
+ }
32
+ },
33
+ "dependencies": {
34
+ "@hazae41/binary": "^2.0.2",
35
+ "@hazae41/cursor": "^2.1.3",
36
+ "@hazae41/jsonrpc": "^1.1.11",
37
+ "@hazae41/mutex": "^3.0.1",
38
+ "@hazae41/wasm": "^1.0.0",
39
+ "@tursodatabase/database": "^0.3.2"
40
+ },
41
+ "devDependencies": {
42
+ "@hazae41/phobos": "^2.0.16",
43
+ "@hazae41/tscousin": "^1.0.28",
44
+ "@types/node": "^24.10.1",
45
+ "typescript": "^5.9.3"
46
+ },
47
+ "keywords": [
48
+ "jsonrpc",
49
+ "webassembly",
50
+ "typescript",
51
+ "esmodules",
52
+ "tested",
53
+ "unit-tested"
54
+ ]
55
+ }