Skip to content

fix(evm-rpc): encode Stable 0x3f nonceKey as a quantity, not a byte string#540

Open
elina-chertova wants to merge 1 commit into
masterfrom
alert-fix/stable-testnet-3f-noncekey
Open

fix(evm-rpc): encode Stable 0x3f nonceKey as a quantity, not a byte string#540
elina-chertova wants to merge 1 commit into
masterfrom
alert-fix/stable-testnet-3f-noncekey

Conversation

@elina-chertova

Copy link
Copy Markdown
Contributor

Cause (proven)

dump-stable-testnet-0 (image subsquid/evm-dump:3325a6fa) is in a crash-loop — 56 restarts in ~7h44m, deterministically stuck at block 61696562. The dump runs with --verify-tx-root and --verify-tx-sender.

Block 61696562 tx #1 (0x0faaea3177859e347bffc7f601321d4b5c9501b0cd12eb219b499971ca063043) is a Stable type 0x3f transaction with nonceKey: "0x0". The 0x3f encoder in verification.ts decodes nonceKey with decodeHex:

decodeHex(assertNotNull(tx.nonceKey, 'tx.nonceKey is missing'))

nonceKey is a JSON-RPC quantity (a uint), and the node returns it in minimal form — "0x0". That string is odd-length, so decodeHex fails its assert(isHex(value)) (isHex requires even length). The throw is fatal → the process crashes → it re-reads the same block on restart → crash-loop. This is both in encodeTransaction (tx-root) and in serializeTransaction (tx-sender), and both were exercised.

The 0x3f handler (decodeHex(nonceKey)) was introduced in 7a1045150e33808ca828d7cdd56dd33c9b66cb5b ("support 0x3f transaction type from stable network"). The one existing fixture (block 59083166) has nonceKey: "0x8000000000000001" — even-length with no leading zeros, which is the one case where decodeHex and the correct quantity encoding produce identical RLP bytes, so the bug stayed hidden. Note the Tempo 0x76 encoder already encodes nonceKey with BigInt(...).

Fix

Encode nonceKey as BigInt(tx.nonceKey) (canonical minimal RLP integer) in both the 0x3f branch of encodeTransaction and serializeTransaction.

Verification (on-chain proof, red → green)

Reproduced against the live block from https://rpc.testnet.stable.xyz:

  • transactionsRoot: with BigInt(nonceKey) the computed root equals the block's transactionsRoot 0x2834b06fceff67d7fa97915a2f32b13cbebd4edfc71fb956367a76a3e9cc27db; decodeHex-as-bytes and left-padded-32 candidates do not match.
  • tx sender: the signing payload with BigInt(nonceKey) recovers 0x4820e7e4c3ee3a8ee654e62114a97faf7bce5614 = tx.from.

Regression fixture evm/evm-rpc/test/fixtures/stable-testnet/61696562 added; it is picked up by the existing transactionsRoot verification and transaction sender recovery tests.

  • Pre-fix: those two tests fail with AssertionError: isHex(value) at decodeHex (matches the production crash).
  • Post-fix: npx vitest run test/verification.test.ts → 134 passed / 0 failed; full npm test → 170 passed; npm run lint (tsc --noEmit) clean; rush build --to @subsquid/evm-rpc clean.

Falsification

If a real 0x3f transaction carries a nonceKey that is genuinely a byte string (not a quantity), BigInt(nonceKey) would drop leading zero bytes and the tx-root would mismatch. The on-chain reproduction above (root + sender both match) rules this out for stable-testnet; a mismatch on any future 0x3f fixture would falsify it.

…tring

The 0x3f transaction encoder (encodeTransaction) and its signing
counterpart (serializeTransaction) decoded tx.nonceKey with decodeHex.
nonceKey is a JSON-RPC quantity: the node returns it in minimal form
(e.g. "0x0"), which is odd-length and fails decodeHex's isHex assert,
crashing tx-root / tx-sender verification. Even for even-length values
it only matched by coincidence. Encode it as BigInt(nonceKey) so RLP
uses the canonical minimal integer form.

Verified against on-chain data (stable-testnet block 61696562): the
computed transactionsRoot and recovered tx sender now match the block.

Regression fixture added: evm/evm-rpc/test/fixtures/stable-testnet/61696562.
@elina-chertova
elina-chertova requested a review from tmcgroul July 17, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant