Skip to content
Merged
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 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.
The server implements eight RPC methods — `getHealth`, `getNetwork`, `getLatestLedger`, `getVersionInfo`, `getFeeStats`, `sendTransaction`, `getTransaction`, 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.

`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", "latestLedger": N, "latestLedgerCloseTime": <now>, "oldestLedger": 0, "oldestLedgerCloseTime": "0", "ledgerRetentionWindow": N+1 }` (everything since genesis is retained)
- `getNetwork` → `{ "passphrase": ..., "protocolVersion": ... }` (passphrase/version come from the request, keeping the semantics network-agnostic; `friendbotUrl` is omitted — no friendbot)
- `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)
- `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 ledger-range fields (`latestLedger`/`latestLedgerCloseTime`/`oldestLedger`/`oldestLedgerCloseTime`), or `{ "status": "NOT_FOUND", ... }` (with the same ledger-range fields) 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. `#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).
Expand Down
3 changes: 2 additions & 1 deletion docs/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ The tests do not yet cover `bytes` / `address` SCVal arguments or `SCVec` / `SCM
- `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.
- `SCVec` / `SCMap` contract arguments are not yet encoded.
- The `xdrFormat` parameter is accepted on `getTransaction` and `sendTransaction`, but only the default `'base64'` is supported; `'json'` is rejected with `-32602`.
- `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.
- 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.
16 changes: 16 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ The node retains every ledger since genesis, so `oldestLedger` is always 0 and t
{ "id": "1715609f7c74...", "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 @@ -254,6 +254,62 @@ module) and the oldest by the epoch (`"0"`).
</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
}
```

`#ledgerId` derives the ledger's `id` (the hash of the ledger header on a real chain) from
its sequence number. This node has no ledger headers to hash — and the semantics have no
hash hooks — so the id is a deterministic stand-in: the sequence (offset by one so ledger 0
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 re
Expand All @@ -25,6 +26,15 @@
# ^[a-f\d]{64}$). Anything else is rejected as Invalid params before reaching K.
_TX_HASH_RE: Final = re.compile(r'[0-9a-f]{64}')

# 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 @@ -288,6 +298,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 re
import shutil
Expand Down Expand Up @@ -729,6 +730,100 @@ def builder() -> TransactionBuilder:
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


# ----------------------------------------------------------------------
# xdrFormat parameter (getTransaction / sendTransaction)
#
Expand Down
Loading