Skip to content

Commit 74c51a7

Browse files
Merge pull request #28 from runtimeverification/fix/trace-transaction-json-array
fix: return traceTransaction result as a JSON array
2 parents 31ee082 + ecb5bc4 commit 74c51a7

6 files changed

Lines changed: 192 additions & 80 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ curl -s http://localhost:8000 -H 'Content-Type: application/json' \
123123
-d '{"jsonrpc":"2.0","id":1,"method":"traceTransaction","params":{"hash":"c7099cbe10a9bfa1cdf9c9d368e1e1c932f535a70e4403b7aa409ce19fc36805"}}'
124124
```
125125

126-
`traceTransaction` returns the stored trace as its result. The trace is a JSONL string (one JSON record per executed WebAssembly instruction); it is shown decoded here for readability:
126+
`traceTransaction` returns the stored trace as its result: a JSON array with one record per executed WebAssembly instruction.
127127

128128
```jsonc
129129
{

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ All of the server's input and output artifacts live in one directory, the *io di
9090
| `state.kore` | persistent | `NodeInterpreter` | the full K world-state configuration — accounts, contract code (including uploaded wasm `ModuleDecl`s), contract storage, ledger metadata — serialized in KORE. Read before each run and rewritten after a successful one. |
9191
| `metadata.json` | persistent | the K semantics | `{"latest_ledger": N}` — the server ledger counter, bumped by 1 per committed transaction. |
9292
| `receipts/receipt_<hash>.json` | persistent | the semantics and the server (on success — the server rewrites the semantics' internal `returnValue` field into `resultXdr`/`resultMetaXdr`), or the server alone (on failure) | one stored receipt per transaction, keyed by tx hash, answering `getTransaction`. Each is `{status, ledger, createdAt, envelopeXdr, resultXdr, resultMetaXdr?}`. |
93-
| `traces/trace_<hash>.jsonl` | persistent | the semantics | one execution trace per transaction, keyed by tx hash — the instruction-level records, one JSON object per line. `traceTransaction` returns this file's contents. |
93+
| `traces/trace_<hash>.jsonl` | persistent | the semantics | one execution trace per transaction, keyed by tx hash — the instruction-level records, one JSON object per line. `traceTransaction` returns these records as a JSON array. |
9494
| `ledgers/ledger_<seq>.json` | persistent | the server (on success) | one record per closed ledger — `{sequence, txHash, closedAt, hash, headerXdr, metadataXdr}` — the ledger→transaction index behind `getTransactions` and `getLedgers`. Written in Python because the header artifacts are XDR, which K cannot construct. |
9595
| `requests/request_<n>.json` | persistent | the server | an archive of each incoming JSON-RPC request, numbered by a monotonic counter, kept for debugging. |
9696
| `wasms/<hash>.wasm` | persistent | the server | the raw bytes of each successfully uploaded wasm module, keyed by hex sha256. The K state stores modules parsed (`ModuleDecl`), so `getLedgerEntries` CONTRACT_CODE entries read the original bytes from here. |

docs/node-semantics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ The trace is not part of the receipt — the executing steps already appended it
105105

106106
### traceTransaction
107107

108-
`traceTransaction` is a komet-specific extension, not part of the Stellar RPC specification (see [server.md](server.md#tracetransaction-komet-specific-extension)). It is a read-only lookup: it takes a `hash` (the same parameter `getTransaction` takes) and responds with the contents of `traces/trace_<hash>.jsonl`, or `null` when no trace file exists for that hash. Because tracing is always on, every `sendTransaction` writes this file.
108+
`traceTransaction` is a komet-specific extension, not part of the Stellar RPC specification (see [server.md](server.md#tracetransaction-komet-specific-extension)). It is a read-only lookup: it takes a `hash` (the same parameter `getTransaction` takes) and responds with the contents of `traces/trace_<hash>.jsonl` as a JSON array (one record per executed instruction), or `null` when no trace file exists for that hash. Rather than decoding and re-encoding each record — a full round-trip whose cost scales with the (potentially very large) trace — the semantics trust that the file the tracer wrote is already valid JSON and build the array by string concatenation, joining the lines with commas inside `[``]`. Because tracing is always on, every `sendTransaction` writes this file.
109109

110110
### Two ways steps are delivered
111111

docs/server.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,12 @@ Failures are reported in the result body, matching real stellar-rpc; only an und
185185

186186
`traceTransaction` is **not part of the Stellar RPC specification** — it exists only on komet-node, and clients must not expect it from real Stellar RPC endpoints. It keeps its plain name rather than a vendor-prefixed one (`komet_traceTransaction`): the official spec has no method of that name and none is announced, so there is no collision to avoid, and renaming would break every existing client for no gain. If stellar-rpc ever claims the name, the method will be renamed with a prefix.
187187

188-
`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.
188+
`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 for that transaction. The result is a JSON array with one record per executed WebAssembly instruction (empty when the transaction ran no instructions), or `null` when no transaction with that hash exists.
189189

190190
```json
191-
"<jsonl string>"
191+
[
192+
{"pos": 3, "instr": ["const", "i32", 1048576], "stack": [], "locals": {}}
193+
]
192194
```
193195

194196
### `getTransaction`

src/komet_node/kdist/node.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -527,21 +527,76 @@ the spec-mandated `resultXdr`/`resultMetaXdr` base64 XDR fields (K cannot constr
527527

528528
Retrieve the execution trace of a previously submitted transaction, looked up by `hash` (the
529529
same parameter `getTransaction` takes). The trace was written to `traces/trace_<hash>.jsonl`
530-
by `sendTransaction`. Responds with the trace file's contents, or `null` when no trace file
531-
exists for that hash.
530+
by `sendTransaction`. The file is JSONL (one JSON record per executed instruction), and each
531+
line is already valid JSON produced by the semantics — so rather than decoding every record
532+
into a `JSON` term and re-encoding the whole array (a full round-trip whose cost scales with
533+
the trace, which can be very large), we build the result array by *string concatenation*:
534+
join the trusted lines with commas and wrap them in `[``]`. Responds with that array —
535+
empty when the transaction ran no instructions — or `null` when no trace file exists for that
536+
hash.
537+
538+
Because the trace body is spliced in as raw text, `#respondTrace` writes the JSON-RPC
539+
envelope directly (mirroring `#respond`) instead of building a `JSON` term for `JSON2String`:
540+
only the small `id` value is serialized through the encoder.
532541

533542
```k
534543
rule <k> #dispatchMethod( "traceTransaction", REQ )
535544
=> #respondTrace( #getJSON( "id", REQ ), #getString( "hash", REQ ) )
536545
...
537546
</k>
538547
539-
rule <k> #respondTrace( ID, HASH ) => #respond( ID, {#readFile( #traceFile( HASH ) )}:>String ) ... </k>
548+
rule <k> #respondTrace( ID, HASH )
549+
=> #writeFile( "response.json",
550+
"{\"jsonrpc\":\"2.0\",\"id\":"
551+
+String JSON2String( ID )
552+
+String ",\"result\":"
553+
+String #traceArray( {#readFile( #traceFile( HASH ) )}:>String )
554+
+String "}" )
555+
~> #remove( "request.json" )
556+
...
557+
</k>
558+
<exitCode> _ => 0 </exitCode>
540559
requires #fileExists( #traceFile( HASH ) )
560+
541561
rule <k> #respondTrace( ID, HASH ) => #respond( ID, null ) ... </k>
542562
requires notBool #fileExists( #traceFile( HASH ) )
543563
```
544564

565+
`#traceArray` wraps the JSONL trace text in array brackets; `#traceElems` joins the
566+
newline-delimited records with commas without touching their contents. Empty segments (a
567+
leading/blank line, or the empty tail after the final record's trailing newline) are skipped
568+
via `#maybeComma`, which prefixes a separator only when a following record actually exists —
569+
so a trailing newline never yields a dangling comma, and an empty file yields `[]`.
570+
571+
```k
572+
syntax String ::= #traceArray( String ) [function, symbol(traceArray)]
573+
| #traceElems( String ) [function, symbol(traceElems)]
574+
| #maybeComma( String ) [function, symbol(maybeComma)]
575+
// -----------------------------------------------------------------------
576+
rule #traceArray( S ) => "[" +String #traceElems( S ) +String "]"
577+
578+
rule #traceElems( "" ) => ""
579+
580+
// No more newlines: the whole remaining string is the final record.
581+
rule #traceElems( S ) => S
582+
requires S =/=String "" andBool findString( S, "\n", 0 ) <Int 0
583+
584+
// Split off the first line and recurse; a comma is added only if the tail is non-empty.
585+
rule #traceElems( S )
586+
=> substrString( S, 0, findString( S, "\n", 0 ) )
587+
+String #maybeComma( #traceElems( substrString( S, findString( S, "\n", 0 ) +Int 1, lengthString( S ) ) ) )
588+
requires findString( S, "\n", 0 ) >Int 0
589+
590+
// Empty leading line (the string starts with a newline): drop it and recurse.
591+
rule #traceElems( S )
592+
=> #traceElems( substrString( S, 1, lengthString( S ) ) )
593+
requires findString( S, "\n", 0 ) ==Int 0
594+
595+
rule #maybeComma( "" ) => ""
596+
rule #maybeComma( S ) => "," +String S
597+
requires S =/=String ""
598+
```
599+
545600
## simulateTransaction
546601

547602
Run a single contract invocation against the current world state *without committing

0 commit comments

Comments
 (0)