Skip to content

Commit 460d81e

Browse files
Merge branch 'main' into feat/rpc-plumbing
Resolve conflicts with main.
2 parents 6d12760 + bd02497 commit 460d81e

12 files changed

Lines changed: 572 additions & 124 deletions

File tree

README.md

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,68 @@ curl -s http://localhost:8000 -H 'Content-Type: application/json' \
130130
"jsonrpc": "2.0",
131131
"id": 1,
132132
"result": [
133+
{
134+
"pos": null, "instr": ["callContract"],
135+
"from": {"type": "address", "addrType": "account", "value": "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8"},
136+
"to": {"type": "address", "addrType": "contract", "value": "6a20fec1a9081773a5f23ce370f925f236346e510438ddd6d40f6b2711c134e0"},
137+
"function": "foo", "args":[], "depth":1, "storage":[]
138+
},
133139
{"pos": 3, "instr": ["const", "i32", 1048576], "stack": [], "locals": {}},
134140
{"pos": 11, "instr": ["const", "i32", 1048576], "stack": [], "locals": {}},
135141
{"pos": 19, "instr": ["const", "i32", 1048576], "stack": [], "locals": {}},
136142
{"pos": null, "instr": ["block"], "stack": [], "locals": {}},
137-
{"pos": 3, "instr": ["const", "i64", 2], "stack": [], "locals": {}}
143+
{"pos": 3, "instr": ["const", "i64", 2], "stack": [], "locals": {}},
144+
{"pos": null, "instr": ["endWasm"], "success":true, "depth":1, "result": {"type": "void"}}
138145
]
139146
}
140147
```
141148

142-
Each trace record captures the VM state at instruction entry: `pos` is the instruction's byte offset in the binary (`null` for synthetic instructions), `instr` is the instruction and its operands, and `stack`/`locals` are the value stack and locals as `[type, value]` pairs. See [docs/interpreter.md](docs/interpreter.md) for the full trace format.
149+
A trace can contain five kinds of records:
150+
151+
- `callContract`
152+
- Wasm instruction records
153+
- `hostCall`
154+
- `contractData`
155+
- `endWasm`
156+
157+
The example above only has three of these: `callContract`, instruction records, and `endWasm`. `foo()` doesn't touch storage or call any host functions, so no `contractData` or `hostCall` records show up.
158+
159+
Here's what each record type carries:
160+
161+
- `callContract`: logged for each contract call in the transaction, including contract-to-contract calls. Records the caller, the callee, the function name, the arguments, the call depth, and the callee's storage before the call runs.
162+
- Instruction records: logged at each WebAssembly instruction's entry. `pos` is the instruction's byte offset in the binary (`null` for synthetic instructions), `instr` is the instruction and its operands, and `stack`/`locals` are the value stack and locals as `[type, value]` pairs.
163+
- `hostCall`: logged when the contract calls a host function. `instr` gives `["hostCall", moduleId, functionId]`, identifying which host function ran. `locals` holds the function's arguments, indexed by position. Host calls don't use the stack, so `stack` is absent.
164+
Here's a `hostCall` record for a call to `put_contract_data`, module id `l`, function id `_`:
165+
```jsonc
166+
{
167+
"pos": null,
168+
"instr": ["hostCall", "l", "_"],
169+
"locals": {"2": ["i64",0], "1": ["i64",530242871224172548], "0": ["i64",45954062]}
170+
}
171+
```
172+
173+
- `contractData`: logged for storage updates. Gives the contract and the storage type (`instance`, `persistent`, or `temporary`). A `put` carries the key and value as its two args; a `del` carries only the key.
174+
Here's a `contractData` record for a `put`, followed by a `del` on the same key:
175+
```jsonc
176+
{
177+
"pos": null,
178+
"instr": ["contractData", "put", "temporary"],
179+
"contract": {"type": "address", "addrType": "contract", "value": "746573742d7363"},
180+
"args": [{"type": "symbol", "value": "foo"}, {"type": "u32", "value": 123456789}]
181+
}
182+
{
183+
"pos": null,
184+
"instr": ["contractData", "del", "temporary"],
185+
"contract": {"type": "address", "addrType": "contract", "value": "746573742d7363"},
186+
"args": [{"type": "symbol", "value": "foo"}]
187+
}
188+
```
189+
190+
- `endWasm`: logged once at the end of a call. Records whether the call succeeded, its depth, and its result.
191+
192+
193+
194+
See [docs/interpreter.md](docs/interpreter.md) for the full trace format.
143195

144196
---
145197

docs/node-semantics.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ If `request.json` is absent, `insert-handleRequestFile` does not fire and K halt
5656

5757
## Dispatch and the read-only methods
5858

59-
`#dispatch` reads the `method` field and routes to a per-method rule. The read-only methods answer directly from constants and the bookkeeping files:
59+
`#dispatch` reads the `method` field and routes to a per-method rule. The read-only methods answer directly from constants and the bookkeeping files. Response shapes follow real stellar-rpc's serialization: ledger sequences and `protocolVersion` are JSON numbers; close-time fields are int64-as-string (JSON strings holding a decimal integer).
6060

