Skip to content

Commit 0615d0b

Browse files
Merge pull request #35 from runtimeverification/feat/get-ledger-entries
feat: implement getLedgerEntries
2 parents 6a52a7a + a1f3632 commit 0615d0b

11 files changed

Lines changed: 895 additions & 23 deletions

File tree

docs/architecture.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ flowchart TB
5151

5252
**[Detailed documentation](server.md)**
5353

54-
The server implements ten RPC methods — `getHealth`, `getNetwork`, `getLatestLedger`, `getVersionInfo`, `getFeeStats`, `sendTransaction`, `getTransaction`, `getTransactions`, `getLedgers`, and the komet-specific `traceTransaction` extension — and the K semantics answer all of them. JSON-RPC framing (single calls, batch arrays, notifications) is handled in Python; the semantics see one request envelope per invocation.
54+
The server implements eleven RPC methods — `getHealth`, `getNetwork`, `getLatestLedger`, `getVersionInfo`, `getFeeStats`, `sendTransaction`, `getTransaction`, `getTransactions`, `getLedgers`, `getLedgerEntries`, and the komet-specific `traceTransaction` extension — and the K semantics answer all of them. JSON-RPC framing (single calls, batch arrays, notifications) is handled in Python; the semantics see one request envelope per invocation. For `getLedgerEntries` the semantics' answer is an intermediate shape: K performs the state lookups, and `ledger_entries.py` translates between the base64 XDR wire format (`LedgerKey` in, `LedgerEntryData` out) and the JSON the semantics exchange, since K cannot parse or produce XDR.
5555

5656
`sendTransaction` always returns `PENDING` and clients poll `getTransaction` for the result — matching the Stellar RPC async pattern even though the transaction executes synchronously. See [server.md](server.md) for details.
5757

@@ -93,6 +93,7 @@ All of the server's input and output artifacts live in one directory, the *io di
9393
| `traces/trace_<hash>.jsonl` | persistent | the semantics | one execution trace per transaction, keyed by tx hash — the instruction-level records, one JSON object per line. `traceTransaction` returns this file's contents. |
9494
| `ledgers/ledger_<seq>.json` | persistent | the server (on success) | one record per closed ledger — `{sequence, txHash, closedAt, hash, headerXdr, metadataXdr}` — the ledger→transaction index behind `getTransactions` and `getLedgers`. Written in Python because the header artifacts are XDR, which K cannot construct. |
9595
| `requests/request_<n>.json` | persistent | the server | an archive of each incoming JSON-RPC request, numbered by a monotonic counter, kept for debugging. |
96+
| `wasms/<hash>.wasm` | persistent | the server | the raw bytes of each successfully uploaded wasm module, keyed by hex sha256. The K state stores modules parsed (`ModuleDecl`), so `getLedgerEntries` CONTRACT_CODE entries read the original bytes from here. |
9697
| `request.json` | transient | the server | the request envelope for the call in flight (`method`, `id`, `now`, and method-specific fields). The semantics remove it once they respond. |
9798
| `response.json` | transient | the semantics | the JSON-RPC response (`{jsonrpc, id, result}`) for the most recent call. The server reads it back; it is absent when a transaction gets stuck. |
9899

