@fluffylabs/anan-as 1.1.4 → 1.1.6-38dddc8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +107 -12
  2. package/dist/bin/build-inline.js +70 -0
  3. package/dist/bin/index.js +293 -0
  4. package/dist/bin/src/fuzz.js +155 -0
  5. package/dist/bin/src/log-host-call.js +41 -0
  6. package/dist/bin/src/test-json.js +135 -0
  7. package/dist/bin/src/trace-parse.js +315 -0
  8. package/dist/bin/src/trace-replay.js +130 -0
  9. package/dist/bin/src/tracer.js +64 -0
  10. package/dist/bin/src/utils.js +25 -0
  11. package/dist/bin/test.js +1 -0
  12. package/dist/build/compiler-inline.d.ts +11 -0
  13. package/dist/build/compiler-inline.js +22 -0
  14. package/dist/build/compiler.d.ts +26 -0
  15. package/dist/build/compiler.js +76 -0
  16. package/dist/build/compiler.wasm +0 -0
  17. package/{build → dist/build}/debug-inline.d.ts +1 -1
  18. package/dist/build/debug-inline.js +22 -0
  19. package/{build → dist/build}/debug-raw-inline.d.ts +1 -1
  20. package/dist/build/debug-raw-inline.js +22 -0
  21. package/{build → dist/build}/debug-raw.d.ts +155 -37
  22. package/{build → dist/build}/debug-raw.js +128 -41
  23. package/dist/build/debug-raw.wasm +0 -0
  24. package/{build → dist/build}/debug.d.ts +155 -37
  25. package/{build → dist/build}/debug.js +141 -43
  26. package/dist/build/debug.wasm +0 -0
  27. package/{build → dist/build}/release-inline.d.ts +1 -1
  28. package/dist/build/release-inline.js +22 -0
  29. package/{build → dist/build}/release-mini-inline.d.ts +1 -1
  30. package/dist/build/release-mini-inline.js +22 -0
  31. package/{build → dist/build}/release-mini.d.ts +155 -37
  32. package/{build → dist/build}/release-mini.js +141 -43
  33. package/dist/build/release-mini.wasm +0 -0
  34. package/{build → dist/build}/release-stub-inline.d.ts +1 -1
  35. package/dist/build/release-stub-inline.js +22 -0
  36. package/{build → dist/build}/release-stub.d.ts +155 -37
  37. package/{build → dist/build}/release-stub.js +141 -43
  38. package/dist/build/release-stub.wasm +0 -0
  39. package/{build → dist/build}/release.d.ts +155 -37
  40. package/{build → dist/build}/release.js +141 -43
  41. package/dist/build/release.wasm +0 -0
  42. package/{build → dist/build}/test-inline.d.ts +1 -1
  43. package/dist/build/test-inline.js +22 -0
  44. package/dist/build/test.wasm +0 -0
  45. package/dist/test/test-as.js +3 -0
  46. package/dist/test/test-gas-cost.js +57 -0
  47. package/dist/test/test-trace-replay.js +19 -0
  48. package/dist/test/test-w3f.js +121 -0
  49. package/dist/web/bump-version.js +7 -0
  50. package/package.json +48 -37
  51. package/build/debug-inline.js +0 -22
  52. package/build/debug-raw-inline.js +0 -22
  53. package/build/debug-raw.wasm +0 -0
  54. package/build/debug.wasm +0 -0
  55. package/build/release-inline.js +0 -22
  56. package/build/release-mini-inline.js +0 -22
  57. package/build/release-mini.wasm +0 -0
  58. package/build/release-stub-inline.js +0 -22
  59. package/build/release-stub.wasm +0 -0
  60. package/build/release.wasm +0 -0
  61. package/build/test-inline.js +0 -22
  62. package/build/test.wasm +0 -0
  63. /package/{build → dist/build}/test.d.ts +0 -0
  64. /package/{build → dist/build}/test.js +0 -0
package/README.md CHANGED
@@ -2,14 +2,11 @@
2
2
 
3
3
  AssemblyScript implementation of the JAM PVM (64-bit).
4
4
 