61-
- `getHealth``{ "status": "healthy" }`
62-
- `getNetwork``{ "friendbotUrl": null, "passphrase": ..., "protocolVersion": ... }` (passphrase/version come from the request, keeping the semantics network-agnostic)
63-
- `getLatestLedger` → reads `metadata.json` and returns `{ "id": <64 zeros>, "protocolVersion": ..., "sequence": <latest_ledger> }`
64-
- `getTransaction` → reads the hash's `receipts/receipt_<hash>.json` file; returns the stored receipt merged with the current `latestLedger`/`latestLedgerCloseTime`, or `{ "status": "NOT_FOUND", ... }` when the file is absent
61+
- `getHealth``{ "status": "healthy", "latestLedger": N, "latestLedgerCloseTime": <now>, "oldestLedger": 0, "oldestLedgerCloseTime": "0", "ledgerRetentionWindow": N+1 }` (everything since genesis is retained)
62+
- `getNetwork``{ "passphrase": ..., "protocolVersion": ... }` (passphrase/version come from the request, keeping the semantics network-agnostic; `friendbotUrl` is omitted — no friendbot)
63+
- `getLatestLedger` → reads `metadata.json` and returns `{ "id": #ledgerId(seq), "protocolVersion": ..., "sequence": <latest_ledger> }`; `#ledgerId` derives a deterministic per-ledger 64-hex id from the sequence (the semantics have no hash hooks, so it is a fixed odd-constant multiply mod 2^256, which is injective)
64+
- `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
6565

66-
`#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.
66+
`#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).
6767

6868
---
6969

@@ -83,9 +83,11 @@ If `request.json` is absent, `insert-handleRequestFile` does not fire and K halt
8383

8484
1. writes `metadata.json` with `latest_ledger + 1`,
8585
2. writes the receipt to `receipts/receipt_<hash>.json`:
86-
`{ status: "SUCCESS", ledger, createdAt, envelopeXdr, resultXdr: "", resultMetaXdr: "" }`,
86+
`{ status: "SUCCESS", applicationOrder: 1, feeBump: false, envelopeXdr, resultXdr: "", resultMetaXdr: "", ledger, createdAt }`,
8787
3. responds with `{hash, status: "PENDING", latestLedger, latestLedgerCloseTime}`.
8888

89+
A `sendTransaction` whose hash already has a receipt never reaches `#runTx`: dispatch answers `{hash, status: "DUPLICATE", latestLedger, latestLedgerCloseTime}` directly, without executing anything or advancing the ledger. (For wasm uploads the Python server also suppresses the `<program>` injection on duplicates, since those steps would run before dispatch.)
90+
8991
The trace is not part of the receipt — the executing steps already appended it to `traces/trace_<hash>.jsonl`. Reaching `#finalizeTx` means the steps completed without getting stuck, so the status is `SUCCESS`. A failed transaction gets stuck before this point, `response.json` is never written, and the Python server records the `FAILED` receipt instead.
9092

