NodeInterpreter runs komet-node's RPC requests through the compiled K semantics (LLVM backend). It builds the initial configuration, feeds each request envelope to the interpreter against state.kore, and persists the resulting world state. It knows nothing about Stellar — XDR decoding lives in TransactionEncoder, and RPC dispatch / bookkeeping / response formatting live in node.md.
class NodeInterpreter:
definition: SimbolikDefinition # compiled K definition (komet-node.simbolik)SimbolikDefinition is a thin subclass of komet.SorobanDefinition, pointing to the komet-node.simbolik compiled K definition (cached under ~/.cache/kdist-*/komet-node/simbolik/). There is no network_passphrase or trace here — those belong to the request side (TransactionEncoder); the interpreter only runs K.
Whole-configuration kore_to_kast / kast_to_kore conversions take seconds and get slower as the configuration grows, so the interpreter avoids them entirely. state.kore is only ever parsed with KoreParser (KORE text → KORE AST, which is cheap) and handed straight to llvm_interpret. Terms that must be constructed are built directly in KORE.
empty_config() produces the initial blank-slate state.kore. It builds the top-cell initializer in KORE — seeding $PGM with a single setExitCode(0) step and $TRACE with an empty string — and runs it through the interpreter. No krun subprocess and no kast conversion are involved.
config = top_cell_initializer({
'$PGM': inj(SortSteps, K_ITEM, kasmerSteps(setExitCode(0), .Steps)), # built in KORE
'$TRACE': inj(SortString, K_ITEM, str_dv('')),
})
with tempfile.TemporaryDirectory() as isolated_dir: # see note below
return _llvm_interpret(self.definition.path, config, cwd=isolated_dir).textThe result is the empty idle K configuration — no accounts, no contracts, no storage.
The run happens in a throwaway empty directory on purpose. The idle configuration ends with empty <k>/<program> cells, which is exactly the precondition that makes the request-handling rule fire if a request.json is present. Running in an empty directory guarantees no stray request.json is picked up and dispatched into the configuration that is about to be saved as state.kore.
run is the main entry point. It runs a single RPC request envelope through the following steps:
- Write the request envelope to
request.jsoninio_dir, and delete any staleresponse.json. - Parse
state.korewithKoreParser(no kast conversion). - For a wasm upload only, splice the upload steps into the
<program>cell (see below). - Run the interpreter with its subprocess working directory set to
io_dir(so the K file-system hooks resolve the relative pathsrequest.json,response.json,metadata.json, and thereceipts/andtraces/files). The directory is set on the subprocess only — the server's own process neverchdirs, so concurrent requests in other threads are unaffected. - If the semantics wrote
response.json, persist the new configuration tostate.koreand return the response text. If not, the transaction got stuck (failed) — leavestate.koreunchanged and returnNone, so the caller can synthesise a failure response.
A wasm upload cannot be expressed as a JSON step, because the resulting ModuleDecl (the parsed Wasm AST from wasm2kast) has no JSON form. Instead the steps are injected directly into the <program> cell of the already-parsed configuration, so KASMER runs them before the request is dispatched.
The injection is done at the KORE level: the small steps term is converted to KORE and spliced into the <program> cell of the parsed pattern. The whole-configuration round-trip is deliberately avoided. The one remaining kast_to_kore call here is bounded by the size of the uploaded module (the only thing that can originate solely as KAST), not by the accumulated world state.
steps_kore = kast_to_kore(self.definition.kdefinition, steps_of(steps), KSort('Steps'))
return _set_cell(pattern, "<program> cell symbol", steps_kore) # KORE-level spliceBecause Soroban allows only a single host-function operation per transaction, a wasm-upload transaction is exactly one uploadWasm op — this path never carries anything else.
The mapping from Stellar operations to kasmer steps is performed by TransactionEncoder; the interpreter only runs the result.
| Stellar operation | kasmer step | Delivered via |
|---|---|---|
CreateAccount |
setAccount(Account(bytes), stroops) |
JSON step in request.json |
InvokeHostFunction / upload wasm |
uploadWasm(hash, ModuleDecl) |
<program> cell (KORE) |
InvokeHostFunction / create contract (V1, V2) |
deployContract(from, address, wasmHash) |
JSON step in request.json |
InvokeHostFunction / invoke contract |
callTx(from, to, func, args, Void) |
JSON step in request.json |
NodeInterpreterError is raised for interpreter-level failures (e.g. the interpreter crashing or producing no output). A transaction failure is not an exception: the semantics get stuck without writing response.json, so run returns None and the server records a FAILED receipt while leaving state.kore unchanged (the state effectively rolls back).