diff --git a/package-lock.json b/package-lock.json index 8f196d996..d760b1162 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9163,6 +9163,10 @@ "@typeberry/jam-host-calls": "*", "@typeberry/logger": "*", "@typeberry/numbers": "*", + "@typeberry/ordering": "*", + "@typeberry/pvm-host-calls": "*", + "@typeberry/pvm-interface": "*", + "@typeberry/pvm-interpreter": "*", "@typeberry/state": "*", "@typeberry/transition": "*", "@typeberry/utils": "*" diff --git a/packages/core/pvm-interface/pvm.ts b/packages/core/pvm-interface/pvm.ts index 1d86ad2cb..5ce94c5e1 100644 --- a/packages/core/pvm-interface/pvm.ts +++ b/packages/core/pvm-interface/pvm.ts @@ -17,6 +17,9 @@ export interface IPvmInterpreter { /** Prepare SPI program to be executed. */ resetJam(program: Uint8Array, args: Uint8Array, pc: number, gas: Gas): void; + /** Prepare a generic (non-SPI) program to be executed. */ + resetGeneric(rawProgram: Uint8Array, pc: number, gas: Gas): void; + /** Execute loaded program. */ runProgram(): void; diff --git a/packages/jam/in-core/externalities/refine.test.ts b/packages/jam/in-core/externalities/refine.test.ts index cb30bbad5..bbeb42d9c 100644 --- a/packages/jam/in-core/externalities/refine.test.ts +++ b/packages/jam/in-core/externalities/refine.test.ts @@ -12,13 +12,15 @@ import { } from "@typeberry/block"; import { Bytes, BytesBlob } from "@typeberry/bytes"; import { HashDictionary } from "@typeberry/collections"; -import { tinyChainSpec } from "@typeberry/config"; +import { PvmBackend, 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); } @@ -81,6 +83,7 @@ function createExt(overrides: Partial = {}) { currentServiceId: tryAsServiceId(42), lookupState: overrides.lookupState ?? defaultState, exportOffset: overrides.exportOffset ?? 0, + pvmBackend: PvmBackend.BuiltIn, ...overrides, }); } @@ -212,4 +215,63 @@ 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); + }); + }); }); diff --git a/packages/jam/in-core/externalities/refine.ts b/packages/jam/in-core/externalities/refine.ts index d202373fe..65f13438c 100644 --- a/packages/jam/in-core/externalities/refine.ts +++ b/packages/jam/in-core/externalities/refine.ts @@ -6,6 +6,8 @@ import { tryAsSegmentIndex, } from "@typeberry/block"; import type { BytesBlob } from "@typeberry/bytes"; +import { SortedArray } from "@typeberry/collections"; +import type { PvmBackend } from "@typeberry/config"; import type { Blake2bHash } from "@typeberry/hash"; import { type MachineId, @@ -17,15 +19,29 @@ import { type ProgramCounter, type RefineExternalities, SegmentExportError, + tryAsMachineId, 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 { Ordering } from "@typeberry/ordering"; +import { type HostCallMemory, type HostCallRegisters, PvmInstanceManager } from "@typeberry/pvm-host-calls"; +import { type BigGas, type IPvmInterpreter, tryAsGas } from "@typeberry/pvm-interface"; +import { ProgramDecoder, type ProgramDecoderError } from "@typeberry/pvm-interpreter"; import type { State } from "@typeberry/state"; import { type OK, Result } from "@typeberry/utils"; +type MachineEntry = [MachineId, IPvmInterpreter]; + +const machineComparator = (a: MachineEntry, b: MachineEntry) => { + if (a[0] < b[0]) { + return Ordering.Less; + } + if (a[0] > b[0]) { + return Ordering.Greater; + } + return Ordering.Equal; +}; + /** * Parameters required to create a RefineExternalitiesImpl. */ @@ -36,9 +52,16 @@ export type RefineExternalitiesParams = { lookupState: State; /** Export offset -- sum of exports from prior work items in this package. */ exportOffset: number; + /** + * PVM backend to use for creating inner PVM instances. + * NIT: Could accept PVMInstanceManager + */ + pvmBackend: PvmBackend; }; export class RefineExternalitiesImpl implements RefineExternalities { + /** Inner PVM instances sorted by MachineId. */ + private machines: SortedArray = SortedArray.fromSortedArray(machineComparator); /** Service being refined (used as default for historicalLookup). */ private readonly currentServiceId: ServiceId; /** State at the lookup anchor for preimage lookups. */ @@ -47,6 +70,8 @@ export class RefineExternalitiesImpl implements RefineExternalities { private readonly exportedSegments: Segment[] = []; /** Offset for segment indexing (sum of exports from prior items). */ private readonly exportOffset: number; + /** PVM backend for creating inner machines. */ + private readonly pvmBackend: PvmBackend; static create(params: RefineExternalitiesParams) { return new RefineExternalitiesImpl(params); @@ -56,6 +81,7 @@ export class RefineExternalitiesImpl implements RefineExternalities { this.currentServiceId = params.currentServiceId; this.lookupState = params.lookupState; this.exportOffset = params.exportOffset; + this.pvmBackend = params.pvmBackend; } getExportedSegments(): readonly Segment[] { @@ -103,8 +129,36 @@ export class RefineExternalitiesImpl implements RefineExternalities { throw new Error("Method not implemented."); } - machineInit(_code: BytesBlob, _programCounter: ProgramCounter): Promise> { - throw new Error("Method not implemented."); + async 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 Result.error(deblobResult.error, deblobResult.details); + } + + const manager = await PvmInstanceManager.new(this.pvmBackend); + const innerPvm = await manager.getInstance(); + + innerPvm.resetGeneric(code.raw, Number(programCounter), tryAsGas(0)); + + // https://graypaper.fluffylabs.dev/#/ab2cdbd/348c00348c00?v=0.7.2 + // Binary search for the minimal free MachineId + const arr = this.machines.array; + let low = 0; + let high = arr.length; + while (low < high) { + const mid = (low + high) >> 1; + if (arr[mid][0] > BigInt(mid)) { + high = mid; + } else { + low = mid + 1; + } + } + + const machineId = tryAsMachineId(low); + // https://graypaper.fluffylabs.dev/#/ab2cdbd/340501340b01?v=0.7.2 + this.machines.insert([machineId, innerPvm]); + return Result.ok(machineId); } machineInvoke( diff --git a/packages/jam/in-core/in-core.ts b/packages/jam/in-core/in-core.ts index 9cf86a05c..c53157807 100644 --- a/packages/jam/in-core/in-core.ts +++ b/packages/jam/in-core/in-core.ts @@ -442,6 +442,7 @@ export class InCore { currentServiceId: args.currentServiceId, lookupState: args.lookupState, exportOffset: args.exportOffset, + pvmBackend: this.pvmBackend, }); return { diff --git a/packages/jam/in-core/package.json b/packages/jam/in-core/package.json index 1cda131cc..aa70d66af 100644 --- a/packages/jam/in-core/package.json +++ b/packages/jam/in-core/package.json @@ -18,6 +18,10 @@ "@typeberry/jam-host-calls": "*", "@typeberry/logger": "*", "@typeberry/numbers": "*", + "@typeberry/ordering": "*", + "@typeberry/pvm-host-calls": "*", + "@typeberry/pvm-interface": "*", + "@typeberry/pvm-interpreter": "*", "@typeberry/state": "*", "@typeberry/transition": "*", "@typeberry/utils": "*"