Skip to content

Latest commit

 

History

History
178 lines (120 loc) · 11 KB

File metadata and controls

178 lines (120 loc) · 11 KB

server.pyStellarRpcServer

StellarRpcServer exposes the Stellar RPC API over HTTP/JSON-RPC. Its job is to make the K semantics usable as a server. The compiled semantics are a one-shot interpreter — one process invocation per request, with no networking and no memory between runs — and StellarRpcServer is the long-running process wrapped around them: it keeps the HTTP socket open and the state files on disk, decodes the XDR envelope (via TransactionEncoder), runs each request through the semantics (via NodeInterpreter), and returns whatever response.json the semantics produced. All RPC dispatch, receipt bookkeeping, ledger accounting, and response formatting happen in K — the server holds none of that state itself.


Class structure

class StellarRpcServer:
    interpreter: NodeInterpreter     # the K runner
    encoder:     TransactionEncoder  # the XDR → request-envelope decoder
    io_dir:      Path                # directory holding every artifact
    state_file:  Path                # io_dir / 'state.kore'
    receipts_dir: Path               # io_dir / 'receipts'  — receipt_<hash>.json per transaction
    traces_dir:   Path               # io_dir / 'traces'    — trace_<hash>.jsonl per transaction
    requests_dir: Path               # io_dir / 'requests'  — request_<n>.json archive

The server is a plain http.server.HTTPServer (not pyk's JsonRpcServer). A BaseHTTPRequestHandler reads each POST body and calls _handle, which parses the JSON-RPC frame and delegates to handle_rpc.

handle_rpc(method, params, request_id) -> str

handle_rpc is the dispatch entry point; it returns the JSON-RPC response envelope as a string. You can call it without the HTTP layer, which is convenient for scripts and tests:

server = StellarRpcServer(io_dir=Path('out'))
server.handle_rpc('sendTransaction', {'transaction': xdr})

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.


Startup

server = StellarRpcServer(
    host='localhost',
    port=8000,
    io_dir=Path('out'),  # omit for a fresh temporary directory
    network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
)
server.serve()

io_dir defaults to None, in which case the server creates a fresh temporary directory (tempfile.mkdtemp) and runs against that — a throwaway chain that starts empty on every launch and leaves the working directory untouched. Pass an explicit io_dir to keep the state in a known place.

At construction the server prepares the io dir, where state.kore lives at io_dir / 'state.kore':

  • state.kore absentinterpreter.empty_config() produces the initial idle K configuration (a blank-slate state with no accounts, contracts, or storage) and writes it; metadata.json is seeded with {"latest_ledger": 0}.
  • state.kore present — it is used as-is, and metadata.json is seeded only if missing. This lets you resume a previous session (ledger counter and stored receipts included) or start against a pre-built state.

In both cases the server creates the receipts/, traces/, and requests/ directories if they do not already exist, because the K file-system hooks write into them but cannot create them.

Once the socket is bound, serve logs three lines to stderr: whether it is starting from a fresh state (an empty io-dir) or resuming an existing one (with the latest ledger), the io-dir path, and the listening address. Instruction tracing is always on, so every transaction the semantics run produces a trace. (Tracing only produces records for contract invocations.)


State lifecycle

state.kore and metadata.json hold the persistent chain state; per-transaction receipts and traces live under receipts/ and traces/; the server and semantics also exchange a few transient files per request. See architecture.md for the complete io-dir layout.

startup (state.kore absent):
          → empty_config() → state.kore ; metadata.json {latest_ledger:0}
          → create receipts/ traces/ requests/

per successful transaction:
          → the semantics run the steps (trace → traces/trace_<hash>.jsonl),
            write receipts/receipt_<hash>.json, bump latest_ledger in metadata.json,
            and write response.json
          → NodeInterpreter persists the new state.kore

per failed (stuck) transaction:
          → no response.json is produced; state.kore and metadata.json are left unchanged
          → the server writes a FAILED receipts/receipt_<hash>.json (see below)

Because these artifacts live on disk, the server can be stopped and restarted without losing the world state, the ledger counter, or the stored receipts.


RPC methods

All methods are answered by the K semantics and follow the Stellar RPC specification, 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.

getHealth

{ "status": "healthy", "latestLedger": 4, "latestLedgerCloseTime": "1716000000", "oldestLedger": 0, "oldestLedgerCloseTime": "0", "ledgerRetentionWindow": 5 }

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.

getNetwork

{ "passphrase": "Test SDF Network ; September 2015", "protocolVersion": 22 }

friendbotUrl is omitted (not null) because the node runs no friendbot — matching real stellar-rpc's omitempty serialization.

getLatestLedger

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

{ "id": "1715609f7c74...", "protocolVersion": 22, "sequence": 4 }

sendTransaction

sendTransaction submits a base64-encoded XDR transaction envelope.

Execution model: real Stellar RPC was designed around a mempool and ledger close, so the API requires sendTransaction to return PENDING and have clients poll getTransaction. komet-node has no mempool — the semantics execute the transaction immediately — but it still returns PENDING to stay compatible with the two-step pattern.

Response:

{ "hash": "<64-char hex>", "status": "PENDING", "latestLedger": 5, "latestLedgerCloseTime": "1716000000" }

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.

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.

traceTransaction

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.

"<jsonl string>"

getTransaction

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

Status Meaning
NOT_FOUND No receipts/receipt_<hash>.json for that hash
SUCCESS Transaction executed successfully
FAILED Transaction was submitted but got stuck in the semantics

SUCCESS response:

{
  "status": "SUCCESS", "applicationOrder": 1, "feeBump": false,
  "envelopeXdr": "<base64 XDR>", "resultXdr": "", "resultMetaXdr": "",
  "ledger": 5, "createdAt": "1716000000",
  "latestLedger": 5, "latestLedgerCloseTime": "1716000000",
  "oldestLedger": 0, "oldestLedgerCloseTime": "0"
}

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.

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.


Failure fallback

A failed transaction leaves the semantics stuck without writing response.json, so interpreter.run returns None. Only sendTransaction executes a transaction, so it is the only method that reaches this path. The server then synthesises the response in Python: it writes a FAILED receipts/receipt_<hash>.json (so a later getTransaction finds it), without bumping the ledger, and returns PENDING. This is the only response content the server builds itself.


CLI

komet-node [--host HOST] [--port PORT] [--io-dir DIR]
Flag Default Description
--host localhost Bind address
--port 8000 Port
--io-dir a fresh temp dir Directory holding every artifact (state.kore, metadata.json, receipts/, traces/, requests/)