|
| 1 | +# `interpreter.py` — `NodeInterpreter` |
| 2 | + |
| 3 | +`NodeInterpreter` is the core of komet-node. It translates a `stellar_sdk.Transaction` into K execution steps, runs them through the compiled K semantics via `krun`, and returns the updated blockchain state. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Class structure |
| 8 | + |
| 9 | +```python |
| 10 | +class NodeInterpreter: |
| 11 | + definition: SimbolikDefinition # compiled K definition (komet-node.simbolik) |
| 12 | + network_passphrase: str |
| 13 | + trace: bool # whether to enable instruction tracing |
| 14 | +``` |
| 15 | + |
| 16 | +`SimbolikDefinition` is a thin subclass of `komet.SorobanDefinition`, pointing to the `komet-node.simbolik` compiled K definition (cached under `~/.cache/kdist-*/komet-node/simbolik/`). |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## Execution paths |
| 21 | + |
| 22 | +`run_transaction(state_file, transaction)` is the main entry point. It chooses between two execution strategies depending on the transaction content: |
| 23 | + |
| 24 | +``` |
| 25 | +Transaction |
| 26 | + │ |
| 27 | + ├─ can encode to JSON? ──yes──► JSON fast path (run_request_file) |
| 28 | + │ writes request.json, krun reads it |
| 29 | + │ |
| 30 | + └─ no (wasm upload) ────────► KORE round-trip (run_steps) |
| 31 | + Python parses state.kore, mutates AST, |
| 32 | + re-serializes, krun on full KORE term |
| 33 | +``` |
| 34 | + |
| 35 | +### JSON fast path (`run_request_file`) |
| 36 | + |
| 37 | +Used for all operations except wasm upload. The goal is to avoid Python-side KORE parsing and AST manipulation entirely. |
| 38 | + |
| 39 | +1. `encode_transaction_to_json(transaction)` serializes the transaction as a JSON string. Returns `None` if any operation cannot be expressed as JSON (currently only wasm upload). |
| 40 | +2. A temporary working directory is created. `request.json` is written there. |
| 41 | +3. `krun` is invoked with the current `state.kore` as input (`--term`). The K semantics detect `request.json`, read and decode it, execute the steps, remove the file, and halt. The updated state is captured from stdout. |
| 42 | +4. If tracing is enabled, `trace.jsonl` is read from the temp dir and included in the response. |
| 43 | + |
| 44 | +```python |
| 45 | +with temp_working_directory() as root: |
| 46 | + (root / 'request.json').write_text(request_str) |
| 47 | + res = _krun(input_file=state_file, definition_dir=..., term=True, output=KORE) |
| 48 | + trace = (root / 'trace.jsonl').read_text() if tracing else None |
| 49 | + return InterpreterResponse(final_kore=res.stdout, trace=trace) |
| 50 | +``` |
| 51 | + |
| 52 | +Each request runs in its own temp dir so that concurrent requests (if added) and trace files are isolated. |
| 53 | + |
| 54 | +### KORE round-trip (`run_steps`) |
| 55 | + |
| 56 | +Used only for wasm upload. Wasm upload requires embedding a parsed `ModuleDecl` (the Wasm AST) directly into the K configuration — something that cannot be expressed as a flat JSON string. |
| 57 | + |
| 58 | +1. Parse `state.kore` into a Python KORE AST. |
| 59 | +2. Convert the AST to KAst (`kore_to_kast`). |
| 60 | +3. Inject K steps (including the `ModuleDecl`) into the `<program>` cell. |
| 61 | +4. Re-serialize to KORE (`kast_to_kore`) and run krun on the full term. |
| 62 | + |
| 63 | +This path is slower because it involves full KORE parsing and re-serialization on every wasm upload, but it is only triggered once per contract deployment. |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## Supported operations |
| 68 | + |
| 69 | +| Stellar operation | K step | Execution path | |
| 70 | +|---|---|---| |
| 71 | +| `CreateAccount` | `setAccount(Account(bytes), stroops)` | JSON | |
| 72 | +| `InvokeHostFunction` / upload wasm | `uploadWasm(hash, ModuleDecl)` | KORE round-trip | |
| 73 | +| `InvokeHostFunction` / create contract (V1, V2) | `deployContract(from, address, wasmHash)` | JSON | |
| 74 | +| `InvokeHostFunction` / invoke contract | `callTx(from, to, func, args, Void)` | JSON | |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## JSON request format |
| 79 | + |
| 80 | +The JSON fast path writes `request.json` with the following structure. **Key ordering is significant**: the K JSON sort is ordered, so Python dicts must produce keys in the same order as the K pattern-match rules in `node.md`. |
| 81 | + |
| 82 | +```json |
| 83 | +{ |
| 84 | + "steps": [ |
| 85 | + { "op": "setAccount", "account": "<hex32>", "balance": <int> }, |
| 86 | + { "op": "deployContract", "from": "<hex32>", "address": "<hex32>", "wasmHash": "<hex32>" }, |
| 87 | + { "op": "callTx", "from": "<hex32>", "fromIsContract": <bool>, |
| 88 | + "func": "<name>", "to": "<hex32>", "args": [ ... ] } |
| 89 | + ] |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +### SCVal argument encoding |
| 94 | + |
| 95 | +Contract function arguments (`callTx` args) are encoded as JSON dicts: |
| 96 | + |
| 97 | +| SCVal type | JSON encoding | |
| 98 | +|---|---| |
| 99 | +| `SCV_BOOL` | `{"type": "bool", "value": true\|false}` | |
| 100 | +| `SCV_I32` / `SCV_U32` | `{"type": "i32"\|"u32", "value": <int>}` | |
| 101 | +| `SCV_I64` / `SCV_U64` | `{"type": "i64"\|"u64", "value": <int>}` | |
| 102 | +| `SCV_I128` / `SCV_U128` | `{"type": "i128"\|"u128", "value": <int>}` (combined hi/lo) | |
| 103 | +| `SCV_SYMBOL` | `{"type": "symbol", "value": "<str>"}` | |
| 104 | +| `SCV_BYTES` | `{"type": "bytes", "value": "<lowercase hex>"}` | |
| 105 | +| `SCV_ADDRESS` (account) | `{"type": "address", "addrType": "account", "value": "<hex32>"}` | |
| 106 | +| `SCV_ADDRESS` (contract) | `{"type": "address", "addrType": "contract", "value": "<hex32>"}` | |
| 107 | + |
| 108 | +--- |
| 109 | + |
| 110 | +## Initial configuration (`empty_config`) |
| 111 | + |
| 112 | +`empty_config()` produces the initial blank-slate `state.kore` by running krun with a single `setExitCode(0)` step. The output is the empty idle K configuration — no accounts, no contracts, no storage — which the server writes to `state.kore` on first startup. |
| 113 | + |
| 114 | +When `trace=True`, `empty_config` passes two extra arguments to krun: |
| 115 | + |
| 116 | +```python |
| 117 | +cmap = {'TRACE': str_dv('trace.jsonl').text} # K string token |
| 118 | +pmap = {'TRACE': 'cat'} # parser: pass through as-is |
| 119 | +``` |
| 120 | + |
| 121 | +These initialize the `<ioDir>` configuration cell (part of the `<trace>` cell, compiled in from the `k-tracing` selector) to `"trace.jsonl"`. Because this value is baked into `state.kore`, every subsequent krun invocation reads it and writes traces to `trace.jsonl` in the current working directory — which is the per-request temp dir. |
| 122 | + |
| 123 | +--- |
| 124 | + |
| 125 | +## Tracing |
| 126 | + |
| 127 | +When the server is started with `--trace`, every `callTx` (contract invocation) produces an instruction-level execution trace. The trace records the VM state at each WebAssembly instruction. |
| 128 | + |
| 129 | +**How it works**: |
| 130 | + |
| 131 | +1. `empty_config()` bakes `<ioDir>trace.jsonl</ioDir>` into `state.kore`. |
| 132 | +2. For each transaction, `run_request_file` creates a temp dir, runs krun from it. |
| 133 | +3. The tracing K rules (from `soroban-semantics.tracing`) detect the non-empty `<ioDir>` and append one JSON record per instruction to `trace.jsonl` in the temp dir. |
| 134 | +4. After krun finishes, the trace file is read and returned in `InterpreterResponse.trace`. |
| 135 | +5. The server stores the trace string in `_transactions[hash]['trace']`, retrievable via `getTransaction`. |
| 136 | + |
| 137 | +**Trace format** (one JSON record per line): |
| 138 | + |
| 139 | +```json |
| 140 | +{"pos": 597, "instr": ["local.get", 0], "stack": [["i64", 4]], "locals": {"0": ["i64", 4]}} |
| 141 | +``` |
| 142 | + |
| 143 | +| Field | Description | |
| 144 | +|---|---| |
| 145 | +| `pos` | Byte offset of the instruction in the binary, or `null` for synthetic instructions | |
| 146 | +| `instr` | Instruction name and operands as a JSON array | |
| 147 | +| `stack` | Value stack at instruction entry, as `[type, value]` pairs | |
| 148 | +| `locals` | Local variable bindings, keyed by index, as `[type, value]` pairs | |
| 149 | + |
| 150 | +Tracing is only active for the LLVM backend. The `komet-node.simbolik` definition is compiled with `md_selector: 'k | k-tracing'`, so the tracing rules are always present; they are activated solely by `<ioDir>` being non-empty. |
| 151 | + |
| 152 | +--- |
| 153 | + |
| 154 | +## `InterpreterResponse` |
| 155 | + |
| 156 | +```python |
| 157 | +class InterpreterResponse(NamedTuple): |
| 158 | + final_kore: str # updated K configuration (to write back to state.kore) |
| 159 | + trace: str | None # JSONL trace string, or None if tracing is disabled |
| 160 | +``` |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## Error handling |
| 165 | + |
| 166 | +`NodeInterpreterError` is raised when krun exits with a non-zero return code. The server catches this, stores a `FAILED` result for the transaction, and leaves `state.kore` unchanged (the state effectively rolls back). |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +## Address utilities |
| 171 | + |
| 172 | +`NodeInterpreter` also provides helpers for Stellar address encoding/decoding: |
| 173 | + |
| 174 | +- `decode_account_id(addr)` — G-strkey → 32-byte public key |
| 175 | +- `decode_contract_id(addr)` — C-strkey → 32-byte contract ID |
| 176 | +- `contract_address_from_deployer_address(deployer, salt)` — computes the C-strkey that `CREATE_CONTRACT` will assign |
| 177 | +- `contract_id_from_preimage(preimage)` — SHA-256 of the `HashIDPreimage` as used by Stellar |
0 commit comments