Skip to content

Commit c0cb0d2

Browse files
fix: encode empty Bytes as empty hex string in receipts
Base2String yields "0" for the zero-length case, an odd-length non-hex value that broke the receipt's XDR rewrite for contracts returning empty Bytes. Add a dedicated K rule mapping empty Bytes to the empty string. Also harden _attach_result_xdr: persist the receipt without the internal returnValue field even when the rewrite fails, so a stored receipt never leaks K-internal fields to getTransaction.
1 parent dbcc7fe commit c0cb0d2

2 files changed

Lines changed: 15 additions & 6 deletions

File tree

src/komet_node/kdist/node.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,13 +497,16 @@ Values with no JSON encoding (e.g. `Error`) render as `null`, i.e. as "no return
497497
```
498498

499499
`#bytesToHex` is the inverse of `HexBytes`: lowercase hex, two digits per byte (zero-padded,
500-
since Base2String drops leading zeroes).
500+
since Base2String drops leading zeroes). Empty bytes encode as the empty string — the
501+
general rule would yield `"0"` (Base2String of 0), which `#padZeros` cannot trim.
501502

502503
```k
503504
syntax String ::= #bytesToHex( Bytes ) [function, total, symbol(bytesToHex)]
504505
| #padZeros( String, Int ) [function, total, symbol(padZeros)]
505506
// --------------------------------------------------------------------------------
507+
rule #bytesToHex( B ) => "" requires lengthBytes(B) ==Int 0
506508
rule #bytesToHex( B ) => #padZeros( Base2String( Bytes2Int(B, BE, Unsigned), 16 ), 2 *Int lengthBytes(B) )
509+
requires lengthBytes(B) >Int 0
507510
508511
rule #padZeros( S, N ) => #padZeros( "0" +String S, N ) requires lengthString(S) <Int N
509512
rule #padZeros( S, _ ) => S [owise]

src/komet_node/server.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,17 @@ def _attach_result_xdr(self, tx_hash: str) -> None:
258258
receipt_file = self.receipts_dir / f'receipt_{tx_hash}.json'
259259
receipt = json.loads(receipt_file.read_text())
260260
return_value_json = receipt.pop('returnValue', None)
261-
return_value = scval_from_json(return_value_json) if return_value_json is not None else None
262-
tx_envelope = TransactionEnvelope.from_xdr(receipt['envelopeXdr'], self.encoder.network_passphrase)
263-
receipt['resultXdr'] = transaction_result_xdr(tx_envelope, return_value, success=True)
264-
receipt['resultMetaXdr'] = transaction_meta_xdr(tx_envelope, return_value)
265-
receipt_file.write_text(json.dumps(receipt))
261+
try:
262+
return_value = scval_from_json(return_value_json) if return_value_json is not None else None
263+
tx_envelope = TransactionEnvelope.from_xdr(receipt['envelopeXdr'], self.encoder.network_passphrase)
264+
receipt['resultXdr'] = transaction_result_xdr(tx_envelope, return_value, success=True)
265+
receipt['resultMetaXdr'] = transaction_meta_xdr(tx_envelope, return_value)
266+
finally:
267+
# Persist the receipt even when the rewrite fails (e.g. a return value this
268+
# encoder cannot decode): the transaction has already committed, and the stored
269+
# receipt must never keep the K-internal `returnValue` field. A receipt with
270+
# `resultXdr` omitted is spec-legal; a leaked internal field is not.
271+
receipt_file.write_text(json.dumps(receipt))
266272

267273
def _failure_response(self, rpc_id: Any, envelope: dict[str, Any], now: str) -> dict[str, Any]:
268274
"""Synthesise the sendTransaction response for a transaction that got stuck (failed).

0 commit comments

Comments
 (0)