Skip to content

Harden inputs and network handling (review findings #1-#5) - #17

Merged
CryptoFewka merged 1 commit into
mainfrom
fix/review-findings-1-5
Jun 25, 2026
Merged

Harden inputs and network handling (review findings #1-#5)#17
CryptoFewka merged 1 commit into
mainfrom
fix/review-findings-1-5

Conversation

@CryptoFewka

@CryptoFewka CryptoFewka commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What

Addresses the High/Medium findings from the full-main review.

#1 (High) — desired_state._read_indices silently accepted bad indices. int(row["validator_index"]) coerced true→1, 3.9→3, and accepted negatives, registering/assigning the wrong validator with no error. Now requires a non-negative JSON integer (bool is rejected explicitly, since it's an int subclass). This is the input path the dashboard boilerplate now steers customers to, so it matters most.

#2 (High) — api_client.batch_register_keys could crash past the ApiError boundary. int(body.get("added", 0)) raised a raw TypeError/ValueError on {"added": null} or a non-numeric value. Wrapped to raise ApiError, matching list_validators.

#3/#4 (Medium) — wrong-network / wrong-beacon-url footgun. The mainnet chain_id/network defaults are convenient but a testnet beacon + omitted chain_id silently stamped every record with 0x1; and a wrong beacon_url resolved an empty set that looks like a mass exit. Added BeaconClient.get_chain_id() (reads /eth/v1/config/deposit_contract); the CLI verifies it matches the configured chain_id before computing a plan. A mismatch exits EXIT_CONFIG; a non-beacon URL fails the probe with EXIT_BEACON. The defaults stay; this just makes a mismatch loud. (Only runs on beacon-backed inputs; the indices-file path carries its own per-record chain_id.)

#5 (Medium) — partial-progress log understated a mid-register failure. batch_register_keys now takes an on_progress callback fired per committed chunk; _apply_plan records it, so a failure on chunk 2 of a >500-key import logs registered_added=500, not 0.

Tests

New coverage for all five: indices-file rejection (bool/float/negative/string/null), non-numeric batch count → ApiError, on-progress survives a mid-chunk failure, get_chain_id parse/404/non-JSON, _chain_id_to_int parsing, and CLI chain-id mismatch→EXIT_CONFIG / match→0 / wrong-beacon→EXIT_BEACON.

  • pytest: 107 passed
  • ruff + mypy: clean

Written with Claude Code

Summary by CodeRabbit

  • New Features

    • Added progress updates during key registration so users can track committed work as batches complete.
    • Added a chain-id check against the beacon node before running operations, helping catch misconfiguration earlier.
  • Bug Fixes

    • Improved error handling for malformed API responses during batch registration.
    • Strengthened validation for validator indices and rejected invalid values more reliably.
    • Made beacon chain-id lookups fail cleanly with a consistent error when the node is unreachable or returns unexpected data.

#1 desired_state._read_indices: reject a bool/float/negative/string
   validator_index instead of letting int() silently coerce it to a wrong
   index (the JSON path is the no-beacon input we steer customers to).
#2 api_client.batch_register_keys: wrap int(added/skipped) so a null or
   non-numeric count surfaces as ApiError, not a raw exception escaping the
   public boundary.
#3/#4 chain_id cross-check: BeaconClient.get_chain_id() reads
   /eth/v1/config/deposit_contract; the CLI verifies it matches the configured
   chain_id before computing a plan. Catches a testnet beacon paired with the
   mainnet default, and a wrong beacon_url (which would otherwise resolve an
   empty set and look like a mass exit). Keeps the convenience defaults.
#5 partial-progress: batch_register_keys takes an on_progress callback invoked
   per committed chunk, so a mid-register failure logs what actually landed
   instead of 0.

Adds tests for each. 107 passed, ruff and mypy clean.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds chunk-level progress reporting to key registration, adds beacon-chain ID fetching with retry and unified BeaconError handling, checks configured chain_id against the beacon node before plan computation, tightens validator_index validation for indices files, and updates tests and CLI stubs for the new behavior.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ApiClient
  participant BatchRegisterKeysAPI
  CLI->>ApiClient: batch_register_keys(..., on_progress)
  loop each committed chunk
    ApiClient->>BatchRegisterKeysAPI: POST chunk
    BatchRegisterKeysAPI-->>ApiClient: added/skipped
    ApiClient-->>CLI: on_progress(added, skipped)
  end
Loading
sequenceDiagram
  participant Main
  participant ComputePlan
  participant BeaconClient
  participant BeaconNode
  participant DesiredState
  Main->>ComputePlan: build plan
  ComputePlan->>BeaconClient: get_chain_id()
  BeaconClient->>BeaconNode: GET /eth/v1/config/deposit_contract
  BeaconNode-->>BeaconClient: chain_id
  BeaconClient-->>ComputePlan: chain_id
  alt beacon error
    ComputePlan-->>Main: EXIT_BEACON
  else chain_id mismatch
    ComputePlan-->>Main: EXIT_CONFIG
  else chain_id match
    ComputePlan->>DesiredState: resolve(...)
    DesiredState-->>ComputePlan: desired validators
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title matches the PR theme but does not follow the required type(domain/pkg): summary format. Rename it to fix(optimum_keysync): harden inputs and network handling or another valid type/domain pair.
Docstring Coverage ⚠️ Warning Docstring coverage is 31.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/optimum_keysync/api_client.py`:
- Around line 161-167: The batch count handling in api_client.py still accepts
coerced or negative values because `ApiClient` adds `body.get("added", 0)` and
`body.get("skipped", 0)` via `int(...)`. Update this logic to validate the
response fields are real non-negative integers before updating the running
totals, and raise `ApiError` for booleans, floats, strings, or any negative
counts. Keep the fix near the existing `added`/`skipped` aggregation and
`on_progress` callback so malformed responses cannot affect either the callback
or the returned totals.

In `@src/optimum_keysync/beacon_client.py`:
- Around line 203-206: Reject non-string chain_id values in the beacon response
parsing path before converting them with int(). In BeaconClient’s chain_id
handling, validate that resp.json()["data"]["chain_id"] is a string matching the
expected decimal format, and raise BeaconError for booleans, numbers, or other
malformed scalars instead of letting int() coerce them. Keep the existing
try/except around the parsing logic, but tighten the type check in the same
block so the deposit_contract lookup cannot falsely succeed on bad payloads.
- Around line 183-188: The beacon chain_id retrieval path in BeaconClient should
also convert malformed URL and payload cases into BeaconError so callers can
handle them uniformly. Update the chain_id flow around _fetch_chain_id() and the
surrounding try/except to catch httpx.InvalidURL in addition to the existing
transport/retryable errors, and wrap it as BeaconError. Also tighten the
chain_id parsing in the response handling so only string values from
resp.json()["data"]["chain_id"] are accepted before conversion, rejecting
booleans/numbers and raising BeaconError for invalid payloads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: getoptimum/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e51433be-2496-4457-8e93-9d0b2551b64f

📥 Commits

Reviewing files that changed from the base of the PR and between afc8eb6 and d481358.

📒 Files selected for processing (9)
  • src/optimum_keysync/api_client.py
  • src/optimum_keysync/beacon_client.py
  • src/optimum_keysync/cli.py
  • src/optimum_keysync/desired_state.py
  • tests/test_api_client.py
  • tests/test_beacon_client.py
  • tests/test_cli_apply.py
  • tests/test_cli_chain_id.py
  • tests/test_desired_state.py

Comment on lines +161 to +167
try:
added += int(body.get("added", 0))
skipped += int(body.get("skipped", 0))
except (TypeError, ValueError) as e:
raise ApiError(f"unexpected validator-keys/batch response: {body!r}") from e
if on_progress is not None:
on_progress(added, skipped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject coerced or negative batch counts.

Lines 161-163 still rely on int(...), so True, 1.7, "2", or -1 will be accepted and folded into the running totals. That leaves this hardening incomplete and can skew both on_progress and the returned (added, skipped) on malformed API responses.

Suggested fix
             body = self._json_obj(resp)
-            try:
-                added += int(body.get("added", 0))
-                skipped += int(body.get("skipped", 0))
-            except (TypeError, ValueError) as e:
-                raise ApiError(f"unexpected validator-keys/batch response: {body!r}") from e
+            chunk_added = body.get("added", 0)
+            chunk_skipped = body.get("skipped", 0)
+            if (
+                isinstance(chunk_added, bool)
+                or not isinstance(chunk_added, int)
+                or chunk_added < 0
+                or isinstance(chunk_skipped, bool)
+                or not isinstance(chunk_skipped, int)
+                or chunk_skipped < 0
+            ):
+                raise ApiError(f"unexpected validator-keys/batch response: {body!r}")
+            added += chunk_added
+            skipped += chunk_skipped
             if on_progress is not None:
                 on_progress(added, skipped)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
added += int(body.get("added", 0))
skipped += int(body.get("skipped", 0))
except (TypeError, ValueError) as e:
raise ApiError(f"unexpected validator-keys/batch response: {body!r}") from e
if on_progress is not None:
on_progress(added, skipped)
body = self._json_obj(resp)
chunk_added = body.get("added", 0)
chunk_skipped = body.get("skipped", 0)
if (
isinstance(chunk_added, bool)
or not isinstance(chunk_added, int)
or chunk_added < 0
or isinstance(chunk_skipped, bool)
or not isinstance(chunk_skipped, int)
or chunk_skipped < 0
):
raise ApiError(f"unexpected validator-keys/batch response: {body!r}")
added += chunk_added
skipped += chunk_skipped
if on_progress is not None:
on_progress(added, skipped)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/optimum_keysync/api_client.py` around lines 161 - 167, The batch count
handling in api_client.py still accepts coerced or negative values because
`ApiClient` adds `body.get("added", 0)` and `body.get("skipped", 0)` via
`int(...)`. Update this logic to validate the response fields are real
non-negative integers before updating the running totals, and raise `ApiError`
for booleans, floats, strings, or any negative counts. Keep the fix near the
existing `added`/`skipped` aggregation and `on_progress` callback so malformed
responses cannot affect either the callback or the returned totals.

Comment on lines +183 to +188
try:
return self._fetch_chain_id()
except _RetryableBeaconError as e:
raise BeaconError(f"beacon chain_id failed after retries: {e}") from e
except httpx.TransportError as e:
raise BeaconError(f"beacon chain_id transport failed after retries: {e}") from e

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import httpx
print("InvalidURL subclass of TransportError:", issubclass(httpx.InvalidURL, httpx.TransportError))
print("InvalidURL MRO:", [cls.__name__ for cls in httpx.InvalidURL.__mro__])
PY

sed -n '172,208p' src/optimum_keysync/beacon_client.py

Repository: getoptimum/optimum-keysync

Length of output: 2103


🏁 Script executed:

python3 - <<'PY'
import httpx, json

print("InvalidURL subclass of TransportError:", issubclass(httpx.InvalidURL, httpx.TransportError))
print("int(True) ->", int(True))
print("int(False) ->", int(False))
print('json.loads("true") ->', json.loads("true"), type(json.loads("true")).__name__)
PY

Repository: getoptimum/optimum-keysync

Length of output: 272


🏁 Script executed:

python3 - <<'PY'
import httpx, json

print("InvalidURL subclass of TransportError:", issubclass(httpx.InvalidURL, httpx.TransportError))
print("int(True) ->", int(True))
print("int(False) ->", int(False))
print('json.loads("true") ->', json.loads("true"), type(json.loads("true")).__name__)
PY

Repository: getoptimum/optimum-keysync

Length of output: 272


Wrap InvalidURL and non-string chain_id into BeaconError.

  • httpx.InvalidURL is not a httpx.TransportError, so malformed beacon_url values can still bypass except BeaconError and crash the CLI instead of exiting via EXIT_BEACON.
  • int(resp.json()["data"]["chain_id"]) also accepts JSON booleans and numbers, so malformed deposit_contract payloads can be treated as valid. Reject non-string values before converting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/optimum_keysync/beacon_client.py` around lines 183 - 188, The beacon
chain_id retrieval path in BeaconClient should also convert malformed URL and
payload cases into BeaconError so callers can handle them uniformly. Update the
chain_id flow around _fetch_chain_id() and the surrounding try/except to catch
httpx.InvalidURL in addition to the existing transport/retryable errors, and
wrap it as BeaconError. Also tighten the chain_id parsing in the response
handling so only string values from resp.json()["data"]["chain_id"] are accepted
before conversion, rejecting booleans/numbers and raising BeaconError for
invalid payloads.

Comment on lines +203 to +206
try:
# The beacon spec returns chain_id as a decimal string, e.g. "1".
return int(resp.json()["data"]["chain_id"])
except (KeyError, TypeError, ValueError) as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject non-string chain_id values before calling int().

int() will silently coerce malformed JSON scalars here (true -> 1, 1.5 -> 1). That turns a bad deposit_contract payload into a valid-looking network match instead of the BeaconError this path is supposed to raise.

Suggested fix
         try:
             # The beacon spec returns chain_id as a decimal string, e.g. "1".
-            return int(resp.json()["data"]["chain_id"])
+            raw_chain_id = resp.json()["data"]["chain_id"]
+            if not isinstance(raw_chain_id, str) or not raw_chain_id.isdecimal():
+                raise ValueError(f"invalid chain_id: {raw_chain_id!r}")
+            return int(raw_chain_id)
         except (KeyError, TypeError, ValueError) as e:
             raise BeaconError(f"beacon deposit_contract malformed: {resp.text!r}") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
# The beacon spec returns chain_id as a decimal string, e.g. "1".
return int(resp.json()["data"]["chain_id"])
except (KeyError, TypeError, ValueError) as e:
try:
# The beacon spec returns chain_id as a decimal string, e.g. "1".
raw_chain_id = resp.json()["data"]["chain_id"]
if not isinstance(raw_chain_id, str) or not raw_chain_id.isdecimal():
raise ValueError(f"invalid chain_id: {raw_chain_id!r}")
return int(raw_chain_id)
except (KeyError, TypeError, ValueError) as e:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/optimum_keysync/beacon_client.py` around lines 203 - 206, Reject
non-string chain_id values in the beacon response parsing path before converting
them with int(). In BeaconClient’s chain_id handling, validate that
resp.json()["data"]["chain_id"] is a string matching the expected decimal
format, and raise BeaconError for booleans, numbers, or other malformed scalars
instead of letting int() coerce them. Keep the existing try/except around the
parsing logic, but tighten the type check in the same block so the
deposit_contract lookup cannot falsely succeed on bad payloads.

@CryptoFewka
CryptoFewka merged commit 9e7993b into main Jun 25, 2026
3 checks passed
@CryptoFewka
CryptoFewka deleted the fix/review-findings-1-5 branch June 25, 2026 18:31
@CryptoFewka CryptoFewka mentioned this pull request Jun 25, 2026
CryptoFewka added a commit that referenced this pull request Jun 25, 2026
Rolls up #16 (HTTP client lifecycle), #17 (input/network hardening + beacon
chain_id cross-check), #18 (log_format validation, clearer URL errors,
status-drop logging) for the v1.1.0 release.
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