@fluffylabs/anan-as 1.1.3-e748fc6 → 1.1.3-eee81bd
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 +106 -11
- package/dist/bin/build-inline.js +70 -0
- package/dist/bin/index.js +292 -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 +154 -37
- package/{build → dist/build}/debug-raw.js +126 -39
- package/dist/build/debug-raw.wasm +0 -0
- package/{build → dist/build}/debug.d.ts +154 -37
- package/{build → dist/build}/debug.js +139 -41
- 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 +154 -37
- package/{build → dist/build}/release-mini.js +139 -41
- 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 +154 -37
- package/{build → dist/build}/release-stub.js +139 -41
- package/dist/build/release-stub.wasm +0 -0
- package/{build → dist/build}/release.d.ts +154 -37
- package/{build → dist/build}/release.js +139 -41
- 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 +50 -41
- 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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { pvmReadMemory } from "../../build/release.js";
|
|
2
|
+
export const LOG_HOST_CALL_INDEX = 100;
|
|
3
|
+
export const LOG_GAS_COST = 10n;
|
|
4
|
+
/** The WHAT return value - indicates the host call is not implemented / acknowledged. */
|
|
5
|
+
export const WHAT = 0xfffffffffffffffen;
|
|
6
|
+
const LOG_LEVELS = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG"];
|
|
7
|
+
const MAX_LOG_LEN = 8192;
|
|
8
|
+
/**
|
|
9
|
+
* Print the log message from a JIP-1 log host call (ecalli 100).
|
|
10
|
+
*
|
|
11
|
+
* Reads the level, target, and message from the PVM registers and memory,
|
|
12
|
+
* then prints via console.info.
|
|
13
|
+
*/
|
|
14
|
+
export function printLogHostCall(pvmId, registers) {
|
|
15
|
+
const level = Number(registers[7]);
|
|
16
|
+
const targetPtr = Number(registers[8] & 0xffffffffn);
|
|
17
|
+
const targetLen = Math.min(Math.max(0, Number(registers[9] & 0xffffffffn)), MAX_LOG_LEN);
|
|
18
|
+
const messagePtr = Number(registers[10] & 0xffffffffn);
|
|
19
|
+
const messageLen = Math.min(Math.max(0, Number(registers[11] & 0xffffffffn)), MAX_LOG_LEN);
|
|
20
|
+
const levelStr = LOG_LEVELS[level] ?? `LEVEL(${level})`;
|
|
21
|
+
let target = "";
|
|
22
|
+
if (targetPtr !== 0 && targetLen > 0) {
|
|
23
|
+
const targetBytes = pvmReadMemory(pvmId, targetPtr, targetLen);
|
|
24
|
+
if (targetBytes) {
|
|
25
|
+
target = new TextDecoder().decode(targetBytes);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let message = "";
|
|
29
|
+
if (messagePtr !== 0 && messageLen > 0) {
|
|
30
|
+
const messageBytes = pvmReadMemory(pvmId, messagePtr, messageLen);
|
|
31
|
+
if (messageBytes) {
|
|
32
|
+
message = new TextDecoder().decode(messageBytes);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (target) {
|
|
36
|
+
console.info(`[${levelStr}] ${target}: ${message}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.info(`[${levelStr}] ${message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "json-bigint-patch";
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
export const OK = "🟢";
|
|
6
|
+
export const ERR = "🔴";
|
|
7
|
+
// Main function
|
|
8
|
+
export function run(processJson, options) {
|
|
9
|
+
// Get the JSON file arguments from the command line
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
for (;;) {
|
|
12
|
+
if (args.length === 0) {
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
if (args[0] === "--debug") {
|
|
16
|
+
args.shift();
|
|
17
|
+
options.isDebug = true;
|
|
18
|
+
}
|
|
19
|
+
else if (args[0] === "--sbrk-gas") {
|
|
20
|
+
args.shift();
|
|
21
|
+
options.useSbrkGas = true;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (args.length === 0) {
|
|
28
|
+
console.error("Error: No JSON files provided.");
|
|
29
|
+
console.error("Usage: index.js [--debug] [--sbrk-gas] <file1.json> [file2.json ...]");
|
|
30
|
+
console.error("read from stdin: index.js [--debug] [--sbrk-gas] -");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
if (args[0] === "-") {
|
|
34
|
+
if (options.isDebug) {
|
|
35
|
+
throw new Error("debug needs to be disabled!");
|
|
36
|
+
}
|
|
37
|
+
readFromStdin(processJson, options);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const status = {
|
|
41
|
+
all: 0,
|
|
42
|
+
ok: [],
|
|
43
|
+
fail: [],
|
|
44
|
+
};
|
|
45
|
+
// Process each file
|
|
46
|
+
args.forEach((filePath) => {
|
|
47
|
+
// try whole directory
|
|
48
|
+
let dir = null;
|
|
49
|
+
try {
|
|
50
|
+
dir = readdirSync(filePath);
|
|
51
|
+
}
|
|
52
|
+
catch (_e) {
|
|
53
|
+
// Not a directory or inaccessible, will try as file
|
|
54
|
+
}
|
|
55
|
+
if (dir !== null) {
|
|
56
|
+
status.all += dir.length;
|
|
57
|
+
for (const file of dir) {
|
|
58
|
+
processFile(processJson, options, status, join(filePath, file));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
status.all += 1;
|
|
63
|
+
// or just process file
|
|
64
|
+
processFile(processJson, options, status, filePath);
|
|
65
|
+
// TODO print results to stdout
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (!options.isSilent) {
|
|
69
|
+
const icon = status.ok.length === status.all ? OK : ERR;
|
|
70
|
+
console.log(`${icon} Tests status: ${status.ok.length}/${status.all}`);
|
|
71
|
+
}
|
|
72
|
+
if (status.fail.length) {
|
|
73
|
+
console.error("Failures:");
|
|
74
|
+
for (const e of status.fail) {
|
|
75
|
+
console.error(`❗ ${e.filePath} (${e.name})`);
|
|
76
|
+
}
|
|
77
|
+
process.exit(-1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function readFromStdin(processJson, options) {
|
|
81
|
+
process.stdin.setEncoding("utf8");
|
|
82
|
+
process.stderr.write("awaiting input\n");
|
|
83
|
+
options.isSilent = true;
|
|
84
|
+
// Read from stdin
|
|
85
|
+
let buffer = "";
|
|
86
|
+
process.stdin.on("data", (data) => {
|
|
87
|
+
buffer += data;
|
|
88
|
+
if (buffer.endsWith("\n\n")) {
|
|
89
|
+
const json = JSON.parse(buffer);
|
|
90
|
+
const out = processJson(json, options, "-");
|
|
91
|
+
// clear previous buffer
|
|
92
|
+
buffer = "";
|
|
93
|
+
console.log(JSON.stringify(out));
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function processFile(processJson, options, status, filePath) {
|
|
99
|
+
let jsonData;
|
|
100
|
+
try {
|
|
101
|
+
// Resolve the full file path
|
|
102
|
+
const absolutePath = resolve(filePath);
|
|
103
|
+
// Read the file synchronously
|
|
104
|
+
const fileContent = readFileSync(absolutePath, "utf-8");
|
|
105
|
+
// Parse the JSON content
|
|
106
|
+
jsonData = JSON.parse(fileContent);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
status.fail.push({ filePath, name: "<unknown>" });
|
|
110
|
+
console.error(`Error reading file: ${filePath}`);
|
|
111
|
+
console.error(error.message);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
// Process the parsed JSON
|
|
116
|
+
const result = processJson(jsonData, options, filePath);
|
|
117
|
+
status.ok.push({ filePath, name: jsonData.name ?? filePath });
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
status.fail.push({ filePath, name: jsonData.name ?? filePath });
|
|
122
|
+
console.error(`Error running test: ${filePath}`);
|
|
123
|
+
console.error(error.message);
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function read(data, field, defaultValue = undefined) {
|
|
128
|
+
if (field in data) {
|
|
129
|
+
return data[field];
|
|
130
|
+
}
|
|
131
|
+
if (defaultValue !== undefined) {
|
|
132
|
+
return defaultValue;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Required field ${field} missing in ${JSON.stringify(data, null, 2)}`);
|
|
135
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const NO_OF_REGISTERS = 13;
|
|
2
|
+
// Access.Write = 2 from assembly/memory-page.ts
|
|
3
|
+
const ACCESS_WRITE = 2;
|
|
4
|
+
export const STATUS = {
|
|
5
|
+
OK: 255,
|
|
6
|
+
HALT: 0,
|
|
7
|
+
PANIC: 1,
|
|
8
|
+
FAULT: 2,
|
|
9
|
+
HOST: 3,
|
|
10
|
+
OOG: 4,
|
|
11
|
+
};
|
|
12
|
+
export const ARGS_SEGMENT_START = 0xfeff0000;
|
|
13
|
+
const LINE_PATTERN = /(program\s+0x|memwrite\s+0x|start\s+pc=|ecalli=\d+|memread\s+0x|setreg\s+r\d+|setgas\s+<-|HALT\s+pc=|OOG\s+pc=|PANIC=)/;
|
|
14
|
+
export function parseTrace(input) {
|
|
15
|
+
const lines = input.split(/\r?\n/);
|
|
16
|
+
let program = null;
|
|
17
|
+
const initialMemWrites = [];
|
|
18
|
+
let start = null;
|
|
19
|
+
const ecalliEntries = [];
|
|
20
|
+
let currentEntry = null;
|
|
21
|
+
let termination = null;
|
|
22
|
+
for (const rawLine of lines) {
|
|
23
|
+
const line = extractPayload(rawLine);
|
|
24
|
+
if (!line) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (line.startsWith("program ")) {
|
|
28
|
+
program = parseHexBytes(line.replace("program ", "").trim());
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (line.startsWith("memwrite ")) {
|
|
32
|
+
const memwrite = parseMemWrite(line);
|
|
33
|
+
if (currentEntry) {
|
|
34
|
+
currentEntry.memWrites.push(memwrite);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
initialMemWrites.push(memwrite);
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (line.startsWith("start ")) {
|
|
42
|
+
start = parseStart(line);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (line.startsWith("ecalli=")) {
|
|
46
|
+
const entry = parseEcalli(line);
|
|
47
|
+
ecalliEntries.push(entry);
|
|
48
|
+
currentEntry = entry;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (line.startsWith("memread ")) {
|
|
52
|
+
if (!currentEntry) {
|
|
53
|
+
throw new Error(`memread without active ecalli: ${line}`);
|
|
54
|
+
}
|
|
55
|
+
currentEntry.memReads.push(parseMemRead(line));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (line.startsWith("setreg ")) {
|
|
59
|
+
if (!currentEntry) {
|
|
60
|
+
throw new Error(`setreg without active ecalli: ${line}`);
|
|
61
|
+
}
|
|
62
|
+
const setReg = parseSetReg(line);
|
|
63
|
+
currentEntry.setRegs.push(setReg);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (line.startsWith("setgas ")) {
|
|
67
|
+
if (!currentEntry) {
|
|
68
|
+
throw new Error(`setgas without active ecalli: ${line}`);
|
|
69
|
+
}
|
|
70
|
+
currentEntry.setGas = parseSetGas(line);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (line.startsWith("HALT ") || line.startsWith("OOG ") || line.startsWith("PANIC=")) {
|
|
74
|
+
termination = parseTermination(line);
|
|
75
|
+
currentEntry = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!program) {
|
|
79
|
+
throw new Error("Missing program line in trace");
|
|
80
|
+
}
|
|
81
|
+
if (!start) {
|
|
82
|
+
throw new Error("Missing start line in trace");
|
|
83
|
+
}
|
|
84
|
+
if (!termination) {
|
|
85
|
+
throw new Error("Missing termination line in trace");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
program,
|
|
89
|
+
initialMemWrites,
|
|
90
|
+
start,
|
|
91
|
+
ecalliEntries,
|
|
92
|
+
termination,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function extractPayload(line) {
|
|
96
|
+
const match = LINE_PATTERN.exec(line);
|
|
97
|
+
if (!match || match.index === undefined) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return line.slice(match.index).trim();
|
|
101
|
+
}
|
|
102
|
+
function parseStart(line) {
|
|
103
|
+
const match = /^start pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
104
|
+
if (!match) {
|
|
105
|
+
throw new Error(`Invalid start line: ${line}`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
pc: parseInt(match[1], 10),
|
|
109
|
+
gas: BigInt(match[2]),
|
|
110
|
+
registers: parseRegisterDump(match[3]),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function parseEcalli(line) {
|
|
114
|
+
const match = /^ecalli=(\d+) pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
115
|
+
if (!match) {
|
|
116
|
+
throw new Error(`Invalid ecalli line: ${line}`);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
index: parseInt(match[1], 10),
|
|
120
|
+
pc: parseInt(match[2], 10),
|
|
121
|
+
gas: BigInt(match[3]),
|
|
122
|
+
registers: parseRegisterDump(match[4]),
|
|
123
|
+
memReads: [],
|
|
124
|
+
memWrites: [],
|
|
125
|
+
setRegs: [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function parseMemWrite(line) {
|
|
129
|
+
const match = /^memwrite\s+0x([0-9a-f]+)\s+len=(\d+)\s+<-\s+0x([0-9a-f]+)$/i.exec(line);
|
|
130
|
+
if (!match) {
|
|
131
|
+
throw new Error(`Invalid memwrite line: ${line}`);
|
|
132
|
+
}
|
|
133
|
+
const address = parseInt(match[1], 16);
|
|
134
|
+
const data = parseHexBytes(`0x${match[3]}`);
|
|
135
|
+
const len = parseInt(match[2], 10);
|
|
136
|
+
if (data.length !== len) {
|
|
137
|
+
throw new Error(`memwrite length mismatch: expected ${len}, got ${data.length}`);
|
|
138
|
+
}
|
|
139
|
+
return { address, data };
|
|
140
|
+
}
|
|
141
|
+
function parseMemRead(line) {
|
|
142
|
+
const match = /^memread\s+0x([0-9a-f]+)\s+len=(\d+)\s+->\s+0x([0-9a-f]+)$/i.exec(line);
|
|
143
|
+
if (!match) {
|
|
144
|
+
throw new Error(`Invalid memread line: ${line}`);
|
|
145
|
+
}
|
|
146
|
+
const address = parseInt(match[1], 16);
|
|
147
|
+
const data = parseHexBytes(`0x${match[3]}`);
|
|
148
|
+
const len = parseInt(match[2], 10);
|
|
149
|
+
if (data.length !== len) {
|
|
150
|
+
throw new Error(`memread length mismatch: expected ${len}, got ${data.length}`);
|
|
151
|
+
}
|
|
152
|
+
return { address, data };
|
|
153
|
+
}
|
|
154
|
+
function parseSetReg(line) {
|
|
155
|
+
const match = /^setreg\s+r(\d+)\s+<-\s+0x([0-9a-f]+)$/i.exec(line);
|
|
156
|
+
if (!match) {
|
|
157
|
+
throw new Error(`Invalid setreg line: ${line}`);
|
|
158
|
+
}
|
|
159
|
+
const index = parseInt(match[1], 10);
|
|
160
|
+
if (index < 0 || index >= NO_OF_REGISTERS) {
|
|
161
|
+
throw new Error(`Register index out of range: ${index}`);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
index,
|
|
165
|
+
value: BigInt(`0x${match[2]}`),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function parseSetGas(line) {
|
|
169
|
+
const match = /^setgas\s+<-\s+(\d+)$/i.exec(line);
|
|
170
|
+
if (!match) {
|
|
171
|
+
throw new Error(`Invalid setgas line: ${line}`);
|
|
172
|
+
}
|
|
173
|
+
return BigInt(match[1]);
|
|
174
|
+
}
|
|
175
|
+
function parseTermination(line) {
|
|
176
|
+
if (line.startsWith("HALT ")) {
|
|
177
|
+
const match = /^HALT pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
178
|
+
if (!match) {
|
|
179
|
+
throw new Error(`Invalid HALT line: ${line}`);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
type: "HALT",
|
|
183
|
+
pc: parseInt(match[1], 10),
|
|
184
|
+
gas: BigInt(match[2]),
|
|
185
|
+
registers: parseRegisterDump(match[3]),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (line.startsWith("OOG ")) {
|
|
189
|
+
const match = /^OOG pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
190
|
+
if (!match) {
|
|
191
|
+
throw new Error(`Invalid OOG line: ${line}`);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
type: "OOG",
|
|
195
|
+
pc: parseInt(match[1], 10),
|
|
196
|
+
gas: BigInt(match[2]),
|
|
197
|
+
registers: parseRegisterDump(match[3]),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (line.startsWith("PANIC=")) {
|
|
201
|
+
const match = /^PANIC=([^\s]+) pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
202
|
+
if (!match) {
|
|
203
|
+
throw new Error(`Invalid PANIC line: ${line}`);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
type: "PANIC",
|
|
207
|
+
pc: parseInt(match[2], 10),
|
|
208
|
+
gas: BigInt(match[3]),
|
|
209
|
+
registers: parseRegisterDump(match[4]),
|
|
210
|
+
panicArg: parseNumber(match[1]),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`Unknown termination line: ${line}`);
|
|
214
|
+
}
|
|
215
|
+
function parseRegisterDump(input) {
|
|
216
|
+
const dump = new Map();
|
|
217
|
+
const regex = /r(\d+)=0x([0-9a-f]+)/gi;
|
|
218
|
+
for (const match of input.matchAll(regex)) {
|
|
219
|
+
const index = parseInt(match[1], 10);
|
|
220
|
+
if (index < 0 || index >= NO_OF_REGISTERS) {
|
|
221
|
+
throw new Error(`Register index out of range: ${index}`);
|
|
222
|
+
}
|
|
223
|
+
const value = BigInt(`0x${match[2]}`);
|
|
224
|
+
dump.set(index, value);
|
|
225
|
+
}
|
|
226
|
+
return dump;
|
|
227
|
+
}
|
|
228
|
+
function parseHexBytes(hex) {
|
|
229
|
+
if (!hex.startsWith("0x")) {
|
|
230
|
+
throw new Error(`Hex value must start with 0x: ${hex}`);
|
|
231
|
+
}
|
|
232
|
+
const data = hex.slice(2);
|
|
233
|
+
if (data.length % 2 !== 0) {
|
|
234
|
+
throw new Error(`Hex value must have even length: ${hex}`);
|
|
235
|
+
}
|
|
236
|
+
const bytes = new Uint8Array(data.length / 2);
|
|
237
|
+
for (let i = 0; i < data.length; i += 2) {
|
|
238
|
+
const pair = data.slice(i, i + 2);
|
|
239
|
+
if (!/^[0-9a-fA-F]{2}$/.test(pair)) {
|
|
240
|
+
throw new Error(`Invalid hex pair "${pair}" in hex value: ${hex}`);
|
|
241
|
+
}
|
|
242
|
+
bytes[i / 2] = Number.parseInt(pair, 16);
|
|
243
|
+
}
|
|
244
|
+
return bytes;
|
|
245
|
+
}
|
|
246
|
+
export function encodeRegistersFromDump(dump) {
|
|
247
|
+
const registers = new Array(NO_OF_REGISTERS).fill(0n);
|
|
248
|
+
for (const [index, value] of dump) {
|
|
249
|
+
registers[index] = value;
|
|
250
|
+
}
|
|
251
|
+
return registers;
|
|
252
|
+
}
|
|
253
|
+
export function buildInitialPages(memWrites) {
|
|
254
|
+
return memWrites
|
|
255
|
+
.filter((write) => write.data.length > 0)
|
|
256
|
+
.map((write) => ({
|
|
257
|
+
address: write.address,
|
|
258
|
+
length: write.data.length,
|
|
259
|
+
access: ACCESS_WRITE,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
export function buildInitialChunks(memWrites) {
|
|
263
|
+
return memWrites
|
|
264
|
+
.filter((write) => write.data.length > 0)
|
|
265
|
+
.map((write) => ({
|
|
266
|
+
address: write.address,
|
|
267
|
+
data: Array.from(write.data),
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
export function isSpiTrace(start, memWrites) {
|
|
271
|
+
const r07 = start.registers.get(7);
|
|
272
|
+
if (r07 === BigInt(ARGS_SEGMENT_START)) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
return memWrites.some((write) => write.address === ARGS_SEGMENT_START);
|
|
276
|
+
}
|
|
277
|
+
export function extractSpiArgs(start, memWrites) {
|
|
278
|
+
const argLenBig = start.registers.get(8) ?? 0n;
|
|
279
|
+
// Validate bounds: must be non-negative and <= 1 MiB (2^20)
|
|
280
|
+
const MAX_ARG_LEN = 1n << 20n;
|
|
281
|
+
if (argLenBig < 0n || argLenBig > MAX_ARG_LEN) {
|
|
282
|
+
return new Uint8Array(0);
|
|
283
|
+
}
|
|
284
|
+
const argLen = Number(argLenBig);
|
|
285
|
+
if (argLen <= 0) {
|
|
286
|
+
return new Uint8Array(0);
|
|
287
|
+
}
|
|
288
|
+
const buffer = new Uint8Array(argLen);
|
|
289
|
+
for (const write of memWrites) {
|
|
290
|
+
if (write.address < ARGS_SEGMENT_START) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const offset = write.address - ARGS_SEGMENT_START;
|
|
294
|
+
if (offset >= buffer.length) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
buffer.set(write.data.subarray(0, buffer.length - offset), offset);
|
|
298
|
+
}
|
|
299
|
+
return buffer;
|
|
300
|
+
}
|
|
301
|
+
export function statusToTermination(status) {
|
|
302
|
+
if (status === STATUS.HALT) {
|
|
303
|
+
return "HALT";
|
|
304
|
+
}
|
|
305
|
+
if (status === STATUS.OOG) {
|
|
306
|
+
return "OOG";
|
|
307
|
+
}
|
|
308
|
+
return "PANIC";
|
|
309
|
+
}
|
|
310
|
+
function parseNumber(value) {
|
|
311
|
+
if (value.startsWith("0x")) {
|
|
312
|
+
return Number.parseInt(value, 16);
|
|
313
|
+
}
|
|
314
|
+
return Number.parseInt(value, 10);
|
|
315
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { InputKind, prepareProgram, pvmDestroy, pvmReadMemory, pvmResume, pvmSetRegisters, pvmStart, pvmWriteMemory, } from "../../build/release.js";
|
|
3
|
+
import { LOG_HOST_CALL_INDEX, printLogHostCall } from "./log-host-call.js";
|
|
4
|
+
import { ARGS_SEGMENT_START, buildInitialChunks, buildInitialPages, encodeRegistersFromDump, extractSpiArgs, isSpiTrace, parseTrace, STATUS, statusToTermination, } from "./trace-parse.js";
|
|
5
|
+
import { ConsoleTracer } from "./tracer.js";
|
|
6
|
+
import { hexEncode } from "./utils.js";
|
|
7
|
+
export function replayTraceFile(filePath, options) {
|
|
8
|
+
const input = readFileSync(filePath, "utf8");
|
|
9
|
+
const trace = parseTrace(input);
|
|
10
|
+
const { program, initialMemWrites, start, ecalliEntries, termination } = trace;
|
|
11
|
+
const hasMetadata = options.hasMetadata;
|
|
12
|
+
const useSpi = isSpiTrace(start, initialMemWrites);
|
|
13
|
+
const programInput = Array.from(program);
|
|
14
|
+
const spiArgs = Array.from(extractSpiArgs(start, initialMemWrites));
|
|
15
|
+
const preparedProgram = useSpi
|
|
16
|
+
? prepareProgram(InputKind.SPI, hasMetadata, programInput, [], [], [], spiArgs)
|
|
17
|
+
: prepareProgram(InputKind.Generic, hasMetadata, programInput, encodeRegistersFromDump(start.registers), buildInitialPages(initialMemWrites), buildInitialChunks(initialMemWrites), []);
|
|
18
|
+
const id = pvmStart(preparedProgram, true);
|
|
19
|
+
const initialEcalliCount = ecalliEntries.length;
|
|
20
|
+
const tracer = options.tracer ?? new ConsoleTracer();
|
|
21
|
+
try {
|
|
22
|
+
let gas = start.gas;
|
|
23
|
+
let pc = start.pc;
|
|
24
|
+
// Print start line
|
|
25
|
+
tracer.start(pc, gas, start.registers);
|
|
26
|
+
if (spiArgs.length > 0) {
|
|
27
|
+
tracer.spiArgs(ARGS_SEGMENT_START, spiArgs);
|
|
28
|
+
}
|
|
29
|
+
for (;;) {
|
|
30
|
+
const pause = pvmResume(id, gas, pc, options.logs);
|
|
31
|
+
if (!pause) {
|
|
32
|
+
throw new Error("pvmResume returned null");
|
|
33
|
+
}
|
|
34
|
+
if (pause.status === STATUS.HOST) {
|
|
35
|
+
const expectedEcalli = ecalliEntries.shift();
|
|
36
|
+
if (!expectedEcalli) {
|
|
37
|
+
throw new Error("Unexpected host call");
|
|
38
|
+
}
|
|
39
|
+
// Print ecalli line
|
|
40
|
+
tracer.ecalli(expectedEcalli.index, pause.pc, pause.gas, pause.registers);
|
|
41
|
+
// Print log message for JIP-1 log host call
|
|
42
|
+
if (pause.exitCode === LOG_HOST_CALL_INDEX && options.logHostCall) {
|
|
43
|
+
printLogHostCall(id, pause.registers);
|
|
44
|
+
}
|
|
45
|
+
if (options.verify) {
|
|
46
|
+
assertEq(pause.exitCode, expectedEcalli.index, "ecalli index");
|
|
47
|
+
assertEq(pause.pc, expectedEcalli.pc, "ecalli pc");
|
|
48
|
+
assertEq(pause.gas, expectedEcalli.gas, "ecalli gas");
|
|
49
|
+
assertRegisters(pause.registers, expectedEcalli.registers);
|
|
50
|
+
}
|
|
51
|
+
// Print and verify memreads
|
|
52
|
+
for (const read of expectedEcalli.memReads) {
|
|
53
|
+
tracer.memread(read.address, read.data);
|
|
54
|
+
if (options.verify) {
|
|
55
|
+
const actualData = pvmReadMemory(id, read.address, read.data.length);
|
|
56
|
+
if (!actualData) {
|
|
57
|
+
throw new Error(`Failed to read memory at 0x${read.address.toString(16)}`);
|
|
58
|
+
}
|
|
59
|
+
assertMemEq(actualData, read.data, `memread at 0x${read.address.toString(16)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Apply memory writes
|
|
63
|
+
for (const write of expectedEcalli.memWrites) {
|
|
64
|
+
tracer.memwrite(write.address, write.data);
|
|
65
|
+
const written = pvmWriteMemory(id, write.address, write.data);
|
|
66
|
+
if (!written) {
|
|
67
|
+
throw new Error(`Failed to write memory at 0x${write.address.toString(16)} for PVM ${id}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Apply register writes
|
|
71
|
+
const regs = pause.registers;
|
|
72
|
+
for (const setReg of expectedEcalli.setRegs) {
|
|
73
|
+
tracer.setreg(setReg.index, setReg.value);
|
|
74
|
+
regs[setReg.index] = setReg.value;
|
|
75
|
+
}
|
|
76
|
+
pvmSetRegisters(id, regs);
|
|
77
|
+
// Update gas
|
|
78
|
+
if (expectedEcalli.setGas !== undefined) {
|
|
79
|
+
tracer.setgas(expectedEcalli.setGas);
|
|
80
|
+
gas = expectedEcalli.setGas;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
gas = pause.gas;
|
|
84
|
+
}
|
|
85
|
+
// Advance PC
|
|
86
|
+
pc = pause.nextPc;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Termination
|
|
90
|
+
const type = statusToTermination(pause.status);
|
|
91
|
+
tracer.termination(type, pause.exitCode, pause.pc, pause.gas, pause.registers);
|
|
92
|
+
if (options.verify) {
|
|
93
|
+
assertEq(ecalliEntries.length, 0, "more host calls expected!");
|
|
94
|
+
assertEq(type, termination.type, "termination type");
|
|
95
|
+
assertEq(pause.pc, termination.pc, "termination pc");
|
|
96
|
+
assertEq(pause.gas, termination.gas, "termination gas");
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
ecalliCount: initialEcalliCount,
|
|
104
|
+
termination: trace.termination,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
// after we are done, make sure to release resources
|
|
109
|
+
pvmDestroy(id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function assertEq(actual, expected, label) {
|
|
113
|
+
if (actual !== expected) {
|
|
114
|
+
throw new Error(`\nMismatch ${label}:\n${expected} (expected)\n${actual} (got)`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function assertRegisters(actual, expected) {
|
|
118
|
+
for (let i = 0; i < actual.length; i++) {
|
|
119
|
+
const actualValue = actual[i];
|
|
120
|
+
const expectedValue = expected.get(i) ?? 0n;
|
|
121
|
+
if (actualValue !== expectedValue) {
|
|
122
|
+
throw new Error(`\nRegister mismatch r${i}:\n0x${expectedValue.toString(16)} (expected)\n0x${actualValue.toString(16)} (got)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function assertMemEq(actual, expected, label) {
|
|
127
|
+
const actualString = hexEncode(actual);
|
|
128
|
+
const expectedString = hexEncode(expected);
|
|
129
|
+
assertEq(actualString, expectedString, label);
|
|
130
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { hexEncode } from "./utils.js";
|
|
2
|
+
export class ConsoleTracer {
|
|
3
|
+
start(pc, gas, registers) {
|
|
4
|
+
console.log(`start pc=${pc} gas=${gas} ${formatRegisters(registers)}`);
|
|
5
|
+
}
|
|
6
|
+
spiArgs(address, data) {
|
|
7
|
+
console.log(` memwrite ${address} len=${data.length} <- ${hexEncode(data)}`);
|
|
8
|
+
}
|
|
9
|
+
ecalli(index, pc, gas, registers) {
|
|
10
|
+
console.log(`\necalli=${index} pc=${pc} gas=${gas} ${formatRegisters(registers)}`);
|
|
11
|
+
}
|
|
12
|
+
memread(address, data) {
|
|
13
|
+
console.log(` memread 0x${address.toString(16)} len=${data.length} -> ${formatHex(data)}`);
|
|
14
|
+
}
|
|
15
|
+
memwrite(address, data) {
|
|
16
|
+
console.log(` memwrite 0x${address.toString(16)} len=${data.length} <- ${formatHex(data)}`);
|
|
17
|
+
}
|
|
18
|
+
setreg(index, value) {
|
|
19
|
+
console.log(` setreg r${index.toString().padStart(2, "0")} <- 0x${value.toString(16)}`);
|
|
20
|
+
}
|
|
21
|
+
setgas(gas) {
|
|
22
|
+
console.log(` setgas <- ${gas}`);
|
|
23
|
+
}
|
|
24
|
+
termination(type, exitCode, pc, gas, registers) {
|
|
25
|
+
let termLine = `\n------\n${type}`;
|
|
26
|
+
if (type === "PANIC" && exitCode !== 0) {
|
|
27
|
+
termLine += `=${exitCode}`;
|
|
28
|
+
}
|
|
29
|
+
termLine += ` pc=${pc} gas=${gas} ${formatRegisters(registers)}`;
|
|
30
|
+
console.log(termLine);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class NoOpTracer {
|
|
34
|
+
start() { }
|
|
35
|
+
spiArgs() { }
|
|
36
|
+
ecalli() { }
|
|
37
|
+
memread() { }
|
|
38
|
+
memwrite() { }
|
|
39
|
+
setreg() { }
|
|
40
|
+
setgas() { }
|
|
41
|
+
termination() { }
|
|
42
|
+
}
|
|
43
|
+
function formatRegisters(registers) {
|
|
44
|
+
const entries = [];
|
|
45
|
+
if (Array.isArray(registers)) {
|
|
46
|
+
registers.forEach((val, idx) => {
|
|
47
|
+
if (val !== 0n)
|
|
48
|
+
entries.push({ idx, val });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
for (const [idx, val] of registers) {
|
|
53
|
+
if (val !== 0n)
|
|
54
|
+
entries.push({ idx, val });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return entries
|
|
58
|
+
.sort((a, b) => a.idx - b.idx)
|
|
59
|
+
.map((e) => `r${e.idx}=0x${e.val.toString(16)}`)
|
|
60
|
+
.join(" ");
|
|
61
|
+
}
|
|
62
|
+
function formatHex(data) {
|
|
63
|
+
return `0x${Buffer.from(data).toString("hex")}`;
|
|
64
|
+
}
|