@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.
- package/README.md +107 -12
- package/dist/bin/build-inline.js +70 -0
- package/dist/bin/index.js +293 -0
- package/dist/bin/src/fuzz.js +155 -0
- package/dist/bin/src/log-host-call.js +41 -0
- package/dist/bin/src/test-json.js +135 -0
- package/dist/bin/src/trace-parse.js +315 -0
- package/dist/bin/src/trace-replay.js +130 -0
- package/dist/bin/src/tracer.js +64 -0
- package/dist/bin/src/utils.js +25 -0
- package/dist/bin/test.js +1 -0
- package/dist/build/compiler-inline.d.ts +11 -0
- package/dist/build/compiler-inline.js +22 -0
- package/dist/build/compiler.d.ts +26 -0
- package/dist/build/compiler.js +76 -0
- package/dist/build/compiler.wasm +0 -0
- package/{build → dist/build}/debug-inline.d.ts +1 -1
- package/dist/build/debug-inline.js +22 -0
- package/{build → dist/build}/debug-raw-inline.d.ts +1 -1
- package/dist/build/debug-raw-inline.js +22 -0
- package/{build → dist/build}/debug-raw.d.ts +155 -37
- package/{build → dist/build}/debug-raw.js +128 -41
- package/dist/build/debug-raw.wasm +0 -0
- package/{build → dist/build}/debug.d.ts +155 -37
- package/{build → dist/build}/debug.js +141 -43
- package/dist/build/debug.wasm +0 -0
- package/{build → dist/build}/release-inline.d.ts +1 -1
- package/dist/build/release-inline.js +22 -0
- package/{build → dist/build}/release-mini-inline.d.ts +1 -1
- package/dist/build/release-mini-inline.js +22 -0
- package/{build → dist/build}/release-mini.d.ts +155 -37
- package/{build → dist/build}/release-mini.js +141 -43
- package/dist/build/release-mini.wasm +0 -0
- package/{build → dist/build}/release-stub-inline.d.ts +1 -1
- package/dist/build/release-stub-inline.js +22 -0
- package/{build → dist/build}/release-stub.d.ts +155 -37
- package/{build → dist/build}/release-stub.js +141 -43
- package/dist/build/release-stub.wasm +0 -0
- package/{build → dist/build}/release.d.ts +155 -37
- package/{build → dist/build}/release.js +141 -43
- package/dist/build/release.wasm +0 -0
- package/{build → dist/build}/test-inline.d.ts +1 -1
- package/dist/build/test-inline.js +22 -0
- package/dist/build/test.wasm +0 -0
- package/dist/test/test-as.js +3 -0
- package/dist/test/test-gas-cost.js +57 -0
- package/dist/test/test-trace-replay.js +19 -0
- package/dist/test/test-w3f.js +121 -0
- package/dist/web/bump-version.js +7 -0
- package/package.json +48 -37
- package/build/debug-inline.js +0 -22
- package/build/debug-raw-inline.js +0 -22
- package/build/debug-raw.wasm +0 -0
- package/build/debug.wasm +0 -0
- package/build/release-inline.js +0 -22
- package/build/release-mini-inline.js +0 -22
- package/build/release-mini.wasm +0 -0
- package/build/release-stub-inline.js +0 -22
- package/build/release-stub.wasm +0 -0
- package/build/release.wasm +0 -0
- package/build/test-inline.js +0 -22
- package/build/test.wasm +0 -0
- /package/{build → dist/build}/test.d.ts +0 -0
- /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
|
-
[
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+

|
|
6
|
+
[](https://www.npmjs.com/package/@fluffylabs/anan-as)
|
|
7
|
+
[](https://www.npmjs.com/package/@fluffylabs/anan-as)
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
124
|
+
To run tests:
|
|
112
125
|
|
|
113
126
|
```cmd
|
|
114
|
-
|
|
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
|
+
}
|