Skip to content

fix(jsonrpc): correct QUANTITY/null format in TransactionResult#6709

Open
waynercheung wants to merge 1 commit intotronprotocol:developfrom
waynercheung:fix/jsonrpc-tx-info-quantity-format-6547
Open

fix(jsonrpc): correct QUANTITY/null format in TransactionResult#6709
waynercheung wants to merge 1 commit intotronprotocol:developfrom
waynercheung:fix/jsonrpc-tx-info-quantity-format-6547

Conversation

@waynercheung
Copy link
Copy Markdown
Collaborator

What this PR does

Fixes 9 spec-violation outputs in TransactionResult, the response body shared by eth_getTransactionByHash, eth_getTransactionByBlockHashAndIndex, eth_getTransactionByBlockNumberAndIndex, and the full-tx mode of eth_getBlockByHash / eth_getBlockByNumber.

All fixed cases share a single root cause: byte[N] zero-fill values were serialised via ByteArray.toJsonHex(byte[]), which preserves leading zero bytes, producing strings that violate the uint (QUANTITY) pattern. The fallback constructor additionally used a literal "0x" placeholder where the spec requires null or a valid uint.

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 is 0x0.
  • TransactionInfo.blockHash / blockNumber / transactionIndex are 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

Field Path Before After
nonce both constructors 0x0000000000000000 0x0
v parseSignature unsigned branch 0x00 0x0
r parseSignature unsigned branch 0x000…000 (64 zeros) 0x0
s parseSignature unsigned branch 0x000…000 (64 zeros) 0x0
blockHash fallback constructor "0x" null
blockNumber fallback constructor "0x" null
transactionIndex fallback constructor "0x" null
gasPrice fallback constructor "0x" 0x0

About the fallback constructor

TransactionResult(Transaction, Wallet) is invoked only on the data-pruning / inconsistency path inside TronJsonRpcImpl#getTransactionByHash: the transaction exists in TransactionStore (so it has been on-chain) but TransactionInfoStore / TransactionHistoryStore and BlockStore cannot serve the corresponding TransactionInfo or Block.

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 with null from eth_getTransactionByHash. Changing the fallback's malformed "0x" to spec-compliant null / 0x0 therefore does not affect mempool query behaviour — those queries continue to return null exactly as before.

Why Block.nonce is intentionally NOT changed

The original issue says nonce should be 0x0, but the spec types differ:

  • TransactionInfo.nonce is uint0x0000000000000000 violates it, must become 0x0. Fixed in this PR.
  • Block.nonce is bytes8, pattern ^0x[0-9a-f]{16}$ → the existing 0x0000000000000000 is already compliant. Changing it to 0x0 would shorten the value to 3 hex chars and break conformance.

BlockResult.nonce is therefore left untouched and the existing JsonrpcServiceTest assertions on it ("0x0000000000000000") remain valid.

Compatibility

  • Live networks: every TRON transaction in production carries a signature, so it goes through the signed path of parseSignature, which this PR does not touch. Production responses are byte-identical.
  • Fallback constructor path: triggered only by data pruning / inconsistency. The previous "0x" outputs were rejected by strict Ethereum clients (hexutil.UnmarshalJSON("0x") reports hex string "0x" is invalid); strict clients can now parse the response — null for unknown block context (matching RPCTransaction pointer-field semantics in geth), 0x0 for gasPrice. Lenient clients see no behavioural change.
  • Mempool queries: unaffected — eth_getTransactionByHash for a mempool-only transaction continues to return null (java-tron's RPC does not query the mempool).

Tests

TransactionResultTest is updated to:

  • Pin the new spec-compliant literal values (0x0 for nonce / v / r / s / gasPrice) and the new null values for the fallback path's block-context fields.
  • Validate every relevant QUANTITY field against the schema regex ^0x(0|[1-9a-f][0-9a-f]*)$ via a small assertQuantity helper, so any future regression that re-introduces a leading-zero or "0x" value will fail the test.

Pre-submit checklist

  • Single module (jsonrpc), single issue (The nonce returned by eth_getTransactionByHash is non-compliant #6547), 2 files changed
  • Code style follows Google Java Style
  • No debug code / temporary comments / stray TODOs
  • No unsafe numeric ops introduced
  • No log-level changes
  • No DB schema / consensus / config changes
  • No dependency upgrades
  • Comments explain WHY (the spec rationale), not WHAT

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
@waynercheung waynercheung changed the title fix(jsonrpc): correct zero-value QUANTITY/null format violations in TransactionResult fix(jsonrpc): correct QUANTITY/null format in TransactionResult Apr 27, 2026
@halibobo1205 halibobo1205 added topic:api rpc/http related issue topic:json-rpc labels Apr 28, 2026
@halibobo1205 halibobo1205 added this to the GreatVoyage-v4.8.2 milestone Apr 28, 2026
// v, r, s are QUANTITY per Ethereum JSON-RPC spec; "0x0" matches uint pattern.
v = "0x0";
r = "0x0";
s = "0x0";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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):

  1. No end-to-end null serialisation assertion. The test asserts Assert.assertNull(transactionResult.getBlockHash()) etc. on the Java object, but never serialises the DTO. TransactionResult has no @JsonInclude(JsonInclude.Include.NON_NULL), so by default these fields render as "blockHash": null in JSON, which matches spec nullability. If a future PR adds @JsonInclude(NON_NULL) (class-level or via a global ObjectMapper config), the fields silently disappear from the wire — and strict clients distinguish "null" from "missing". Adding one ObjectMapper.writeValueAsString(...) assertion that the produced JSON contains "blockHash":null, "blockNumber":null, "transactionIndex":null would lock the wire contract.

  2. gas and value not covered by assertQuantity. The fallback path sets gas = "0x0" and value = "0x0" when the contract list is empty. The new sentinels cover nonce/gasPrice/v/r/s but skip gas and value, leaving a hole: a future change setting gas = ByteArray.toJsonHex(new byte[8]) would not be caught. The other test (testBuildTransactionResultWithBlock) has the same gap for value.

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";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic:api rpc/http related issue topic:json-rpc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The nonce returned by eth_getTransactionByHash is non-compliant

3 participants