9193
### traceTransaction

docs/notes.md

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,9 @@ The tests do not yet cover `bytes` / `address` SCVal arguments or `SCVec` / `SCM
3333

3434
## Known gaps
3535

36-
Measured against the official Stellar RPC surface for protocol 22 (the OpenRPC spec plus the `protocols/rpc` structs in go-stellar-sdk, which are what real stellar-rpc emits).
37-
38-
### Missing methods
39-
40-
`simulateTransaction`, `getLedgerEntries`, `getEvents`, `getTransactions`, `getLedgers`, `getFeeStats`, and `getVersionInfo` are not implemented and return `-32601` Method not found. TTL/footprint operations are likewise unsupported.
41-
42-
### Deviations in the implemented methods
43-
44-
- `getHealth` returns only `{status}`; the spec adds `latestLedger`, `oldestLedger`, and `ledgerRetentionWindow` (numbers).
45-
- `getNetwork` returns `protocolVersion` as a string (`"22"`) where the spec has a number, and `friendbotUrl: null` where real stellar-rpc omits the field entirely.
46-
- `getLatestLedger` returns `protocolVersion` as a string, and `id` is a constant all-zeros hash instead of a per-ledger value.
47-
- `sendTransaction` returns `latestLedger` as a string; the statuses `DUPLICATE`, `ERROR`, and `TRY_AGAIN_LATER` are never returned (resubmitting a transaction re-executes it instead of reporting `DUPLICATE`), and `errorResultXdr` / `diagnosticEventsXdr` are never returned.
48-
- `getTransaction` is missing the required `oldestLedger` / `oldestLedgerCloseTime` (all statuses) and `applicationOrder` / `feeBump` (success/failed); `latestLedger` and `ledger` are strings where the spec has numbers; `resultXdr` / `resultMetaXdr` are empty-string stubs (contract return values are not surfaced); the `hash` parameter is not validated against the 64-lowercase-hex format.
49-
50-
### Cross-cutting
51-
52-
- The `xdrFormat` parameter is accepted on `getTransaction` and `sendTransaction`, but only the default `'base64'` is supported; `'json'` is rejected with `-32602`.
53-
- The K semantics' unknown-method fallback answers with `result: null` instead of error `-32601`. This is currently unreachable — the Python layer rejects unknown methods before the semantics run — but latent.
36+
- `resultXdr` / `resultMetaXdr` are empty stubs (contract return values not surfaced).
37+
- `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.
5438
- `SCVec` / `SCMap` contract arguments are not yet encoded.
55-
- `traceTransaction` is a komet-specific extension, absent from real Stellar RPC (see [server.md](server.md#tracetransaction-komet-specific-extension)).
39+
- The `xdrFormat` parameter is accepted on `getTransaction` and `sendTransaction`, but only the default `'base64'` is supported; `'json'` is rejected with `-32602`.
40+
- `simulateTransaction`, `getEvents`, `getLedgerEntries`, `getFeeStats`, and TTL/footprint operations are not implemented.
41+
- 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: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ server = StellarRpcServer(io_dir=Path('out'))
4242
server.handle_rpc('sendTransaction', {'transaction': xdr})
4343
```
4444

45-
For `sendTransaction` it builds the request envelope with `encoder.build_tx_request` and runs it with `interpreter.run`; for the read-only methods (`getHealth`, `getNetwork`, `getLatestLedger`, `getTransaction`, `traceTransaction`) it builds a small envelope and runs it. In every case the *content* of the response is produced by the semantics (`node.md`), not by Python — the one exception is the failure fallback (below). Each call is logged to stderr.
45+
For `sendTransaction` it builds the request envelope with `encoder.build_tx_request` and runs it with `interpreter.run` (skipping the `<program>` wasm injection when the hash already has a receipt, so a duplicate is not re-executed — the semantics answer `DUPLICATE`); for the read-only methods (`getHealth`, `getNetwork`, `getLatestLedger`, `getTransaction`, `traceTransaction`) it builds a small envelope and runs it. In every case the *content* of the response is produced by the semantics (`node.md`), not by Python — the one exception is the failure fallback (below). Each call is logged to stderr.
4646

4747
---
4848

@@ -97,24 +97,30 @@ Because these artifacts live on disk, the server can be stopped and restarted wi
9797

9898
## RPC methods
9999

100-
All methods are answered by the K semantics. All follow the [Stellar RPC specification](https://developers.stellar.org/docs/data/apis/rpc/methods) except `traceTransaction`, which is a komet-specific extension (see below).
100+
All methods are answered by the K semantics and follow the [Stellar RPC specification](https://developers.stellar.org/docs/data/apis/rpc/methods) except `traceTransaction`, which is a komet-specific extension (see below) — including its serialization quirks: ledger sequence numbers and `protocolVersion` are JSON numbers, while close-time fields (`latestLedgerCloseTime`, `oldestLedgerCloseTime`, `createdAt`) are int64s serialized as JSON strings holding a decimal integer, matching what real stellar-rpc emits.
101101

102102
### `getHealth`
103103

104-
`getHealth` returns `{"status": "healthy"}`.
104+
```json
105+
{ "status": "healthy", "latestLedger": 4, "latestLedgerCloseTime": "1716000000", "oldestLedger": 0, "oldestLedgerCloseTime": "0", "ledgerRetentionWindow": 5 }
106+
```
107+
108+
The node retains every ledger since genesis, so `oldestLedger` is always 0 and the retention window covers all ledgers so far. Per-ledger close times are not recorded: the latest close time is approximated by the request time and the oldest by the epoch.
105109

106110
### `getNetwork`
107111

108112
```json
109-
{ "friendbotUrl": null, "passphrase": "Test SDF Network ; September 2015", "protocolVersion": "22" }
113+
{ "passphrase": "Test SDF Network ; September 2015", "protocolVersion": 22 }
110114
```
111115

116+
`friendbotUrl` is omitted (not `null`) because the node runs no friendbot — matching real stellar-rpc's `omitempty` serialization.
117+
112118
### `getLatestLedger`
113119

114-
`getLatestLedger` returns the current ledger sequence (the `latest_ledger` from `metadata.json`), which increments by 1 per successfully committed transaction.
120+
`getLatestLedger` returns the current ledger sequence (the `latest_ledger` from `metadata.json`), which increments by 1 per successfully committed transaction. The `id` is a deterministic per-ledger stand-in for the ledger-header hash, derived from the sequence (see `#ledgerId` in [node-semantics.md](node-semantics.md)).
115121

116122
```json
117-
{ "id": "0000...0000", "protocolVersion": "22", "sequence": 4 }
123+
{ "id": "1715609f7c74...", "protocolVersion": 22, "sequence": 4 }
118124
```
119125

120126
### `sendTransaction`
@@ -125,9 +131,13 @@ All methods are answered by the K semantics. All follow the [Stellar RPC specifi
125131

126132
**Response**:
127133
```json
128-
{ "hash": "<64-char hex>", "status": "PENDING", "latestLedger": "5", "latestLedgerCloseTime": "1716000000" }
134+
{ "hash": "<64-char hex>", "status": "PENDING", "latestLedger": 5, "latestLedgerCloseTime": "1716000000" }
129135
```
130136

137+
Resubmitting a transaction whose hash already has a receipt returns status `DUPLICATE` without re-executing it: the ledger does not advance and the stored receipt is untouched.
138+
139+
A transaction that decodes as XDR but cannot be processed (e.g. it contains an operation the semantics do not support) is rejected at admission time with status `ERROR` and an `errorResultXdr` carrying a `txMALFORMED` `TransactionResult` — mirroring how real stellar-rpc reports core's admission rejections. Such a transaction never reaches the ledger: no receipt is stored (`getTransaction` stays `NOT_FOUND`) and the ledger does not advance. Undecodable XDR remains a plain `-32602 Invalid params` error, as in real stellar-rpc. `TRY_AGAIN_LATER` is never returned: it signals mempool backpressure, and komet-node executes synchronously without a mempool, so the condition it reports cannot arise.
140+
131141
### `traceTransaction` (komet-specific extension)
132142

133143
`traceTransaction` is **not part of the Stellar RPC specification** — it exists only on komet-node, and clients must not expect it from real Stellar RPC endpoints. It keeps its plain name rather than a vendor-prefixed one (`komet_traceTransaction`): the official spec has no method of that name and none is announced, so there is no collision to avoid, and renaming would break every existing client for no gain. If stellar-rpc ever claims the name, the method will be renamed with a prefix.
@@ -140,7 +150,7 @@ All methods are answered by the K semantics. All follow the [Stellar RPC specifi
140150

141151
### `getTransaction`
142152

143-
`getTransaction` reads the hash's `receipts/receipt_<hash>.json` file.
153+
`getTransaction` reads the hash's `receipts/receipt_<hash>.json` file. The `hash` parameter must be a 64-character hex string; anything else is rejected with `-32602 Invalid params` (this and `traceTransaction` share the validation).
144154

145155
| Status | Meaning |
146156
|---|---|
@@ -151,13 +161,17 @@ All methods are answered by the K semantics. All follow the [Stellar RPC specifi
151161
**`SUCCESS` response**:
152162
```json
153163
{
154-
"status": "SUCCESS", "ledger": "5", "createdAt": "1716000000",
164+
"status": "SUCCESS", "applicationOrder": 1, "feeBump": false,
155165
"envelopeXdr": "<base64 XDR>", "resultXdr": "", "resultMetaXdr": "",
156-
"latestLedger": "5", "latestLedgerCloseTime": "1716000000"
166+
"ledger": 5, "createdAt": "1716000000",
167+
"latestLedger": 5, "latestLedgerCloseTime": "1716000000",
168+
"oldestLedger": 0, "oldestLedgerCloseTime": "0"
157169
}
158170
```
159171

160-
`resultXdr` and `resultMetaXdr` are currently empty stubs. The receipt carries no trace — use `traceTransaction` with the same hash to fetch it.
172+
The ledger-range fields (`latestLedger` through `oldestLedgerCloseTime`) are present on every response, `NOT_FOUND` included. Every ledger contains exactly one transaction, so `applicationOrder` is always 1; fee-bump envelopes are not supported, so `feeBump` is always false. `resultXdr` and `resultMetaXdr` are currently empty stubs. The receipt carries no trace — use `traceTransaction` with the same hash to fetch it.
173+
174+
Receipts are an internal format, not a stable one: receipts written by older komet-node versions stored `ledger` as a string and lack `applicationOrder`/`feeBump`, and `getTransaction` returns stored receipts verbatim. Start from a fresh io-dir after upgrading rather than resuming one with old receipts.
161175

162176
---
163177

flake.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# `k` package directly (replacing the imperative `kup install k`); its
1010
# nixpkgs is intentionally NOT followed, so the k-framework binary caches
1111
# are hit instead of rebuilding K against our nixpkgs.
12-
k-framework.url = "github:runtimeverification/k/v7.1.319";
12+
k-framework.url = "github:runtimeverification/k/v7.1.323";
1313
# Use the same uv2nix as k-framework so we inherit the pyproject-nix version
1414
# that fixes the missing 'riscv64' attribute in pep600.nix (pep599.manyLinuxTargetMachines
1515
# lookup now uses `or tagArch` as a safe default for unknown architectures).

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ readme = "README.md"
1010
requires-python = "~=3.10"
1111
dependencies = [
1212
"stellar-sdk>=13.2.1",
13-
"komet@git+https://github.com/runtimeverification/komet.git@v0.1.79",
14-
"kframework>=7.1.318,<7.1.321",
13+
"komet@git+https://github.com/runtimeverification/komet.git@v0.1.85",
14+
"kframework>=7.1.323,<7.1.324",
1515
]
1616

1717
[[project.authors]]

0 commit comments

Comments
 (0)