@@ -103,7 +104,7 @@ The world state stays in KORE (rather than a JSON snapshot) because an uploaded
103104
```mermaid
104105
flowchart TB
105106
boot(["server start"]) --> exists{"state.kore exists?"}
106-
exists -->|"no"| init["empty_config() builds the idle K config in KORE<br/>write state.kore · seed metadata.json {latest_ledger: 0}<br/>create receipts/ traces/ requests/"]
107+
exists -->|"no"| init["empty_config() builds the idle K config in KORE<br/>write state.kore · seed metadata.json {latest_ledger: 0}<br/>create receipts/ traces/ requests/ wasms/"]
107108
exists -->|"yes"| reuse["use existing state.kore<br/>seed metadata.json if missing · ensure artifact dirs exist"]
108109
init --> ready(["ready for requests"])
109110
reuse --> ready
@@ -178,6 +179,6 @@ sequenceDiagram
178179

179180
- `resultXdr` / `resultMetaXdr` in `getTransaction` responses (contract return values)
180181
- `simulateTransaction` (dry-run without state mutation)
181-
- `getEvents`, `getLedgerEntries`, `getTransactions`, `getLedgers` and other read-only RPC methods
182+
- `getEvents` and other read-only RPC methods (`getLedgerEntries` is implemented for the entry types the K state tracks: `ACCOUNT`, `CONTRACT_DATA`, `CONTRACT_CODE`)
182183
- `ExtendFootprintTTL` and `RestoreFootprint` operations
183184
- `SCVec` / `SCMap` contract-argument types in the request encoder (`scval_to_json`)

docs/node-semantics.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ insert-handleRequestFile → handleRequestFile
4141
#dispatchMethod(method, request) ← routes on the "method" field
4242
4343
├─ getHealth / getNetwork / getLatestLedger / getVersionInfo / getFeeStats
44-
│ / getTransaction / getTransactions / getLedgers / traceTransaction → #respond(...)
44+
│ / getTransaction / getTransactions / getLedgers / getLedgerEntries / traceTransaction → #respond(...)
4545
4646
└─ sendTransaction → #runTx → run steps
4747
→ #finalizeTx → record receipt + bump ledger → #respond(...)
@@ -67,6 +67,7 @@ If `request.json` is absent, `insert-handleRequestFile` does not fire and K halt
6767
- `getFeeStats` → constant `#feeDistribution` objects (komet-node has no fee market) for `sorobanInclusionFee`/`inclusionFee`, plus a live `latestLedger` number from `metadata.json`; all distribution fields except `ledgerCount` are decimal strings, matching real stellar-rpc's Go `,string` encoding
6868
- `getTransaction` → reads the hash's `receipts/receipt_<hash>.json` file; returns the stored receipt merged with the ledger-range fields (`latestLedger`/`latestLedgerCloseTime`/`oldestLedger`/`oldestLedgerCloseTime`), or `{ "status": "NOT_FOUND", ... }` (with the same ledger-range fields) when the file is absent
6969
- `getTransactions` / `getLedgers` → walk the per-ledger index files `ledgers/ledger_<seq>.json` from the envelope's `startSeq` up to the latest ledger, taking at most `limit` records (`#txInfos` / `#ledgerInfos`), and format the history page (`#txHistoryPage` / `#ledgerHistoryPage`). Parameter validation and cursor resolution happen in the server; the response `cursor` (`#pageCursor`) names the page's last record when the page is full — a TOID for transactions, the plain sequence for ledgers — and is empty otherwise. Serialization matches real stellar-rpc, including its quirks: per-transaction `createdAt` is a JSON number (unlike singular `getTransaction`), per-ledger `ledgerCloseTime` is a decimal string, and empty `resultXdr`/`resultMetaXdr` stubs are omitted (`#optXdrEntry`)
70+
- `getLedgerEntries` → looks each *key descriptor* of the request up in the world-state cells (`<accounts>`, `<contracts>`, `<contractData>`, `<contractCodes>`) and responds with `{ "entries": [...], "latestLedger": <int> }`. The server decodes the base64 `LedgerKey` XDR into the descriptors beforehand and re-encodes the found entries as `LedgerEntryData` XDR afterwards (`ledger_entries.py`) — the entries in K's response are an intermediate JSON shape (per-kind payloads such as `balance`, `wasmHash`, or an ScVal value serialised by `#scVal2JSON`, the inverse of `#decodeArg`). Keys that match nothing are skipped via an `[owise]` rule — per the spec they are not an error
7071

7172
`#respond(ID, RESULT)` is the shared terminal: it writes the JSON-RPC envelope to `response.json`, removes `request.json`, and sets the exit code to 0. `#respondError(ID, CODE, MSG)` is its error counterpart, writing `{jsonrpc, id, error: {code, message}}` instead; the unknown-method fallback uses it to answer `-32601 Method not found` (a safety net — the Python server filters unknown methods before they reach K).
7273

