Skip to content

rpc/jsonrpc: reject out-of-int-range float IDs in idFromInterface (#5846)#5861

Open
satyakwok wants to merge 4 commits into
cometbft:mainfrom
satyakwok:fix-jsonrpc-id-float-5846
Open

rpc/jsonrpc: reject out-of-int-range float IDs in idFromInterface (#5846)#5861
satyakwok wants to merge 4 commits into
cometbft:mainfrom
satyakwok:fix-jsonrpc-id-float-5846

Conversation

@satyakwok
Copy link
Copy Markdown

Closes #5846.

json.Unmarshal decodes JSON numbers as float64. A naive int(id) for values above math.MaxInt or below math.MinInt saturates to math.MinInt on amd64 (cvttsd2si), so distinct large numeric IDs collide on the same negative integer with no error. This silently breaks request/response correlation for any client keying on the numeric id.

Fix

Add a float64 -> int -> float64 round-trip check on the float64 arm of idFromInterface and reject NaN / +-Inf explicitly. The round-trip catches the tricky 2^63 boundary that a naive id > math.MaxInt check misses (because float64(math.MaxInt64) rounds up to 2^63).

Tests

TestIDFromInterface_FloatRange/in_range_int_accepted               PASS
TestIDFromInterface_FloatRange/fractional_rejected                 PASS
TestIDFromInterface_FloatRange/nan_rejected                        PASS
TestIDFromInterface_FloatRange/pos_inf_rejected                    PASS
TestIDFromInterface_FloatRange/neg_inf_rejected                    PASS
TestIDFromInterface_FloatRange/two_to_63_rejected                  PASS
TestIDFromInterface_FloatRange/neg_two_to_63_minus_one_rejected    PASS
TestIDFromInterface_FloatRange/very_large_finite_rejected          PASS
TestIDFromInterface_FloatRange/oversized_ids_do_not_collide        PASS

The oversized_ids_do_not_collide case is the regression pin — five distinct oversized inputs that previously all produced JSONRPCIntID(math.MinInt) must now each return an error.

Existing TestResponses / TestUnmarshallResponses still pass.

Compatibility

  • In-range int IDs unchanged.
  • String IDs unchanged.
  • Out-of-int-range float IDs now return an error instead of silently saturating. Any client relying on the old silent saturation will now see an explicit error, which is the desired behavior per the issue.

satyakwok added 2 commits May 11, 2026 10:01
…ometbft#1693)

For replay paths that only need validator addresses + voting powers
(buildLastCommitInfo, buildExtendedCommitInfo), advancing proposer
priorities forward from the most recent stored checkpoint is wasted
work. The advance is O(height % valSetCheckpointInterval) and dominates
replay time on chains with infrequent validator-set changes (the issue
reports ~10x slowdown, this PR's benchmark shows up to 903x at worst
case).

Add Store.LoadValidatorsFast that returns the ValidatorSet without
calling IncrementProposerPriority. The returned set has correct
addresses, public keys, and voting powers; ProposerPriority reflects
the last stored checkpoint, not the requested height. Callers needing
accurate proposer priority must continue to use LoadValidators.

Wire buildLastCommitInfoFromStore and buildExtendedCommitInfoFromStore
to use the fast path. Both downstream paths only call TM2PB.Validator
(types/protobuf.go:41) which reads PubKey.Address() and VotingPower
only — no proposer priority access.

Bench results (state/store_test.go BenchmarkLoadValidatorsSawtooth)
on AMD EPYC, 100-validator set, ValidatorsInfo header at offset
queried but ValidatorSet only stored at checkpoint:

| Offset       | LoadValidators | LoadValidatorsFast | Speedup |
|--------------|----------------|--------------------|---------|
| at_checkpoint| 165 us         | 158 us             | 1.04x   |
| +1           | 149 us         | 137 us             | 1.09x   |
| +100         | 270 us         | 134 us             | 2.0x    |
| +10000       | 12.4 ms        | 132 us             | 94x     |
| +99999       | 126 ms         | 140 us             | 903x    |

Refactors LoadValidators internals into a shared loadValidatorsAtHeight
helper so both fast and full paths share the lookup logic.

Closes cometbft#1693
…metbft#5846)

json.Unmarshal decodes JSON numbers as float64. A naive int(id) for
values above math.MaxInt or below math.MinInt saturates to math.MinInt
on amd64 (cvttsd2si), so distinct large numeric IDs collide on the same
negative value with no error. This breaks request/response correlation
for any client keying on the numeric id.

Add a float64 -> int -> float64 round-trip check on the float64 arm and
reject NaN / +-Inf explicitly. The round-trip catches the tricky 2^63
boundary that a naive 'id > math.MaxInt' check misses (because
float64(math.MaxInt64) rounds up to 2^63).

Tests cover in-range int, fractional, NaN, +-Inf, the 2^63 boundary,
-2^63 - 1, very large finite (1e20), and an oversized-collision case
that previously produced the same math.MinInt for five distinct inputs.

Closes cometbft#5846
@satyakwok satyakwok requested a review from a team as a code owner May 11, 2026 17:18
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

PR author is not in the allowed authors list.

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.

Bug: idFromInterface overflows int on large JSON-RPC numeric ids

1 participant