From fcc3d2b1332527b59cf4a29b74cedaf238aaf6bd Mon Sep 17 00:00:00 2001 From: Raoul Date: Thu, 23 Jul 2026 09:11:45 +0000 Subject: [PATCH] refactor(server,scval): drop dead code, dedupe validation and response builders Post-merge cleanup of the implementation (test suite was already deduped). - scval.py: remove scvalue_from_xdr / sc_address_from_xdr (the XDR -> Komet SCValue path had no consumer) and their now-orphaned komet.scval imports. - server.py: unify the three divergent xdrFormat validators behind one _unsupported_xdr_format helper, fixing their disagreement on empty/null/absent. - server.py: make _latest_ledger the sole reader of metadata.json's latest_ledger. - server.py: add _result_str alongside _error_str; response builders now return the JSON string directly instead of a dict re-serialised at each call site. - server.py: extract the inline sendTransaction block into _handle_send_transaction so handle_rpc is a uniform dispatcher. - docs: drop the reference to the deleted scvalue_from_xdr. No behaviour change; all 90 tests pass. --- docs/notes.md | 2 +- src/komet_node/scval.py | 123 ---------------------- src/komet_node/server.py | 216 +++++++++++++++++++++------------------ 3 files changed, 118 insertions(+), 223 deletions(-) diff --git a/docs/notes.md b/docs/notes.md index 98c5269..f6a4639 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -13,7 +13,7 @@ | [`server.py`](server.md) — `StellarRpcServer` | Long-running HTTP/JSON-RPC server wrapping the one-shot K interpreter; `handle_rpc` dispatch; owns the io-dir files. Holds no ledger or receipt state. | | [`transaction.py`](transaction.md) — `TransactionEncoder` | XDR → request envelope + (for wasm uploads) kasmer steps; address/contract-id helpers. | | [`interpreter.py`](interpreter.md) — `NodeInterpreter` | Runs request envelopes through `llvm_interpret`; persists `state.kore`. No `kast`↔`kore` whole-config conversions. | -| `scval.py` | XDR `SCVal` ↔ Komet `SCValue` (`scvalue_from_xdr`), XDR `SCVal` ↔ request/response JSON (`scval_to_json`, `scval_from_json`). | +| `scval.py` | XDR `SCVal` ↔ request/response JSON (`scval_to_json`, `scval_from_json`). | | `ledger_entries.py` | `getLedgerEntries` XDR translation: base64 `LedgerKey` → key descriptors for the semantics, intermediate entries → base64 `LedgerEntryData`. | | [`kdist/node.md`](node-semantics.md) | The K RPC layer: reads `request.json`, dispatches, updates `metadata.json` and the per-transaction `receipts/` files, writes `response.json`. | diff --git a/src/komet_node/scval.py b/src/komet_node/scval.py index aea6925..36d21ed 100644 --- a/src/komet_node/scval.py +++ b/src/komet_node/scval.py @@ -2,24 +2,6 @@ from typing import TYPE_CHECKING -from komet.scval import ( - SCI32, - SCI64, - SCI128, - SCI256, - SCU32, - SCU64, - SCU128, - SCU256, - AccountId, - ContractId, - SCAddress, - SCBool, - SCBytes, - SCMap, - SCSymbol, - SCVec, -) from stellar_sdk import xdr as stellar_xdr from stellar_sdk.xdr.sc_address_type import SCAddressType from stellar_sdk.xdr.sc_val_type import SCValType @@ -27,8 +9,6 @@ _UINT64_MASK = (1 << 64) - 1 if TYPE_CHECKING: - from komet.scval import SCValue - from stellar_sdk.xdr.sc_address import SCAddress as XDRSCAddress from stellar_sdk.xdr.sc_val import SCVal @@ -176,106 +156,3 @@ def _map_entry(pair: object) -> tuple[dict, dict]: if isinstance(pair, (list, tuple)) and len(pair) == 2: return pair[0], pair[1] raise NotImplementedError(f'Unsupported SCMap entry in JSON encoding: {pair!r}') - - -def sc_address_from_xdr(xdr: XDRSCAddress) -> SCAddress: - """Convert an XDR SCAddress to a Komet SCAddress.""" - match xdr.type: - case SCAddressType.SC_ADDRESS_TYPE_ACCOUNT: - assert xdr.account_id is not None - # The account_id is a PublicKey XDR — extract the raw 32-byte ed25519 key - assert xdr.account_id.account_id.ed25519 is not None - raw_bytes = xdr.account_id.account_id.ed25519.uint256 - return SCAddress(AccountId(raw_bytes)) - case SCAddressType.SC_ADDRESS_TYPE_CONTRACT: - assert xdr.contract_id is not None - return SCAddress(ContractId(xdr.contract_id.contract_id.hash)) - case _: - raise NotImplementedError(f'Unsupported SCAddress type: {xdr.type}') - - -def scvalue_from_xdr(xdr: SCVal) -> SCValue: - """ - Convert a Stellar XDR SCVal to a Komet SCValue. - - The XDR SCVal is a large union type — each case maps directly to one of - the Komet SCValue dataclasses. Unsupported types (void, error, timepoint, - duration, contract instance, ledger keys) raise NotImplementedError. - """ - match xdr.type: - - case SCValType.SCV_BOOL: - assert xdr.b is not None - return SCBool(xdr.b) - - case SCValType.SCV_I32: - assert xdr.i32 is not None - return SCI32(xdr.i32.int32) - - case SCValType.SCV_I64: - assert xdr.i64 is not None - return SCI64(xdr.i64.int64) - - case SCValType.SCV_I128: - assert xdr.i128 is not None - # i128 is stored as (hi: int64, lo: uint64) parts - val = (xdr.i128.hi.int64 << 64) | xdr.i128.lo.uint64 - return SCI128(val) - - case SCValType.SCV_I256: - assert xdr.i256 is not None - # i256 is stored as (hi_hi, hi_lo, lo_hi, lo_lo) parts - val = ( - (xdr.i256.hi_hi.int64 << 192) - | (xdr.i256.hi_lo.uint64 << 128) - | (xdr.i256.lo_hi.uint64 << 64) - | xdr.i256.lo_lo.uint64 - ) - return SCI256(val) - - case SCValType.SCV_U32: - assert xdr.u32 is not None - return SCU32(xdr.u32.uint32) - - case SCValType.SCV_U64: - assert xdr.u64 is not None - return SCU64(xdr.u64.uint64) - - case SCValType.SCV_U128: - assert xdr.u128 is not None - val = (xdr.u128.hi.uint64 << 64) | xdr.u128.lo.uint64 - return SCU128(val) - - case SCValType.SCV_U256: - assert xdr.u256 is not None - val = ( - (xdr.u256.hi_hi.uint64 << 192) - | (xdr.u256.hi_lo.uint64 << 128) - | (xdr.u256.lo_hi.uint64 << 64) - | xdr.u256.lo_lo.uint64 - ) - return SCU256(val) - - case SCValType.SCV_SYMBOL: - assert xdr.sym is not None - return SCSymbol(xdr.sym.sc_symbol.decode()) - - case SCValType.SCV_BYTES: - assert xdr.bytes is not None - return SCBytes(xdr.bytes.sc_bytes) - - case SCValType.SCV_ADDRESS: - assert xdr.address is not None - return sc_address_from_xdr(xdr.address) - - case SCValType.SCV_VEC: - assert xdr.vec is not None - # komet annotates SCVec.val as tuple[SCValue] instead of tuple[SCValue, ...] - return SCVec(tuple(scvalue_from_xdr(v) for v in xdr.vec.sc_vec)) # type: ignore[arg-type] - - case SCValType.SCV_MAP: - assert xdr.map is not None - return SCMap({scvalue_from_xdr(entry.key): scvalue_from_xdr(entry.val) for entry in xdr.map.sc_map}) - - case _: - raise NotImplementedError(f'Unsupported SCVal type: {xdr.type}') diff --git a/src/komet_node/server.py b/src/komet_node/server.py index e3b33bc..3376975 100644 --- a/src/komet_node/server.py +++ b/src/komet_node/server.py @@ -220,8 +220,7 @@ def _log_ready(self) -> None: if self._fresh: status = 'starting from a fresh state (empty io-dir)' else: - metadata = json.loads((self.io_dir / 'metadata.json').read_text()) - status = f'resuming existing state (latest ledger {metadata.get("latest_ledger", 0)})' + status = f'resuming existing state (latest ledger {self._latest_ledger()})' _log.info('komet-node ready — %s', status) _log.info('io-dir: %s', self.io_dir) _log.info('listening on http://%s:%d', self.host, self.port()) @@ -319,56 +318,7 @@ def handle_rpc(self, method: str | None, params: dict[str, Any], request_id: Any return format_error if method in _TX_METHODS: - transaction = params.get('transaction') - if not isinstance(transaction, str): - return _error_str(request_id, -32602, "Invalid params: 'transaction' (XDR string) is required") - # Undecodable XDR is a JSON-RPC client error (-32602, as in real stellar-rpc); - # a transaction that decodes but cannot be processed (e.g. an unsupported - # operation) is instead rejected at admission time with status ERROR below. - try: - parsed = TransactionEnvelope.from_xdr(transaction, self.encoder.network_passphrase) - except Exception: - traceback.print_exc() - return _error_str(request_id, -32602, 'Invalid params: could not decode transaction XDR') - try: - envelope, program_steps, uploaded_wasms = self.encoder.build_tx_request( - method, request_id, transaction, now - ) - except Exception: - # Log the detail, but keep the client-facing response neutral rather than - # leaking internal exceptions. - traceback.print_exc() - return json.dumps(self._error_status_response(request_id, parsed.hash_hex(), now)) - # A hash that already has a receipt is a duplicate submission: the semantics - # answer DUPLICATE without running the steps (see node.md). Steps injected into - # the cell (wasm uploads) would execute *before* dispatch, though, so - # they must not be injected for a duplicate. - is_duplicate = (self.receipts_dir / f'receipt_{envelope["txHash"]}.json').exists() - if is_duplicate: - program_steps = None - # Stale staged events (e.g. from a previously failed transaction) must not leak - # into this transaction's event records. - (self.io_dir / _EVENTS_STAGING).unlink(missing_ok=True) - response = self.interpreter.run(self.state_file, self.io_dir, envelope, program_steps) - if response is None: - (self.io_dir / _EVENTS_STAGING).unlink(missing_ok=True) - return json.dumps(self._failure_response(request_id, envelope, now)) - # Rewrite the freshly written SUCCESS receipt's internal `returnValue` into real - # resultXdr/resultMetaXdr. Skip it for a duplicate: that receipt was already - # finalised on first submission and no longer carries `returnValue`, so - # reprocessing it would overwrite the stored result with an empty one. - if not is_duplicate: - self._attach_result_xdr(envelope['txHash']) - # Turn any events the run staged into events/events_.json. A duplicate - # runs no steps and stages nothing, so this is a no-op for it. - self._finalize_events(envelope['txHash'], now) - # Keep the raw bytes of successfully uploaded wasm modules: the K configuration - # stores modules parsed, and getLedgerEntries CONTRACT_CODE entries must return - # the original bytes. - for wasm_hash, wasm in uploaded_wasms.items(): - (self.wasms_dir / f'{wasm_hash}.wasm').write_bytes(wasm) - self._record_closed_ledger(envelope, now) - return response + return self._handle_send_transaction(method, params, request_id, now) if method == 'simulateTransaction': return self._handle_simulate(params, request_id, now) @@ -386,6 +336,63 @@ def handle_rpc(self, method: str | None, params: dict[str, Any], request_id: Any return _error_str(request_id, -32603, 'Internal error') return response + def _handle_send_transaction(self, method: str, params: dict[str, Any], request_id: Any, now: str) -> str: + """Execute and commit a ``sendTransaction`` call, returning the response JSON string. + + Decodes the transaction XDR, runs it through the semantics, and — on the success + path — finalises the receipt's result XDR, materialises any staged events, persists + uploaded wasm bytes, and records the closed ledger. Undecodable XDR is a JSON-RPC + client error (-32602); a transaction that decodes but cannot be admitted gets an + ERROR status response; one that gets stuck mid-run gets a FAILED receipt. + """ + transaction = params.get('transaction') + if not isinstance(transaction, str): + return _error_str(request_id, -32602, "Invalid params: 'transaction' (XDR string) is required") + try: + parsed = TransactionEnvelope.from_xdr(transaction, self.encoder.network_passphrase) + except Exception: + traceback.print_exc() + return _error_str(request_id, -32602, 'Invalid params: could not decode transaction XDR') + try: + envelope, program_steps, uploaded_wasms = self.encoder.build_tx_request( + method, request_id, transaction, now + ) + except Exception: + # Log the detail, but keep the client-facing response neutral rather than + # leaking internal exceptions. + traceback.print_exc() + return self._error_status_response(request_id, parsed.hash_hex(), now) + # A hash that already has a receipt is a duplicate submission: the semantics + # answer DUPLICATE without running the steps (see node.md). Steps injected into + # the cell (wasm uploads) would execute *before* dispatch, though, so + # they must not be injected for a duplicate. + is_duplicate = (self.receipts_dir / f'receipt_{envelope["txHash"]}.json').exists() + if is_duplicate: + program_steps = None + # Stale staged events (e.g. from a previously failed transaction) must not leak + # into this transaction's event records. + (self.io_dir / _EVENTS_STAGING).unlink(missing_ok=True) + response = self.interpreter.run(self.state_file, self.io_dir, envelope, program_steps) + if response is None: + (self.io_dir / _EVENTS_STAGING).unlink(missing_ok=True) + return self._failure_response(request_id, envelope, now) + # Rewrite the freshly written SUCCESS receipt's internal `returnValue` into real + # resultXdr/resultMetaXdr. Skip it for a duplicate: that receipt was already + # finalised on first submission and no longer carries `returnValue`, so + # reprocessing it would overwrite the stored result with an empty one. + if not is_duplicate: + self._attach_result_xdr(envelope['txHash']) + # Turn any events the run staged into events/events_.json. A duplicate + # runs no steps and stages nothing, so this is a no-op for it. + self._finalize_events(envelope['txHash'], now) + # Keep the raw bytes of successfully uploaded wasm modules: the K configuration + # stores modules parsed, and getLedgerEntries CONTRACT_CODE entries must return + # the original bytes. + for wasm_hash, wasm in uploaded_wasms.items(): + (self.wasms_dir / f'{wasm_hash}.wasm').write_bytes(wasm) + self._record_closed_ledger(envelope, now) + return response + def _get_ledger_entries(self, params: dict[str, Any], request_id: Any, now: str) -> str: """Handle getLedgerEntries: decode the LedgerKey XDR, run the K lookup, re-encode. @@ -465,7 +472,7 @@ def _handle_simulate(self, params: dict[str, Any], request_id: Any, now: str) -> try: envelope = self.encoder.build_simulate_request(request_id, transaction, now) except SimulationRejected as err: - return json.dumps(self._simulate_error_response(request_id, str(err))) + return self._simulate_error_response(request_id, str(err)) except Exception: traceback.print_exc() return _error_str(request_id, -32602, 'Invalid params: could not process transaction') @@ -474,10 +481,10 @@ def _handle_simulate(self, params: dict[str, Any], request_id: Any, now: str) -> if response is None: # The run got stuck: the invocation could not be executed at all (e.g. the # target contract does not exist). Report it as a simulation error. - return json.dumps(self._simulate_error_response(request_id, 'transaction simulation failed')) - return json.dumps(self._simulate_response(request_id, json.loads(response)['result'])) + return self._simulate_error_response(request_id, 'transaction simulation failed') + return self._simulate_response(request_id, json.loads(response)['result']) - def _simulate_response(self, rpc_id: Any, k_result: dict[str, Any]) -> dict[str, Any]: + def _simulate_response(self, rpc_id: Any, k_result: dict[str, Any]) -> str: """Map the internal K simulation result to the spec response shape. This is the one read path where Python builds response content: the spec requires @@ -494,13 +501,11 @@ def _simulate_response(self, rpc_id: Any, k_result: dict[str, Any]) -> dict[str, 'results': [{'xdr': scval_from_json(k_result['returnValue']).to_xdr(), 'auth': []}], 'transactionData': _TRANSACTION_DATA, } - return {'jsonrpc': '2.0', 'id': rpc_id, 'result': result} + return _result_str(rpc_id, result) - def _simulate_error_response(self, rpc_id: Any, message: str) -> dict[str, Any]: + def _simulate_error_response(self, rpc_id: Any, message: str) -> str: """Build the ``{error, latestLedger}`` result for a simulation that never ran in K.""" - metadata = json.loads((self.io_dir / 'metadata.json').read_text()) - result = {'error': message, 'latestLedger': metadata.get('latest_ledger', 0)} - return {'jsonrpc': '2.0', 'id': rpc_id, 'result': result} + return _result_str(rpc_id, {'error': message, 'latestLedger': self._latest_ledger()}) def _history_envelope( self, method: str, params: dict[str, Any], request_id: Any, base: dict[str, Any] @@ -517,11 +522,9 @@ def _history_envelope( def invalid(message: str) -> str: return _error_str(request_id, -32602, f'Invalid params: {message}') - xdr_format = params.get('xdrFormat', 'base64') - if xdr_format == 'json': - return invalid("xdrFormat 'json' is not supported; use 'base64'") - if xdr_format not in ('', 'base64'): - return invalid("'xdrFormat' must be 'base64'") + xdr_format_error = _unsupported_xdr_format(params) + if xdr_format_error is not None: + return invalid(xdr_format_error) pagination = params.get('pagination') or {} if not isinstance(pagination, dict): @@ -621,11 +624,9 @@ def _get_events_envelope( well-typed envelopes; the one state-dependent check (the window against the chain tip) lives in K. Returns a pre-formatted JSON-RPC error string on invalid params. """ - xdr_format = params.get('xdrFormat') or 'base64' - if xdr_format == 'json': - return _error_str(request_id, -32602, "Invalid params: xdrFormat 'json' is not supported, use 'base64'") - if xdr_format != 'base64': - return _error_str(request_id, -32602, "Invalid params: xdrFormat must be 'base64' or 'json'") + xdr_format_error = _unsupported_xdr_format(params) + if xdr_format_error is not None: + return _error_str(request_id, -32602, f'Invalid params: {xdr_format_error}') pagination = params.get('pagination') or {} if not isinstance(pagination, dict): @@ -683,8 +684,7 @@ def _finalize_events(self, tx_hash: str, now: str) -> None: staging.unlink() if not lines: return - metadata = json.loads((self.io_dir / 'metadata.json').read_text()) - ledger = metadata['latest_ledger'] + ledger = self._latest_ledger() closed_at = datetime.fromtimestamp(int(now), tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') # One transaction per ledger: TOID with application order 1, operation index 0. toid = (ledger << 32) | (1 << 12) @@ -724,7 +724,7 @@ def _archive_request(self, method: str | None, params: dict[str, Any], request_i (self.requests_dir / f'request_{self._request_count}.json').write_text(json.dumps(archive)) self._request_count += 1 - def _error_status_response(self, rpc_id: Any, tx_hash: str, now: str) -> dict[str, Any]: + def _error_status_response(self, rpc_id: Any, tx_hash: str, now: str) -> str: """Synthesise the sendTransaction response for a transaction rejected at admission. Mirrors real stellar-rpc: a transaction that decodes as XDR but cannot be processed @@ -732,17 +732,18 @@ def _error_status_response(self, rpc_id: Any, tx_hash: str, now: str) -> dict[st the ledger, so no receipt is stored (``getTransaction`` stays ``NOT_FOUND``) and the ledger does not advance. """ - metadata = json.loads((self.io_dir / 'metadata.json').read_text()) - result = { - 'hash': tx_hash, - 'status': 'ERROR', - 'errorResultXdr': malformed_tx_result_xdr(), - 'latestLedger': metadata.get('latest_ledger', 0), - 'latestLedgerCloseTime': now, - } - return {'jsonrpc': '2.0', 'id': rpc_id, 'result': result} + return _result_str( + rpc_id, + { + 'hash': tx_hash, + 'status': 'ERROR', + 'errorResultXdr': malformed_tx_result_xdr(), + 'latestLedger': self._latest_ledger(), + 'latestLedgerCloseTime': now, + }, + ) - def _failure_response(self, rpc_id: Any, envelope: dict[str, Any], now: str) -> dict[str, Any]: + def _failure_response(self, rpc_id: Any, envelope: dict[str, Any], now: str) -> str: """Synthesise the sendTransaction response for a transaction that got stuck (failed). The K run does not produce a ``response.json`` for a failed transaction and the @@ -772,13 +773,15 @@ def _failure_response(self, rpc_id: Any, envelope: dict[str, Any], now: str) -> } (self.receipts_dir / f'receipt_{tx_hash}.json').write_text(json.dumps(receipt)) - result = { - 'hash': tx_hash, - 'status': 'PENDING', - 'latestLedger': ledger, - 'latestLedgerCloseTime': now, - } - return {'jsonrpc': '2.0', 'id': rpc_id, 'result': result} + return _result_str( + rpc_id, + { + 'hash': tx_hash, + 'status': 'PENDING', + 'latestLedger': ledger, + 'latestLedgerCloseTime': now, + }, + ) def _is_int(value: Any) -> bool: @@ -851,25 +854,40 @@ def _configure_logging() -> None: _log.setLevel(logging.INFO) -def _check_xdr_format(params: dict[str, Any], request_id: Any) -> str | None: - """Validate the optional ``xdrFormat`` param; ``None`` means the request may proceed. +def _unsupported_xdr_format(params: dict[str, Any]) -> str | None: + """Describe why the request's ``xdrFormat`` is unsupported, or ``None`` if it is fine. - Only ``'base64'`` (the spec default) is supported. The spec's alternative ``'json'`` - format is not implemented in komet-node, so it gets a dedicated error message; any - other value is rejected as invalid. Both cases are Invalid params (-32602), matching + komet-node implements only ``'base64'`` (the spec default); an absent, null, or empty + value means the default, so it is also fine. The spec's alternative ``'json'`` format is + not implemented and gets a dedicated message; any other value is reported as unknown. + Callers turn the returned message into their own Invalid params (-32602) error, matching real stellar-rpc's handling of a bad format value. """ - xdr_format = params.get('xdrFormat', 'base64') + xdr_format = params.get('xdrFormat') or 'base64' if xdr_format == 'base64': return None if xdr_format == 'json': - return _error_str(request_id, -32602, "Invalid params: xdrFormat 'json' is not supported, use 'base64'") - return _error_str(request_id, -32602, "Invalid params: unknown xdrFormat, expected 'base64'") + return "xdrFormat 'json' is not supported, use 'base64'" + return "unknown xdrFormat, expected 'base64'" + + +def _check_xdr_format(params: dict[str, Any], request_id: Any) -> str | None: + """Validate the optional ``xdrFormat`` param; ``None`` means the request may proceed. + + Returns a pre-formatted -32602 error string for an unsupported format (see + :func:`_unsupported_xdr_format`). + """ + message = _unsupported_xdr_format(params) + return None if message is None else _error_str(request_id, -32602, f'Invalid params: {message}') def _error_str(rpc_id: Any, code: int, message: str) -> str: return json.dumps({'jsonrpc': '2.0', 'id': rpc_id, 'error': {'code': code, 'message': message}}) +def _result_str(rpc_id: Any, result: dict[str, Any]) -> str: + return json.dumps({'jsonrpc': '2.0', 'id': rpc_id, 'result': result}) + + def _error_bytes(rpc_id: Any, code: int, message: str) -> bytes: return _error_str(rpc_id, code, message).encode('utf-8')