@@ -149,6 +150,10 @@ rule HexBytes(S) => Int2Bytes(lengthString(S) /Int 2, String2Base(S, 16), BE)
149150
requires lengthString(S) >Int 0
150151
```
151152

153+
### `Bytes2Hex(Bytes) → String`
154+
155+
The inverse direction is K's built-in `Bytes2Hex` (hook `BYTES.bytes2hex`): it encodes `Bytes` as a lowercase, zero-padded hex string. The `getLedgerEntries` rules use it to report hashes, addresses, and `ScBytes` values to the server.
156+
152157
### `string2WasmToken(String) → WasmStringToken`
153158

154159
`string2WasmToken` wraps a K `String` into a `WasmStringToken` (`hook(STRING.string2token)`). It is required because `callTx` expects a `WasmString` for the function name.

docs/notes.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
| [`server.py`](server.md)`StellarRpcServer` | Long-running HTTP/JSON-RPC server wrapping the one-shot K interpreter; `handle_rpc` dispatch; owns the io-dir files. Holds no ledger or receipt state. |
1414
| [`transaction.py`](transaction.md)`TransactionEncoder` | XDR → request envelope + (for wasm uploads) kasmer steps; address/contract-id helpers. |
1515
| [`interpreter.py`](interpreter.md)`NodeInterpreter` | Runs request envelopes through `llvm_interpret`; persists `state.kore`. No `kast``kore` whole-config conversions. |
16-
| `scval.py` | XDR `SCVal` ↔ Komet `SCValue` (`scvalue_from_xdr`) and XDR `SCVal` → request JSON (`scval_to_json`). |
16+
| `scval.py` | XDR `SCVal` ↔ Komet `SCValue` (`scvalue_from_xdr`), XDR `SCVal` ↔ request/response JSON (`scval_to_json`, `scval_from_json`). |
17+
| `ledger_entries.py` | `getLedgerEntries` XDR translation: base64 `LedgerKey` → key descriptors for the semantics, intermediate entries → base64 `LedgerEntryData`. |
1718
| [`kdist/node.md`](node-semantics.md) | The K RPC layer: reads `request.json`, dispatches, updates `metadata.json` and the per-transaction `receipts/` files, writes `response.json`. |
1819

1920
State lives in the io dir as `state.kore` (KORE world state) and `metadata.json` (ledger counter), with per-transaction receipts and traces under `receipts/` and `traces/`. See [architecture.md](architecture.md).
@@ -22,7 +23,7 @@ State lives in the io dir as `state.kore` (KORE world state) and `metadata.json`
2223

2324
## Tests (`src/tests/integration/`)
2425

25-
- `test_server.py` drives the running HTTP server end-to-end. It exercises the read-only methods, `sendTransaction` + `getTransaction`, the history methods (`getTransactions`/`getLedgers` response shapes, pagination, and parameter validation against the official spec and the Go protocol structs), ledger increments, the full lifecycle (create → upload wasm → deploy → invoke), and the `traceTransaction` flows. `test_call_tx_with_args` deploys `args.wat` and calls functions with `bool`, `u32`, `i32`, `u64`, `i64`, `u128`, `i128`, and `symbol` arguments, exercising the `scval_to_json` / `#decodeArg` pipeline.
26+
- `test_server.py` drives the running HTTP server end-to-end. It exercises the read-only methods, `sendTransaction` + `getTransaction`, the history methods (`getTransactions`/`getLedgers` response shapes, pagination, and parameter validation against the official spec and the Go protocol structs), ledger increments, the full lifecycle (create → upload wasm → deploy → invoke), the `traceTransaction` flows, and `getLedgerEntries` (account, contract code, contract instance, and persistent storage entries via `storage.wat`, plus its parameter validation). `test_call_tx_with_args` deploys `args.wat` and calls functions with `bool`, `u32`, `i32`, `u64`, `i64`, `u128`, `i128`, and `symbol` arguments, exercising the `scval_to_json` / `#decodeArg` pipeline.
2627
- `test_integration.py` and `test_unit.py` hold small sanity checks.
2728

