From e271b28bb6ea4813c90661dfe4d473ecaad6cf22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 11:11:41 +0000 Subject: [PATCH 01/14] Per block gas accounting --- assembly/api-debugger.ts | 21 +++++++++++++++++-- assembly/api-internal.ts | 5 +++-- assembly/api-types.ts | 1 + assembly/api-utils.ts | 6 ++++-- assembly/interpreter.ts | 22 ++++++++++++++++---- assembly/program.ts | 45 +++++++++++++++++++++++++++++++++++++++- bench/run.ts | 7 ++++++- bin/src/trace-replay.ts | 3 ++- 8 files changed, 97 insertions(+), 13 deletions(-) diff --git a/assembly/api-debugger.ts b/assembly/api-debugger.ts index d63705a..d8fa237 100644 --- a/assembly/api-debugger.ts +++ b/assembly/api-debugger.ts @@ -11,13 +11,21 @@ import { decodeSpi } from "./spi"; let interpreter: Interpreter | null = null; -export function resetJAM(program: u8[], pc: u32, initialGas: Gas, args: u8[], hasMetadata: boolean = false): void { +export function resetJAM( + program: u8[], + pc: u32, + initialGas: Gas, + args: u8[], + hasMetadata: boolean = false, + useBlockGas: boolean = false, +): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); const p = decodeSpi(code, liftBytes(args), 128); const int = new Interpreter(p.program, p.registers, p.memory); int.nextPc = pc; int.gas.set(initialGas); + int.useBlockGas = useBlockGas; if (interpreter !== null) { (interpreter).memory.free(); @@ -26,7 +34,13 @@ export function resetJAM(program: u8[], pc: u32, initialGas: Gas, args: u8[], ha interpreter = int; } -export function resetGeneric(program: u8[], flatRegisters: u8[], initialGas: Gas, hasMetadata: boolean = false): void { +export function resetGeneric( + program: u8[], + flatRegisters: u8[], + initialGas: Gas, + hasMetadata: boolean = false, + useBlockGas: boolean = false, +): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); const p = deblob(code); @@ -34,6 +48,7 @@ export function resetGeneric(program: u8[], flatRegisters: u8[], initialGas: Gas fillRegisters(registers, flatRegisters); const int = new Interpreter(p, registers); int.gas.set(initialGas); + int.useBlockGas = useBlockGas; if (interpreter !== null) { (interpreter).memory.free(); @@ -49,6 +64,7 @@ export function resetGenericWithMemory( chunks: Uint8Array, initialGas: Gas, hasMetadata: boolean = false, + useBlockGas: boolean = false, ): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); @@ -61,6 +77,7 @@ export function resetGenericWithMemory( const int = new Interpreter(p, registers, memory); int.gas.set(initialGas); + int.useBlockGas = useBlockGas; interpreter = int; } diff --git a/assembly/api-internal.ts b/assembly/api-internal.ts index d53bb6a..e68c73f 100644 --- a/assembly/api-internal.ts +++ b/assembly/api-internal.ts @@ -71,9 +71,10 @@ export function buildMemory(builder: MemoryBuilder, pages: InitialPage[], chunks } /** Initialize new VM for execution. */ -export function vmInit(input: VmInput, useSbrkGas: boolean = false): Interpreter { +export function vmInit(input: VmInput, useSbrkGas: boolean = false, useBlockGas: boolean = false): Interpreter { const int = new Interpreter(input.program, input.registers, input.memory); int.useSbrkGas = useSbrkGas; + int.useBlockGas = useBlockGas; int.nextPc = input.pc; int.gas.set(input.gas); return int; @@ -81,7 +82,7 @@ export function vmInit(input: VmInput, useSbrkGas: boolean = false): Interpreter /** Initialize & run & destroy a VM in a single go. */ export function vmRunOnce(input: VmInput, options: VmRunOptions): VmOutput { - const int = vmInit(input, options.useSbrkGas); + const int = vmInit(input, options.useSbrkGas, options.useBlockGas); vmExecute(int, options.logs); return vmDestroy(int, options.dumpMemory); } diff --git a/assembly/api-types.ts b/assembly/api-types.ts index db23745..81dfa17 100644 --- a/assembly/api-types.ts +++ b/assembly/api-types.ts @@ -19,6 +19,7 @@ export class InitialChunk { export class VmRunOptions { useSbrkGas: boolean = false; + useBlockGas: boolean = false; logs: boolean = false; dumpMemory: boolean = false; } diff --git a/assembly/api-utils.ts b/assembly/api-utils.ts index 32b5eae..0f7be78 100644 --- a/assembly/api-utils.ts +++ b/assembly/api-utils.ts @@ -103,6 +103,7 @@ export function runProgram( logs: boolean = false, useSbrkGas: boolean = false, dumpMemory: boolean = false, + useBlockGas: boolean = false, ): VmOutput { const vmInput = new VmInput(program.program, program.memory, program.registers); vmInput.gas = i64(initialGas); @@ -111,6 +112,7 @@ export function runProgram( const vmOptions = new VmRunOptions(); vmOptions.logs = logs; vmOptions.useSbrkGas = useSbrkGas; + vmOptions.useBlockGas = useBlockGas; vmOptions.dumpMemory = dumpMemory; return vmRunOnce(vmInput, vmOptions); @@ -126,11 +128,11 @@ const pvms = new Map(); * * NOTE: the PVM MUST be de-allocated using `pvmDestroy`. */ -export function pvmStart(program: StandardProgram, useSbrkGas: boolean = false): u32 { +export function pvmStart(program: StandardProgram, useSbrkGas: boolean = false, useBlockGas: boolean = false): u32 { const vmInput = new VmInput(program.program, program.memory, program.registers); nextPvmId += 1; - pvms.set(nextPvmId, vmInit(vmInput, useSbrkGas)); + pvms.set(nextPvmId, vmInit(vmInput, useSbrkGas, useBlockGas)); return nextPvmId; } diff --git a/assembly/interpreter.ts b/assembly/interpreter.ts index dad1e41..44e3c8c 100644 --- a/assembly/interpreter.ts +++ b/assembly/interpreter.ts @@ -48,6 +48,7 @@ export class Interpreter { public exitCode: u32; public nextPc: u32; public useSbrkGas: boolean; + public useBlockGas: boolean; private djumpRes: DjumpResult = new DjumpResult(); private argsRes: Args = new Args(); @@ -64,6 +65,7 @@ export class Interpreter { this.exitCode = 0; this.nextPc = 0; this.useSbrkGas = false; + this.useBlockGas = false; } nextSteps(nSteps: u32 = 1): boolean { @@ -93,6 +95,8 @@ export class Interpreter { const mask = program.mask; const argsRes = this.argsRes; const outcomeRes = this.outcomeRes; + const useBlockGas = this.useBlockGas; + const blockGasCosts = program.blockGasCosts; for (let i: u32 = 0; i < nSteps; i++) { // reset some stuff at start @@ -115,10 +119,20 @@ export class Interpreter { const instruction = unchecked(code[pc]); const iData = instruction < INSTRUCTIONS.length ? unchecked(INSTRUCTIONS[instruction]) : MISSING_INSTRUCTION; - // check gas (might be done for each block instead). - if (this.gas.sub(iData.gas)) { - this.status = Status.OOG; - return false; + // check gas: per-block or per-instruction + if (useBlockGas) { + const blockGas = portable.staticArrayAt(blockGasCosts, pc); + if (blockGas > 0) { + if (this.gas.sub(blockGas)) { + this.status = Status.OOG; + return false; + } + } + } else { + if (this.gas.sub(iData.gas)) { + this.status = Status.OOG; + return false; + } } if (iData === MISSING_INSTRUCTION) { diff --git a/assembly/program.ts b/assembly/program.ts index 5f47f56..428bb45 100644 --- a/assembly/program.ts +++ b/assembly/program.ts @@ -229,12 +229,55 @@ export class JumpTable { } export class Program { + /** + * Pre-computed gas cost per basic block, indexed by PC. + * At block-start PCs, stores the total gas cost for the entire block; 0 elsewhere. + */ + public readonly blockGasCosts: StaticArray; + constructor( public readonly code: u8[], public readonly mask: Mask, public readonly jumpTable: JumpTable, public readonly basicBlocks: BasicBlocks, - ) {} + ) { + const len = code.length; + const blockGasCosts = new StaticArray(len); + + // Two-pass approach: first accumulate gas per block, then store at block-start PCs + let blockStartPc: i32 = -1; + let blockGas: u64 = u64(0); + + for (let i = 0; i < len; i++) { + if (!mask.isInstruction(i)) { + continue; + } + + const instruction = code[i]; + const iData = instruction < INSTRUCTIONS.length ? INSTRUCTIONS[instruction] : MISSING_INSTRUCTION; + + if (basicBlocks.isStart(i)) { + // Store previous block's gas + if (blockStartPc >= 0) { + blockGasCosts[blockStartPc] = blockGas; + } + blockStartPc = i; + blockGas = iData.gas; + } else { + blockGas = portable.u64_add(blockGas, iData.gas); + } + + // Skip argument bytes + i += mask.skipBytesToNextInstruction(i); + } + + // Store the final block's gas + if (blockStartPc >= 0) { + blockGasCosts[blockStartPc] = blockGas; + } + + this.blockGasCosts = blockGasCosts; + } toString(): string { return `Program { code: ${this.code}, mask: ${this.mask}, jumpTable: ${this.jumpTable}, basicBlocks: ${this.basicBlocks} }`; diff --git a/bench/run.ts b/bench/run.ts index 09f6daf..2d3c627 100644 --- a/bench/run.ts +++ b/bench/run.ts @@ -28,6 +28,7 @@ const { values } = parseArgs({ iterations: { type: "string", default: "5" }, warmup: { type: "string", default: "1" }, output: { type: "string", default: "" }, + "block-gas": { type: "boolean", default: false }, help: { type: "boolean", short: "h", default: false }, }, }); @@ -116,6 +117,9 @@ function benchRun(name: string, fn: () => void): BenchResult { const max = Math.max(...samples); const p95 = percentile(samples, 95); + return { name, medianMs: med, minMs: min, maxMs: max, p95Ms: p95, samples }; +} + function formatResult(r: BenchResult): string { return ` ${r.name.padEnd(40)} median=${r.medianMs.toFixed(1)}ms min=${r.minMs.toFixed(1)}ms max=${r.maxMs.toFixed(1)}ms p95=${r.p95Ms.toFixed(1)}ms`; } @@ -145,6 +149,7 @@ function benchTraces(dir: string): BenchResult[] { hasMetadata: HasMetadata.Yes, verify: false, tracer: new NoOpTracer(), + useBlockGas: values["block-gas"], }); }); @@ -216,7 +221,7 @@ function benchW3f(dir: string): BenchResult | null { [], 16, ); - runProgram(exe, gas, pc, false, false, false); + runProgram(exe, gas, pc, false, false, false, values["block-gas"]); } }); diff --git a/bin/src/trace-replay.ts b/bin/src/trace-replay.ts index 4150209..be40f01 100644 --- a/bin/src/trace-replay.ts +++ b/bin/src/trace-replay.ts @@ -32,6 +32,7 @@ type ReplayOptions = { verify: boolean; logHostCall?: boolean; tracer?: Tracer; + useBlockGas?: boolean; }; export function replayTraceFile(filePath: string, options: ReplayOptions): TraceSummary { @@ -58,7 +59,7 @@ export function replayTraceFile(filePath: string, options: ReplayOptions): Trace 128, ); - const id = pvmStart(preparedProgram, true); + const id = pvmStart(preparedProgram, true, options.useBlockGas ?? false); const initialEcalliCount = ecalliEntries.length; const tracer = options.tracer ?? new ConsoleTracer(); From b5eb9eb6b6022eb8e36c32cc1594d20fd4ee8aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 16:36:55 +0000 Subject: [PATCH 02/14] fix formatting --- bench/compare.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bench/compare.ts b/bench/compare.ts index 11a4a1d..86d7147 100644 --- a/bench/compare.ts +++ b/bench/compare.ts @@ -176,6 +176,7 @@ if (baselineSuite.summary.totalTraceMedianMs === 0) { } else { totalDiffPercent = (totalDiff / baselineSuite.summary.totalTraceMedianMs) * 100; } +console.log( `Difference: ${totalDiff >= 0 ? "+" : ""}${totalDiff.toFixed(1)}ms (${totalDiffPercent.toFixed(2)}%)` ); @@ -190,6 +191,7 @@ if (baselineSuite.w3f && resultsSuite.w3f) { } else { w3fDiffPercent = (w3fDiff / baselineSuite.w3f.medianMs) * 100; } + console.log( `\nW3F suite: ${baselineSuite.w3f.medianMs.toFixed(1)}ms -> ${resultsSuite.w3f.medianMs.toFixed(1)}ms` ); console.log( From 20408cfe1e4d15ef7d2ef7fa3860c8ac3bcc65ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 17:02:52 +0000 Subject: [PATCH 03/14] don't suppress baseline bench output --- .github/workflows/build.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62fd7d8..5a1c26c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,12 +75,8 @@ jobs: cd ./baseline npm ci npm run build - if npm run bench:baseline 2>/dev/null; then - cp ./bench/baseline.json ../baseline.json - else - echo "bench:baseline script not found on base branch, skipping comparison" - echo '{}' > ../baseline.json - fi + npm run bench:baseline + cp ./bench/baseline.json ../baseline.json # Run benchmarks on current PR - name: Run PR benchmark run: | From f6114287e0c02f054c87e2b5d284768f055baed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 17:34:51 +0000 Subject: [PATCH 04/14] Add bench scripts to biome --- bench/compare.ts | 61 ++++++++++++++++++------------------------------ bench/run.ts | 51 +++++++++++++--------------------------- biome.jsonc | 2 +- 3 files changed, 40 insertions(+), 74 deletions(-) diff --git a/bench/compare.ts b/bench/compare.ts index 86d7147..f49a43e 100644 --- a/bench/compare.ts +++ b/bench/compare.ts @@ -1,18 +1,18 @@ #!/usr/bin/env node /** * Benchmark Comparison Tool - * + * * Compares two benchmark result JSON files and reports regressions/improvements. - * + * * Usage: * tsx bench/compare.ts [--threshold ] - * + * * Options: * --threshold Regression threshold as percentage (default: 5%) * --verbose Show detailed per-trace comparison */ -import { readFileSync, existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { parseArgs } from "node:util"; @@ -36,7 +36,7 @@ Options: } const [baselinePath, resultsPath] = positionals; -const threshold = parseFloat(values.threshold!); +const threshold = parseFloat(values.threshold ?? "5"); // Validate threshold if (!Number.isFinite(threshold) || threshold < 0) { @@ -81,12 +81,8 @@ const baselineSuite = baseline as SuiteResult; const resultsSuite = results as SuiteResult; // Build maps for easy lookup -const baselineTraces = new Map( - baselineSuite.traces.map((t) => [t.name, t]) -); -const resultsTraces = new Map( - resultsSuite.traces.map((t) => [t.name, t]) -); +const baselineTraces = new Map(baselineSuite.traces.map((t) => [t.name, t])); +const resultsTraces = new Map(resultsSuite.traces.map((t) => [t.name, t])); interface Comparison { name: string; @@ -98,8 +94,8 @@ interface Comparison { } const comparisons: Comparison[] = []; -let regressions: Comparison[] = []; -let improvements: Comparison[] = []; +const regressions: Comparison[] = []; +const improvements: Comparison[] = []; // Compare traces for (const [name, baselineTrace] of baselineTraces) { @@ -110,11 +106,11 @@ for (const [name, baselineTrace] of baselineTraces) { } const diffMs = currentTrace.medianMs - baselineTrace.medianMs; - + // Guard against division by zero let diffPercent: number; if (baselineTrace.medianMs === 0) { - diffPercent = diffMs === 0 ? 0 : (diffMs > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY); + diffPercent = diffMs === 0 ? 0 : diffMs > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; } else { diffPercent = (diffMs / baselineTrace.medianMs) * 100; } @@ -163,40 +159,33 @@ console.log(`Threshold: ${threshold}%\n`); console.log("--- Summary ---\n"); console.log( - `Total trace time: ${baselineSuite.summary.totalTraceMedianMs.toFixed(1)}ms -> ${resultsSuite.summary.totalTraceMedianMs.toFixed(1)}ms` + `Total trace time: ${baselineSuite.summary.totalTraceMedianMs.toFixed(1)}ms -> ${resultsSuite.summary.totalTraceMedianMs.toFixed(1)}ms`, ); -const totalDiff = - resultsSuite.summary.totalTraceMedianMs - - baselineSuite.summary.totalTraceMedianMs; +const totalDiff = resultsSuite.summary.totalTraceMedianMs - baselineSuite.summary.totalTraceMedianMs; // Guard against division by zero let totalDiffPercent: number; if (baselineSuite.summary.totalTraceMedianMs === 0) { - totalDiffPercent = totalDiff === 0 ? 0 : (totalDiff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY); + totalDiffPercent = totalDiff === 0 ? 0 : totalDiff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; } else { totalDiffPercent = (totalDiff / baselineSuite.summary.totalTraceMedianMs) * 100; } -console.log( - `Difference: ${totalDiff >= 0 ? "+" : ""}${totalDiff.toFixed(1)}ms (${totalDiffPercent.toFixed(2)}%)` -); +console.log(`Difference: ${totalDiff >= 0 ? "+" : ""}${totalDiff.toFixed(1)}ms (${totalDiffPercent.toFixed(2)}%)`); if (baselineSuite.w3f && resultsSuite.w3f) { - const w3fDiff = - resultsSuite.w3f.medianMs - baselineSuite.w3f.medianMs; - + const w3fDiff = resultsSuite.w3f.medianMs - baselineSuite.w3f.medianMs; + // Guard against division by zero let w3fDiffPercent: number; if (baselineSuite.w3f.medianMs === 0) { - w3fDiffPercent = w3fDiff === 0 ? 0 : (w3fDiff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY); + w3fDiffPercent = w3fDiff === 0 ? 0 : w3fDiff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; } else { w3fDiffPercent = (w3fDiff / baselineSuite.w3f.medianMs) * 100; } console.log( - `\nW3F suite: ${baselineSuite.w3f.medianMs.toFixed(1)}ms -> ${resultsSuite.w3f.medianMs.toFixed(1)}ms` - ); - console.log( - `Difference: ${w3fDiff >= 0 ? "+" : ""}${w3fDiff.toFixed(1)}ms (${w3fDiffPercent.toFixed(2)}%)` + `\nW3F suite: ${baselineSuite.w3f.medianMs.toFixed(1)}ms -> ${resultsSuite.w3f.medianMs.toFixed(1)}ms`, ); + console.log(`Difference: ${w3fDiff >= 0 ? "+" : ""}${w3fDiff.toFixed(1)}ms (${w3fDiffPercent.toFixed(2)}%)`); } console.log(`\nRegressions: ${regressions.length}`); @@ -205,18 +194,14 @@ console.log(`Improvements: ${improvements.length}`); if (regressions.length > 0) { console.log("\n--- Regressions (worst first) ---\n"); for (const r of regressions) { - console.log( - ` ${r.name.padEnd(40)} ${r.currentMs.toFixed(1).padStart(8)}ms (+${r.diffPercent.toFixed(1)}%)` - ); + console.log(` ${r.name.padEnd(40)} ${r.currentMs.toFixed(1).padStart(8)}ms (+${r.diffPercent.toFixed(1)}%)`); } } if (improvements.length > 0) { console.log("\n--- Improvements ---\n"); for (const i of improvements) { - console.log( - ` ${i.name.padEnd(40)} ${i.currentMs.toFixed(1).padStart(8)}ms (${i.diffPercent.toFixed(1)}%)` - ); + console.log(` ${i.name.padEnd(40)} ${i.currentMs.toFixed(1).padStart(8)}ms (${i.diffPercent.toFixed(1)}%)`); } } @@ -227,7 +212,7 @@ if (values.verbose && comparisons.length > 0) { const sign = c.diffMs >= 0 ? "+" : ""; const marker = c.status === "regression" ? "⚠️" : c.status === "improvement" ? "✓" : " "; console.log( - ` ${marker} ${c.name.padEnd(40)} ${c.baselineMs.toFixed(1).padStart(7)}ms -> ${c.currentMs.toFixed(1).padStart(7)}ms (${sign}${c.diffPercent.toFixed(1)}%)` + ` ${marker} ${c.name.padEnd(40)} ${c.baselineMs.toFixed(1).padStart(7)}ms -> ${c.currentMs.toFixed(1).padStart(7)}ms (${sign}${c.diffPercent.toFixed(1)}%)`, ); } } diff --git a/bench/run.ts b/bench/run.ts index 2d3c627..2b3ae43 100644 --- a/bench/run.ts +++ b/bench/run.ts @@ -9,15 +9,15 @@ * tsx bench/run.ts [--traces ] [--w3f ] [--iterations ] [--warmup ] */ -import { readdirSync, existsSync, writeFileSync, readFileSync } from "node:fs"; -import { join, basename, resolve } from "node:path"; -import { parseArgs } from "node:util"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { basename, join, resolve } from "node:path"; import { performance } from "node:perf_hooks"; +import { parseArgs } from "node:util"; import "json-bigint-patch"; import { replayTraceFile } from "../bin/src/trace-replay.js"; -import { HasMetadata, InputKind, prepareProgram, runProgram } from "../build/release.js"; import { NoOpTracer } from "../bin/src/tracer.js"; +import { HasMetadata, InputKind, prepareProgram, runProgram } from "../build/release.js"; // ---- CLI ---- @@ -46,8 +46,8 @@ Options: process.exit(0); } -const ITERATIONS = parseInt(values.iterations!, 10); -const WARMUP = parseInt(values.warmup!, 10); +const ITERATIONS = parseInt(values.iterations ?? "5", 10); +const WARMUP = parseInt(values.warmup ?? "1", 10); // Validate iterations and warmup if (!Number.isInteger(ITERATIONS) || ITERATIONS < 1) { @@ -55,7 +55,9 @@ if (!Number.isInteger(ITERATIONS) || ITERATIONS < 1) { process.exit(1); } if (!Number.isInteger(WARMUP) || WARMUP < 0 || WARMUP >= ITERATIONS) { - console.error(`Error: Invalid warmup value: ${values.warmup}. Must be an integer >= 0 and < iterations (${ITERATIONS}).`); + console.error( + `Error: Invalid warmup value: ${values.warmup}. Must be an integer >= 0 and < iterations (${ITERATIONS}).`, + ); process.exit(1); } // ---- Types ---- @@ -211,16 +213,7 @@ function benchW3f(dir: string): BenchResult | null { const gas = BigInt(data["initial-gas"] || 10000); const pc = data["initial-pc"] || 0; - const exe = prepareProgram( - InputKind.Generic, - HasMetadata.No, - data.program, - registers, - pageMap, - memory, - [], - 16, - ); + const exe = prepareProgram(InputKind.Generic, HasMetadata.No, data.program, registers, pageMap, memory, [], 16); runProgram(exe, gas, pc, false, false, false, values["block-gas"]); } }); @@ -245,15 +238,11 @@ function main() { }; // Trace benchmarks - const traceDirs = [ - values.traces, - "../anan-as2/bench/traces", - "./bench/traces", - ].filter(Boolean); + const traceDirs = [values.traces, "../anan-as2/bench/traces", "./bench/traces"].filter(Boolean) as string[]; let traceDir: string | null = null; for (const d of traceDirs) { - const resolved = resolve(d!); + const resolved = resolve(d); if (existsSync(resolved)) { traceDir = resolved; break; @@ -263,26 +252,18 @@ function main() { if (traceDir) { console.log(`Trace replays (${traceDir}):`); suiteResult.traces = benchTraces(traceDir); - suiteResult.summary.totalTraceMedianMs = suiteResult.traces.reduce( - (sum, r) => sum + r.medianMs, - 0, - ); - console.log( - `\n TOTAL trace median: ${suiteResult.summary.totalTraceMedianMs.toFixed(1)}ms\n`, - ); + suiteResult.summary.totalTraceMedianMs = suiteResult.traces.reduce((sum, r) => sum + r.medianMs, 0); + console.log(`\n TOTAL trace median: ${suiteResult.summary.totalTraceMedianMs.toFixed(1)}ms\n`); } else { console.log("No trace directory found. Skipping trace benchmarks."); } // W3F benchmarks - const w3fDirs = [ - values.w3f, - "./test/gas-cost-tests", - ].filter(Boolean); + const w3fDirs = [values.w3f, "./test/gas-cost-tests"].filter(Boolean) as string[]; let w3fDir: string | null = null; for (const d of w3fDirs) { - const resolved = resolve(d!); + const resolved = resolve(d); if (existsSync(resolved)) { w3fDir = resolved; break; diff --git a/biome.jsonc b/biome.jsonc index 180f233..815cda5 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -43,7 +43,7 @@ } }, "files": { - "includes": ["**/assembly/**/*", "**/bin/**/*", "**/test/**/*", "!dist", "!web/build"] + "includes": ["**/assembly/**/*", "**/bin/**/*", "**/test/**/*", "**/bench/**/*", "!dist", "!web/build"] }, "overrides": [ { From 75e59555fa5bef64dc4220c3740df3d8d9fdf498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 17:44:51 +0000 Subject: [PATCH 05/14] Fix baseline location. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a1c26c..50456cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,7 +76,7 @@ jobs: npm ci npm run build npm run bench:baseline - cp ./bench/baseline.json ../baseline.json + cp ./bench/baseline.json ../../baseline.json # Run benchmarks on current PR - name: Run PR benchmark run: | From fd9967f25a598d204e8295e7abb68a4a760490ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 17:54:43 +0000 Subject: [PATCH 06/14] Post benchmark comments --- .github/workflows/build.yml | 18 +++++++- bench/compare.ts | 89 ++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50456cc..7621a39 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,8 @@ jobs: runs-on: ubuntu-latest # Only run benchmarks on PRs, not on main branch pushes if: github.event_name == 'pull_request' + permissions: + pull-requests: write steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ env.NODE_VERSION }} @@ -82,7 +84,19 @@ jobs: run: | npm run bench:ci cp ./bench/results.json ../results.json - # Compare results + # Compare results and post PR comment - name: Compare benchmarks run: | - npm run bench:compare ../baseline.json ../results.json -- --threshold 5 + npm run bench:compare ../baseline.json ../results.json -- --threshold 100 --markdown ../bench-comment.md + - name: Post benchmark comment + if: always() && hashFiles('../bench-comment.md') != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + # Delete previous benchmark comment if it exists + COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \ + --jq '.[] | select(.body | startswith("## Benchmark Results")) | .id' | head -1) + if [ -n "$COMMENT_ID" ]; then + gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID -X DELETE + fi + gh pr comment ${{ github.event.pull_request.number }} --body-file ../bench-comment.md diff --git a/bench/compare.ts b/bench/compare.ts index f49a43e..56e8ec5 100644 --- a/bench/compare.ts +++ b/bench/compare.ts @@ -12,7 +12,7 @@ * --verbose Show detailed per-trace comparison */ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { parseArgs } from "node:util"; @@ -20,6 +20,7 @@ const { values, positionals } = parseArgs({ options: { threshold: { type: "string", short: "t", default: "5" }, verbose: { type: "boolean", short: "v", default: false }, + markdown: { type: "string", short: "m", default: "" }, help: { type: "boolean", short: "h", default: false }, }, allowPositionals: true, @@ -217,6 +218,22 @@ if (values.verbose && comparisons.length > 0) { } } +// Write markdown report +if (values.markdown) { + const md = formatMarkdown( + baselineSuite, + resultsSuite, + comparisons, + regressions, + improvements, + totalDiff, + totalDiffPercent, + threshold, + ); + writeFileSync(resolve(values.markdown), md); + console.log(`\nMarkdown report written to ${values.markdown}`); +} + // Exit code if (regressions.length > 0) { console.log("\n❌ FAILED: Regressions detected above threshold\n"); @@ -228,3 +245,73 @@ if (regressions.length > 0) { console.log("\n✅ PASSED: No regressions or improvements beyond threshold\n"); process.exit(0); } + +function formatMarkdown( + base: SuiteResult, + current: SuiteResult, + all: Comparison[], + regs: Comparison[], + imps: Comparison[], + totalDiffMs: number, + totalDiffPct: number, + thresh: number, +): string { + const sign = (n: number) => (n >= 0 ? "+" : ""); + const status = + regs.length > 0 + ? `### :warning: ${regs.length} regression(s) detected (>${thresh}% threshold)` + : imps.length > 0 + ? "### :white_check_mark: No regressions (improvements detected)" + : "### :white_check_mark: No significant changes"; + + let md = "## Benchmark Results\n\n"; + md += `${status}\n\n`; + + // Summary table + md += "| Metric | Baseline | Current | Change |\n"; + md += "|--------|----------|---------|--------|\n"; + md += `| **Trace total** | ${base.summary.totalTraceMedianMs.toFixed(1)}ms | ${current.summary.totalTraceMedianMs.toFixed(1)}ms | ${sign(totalDiffMs)}${totalDiffMs.toFixed(1)}ms (${sign(totalDiffPct)}${totalDiffPct.toFixed(1)}%) |\n`; + + if (base.w3f && current.w3f) { + const w3fDiff = current.w3f.medianMs - base.w3f.medianMs; + const w3fPct = base.w3f.medianMs === 0 ? 0 : (w3fDiff / base.w3f.medianMs) * 100; + md += `| **W3F suite** | ${base.w3f.medianMs.toFixed(1)}ms | ${current.w3f.medianMs.toFixed(1)}ms | ${sign(w3fDiff)}${w3fDiff.toFixed(1)}ms (${sign(w3fPct)}${w3fPct.toFixed(1)}%) |\n`; + } + + // Regressions + if (regs.length > 0) { + md += "\n
Regressions (worst first)\n\n"; + md += "| Trace | Baseline | Current | Change |\n"; + md += "|-------|----------|---------|--------|\n"; + for (const r of regs) { + md += `| ${r.name} | ${r.baselineMs.toFixed(1)}ms | ${r.currentMs.toFixed(1)}ms | +${r.diffPercent.toFixed(1)}% |\n`; + } + md += "\n
\n"; + } + + // Improvements + if (imps.length > 0) { + md += "\n
Improvements\n\n"; + md += "| Trace | Baseline | Current | Change |\n"; + md += "|-------|----------|---------|--------|\n"; + for (const i of imps) { + md += `| ${i.name} | ${i.baselineMs.toFixed(1)}ms | ${i.currentMs.toFixed(1)}ms | ${i.diffPercent.toFixed(1)}% |\n`; + } + md += "\n
\n"; + } + + // All traces + if (all.length > 0) { + const sorted = [...all].sort((a, b) => Math.abs(b.diffPercent) - Math.abs(a.diffPercent)); + md += "\n
All traces\n\n"; + md += "| Trace | Baseline | Current | Change |\n"; + md += "|-------|----------|---------|--------|\n"; + for (const c of sorted) { + const icon = c.status === "regression" ? ":warning:" : c.status === "improvement" ? ":white_check_mark:" : ""; + md += `| ${icon} ${c.name} | ${c.baselineMs.toFixed(1)}ms | ${c.currentMs.toFixed(1)}ms | ${sign(c.diffPercent)}${c.diffPercent.toFixed(1)}% |\n`; + } + md += "\n
\n"; + } + + return md; +} From 2efeff7aa9ed825956a2a2a7be077e596b820a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 18:05:27 +0000 Subject: [PATCH 07/14] Fix comment location --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7621a39..36ac2d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,9 +87,9 @@ jobs: # Compare results and post PR comment - name: Compare benchmarks run: | - npm run bench:compare ../baseline.json ../results.json -- --threshold 100 --markdown ../bench-comment.md + npm run bench:compare ../baseline.json ../results.json -- --threshold 100 --markdown ./bench-comment.md - name: Post benchmark comment - if: always() && hashFiles('../bench-comment.md') != '' + if: always() && hashFiles('bench-comment.md') != '' env: GH_TOKEN: ${{ github.token }} run: | @@ -99,4 +99,4 @@ jobs: if [ -n "$COMMENT_ID" ]; then gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID -X DELETE fi - gh pr comment ${{ github.event.pull_request.number }} --body-file ../bench-comment.md + gh pr comment ${{ github.event.pull_request.number }} --body-file ./bench-comment.md From b7ec583e6736f5194c7cfd212175fa2b674d6c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 21:24:07 +0000 Subject: [PATCH 08/14] Fix performance issues --- assembly/api-debugger.ts | 12 +++++++++--- assembly/api-internal.ts | 4 +++- assembly/interpreter.ts | 28 ++++++++++----------------- assembly/program.ts | 41 ++++++++++++++++++++++++++++++++++------ 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/assembly/api-debugger.ts b/assembly/api-debugger.ts index d8fa237..d2abae1 100644 --- a/assembly/api-debugger.ts +++ b/assembly/api-debugger.ts @@ -25,7 +25,9 @@ export function resetJAM( const int = new Interpreter(p.program, p.registers, p.memory); int.nextPc = pc; int.gas.set(initialGas); - int.useBlockGas = useBlockGas; + if (useBlockGas) { + int.gasCosts = int.program.getBlockGasCosts(); + } if (interpreter !== null) { (interpreter).memory.free(); @@ -48,7 +50,9 @@ export function resetGeneric( fillRegisters(registers, flatRegisters); const int = new Interpreter(p, registers); int.gas.set(initialGas); - int.useBlockGas = useBlockGas; + if (useBlockGas) { + int.gasCosts = int.program.getBlockGasCosts(); + } if (interpreter !== null) { (interpreter).memory.free(); @@ -77,7 +81,9 @@ export function resetGenericWithMemory( const int = new Interpreter(p, registers, memory); int.gas.set(initialGas); - int.useBlockGas = useBlockGas; + if (useBlockGas) { + int.gasCosts = int.program.getBlockGasCosts(); + } interpreter = int; } diff --git a/assembly/api-internal.ts b/assembly/api-internal.ts index e68c73f..42f7e29 100644 --- a/assembly/api-internal.ts +++ b/assembly/api-internal.ts @@ -74,7 +74,9 @@ export function buildMemory(builder: MemoryBuilder, pages: InitialPage[], chunks export function vmInit(input: VmInput, useSbrkGas: boolean = false, useBlockGas: boolean = false): Interpreter { const int = new Interpreter(input.program, input.registers, input.memory); int.useSbrkGas = useSbrkGas; - int.useBlockGas = useBlockGas; + if (useBlockGas) { + int.gasCosts = input.program.getBlockGasCosts(); + } int.nextPc = input.pc; int.gas.set(input.gas); return int; diff --git a/assembly/interpreter.ts b/assembly/interpreter.ts index 44e3c8c..281ce60 100644 --- a/assembly/interpreter.ts +++ b/assembly/interpreter.ts @@ -48,7 +48,8 @@ export class Interpreter { public exitCode: u32; public nextPc: u32; public useSbrkGas: boolean; - public useBlockGas: boolean; + /** Gas costs table used for per-instruction gas deduction. */ + public gasCosts: StaticArray; private djumpRes: DjumpResult = new DjumpResult(); private argsRes: Args = new Args(); @@ -65,7 +66,7 @@ export class Interpreter { this.exitCode = 0; this.nextPc = 0; this.useSbrkGas = false; - this.useBlockGas = false; + this.gasCosts = program.gasCosts; } nextSteps(nSteps: u32 = 1): boolean { @@ -95,8 +96,7 @@ export class Interpreter { const mask = program.mask; const argsRes = this.argsRes; const outcomeRes = this.outcomeRes; - const useBlockGas = this.useBlockGas; - const blockGasCosts = program.blockGasCosts; + const gasCosts = this.gasCosts; for (let i: u32 = 0; i < nSteps; i++) { // reset some stuff at start @@ -119,20 +119,11 @@ export class Interpreter { const instruction = unchecked(code[pc]); const iData = instruction < INSTRUCTIONS.length ? unchecked(INSTRUCTIONS[instruction]) : MISSING_INSTRUCTION; - // check gas: per-block or per-instruction - if (useBlockGas) { - const blockGas = portable.staticArrayAt(blockGasCosts, pc); - if (blockGas > 0) { - if (this.gas.sub(blockGas)) { - this.status = Status.OOG; - return false; - } - } - } else { - if (this.gas.sub(iData.gas)) { - this.status = Status.OOG; - return false; - } + // check gas via pre-computed cost table (per-instruction or per-block) + const gasCost = portable.staticArrayAt(gasCosts, pc); + if (gasCost > 0 && this.gas.sub(gasCost)) { + this.status = Status.OOG; + return false; } if (iData === MISSING_INSTRUCTION) { @@ -231,6 +222,7 @@ export class Interpreter { return true; } + } function branch(r: BranchResult, basicBlocks: BasicBlocks, pc: u32, offset: i32): BranchResult { diff --git a/assembly/program.ts b/assembly/program.ts index 428bb45..4ec24fe 100644 --- a/assembly/program.ts +++ b/assembly/program.ts @@ -229,11 +229,18 @@ export class JumpTable { } export class Program { + /** + * Pre-computed gas cost per instruction, indexed by PC. + * Each instruction PC has its individual gas cost; 0 for non-instruction bytes. + */ + public readonly gasCosts: StaticArray; + /** * Pre-computed gas cost per basic block, indexed by PC. * At block-start PCs, stores the total gas cost for the entire block; 0 elsewhere. + * Lazily computed on first access via `getBlockGasCosts()`. */ - public readonly blockGasCosts: StaticArray; + private _blockGasCosts: StaticArray | null = null; constructor( public readonly code: u8[], @@ -241,10 +248,34 @@ export class Program { public readonly jumpTable: JumpTable, public readonly basicBlocks: BasicBlocks, ) { + const len = code.length; + const gasCosts = new StaticArray(len); + + for (let i = 0; i < len; i++) { + if (!mask.isInstruction(i)) { + continue; + } + const instruction = code[i]; + const iData = instruction < INSTRUCTIONS.length ? INSTRUCTIONS[instruction] : MISSING_INSTRUCTION; + gasCosts[i] = iData.gas; + i += mask.skipBytesToNextInstruction(i); + } + + this.gasCosts = gasCosts; + } + + /** Lazily compute and return block-aggregated gas costs. */ + getBlockGasCosts(): StaticArray { + if (this._blockGasCosts !== null) { + return this._blockGasCosts!; + } + + const code = this.code; + const mask = this.mask; + const basicBlocks = this.basicBlocks; const len = code.length; const blockGasCosts = new StaticArray(len); - // Two-pass approach: first accumulate gas per block, then store at block-start PCs let blockStartPc: i32 = -1; let blockGas: u64 = u64(0); @@ -257,7 +288,6 @@ export class Program { const iData = instruction < INSTRUCTIONS.length ? INSTRUCTIONS[instruction] : MISSING_INSTRUCTION; if (basicBlocks.isStart(i)) { - // Store previous block's gas if (blockStartPc >= 0) { blockGasCosts[blockStartPc] = blockGas; } @@ -267,16 +297,15 @@ export class Program { blockGas = portable.u64_add(blockGas, iData.gas); } - // Skip argument bytes i += mask.skipBytesToNextInstruction(i); } - // Store the final block's gas if (blockStartPc >= 0) { blockGasCosts[blockStartPc] = blockGas; } - this.blockGasCosts = blockGasCosts; + this._blockGasCosts = blockGasCosts; + return blockGasCosts; } toString(): string { From 01e8fa89036af60dbdc8e82ce7e74dafc2276d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 23:13:45 +0000 Subject: [PATCH 09/14] Rewrite. --- assembly/api-debugger.ts | 15 +--- assembly/api-internal.ts | 8 +-- assembly/api-types.ts | 2 - assembly/api-utils.ts | 40 +++++++---- assembly/gas-costs.ts | 47 ------------- assembly/index-compiler.ts | 4 +- assembly/instructions.ts | 4 +- assembly/interpreter.ts | 40 +++-------- assembly/program.test.ts | 33 ++++++++- assembly/program.ts | 136 ++++++++++++++++--------------------- assembly/spi.ts | 9 ++- bin/index.ts | 15 +++- bin/src/fuzz.ts | 2 +- bin/src/test-json.ts | 9 +-- bin/src/trace-replay.ts | 10 +-- test/test-gas-cost.ts | 5 +- test/test-w3f-common.ts | 3 +- 17 files changed, 169 insertions(+), 213 deletions(-) delete mode 100644 assembly/gas-costs.ts diff --git a/assembly/api-debugger.ts b/assembly/api-debugger.ts index d2abae1..b1326ec 100644 --- a/assembly/api-debugger.ts +++ b/assembly/api-debugger.ts @@ -21,13 +21,10 @@ export function resetJAM( ): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); - const p = decodeSpi(code, liftBytes(args), 128); + const p = decodeSpi(code, liftBytes(args), 128, useBlockGas); const int = new Interpreter(p.program, p.registers, p.memory); int.nextPc = pc; int.gas.set(initialGas); - if (useBlockGas) { - int.gasCosts = int.program.getBlockGasCosts(); - } if (interpreter !== null) { (interpreter).memory.free(); @@ -45,14 +42,11 @@ export function resetGeneric( ): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); - const p = deblob(code); + const p = deblob(code, useBlockGas); const registers: Registers = newRegisters(); fillRegisters(registers, flatRegisters); const int = new Interpreter(p, registers); int.gas.set(initialGas); - if (useBlockGas) { - int.gasCosts = int.program.getBlockGasCosts(); - } if (interpreter !== null) { (interpreter).memory.free(); @@ -72,7 +66,7 @@ export function resetGenericWithMemory( ): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); - const p = deblob(code); + const p = deblob(code, useBlockGas); const registers: Registers = newRegisters(); fillRegisters(registers, flatRegisters); @@ -81,9 +75,6 @@ export function resetGenericWithMemory( const int = new Interpreter(p, registers, memory); int.gas.set(initialGas); - if (useBlockGas) { - int.gasCosts = int.program.getBlockGasCosts(); - } interpreter = int; } diff --git a/assembly/api-internal.ts b/assembly/api-internal.ts index 42f7e29..f6ea831 100644 --- a/assembly/api-internal.ts +++ b/assembly/api-internal.ts @@ -71,12 +71,8 @@ export function buildMemory(builder: MemoryBuilder, pages: InitialPage[], chunks } /** Initialize new VM for execution. */ -export function vmInit(input: VmInput, useSbrkGas: boolean = false, useBlockGas: boolean = false): Interpreter { +export function vmInit(input: VmInput): Interpreter { const int = new Interpreter(input.program, input.registers, input.memory); - int.useSbrkGas = useSbrkGas; - if (useBlockGas) { - int.gasCosts = input.program.getBlockGasCosts(); - } int.nextPc = input.pc; int.gas.set(input.gas); return int; @@ -84,7 +80,7 @@ export function vmInit(input: VmInput, useSbrkGas: boolean = false, useBlockGas: /** Initialize & run & destroy a VM in a single go. */ export function vmRunOnce(input: VmInput, options: VmRunOptions): VmOutput { - const int = vmInit(input, options.useSbrkGas, options.useBlockGas); + const int = vmInit(input); vmExecute(int, options.logs); return vmDestroy(int, options.dumpMemory); } diff --git a/assembly/api-types.ts b/assembly/api-types.ts index 81dfa17..4b30aba 100644 --- a/assembly/api-types.ts +++ b/assembly/api-types.ts @@ -18,8 +18,6 @@ export class InitialChunk { } export class VmRunOptions { - useSbrkGas: boolean = false; - useBlockGas: boolean = false; logs: boolean = false; dumpMemory: boolean = false; } diff --git a/assembly/api-utils.ts b/assembly/api-utils.ts index 0f7be78..187073c 100644 --- a/assembly/api-utils.ts +++ b/assembly/api-utils.ts @@ -1,11 +1,9 @@ import { buildMemory, getAssembly, vmDestroy, vmExecute, vmInit, vmRunOnce } from "./api-internal"; import { InitialChunk, InitialPage, VmInput, VmOutput, VmPause, VmRunOptions } from "./api-types"; import { Gas } from "./gas"; -import { BlockGasCost, computeGasCosts } from "./gas-costs"; import { Interpreter } from "./interpreter"; import { MaybePageFault, MemoryBuilder } from "./memory"; -import { portable } from "./portable"; -import { deblob, extractCodeAndMetadata, liftBytes } from "./program"; +import { deblob, extractCodeAndMetadata, liftBytes, ProgramCounter } from "./program"; import { NO_OF_REGISTERS, newRegisters, Registers } from "./registers"; import { decodeSpi, StandardProgram } from "./spi"; @@ -19,15 +17,29 @@ export enum HasMetadata { No = 1, } -export function getGasCosts(input: u8[], kind: InputKind, withMetadata: HasMetadata): BlockGasCost[] { - const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0); +class BlockGasCost { + pc: ProgramCounter = 0; + gas: Gas = 0; +} - // @ts-ignore: AS returns T[], JS returns iterator - asArray handles both - return portable.asArray(computeGasCosts(program.program).values()); +export function getBlockGasCosts(input: u8[], kind: InputKind, withMetadata: HasMetadata): BlockGasCost[] { + const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0, true); + const blockCosts: BlockGasCost[] = []; + const costs = program.program.gasCosts.costs; + for (let n: i32 = 0; n < costs.length; n += 1) { + const gas = costs[n]; + if (gas !== 0) { + const x = new BlockGasCost(); + x.pc = n; + x.gas = costs[n]; + blockCosts.push(x); + } + } + return blockCosts; } export function disassemble(input: u8[], kind: InputKind, withMetadata: HasMetadata): string { - const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0); + const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0, false); let output = ""; if (withMetadata === HasMetadata.Yes) { @@ -56,6 +68,8 @@ export function prepareProgram( args: u8[], /** Preallocate a bunch of memory pages for faster execution. */ preallocateMemoryPages: u32, + /** Compute gas per-block instead of per-instruction. */ + useBlockGas: boolean, ): StandardProgram { let code = liftBytes(program); let metadata = new Uint8Array(0); @@ -69,7 +83,7 @@ export function prepareProgram( } if (kind === InputKind.Generic) { - const program = deblob(code); + const program = deblob(code, useBlockGas); const builder = new MemoryBuilder(preallocateMemoryPages); const memory = buildMemory(builder, initialPageMap, initialMemory); @@ -101,9 +115,7 @@ export function runProgram( initialGas: i64 = 0, programCounter: u32 = 0, logs: boolean = false, - useSbrkGas: boolean = false, dumpMemory: boolean = false, - useBlockGas: boolean = false, ): VmOutput { const vmInput = new VmInput(program.program, program.memory, program.registers); vmInput.gas = i64(initialGas); @@ -111,8 +123,6 @@ export function runProgram( const vmOptions = new VmRunOptions(); vmOptions.logs = logs; - vmOptions.useSbrkGas = useSbrkGas; - vmOptions.useBlockGas = useBlockGas; vmOptions.dumpMemory = dumpMemory; return vmRunOnce(vmInput, vmOptions); @@ -128,11 +138,11 @@ const pvms = new Map(); * * NOTE: the PVM MUST be de-allocated using `pvmDestroy`. */ -export function pvmStart(program: StandardProgram, useSbrkGas: boolean = false, useBlockGas: boolean = false): u32 { +export function pvmStart(program: StandardProgram): u32 { const vmInput = new VmInput(program.program, program.memory, program.registers); nextPvmId += 1; - pvms.set(nextPvmId, vmInit(vmInput, useSbrkGas, useBlockGas)); + pvms.set(nextPvmId, vmInit(vmInput)); return nextPvmId; } diff --git a/assembly/gas-costs.ts b/assembly/gas-costs.ts deleted file mode 100644 index 0ab9298..0000000 --- a/assembly/gas-costs.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; -import { portable } from "./portable"; -import { Program } from "./program"; - -export class BlockGasCost { - pc: u32 = 0; - gas: u64 = u64(0); -} - -export function computeGasCosts(p: Program): Map { - const len = p.code.length; - const blocks: Map = new Map(); - let currentBlock: BlockGasCost | null = null; - - for (let i = 0; i < len; i++) { - if (!p.mask.isInstruction(i)) { - throw new Error("We should iterate only over instructions!"); - } - - const instruction = p.code[i]; - const iData = instruction >= INSTRUCTIONS.length ? MISSING_INSTRUCTION : INSTRUCTIONS[instruction]; - - if (p.basicBlocks.isStart(i)) { - if (currentBlock !== null) { - blocks.set(currentBlock.pc, currentBlock); - } - currentBlock = new BlockGasCost(); - currentBlock.pc = i; - } - - if (currentBlock !== null) { - // add gas for current instruction - currentBlock.gas = portable.u64_add(currentBlock.gas, iData.gas); - } - - // move forward - const skipBytes = p.mask.skipBytesToNextInstruction(i); - i += skipBytes; - } - - // add the final block - if (currentBlock !== null) { - blocks.set(currentBlock.pc, currentBlock); - } - - return blocks; -} diff --git a/assembly/index-compiler.ts b/assembly/index-compiler.ts index 47dfa85..c5ace5a 100644 --- a/assembly/index-compiler.ts +++ b/assembly/index-compiler.ts @@ -81,6 +81,7 @@ export function main(argsPtr: u32, argsLen: u32): void { // Parse SPI program and prepare memory layout const preallocateMemoryPages: u32 = 0; + const useBlockGas = true; const program = prepareProgram( InputKind.SPI, HasMetadata.Yes, @@ -90,10 +91,11 @@ export function main(argsPtr: u32, argsLen: u32): void { [], innerArgs, preallocateMemoryPages, + useBlockGas, ); // Run the program - const result = runProgram(program, gas, pc, false, false, false); + const result = runProgram(program, gas, pc, false, false); // Calculate exact result size: 1 (status) + 4 (exitCode) + 8 (gas) + 4 (pc) + ? (result data) const dataLen: u32 = result.result.length; diff --git a/assembly/instructions.ts b/assembly/instructions.ts index 8aa52c6..65553c0 100644 --- a/assembly/instructions.ts +++ b/assembly/instructions.ts @@ -19,8 +19,6 @@ function instruction(name: string, kind: Arguments, gas: Gas, isTerminating: boo export const MISSING_INSTRUCTION = instruction("INVALID", Arguments.Zero, 1, false); -export const SBRK = instruction("SBRK", Arguments.TwoReg, 1); - export const INSTRUCTIONS: Instruction[] = [ /* 000 */ instruction("TRAP", Arguments.Zero, 1, true), /* 001 */ instruction("FALLTHROUGH", Arguments.Zero, 1, true), @@ -133,7 +131,7 @@ export const INSTRUCTIONS: Instruction[] = [ MISSING_INSTRUCTION, /* 100 */ instruction("MOVE_REG", Arguments.TwoReg, 1), - /* 101 */ SBRK, + /* 101 */ instruction("SBRK", Arguments.TwoReg, 1), /* 102 */ instruction("COUNT_SET_BITS_64", Arguments.TwoReg, 1), /* 103 */ instruction("COUNT_SET_BITS_32", Arguments.TwoReg, 1), /* 104 */ instruction("LEADING_ZERO_BITS_64", Arguments.TwoReg, 1), diff --git a/assembly/interpreter.ts b/assembly/interpreter.ts index 281ce60..513a366 100644 --- a/assembly/interpreter.ts +++ b/assembly/interpreter.ts @@ -1,11 +1,10 @@ import { Args } from "./arguments"; import { GasCounter, gasCounter } from "./gas"; -import { INSTRUCTIONS, MISSING_INSTRUCTION, SBRK } from "./instructions"; +import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; import { Outcome, OutcomeData, Result } from "./instructions/outcome"; -import { reg } from "./instructions/utils"; import { RUN } from "./instructions-exe"; import { Memory, MemoryBuilder } from "./memory"; -import { PAGE_SIZE, PAGE_SIZE_SHIFT, RESERVED_MEMORY } from "./memory-page"; +import { RESERVED_MEMORY } from "./memory-page"; import { portable } from "./portable"; import { BasicBlocks, decodeArguments, JumpTable, Program, ProgramCounter } from "./program"; import { Registers } from "./registers"; @@ -47,9 +46,6 @@ export class Interpreter { public status: Status; public exitCode: u32; public nextPc: u32; - public useSbrkGas: boolean; - /** Gas costs table used for per-instruction gas deduction. */ - public gasCosts: StaticArray; private djumpRes: DjumpResult = new DjumpResult(); private argsRes: Args = new Args(); @@ -65,8 +61,6 @@ export class Interpreter { this.status = Status.OK; this.exitCode = 0; this.nextPc = 0; - this.useSbrkGas = false; - this.gasCosts = program.gasCosts; } nextSteps(nSteps: u32 = 1): boolean { @@ -91,12 +85,14 @@ export class Interpreter { return true; } - const program = this.program; - const code = program.code; - const mask = program.mask; + const code = this.program.code; + const mask = this.program.mask; + const gasCosts = this.program.gasCosts.costs; + const basicBlocks = this.program.basicBlocks; + const jumpTable = this.program.jumpTable; + const argsRes = this.argsRes; const outcomeRes = this.outcomeRes; - const gasCosts = this.gasCosts; for (let i: u32 = 0; i < nSteps; i++) { // reset some stuff at start @@ -135,19 +131,6 @@ export class Interpreter { const skipBytes = mask.skipBytesToNextInstruction(pc); const args = decodeArguments(argsRes, iData.kind, code, pc + 1, skipBytes); - // additional gas cost of sbrk - if (iData === SBRK && this.useSbrkGas) { - const alloc = u64(u32(this.registers[reg(u64(args.a))])); - const gas = portable.u64_mul( - portable.u64_sub(portable.u64_add(alloc, u64(PAGE_SIZE)), u64(1)) >> u64(PAGE_SIZE_SHIFT), - u64(16), - ); - if (this.gas.sub(gas)) { - this.status = Status.OOG; - return false; - } - } - const exe = unchecked(RUN[instruction]); const outcome = exe(outcomeRes, args, this.registers, this.memory); @@ -159,7 +142,7 @@ export class Interpreter { switch (outcome.outcome) { case Outcome.StaticJump: { - const branchResult = branch(this.branchRes, program.basicBlocks, pc, outcome.staticJump); + const branchResult = branch(this.branchRes, basicBlocks, pc, outcome.staticJump); if (!branchResult.isOkay) { this.status = Status.PANIC; return false; @@ -169,7 +152,7 @@ export class Interpreter { continue; } case Outcome.DynamicJump: { - const res = dJump(this.djumpRes, program.jumpTable, outcome.dJump); + const res = dJump(this.djumpRes, jumpTable, outcome.dJump); if (res.status === DjumpStatus.HALT) { this.status = Status.HALT; return false; @@ -178,7 +161,7 @@ export class Interpreter { this.status = Status.PANIC; return false; } - const branchResult = branch(this.branchRes, program.basicBlocks, res.newPc, 0); + const branchResult = branch(this.branchRes, basicBlocks, res.newPc, 0); if (!branchResult.isOkay) { this.status = Status.PANIC; return false; @@ -222,7 +205,6 @@ export class Interpreter { return true; } - } function branch(r: BranchResult, basicBlocks: BasicBlocks, pc: u32, offset: i32): BranchResult { diff --git a/assembly/program.test.ts b/assembly/program.test.ts index 8523115..133d1d4 100644 --- a/assembly/program.test.ts +++ b/assembly/program.test.ts @@ -75,7 +75,7 @@ export const TESTS: Test[] = [ 0, 0, 33, 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, 200, 152, 8, 100, 169, 40, 243, 100, 135, 51, 8, 51, 9, 1, 50, 0, 73, 147, 82, 213, 0, ]); - const program = deblob(raw); + const program = deblob(raw, false); const assert = new Assert(); assert.isEqual( program.mask.toString(), @@ -86,6 +86,37 @@ export const TESTS: Test[] = [ program.basicBlocks.toString(), "BasicBlocks[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", ); + assert.isEqual( + program.basicBlocks.toString(), + "BasicBlocks[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", + ); + assert.isEqual( + program.gasCosts.toString(), + "GasCosts[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", + ); + return assert; + }), + + test("should deblob with block gas costs", () => { + const raw = u8arr([ + 0, 0, 33, 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, 200, 152, 8, 100, 169, 40, 243, 100, + 135, 51, 8, 51, 9, 1, 50, 0, 73, 147, 82, 213, 0, + ]); + const program = deblob(raw, true); + const assert = new Assert(); + assert.isEqual( + program.mask.toString(), + "Mask[0, 2, 1, 0, 2, 1, 0, 1, 0, 0, 2, 1, 0, 2, 1, 0, 1, 0, 2, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, ]", + ); + assert.isEqual(program.jumpTable.toString(), "JumpTable[]"); + assert.isEqual( + program.basicBlocks.toString(), + "BasicBlocks[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", + ); + assert.isEqual( + program.gasCosts.toString(), + "GasCosts[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", + ); return assert; }), diff --git a/assembly/program.ts b/assembly/program.ts index 4ec24fe..c9ff81c 100644 --- a/assembly/program.ts +++ b/assembly/program.ts @@ -1,5 +1,6 @@ import { Args, Arguments, DECODERS, REQUIRED_BYTES } from "./arguments"; import { Decoder } from "./codec"; +import { Gas } from "./gas"; import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; import { reg, u32SignExtend } from "./instructions/utils"; import { portable } from "./portable"; @@ -42,7 +43,7 @@ export function lowerBytes(data: Uint8Array): u8[] { } /** https://graypaper.fluffylabs.dev/#/cc517d7/234f01234f01?v=0.6.5 */ -export function deblob(program: Uint8Array): Program { +export function deblob(program: Uint8Array, useBlockGas: boolean): Program { const decoder = new Decoder(program); // number of items in the jump table @@ -64,8 +65,9 @@ export function deblob(program: Uint8Array): Program { const mask = new Mask(rawMask, codeLength); const jumpTable = new JumpTable(jumpTableItemLength, rawJumpTable); const basicBlocks = new BasicBlocks(rawCode, mask); + const gasCosts = new GasCosts(rawCode, mask, basicBlocks, useBlockGas); - return new Program(rawCode, mask, jumpTable, basicBlocks); + return new Program(rawCode, mask, jumpTable, basicBlocks, gasCosts); } /** @@ -131,6 +133,59 @@ export class Mask { } } +export class GasCosts { + readonly costs: StaticArray; + + constructor(code: u8[], mask: Mask, blocks: BasicBlocks, useBlockGasCost: boolean) { + const len = code.length; + const costs = new StaticArray(len); + for (let n: i32 = 0; n < len; n += 1) { + const isInstructionInMask = mask.isInstruction(n); + if (!isInstructionInMask) { + continue; + } + + const skipArgs = mask.skipBytesToNextInstruction(n); + const iData = code[n] >= INSTRUCTIONS.length ? MISSING_INSTRUCTION : INSTRUCTIONS[code[n]]; + costs[n] = iData.gas; + n += skipArgs; + } + + // sum up costs per block + if (useBlockGasCost) { + let previousStart: u32 = 0; + let previousSum: u64 = 0; + for (let n: i32 = 0; n < len; n += 1) { + const current = costs[n]; + costs[n] = 0; + if (blocks.isStart(n)) { + costs[previousStart] = previousSum; + previousSum = current; + previousStart = n; + } else { + previousSum = portable.u64_add(previousSum, current); + } + + n += mask.skipBytesToNextInstruction(n); + } + // final assignment + costs[previousStart] = previousSum; + } + + this.costs = costs; + } + + toString(): string { + let v = "GasCosts["; + for (let i = 0; i < this.costs.length; i += 1) { + if (this.costs[i] !== 0) { + v += `${i} -> ${this.costs[i]}, `; + } + } + return `${v}]`; + } +} + export enum BasicBlock { NONE = 0, START = 2, @@ -229,87 +284,16 @@ export class JumpTable { } export class Program { - /** - * Pre-computed gas cost per instruction, indexed by PC. - * Each instruction PC has its individual gas cost; 0 for non-instruction bytes. - */ - public readonly gasCosts: StaticArray; - - /** - * Pre-computed gas cost per basic block, indexed by PC. - * At block-start PCs, stores the total gas cost for the entire block; 0 elsewhere. - * Lazily computed on first access via `getBlockGasCosts()`. - */ - private _blockGasCosts: StaticArray | null = null; - constructor( public readonly code: u8[], public readonly mask: Mask, public readonly jumpTable: JumpTable, public readonly basicBlocks: BasicBlocks, - ) { - const len = code.length; - const gasCosts = new StaticArray(len); - - for (let i = 0; i < len; i++) { - if (!mask.isInstruction(i)) { - continue; - } - const instruction = code[i]; - const iData = instruction < INSTRUCTIONS.length ? INSTRUCTIONS[instruction] : MISSING_INSTRUCTION; - gasCosts[i] = iData.gas; - i += mask.skipBytesToNextInstruction(i); - } - - this.gasCosts = gasCosts; - } - - /** Lazily compute and return block-aggregated gas costs. */ - getBlockGasCosts(): StaticArray { - if (this._blockGasCosts !== null) { - return this._blockGasCosts!; - } - - const code = this.code; - const mask = this.mask; - const basicBlocks = this.basicBlocks; - const len = code.length; - const blockGasCosts = new StaticArray(len); - - let blockStartPc: i32 = -1; - let blockGas: u64 = u64(0); - - for (let i = 0; i < len; i++) { - if (!mask.isInstruction(i)) { - continue; - } - - const instruction = code[i]; - const iData = instruction < INSTRUCTIONS.length ? INSTRUCTIONS[instruction] : MISSING_INSTRUCTION; - - if (basicBlocks.isStart(i)) { - if (blockStartPc >= 0) { - blockGasCosts[blockStartPc] = blockGas; - } - blockStartPc = i; - blockGas = iData.gas; - } else { - blockGas = portable.u64_add(blockGas, iData.gas); - } - - i += mask.skipBytesToNextInstruction(i); - } - - if (blockStartPc >= 0) { - blockGasCosts[blockStartPc] = blockGas; - } - - this._blockGasCosts = blockGasCosts; - return blockGasCosts; - } + public readonly gasCosts: GasCosts, + ) {} toString(): string { - return `Program { code: ${this.code}, mask: ${this.mask}, jumpTable: ${this.jumpTable}, basicBlocks: ${this.basicBlocks} }`; + return `Program { code: ${this.code}, mask: ${this.mask}, jumpTable: ${this.jumpTable}, basicBlocks: ${this.basicBlocks}, gasCosts: ${this.gasCosts} }`; } } diff --git a/assembly/spi.ts b/assembly/spi.ts index 955a5cc..dc5ac54 100644 --- a/assembly/spi.ts +++ b/assembly/spi.ts @@ -12,7 +12,12 @@ export const ARGS_SEGMENT_START: u32 = 2 ** 32 - SEGMENT_SIZE - MAX_ARGS_LEN; export const STACK_SEGMENT_END: u32 = ARGS_SEGMENT_START - SEGMENT_SIZE; /** https://graypaper.fluffylabs.dev/#/ab2cdbd/2da3002da300?v=0.7.2 */ -export function decodeSpi(data: Uint8Array, args: Uint8Array, preallocateMemoryPages: u32 = 0): StandardProgram { +export function decodeSpi( + data: Uint8Array, + args: Uint8Array, + preallocateMemoryPages: u32 = 0, + useBlockGas: boolean = false, +): StandardProgram { const argsLength = args.length; if (argsLength > MAX_ARGS_LEN) { throw new Error(`Arguments length is too big. Got: ${argsLength}, max: ${MAX_ARGS_LEN}`); @@ -32,7 +37,7 @@ export function decodeSpi(data: Uint8Array, args: Uint8Array, preallocateMemoryP const code = decoder.bytes(codeLength); decoder.finish(); - const program = deblob(code); + const program = deblob(code, useBlockGas); // building memory const builder = new MemoryBuilder(preallocateMemoryPages); diff --git a/bin/index.ts b/bin/index.ts index d7d434d..1bb35c2 100755 --- a/bin/index.ts +++ b/bin/index.ts @@ -192,8 +192,19 @@ function handleRun(args: string[]) { try { const preallocateMemoryPages = 128; - const program = prepareProgram(kind, hasMetadata, programCode, [], [], [], spiArgs, preallocateMemoryPages); - const id = pvmStart(program, false); + const useBlockGas = true; + const program = prepareProgram( + kind, + hasMetadata, + programCode, + [], + [], + [], + spiArgs, + preallocateMemoryPages, + useBlockGas, + ); + const id = pvmStart(program); let gas = initialGas; let pc = initialPc; diff --git a/bin/src/fuzz.ts b/bin/src/fuzz.ts index 5806aff..5ed6ed6 100755 --- a/bin/src/fuzz.ts +++ b/bin/src/fuzz.ts @@ -26,7 +26,7 @@ export function fuzz(data: Uint8Array | number[]) { .join(",") .split(",") .map(() => BigInt(0)); - const exe = prepareProgram(InputKind.Generic, HasMetadata.No, Array.from(program), registers, [], [], [], 0); + const exe = prepareProgram(InputKind.Generic, HasMetadata.No, Array.from(program), registers, [], [], [], 0, false); const output = runProgram(exe, gas, pc, printDebugInfo); const vmRegisters = decodeRegistersFromTypeberry(vm); diff --git a/bin/src/test-json.ts b/bin/src/test-json.ts index 9eadf63..e5d6020 100644 --- a/bin/src/test-json.ts +++ b/bin/src/test-json.ts @@ -16,8 +16,6 @@ export interface TestOptions { isDebug: boolean; /** don't print anything (jsonin-jsonout mode) */ isSilent: boolean; - /** enable sbrk gas */ - useSbrkGas: boolean; } type ProcessJsonFn = (data: T, options: TestOptions, filePath: string) => T; @@ -40,9 +38,6 @@ export function run(processJson: ProcessJsonFn, op if (args[0] === "--debug") { args.shift(); options.isDebug = true; - } else if (args[0] === "--sbrk-gas") { - args.shift(); - options.useSbrkGas = true; } else { break; } @@ -50,8 +45,8 @@ export function run(processJson: ProcessJsonFn, op if (args.length === 0) { console.error("Error: No JSON files provided."); - console.error("Usage: index.js [--debug] [--sbrk-gas] [file2.json ...]"); - console.error("read from stdin: index.js [--debug] [--sbrk-gas] -"); + console.error("Usage: index.js [--debug] [file2.json ...]"); + console.error("read from stdin: index.js [--debug] -"); process.exit(1); } diff --git a/bin/src/trace-replay.ts b/bin/src/trace-replay.ts index be40f01..40a2307 100644 --- a/bin/src/trace-replay.ts +++ b/bin/src/trace-replay.ts @@ -32,7 +32,6 @@ type ReplayOptions = { verify: boolean; logHostCall?: boolean; tracer?: Tracer; - useBlockGas?: boolean; }; export function replayTraceFile(filePath: string, options: ReplayOptions): TraceSummary { @@ -46,8 +45,10 @@ export function replayTraceFile(filePath: string, options: ReplayOptions): Trace const programInput = Array.from(program); const spiArgs = Array.from(extractSpiArgs(start, initialMemWrites)); + const preallocateMemoryPages = 128; + const useBlockGas = false; const preparedProgram = useSpi - ? prepareProgram(InputKind.SPI, hasMetadata, programInput, [], [], [], spiArgs, 128) + ? prepareProgram(InputKind.SPI, hasMetadata, programInput, [], [], [], spiArgs, preallocateMemoryPages, useBlockGas) : prepareProgram( InputKind.Generic, hasMetadata, @@ -56,10 +57,11 @@ export function replayTraceFile(filePath: string, options: ReplayOptions): Trace buildInitialPages(initialMemWrites), buildInitialChunks(initialMemWrites), [], - 128, + preallocateMemoryPages, + useBlockGas, ); - const id = pvmStart(preparedProgram, true, options.useBlockGas ?? false); + const id = pvmStart(preparedProgram); const initialEcalliCount = ecalliEntries.length; const tracer = options.tracer ?? new ConsoleTracer(); diff --git a/test/test-gas-cost.ts b/test/test-gas-cost.ts index dc985f3..a063c79 100755 --- a/test/test-gas-cost.ts +++ b/test/test-gas-cost.ts @@ -3,7 +3,7 @@ import "json-bigint-patch"; import * as assert from "node:assert"; import { ERR, OK, ProcessableData, read, run, TestOptions } from "../bin/src/test-json.js"; -import { disassemble, getGasCosts, HasMetadata, InputKind } from "../build/release.js"; +import { disassemble, getBlockGasCosts, HasMetadata, InputKind } from "../build/release.js"; // Run the CLI application main(); @@ -18,7 +18,6 @@ function main() { const options: TestOptions = { isDebug: false, isSilent: false, - useSbrkGas: false, }; run(processGasCost, options); @@ -41,7 +40,7 @@ function processGasCost(data: GasCostTest, options: TestOptions, filePath: strin console.info("\n^^^^^^^^^^^\n"); } - const result = asMap(getGasCosts(input.program, InputKind.Generic, HasMetadata.No)); + const result = asMap(getBlockGasCosts(input.program, InputKind.Generic, HasMetadata.No)); // silent mode - just put our vals into expected (comparison done externally) if (options.isSilent) { diff --git a/test/test-w3f-common.ts b/test/test-w3f-common.ts index 08476f9..ff7c911 100644 --- a/test/test-w3f-common.ts +++ b/test/test-w3f-common.ts @@ -62,7 +62,6 @@ export function runW3fTests(pvm: PvmModule) { const options: TestOptions = { isDebug: false, isSilent: false, - useSbrkGas: false, }; run((data, opts, filePath) => processW3f(pvm, data, opts, filePath), options); @@ -106,7 +105,7 @@ function processW3f(pvm: PvmModule, data: PvmTest, options: TestOptions, _filePa [], 16, ); - const result = pvm.runProgram(exe, input.gas, input.pc, options.isDebug, options.useSbrkGas, true); + const result = pvm.runProgram(exe, input.gas, input.pc, options.isDebug, true); const status = statusAsString(result.status); // Normalize registers to plain unsigned BigInt array for comparison. From 991fc3116e370b19bde97059b565997cee876b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 28 Feb 2026 23:26:47 +0000 Subject: [PATCH 10/14] fix bugs --- assembly/program.test.ts | 4 ++-- assembly/spi.test.ts | 2 +- bench/run.ts | 4 ++-- bin/src/trace-replay.ts | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/assembly/program.test.ts b/assembly/program.test.ts index 133d1d4..97f6423 100644 --- a/assembly/program.test.ts +++ b/assembly/program.test.ts @@ -92,7 +92,7 @@ export const TESTS: Test[] = [ ); assert.isEqual( program.gasCosts.toString(), - "GasCosts[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", + "GasCosts[0 -> 1, 3 -> 1, 6 -> 1, 8 -> 1, 9 -> 1, 12 -> 1, 15 -> 1, 17 -> 1, 20 -> 1, 22 -> 1, 24 -> 1, 26 -> 1, 28 -> 1, 30 -> 1, 31 -> 1, ]", ); return assert; }), @@ -115,7 +115,7 @@ export const TESTS: Test[] = [ ); assert.isEqual( program.gasCosts.toString(), - "GasCosts[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", + "GasCosts[0 -> 3, 8 -> 1, 9 -> 2, 15 -> 4, 24 -> 4, 31 -> 1, ]", ); return assert; }), diff --git a/assembly/spi.test.ts b/assembly/spi.test.ts index 950bf09..d72e6e0 100644 --- a/assembly/spi.test.ts +++ b/assembly/spi.test.ts @@ -15,7 +15,7 @@ export const TESTS: Test[] = [ const assert = new Assert(); assert.isEqual( spi.toString(), - "StandardProgram { program: Program { code: 200,135,9, mask: Mask[0, 2, 1, ], jumpTable: JumpTable[], basicBlocks: BasicBlocks[0 -> start, ] }, memory_pages: 7, registers: 4294901760,4278059008,0,0,0,0,0,4278124544,3,0,0,0,0 }", + "StandardProgram { program: Program { code: 200,135,9, mask: Mask[0, 2, 1, ], jumpTable: JumpTable[], basicBlocks: BasicBlocks[0 -> start, ], gasCosts: GasCosts[0 -> 1, ] }, memory_pages: 7, registers: 4294901760,4278059008,0,0,0,0,0,4278124544,3,0,0,0,0 }", ); return assert; }), diff --git a/bench/run.ts b/bench/run.ts index cd1c05f..ba77f3b 100644 --- a/bench/run.ts +++ b/bench/run.ts @@ -213,8 +213,8 @@ function benchW3f(dir: string): BenchResult | null { const gas = BigInt(data["initial-gas"] || 10000); const pc = data["initial-pc"] || 0; - const exe = prepareProgram(InputKind.Generic, HasMetadata.No, data.program, registers, pageMap, memory, [], 16); - runProgram(exe, gas, pc, false, false, false, values["block-gas"]); + const exe = prepareProgram(InputKind.Generic, HasMetadata.No, data.program, registers, pageMap, memory, [], 16, values["block-gas"]); + runProgram(exe, gas, pc, false, false); } }); diff --git a/bin/src/trace-replay.ts b/bin/src/trace-replay.ts index 40a2307..0640d46 100644 --- a/bin/src/trace-replay.ts +++ b/bin/src/trace-replay.ts @@ -32,6 +32,7 @@ type ReplayOptions = { verify: boolean; logHostCall?: boolean; tracer?: Tracer; + useBlockGas?: boolean; }; export function replayTraceFile(filePath: string, options: ReplayOptions): TraceSummary { @@ -46,7 +47,7 @@ export function replayTraceFile(filePath: string, options: ReplayOptions): Trace const spiArgs = Array.from(extractSpiArgs(start, initialMemWrites)); const preallocateMemoryPages = 128; - const useBlockGas = false; + const useBlockGas = options.useBlockGas ?? false; const preparedProgram = useSpi ? prepareProgram(InputKind.SPI, hasMetadata, programInput, [], [], [], spiArgs, preallocateMemoryPages, useBlockGas) : prepareProgram( From f155bc85ed37a2019885357e06c3a9a73d54a69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sun, 1 Mar 2026 00:18:42 +0000 Subject: [PATCH 11/14] perf --- assembly/api-utils.ts | 4 ++-- assembly/arguments.ts | 12 +++++----- assembly/gas.ts | 2 +- assembly/instructions.ts | 8 ++++--- assembly/interpreter.ts | 11 +++++---- assembly/program.test.ts | 8 +++---- assembly/program.ts | 51 +++++++++++++++++++++------------------- 7 files changed, 51 insertions(+), 45 deletions(-) diff --git a/assembly/api-utils.ts b/assembly/api-utils.ts index 187073c..6f62390 100644 --- a/assembly/api-utils.ts +++ b/assembly/api-utils.ts @@ -25,9 +25,9 @@ class BlockGasCost { export function getBlockGasCosts(input: u8[], kind: InputKind, withMetadata: HasMetadata): BlockGasCost[] { const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0, true); const blockCosts: BlockGasCost[] = []; - const costs = program.program.gasCosts.costs; + const costs = program.program.gasCosts.codeAndGas; for (let n: i32 = 0; n < costs.length; n += 1) { - const gas = costs[n]; + const gas = costs[n] >> 8; if (gas !== 0) { const x = new BlockGasCost(); x.pc = n; diff --git a/assembly/arguments.ts b/assembly/arguments.ts index b371546..cae9e77 100644 --- a/assembly/arguments.ts +++ b/assembly/arguments.ts @@ -51,10 +51,10 @@ export class Args { d: u32 = 0; } -type ArgsDecoder = (args: Args, code: u8[], offset: u32, end: u32) => Args; +type ArgsDecoder = (args: Args, code: StaticArray, offset: u32, end: u32) => Args; -function twoImm(args: Args, code: u8[], offset: u32, end: u32): Args { - const low = lowNibble(portable.arrayAt(code, offset)); +function twoImm(args: Args, code: StaticArray, offset: u32, end: u32): Args { + const low = lowNibble(portable.staticArrayAt(code, offset)); const split = minI32(4, low) + 1; const first = decodeI32(code, offset + 1, offset + split); const second = decodeI32(code, offset + split, end); @@ -146,7 +146,7 @@ export function higNibble(byte: u8): u8 { } //@inline -function decodeI32(input: u8[], start: u32, end: u32): u32 { +function decodeI32(input: StaticArray, start: u32, end: u32): u32 { if (end <= start) { return 0; } @@ -156,14 +156,14 @@ function decodeI32(input: u8[], start: u32, end: u32): u32 { for (let i: u32 = 0; i < len; i++) { num |= u32(input[start + i]) << (i * 8); } - const msb = portable.arrayAt(input, start + len - 1) & 0x80; + const msb = portable.staticArrayAt(input, start + len - 1) & 0x80; if (len < 4 && msb > 0) { num |= 0xffff_ffff << (len * 8); } return num; } -function decodeU32(data: u8[], offset: u32): u32 { +function decodeU32(data: StaticArray, offset: u32): u32 { let num = u32(data[offset + 0]); num |= u32(data[offset + 1]) << 8; num |= u32(data[offset + 2]) << 16; diff --git a/assembly/gas.ts b/assembly/gas.ts index fcc771c..44bbedb 100644 --- a/assembly/gas.ts +++ b/assembly/gas.ts @@ -18,7 +18,7 @@ export class GasCounter { } @inline - sub(g: Gas): boolean { + sub(g: u32): boolean { if (g > this.gas) { this.gas = u64(0); return true; diff --git a/assembly/instructions.ts b/assembly/instructions.ts index 65553c0..26e0a14 100644 --- a/assembly/instructions.ts +++ b/assembly/instructions.ts @@ -4,7 +4,7 @@ import { Gas } from "./gas"; export class Instruction { name: string = ""; kind: Arguments = Arguments.Zero; - gas: Gas = u64(0); + gas: u32 = u32(0); isTerminating: boolean = false; } @@ -12,13 +12,15 @@ function instruction(name: string, kind: Arguments, gas: Gas, isTerminating: boo const i = new Instruction(); i.name = name; i.kind = kind; - i.gas = u64(gas); + i.gas = u32(gas); i.isTerminating = isTerminating; return i; } export const MISSING_INSTRUCTION = instruction("INVALID", Arguments.Zero, 1, false); +export const SBRK = instruction("SBRK", Arguments.TwoReg, 1); + export const INSTRUCTIONS: Instruction[] = [ /* 000 */ instruction("TRAP", Arguments.Zero, 1, true), /* 001 */ instruction("FALLTHROUGH", Arguments.Zero, 1, true), @@ -131,7 +133,7 @@ export const INSTRUCTIONS: Instruction[] = [ MISSING_INSTRUCTION, /* 100 */ instruction("MOVE_REG", Arguments.TwoReg, 1), - /* 101 */ instruction("SBRK", Arguments.TwoReg, 1), + /* 101 */ SBRK, /* 102 */ instruction("COUNT_SET_BITS_64", Arguments.TwoReg, 1), /* 103 */ instruction("COUNT_SET_BITS_32", Arguments.TwoReg, 1), /* 104 */ instruction("LEADING_ZERO_BITS_64", Arguments.TwoReg, 1), diff --git a/assembly/interpreter.ts b/assembly/interpreter.ts index 513a366..08e1670 100644 --- a/assembly/interpreter.ts +++ b/assembly/interpreter.ts @@ -87,7 +87,7 @@ export class Interpreter { const code = this.program.code; const mask = this.program.mask; - const gasCosts = this.program.gasCosts.costs; + const gasCosts = this.program.gasCosts.codeAndGas; const basicBlocks = this.program.basicBlocks; const jumpTable = this.program.jumpTable; @@ -112,12 +112,13 @@ export class Interpreter { return false; } - const instruction = unchecked(code[pc]); + // check gas via pre-computed cost table (per-instruction or per-block) + const codeAndGas = portable.staticArrayAt(gasCosts, pc); + const instruction = codeAndGas & 0xff; + const gasCost = codeAndGas >> 8; const iData = instruction < INSTRUCTIONS.length ? unchecked(INSTRUCTIONS[instruction]) : MISSING_INSTRUCTION; - // check gas via pre-computed cost table (per-instruction or per-block) - const gasCost = portable.staticArrayAt(gasCosts, pc); - if (gasCost > 0 && this.gas.sub(gasCost)) { + if (this.gas.sub(gasCost)) { this.status = Status.OOG; return false; } diff --git a/assembly/program.test.ts b/assembly/program.test.ts index 97f6423..dad56f5 100644 --- a/assembly/program.test.ts +++ b/assembly/program.test.ts @@ -46,7 +46,7 @@ export const TESTS: Test[] = [ test("should decode arguments correctly", () => { const r = new Args(); - const data: u8[] = [0xff, 0xff, 0xff, 0xff]; + const data = StaticArray.fromArray([0xff, 0xff, 0xff, 0xff]); const args = decodeArguments(r, Arguments.OneImm, data, 0, 4); const assert = new Assert(); @@ -59,7 +59,7 @@ export const TESTS: Test[] = [ test("should decode positive bounded by skip", () => { const r = new Args(); - const data: u8[] = [0x05, 0x05]; + const data = StaticArray.fromArray([0x05, 0x05]); const args = decodeArguments(r, Arguments.OneImm, data, 0, 1); const assert = new Assert(); @@ -121,7 +121,7 @@ export const TESTS: Test[] = [ }), test("should construct basic blocks correctly based on skip", () => { - const code: u8[] = [ + const code = StaticArray.fromArray([ opcode(trap), 0, 0, @@ -154,7 +154,7 @@ export const TESTS: Test[] = [ 0, 0, opcode(jump_ind), - ]; + ]); const mask = new Mask(u8arr([0b0000_0001, 0b0000_0000, 0b0000_0000, 0b1000_0000]), 32); const basicBlocks = new BasicBlocks(code, mask); const assert = new Assert(); diff --git a/assembly/program.ts b/assembly/program.ts index c9ff81c..55e6678 100644 --- a/assembly/program.ts +++ b/assembly/program.ts @@ -1,12 +1,12 @@ import { Args, Arguments, DECODERS, REQUIRED_BYTES } from "./arguments"; import { Decoder } from "./codec"; -import { Gas } from "./gas"; import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; import { reg, u32SignExtend } from "./instructions/utils"; import { portable } from "./portable"; import { Registers } from "./registers"; export type ProgramCounter = u32; +export type Code = StaticArray; const MAX_SKIP: u32 = 24; @@ -33,9 +33,9 @@ export function liftBytes(data: u8[]): Uint8Array { return p; } -/** Convert `Uint8Array` to `u8` */ -export function lowerBytes(data: Uint8Array): u8[] { - const r = new Array(data.length); +/** Convert `Uint8Array` to `Code` (StaticArray) */ +export function lowerBytes(data: Uint8Array): Code { + const r = new StaticArray(data.length); for (let i = 0; i < data.length; i++) { r[i] = data[i]; } @@ -134,52 +134,55 @@ export class Mask { } export class GasCosts { - readonly costs: StaticArray; + // Since code is just u8, we use the other 24 bytes to store the gas cost. + readonly codeAndGas: StaticArray; - constructor(code: u8[], mask: Mask, blocks: BasicBlocks, useBlockGasCost: boolean) { + constructor(code: Code, mask: Mask, blocks: BasicBlocks, useBlockGasCost: boolean) { const len = code.length; - const costs = new StaticArray(len); + const costs = new StaticArray(len); for (let n: i32 = 0; n < len; n += 1) { const isInstructionInMask = mask.isInstruction(n); if (!isInstructionInMask) { + costs[n] = code[n]; continue; } const skipArgs = mask.skipBytesToNextInstruction(n); const iData = code[n] >= INSTRUCTIONS.length ? MISSING_INSTRUCTION : INSTRUCTIONS[code[n]]; - costs[n] = iData.gas; + costs[n] = code[n] | (iData.gas << 8); n += skipArgs; } // sum up costs per block if (useBlockGasCost) { let previousStart: u32 = 0; - let previousSum: u64 = 0; + let previousSum: u32 = 0; for (let n: i32 = 0; n < len; n += 1) { - const current = costs[n]; - costs[n] = 0; + const currentGas = costs[n] >> 8; + costs[n] = code[n]; // reset to just opcode (gas=0) if (blocks.isStart(n)) { - costs[previousStart] = previousSum; - previousSum = current; + costs[previousStart] = code[previousStart] | (previousSum << 8); + previousSum = currentGas; previousStart = n; } else { - previousSum = portable.u64_add(previousSum, current); + previousSum += currentGas; } n += mask.skipBytesToNextInstruction(n); } // final assignment - costs[previousStart] = previousSum; + costs[previousStart] = code[previousStart] | (previousSum << 8); } - this.costs = costs; + this.codeAndGas = costs; } toString(): string { let v = "GasCosts["; - for (let i = 0; i < this.costs.length; i += 1) { - if (this.costs[i] !== 0) { - v += `${i} -> ${this.costs[i]}, `; + for (let i = 0; i < this.codeAndGas.length; i += 1) { + const gas = this.codeAndGas[i] >> 8; + if (gas !== 0) { + v += `${i} -> ${gas}, `; } } return `${v}]`; @@ -198,7 +201,7 @@ export enum BasicBlock { export class BasicBlocks { readonly isStartOrEnd: StaticArray; - constructor(code: u8[], mask: Mask) { + constructor(code: Code, mask: Mask) { const len = code.length; const isStartOrEnd = new StaticArray(len); if (len > 0) { @@ -285,7 +288,7 @@ export class JumpTable { export class Program { constructor( - public readonly code: u8[], + public readonly code: Code, public readonly mask: Mask, public readonly jumpTable: JumpTable, public readonly basicBlocks: BasicBlocks, @@ -299,9 +302,9 @@ export class Program { // Pre-allocated buffer for the rare case when code is shorter than needed. // Max REQUIRED_BYTES is 9 (OneRegOneExtImm). We allocate 16 for safety. -const EXTENDED_BUF: u8[] = new Array(16); +const EXTENDED_BUF: Code = new StaticArray(16); -export function decodeArguments(args: Args, kind: Arguments, code: u8[], offset: i32, lim: u32): Args { +export function decodeArguments(args: Args, kind: Arguments, code: Code, offset: i32, lim: u32): Args { if (code.length < offset + REQUIRED_BYTES[kind]) { // in case we have less data than needed we extend the data with zeros. const reqBytes = REQUIRED_BYTES[kind]; @@ -327,7 +330,7 @@ class ResolvedArguments { export function resolveArguments( argsRes: Args, kind: Arguments, - code: u8[], + code: Code, offset: u32, lim: u32, registers: Registers, From 081a295674e6dd7cb21dfe337bf549544b72453e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sun, 1 Mar 2026 00:27:28 +0000 Subject: [PATCH 12/14] use static arrays --- assembly/arguments.ts | 8 ++++---- assembly/instructions.ts | 4 ++-- assembly/portable.ts | 1 + assembly/program.test.ts | 5 +---- bench/run.ts | 12 +++++++++++- bin/index.ts | 2 +- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/assembly/arguments.ts b/assembly/arguments.ts index cae9e77..a0463b4 100644 --- a/assembly/arguments.ts +++ b/assembly/arguments.ts @@ -18,9 +18,9 @@ export enum Arguments { } /** How many numbers in `Args` is relevant for given `Arguments`. */ -export const RELEVANT_ARGS = [0, 1, 2, 1, 2, 3, 3, 3, 2, 3, 3, 4, 3]; +export const RELEVANT_ARGS: StaticArray = StaticArray.fromArray([0, 1, 2, 1, 2, 3, 3, 3, 2, 3, 3, 4, 3]); /** How many bytes is required by given `Arguments`. */ -export const REQUIRED_BYTES = [0, 0, 1, 0, 1, 9, 1, 1, 1, 1, 1, 2, 2]; +export const REQUIRED_BYTES: StaticArray = StaticArray.fromArray([0, 0, 1, 0, 1, 9, 1, 1, 1, 1, 1, 2, 2]); // @unmanaged export class Args { @@ -61,7 +61,7 @@ function twoImm(args: Args, code: StaticArray, offset: u32, end: u32): Args return args.fill(first, second, 0, 0); } -export const DECODERS: ArgsDecoder[] = [ +export const DECODERS: StaticArray = StaticArray.fromArray([ // DECODERS[Arguments.Zero] = (args, _d, _o, _l) => { return args.fill(0, 0, 0, 0); @@ -135,7 +135,7 @@ export const DECODERS: ArgsDecoder[] = [ const b = lowNibble(data[o + 1]); return args.fill(hig, low, b, 0); }, -]; +]); // @inline export function lowNibble(byte: u8): u8 { diff --git a/assembly/instructions.ts b/assembly/instructions.ts index 26e0a14..122e814 100644 --- a/assembly/instructions.ts +++ b/assembly/instructions.ts @@ -21,7 +21,7 @@ export const MISSING_INSTRUCTION = instruction("INVALID", Arguments.Zero, 1, fal export const SBRK = instruction("SBRK", Arguments.TwoReg, 1); -export const INSTRUCTIONS: Instruction[] = [ +export const INSTRUCTIONS: StaticArray = StaticArray.fromArray([ /* 000 */ instruction("TRAP", Arguments.Zero, 1, true), /* 001 */ instruction("FALLTHROUGH", Arguments.Zero, 1, true), MISSING_INSTRUCTION, @@ -276,4 +276,4 @@ export const INSTRUCTIONS: Instruction[] = [ /* 229 */ instruction("MIN", Arguments.ThreeReg, 1), /* 230 */ instruction("MIN_U", Arguments.ThreeReg, 1), -]; +]); diff --git a/assembly/portable.ts b/assembly/portable.ts index 00d09de..0d0b68b 100644 --- a/assembly/portable.ts +++ b/assembly/portable.ts @@ -22,6 +22,7 @@ export class portable { arr.fill(0); return arr; }; + g.StaticArray.fromArray = (x: any[]) => x; g.ASC_TARGET = 0; diff --git a/assembly/program.test.ts b/assembly/program.test.ts index dad56f5..e35ad6f 100644 --- a/assembly/program.test.ts +++ b/assembly/program.test.ts @@ -113,10 +113,7 @@ export const TESTS: Test[] = [ program.basicBlocks.toString(), "BasicBlocks[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", ); - assert.isEqual( - program.gasCosts.toString(), - "GasCosts[0 -> 3, 8 -> 1, 9 -> 2, 15 -> 4, 24 -> 4, 31 -> 1, ]", - ); + assert.isEqual(program.gasCosts.toString(), "GasCosts[0 -> 3, 8 -> 1, 9 -> 2, 15 -> 4, 24 -> 4, 31 -> 1, ]"); return assert; }), diff --git a/bench/run.ts b/bench/run.ts index ba77f3b..5a606b7 100644 --- a/bench/run.ts +++ b/bench/run.ts @@ -213,7 +213,17 @@ function benchW3f(dir: string): BenchResult | null { const gas = BigInt(data["initial-gas"] || 10000); const pc = data["initial-pc"] || 0; - const exe = prepareProgram(InputKind.Generic, HasMetadata.No, data.program, registers, pageMap, memory, [], 16, values["block-gas"]); + const exe = prepareProgram( + InputKind.Generic, + HasMetadata.No, + data.program, + registers, + pageMap, + memory, + [], + 16, + values["block-gas"], + ); runProgram(exe, gas, pc, false, false); } }); diff --git a/bin/index.ts b/bin/index.ts index 1bb35c2..c76224b 100755 --- a/bin/index.ts +++ b/bin/index.ts @@ -192,7 +192,7 @@ function handleRun(args: string[]) { try { const preallocateMemoryPages = 128; - const useBlockGas = true; + const useBlockGas = false; const program = prepareProgram( kind, hasMetadata, From 97c14d995437d324b654f713a5dfbb10aa1bb190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sun, 1 Mar 2026 00:34:52 +0000 Subject: [PATCH 13/14] alterations --- assembly/interpreter.ts | 2 +- assembly/program.ts | 8 ++++---- bin/index.ts | 2 +- portable/types/assemblyscript.d.ts | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/assembly/interpreter.ts b/assembly/interpreter.ts index 08e1670..a95de8b 100644 --- a/assembly/interpreter.ts +++ b/assembly/interpreter.ts @@ -118,7 +118,7 @@ export class Interpreter { const gasCost = codeAndGas >> 8; const iData = instruction < INSTRUCTIONS.length ? unchecked(INSTRUCTIONS[instruction]) : MISSING_INSTRUCTION; - if (this.gas.sub(gasCost)) { + if (gasCost > 0 && this.gas.sub(gasCost)) { this.status = Status.OOG; return false; } diff --git a/assembly/program.ts b/assembly/program.ts index 55e6678..0aad11f 100644 --- a/assembly/program.ts +++ b/assembly/program.ts @@ -307,16 +307,16 @@ const EXTENDED_BUF: Code = new StaticArray(16); export function decodeArguments(args: Args, kind: Arguments, code: Code, offset: i32, lim: u32): Args { if (code.length < offset + REQUIRED_BYTES[kind]) { // in case we have less data than needed we extend the data with zeros. - const reqBytes = REQUIRED_BYTES[kind]; + const reqBytes = unchecked(REQUIRED_BYTES[kind]); for (let i = 0; i < reqBytes; i++) { EXTENDED_BUF[i] = 0; } for (let i = offset; i < code.length; i++) { - EXTENDED_BUF[i - offset] = code[i]; + EXTENDED_BUF[i - offset] = unchecked(code[i]); } - return DECODERS[kind](args, EXTENDED_BUF, 0, lim); + return unchecked(DECODERS[kind])(args, EXTENDED_BUF, 0, lim); } - return DECODERS[kind](args, code, offset, offset + lim); + return unchecked(DECODERS[kind])(args, code, offset, offset + lim); } class ResolvedArguments { diff --git a/bin/index.ts b/bin/index.ts index c76224b..1bb35c2 100755 --- a/bin/index.ts +++ b/bin/index.ts @@ -192,7 +192,7 @@ function handleRun(args: string[]) { try { const preallocateMemoryPages = 128; - const useBlockGas = false; + const useBlockGas = true; const program = prepareProgram( kind, hasMetadata, diff --git a/portable/types/assemblyscript.d.ts b/portable/types/assemblyscript.d.ts index daba955..16eeb6d 100644 --- a/portable/types/assemblyscript.d.ts +++ b/portable/types/assemblyscript.d.ts @@ -76,6 +76,7 @@ declare function bool(value: ASPrimitive): bool; interface StaticArray extends Array {} interface StaticArrayConstructor { new (length: number): StaticArray; + fromArray(arr: T[]): StaticArray; } declare const StaticArray: StaticArrayConstructor; From 4f0bace11b61c790ad30aa0b2bd0cc4b0512e496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sun, 1 Mar 2026 00:58:20 +0000 Subject: [PATCH 14/14] Fix portable build. --- assembly/gas.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/assembly/gas.ts b/assembly/gas.ts index 44bbedb..8b9981d 100644 --- a/assembly/gas.ts +++ b/assembly/gas.ts @@ -19,11 +19,12 @@ export class GasCounter { @inline sub(g: u32): boolean { - if (g > this.gas) { + const cost = u64(g); + if (cost > this.gas) { this.gas = u64(0); return true; } - this.gas = this.gas - g; + this.gas = this.gas - cost; return false; } }