Skip to content

Commit b85040a

Browse files
Merge branch 'main' into feat/tx-result-xdr
Resolve conflicts with main.
2 parents 64354eb + bd02497 commit b85040a

12 files changed

Lines changed: 574 additions & 108 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,11 +83,13 @@ 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, returnValue }``returnValue` is
86+
`{ status: "SUCCESS", applicationOrder: 1, feeBump: false, envelopeXdr, ledger, createdAt, returnValue }``returnValue` is
8787
the contract call's return `ScVal`, JSON-encoded by `#scValToJSON` (or `null` when the
8888
transaction made no contract call), read off the `<hostStack>` before the host is reset,
8989
3. responds with `{hash, status: "PENDING", latestLedger, latestLedgerCloseTime}`.
9090

91+
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.)
92+
9193
The internal `returnValue` field never reaches an RPC client: the Python server immediately rewrites it into the spec-mandated `resultXdr`/`resultMetaXdr` base64 XDR fields (`_attach_result_xdr` in `server.py`), because K cannot construct XDR. To keep the return value observable here, `uncheckedCallTx` (unlike komet's `callTx`) does not `#resetHost` after the call; `#recordAndRespond` serialises the stack top and resets the host instead.
9294

9395
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.

docs/notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,7 @@ The tests do not yet cover `bytes` / `address` SCVal arguments or `SCVec` / `SCM
3636
- `resultXdr` / `resultMetaXdr` are synthesised: `feeCharged` is 0, ledger-entry change
3737
sets are empty, and the InvokeHostFunction success hash covers only the return value
3838
(komet-node does not track fees, entry changes, or events).
39+
- `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.
3940
- `SCVec` / `SCMap` contract arguments are not yet encoded.
4041
- `simulateTransaction`, `getEvents`, `getLedgerEntries`, `getFeeStats`, and TTL/footprint operations are not implemented.
42+
- 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: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ server = StellarRpcServer(io_dir=Path('out'))
2828
server.handle_rpc('sendTransaction', {'transaction': xdr})
2929
```
3030

31-
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.
31+
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.
3232

3333
---
3434

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

8484
## RPC methods
8585

86-
All methods are answered by the K semantics and follow the [Stellar RPC specification](https://developers.stellar.org/docs/data/apis/rpc/methods).
86+
All methods are answered by the K semantics and follow the [Stellar RPC specification](https://developers.stellar.org/docs/data/apis/rpc/methods), 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.
8787

8888
### `getHealth`
8989

90-
`getHealth` returns `{"status": "healthy"}`.
90+
```json
91+
{ "status": "healthy", "latestLedger": 4, "latestLedgerCloseTime": "1716000000", "oldestLedger": 0, "oldestLedgerCloseTime": "0", "ledgerRetentionWindow": 5 }
92+
```
93+
94+
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.
9195

9296
### `getNetwork`
9397

9498
```json
95-
{ "friendbotUrl": null, "passphrase": "Test SDF Network ; September 2015", "protocolVersion": "22" }
99+
{ "passphrase": "Test SDF Network ; September 2015", "protocolVersion": 22 }
96100
```
97101

102+
`friendbotUrl` is omitted (not `null`) because the node runs no friendbot — matching real stellar-rpc's `omitempty` serialization.
103+
98104
### `getLatestLedger`
99105

100-
`getLatestLedger` returns the current ledger sequence (the `latest_ledger` from `metadata.json`), which increments by 1 per successfully committed transaction.
106+
`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)).
101107

102108
```json
103-
{ "id": "0000...0000", "protocolVersion": "22", "sequence": 4 }
109+
{ "id": "1715609f7c74...", "protocolVersion": 22, "sequence": 4 }
104110
```
105111

106112
### `sendTransaction`
@@ -111,9 +117,13 @@ All methods are answered by the K semantics and follow the [Stellar RPC specific
111117

112118
**Response**:
113119
```json
114-
{ "hash": "<64-char hex>", "status": "PENDING", "latestLedger": "5", "latestLedgerCloseTime": "1716000000" }
120+
{ "hash": "<64-char hex>", "status": "PENDING", "latestLedger": 5, "latestLedgerCloseTime": "1716000000" }
115121
```
116122

123+
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.
124+
125+
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.
126+
117127
### `traceTransaction`
118128

119129
`traceTransaction` retrieves the instruction trace of a previously submitted transaction. It takes a `hash` parameter (the same one `getTransaction` takes) and returns the trace that `sendTransaction` stored on that transaction's receipt. The result is the trace itself: a JSONL string with one record per executed WebAssembly instruction, or `null` when no transaction with that hash exists.
@@ -124,7 +134,7 @@ All methods are answered by the K semantics and follow the [Stellar RPC specific
124134

125135
### `getTransaction`
126136

127-
`getTransaction` reads the hash's `receipts/receipt_<hash>.json` file.
137+
`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).
128138

129139
| Status | Meaning |
130140
|---|---|
@@ -135,14 +145,20 @@ All methods are answered by the K semantics and follow the [Stellar RPC specific
135145
**`SUCCESS` response**:
136146
```json
137147
{
138-
"status": "SUCCESS", "ledger": "5", "createdAt": "1716000000",
148+
"status": "SUCCESS", "applicationOrder": 1, "feeBump": false,
139149
"envelopeXdr": "<base64 XDR>", "resultXdr": "<base64 XDR>", "resultMetaXdr": "<base64 XDR>",
140-
"latestLedger": "5", "latestLedgerCloseTime": "1716000000"
150+
"ledger": 5, "createdAt": "1716000000",
151+
"latestLedger": 5, "latestLedgerCloseTime": "1716000000",
152+
"oldestLedger": 0, "oldestLedgerCloseTime": "0"
141153
}
142154
```
143155

156+
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.
157+
144158
`resultXdr` is a base64 `TransactionResult` (code `txSUCCESS` or `txFAILED`) and `resultMetaXdr` a base64 `TransactionMeta` v3; when the transaction invoked a contract, its return value is reported as `sorobanMeta.returnValue` inside the meta. Both are synthesised by the server (`result_xdr.py`) from the envelope and the return value the semantics recorded in the receipt — see [node-semantics.md](node-semantics.md). Fees and ledger-entry change sets in these structs are zero/empty (komet-node does not track them). `resultMetaXdr` is omitted on `FAILED` receipts — a failed run rolls back and produces no meta. The receipt carries no trace — use `traceTransaction` with the same hash to fetch it.
145159

160+
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.
161+
146162
---
147163

148164
## Failure fallback

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)