From bfeb6862794a3cb2216dab7c65a0a3295fdd8bb7 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Wed, 25 Mar 2026 17:53:19 +0100 Subject: [PATCH 1/2] Add expunge host call --- package-lock.json | 2 + .../jam/in-core/externalities/refine.test.ts | 115 +++++++++++++++++- packages/jam/in-core/externalities/refine.ts | 43 +++++-- packages/jam/in-core/package.json | 2 + .../externalities/refine-externalities.ts | 2 +- 5 files changed, 155 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f196d996..11e60f4e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9163,6 +9163,8 @@ "@typeberry/jam-host-calls": "*", "@typeberry/logger": "*", "@typeberry/numbers": "*", + "@typeberry/pvm-interface": "*", + "@typeberry/pvm-interpreter": "*", "@typeberry/state": "*", "@typeberry/transition": "*", "@typeberry/utils": "*" diff --git a/packages/jam/in-core/externalities/refine.test.ts b/packages/jam/in-core/externalities/refine.test.ts index cb30bbad5..5378d359e 100644 --- a/packages/jam/in-core/externalities/refine.test.ts +++ b/packages/jam/in-core/externalities/refine.test.ts @@ -14,11 +14,13 @@ import { Bytes, BytesBlob } from "@typeberry/bytes"; import { HashDictionary } from "@typeberry/collections"; import { tinyChainSpec } from "@typeberry/config"; import { HASH_SIZE } from "@typeberry/hash"; -import { SegmentExportError } from "@typeberry/jam-host-calls"; +import { SegmentExportError, tryAsMachineId, tryAsProgramCounter } from "@typeberry/jam-host-calls"; import { tryAsU32, tryAsU64 } from "@typeberry/numbers"; import { InMemoryService, InMemoryState, PreimageItem, ServiceAccountInfo, type State } from "@typeberry/state"; import { RefineExternalitiesImpl, type RefineExternalitiesParams } from "./refine.js"; +const MINIMAL_PROGRAM = new Uint8Array([0, 1, 1, 0, 0x00]); + function createSegment(byte = 0xab): Segment { return Bytes.fill(SEGMENT_BYTES, byte); } @@ -212,4 +214,115 @@ describe("RefineExternalitiesImpl", () => { assert.deepStrictEqual(exported[0].raw.subarray(0, 5), new Uint8Array([1, 2, 3, 4, 5])); }); }); + + describe("machineInit", () => { + it("should create a new inner PVM and return a machine ID", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const pc = tryAsProgramCounter(0); + + const result = await ext.machineInit(code, pc); + + assert.strictEqual(result.isOk, true); + assert.strictEqual(result.ok, tryAsMachineId(0)); + }); + + it("should assign sequential machine IDs", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const pc = tryAsProgramCounter(0); + + const r1 = await ext.machineInit(code, pc); + const r2 = await ext.machineInit(code, pc); + const r3 = await ext.machineInit(code, pc); + + assert.strictEqual(r1.isOk, true); + assert.strictEqual(r1.ok, tryAsMachineId(0)); + assert.strictEqual(r2.isOk, true); + assert.strictEqual(r2.ok, tryAsMachineId(1)); + assert.strictEqual(r3.isOk, true); + assert.strictEqual(r3.ok, tryAsMachineId(2)); + }); + + it("should return error for invalid program blob", async () => { + const ext = createExt(); + const invalidCode = BytesBlob.blobFrom(new Uint8Array([0xff, 0xff, 0xff])); + const pc = tryAsProgramCounter(0); + + const result = await ext.machineInit(invalidCode, pc); + + assert.strictEqual(result.isError, true); + }); + + it("should return error for empty program blob", async () => { + const ext = createExt(); + const emptyCode = BytesBlob.blobFrom(new Uint8Array([])); + const pc = tryAsProgramCounter(0); + + const result = await ext.machineInit(emptyCode, pc); + + assert.strictEqual(result.isError, true); + }); + + it("should accept a non-zero program counter", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const pc = tryAsProgramCounter(1); + + const result = await ext.machineInit(code, pc); + assert.strictEqual(result.isOk, true); + }); + }); + + describe("machineExpunge", () => { + it("should remove machine and return its program counter (0)", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const initResult = await ext.machineInit(code, tryAsProgramCounter(0)); + assert.strictEqual(initResult.isOk, true); + + const machineId = initResult.ok; + const result = await ext.machineExpunge(machineId); + + assert.strictEqual(result.isOk, true); + // PC should be 0 since we initialized with PC=0 + assert.strictEqual(result.ok, tryAsProgramCounter(0)); + }); + + it("should remove machine and return its program counter (10)", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const initResult = await ext.machineInit(code, tryAsProgramCounter(10)); + assert.strictEqual(initResult.isOk, true); + + const machineId = initResult.ok; + const result = await ext.machineExpunge(machineId); + + assert.strictEqual(result.isOk, true); + // PC should be 10 since we initialized with PC=10 + assert.strictEqual(result.ok, tryAsProgramCounter(10)); + }); + + it("should return NoMachineError for non-existent machine", async () => { + const ext = createExt(); + const result = await ext.machineExpunge(tryAsMachineId(999)); + + assert.strictEqual(result.isError, true); + }); + + it("should not allow double expunge", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const initResult = await ext.machineInit(code, tryAsProgramCounter(0)); + + assert.strictEqual(initResult.isOk, true); + const machineId = initResult.ok; + + const r1 = await ext.machineExpunge(machineId); + assert.strictEqual(r1.isOk, true); + + const r2 = await ext.machineExpunge(machineId); + assert.strictEqual(r2.isError, true); + }); + }); }); diff --git a/packages/jam/in-core/externalities/refine.ts b/packages/jam/in-core/externalities/refine.ts index d202373fe..efa6f3679 100644 --- a/packages/jam/in-core/externalities/refine.ts +++ b/packages/jam/in-core/externalities/refine.ts @@ -11,18 +11,20 @@ import { type MachineId, type MachineResult, type MemoryOperation, - type NoMachineError, + NoMachineError, type PagesError, type PeekPokeError, type ProgramCounter, type RefineExternalities, SegmentExportError, + tryAsMachineId, + tryAsProgramCounter, type ZeroVoidError, } from "@typeberry/jam-host-calls"; import type { U64 } from "@typeberry/numbers"; import type { HostCallMemory, HostCallRegisters } from "@typeberry/pvm-host-calls"; -import type { BigGas } from "@typeberry/pvm-interface"; -import type { ProgramDecoderError } from "@typeberry/pvm-interpreter"; +import { type BigGas, tryAsGas } from "@typeberry/pvm-interface"; +import { Interpreter, ProgramDecoder, type ProgramDecoderError } from "@typeberry/pvm-interpreter"; import type { State } from "@typeberry/state"; import { type OK, Result } from "@typeberry/utils"; @@ -39,6 +41,8 @@ export type RefineExternalitiesParams = { }; export class RefineExternalitiesImpl implements RefineExternalities { + /** Map of inner PVM instances keyed by MachineId. */ + private machines: Map = new Map(); /** Service being refined (used as default for historicalLookup). */ private readonly currentServiceId: ServiceId; /** State at the lookup anchor for preimage lookups. */ @@ -62,8 +66,14 @@ export class RefineExternalitiesImpl implements RefineExternalities { return this.exportedSegments; } - machineExpunge(_machineIndex: MachineId): Promise> { - throw new Error("Method not implemented."); + machineExpunge(machineIndex: MachineId): Promise> { + const innerPvm = this.machines.get(machineIndex); + if (innerPvm === undefined) { + return Promise.resolve(Result.error(NoMachineError, () => `Machine not found (id: ${machineIndex})`)); + } + const pc = tryAsProgramCounter(innerPvm.getPC()); + this.machines.delete(machineIndex); + return Promise.resolve(Result.ok(pc)); } machinePages( @@ -103,8 +113,27 @@ export class RefineExternalitiesImpl implements RefineExternalities { throw new Error("Method not implemented."); } - machineInit(_code: BytesBlob, _programCounter: ProgramCounter): Promise> { - throw new Error("Method not implemented."); + machineInit(code: BytesBlob, programCounter: ProgramCounter): Promise> { + // https://graypaper.fluffylabs.dev/#/ab2cdbd/346400346400?v=0.7.2 + const deblobResult = ProgramDecoder.deblob(code.raw); + if (deblobResult.isError) { + return Promise.resolve(Result.error(deblobResult.error, deblobResult.details)); + } + + const innerPvm = new Interpreter({ useSbrkGas: false }); + + innerPvm.resetGeneric(code.raw, Number(programCounter), tryAsGas(0)); + + // https://graypaper.fluffylabs.dev/#/ab2cdbd/348c00348c00?v=0.7.2 + let id = 0n; + while (this.machines.has(id)) { + id++; + } + + const machineId = tryAsMachineId(id); + // https://graypaper.fluffylabs.dev/#/ab2cdbd/340501340b01?v=0.7.2 + this.machines.set(id, innerPvm); + return Promise.resolve(Result.ok(machineId)); } machineInvoke( diff --git a/packages/jam/in-core/package.json b/packages/jam/in-core/package.json index 1cda131cc..3a7d28661 100644 --- a/packages/jam/in-core/package.json +++ b/packages/jam/in-core/package.json @@ -18,6 +18,8 @@ "@typeberry/jam-host-calls": "*", "@typeberry/logger": "*", "@typeberry/numbers": "*", + "@typeberry/pvm-interface": "*", + "@typeberry/pvm-interpreter": "*", "@typeberry/state": "*", "@typeberry/transition": "*", "@typeberry/utils": "*" diff --git a/packages/jam/jam-host-calls/externalities/refine-externalities.ts b/packages/jam/jam-host-calls/externalities/refine-externalities.ts index bc858b467..ff501d7ab 100644 --- a/packages/jam/jam-host-calls/externalities/refine-externalities.ts +++ b/packages/jam/jam-host-calls/externalities/refine-externalities.ts @@ -110,7 +110,7 @@ export interface RefineExternalities { /** Get the segments exported during this work item's refinement. */ getExportedSegments(): readonly Segment[]; - /** Forget a previously started nested VM. */ + /** Forget a previously started nested VM. Return its current program counter.*/ machineExpunge(machineIndex: MachineId): Promise>; /** Set given range of pages as non-accessible and re-initialize them with zeros. */ From 94a0dc42efffa1565a91efad3344ed4d78cacc3c Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Mon, 13 Apr 2026 10:26:54 +0200 Subject: [PATCH 2/2] Search by SortedArray --- .../jam/in-core/externalities/refine.test.ts | 68 +++++++++++++++++++ packages/jam/in-core/externalities/refine.ts | 10 +-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/jam/in-core/externalities/refine.test.ts b/packages/jam/in-core/externalities/refine.test.ts index bbeb42d9c..f11e0fe76 100644 --- a/packages/jam/in-core/externalities/refine.test.ts +++ b/packages/jam/in-core/externalities/refine.test.ts @@ -274,4 +274,72 @@ describe("RefineExternalitiesImpl", () => { assert.strictEqual(result.isOk, true); }); }); + + describe("machineExpunge", () => { + it("should remove machine and return its program counter (0)", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const initResult = await ext.machineInit(code, tryAsProgramCounter(0)); + assert.strictEqual(initResult.isOk, true); + + const machineId = initResult.ok; + const result = await ext.machineExpunge(machineId); + + assert.strictEqual(result.isOk, true); + // PC should be 0 since we initialized with PC=0 + assert.strictEqual(result.ok, tryAsProgramCounter(0)); + }); + + it("should remove machine and return its program counter (10)", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const initResult = await ext.machineInit(code, tryAsProgramCounter(10)); + assert.strictEqual(initResult.isOk, true); + + const machineId = initResult.ok; + const result = await ext.machineExpunge(machineId); + + assert.strictEqual(result.isOk, true); + // PC should be 10 since we initialized with PC=10 + assert.strictEqual(result.ok, tryAsProgramCounter(10)); + }); + + it("should return NoMachineError for non-existent machine", async () => { + const ext = createExt(); + const result = await ext.machineExpunge(tryAsMachineId(999)); + + assert.strictEqual(result.isError, true); + }); + + it("should not allow double expunge", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + const initResult = await ext.machineInit(code, tryAsProgramCounter(0)); + + assert.strictEqual(initResult.isOk, true); + const machineId = initResult.ok; + + const r1 = await ext.machineExpunge(machineId); + assert.strictEqual(r1.isOk, true); + + const r2 = await ext.machineExpunge(machineId); + assert.strictEqual(r2.isError, true); + }); + + it("should remove exact machine from multiple and return its program counter (10)", async () => { + const ext = createExt(); + const code = BytesBlob.blobFrom(MINIMAL_PROGRAM); + await ext.machineInit(code, tryAsProgramCounter(0)); + const initResult = await ext.machineInit(code, tryAsProgramCounter(10)); + await ext.machineInit(code, tryAsProgramCounter(20)); + assert.strictEqual(initResult.isOk, true); + + const machineId = initResult.ok; + const result = await ext.machineExpunge(machineId); + + assert.strictEqual(result.isOk, true); + // PC should be 10 since we initialized with PC=10 + assert.strictEqual(result.ok, tryAsProgramCounter(10)); + }); + }); }); diff --git a/packages/jam/in-core/externalities/refine.ts b/packages/jam/in-core/externalities/refine.ts index f5b8f7eb0..578222d64 100644 --- a/packages/jam/in-core/externalities/refine.ts +++ b/packages/jam/in-core/externalities/refine.ts @@ -20,6 +20,7 @@ import { type RefineExternalities, SegmentExportError, tryAsMachineId, + tryAsProgramCounter, type ZeroVoidError, } from "@typeberry/jam-host-calls"; import type { U64 } from "@typeberry/numbers"; @@ -89,12 +90,13 @@ export class RefineExternalitiesImpl implements RefineExternalities { } machineExpunge(machineIndex: MachineId): Promise> { - const innerPvm = this.machines.get(machineIndex); - if (innerPvm === undefined) { + // We just care about machineIndex + const entry = this.machines.findExact([machineIndex, undefined as unknown as IPvmInterpreter]); + if (entry === undefined) { return Promise.resolve(Result.error(NoMachineError, () => `Machine not found (id: ${machineIndex})`)); } - const pc = tryAsProgramCounter(innerPvm.getPC()); - this.machines.delete(machineIndex); + const pc = tryAsProgramCounter(entry[1].getPC()); + this.machines.removeOne(entry); return Promise.resolve(Result.ok(pc)); }