From 521b69a674d741bd88d2e9574791dd91fec29522 Mon Sep 17 00:00:00 2001 From: Raoul Date: Thu, 2 Jul 2026 13:41:21 +0000 Subject: [PATCH 1/2] test: add spec-shape tests for getVersionInfo and getFeeStats Cover the wire format of real stellar-rpc (protocol 22): getVersionInfo returns exactly version/commitHash/buildTimestamp/captiveCoreVersion/ protocolVersion with protocolVersion as a JSON number, and getFeeStats returns both FeeDistribution objects (fee values and transactionCount as decimal strings, ledgerCount as a number) plus a live latestLedger number. Both methods take no parameters. The tests currently fail with -32601 since neither method is implemented yet. --- src/tests/integration/test_server.py | 95 ++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/tests/integration/test_server.py b/src/tests/integration/test_server.py index 3e5cfc3..ee127ae 100644 --- a/src/tests/integration/test_server.py +++ b/src/tests/integration/test_server.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib.metadata import json import shutil import socket @@ -498,3 +499,97 @@ def builder() -> TransactionBuilder: # All four transactions, including the non-Void invocation, advanced the ledger. assert _rpc(server.port(), 'getLatestLedger', {})['result']['sequence'] == 4 + + +def test_get_version_info(server: StellarRpcServer) -> None: + """getVersionInfo returns exactly the five spec fields with the right JSON types. + + Real stellar-rpc (protocol 22+) emits camelCase keys only; the deprecated snake_case + aliases (``commit_hash``, ...) were removed, so an exact key-set check covers both the + required fields and the absence of the legacy ones. ``protocolVersion`` is a Go uint32, + i.e. a JSON number, not a string. + """ + resp = _rpc(server.port(), 'getVersionInfo', {}) + assert 'error' not in resp, resp + result = resp['result'] + + assert set(result) == {'version', 'commitHash', 'buildTimestamp', 'captiveCoreVersion', 'protocolVersion'} + # komet-node reports its own package version as the RPC server version. + assert result['version'] == importlib.metadata.version('komet-node') + assert type(result['commitHash']) is str + assert type(result['buildTimestamp']) is str + assert type(result['captiveCoreVersion']) is str + assert type(result['protocolVersion']) is int # `is int` also rejects booleans + assert result['protocolVersion'] == 22 + + +def test_get_version_info_accepts_omitted_params(server: StellarRpcServer) -> None: + """getVersionInfo takes no parameters; a request without a params member must succeed.""" + resp = _post(server.port(), b'{"jsonrpc": "2.0", "id": 1, "method": "getVersionInfo"}') + assert 'error' not in resp, resp + assert resp['result']['protocolVersion'] == 22 + + +# Every FeeDistribution field except ledgerCount is an unsigned integer serialised with Go's +# `,string` option, i.e. a JSON string holding a decimal number (see the getFeeStats spec +# example: `"transactionCount": "10"` but `"ledgerCount": 50`). +_FEE_DISTRIBUTION_STRING_FIELDS = ( + 'max', + 'min', + 'mode', + 'p10', + 'p20', + 'p30', + 'p40', + 'p50', + 'p60', + 'p70', + 'p80', + 'p90', + 'p95', + 'p99', + 'transactionCount', +) + + +def _assert_fee_distribution(dist: dict[str, Any]) -> None: + """Check one FeeDistribution object against the stellar-rpc wire format.""" + assert set(dist) == {*_FEE_DISTRIBUTION_STRING_FIELDS, 'ledgerCount'} + for field in _FEE_DISTRIBUTION_STRING_FIELDS: + value = dist[field] + assert type(value) is str, f'{field} must be a JSON string, got {type(value).__name__}' + assert value.isdigit(), f'{field} must hold a decimal number, got {value!r}' + assert type(dist['ledgerCount']) is int, 'ledgerCount must be a JSON number' + # The distribution must at least be internally consistent. + assert int(dist['min']) <= int(dist['p50']) <= int(dist['max']) + + +def test_get_fee_stats(server: StellarRpcServer) -> None: + """getFeeStats returns both fee distributions and latestLedger with the right JSON types.""" + resp = _rpc(server.port(), 'getFeeStats', {}) + assert 'error' not in resp, resp + result = resp['result'] + + assert set(result) == {'sorobanInclusionFee', 'inclusionFee', 'latestLedger'} + _assert_fee_distribution(result['sorobanInclusionFee']) + _assert_fee_distribution(result['inclusionFee']) + assert type(result['latestLedger']) is int # a JSON number, and 0 on a fresh chain + assert result['latestLedger'] == 0 + + +def test_get_fee_stats_latest_ledger_tracks_chain(server: StellarRpcServer) -> None: + """getFeeStats reports the live ledger sequence, not a constant.""" + keypair = Keypair.random() + account = Account(keypair.public_key, sequence=0) + envelope = ( + TransactionBuilder(account, Network.TESTNET_NETWORK_PASSPHRASE) + .append_create_account_op(destination=keypair.public_key, starting_balance='1000') + .set_timeout(30) + .build() + ) + envelope.sign(keypair) + assert _rpc(server.port(), 'sendTransaction', {'transaction': envelope.to_xdr()})['result']['status'] == 'PENDING' + + resp = _rpc(server.port(), 'getFeeStats', {}) + assert 'error' not in resp, resp + assert resp['result']['latestLedger'] == 1 From 94f2dc3349bc2cd8e392ee0c5da5a459e165a128 Mon Sep 17 00:00:00 2001 From: Raoul Date: Thu, 2 Jul 2026 18:59:33 +0000 Subject: [PATCH 2/2] feat: implement getVersionInfo and getFeeStats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the two parameterless read-only methods per the Stellar RPC spec. getVersionInfo reports the komet-node package version, all-zeros commit hash and epoch build timestamp (nothing is baked in at build time), the komet package as the Captive Core, and protocolVersion 22 as a JSON number. getFeeStats returns constant fee distributions (there is no fee market; every statistic is the 100-stroop network minimum over an empty sample) with the stellar-rpc wire types — all fields except ledgerCount as decimal strings — and a live latestLedger number from metadata.json. Dispatch and response formatting live in node.md; server.py only supplies the version strings, which come from Python package metadata that K cannot read. --- docs/architecture.md | 4 +-- docs/node-semantics.md | 5 +++- docs/notes.md | 3 +- docs/server.md | 16 +++++++++++ src/komet_node/kdist/node.md | 56 ++++++++++++++++++++++++++++++++++++ src/komet_node/server.py | 23 +++++++++++++++ 6 files changed, 103 insertions(+), 4 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index c36ae26..c047b12 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -51,7 +51,7 @@ flowchart TB → **[Detailed documentation](server.md)** -The server implements six RPC methods — `getHealth`, `getNetwork`, `getLatestLedger`, `sendTransaction`, `getTransaction`, and `traceTransaction` — and the K semantics answer all of them. +The server implements eight RPC methods — `getHealth`, `getNetwork`, `getLatestLedger`, `getVersionInfo`, `getFeeStats`, `sendTransaction`, `getTransaction`, and `traceTransaction` — and the K semantics answer all of them. `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. @@ -177,6 +177,6 @@ sequenceDiagram - `resultXdr` / `resultMetaXdr` in `getTransaction` responses (contract return values) - `simulateTransaction` (dry-run without state mutation) -- `getEvents`, `getLedgerEntries`, `getFeeStats` and other read-only RPC methods +- `getEvents`, `getLedgerEntries`, `getTransactions`, `getLedgers` and other read-only RPC methods - `ExtendFootprintTTL` and `RestoreFootprint` operations - `SCVec` / `SCMap` contract-argument types in the request encoder (`scval_to_json`) diff --git a/docs/node-semantics.md b/docs/node-semantics.md index 6f72b79..3375364 100644 --- a/docs/node-semantics.md +++ b/docs/node-semantics.md @@ -39,7 +39,8 @@ insert-handleRequestFile → handleRequestFile ▼ #dispatchMethod(method, request) ← routes on the "method" field │ - ├─ getHealth / getNetwork / getLatestLedger / getTransaction / traceTransaction → #respond(...) + ├─ getHealth / getNetwork / getLatestLedger / getVersionInfo / getFeeStats + │ / getTransaction / traceTransaction → #respond(...) │ └─ sendTransaction → #runTx → run steps → #finalizeTx → record receipt + bump ledger → #respond(...) @@ -61,6 +62,8 @@ If `request.json` is absent, `insert-handleRequestFile` does not fire and K halt - `getHealth` → `{ "status": "healthy" }` - `getNetwork` → `{ "friendbotUrl": null, "passphrase": ..., "protocolVersion": ... }` (passphrase/version come from the request, keeping the semantics network-agnostic) - `getLatestLedger` → reads `metadata.json` and returns `{ "id": <64 zeros>, "protocolVersion": ..., "sequence": }` +- `getVersionInfo` → echoes the version fields the server put into the envelope (`version`, `commitHash`, `buildTimestamp`, `captiveCoreVersion` — package metadata lives on the Python side); `protocolVersion` is a JSON number +- `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 - `getTransaction` → reads the hash's `receipts/receipt_.json` file; returns the stored receipt merged with the current `latestLedger`/`latestLedgerCloseTime`, or `{ "status": "NOT_FOUND", ... }` when the file is absent `#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. diff --git a/docs/notes.md b/docs/notes.md index f1a0b3d..5975534 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -35,4 +35,5 @@ The tests do not yet cover `bytes` / `address` SCVal arguments or `SCVec` / `SCM - `resultXdr` / `resultMetaXdr` are empty stubs (contract return values not surfaced). - `SCVec` / `SCMap` contract arguments are not yet encoded. -- `simulateTransaction`, `getEvents`, `getLedgerEntries`, `getFeeStats`, and TTL/footprint operations are not implemented. +- `simulateTransaction`, `getEvents`, `getLedgerEntries`, `getTransactions`, `getLedgers`, and TTL/footprint operations are not implemented. +- `getFeeStats` reports constant distributions (there is no fee market); only its `latestLedger` is live. diff --git a/docs/server.md b/docs/server.md index 0af41c5..476b12b 100644 --- a/docs/server.md +++ b/docs/server.md @@ -103,6 +103,22 @@ All methods are answered by the K semantics and follow the [Stellar RPC specific { "id": "0000...0000", "protocolVersion": "22", "sequence": 4 } ``` +### `getVersionInfo` + +`getVersionInfo` reports the komet-node package version as the RPC server version and the komet package (the K semantics of Soroban executing the transactions) as the "Captive Core". komet-node is a Python package, so no commit hash or build timestamp is baked in at build time — those fields are all-zeros / epoch placeholders with the spec-correct types. `protocolVersion` is a JSON number. + +```json +{ "version": "0.1.0", "commitHash": "0000...0000", "buildTimestamp": "1970-01-01T00:00:00", "captiveCoreVersion": "komet 0.1.79 (K semantics of Soroban)", "protocolVersion": 22 } +``` + +### `getFeeStats` + +`getFeeStats` returns constant fee distributions: komet-node has no fee market, so every statistic is the network minimum inclusion fee of 100 stroops over an empty sample (`transactionCount: "0"`, `ledgerCount: 0`). Matching real stellar-rpc, every distribution field except `ledgerCount` is a JSON string holding a decimal number; `latestLedger` is a JSON number read live from `metadata.json`. + +```json +{ "sorobanInclusionFee": { "max": "100", "min": "100", "mode": "100", "p10": "100", ..., "p99": "100", "transactionCount": "0", "ledgerCount": 0 }, "inclusionFee": { ... }, "latestLedger": 4 } +``` + ### `sendTransaction` `sendTransaction` submits a base64-encoded XDR transaction envelope. diff --git a/src/komet_node/kdist/node.md b/src/komet_node/kdist/node.md index b8f1db2..34f0183 100644 --- a/src/komet_node/kdist/node.md +++ b/src/komet_node/kdist/node.md @@ -203,6 +203,62 @@ rule. `#respond(ID, RESULT)` writes the JSON-RPC envelope to `response.json`, re ``` +`getVersionInfo` echoes the version strings the Python server put into the request envelope +(the package versions live in Python's package metadata, which K cannot read). Its +`protocolVersion` is a JSON number, per the spec and real stellar-rpc. + +```k + rule #dispatchMethod( "getVersionInfo", REQ ) + => #respond( #getJSON( "id", REQ ), { + "version" : #getString( "version", REQ ), + "commitHash" : #getString( "commitHash", REQ ), + "buildTimestamp" : #getString( "buildTimestamp", REQ ), + "captiveCoreVersion" : #getString( "captiveCoreVersion", REQ ), + "protocolVersion" : #getInt( "protocolVersion", REQ ) + }) + ... + +``` + +`getFeeStats` reports a constant fee distribution: komet-node has no fee market (transactions +execute immediately and pay no fees), so every statistic is the network minimum inclusion fee +of 100 stroops over an empty sample. Real stellar-rpc serialises every distribution field +except `ledgerCount` with Go's `,string` option, so those are JSON strings holding decimal +numbers, while `ledgerCount` and `latestLedger` are JSON numbers. `latestLedger` is read live +from `metadata.json`. + +```k + rule #dispatchMethod( "getFeeStats", REQ ) + => #respond( #getJSON( "id", REQ ), { + "sorobanInclusionFee" : #feeDistribution, + "inclusionFee" : #feeDistribution, + "latestLedger" : #getInt( "latest_ledger", String2JSON( {#readFile("metadata.json")}:>String ) ) + }) + ... + + + syntax JSON ::= "#feeDistribution" [function, symbol(feeDistribution)] + // ---------------------------------------------------------------------- + rule #feeDistribution => { + "max" : "100", + "min" : "100", + "mode" : "100", + "p10" : "100", + "p20" : "100", + "p30" : "100", + "p40" : "100", + "p50" : "100", + "p60" : "100", + "p70" : "100", + "p80" : "100", + "p90" : "100", + "p95" : "100", + "p99" : "100", + "transactionCount" : "0", + "ledgerCount" : 0 + } +``` + ## getTransaction Look up the stored receipt by hash in its `receipts/receipt_.json` file. If the file diff --git a/src/komet_node/server.py b/src/komet_node/server.py index 6399575..d716a8b 100644 --- a/src/komet_node/server.py +++ b/src/komet_node/server.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib.metadata import json import logging import sys @@ -20,6 +21,15 @@ _PROTOCOL_VERSION: Final = '22' +# getVersionInfo fields. komet-node is a Python package, not a Go binary, so there is no +# commit hash or build timestamp baked in at compile time; report all-zeros / epoch +# placeholders with the correct spec types instead. The "Captive Core" of komet-node is the +# komet package (the K semantics of Soroban that execute the transactions). +_VERSION: Final = importlib.metadata.version('komet-node') +_COMMIT_HASH: Final = '0' * 40 +_BUILD_TIMESTAMP: Final = '1970-01-01T00:00:00' +_CAPTIVE_CORE_VERSION: Final = f'komet {importlib.metadata.version("komet")} (K semantics of Soroban)' + # Only sendTransaction executes a transaction. traceTransaction is a read-only lookup of the # trace stored on a previously executed transaction's receipt (see _read_only_envelope). _TX_METHODS: Final = ('sendTransaction',) @@ -225,6 +235,19 @@ def _read_only_envelope( return {**base, 'passphrase': self.encoder.network_passphrase, 'protocolVersion': _PROTOCOL_VERSION} if method == 'getLatestLedger': return {**base, 'protocolVersion': _PROTOCOL_VERSION} + if method == 'getVersionInfo': + # protocolVersion is a JSON number here (a Go uint32 in stellar-rpc), unlike the + # string the older methods still emit. + return { + **base, + 'version': _VERSION, + 'commitHash': _COMMIT_HASH, + 'buildTimestamp': _BUILD_TIMESTAMP, + 'captiveCoreVersion': _CAPTIVE_CORE_VERSION, + 'protocolVersion': int(_PROTOCOL_VERSION), + } + if method == 'getFeeStats': + return base if method in ('getTransaction', 'traceTransaction'): tx_hash = params.get('hash') if not isinstance(tx_hash, str):