Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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`)
5 changes: 4 additions & 1 deletion docs/node-semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(...)
Expand All @@ -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": <latest_ledger> }`
- `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_<hash>.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.
Expand Down
3 changes: 2 additions & 1 deletion docs/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
16 changes: 16 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 56 additions & 0 deletions src/komet_node/kdist/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,62 @@ rule. `#respond(ID, RESULT)` writes the JSON-RPC envelope to `response.json`, re
</k>
```

`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 <k> #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 )
})
...
</k>
```

`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 <k> #dispatchMethod( "getFeeStats", REQ )
=> #respond( #getJSON( "id", REQ ), {
"sorobanInclusionFee" : #feeDistribution,
"inclusionFee" : #feeDistribution,
"latestLedger" : #getInt( "latest_ledger", String2JSON( {#readFile("metadata.json")}:>String ) )
})
...
</k>

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_<hash>.json` file. If the file
Expand Down
23 changes: 23 additions & 0 deletions src/komet_node/server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import importlib.metadata
import json
import logging
import sys
Expand All @@ -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',)
Expand Down Expand Up @@ -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):
Expand Down
95 changes: 95 additions & 0 deletions src/tests/integration/test_server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import importlib.metadata
import json
import shutil
import socket
Expand Down Expand Up @@ -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
Loading