2829
Run with `make test` (requires `make kdist-build` first).
@@ -36,8 +37,9 @@ The tests do not yet cover `bytes` / `address` SCVal arguments or `SCVec` / `SCM
3637
- `resultXdr` / `resultMetaXdr` are empty stubs (contract return values not surfaced).
3738
- `sendTransaction`'s `errorResultXdr` is always a generic `txMALFORMED` result (no per-cause codes such as `txBAD_SEQ` — sequence numbers, fees, and signatures are not modelled), and `diagnosticEventsXdr` is never populated (both fields are optional in the spec). `TRY_AGAIN_LATER` is never returned by design: it signals mempool backpressure, which cannot arise in a synchronous node without a mempool.
3839
- `SCVec` / `SCMap` contract arguments are not yet encoded.
39-
- The `xdrFormat` parameter is accepted on `getTransaction`, `sendTransaction`, `getTransactions`, and `getLedgers`, but only the default `'base64'` is supported; `'json'` is rejected with `-32602`.
40-
- `simulateTransaction`, `getEvents`, `getLedgerEntries`, and TTL/footprint operations are not implemented.
40+
- The `xdrFormat` parameter is accepted on `getTransaction`, `sendTransaction`, `getTransactions`, `getLedgers`, and `getLedgerEntries`, but only the default `'base64'` is supported; `'json'` is rejected with `-32602`.
41+
- `simulateTransaction`, `getEvents`, and TTL/footprint operations are not implemented.
4142
- `getFeeStats` reports constant distributions (there is no fee market); only its `latestLedger` is live.
4243
- `getTransactions` / `getLedgers` serve only ledgers with an index file under `ledgers/`; io-dirs created before the ledger index existed resume fine, but their earlier ledgers do not appear in the history.
44+
- `getLedgerEntries` covers the entry types the K state tracks (`ACCOUNT`, `CONTRACT_DATA`, `CONTRACT_CODE`); other key types are reported as not found. `lastModifiedLedgerSeq` is approximated by the current ledger (per-entry modification ledgers are not tracked), and only the balance is real in `ACCOUNT` entries (sequence number, thresholds, etc. are synthesised constants).
4345
- Receipts are not a stable format: older io-dirs store `ledger`/`createdAt` with pre-spec types and lack `applicationOrder`/`feeBump`. Resume only io-dirs written by the same version, or start fresh.

docs/server.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Once the socket is bound, `serve` logs three lines to stderr: whether it is star
7979
```
8080
startup (state.kore absent):
8181
→ empty_config() → state.kore ; metadata.json {latest_ledger:0}
82-
→ create receipts/ traces/ requests/
82+
→ create receipts/ traces/ requests/ wasms/
8383
8484
per successful transaction:
8585
→ the semantics run the steps (trace → traces/trace_<hash>.jsonl),
@@ -228,6 +228,23 @@ Serialization follows real stellar-rpc: ledger sequences and the top-level close
228228

229229
Per-ledger `ledgerCloseTime` is a *string* holding a decimal number (matching real stellar-rpc's Go `,string` encoding), while the top-level close times are numbers. The `cursor` is the last returned ledger sequence, stringified, under the same full-page rule as `getTransactions`.
230230

231+
### `getLedgerEntries`
232+
233+
`getLedgerEntries` takes `keys` (an array of up to 200 base64-encoded `LedgerKey` XDR strings; required) and an optional `xdrFormat` (only `"base64"` is supported — `"json"` is rejected with `-32602`). It returns the entries found for the supported key types — `ACCOUNT`, `CONTRACT_DATA` (both the contract-instance entry and persistent/temporary storage), and `CONTRACT_CODE` — the ones the K world state tracks. Keys that do not resolve (unknown, or of an untracked type) are not an error; they are simply absent from `entries`.
234+
235+
**Response** (`latestLedger` and `lastModifiedLedgerSeq` are JSON numbers; `liveUntilLedgerSeq` appears only on Soroban entries):
236+
```json
237+
{
238+
"entries": [
239+
{ "key": "<base64 LedgerKey>", "xdr": "<base64 LedgerEntryData>",
240+
"lastModifiedLedgerSeq": 4, "liveUntilLedgerSeq": 4095 }
241+
],
242+
"latestLedger": 4
243+
}
244+
```
245+
246+
This is the one method whose response the server post-processes: the semantics look the keys up in the K state and answer with intermediate JSON entries, and `ledger_entries.py` re-encodes them as `LedgerEntryData` XDR (K cannot produce XDR). Because the K state keeps uploaded wasm parsed, the server stores the raw bytes under `wasms/<hash>.wasm` at upload time and reattaches them to `CONTRACT_CODE` entries. The semantics do not track per-entry modification ledgers, so `lastModifiedLedgerSeq` reports the current ledger; `ACCOUNT` entries carry the real balance but synthesised constants for the remaining required fields (sequence number 0, master weight 1, no signers).
247+
231248
---
232249

233250
## Failure fallback
@@ -246,4 +263,4 @@ komet-node [--host HOST] [--port PORT] [--io-dir DIR]
246263
|---|---|---|
247264
| `--host` | `localhost` | Bind address |
248265
| `--port` | `8000` | Port |
249-
| `--io-dir` | a fresh temp dir | Directory holding every artifact (`state.kore`, `metadata.json`, `receipts/`, `traces/`, `ledgers/`, `requests/`) |
266+
| `--io-dir` | a fresh temp dir | Directory holding every artifact (`state.kore`, `metadata.json`, `receipts/`, `traces/`, `ledgers/`, `requests/`, `wasms/`) |

0 commit comments

Comments
 (0)