Skip to content

feat(chain): cap JSON-RPC response size before lowering into the guest#354

Merged
mfw78 merged 2 commits into
dev/m1from
feat/154-chain-response-size-cap-clean
Jul 15, 2026
Merged

feat(chain): cap JSON-RPC response size before lowering into the guest#354
mfw78 merged 2 commits into
dev/m1from
feat/154-chain-response-size-cap-clean

Conversation

@mfw78

@mfw78 mfw78 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What

Adds a host-side cap on chain JSON-RPC response bodies so a misbehaving or adversarial node cannot saturate the guest heap via a single large reply.

engine_config.rs gains a new [limits.chain] TOML section with a response_body_max_bytes knob (default 1 MiB), resolved through ModuleLimits::chain_response_max_bytes() with the same zero-saturates-to-one handling as the http/logs/poison sections.

HostState carries a chain_response_max_bytes: usize, threaded from config at boot in supervisor.rs and preserved across restarts via LoadedModule.

host/impls/chain.rs applies check_response_cap() to request() after receiving the provider response; a rejection logs a warning, increments the shepherd_chain_response_capped_total metric, and returns ChainError::Fault(InvalidInput). The request-batch path also tracks a running aggregate total against the same cap, so a wide batch of individually-legal bodies cannot lower an unbounded Vec<RpcResult> into guest memory in one go.

Subscription-delivered payloads (dispatch_chain_log) are not covered by this cap; that path lowers eth_subscribe notifications rather than chain::request responses, and bounding it is left as a follow-up rather than widened into this PR.

Why

A chain node (or anything on-path to it) that returns an oversized JSON-RPC response body can otherwise force the guest to buffer an arbitrarily large payload, giving an adversarial or misbehaving node a way to exhaust guest memory. Capping the response host-side, before it is copied into the guest, closes that gap the same way the existing [limits.http] cap does for outbound HTTP.

Testing

  • chain_limits_default_when_absent, chain_limits_parse_with_override, chain_limits_saturate_degenerate_zero in engine_config.rs cover config resolution.
  • response_at_cap_is_accepted and response_over_cap_returns_invalid_input in host::impls::chain::tests cover the per-entry cap boundary.
  • An end-to-end harness test in test_utils/harness.rs drives the cap through the real config -> HostState -> chain::request wiring with the price-alert module.
  • engine.example.toml documents the new [limits.chain] section.

Closes #154

@mfw78 mfw78 added the base: dev/m1 Open PR targets dev/m1 integration branch — merge AFTER develop PRs; develop merged in last. label Jul 15, 2026
lgahdl added 2 commits July 15, 2026 10:48
#154)

Adds a configurable `[limits.chain] response_max_bytes` knob (default
1 MiB) that is enforced host-side in `chain::request()` before the
response string is copied into the guest heap. Oversized responses are
rejected with an `InvalidInput` fault, a `WARN` log, and a dedicated
`shepherd_chain_response_capped_total` metric.
Review follow-ups on the chain response cap:

- request_batch: the per-entry cap bounds each body, but the aggregate
  Vec<RpcResult> lowered into guest memory was unbounded - ~64 entries
  just under the cap is ~64 MiB, exactly the guest-heap saturation
  issue #154 exists to prevent, reachable via the block-range chunking
  the issue itself recommends. A running total now converts entries
  past the cap into typed invalid-input results.
- config: renamed [limits.chain].response_max_bytes to
  response_body_max_bytes (u64) for exact symmetry with the
  [limits.http] sibling; a degenerate 0 saturates to 1, matching the
  logs/poison zero handling.
- new harness test drives the cap through the real path - config ->
  ModuleLimits -> HostState -> chain::request - with the price-alert
  module: an over-cap oracle response surfaces to the guest as the
  typed fault and never reaches classify. The existing unit tests only
  covered the free function, so a deleted and_then would have passed
  the suite.
- TOML parse tests for [limits.chain] (absent -> 1 MiB default,
  override, zero saturation), mirroring the HTTP section's tests.
- engine.example.toml documents the new section; warn message dash
  matches house style.
@mfw78 mfw78 force-pushed the feat/154-chain-response-size-cap-clean branch from d497832 to 41a50d5 Compare July 15, 2026 10:50
@mfw78 mfw78 merged commit a7d1ddb into dev/m1 Jul 15, 2026
5 checks passed
@mfw78 mfw78 deleted the feat/154-chain-response-size-cap-clean branch July 15, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

base: dev/m1 Open PR targets dev/m1 integration branch — merge AFTER develop PRs; develop merged in last.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants