Skip to content

Commit 82a6d26

Browse files
feat: reject unprocessable transactions with sendTransaction status ERROR
A transaction that decodes as XDR but cannot be processed (for example an operation the semantics do not support) previously came back as a generic -32602 JSON-RPC error. Real stellar-rpc reports these admission-time rejections as status ERROR with a txMALFORMED TransactionResult in errorResultXdr, so komet-node now does the same: the transaction never reaches the ledger, no receipt is stored, and getTransaction stays NOT_FOUND. Undecodable XDR remains -32602, matching real stellar-rpc. TRY_AGAIN_LATER stays intentionally unreturned: it signals mempool backpressure, which cannot arise in a synchronous node without a mempool.
1 parent c9cb9ab commit 82a6d26

4 files changed

Lines changed: 48 additions & 7 deletions

File tree

docs/notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ The tests do not yet cover `bytes` / `address` SCVal arguments or `SCVec` / `SCM
3434
## Known gaps
3535

3636
- `resultXdr` / `resultMetaXdr` are empty stubs (contract return values not surfaced).
37-
- `sendTransaction` never returns `ERROR` or `TRY_AGAIN_LATER` (a failed transaction is reported as `PENDING`, then `FAILED` by `getTransaction`); `errorResultXdr` / `diagnosticEventsXdr` are never returned.
37+
- `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.
3838
- `SCVec` / `SCMap` contract arguments are not yet encoded.
3939
- `simulateTransaction`, `getEvents`, `getLedgerEntries`, `getFeeStats`, and TTL/footprint operations are not implemented.
4040
- 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.

docs/server.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ The node retains every ledger since genesis, so `oldestLedger` is always 0 and t
122122

123123
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.
124124

125+
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.
126+
125127
### `traceTransaction`
126128

127129
`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.

src/komet_node/server.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from pathlib import Path
1212
from typing import TYPE_CHECKING, Any, Final
1313

14-
from stellar_sdk import Network
14+
from stellar_sdk import Network, TransactionEnvelope
1515

1616
from komet_node.interpreter import NodeInterpreter
17-
from komet_node.transaction import TransactionEncoder
17+
from komet_node.transaction import TransactionEncoder, malformed_tx_result_xdr
1818

1919
if TYPE_CHECKING:
2020
from http.server import HTTPServer as HTTPServerType
@@ -192,14 +192,21 @@ def handle_rpc(self, method: str | None, params: dict[str, Any], request_id: Any
192192
transaction = params.get('transaction')
193193
if not isinstance(transaction, str):
194194
return _error_str(request_id, -32602, "Invalid params: 'transaction' (XDR string) is required")
195+
# Undecodable XDR is a JSON-RPC client error (-32602, as in real stellar-rpc);
196+
# a transaction that decodes but cannot be processed (e.g. an unsupported
197+
# operation) is instead rejected at admission time with status ERROR below.
198+
try:
199+
parsed = TransactionEnvelope.from_xdr(transaction, self.encoder.network_passphrase)
200+
except Exception:
201+
traceback.print_exc()
202+
return _error_str(request_id, -32602, 'Invalid params: could not decode transaction XDR')
195203
try:
196204
envelope, program_steps = self.encoder.build_tx_request(method, request_id, transaction, now)
197205
except Exception:
198-
# build_tx_request both decodes XDR and validates it (e.g. rejecting
199-
# sub-stroop amounts); either is a client error. Log the detail, but keep
200-
# the client-facing message neutral rather than leaking internal exceptions.
206+
# Log the detail, but keep the client-facing response neutral rather than
207+
# leaking internal exceptions.
201208
traceback.print_exc()
202-
return _error_str(request_id, -32602, 'Invalid params: could not process transaction')
209+
return json.dumps(self._error_status_response(request_id, parsed.hash_hex(), now))
203210
# A hash that already has a receipt is a duplicate submission: the semantics
204211
# answer DUPLICATE without running the steps (see node.md). Steps injected into
205212
# the <program> cell (wasm uploads) would execute *before* dispatch, though, so
@@ -256,6 +263,24 @@ def _archive_request(self, method: str | None, params: dict[str, Any], request_i
256263
(self.requests_dir / f'request_{self._request_count}.json').write_text(json.dumps(archive))
257264
self._request_count += 1
258265

266+
def _error_status_response(self, rpc_id: Any, tx_hash: str, now: str) -> dict[str, Any]:
267+
"""Synthesise the sendTransaction response for a transaction rejected at admission.
268+
269+
Mirrors real stellar-rpc: a transaction that decodes as XDR but cannot be processed
270+
gets status ``ERROR`` with a ``txMALFORMED`` ``errorResultXdr``. It never reaches
271+
the ledger, so no receipt is stored (``getTransaction`` stays ``NOT_FOUND``) and the
272+
ledger does not advance.
273+
"""
274+
metadata = json.loads((self.io_dir / 'metadata.json').read_text())
275+
result = {
276+
'hash': tx_hash,
277+
'status': 'ERROR',
278+
'errorResultXdr': malformed_tx_result_xdr(),
279+
'latestLedger': metadata.get('latest_ledger', 0),
280+
'latestLedgerCloseTime': now,
281+
}
282+
return {'jsonrpc': '2.0', 'id': rpc_id, 'result': result}
283+
259284
def _failure_response(self, rpc_id: Any, envelope: dict[str, Any], now: str) -> dict[str, Any]:
260285
"""Synthesise the sendTransaction response for a transaction that got stuck (failed).
261286

src/komet_node/transaction.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@
2323
_STROOPS_PER_XLM = Decimal('10000000')
2424

2525

26+
def malformed_tx_result_xdr() -> str:
27+
"""Base64 ``TransactionResult`` with code ``txMALFORMED`` and no fee charged.
28+
29+
Returned as ``errorResultXdr`` when ``sendTransaction`` rejects a transaction at
30+
admission time (it decodes as XDR but cannot be processed by the semantics).
31+
"""
32+
result = xdr.TransactionResult(
33+
fee_charged=xdr.Int64(0),
34+
result=xdr.TransactionResultResult(code=xdr.TransactionResultCode.txMALFORMED),
35+
ext=xdr.TransactionResultExt(0),
36+
)
37+
return result.to_xdr()
38+
39+
2640
def _xlm_to_stroops(balance: object) -> int:
2741
"""Convert an XLM amount (which may carry up to 7 decimals) to integer stroops.
2842

0 commit comments

Comments
 (0)