5
- [Demo](https://todr.me/anan-as)
6
-
7
- ## Todo
5
+ ![Gray Paper](https://img.shields.io/badge/Gray%20Paper-0.7.2-green)
6
+ [![npm](https://img.shields.io/npm/v/@fluffylabs/anan-as)](https://www.npmjs.com/package/@fluffylabs/anan-as)
7
+ [![npm dev](https://img.shields.io/npm/v/@fluffylabs/anan-as/next?label=dev)](https://www.npmjs.com/package/@fluffylabs/anan-as)
8
8
 
9
- - [x] Memory
10
- - [x] [JAM tests](https://github.com/w3f/jamtestvectors/pull/3) compatibility
11
- - [x] 64-bit & new instructions ([GrayPaper v0.5.0](https://graypaper.fluffylabs.dev))
12
- - [x] GP 0.5.4 compatibility (ZBB extensions)
9
+ [Demo](https://todr.me/anan-as)
13
10
 
14
11
  ## Why?
15
12
 
@@ -19,7 +16,7 @@ AssemblyScript implementation of the JAM PVM (64-bit).
19
16
 
20
17
  ## Useful where?
21
18
 
22
- - Potentially as an alternative implementation for [`typeberry`](https://github.com/fluffylabs).
19
+ - Main PVM backend of [`typeberry`](https://github.com/fluffylabs) JAM client.
23
20
  - To test out the [PVM debugger](https://pvm.fluffylabs.dev).
24
21
 
25
22
  ## Installation
@@ -51,9 +48,26 @@ import ananAs from '@fluffylabs/anan-as/release-mini';
51
48
  // make sure to call GC after multiple independent runs
52
49
  ananAs.__collect();
53
50
 
51
+ // Release build with stub host functions (for standalone testing)
52
+ import ananAs from '@fluffylabs/anan-as/release-stub';
53
+
54
+ // Compiler module (for PVM bytecode compilation)
55
+ import ananAs from '@fluffylabs/anan-as/compiler';
56
+ ```
57
+
58
+ ### Inline Builds
59
+
60
+ Inline builds bundle the WASM binary directly into the JavaScript module (base64 encoded),
61
+ eliminating the need to fetch a separate `.wasm` file:
62
+
63
+ ```javascript
64
+ import ananAs from '@fluffylabs/anan-as/debug-inline';
65
+ import ananAs from '@fluffylabs/anan-as/release-inline';
66
+ import ananAs from '@fluffylabs/anan-as/release-mini-inline';
67
+ import ananAs from '@fluffylabs/anan-as/release-stub-inline';
54
68
  ```
55
69
 
56
- ## Raw Bindings
70
+ ### Raw Bindings
57
71
 
58
72
  Raw bindings give you direct access to WebAssembly exports
59
73
  without the JavaScript wrapper layer.
@@ -72,7 +86,6 @@ const module = await WebAssembly.instantiateStreaming(
72
86
  imports
73
87
  );
74
88
  const ananAs = await instantiate(module);
75
-
76
89
  ```
77
90
 
78
91
  ## Version Tags
@@ -108,8 +121,90 @@ To run the example in the browser at [http://localhost:3000](http://localhost:30
108
121
  npm run web
109
122
  ```
110
123
 
111
- To run JSON test vectors.
124
+ To run tests:
112
125
 
113
126
  ```cmd
114
- npm start ./path/to/tests/*.json
127
+ # Run AssemblyScript unit tests and trace replay tests
128
+ npm test
129
+
130
+ # Run W3F test vectors
131
+ npm run test:w3f
132
+
133
+ # Run gas cost tests
134
+ npm run test:gas-cost
135
+ ```
136
+
137
+ ## CLI Usage
138
+
139
+ The package includes a CLI tool for disassembling, running, and replaying PVM bytecode:
140
+
141
+ ```bash
142
+ # Disassemble bytecode to assembly
143
+ npx @fluffylabs/anan-as disassemble [--spi] [--no-metadata] <file.pvm>
144
+
145
+ # Run JAM programs
146
+ npx @fluffylabs/anan-as run [--spi] [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.pvm> [spi-args.bin or hex]
147
+
148
+ # Replay an ecalli trace
149
+ # Learn more: https://github.com/tomusdrw/JIPs/blob/td-jip6-ecalliloggin/JIP-6.md
150
+ npx @fluffylabs/anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] <trace.log>
151
+
152
+ # Show help
153
+ npx @fluffylabs/anan-as --help
154
+ npx @fluffylabs/anan-as disassemble --help
155
+ npx @fluffylabs/anan-as run --help
156
+ ```
157
+
158
+ The `run` command executes PVM bytecode until it encounters a `halt` instruction or a host call.
159
+ The `replay-trace` command re-executes an ecalli trace, replaying recorded host call responses.
160
+
161
+ ### Commands
162
+
163
+ - `disassemble`: Convert PVM bytecode to human-readable assembly
164
+ - `run`: Execute PVM bytecode and show results
165
+ - `replay-trace`: Re-execute an ecalli trace with recorded host call responses
166
+
167
+ ### Flags
168
+
169
+ - `--spi`: Treat input as JAM SPI format instead of generic PVM
170
+ - `--no-metadata`: Input does not start with metadata
171
+ - `--no-logs`: Disable execution logs (run and replay-trace commands)
172
+ - `--no-verify`: Skip verification against trace data (replay-trace only)
173
+ - `--pc <number>`: Set initial program counter (default: 0)
174
+ - `--gas <number>`: Set initial gas amount (default: 10,000)
175
+ - `--help`, `-h`: Show help information
176
+
177
+ ### Examples
178
+
179
+ ```bash
180
+ # Disassemble a JAM file (includes metadata by default)
181
+ npx @fluffylabs/anan-as disassemble program.pvm
182
+
183
+ # Disassemble without metadata
184
+ npx @fluffylabs/anan-as disassemble --no-metadata program.pvm
185
+
186
+ # Disassemble JAM SPI program
187
+ npx @fluffylabs/anan-as disassemble --spi program.jam
188
+
189
+ # Run a JAM program with logs (includes metadata by default)
190
+ npx @fluffylabs/anan-as run program.pvm
191
+
192
+ # Run a JAM program without metadata
193
+ npx @fluffylabs/anan-as run --no-metadata program.pvm
194
+
195
+ # Run a JAM program quietly
196
+ npx @fluffylabs/anan-as run --no-logs program.pvm
197
+
198
+ # Run a JAM program with custom initial PC and gas
199
+ npx @fluffylabs/anan-as run --pc 100 --gas 10000 program.pvm
200
+
201
+ # Run JAM SPI program with arguments (file or hex)
202
+ npx @fluffylabs/anan-as run --spi program.jam args.bin
203
+ npx @fluffylabs/anan-as run --spi program.jam 0xdeadbeef
204
+
205
+ # Replay an ecalli trace
206
+ npx @fluffylabs/anan-as replay-trace trace.log
207
+
208
+ # Replay without verification
209
+ npx @fluffylabs/anan-as replay-trace --no-verify trace.log
115
210
  ```
@@ -0,0 +1,70 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const projectRoot = resolve(__dirname, "..");
6
+ // Load asconfig.json
7
+ const asconfigPath = resolve(projectRoot, "asconfig.json");
8
+ const asconfig = JSON.parse(readFileSync(asconfigPath, "utf-8"));
9
+ console.log("Building inline JS files with base64-encoded WASM...\n");
10
+ // Process each target
11
+ for (const [targetName, config] of Object.entries(asconfig.targets)) {
12
+ const wasmPath = resolve(projectRoot, config.outFile);
13
+ try {
14
+ // Read the WASM file
15
+ const wasmBuffer = readFileSync(wasmPath);
16
+ // Base64 encode
17
+ const wasmBase64 = wasmBuffer.toString("base64");
18
+ // Generate the output JS file name
19
+ // e.g., "build/release.wasm" -> "build/release-inline.js"
20
+ const wasmFileName = basename(config.outFile, ".wasm");
21
+ const outputPath = resolve(projectRoot, dirname(config.outFile), `${wasmFileName}-inline.js`);
22
+ // Create the JS content
23
+ const jsContent = `// Auto-generated inline WASM module
24
+ // Target: ${targetName}
25
+ // Source: ${config.outFile}
26
+
27
+ import * as raw from './debug-raw.js';
28
+
29
+ export const wasmBase64 = "${wasmBase64}";
30
+ let compiledModulePromise = null;
31
+
32
+ // Helper function to decode and instantiate the module
33
+ export async function instantiate(imports) {
34
+ if (compiledModulePromise === null) {
35
+ compiledModulePromise = WebAssembly.compile(getWasmBytes());
36
+ }
37
+ const module = await compiledModulePromise;
38
+ return raw.instantiate(module, imports);
39
+ }
40
+
41
+ // Helper function to just get the bytes
42
+ export function getWasmBytes() {
43
+ return Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0));
44
+ }
45
+ `;
46
+ // Write the JS file
47
+ writeFileSync(outputPath, jsContent, "utf-8");
48
+ // Generate and write the .d.ts file
49
+ const dtsPath = outputPath.replace(/\.js$/, ".d.ts");
50
+ const dtsContent = `// Auto-generated type definitions for inline WASM module
51
+ // Target: ${targetName}
52
+ // Source: ${config.outFile}
53
+
54
+ import {__AdaptedExports} from "./debug-raw.d.ts";
55
+
56
+ export const wasmBase64: string;
57
+
58
+ export function instantiate(imports?: { env?: any }): Promise<typeof __AdaptedExports>;
59
+
60
+ export function getWasmBytes(): Uint8Array;
61
+ `;
62
+ writeFileSync(dtsPath, dtsContent, "utf-8");
63
+ console.log(`✓ ${targetName}: ${outputPath} (${Math.round(wasmBase64.length / 1024)} KB base64)`);
64
+ }
65
+ catch (error) {
66
+ console.error(`✗ ${targetName}: Failed to process ${wasmPath}`);
67
+ console.error(` ${error instanceof Error ? error.message : error}`);
68
+ }
69
+ }
70
+ console.log("\nDone!");
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import minimist from "minimist";
4
+ import { disassemble, HasMetadata, InputKind, prepareProgram, pvmDestroy, pvmResume, pvmSetRegisters, pvmStart, } from "../build/release.js";
5
+ import { LOG_GAS_COST, LOG_HOST_CALL_INDEX, printLogHostCall, WHAT } from "./src/log-host-call.js";
6
+ import { STATUS } from "./src/trace-parse.js";
7
+ import { replayTraceFile } from "./src/trace-replay.js";
8
+ import { hexDecode, hexEncode } from "./src/utils.js";
9
+ const HELP_TEXT = `Usage:
10
+ anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>
11
+ anan-as run [--spi] [--no-logs] [--no-metadata] [--no-log-host-call] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin or hex]
12
+ anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] [--no-log-host-call] <trace.log>
13
+
14
+ Commands:
15
+ disassemble Disassemble PVM bytecode to assembly
16
+ run Execute PVM bytecode
17
+ replay-trace Re-execute a ecalli IO trace
18
+
19
+ Flags:
20
+ --spi Treat input as JAM SPI format
21
+ --no-metadata Input does not contain metadata
22
+ --no-logs Disable execution logs
23
+ --no-log-host-call Disable built-in handling of JIP-1 log host call (ecalli 100)
24
+ --no-verify Skip verification against trace data (replay-trace only)
25
+ --pc <number> Set initial program counter (default: 0)
26
+ --gas <number> Set initial gas amount (default: 10_000)
27
+ --help, -h Show this help message`;
28
+ main();
29
+ function main() {
30
+ const args = process.argv.slice(2);
31
+ // Handle global help flags
32
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
33
+ console.log(HELP_TEXT);
34
+ return;
35
+ }
36
+ const subCommand = args[0];
37
+ switch (subCommand) {
38
+ case "disassemble":
39
+ handleDisassemble(args.slice(1));
40
+ break;
41
+ case "run":
42
+ handleRun(args.slice(1));
43
+ break;
44
+ case "replay-trace":
45
+ handleReplayTrace(args.slice(1));
46
+ break;
47
+ default:
48
+ console.error(`Error: Unknown sub-command '${subCommand}'`);
49
+ console.error("");
50
+ console.error(HELP_TEXT);
51
+ process.exit(1);
52
+ }
53
+ }
54
+ function handleDisassemble(args) {
55
+ const parsed = minimist(args, {
56
+ boolean: ["spi", "metadata", "help"],
57
+ alias: { h: "help" },
58
+ default: { metadata: true },
59
+ });
60
+ if (parsed.help) {
61
+ console.log(HELP_TEXT);
62
+ return;
63
+ }
64
+ const files = parsed._;
65
+ if (files.length === 0) {
66
+ console.error("Error: No file provided for disassemble command.");
67
+ console.error("Usage: anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>");
68
+ process.exit(1);
69
+ }
70
+ if (files.length > 1) {
71
+ console.error("Error: Only one file can be disassembled at a time.");
72
+ console.error("Usage: anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>");
73
+ process.exit(1);
74
+ }
75
+ const file = files[0];
76
+ // Validate file extension for disassemble command
77
+ const validExtensions = [".jam", ".pvm", ".spi", ".bin"];
78
+ const dotIndex = file.lastIndexOf(".");
79
+ if (dotIndex === -1) {
80
+ console.error(`Error: File '${file}' has no extension.`);
81
+ console.error("Supported extensions: .jam, .pvm, .spi, .bin");
82
+ process.exit(1);
83
+ }
84
+ const ext = file.substring(dotIndex);
85
+ if (!validExtensions.includes(ext)) {
86
+ console.error(`Error: Invalid file extension '${ext}' for disassemble command.`);
87
+ console.error("Supported extensions: .jam, .pvm, .spi, .bin");
88
+ process.exit(1);
89
+ }
90
+ const kind = parsed.spi ? InputKind.SPI : InputKind.Generic;
91
+ const hasMetadata = parsed.metadata ? HasMetadata.Yes : HasMetadata.No;
92
+ const f = readFileSync(file);
93
+ const name = kind === InputKind.Generic ? "generic PVM" : "JAM SPI";
94
+ console.log(`🤖 Assembly of ${file} (as ${name})`);
95
+ console.log(disassemble(Array.from(f), kind, hasMetadata));
96
+ }
97
+ function handleRun(args) {
98
+ const parsed = minimist(args, {
99
+ boolean: ["spi", "logs", "metadata", "help", "log-host-call"],
100
+ /** Prevents parsing hex values as numbers. */
101
+ string: ["pc", "gas", "_"],
102
+ alias: { h: "help" },
103
+ default: { metadata: true, logs: true, "log-host-call": true },
104
+ });
105
+ if (parsed.help) {
106
+ console.log(HELP_TEXT);
107
+ return;
108
+ }
109
+ const files = parsed._;
110
+ if (files.length === 0) {
111
+ console.error("Error: No file provided for run command.");
112
+ console.error("Usage: anan-as run [--spi] [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin]");
113
+ process.exit(1);
114
+ }
115
+ const kind = parsed.spi ? InputKind.SPI : InputKind.Generic;
116
+ let programFile;
117
+ let spiArgsStr;
118
+ if (kind === InputKind.SPI) {
119
+ // For SPI programs, expect: <program.spi> [spi-args.bin or hex]
120
+ if (files.length > 2) {
121
+ console.error("Error: Too many arguments for SPI run command.");
122
+ console.error("Usage: anan-as run --spi [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <program.spi> [spi-args.bin or hex]");
123
+ process.exit(1);
124
+ }
125
+ programFile = files[0];
126
+ spiArgsStr = files[1]; // optional
127
+ }
128
+ else {
129
+ // For generic programs, expect exactly one file
130
+ if (files.length > 1) {
131
+ console.error("Error: Only one file can be run at a time.");
132
+ console.error("Usage: anan-as run [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam>");
133
+ process.exit(1);
134
+ }
135
+ programFile = files[0];
136
+ }
137
+ // Validate SPI args file if provided
138
+ const spiArgs = parseSpiArgs(spiArgsStr);
139
+ const logs = parsed.logs;
140
+ const logHostCall = parsed["log-host-call"];
141
+ const hasMetadata = parsed.metadata ? HasMetadata.Yes : HasMetadata.No;
142
+ // Parse and validate PC and gas options
143
+ const initialPc = parsePc(parsed);
144
+ const initialGas = parseGas(parsed);
145
+ const programCode = Array.from(readFileSync(programFile));
146
+ const name = kind === InputKind.Generic ? "generic PVM" : "JAM SPI";
147
+ console.log(`🚀 Running ${programFile} (as ${name})`);
148
+ try {
149
+ const preallocateMemoryPages = 128;
150
+ const program = prepareProgram(kind, hasMetadata, programCode, [], [], [], spiArgs, preallocateMemoryPages);
151
+ const id = pvmStart(program, false);
152
+ let gas = initialGas;
153
+ let pc = initialPc;
154
+ for (;;) {
155
+ const pause = pvmResume(id, gas, pc, logs);
156
+ if (!pause) {
157
+ throw new Error("pvmResume returned null");
158
+ }
159
+ if (pause.status === STATUS.HOST && pause.exitCode === LOG_HOST_CALL_INDEX && logHostCall) {
160
+ printLogHostCall(id, pause.registers);
161
+ // Set r7 = WHAT
162
+ const regs = pause.registers;
163
+ regs[7] = WHAT;
164
+ pvmSetRegisters(id, regs);
165
+ // Deduct gas and advance PC
166
+ gas = pause.gas >= LOG_GAS_COST ? pause.gas - LOG_GAS_COST : 0n;
167
+ pc = pause.nextPc;
168
+ }
169
+ else {
170
+ console.warn(`Unhandled host call: ecalli ${pause.exitCode}. Finishing.`);
171
+ break;
172
+ }
173
+ }
174
+ const result = pvmDestroy(id);
175
+ console.log(`Status: ${result?.status}`);
176
+ console.log(`Exit code: ${result?.exitCode}`);
177
+ console.log(`Program counter: ${result?.pc}`);
178
+ console.log(`Gas remaining: ${result?.gas}`);
179
+ console.log(`Registers: [${result?.registers.join(", ")}]`);
180
+ console.log(`Result: [${hexEncode(result?.result ?? [])}]`);
181
+ }
182
+ catch (error) {
183
+ console.error(`Error running ${programFile}:`, error);
184
+ process.exit(1);
185
+ }
186
+ }
187
+ function handleReplayTrace(args) {
188
+ const parsed = minimist(args, {
189
+ boolean: ["metadata", "verify", "logs", "help", "log-host-call"],
190
+ alias: { h: "help" },
191
+ default: { metadata: true, logs: true, verify: true, "log-host-call": true },
192
+ });
193
+ if (parsed.help) {
194
+ console.log(HELP_TEXT);
195
+ return;
196
+ }
197
+ const files = parsed._;
198
+ if (files.length === 0) {
199
+ console.error("Error: No trace file provided for replay-trace command.");
200
+ console.error("Usage: anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] <trace.log>");
201
+ process.exit(1);
202
+ }
203
+ if (files.length > 1) {
204
+ console.error("Error: Only one trace file can be replayed at a time.");
205
+ console.error("Usage: anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] <trace.log>");
206
+ process.exit(1);
207
+ }
208
+ const file = files[0];
209
+ const hasMetadata = parsed.metadata ? HasMetadata.Yes : HasMetadata.No;
210
+ const verify = parsed.verify;
211
+ const logs = parsed.logs;
212
+ const logHostCall = parsed["log-host-call"];
213
+ try {
214
+ const summary = replayTraceFile(file, {
215
+ logs,
216
+ hasMetadata,
217
+ verify,
218
+ logHostCall,
219
+ });
220
+ console.log(`✅ Replay complete: ${summary.ecalliCount} ecalli entries`);
221
+ console.log(`Status: ${summary.termination.type}`);
222
+ console.log(`Program counter: ${summary.termination.pc}`);
223
+ console.log(`Gas remaining: ${summary.termination.gas}`);
224
+ }
225
+ catch (error) {
226
+ console.error(`Error replaying trace ${file}:`, error);
227
+ process.exit(1);
228
+ }
229
+ }
230
+ function parseGas(parsed) {
231
+ if (parsed.gas === undefined) {
232
+ return BigInt(10_000);
233
+ }
234
+ // Ensure it's a string/number, not boolean
235
+ if (typeof parsed.gas === "boolean") {
236
+ console.error("Error: --gas requires a value.");
237
+ process.exit(1);
238
+ }
239
+ const gasStr = String(parsed.gas);
240
+ // Reject floats and non-integer strings
241
+ if (gasStr.includes(".") || !/^-?\d+$/.test(gasStr)) {
242
+ console.error("Error: --gas must be a valid integer.");
243
+ process.exit(1);
244
+ }
245
+ let gasValue;
246
+ try {
247
+ gasValue = BigInt(gasStr);
248
+ }
249
+ catch (_e) {
250
+ console.error("Error: --gas must be a valid integer.");
251
+ process.exit(1);
252
+ }
253
+ const MAX_I64 = (1n << 63n) - 1n;
254
+ if (gasValue < 0n || gasValue > MAX_I64) {
255
+ console.error("Error: --gas must be a non-negative integer <= 2^63-1.");
256
+ process.exit(1);
257
+ }
258
+ return gasValue;
259
+ }
260
+ function parseSpiArgs(spiArgsStr) {
261
+ if (!spiArgsStr) {
262
+ return [];
263
+ }
264
+ try {
265
+ return Array.from(hexDecode(spiArgsStr));
266
+ }
267
+ catch (e) {
268
+ console.log(`Attempting to read ${spiArgsStr} as a file, since it's not a hex value: ${e}`);
269
+ return Array.from(readFileSync(spiArgsStr));
270
+ }
271
+ }
272
+ function parsePc(parsed) {
273
+ if (parsed.pc === undefined) {
274
+ return 0;
275
+ }
276
+ // Ensure it's a string/number, not boolean
277
+ if (typeof parsed.pc === "boolean") {
278
+ console.error("Error: --pc requires a value.");
279
+ process.exit(1);
280
+ }
281
+ const pcStr = String(parsed.pc);
282
+ // Reject floats and non-integer strings
283
+ if (pcStr.includes(".") || !/^-?\d+$/.test(pcStr)) {
284
+ console.error("Error: --pc must be a valid integer.");
285
+ process.exit(1);
286
+ }
287
+ const pcValue = parseInt(pcStr, 10);
288
+ if (!Number.isInteger(pcValue) || pcValue < 0 || pcValue > 0xffffffff) {
289
+ console.error("Error: --pc must be a non-negative integer <= 2^32-1.");
290
+ process.exit(1);
291
+ }
292
+ return pcValue;
293
+ }
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ import "json-bigint-patch";
3
+ import fs from "node:fs";
4
+ import { tryAsGas } from "@typeberry/lib/pvm-interface";
5
+ import { Interpreter } from "@typeberry/lib/pvm-interpreter";
6
+ import { disassemble, HasMetadata, InputKind, prepareProgram, runProgram, wrapAsProgram } from "../../build/release.js";
7
+ const runNumber = 0;
8
+ export function fuzz(data) {
9
+ const gas = 200n;
10
+ const pc = 0;
11
+ const vm = new Interpreter();
12
+ const program = wrapAsProgram(new Uint8Array(data));
13
+ if (program.length > 100) {
14
+ return;
15
+ }
16
+ try {
17
+ vm.resetGeneric(program, pc, tryAsGas(gas));
18
+ vm.runProgram();
19
+ const printDebugInfo = false;
20
+ const registers = Array(13)
21
+ .join(",")
22
+ .split(",")
23
+ .map(() => BigInt(0));
24
+ const exe = prepareProgram(InputKind.Generic, HasMetadata.No, Array.from(program), registers, [], [], [], 0);
25
+ const output = runProgram(exe, gas, pc, printDebugInfo);
26
+ const vmRegisters = decodeRegistersFromTypeberry(vm);
27
+ collectErrors((assertFn) => {
28
+ assertFn(normalizeStatus(vm.getStatus()), normalizeStatus(output.status), "status");
29
+ assertFn(vm.gas.get(), output.gas, "gas");
30
+ assertFn(vmRegisters, output.registers, "registers");
31
+ assertFn(vm.getPC(), output.pc, "pc");
32
+ });
33
+ try {
34
+ if (runNumber % 100000 === 0) {
35
+ writeTestCase(program, {
36
+ pc,
37
+ gas,
38
+ registers,
39
+ }, {
40
+ status: normalizeStatus(vm.getStatus()),
41
+ gasLeft: vm.gas.get(),
42
+ pc: vm.getPC(),
43
+ registers: vmRegisters,
44
+ });
45
+ }
46
+ }
47
+ catch (e) {
48
+ console.warn("Unable to write file", e);
49
+ }
50
+ }
51
+ catch (e) {
52
+ const hex = programHex(program);
53
+ console.log(program);
54
+ console.log(linkTo(hex));
55
+ console.log(disassemble(Array.from(program), InputKind.Generic, HasMetadata.No));
56
+ throw e;
57
+ }
58
+ }
59
+ import { hexEncode } from "./utils.js";
60
+ function programHex(program) {
61
+ return hexEncode(program, false);
62
+ }
63
+ function linkTo(programHex) {
64
+ return `https://pvm.fluffylabs.dev/?program=0x${programHex}#/`;
65
+ }
66
+ function decodeRegistersFromTypeberry(vm) {
67
+ const registers = [];
68
+ // Try to get up to 13 registers (common register count)
69
+ for (let i = 0; i < 13; i++) {
70
+ try {
71
+ registers.push(vm.registers.getU64(i));
72
+ }
73
+ catch (_e) {
74
+ // If we can't get a register, break
75
+ break;
76
+ }
77
+ }
78
+ return registers;
79
+ }
80
+ function normalizeStatus(status) {
81
+ if (status === 2) {
82
+ return 1;
83
+ }
84
+ return status;
85
+ }
86
+ function assert(tb, an, comment = "") {
87
+ let condition = tb !== an;
88
+ if (Array.isArray(tb) && Array.isArray(an)) {
89
+ condition = tb.toString() !== an.toString();
90
+ }
91
+ if (condition) {
92
+ const alsoAsHex = (f) => {
93
+ if (Array.isArray(f)) {
94
+ return `${f.map(alsoAsHex).join(", ")}`;
95
+ }
96
+ if (typeof f === "number" || typeof f === "bigint") {
97
+ if (BigInt(f) !== 0n) {
98
+ return `${f} | 0x${f.toString(16)}`;
99
+ }
100
+ return `${f}`;
101
+ }
102
+ return f;
103
+ };
104
+ throw new Error(`Diverging value: ${comment}
105
+ \t(typeberry) ${alsoAsHex(tb)}
106
+ \t(ananas) ${alsoAsHex(an)}`);
107
+ }
108
+ }
109
+ function collectErrors(cb) {
110
+ const errors = [];
111
+ cb((tb, an, comment = "") => {
112
+ try {
113
+ assert(tb, an, comment);
114
+ }
115
+ catch (e) {
116
+ errors.push(`${e}`);
117
+ }
118
+ });
119
+ if (errors.length > 0) {
120
+ throw new Error(errors.join("\n"));
121
+ }
122
+ }
123
+ function writeTestCase(program, initial, expected) {
124
+ const hex = programHex(program);
125
+ fs.mkdirSync(`../tests/length_${hex.length}`, { recursive: true });
126
+ fs.writeFileSync(`../tests/length_${hex.length}/${hex}.json`, JSON.stringify({
127
+ name: linkTo(hex),
128
+ "initial-regs": initial.registers,
129
+ "initial-pc": initial.pc,
130
+ "initial-page-map": [],
131
+ "initial-memory": [],
132
+ "initial-gas": initial.gas,
133
+ program: Array.from(program),
134
+ "expected-status": statusToStr(expected.status),
135
+ "expected-regs": Array.from(expected.registers),
136
+ "expected-pc": expected.pc,
137
+ "expected-gas": expected.gasLeft,
138
+ "expected-memory": [],
139
+ }));
140
+ }
141
+ function statusToStr(status) {
142
+ if (status === 0) {
143
+ return "halt";
144
+ }
145
+ if (status === 1) {
146
+ return "trap";
147
+ }
148
+ if (status === 4) {
149
+ return "oog";
150
+ }
151
+ if (status === 3) {
152
+ return "host";
153
+ }
154
+ throw new Error(`unexpected status: ${status}`);
155
+ }