Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ecalli handling for other pvms #285

Merged
merged 16 commits into from
Jan 17, 2025
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"@radix-ui/react-tooltip": "^1.1.2",
"@reduxjs/toolkit": "^2.2.8",
"@tanstack/react-virtual": "^3.10.9",
"@typeberry/block": "^0.0.1-b25856a",
"@typeberry/jam-host-calls": "0.0.1-b25856a",
"@typeberry/block": "0.0.1-447d5c4",
"@typeberry/jam-host-calls": "0.0.1-459ce0b",
"@typeberry/pvm-debugger-adapter": "0.1.0-459ce0b",
"@typeberry/spectool-wasm": "0.18.1",
"@uiw/react-codemirror": "^4.23.6",
Expand Down
135 changes: 116 additions & 19 deletions src/packages/web-worker/command-handlers/host-call.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { HostCallIdentifiers } from "@/types/pvm";
import { CommandStatus, PvmApiInterface, Storage } from "../types";
import { read, write } from "@typeberry/jam-host-calls";
import { read, Registers, write, Memory } from "@typeberry/jam-host-calls";
import { WriteAccounts } from "@/packages/host-calls/write";
import { isInternalPvm } from "../utils";
import { ReadAccounts } from "@/packages/host-calls/read";
import { tryAsServiceId } from "@typeberry/block";
import { Gas, MemoryIndex, tryAsGas } from "@typeberry/pvm-debugger-adapter";
import { WasmPvmShellInterface } from "../wasmBindgenShell";

type HostCallParams = {
pvm: PvmApiInterface | null;
Expand All @@ -26,9 +28,115 @@ type HostCallResponse =
error?: unknown;
};

type ExecuteParams = Parameters<write.Write["execute"]>;
type RegistersType = ExecuteParams[1];
type MemoryType = ExecuteParams[2];
class SimpleGas {
pvm!: WasmPvmShellInterface;

get(): Gas {
return tryAsGas(this.pvm.getGasLeft());
}
set(gas: Gas) {
if (this.pvm.setGasLeft) {
this.pvm.setGasLeft(BigInt(gas));
}
}
sub(v: Gas) {
const current = this.get();
if (current > v) {
this.set(tryAsGas(BigInt(current) - BigInt(v)));
return false;
}
// underflow
this.set(tryAsGas(0));
return true;
}
}

const getGasCounter = (pvm: PvmApiInterface) => {
if (isInternalPvm(pvm)) {
return pvm.getInterpreter().getGasCounter();
}

const gas = new SimpleGas();
gas.pvm = pvm;

return gas;
};

class SimpleRegisters implements Registers {
flatRegisters!: Uint8Array;
pvm!: WasmPvmShellInterface;

getU32(registerIndex: number): number {
return Number(this.getU64(registerIndex) & 0xffff_ffffn);
}
getI32(registerIndex: number): number {
return Number(this.getU32(registerIndex)) >> 0;
}
setU32(registerIndex: number, value: number): void {
this.setU64(registerIndex, BigInt(value));
}
setI32(registerIndex: number, value: number): void {
this.setI64(registerIndex, BigInt(value));
}
getU64(registerIndex: number): bigint {
return new BigUint64Array(this.flatRegisters.buffer)[registerIndex];
}
getI64(registerIndex: number): bigint {
return new BigInt64Array(this.flatRegisters.buffer)[registerIndex];
}
setU64(registerIndex: number, value: bigint): void {
new BigUint64Array(this.flatRegisters.buffer)[registerIndex] = value;
this.pvm.setRegisters(this.flatRegisters);
}
setI64(registerIndex: number, value: bigint): void {
new BigInt64Array(this.flatRegisters.buffer)[registerIndex] = value;
krystian50 marked this conversation as resolved.
Show resolved Hide resolved
this.pvm.setRegisters(this.flatRegisters);
}
}

const getRegisters = (pvm: PvmApiInterface) => {
if (isInternalPvm(pvm)) {
return pvm.getInterpreter().getRegisters();
}

const registers = new SimpleRegisters();
registers.flatRegisters = pvm.getRegisters();
registers.pvm = pvm;

return registers;
};

class SimpleMemory implements Memory {
pvm!: WasmPvmShellInterface;
memorySize: number = 4096;

loadInto(result: Uint8Array, startAddress: MemoryIndex) {
const memoryDump = this.pvm.getPageDump(startAddress / this.memorySize);
result.set(memoryDump.subarray(0, result.length));

return null;
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
isWriteable(_destinationStart: MemoryIndex, _length: number) {
return true;
}

storeFrom(address: MemoryIndex, bytes: Uint8Array) {
// TODO [ToDr] Either change the API to require handling multi-page writes or change this code to split the write into multiple pages.
this.pvm.setMemory(address, bytes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.pvm.setMemory(address, bytes);
// TODO [ToDr] Either change the API to require handling multi-page writes or change this code to split the write into multiple pages.
this.pvm.setMemory(address, bytes);

return null;
}
}

const getMemory = (pvm: PvmApiInterface) => {
if (isInternalPvm(pvm)) {
return pvm.getInterpreter().getMemory();
}
const memory = new SimpleMemory();
memory.pvm = pvm;

return memory;
};

const hostCall = async ({
pvm,
Expand All @@ -41,32 +149,21 @@ const hostCall = async ({
storage: Storage;
serviceId: number;
}): Promise<HostCallResponse> => {
if (!isInternalPvm(pvm)) {
return { hostCallIdentifier, status: CommandStatus.ERROR, error: new Error("External PVM not supported") };
}

if (hostCallIdentifier === HostCallIdentifiers.READ) {
const readAccounts = new ReadAccounts(storage);
const jamHostCall = new read.Read(readAccounts);
// TODO the types are the same, but exported from different packages and lost track of the type
jamHostCall.currentServiceId = tryAsServiceId(serviceId) as unknown as typeof jamHostCall.currentServiceId;

await jamHostCall.execute(
pvm.getInterpreter().getGasCounter(),
pvm.getInterpreter().getRegisters() as unknown as RegistersType,
pvm.getInterpreter().getMemory() as unknown as MemoryType,
);
const registers = getRegisters(pvm);
await jamHostCall.execute(getGasCounter(pvm), registers, getMemory(pvm));

return { hostCallIdentifier, status: CommandStatus.SUCCESS };
} else if (hostCallIdentifier === HostCallIdentifiers.WRITE) {
const writeAccounts = new WriteAccounts(storage);
const jamHostCall = new write.Write(writeAccounts);

await jamHostCall.execute(
pvm.getInterpreter().getGasCounter(),
pvm.getInterpreter().getRegisters() as unknown as RegistersType,
pvm.getInterpreter().getMemory() as unknown as MemoryType,
);
await jamHostCall.execute(getGasCounter(pvm), getRegisters(pvm), getMemory(pvm));

return { hostCallIdentifier, storage, status: CommandStatus.SUCCESS };
}

Expand Down
2 changes: 1 addition & 1 deletion src/packages/web-worker/command-handlers/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type MemoryResponse = {
error?: unknown;
};

const getMemoryPage = (pageNumber: number, pvm: PvmApiInterface | null) => {
export const getMemoryPage = (pageNumber: number, pvm: PvmApiInterface | null) => {
if (!pvm) {
return new Uint8Array();
}
Expand Down
12 changes: 2 additions & 10 deletions src/packages/web-worker/command-handlers/step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { CurrentInstruction, ExpectedState, Status } from "@/types/pvm";
import { nextInstruction } from "../pvm";
import { getState } from "../utils";
import { CommandStatus, PvmApiInterface, Storage } from "../types";
import { runHostCall } from "./host-call";

export type StepParams = {
program: Uint8Array;
Expand All @@ -20,20 +19,13 @@ export type StepResponse = {
isFinished: boolean;
};

const step = async ({ pvm, program, stepsToPerform, storage, serviceId }: StepParams) => {
const step = async ({ pvm, program, stepsToPerform }: StepParams) => {
if (!pvm) {
throw new Error("PVM is uninitialized.");
}

let isFinished = stepsToPerform > 1 ? !pvm.nSteps(stepsToPerform) : !pvm.nextStep();
let state = getState(pvm);

if (state.status === Status.HOST && storage !== null && serviceId !== null) {
const hostCallIdentifier = pvm.getExitArg();
await runHostCall({ pvm, hostCallIdentifier, storage, serviceId });
// pvm.nextStep();
state = getState(pvm);
}
const state = getState(pvm);

// It's not really finished if we're in host status
if (isFinished && state.status === Status.HOST) {
Expand Down
46 changes: 21 additions & 25 deletions src/store/workers/workersSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,33 +288,29 @@ export const handleHostCall = createAsyncThunk("workers/handleHostCall", async (
}

await Promise.all(
state.workers
// [KF] Remove or change this condition when we support host calls on more PVMs.
.filter(({ id }) => id === "typeberry")
.map(async (worker) => {
const resp = await asyncWorkerPostMessage(worker.id, worker.worker, {
command: Commands.HOST_CALL,
payload: { hostCallIdentifier: worker.exitArg as HostCallIdentifiers },
});

if (
resp.payload.hostCallIdentifier === HostCallIdentifiers.WRITE &&
resp.payload.storage &&
// Remove if we decide to make storage initialization optional
state.debugger.storage
) {
const newStorage = mergePVMAndDebuggerEcalliStorage(resp.payload.storage, state.debugger.storage);
dispatch(setStorage(newStorage));
}
state.workers.map(async (worker) => {
const resp = await asyncWorkerPostMessage(worker.id, worker.worker, {
command: Commands.HOST_CALL,
payload: { hostCallIdentifier: worker.exitArg as HostCallIdentifiers },
});
if (
resp.payload.hostCallIdentifier === HostCallIdentifiers.WRITE &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These HostCallIndentifiers should rather come from host calls impls (new Write().index) to avoid duplicating this stuff in case it ever changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I'm seeing it's only exported as

import { write } from "@typeberry/jam-host-calls";
new write.Write().index

which is fine, but it requires account param. Maybe you could add index as a static property?

resp.payload.storage &&
// Remove if we decide to make storage initialization optional
state.debugger.storage
) {
const newStorage = mergePVMAndDebuggerEcalliStorage(resp.payload.storage, state.debugger.storage);
dispatch(setStorage(newStorage));
}

if ((getState() as RootState).debugger.isRunMode) {
dispatch(continueAllWorkers());
}
if ((getState() as RootState).debugger.isRunMode) {
dispatch(continueAllWorkers());
}

if (hasCommandStatusError(resp)) {
throw new Error(resp.error.message);
}
}),
if (hasCommandStatusError(resp)) {
throw new Error(resp.error.message);
}
}),
);

if (selectShouldContinueRunning(state)) {
Expand Down
Loading