1022 lines
36 KiB
TypeScript
1022 lines
36 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { access, readFile, stat, writeFile } from "node:fs/promises";
|
|
import { constants } from "node:fs";
|
|
import { isAbsolute, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { z } from "zod";
|
|
import {
|
|
createAnalysisExecution,
|
|
type ExecutionOptions,
|
|
type ProviderAvailability,
|
|
} from "../application/AnalysisProvider.js";
|
|
import type {
|
|
EvmInterfaceExecution,
|
|
EvmInterfacePort,
|
|
} from "../application/evm/EvmInterfacePort.js";
|
|
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
|
|
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
|
|
import {
|
|
AnalysisCleanupLease,
|
|
AnalysisCleanupLeaseGroup,
|
|
} from "../artifacts/AnalysisCleanupLease.js";
|
|
import {
|
|
OwnedCommandFailure,
|
|
ownedCommandResources,
|
|
} from "../process/OwnedCommand.js";
|
|
import {
|
|
AnalysisError,
|
|
type AnalysisCapturedOutput,
|
|
} from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisUnsupportedTargetError,
|
|
AnalysisInputError,
|
|
AnalysisOutputError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { evmInterfaceEvidence } from "../domain/evm/evmInterfaceEvidence.js";
|
|
import {
|
|
DEFAULT_EVM_TIMEOUT_MS,
|
|
type InspectEvmInterfaceInput,
|
|
} from "../domain/evm/evmInterface.js";
|
|
import { evmInterfaceSchema } from "../domain/evm/evmInterface.js";
|
|
import { solidityMetadataSchema } from "../domain/evm/solidityMetadata.js";
|
|
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import { runOwnedCommand } from "../process/OwnedCommand.js";
|
|
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
|
import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
|
|
import {
|
|
evmInterfaceFailure,
|
|
capturedEvmOutput,
|
|
evmReplyBudgetFailure,
|
|
type EvmFileSizeFailureEvidence,
|
|
} from "./EvmoleFailures.js";
|
|
import {
|
|
evmoleInterfaceReplySchema,
|
|
projectEvmoleInterface,
|
|
} from "./EvmoleInterfaceAdapter.js";
|
|
import {
|
|
EVMOLE_PROVIDER_IDENTITY,
|
|
EVM_CPU_SECONDS,
|
|
EVM_INTERFACE_LIMITS,
|
|
EVM_FILE_SIZE_FAILURE_EXIT,
|
|
} from "./EvmoleRelease.js";
|
|
import { decodeEvmBytecodeCarrier } from "./EvmBytecodeCarrier.js";
|
|
import {
|
|
selectEvmWorkerLimits,
|
|
type EvmWorkerLimits,
|
|
} from "./EvmWorkerLimits.js";
|
|
|
|
const OPERATION = "inspect_evm_interface";
|
|
const HOST_REQUIREMENT =
|
|
"Initial real-verified offline EVM interface profile supports Linux x64 only.";
|
|
|
|
/** The EVM interface profile runs only on Linux x64; discovery and execution share this check. */
|
|
export const evmHostSupported = (
|
|
platform: NodeJS.Platform,
|
|
arch: string,
|
|
): boolean => platform === "linux" && arch === "x64";
|
|
export const EVM_PRLIMIT_COMMAND_DEFAULT = "/usr/bin/prlimit";
|
|
|
|
export interface EvmLimiterProbeFileSystem {
|
|
access(path: string, mode: number): Promise<void>;
|
|
stat(path: string): Promise<{ isFile(): boolean }>;
|
|
}
|
|
|
|
export interface EvmLimiterProbeOptions {
|
|
readonly platform?: NodeJS.Platform;
|
|
readonly arch?: string;
|
|
readonly fs?: EvmLimiterProbeFileSystem;
|
|
readonly signal?: AbortSignal;
|
|
}
|
|
|
|
type EvmLimiterSelection = {
|
|
readonly path: string;
|
|
readonly diagnostics: Readonly<Record<string, string>>;
|
|
};
|
|
type EvmLimiterUnavailable = EvmLimiterSelection & {
|
|
readonly reason: string;
|
|
readonly cause: unknown;
|
|
};
|
|
|
|
/** Select and validate the exact limiter using the same owner check at discovery and admission. */
|
|
const selectEvmLimiter = async (
|
|
environment: Readonly<NodeJS.ProcessEnv>,
|
|
probes?: {
|
|
readonly platform?: NodeJS.Platform;
|
|
readonly arch?: string;
|
|
readonly fs?: EvmLimiterProbeFileSystem;
|
|
},
|
|
signal?: AbortSignal,
|
|
operation = OPERATION,
|
|
): Promise<Result<EvmLimiterSelection, EvmLimiterUnavailable> | undefined> => {
|
|
const throwIfAborted = (): void => {
|
|
if (signal?.aborted)
|
|
throw new AnalysisCancelledError(operation, { cause: signal.reason });
|
|
};
|
|
throwIfAborted();
|
|
const platform = probes?.platform ?? process.platform;
|
|
const arch = probes?.arch ?? process.arch;
|
|
if (!evmHostSupported(platform, arch)) return undefined;
|
|
|
|
const path =
|
|
environment.REA_EVM_PRLIMIT_COMMAND ?? EVM_PRLIMIT_COMMAND_DEFAULT;
|
|
const diagnostics = { limiter_path: path, platform, arch };
|
|
try {
|
|
if (!isAbsolute(path))
|
|
throw new Error(
|
|
"REA_EVM_PRLIMIT_COMMAND must be an absolute executable path.",
|
|
);
|
|
const fs = probes?.fs ?? { access, stat };
|
|
await fs.access(path, constants.X_OK);
|
|
throwIfAborted();
|
|
if (!(await fs.stat(path)).isFile())
|
|
throw new Error(
|
|
"Configured prlimit must be a regular executable file, not a directory.",
|
|
);
|
|
throwIfAborted();
|
|
return ok({ path, diagnostics });
|
|
} catch (cause: unknown) {
|
|
throwIfAborted();
|
|
const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
return err({ path, diagnostics, reason, cause });
|
|
}
|
|
};
|
|
|
|
/** Fresh, process-free observation of the limiter prerequisite used by EVM execution. */
|
|
export const evmLimiterAvailability = async (
|
|
environment: Readonly<NodeJS.ProcessEnv>,
|
|
probes?: EvmLimiterProbeOptions,
|
|
): Promise<ProviderAvailability | undefined> => {
|
|
const selected = await selectEvmLimiter(
|
|
environment,
|
|
probes,
|
|
probes?.signal,
|
|
"binary_session",
|
|
);
|
|
if (selected === undefined) return undefined;
|
|
return selected.ok
|
|
? {
|
|
status: "available",
|
|
code: null,
|
|
reason: null,
|
|
diagnostics: selected.value.diagnostics,
|
|
}
|
|
: {
|
|
status: "unavailable",
|
|
code: "executable_missing",
|
|
reason: selected.error.reason,
|
|
diagnostics: selected.error.diagnostics,
|
|
};
|
|
};
|
|
|
|
const replySchema = z.discriminatedUnion("ok", [
|
|
z.strictObject({
|
|
ok: z.literal(true),
|
|
version: z.literal("0.9.3"),
|
|
bytecode: z.strictObject({
|
|
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
bytes: z.int().nonnegative(),
|
|
hex: z.string().regex(/^(?:[a-f0-9]{2})*$/),
|
|
metadata: solidityMetadataSchema.optional(),
|
|
}),
|
|
raw: evmoleInterfaceReplySchema,
|
|
}),
|
|
z.strictObject({
|
|
ok: z.literal(false),
|
|
reason: z.enum(["format", "decoder", "output-limit", "unsupported"]),
|
|
message: z.string(),
|
|
}),
|
|
]);
|
|
type Launcher = NonNullable<Parameters<typeof runOwnedCommand>[2]>["launcher"];
|
|
type EvmOutcome = Result<EvmInterfaceExecution, AnalysisError>;
|
|
type EvmRuntimeRoot = Pick<PrivateRuntimeRoot, "path" | "close">;
|
|
type PendingEvmCleanup = {
|
|
readonly input: InspectEvmInterfaceInput;
|
|
readonly result: EvmOutcome;
|
|
readonly output: AnalysisCapturedOutput | undefined;
|
|
root:
|
|
| { readonly owner: EvmRuntimeRoot; readonly lease: AnalysisCleanupLease }
|
|
| undefined;
|
|
process: { readonly lease: AnalysisCleanupLease } | undefined;
|
|
};
|
|
|
|
const ownerCleanupMarker = (
|
|
owners: readonly AnalysisCleanupLease[],
|
|
): ProviderCleanupError =>
|
|
new ProviderCleanupError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
owners.flatMap((owner) => owner.cleanupResources ?? ["owned-EVM-resource"]),
|
|
{ reason: "Exact EVM cleanup owners have not confirmed release." },
|
|
{ operation: OPERATION },
|
|
);
|
|
|
|
const bindEvmCleanupOwners = (
|
|
failure: AnalysisError,
|
|
owners: readonly AnalysisCleanupLease[],
|
|
primary?: AnalysisError,
|
|
): void => {
|
|
const marker = ownerCleanupMarker(owners);
|
|
new AnalysisCleanupLeaseGroup(marker, owners);
|
|
failure.bindCleanupDependencies([
|
|
marker,
|
|
...(primary?.cleanupIncomplete === true ? [primary] : []),
|
|
]);
|
|
};
|
|
|
|
const retainedCleanupFailure = (cause: unknown): ProviderCleanupError => {
|
|
if (cause instanceof ProviderCleanupError) return cause;
|
|
if (cause instanceof ArtifactReaderFailure) {
|
|
const observation = ArtifactReaderFailure.cleanupObservation(
|
|
cause,
|
|
"EVM artifact resources",
|
|
);
|
|
const failure = new ProviderCleanupError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
observation.resources.length === 0
|
|
? ["EVM artifact resources"]
|
|
: observation.resources,
|
|
{ reason: observation.reason },
|
|
{ operation: OPERATION, cause },
|
|
);
|
|
bindArtifactFileCleanup(cause, failure);
|
|
return failure;
|
|
}
|
|
return new ProviderCleanupError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
cause instanceof AnalysisError && cause.cleanupResources.length > 0
|
|
? cause.cleanupResources
|
|
: ["EVM provider cleanup"],
|
|
{ reason: cause instanceof Error ? cause.message : String(cause) },
|
|
{
|
|
operation: OPERATION,
|
|
cause,
|
|
...(cause instanceof AnalysisError ? { cleanupPrimary: cause } : {}),
|
|
},
|
|
);
|
|
};
|
|
|
|
const compactErrorProjection = (error: AnalysisError) => {
|
|
const projection = projectAnalysisError(error);
|
|
if (error.capturedOutput === undefined) return projection;
|
|
// The cleanup result owns this exact captured stream. Other diagnostics and
|
|
// partial Evidence retain their original fields, including stdout/stderr.
|
|
const details = { ...projection.details };
|
|
delete details.captured_output;
|
|
return { ...projection, details };
|
|
};
|
|
|
|
const evmCleanupFailure = (
|
|
input: InspectEvmInterfaceInput,
|
|
result: EvmOutcome,
|
|
resources: readonly string[],
|
|
reason: string,
|
|
output?: AnalysisCapturedOutput,
|
|
cause?: unknown,
|
|
): ProviderCleanupError => {
|
|
const partialObservation = result.ok
|
|
? evmInterfaceEvidence(input, result.value)
|
|
: result.error.partialObservation;
|
|
const capturedOutput = result.ok
|
|
? output
|
|
: (result.error.capturedOutput ?? output);
|
|
return new ProviderCleanupError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
resources,
|
|
{
|
|
reason,
|
|
previous_error: result.ok ? null : compactErrorProjection(result.error),
|
|
...(capturedOutput === undefined
|
|
? {}
|
|
: { captured_output: { ...capturedOutput } }),
|
|
},
|
|
{
|
|
operation: OPERATION,
|
|
...(result.ok ? {} : { cleanupPrimary: result.error }),
|
|
...(cause === undefined ? {} : { cause }),
|
|
...(partialObservation === undefined ? {} : { partialObservation }),
|
|
},
|
|
);
|
|
};
|
|
|
|
/** Run unchanged EVMole/WASM in an owned worker; no target, chain or network execution. */
|
|
export class EvmoleInterfaceProvider implements EvmInterfacePort {
|
|
readonly #resources = new ArtifactResourceScope();
|
|
#resourceScopeLease: AnalysisCleanupLease | undefined;
|
|
#tail: Promise<void> = Promise.resolve();
|
|
#closing = false;
|
|
#closePromise: Promise<void> | undefined;
|
|
#pendingCleanup: PendingEvmCleanup | undefined;
|
|
|
|
#artifactScopeLease(): AnalysisCleanupLease {
|
|
if (this.#resourceScopeLease !== undefined) return this.#resourceScopeLease;
|
|
const provider = this;
|
|
const owner = {
|
|
close: () => provider.#resources.close(),
|
|
get cleanupResources() {
|
|
return provider.#resources.cleanupResources;
|
|
},
|
|
};
|
|
this.#resourceScopeLease = AnalysisCleanupLease.forCloseOwner(
|
|
owner,
|
|
"EVM artifact resources",
|
|
);
|
|
return this.#resourceScopeLease;
|
|
}
|
|
|
|
constructor(
|
|
readonly environment: Readonly<NodeJS.ProcessEnv> = process.env,
|
|
readonly launcher?: Launcher,
|
|
readonly createRuntime: () => Promise<
|
|
Pick<PrivateRuntimeRoot, "path" | "close">
|
|
> = () => PrivateRuntimeRoot.create({ prefix: "rea-evm-interface-" }),
|
|
) {}
|
|
|
|
/** Bind recovered interface candidates to both original carrier and decoded byte identity. */
|
|
inspect(
|
|
input: InspectEvmInterfaceInput,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<EvmInterfaceExecution, AnalysisError>> {
|
|
if (this.#closing) return Promise.resolve(err(this.#closedError()));
|
|
const operation = this.#tail.then(async () => {
|
|
if (this.#closing) return err(this.#closedError());
|
|
try {
|
|
return await this.#resources.run(() => this.#inspect(input, options));
|
|
} catch (cause: unknown) {
|
|
const failure = evmInterfaceFailure(
|
|
cause,
|
|
"artifact-read",
|
|
input.path,
|
|
undefined,
|
|
this.environment.REA_EVM_PRLIMIT_COMMAND ??
|
|
EVM_PRLIMIT_COMMAND_DEFAULT,
|
|
undefined,
|
|
input.timeout_ms ?? DEFAULT_EVM_TIMEOUT_MS,
|
|
);
|
|
if (cause instanceof ArtifactReaderFailure)
|
|
bindArtifactFileCleanup(cause, failure);
|
|
return err(failure);
|
|
}
|
|
});
|
|
this.#tail = operation.then(
|
|
() => undefined,
|
|
() => undefined,
|
|
);
|
|
return operation;
|
|
}
|
|
|
|
async #inspect(
|
|
input: InspectEvmInterfaceInput,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<EvmInterfaceExecution, AnalysisError>> {
|
|
const pending = await this.#retire();
|
|
if (pending !== undefined) return err(pending);
|
|
let root: Pick<PrivateRuntimeRoot, "path" | "close"> | undefined;
|
|
let rootLease: AnalysisCleanupLease | undefined;
|
|
let processOwner: ProviderProcessSupervisor | undefined;
|
|
let processLease: AnalysisCleanupLease | undefined;
|
|
let processCleanup:
|
|
| {
|
|
readonly reason: string;
|
|
readonly resources: readonly string[];
|
|
}
|
|
| undefined;
|
|
const processLeases = new WeakMap<
|
|
ProviderProcessSupervisor,
|
|
AnalysisCleanupLease
|
|
>();
|
|
const processLeaseFor = (owner: ProviderProcessSupervisor) => {
|
|
let lease = processLeases.get(owner);
|
|
if (lease === undefined) {
|
|
lease = new AnalysisCleanupLease(
|
|
async () => {
|
|
const stopped = await owner.stop();
|
|
if (
|
|
stopped.status === "incomplete" ||
|
|
stopped.status === "not-owned"
|
|
)
|
|
throw new Error(
|
|
stopped.status === "incomplete"
|
|
? stopped.reason
|
|
: "Owned EVM worker cleanup did not confirm ownership.",
|
|
{ cause: stopped },
|
|
);
|
|
},
|
|
owner.launch.ownership === undefined
|
|
? ["owned-worker"]
|
|
: ownedCommandResources(owner.launch.ownership, root?.path),
|
|
);
|
|
processLeases.set(owner, lease);
|
|
}
|
|
return lease;
|
|
};
|
|
const rootLeaseFor = (owner: EvmRuntimeRoot) => {
|
|
rootLease ??= AnalysisCleanupLease.forCloseOwner(owner, owner.path);
|
|
return rootLease;
|
|
};
|
|
const timeoutMs = input.timeout_ms ?? DEFAULT_EVM_TIMEOUT_MS;
|
|
let retainedOutput: AnalysisCapturedOutput | undefined;
|
|
let phase: "configuration" | "artifact-read" | "worker" = "configuration";
|
|
let workerLimits: EvmWorkerLimits | undefined;
|
|
let limiter =
|
|
this.environment.REA_EVM_PRLIMIT_COMMAND ?? EVM_PRLIMIT_COMMAND_DEFAULT;
|
|
let result: Result<EvmInterfaceExecution, AnalysisError>;
|
|
try {
|
|
if (options?.signal?.aborted)
|
|
throw new AnalysisCancelledError(OPERATION, {
|
|
cause: options.signal.reason,
|
|
});
|
|
if (!evmHostSupported(process.platform, process.arch))
|
|
throw new AnalysisCapabilityUnavailableError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
OPERATION,
|
|
HOST_REQUIREMENT,
|
|
{ userMessage: HOST_REQUIREMENT },
|
|
);
|
|
const selectedLimiter = await selectEvmLimiter(
|
|
this.environment,
|
|
undefined,
|
|
options?.signal,
|
|
);
|
|
if (selectedLimiter === undefined)
|
|
throw new AnalysisCapabilityUnavailableError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
OPERATION,
|
|
HOST_REQUIREMENT,
|
|
{ userMessage: HOST_REQUIREMENT },
|
|
);
|
|
if (!selectedLimiter.ok) {
|
|
limiter = selectedLimiter.error.path;
|
|
throw new AnalysisCapabilityUnavailableError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
OPERATION,
|
|
selectedLimiter.error.reason,
|
|
{
|
|
cause: selectedLimiter.error.cause,
|
|
userMessage: selectedLimiter.error.reason,
|
|
},
|
|
);
|
|
}
|
|
limiter = selectedLimiter.value.path;
|
|
let inheritedLimits: string;
|
|
try {
|
|
inheritedLimits = await readFile("/proc/self/limits", "utf8");
|
|
} catch (cause: unknown) {
|
|
const reason = `Could not read inherited Linux resource limits at /proc/self/limits: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
throw new AnalysisCapabilityUnavailableError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
OPERATION,
|
|
reason,
|
|
{ cause, userMessage: reason },
|
|
);
|
|
}
|
|
const limits = selectEvmWorkerLimits(inheritedLimits);
|
|
if (!limits.ok)
|
|
throw new AnalysisCapabilityUnavailableError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
OPERATION,
|
|
limits.error,
|
|
{ userMessage: limits.error },
|
|
);
|
|
workerLimits = limits.value;
|
|
phase = "artifact-read";
|
|
const carrier = await readStableArtifact(
|
|
input.path,
|
|
EVM_INTERFACE_LIMITS.inputBytes,
|
|
options?.signal,
|
|
undefined,
|
|
this.#resources,
|
|
);
|
|
phase = "worker";
|
|
root = await this.createRuntime();
|
|
rootLeaseFor(root);
|
|
const snapshotPath = join(root.path, "carrier.snapshot");
|
|
const requestPath = join(root.path, "request.json");
|
|
const replyPath = join(root.path, "reply.json");
|
|
await writeFile(snapshotPath, carrier.bytes, {
|
|
flag: "wx",
|
|
mode: 0o600,
|
|
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
|
});
|
|
await writeFile(
|
|
requestPath,
|
|
JSON.stringify({
|
|
snapshot_path: snapshotPath,
|
|
reply_path: replyPath,
|
|
failure_marker_path: join(root.path, "resource.failure"),
|
|
encoding: input.encoding,
|
|
}),
|
|
{ flag: "wx", mode: 0o600 },
|
|
);
|
|
const { NODE_OPTIONS: _ambientNodeOptions, ...environment } =
|
|
this.environment;
|
|
const execution = await runOwnedCommand(
|
|
{
|
|
command: limiter,
|
|
arguments: [
|
|
`--as=${String(workerLimits.addressSpaceBytes)}:`,
|
|
`--fsize=${String(workerLimits.fileSizeBytes)}:`,
|
|
"--core=0:",
|
|
`--cpu=${String(workerLimits.cpuSeconds)}:`,
|
|
"--",
|
|
process.execPath,
|
|
"--max-old-space-size=256",
|
|
"--v8-pool-size=1",
|
|
"--disable-wasm-trap-handler",
|
|
fileURLToPath(
|
|
new URL("./EvmoleInterfaceWorker.js", import.meta.url),
|
|
),
|
|
requestPath,
|
|
],
|
|
cwd: root.path,
|
|
expectedCommand: null,
|
|
runId: `rea-evm-interface-${randomUUID()}`,
|
|
hostEnvironment: environment,
|
|
},
|
|
{
|
|
timeoutMs,
|
|
diagnosticBytes: EVM_INTERFACE_LIMITS.diagnosticBytes,
|
|
},
|
|
{
|
|
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
|
...(this.launcher === undefined ? {} : { launcher: this.launcher }),
|
|
retainCleanup: (owner, cleanup) => {
|
|
processOwner = owner;
|
|
processLease = processLeaseFor(owner);
|
|
processCleanup = {
|
|
reason: cleanup.reason,
|
|
resources:
|
|
owner.launch.ownership === undefined
|
|
? ["owned-worker"]
|
|
: ownedCommandResources(owner.launch.ownership, root?.path),
|
|
};
|
|
},
|
|
},
|
|
);
|
|
const capturedOutput = capturedEvmOutput(execution);
|
|
retainedOutput = capturedOutput;
|
|
let reply: z.output<typeof replySchema>;
|
|
try {
|
|
const file = await readStableArtifact(
|
|
replyPath,
|
|
EVM_INTERFACE_LIMITS.outputBytes,
|
|
options?.signal,
|
|
undefined,
|
|
this.#resources,
|
|
);
|
|
reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8")));
|
|
} catch (cause: unknown) {
|
|
const cleanup =
|
|
cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined;
|
|
if (options?.signal?.aborted) {
|
|
const failure = new AnalysisCancelledError(OPERATION, {
|
|
cause: options.signal.reason,
|
|
capturedOutput,
|
|
...(cleanup === undefined ? {} : { cleanup }),
|
|
});
|
|
if (cause instanceof ArtifactReaderFailure)
|
|
bindArtifactFileCleanup(cause, failure);
|
|
throw failure;
|
|
}
|
|
const failure =
|
|
cause instanceof ArtifactReaderFailure &&
|
|
cause.reason === "limit" &&
|
|
cause.boundary === "file-size"
|
|
? evmReplyBudgetFailure(cause.message, {
|
|
cause,
|
|
capturedOutput,
|
|
...(cleanup === undefined ? {} : { cleanup }),
|
|
})
|
|
: new AnalysisOutputError(
|
|
OPERATION,
|
|
`Owned interface worker reply failed for ${input.path}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
{
|
|
cause,
|
|
capturedOutput,
|
|
...(cleanup === undefined ? {} : { cleanup }),
|
|
},
|
|
);
|
|
if (cause instanceof ArtifactReaderFailure)
|
|
bindArtifactFileCleanup(cause, failure);
|
|
throw failure;
|
|
}
|
|
if (!reply.ok) {
|
|
if (reply.reason === "unsupported")
|
|
throw new AnalysisUnsupportedTargetError(
|
|
OPERATION,
|
|
input.path,
|
|
reply.message,
|
|
{ capturedOutput },
|
|
);
|
|
if (reply.reason === "format")
|
|
throw new AnalysisInputError(OPERATION, { capturedOutput }, [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_format",
|
|
message: reply.message,
|
|
},
|
|
]);
|
|
if (reply.reason === "output-limit")
|
|
throw evmReplyBudgetFailure(reply.message, {
|
|
capturedOutput,
|
|
});
|
|
throw new ProviderAdapterError(EVMOLE_PROVIDER_IDENTITY.id, OPERATION, {
|
|
diagnostics: {
|
|
phase,
|
|
failure_kind: reply.reason,
|
|
reason: reply.message,
|
|
stdout: execution.stdout.text,
|
|
stderr: execution.stderr.text,
|
|
captured_output: { ...capturedOutput },
|
|
},
|
|
});
|
|
}
|
|
const selectedBytes = decodeEvmBytecodeCarrier(
|
|
carrier.bytes,
|
|
input.encoding,
|
|
);
|
|
const selectedHex = Buffer.from(selectedBytes).toString("hex");
|
|
const decodedHash = createHash("sha256")
|
|
.update(selectedBytes)
|
|
.digest("hex");
|
|
if (
|
|
decodedHash !== reply.bytecode.sha256 ||
|
|
selectedHex !== reply.bytecode.hex ||
|
|
selectedBytes.length !== reply.bytecode.bytes
|
|
)
|
|
throw new AnalysisOutputError(
|
|
OPERATION,
|
|
"Decoded bytecode representation does not match the selected carrier, encoding or reported SHA-256.",
|
|
{ capturedOutput },
|
|
);
|
|
const limitations = [
|
|
`Configured worker resource soft limits: address_space_bytes=${String(workerLimits.addressSpaceBytes)}, cpu_seconds=${String(workerLimits.cpuSeconds)}, file_size_bytes=${String(workerLimits.fileSizeBytes)}. Selected from inherited tighter limits; original hard limits are retained. Effective worker limits are not independently probed.`,
|
|
"Upstream may also report terminal metadata while recovering selectors. Its reported values are retained in raw_result, but integer precision and non-text-key coverage are unknown in the JS binding; complete original bytecode remains available.",
|
|
"Recovered selectors, argument strings and mutability are upstream inferences, not verified ABI signatures or names. Empty results do not prove the absence of callable dispatch.",
|
|
"Bytecode kind, hardfork and deployed-contract authenticity are unknown. This inspection performs no target runtime execution, chain/RPC lookup or network request.",
|
|
"Original carrier and decoded bytecode SHA-256 are distinct identities; SHA-256 is not the Ethereum Keccak code hash.",
|
|
"Terminal CBOR metadata is an independently decoded candidate, not proof of Solidity origin, source identity or deployment. Nested or unsupported values retain explicit unavailable interpretation and original byte ranges.",
|
|
`The owned worker has a 256 MiB JavaScript heap. The profile maxima are ${String(EVM_INTERFACE_LIMITS.addressSpaceBytes)} address-space bytes and ${String(EVM_CPU_SECONDS)} CPU seconds; the configured soft limits above retain tighter inherited limits. The command deadline is timeout_ms at ${String(timeoutMs)} ms (default ${String(DEFAULT_EVM_TIMEOUT_MS)}). Those byte, address-space, and CPU maxima have no runtime override, and timeout_ms does not raise them. Virtual address space is distinct from resident memory; limits fail without partial success.`,
|
|
];
|
|
const validated = evmInterfaceSchema.safeParse({
|
|
artifact: {
|
|
path: input.path,
|
|
sha256: carrier.sha256,
|
|
bytes: carrier.bytes.length,
|
|
encoding: input.encoding,
|
|
},
|
|
bytecode: {
|
|
...reply.bytecode,
|
|
digest_algorithm: "sha256",
|
|
kind: "unknown",
|
|
deployment_authenticity: "unknown",
|
|
hardfork: "unknown",
|
|
},
|
|
evidence_kind: "inferred",
|
|
functions: projectEvmoleInterface(reply.raw).functions,
|
|
discovery_completeness: "unknown",
|
|
runtime_execution: "not-performed",
|
|
diagnostics: {
|
|
stdout: execution.stdout.text,
|
|
stderr: execution.stderr.text,
|
|
truncated: capturedOutput.truncated,
|
|
},
|
|
limitations,
|
|
});
|
|
if (!validated.success)
|
|
throw new AnalysisOutputError(
|
|
OPERATION,
|
|
`Interface worker returned invalid bytecode-relative evidence: ${validated.error.issues[0]?.message ?? "schema mismatch"}`,
|
|
{ capturedOutput },
|
|
);
|
|
const report = validated.data;
|
|
result = ok(
|
|
createAnalysisExecution(report, EVMOLE_PROVIDER_IDENTITY, {
|
|
rawResult: reply.raw,
|
|
subject: { path: input.path, format: "file", sha256: carrier.sha256 },
|
|
locations: [{ kind: "artifact-path", path: input.path }],
|
|
limitations,
|
|
}),
|
|
);
|
|
} catch (cause: unknown) {
|
|
if (
|
|
cause instanceof OwnedCommandFailure &&
|
|
cause.cleanupOwner !== undefined
|
|
) {
|
|
processOwner = cause.cleanupOwner;
|
|
processLease = processLeaseFor(cause.cleanupOwner);
|
|
}
|
|
let fileFailure: EvmFileSizeFailureEvidence | undefined;
|
|
let markerCleanupFailure: ArtifactReaderFailure | undefined;
|
|
if (
|
|
cause instanceof OwnedCommandFailure &&
|
|
cause.snapshot?.exitCode === EVM_FILE_SIZE_FAILURE_EXIT &&
|
|
cause.snapshot.signal === null &&
|
|
root !== undefined
|
|
) {
|
|
try {
|
|
const marker = await readStableArtifact(
|
|
join(root.path, "resource.failure"),
|
|
1,
|
|
undefined,
|
|
undefined,
|
|
this.#resources,
|
|
);
|
|
if (!marker.bytes.equals(Buffer.from("F")))
|
|
throw new Error(
|
|
"Private worker failure marker does not match the observed exit status.",
|
|
);
|
|
fileFailure = { verified: true, failure: null };
|
|
} catch (markerFailure: unknown) {
|
|
if (
|
|
markerFailure instanceof ArtifactReaderFailure &&
|
|
markerFailure.cleanup !== undefined
|
|
)
|
|
markerCleanupFailure = ArtifactReaderFailure.withCleanup(
|
|
cause,
|
|
markerFailure.cleanup,
|
|
);
|
|
fileFailure = {
|
|
verified: false,
|
|
failure:
|
|
markerFailure instanceof Error
|
|
? markerFailure.message
|
|
: String(markerFailure),
|
|
};
|
|
}
|
|
}
|
|
const failure =
|
|
markerCleanupFailure !== undefined
|
|
? evmInterfaceFailure(
|
|
markerCleanupFailure,
|
|
"artifact-read",
|
|
input.path,
|
|
workerLimits,
|
|
limiter,
|
|
fileFailure,
|
|
timeoutMs,
|
|
)
|
|
: options?.signal?.aborted &&
|
|
(cause === options.signal.reason ||
|
|
(cause instanceof Error && cause.name === "AbortError"))
|
|
? new AnalysisCancelledError(OPERATION, {
|
|
cause,
|
|
...(retainedOutput === undefined
|
|
? {}
|
|
: { capturedOutput: retainedOutput }),
|
|
...(cause instanceof ArtifactReaderFailure &&
|
|
cause.cleanup !== undefined
|
|
? { cleanup: cause.cleanup }
|
|
: {}),
|
|
})
|
|
: evmInterfaceFailure(
|
|
cause,
|
|
phase,
|
|
input.path,
|
|
workerLimits,
|
|
limiter,
|
|
fileFailure,
|
|
timeoutMs,
|
|
);
|
|
const artifactFailure =
|
|
markerCleanupFailure ??
|
|
(cause instanceof ArtifactReaderFailure ? cause : undefined);
|
|
if (artifactFailure !== undefined)
|
|
bindArtifactFileCleanup(artifactFailure, failure);
|
|
result = err(failure);
|
|
}
|
|
const retainCancellation = (outcome: EvmOutcome): EvmOutcome =>
|
|
outcome.ok && options?.signal?.aborted
|
|
? err(
|
|
new AnalysisCancelledError(OPERATION, {
|
|
cause: options.signal.reason,
|
|
partialObservation: evmInterfaceEvidence(input, outcome.value),
|
|
...(retainedOutput === undefined
|
|
? {}
|
|
: { capturedOutput: retainedOutput }),
|
|
}),
|
|
)
|
|
: outcome;
|
|
result = retainCancellation(result);
|
|
if (processOwner !== undefined) {
|
|
const lease = processLeaseFor(processOwner);
|
|
const rootOwnerLease =
|
|
root === undefined ? undefined : rootLeaseFor(root);
|
|
this.#pendingCleanup = {
|
|
input,
|
|
result,
|
|
output: retainedOutput,
|
|
root:
|
|
root === undefined || rootOwnerLease === undefined
|
|
? undefined
|
|
: { owner: root, lease: rootOwnerLease },
|
|
process: { lease },
|
|
};
|
|
if (processCleanup === undefined) {
|
|
const owners = [lease, rootOwnerLease].filter(
|
|
(owner): owner is AnalysisCleanupLease => owner !== undefined,
|
|
);
|
|
if (!result.ok && result.error.cleanupIncomplete) {
|
|
bindEvmCleanupOwners(result.error, owners);
|
|
return result;
|
|
}
|
|
const failure = evmCleanupFailure(
|
|
input,
|
|
result,
|
|
[
|
|
...new Set([
|
|
...(root === undefined ? [] : [root.path]),
|
|
...(lease.cleanupResources ?? []),
|
|
]),
|
|
],
|
|
"Owned EVM worker cleanup remained unresolved without a failure description.",
|
|
retainedOutput,
|
|
processOwner,
|
|
);
|
|
bindEvmCleanupOwners(
|
|
failure,
|
|
owners,
|
|
result.ok ? undefined : result.error,
|
|
);
|
|
return err(failure);
|
|
}
|
|
const failure = evmCleanupFailure(
|
|
input,
|
|
result,
|
|
[
|
|
...new Set([
|
|
...(root === undefined ? [] : [root.path]),
|
|
...processCleanup.resources,
|
|
]),
|
|
],
|
|
processCleanup.reason,
|
|
retainedOutput,
|
|
processOwner,
|
|
);
|
|
bindEvmCleanupOwners(
|
|
failure,
|
|
[lease, rootOwnerLease].filter(
|
|
(lease): lease is AnalysisCleanupLease => lease !== undefined,
|
|
),
|
|
result.ok ? undefined : result.error,
|
|
);
|
|
return err(failure);
|
|
}
|
|
if (root !== undefined) {
|
|
try {
|
|
await rootLeaseFor(root).release();
|
|
} catch (cause: unknown) {
|
|
const retainedRoot = root;
|
|
const lease = rootLeaseFor(retainedRoot);
|
|
const cleanupResult = retainCancellation(result);
|
|
this.#pendingCleanup = {
|
|
input,
|
|
result: cleanupResult,
|
|
output: retainedOutput,
|
|
root: { owner: retainedRoot, lease },
|
|
process: undefined,
|
|
};
|
|
root = undefined;
|
|
const failure = evmCleanupFailure(
|
|
input,
|
|
cleanupResult,
|
|
[retainedRoot.path],
|
|
cause instanceof Error ? cause.message : String(cause),
|
|
retainedOutput,
|
|
cause,
|
|
);
|
|
bindEvmCleanupOwners(
|
|
failure,
|
|
[lease],
|
|
cleanupResult.ok ? undefined : cleanupResult.error,
|
|
);
|
|
return err(failure);
|
|
}
|
|
}
|
|
return retainCancellation(result);
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
this.#closing = true;
|
|
this.#closePromise ??= this.#tail
|
|
.then(async () => {
|
|
const [retirement, artifacts] = await Promise.allSettled([
|
|
this.#retire(),
|
|
this.#artifactScopeLease().release(),
|
|
]);
|
|
const failures: AnalysisError[] = [];
|
|
if (retirement.status === "rejected")
|
|
failures.push(retainedCleanupFailure(retirement.reason));
|
|
else if (retirement.value !== undefined)
|
|
failures.push(retirement.value);
|
|
if (artifacts.status === "rejected") {
|
|
const scopeLease = this.#artifactScopeLease();
|
|
const artifactFailure = retainedCleanupFailure(artifacts.reason);
|
|
const marker = ownerCleanupMarker([scopeLease]);
|
|
new AnalysisCleanupLeaseGroup(marker, [scopeLease]);
|
|
artifactFailure.bindCleanupDependencies([marker]);
|
|
failures.push(artifactFailure);
|
|
}
|
|
if (failures.length > 0) {
|
|
const resources = failures.flatMap((failure) =>
|
|
failure instanceof ProviderCleanupError
|
|
? failure.cleanupResources
|
|
: failure instanceof ArtifactReaderFailure &&
|
|
failure.cleanup !== undefined
|
|
? failure.cleanup.resources
|
|
: ["artifact-resources"],
|
|
);
|
|
const failure = new ProviderCleanupError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
[...new Set(resources)],
|
|
{
|
|
failures: failures.map((failure) =>
|
|
failure instanceof Error ? failure.message : String(failure),
|
|
),
|
|
},
|
|
{ operation: OPERATION, cause: new AggregateError(failures) },
|
|
);
|
|
failure.bindCleanupDependencies(failures);
|
|
throw failure;
|
|
}
|
|
})
|
|
.catch((cause: unknown) => {
|
|
this.#closePromise = undefined;
|
|
throw cause;
|
|
});
|
|
return this.#closePromise;
|
|
}
|
|
|
|
#closedError(): ProviderCleanupError {
|
|
return new ProviderCleanupError(
|
|
EVMOLE_PROVIDER_IDENTITY.id,
|
|
[],
|
|
{ reason: "Provider is closing" },
|
|
{ operation: OPERATION },
|
|
);
|
|
}
|
|
|
|
async #retire(): Promise<ProviderCleanupError | undefined> {
|
|
const pending = this.#pendingCleanup;
|
|
if (pending === undefined) return undefined;
|
|
if (pending.process !== undefined) {
|
|
try {
|
|
await pending.process.lease.release();
|
|
} catch (cause: unknown) {
|
|
const failure = evmCleanupFailure(
|
|
pending.input,
|
|
pending.result,
|
|
[
|
|
...new Set([
|
|
...(pending.root === undefined ? [] : [pending.root.owner.path]),
|
|
...(pending.process.lease.cleanupResources ?? []),
|
|
]),
|
|
],
|
|
cause instanceof Error ? cause.message : String(cause),
|
|
pending.output,
|
|
cause,
|
|
);
|
|
bindEvmCleanupOwners(
|
|
failure,
|
|
[
|
|
pending.process.lease,
|
|
...(pending.root === undefined ? [] : [pending.root.lease]),
|
|
],
|
|
pending.result.ok ? undefined : pending.result.error,
|
|
);
|
|
return failure;
|
|
}
|
|
pending.process = undefined;
|
|
}
|
|
if (pending.root !== undefined) {
|
|
const { owner: root, lease } = pending.root;
|
|
try {
|
|
await lease.release();
|
|
pending.root = undefined;
|
|
} catch (cause: unknown) {
|
|
const failure = evmCleanupFailure(
|
|
pending.input,
|
|
pending.result,
|
|
lease.cleanupResources ?? [root.path],
|
|
cause instanceof Error ? cause.message : String(cause),
|
|
pending.output,
|
|
cause,
|
|
);
|
|
bindEvmCleanupOwners(
|
|
failure,
|
|
[lease],
|
|
pending.result.ok ? undefined : pending.result.error,
|
|
);
|
|
return failure;
|
|
}
|
|
}
|
|
this.#pendingCleanup = undefined;
|
|
return undefined;
|
|
}
|
|
}
|