fix(jsonrpc): correct QUANTITY/null format in TransactionResult#6709
fix(jsonrpc): correct QUANTITY/null format in TransactionResult#6709waynercheung wants to merge 1 commit intotronprotocol:developfrom
Conversation
Per ethereum/execution-apis, uint (QUANTITY) fields must match the pattern `^0x(0|[1-9a-f][0-9a-f]*)$` (no leading zeros), and contextual fields are nullable when block context is absent. TransactionResult violated this in several places. What changes: * nonce in both constructors emitted `0x0000000000000000` (8 zero bytes); per the TransactionInfo schema this field is uint, so it now emits `0x0`. * In the unsigned-signature branch of parseSignature, v/r/s were built from `new byte[1]` / `new byte[32]`, producing `0x00` and 64 zero hex chars; per the legacy signature schema these are uint, so they now emit `0x0`. * In the fallback constructor (used when the containing block cannot be resolved), blockHash / blockNumber / transactionIndex were emitted as the literal `0x`, which is neither a valid hash32, nor a valid uint, nor null. Per spec they must be null when block context is unknown; gasPrice in the same path is corrected from `0x` to `0x0`. Notes: * Block.nonce is intentionally not changed -- the Block schema defines it as bytes8 (DATA), so `0x0000000000000000` is already compliant. Changing it would break conformance with the spec. * The 32-byte r/s outputs in the signed path are not altered here; a conformance-correct rewrite has wider implications and will be tracked separately. Strict Ethereum-compatible clients (geth ethclient, ethers.js, web3.js, Web3j) reject malformed QUANTITY values, so this also unblocks correct parsing on those clients. Tests pin the spec-compliant values and validate each QUANTITY field against the schema regex to prevent regressions. Closes tronprotocol#6547
| // v, r, s are QUANTITY per Ethereum JSON-RPC spec; "0x0" matches uint pattern. | ||
| v = "0x0"; | ||
| r = "0x0"; | ||
| s = "0x0"; |
There was a problem hiding this comment.
[DISCUSS] Signed-path r/s may still violate uint pattern when high byte is 0x00
For signed transactions, r and s come from Rsv.fromSignature(...) as fixed 32-byte arrays and are serialised in the branch right below this via ByteArray.toJsonHex(byte[]), which preserves leading zeros. When the high byte of r (or s) happens to be 0x00 — roughly a 1/256 probability per byte — the output becomes "0x00ab...", which does NOT match ^0x(0|[1-9a-f][0-9a-f]*)$ and will be rejected by the same strict clients this PR aims to unblock.
The PR description scopes the fix to no-value paths and states production responses are byte-identical, which is true. But the headline statement "strict clients can now parse the response" is slightly stronger than what is actually delivered: for live transactions, every signature still has a non-trivial chance of producing a non-compliant r/s.
Suggestion: Add a "Known limitation" paragraph in the PR description and open a follow-up issue covering the signed path; longer-term, an API-layer toQuantity(byte[]) helper that strips leading zeros (and returns "0x0" for empty/all-zero) would close the gap without leaking spec policy into common/utils/ByteArray.
| assertQuantity(transactionResult.getGasPrice()); | ||
| assertQuantity(transactionResult.getV()); | ||
| assertQuantity(transactionResult.getR()); | ||
| assertQuantity(transactionResult.getS()); |
There was a problem hiding this comment.
[NIT] Tighten test sentinels — 2 small follow-ups on TransactionResultTest
Two small gaps in the new assertions, both in the fallback-path test (around lines 94-107):
-
No end-to-end null serialisation assertion. The test asserts
Assert.assertNull(transactionResult.getBlockHash())etc. on the Java object, but never serialises the DTO.TransactionResulthas no@JsonInclude(JsonInclude.Include.NON_NULL), so by default these fields render as"blockHash": nullin JSON, which matches spec nullability. If a future PR adds@JsonInclude(NON_NULL)(class-level or via a globalObjectMapperconfig), the fields silently disappear from the wire — and strict clients distinguish "null" from "missing". Adding oneObjectMapper.writeValueAsString(...)assertion that the produced JSON contains"blockHash":null,"blockNumber":null,"transactionIndex":nullwould lock the wire contract. -
gasandvaluenot covered byassertQuantity. The fallback path setsgas = "0x0"andvalue = "0x0"when the contract list is empty. The new sentinels covernonce/gasPrice/v/r/sbut skipgasandvalue, leaving a hole: a future change settinggas = ByteArray.toJsonHex(new byte[8])would not be caught. The other test (testBuildTransactionResultWithBlock) has the same gap forvalue.
Suggestion: Add assertQuantity(getGas()) and assertQuantity(getValue()) to both test methods, and add one JSON-serialisation assertion on the fallback path.
| r = ByteArray.toJsonHex(new byte[32]); | ||
| s = ByteArray.toJsonHex(new byte[32]); | ||
| // v, r, s are QUANTITY per Ethereum JSON-RPC spec; "0x0" matches uint pattern. | ||
| v = "0x0"; |
There was a problem hiding this comment.
[NIT] Consider a QUANTITY_ZERO constant for the literal repeated 5 times
The literal "0x0" now appears in five places in this class (parseSignature: v/r/s; main constructor: nonce; fallback constructor: nonce, gasPrice — gas was already "0x0"). They all carry the same semantic — "spec-compliant uint zero". When the next ETH-shaped field is added (e.g. EIP-1559 maxFeePerGas / maxPriorityFeePerGas placeholders), it is easy to copy the wrong literal — "0x" is the empty-bytes value for DATA fields, and the two must not be conflated.
Suggestion: Introduce private static final String QUANTITY_ZERO = "0x0"; (or a tiny helper in JsonRpcApiUtil) and reuse it. Pure cleanup; no behaviour change.
What this PR does
Fixes 9 spec-violation outputs in
TransactionResult, the response body shared byeth_getTransactionByHash,eth_getTransactionByBlockHashAndIndex,eth_getTransactionByBlockNumberAndIndex, and the full-tx mode ofeth_getBlockByHash/eth_getBlockByNumber.All fixed cases share a single root cause:
byte[N]zero-fill values were serialised viaByteArray.toJsonHex(byte[]), which preserves leading zero bytes, producing strings that violate theuint(QUANTITY) pattern. The fallback constructor additionally used a literal"0x"placeholder where the spec requiresnullor a validuint.Closes #6547.
Why
Per the Ethereum JSON-RPC schema in
ethereum/execution-apis:uint(QUANTITY) MUST match^0x(0|[1-9a-f][0-9a-f]*)$— no leading zeros; zero is0x0.TransactionInfo.blockHash/blockNumber/transactionIndexare nullable when block context is absent."0x"matches none of these forms.Strict Ethereum-compatible clients (geth
ethclient, ethers.js, web3.js, Web3j) reject the malformed values produced today, so this PR also unblocks correct parsing on those clients.What changes
nonce0x00000000000000000x0vparseSignatureunsigned branch0x000x0rparseSignatureunsigned branch0x000…000(64 zeros)0x0sparseSignatureunsigned branch0x000…000(64 zeros)0x0blockHash"0x"nullblockNumber"0x"nulltransactionIndex"0x"nullgasPrice"0x"0x0About the fallback constructor
TransactionResult(Transaction, Wallet)is invoked only on the data-pruning / inconsistency path insideTronJsonRpcImpl#getTransactionByHash: the transaction exists inTransactionStore(so it has been on-chain) butTransactionInfoStore/TransactionHistoryStoreandBlockStorecannot serve the correspondingTransactionInfoorBlock.This is not the pending-pool path. java-tron's jsonrpc module does not consult the mempool (
Manager#pendingTransactions); a transaction that lives only in the mempool already responds withnullfrometh_getTransactionByHash. Changing the fallback's malformed"0x"to spec-compliantnull/0x0therefore does not affect mempool query behaviour — those queries continue to returnnullexactly as before.Why
Block.nonceis intentionally NOT changedThe original issue says
nonceshould be0x0, but the spec types differ:TransactionInfo.nonceisuint→0x0000000000000000violates it, must become0x0. Fixed in this PR.Block.nonceisbytes8, pattern^0x[0-9a-f]{16}$→ the existing0x0000000000000000is already compliant. Changing it to0x0would shorten the value to 3 hex chars and break conformance.BlockResult.nonceis therefore left untouched and the existingJsonrpcServiceTestassertions on it ("0x0000000000000000") remain valid.Compatibility
parseSignature, which this PR does not touch. Production responses are byte-identical."0x"outputs were rejected by strict Ethereum clients (hexutil.UnmarshalJSON("0x")reportshex string "0x" is invalid); strict clients can now parse the response —nullfor unknown block context (matchingRPCTransactionpointer-field semantics in geth),0x0forgasPrice. Lenient clients see no behavioural change.eth_getTransactionByHashfor a mempool-only transaction continues to returnnull(java-tron's RPC does not query the mempool).Tests
TransactionResultTestis updated to:0x0for nonce / v / r / s / gasPrice) and the newnullvalues for the fallback path's block-context fields.^0x(0|[1-9a-f][0-9a-f]*)$via a smallassertQuantityhelper, so any future regression that re-introduces a leading-zero or"0x"value will fail the test.Pre-submit checklist
noncereturned byeth_getTransactionByHashis non-compliant #6547), 2 files changed