Skip to content

fix(erpc:PLA-751): ignore oversized eth_getLogs payloads#49

Merged
0x666c6f merged 5 commits intomorpho-mainfrom
feature/pla-751-erpc-add-per-log-eth_getlogs-payload-cap-before-cachestorage
Mar 9, 2026
Merged

fix(erpc:PLA-751): ignore oversized eth_getLogs payloads#49
0x666c6f merged 5 commits intomorpho-mainfrom
feature/pla-751-erpc-add-per-log-eth_getlogs-payload-cap-before-cachestorage

Conversation

@0x666c6f
Copy link
Copy Markdown
Collaborator

@0x666c6f 0x666c6f commented Mar 9, 2026

Summary

  • add opt-in eth_getLogs.params[0].maxSize handling for per-log data payload filtering
  • strip maxSize from upstream wire requests while preserving cache-key separation
  • apply filtering across direct responses, split sub-requests, and block-receipts fallback

Changes

  • add request transport sanitization for eth_getLogs so upstreams only receive standard JSON-RPC params
  • add payload-size parsing and log filtering helpers for single-log data payloads
  • add regression tests for direct, chunked, fallback, and transport behavior

Linear

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecd6f41ae4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/http_json_rpc_client.go Outdated
Comment on lines +789 to +790
if cleanup != nil {
defer cleanup()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep getLogs response buffer alive after ParseFromBytes

This branch defers cleanup() immediately after readResponseBodyMax, but JsonRpcResponse.ParseFromBytes can retain slices into bodyBytes for large result payloads instead of copying them (see common/json_rpc.go behavior for large results). When the function returns, the deferred cleanup returns that pooled buffer, so subsequent reads of the parsed response can observe corrupted data under normal concurrent traffic. This affects successful eth_getLogs responses with large payloads.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in eaa16e8.

Change:

  • detach large parsed eth_getLogs result bytes from the pooled read buffer before running cleanup in clients/http_json_rpc_client.go
  • added regression coverage in clients/http_json_rpc_client_getlogs_limit_test.go

This keeps the bounded read path while avoiding buffer aliasing after ParseFromBytes.

if math.Trunc(v) != v {
return 0, fmt.Errorf("%s must be an integer", getLogsMaxSizeField)
}
return int64(v), nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate float maxSize range before int64 conversion

maxSize from JSON is decoded as float64, and this path only checks that it is non-negative and integral before casting to int64. For values larger than math.MaxInt64 (for example 1e20), the conversion result is implementation-dependent, so invalid client input can silently become a truncated/negative limit instead of being rejected, which can disable or misapply log payload filtering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in eaa16e8.

Added an explicit float64 > math.MaxInt64 guard in extractGetLogsMaxDataBytes, plus a regression test that rejects oversized numeric maxSize values early.

@0x666c6f 0x666c6f changed the base branch from main to morpho-main March 9, 2026 09:08
@0x666c6f 0x666c6f requested a review from Copilot March 9, 2026 09:08
@0x666c6f
Copy link
Copy Markdown
Collaborator Author

0x666c6f commented Mar 9, 2026

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Mar 9, 2026

Claude finished @0x666c6f's task in 1m 31s —— View job


PR Review: fix(erpc:PLA-751): ignore oversized eth_getLogs payloads

  • Read and understand all changed files
  • Analyze code quality, correctness, and security
  • Post review feedback

Overall this is a well-structured PR. The transport sanitization, cache-key separation, and filtering across all three paths (direct, chunked, fallback) are cleanly implemented. The test coverage is good. A few issues worth addressing:


1. Float64 overflow in extractGetLogsMaxDataBytes (Bug)

File: architecture/evm/eth_getLogs_payload_filter.go:39-46

The Codex review flagged this correctly. When maxSize arrives as a float64 from JSON (the default for json.Unmarshal), values exceeding math.MaxInt64 (e.g., 1e20) will silently produce undefined behavior on int64(v) conversion. This should be bounded:

case float64:
    if v < 0 {
        return 0, fmt.Errorf("%s must be greater than or equal to 0", getLogsMaxSizeField)
    }
    if math.Trunc(v) != v {
        return 0, fmt.Errorf("%s must be an integer", getLogsMaxSizeField)
    }
    if v > float64(math.MaxInt64) {
        return 0, fmt.Errorf("%s exceeds maximum allowed value", getLogsMaxSizeField)
    }
    return int64(v), nil

Fix this ->


2. getLogsFilter.matchesLog silently passes when data parsing fails (Minor)

File: architecture/evm/eth_getLogs_payload_filter.go:99-107

if f.maxDataBytes > 0 {
    dataNode := log.Get("data")
    if dataNode != nil && dataNode.Exists() {
        data, err := dataNode.String()
        if err == nil && logDataExceedsLimit(data, f.maxDataBytes) {
            return false
        }
    }
}

If dataNode.String() returns an error, the log passes the filter. This is a reasonable fail-open behavior, but worth noting — if a malformed log entry has a non-string data field, it would bypass the size check. Consider whether fail-closed (return false on error) is more appropriate for your use case.


3. sanitizeForwardParams only handles eth_getLogs — consider centralizing

File: common/request.go:1009-1033

The sanitizeForwardParams function is clean and correct. The deep copy before deletion is the right approach. Just noting that if more custom params are added for other methods in the future, this pattern should be generalized rather than adding more method-specific branches.


4. extractGetLogsPayloadLimitFromRequest — request-level maxSize takes absolute precedence

File: architecture/evm/eth_getLogs_payload_filter.go:64-85

When maxSize is present in the request params, it completely overrides the configured GetLogsMaxDataBytes. This means a client could set maxSize: 0 to disable filtering even when the server has a configured limit. If that's intentional (client opt-out), it's fine, but if the server limit should act as a floor, the logic should use min(requestLimit, configLimit) when both are non-zero. Worth confirming the intended behavior.


5. Test coverage is solid

The new tests cover:

  • Invalid maxSize rejection at the project pre-forward level
  • Cache chunking with payload filtering
  • Direct upstream post-forward filtering
  • Configured defaults vs request-level override
  • Transport sanitization (stripping maxSize from wire, preserving cache hash)
  • The blockReceipts fallback path also receives payloadLimit

One additional test that would be valuable: a test for maxSize: 0 in the request when GetLogsMaxDataBytes is configured, to document the override-vs-floor behavior.


6. Codex P1 comment about buffer lifetime — likely not an issue here

The Codex review raised a concern about cleanup() after ParseFromBytes in http_json_rpc_client.go. This doesn't appear to be part of this PR's changes (the diff doesn't touch that file's buffer handling). The concern about ParseFromBytes retaining slices into pooled buffers would be a pre-existing issue, not introduced by this PR.


Summary

The implementation is clean and well-tested. The main actionable item is the float64 overflow bounds check (item 1), which is a real bug that could silently produce incorrect limits from malicious or careless input. The other items are minor design considerations.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in per-log data payload size cap for eth_getLogs, allowing oversized logs to be dropped before merge/cache while ensuring upstreams only receive standard JSON-RPC parameters.

Changes:

  • Introduces getLogsMaxDataBytes network config + request override via eth_getLogs.params[0].maxSize.
  • Sanitizes forwarded upstream payloads by stripping maxSize while preserving cache-key separation.
  • Applies payload filtering across direct responses, split sub-requests, and block-receipts fallback; adds regression tests and docs.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
typescript/config/src/generated.ts Exposes getLogsMaxDataBytes in generated TS config types.
typescript/config/lib/generated.d.ts Exposes getLogsMaxDataBytes in published TS typings.
docs/pages/config/projects/networks.mdx Documents per-log payload cap and request override behavior.
common/config.go Adds GetLogsMaxDataBytes to Go network config struct.
common/validation.go Validates GetLogsMaxDataBytes is non-negative.
common/defaults.go Copies GetLogsMaxDataBytes from network defaults when unset.
common/defaults_test.go Tests default-copy behavior for GetLogsMaxDataBytes.
common/request.go Strips maxSize from forwarded upstream request payloads.
common/request_test.go Tests cache-hash separation + upstream payload sanitization.
architecture/evm/eth_getLogs_payload_filter.go Implements maxSize parsing and response filtering by log data size.
architecture/evm/eth_getLogs_blockreceipts_fallback.go Extends log filter to include per-log data size limit; plumbs into receipts fallback.
architecture/evm/eth_getLogs.go Applies payload filtering in upstream post-forward, split sub-requests, and receipts fallback path.
architecture/evm/eth_getLogs_test.go Adds tests covering direct, chunked, fallback, and transport behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread common/request.go
Comment on lines 978 to 983
if body := r.Body(); len(body) > 0 {
return body, nil
method, _ := r.Method()
if !strings.EqualFold(method, "eth_getLogs") {
return body, nil
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

ForwardBody now bypasses the raw-body fast path for all eth_getLogs requests, even when params[0] does not include maxSize. That forces a full JSON-RPC unmarshal + re-marshal on every unmodified eth_getLogs, which is likely a noticeable perf regression on high-throughput endpoints. Consider preserving the fast path when maxSize is absent (e.g., detect params[0].maxSize cheaply and only re-marshal when it exists).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b02ade6.

Change:

  • preserve the raw-body fast path for unmodified eth_getLogs requests unless the raw payload actually contains "maxSize"
  • added a regression test covering the no-maxSize fast-path case in common/request_test.go

if v > float64(math.MaxInt64) {
return 0, fmt.Errorf("%s must be less than or equal to %d", getLogsMaxSizeField, int64(math.MaxInt64))
}
return int64(v), nil
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

extractGetLogsMaxDataBytes treats an empty string maxSize: "" as 0 (disabled). Because extractGetLogsPayloadLimitFromRequest treats presence of the key as an override, this can silently disable a non-zero configured GetLogsMaxDataBytes. It would be safer to reject empty strings as invalid input, or treat them as “not set” so the configured default still applies.

Suggested change
return int64(v), nil
return 0, fmt.Errorf("%s must not be empty", getLogsMaxSizeField)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b02ade6.

Empty-string maxSize is now rejected as invalid input instead of silently overriding the configured default to 0. Added regression coverage in architecture/evm/eth_getLogs_test.go.

Comment on lines +450 to +462
if reqFilter != nil {
filter := newGetLogsFilter(reqFilter["address"], reqFilter["topics"], payloadLimit)
dropped, err := filterGetLogsResponseByDataLimit(jrr, filter)
if err != nil {
return nil, err
}
if dropped > 0 {
n.Logger().Debug().
Int("droppedLogs", dropped).
Int64("maxSize", payloadLimit).
Msg("filtered oversized eth_getLogs payloads")
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

When payloadLimit > 0, filtering is currently skipped unless the request params can be asserted to map[string]interface{} (reqFilter != nil). This means the configured GetLogsMaxDataBytes cap won’t apply for requests with missing/invalid filter objects, even though you already computed a non-zero payloadLimit. Consider applying the size-only filtering with newGetLogsFilter(nil, nil, payloadLimit) when reqFilter is nil so the cap is consistently enforced.

Suggested change
if reqFilter != nil {
filter := newGetLogsFilter(reqFilter["address"], reqFilter["topics"], payloadLimit)
dropped, err := filterGetLogsResponseByDataLimit(jrr, filter)
if err != nil {
return nil, err
}
if dropped > 0 {
n.Logger().Debug().
Int("droppedLogs", dropped).
Int64("maxSize", payloadLimit).
Msg("filtered oversized eth_getLogs payloads")
}
}
// Always enforce payloadLimit: fall back to size-only filtering when reqFilter is nil.
var addr interface{}
var topics interface{}
if reqFilter != nil {
addr = reqFilter["address"]
topics = reqFilter["topics"]
}
filter := newGetLogsFilter(addr, topics, payloadLimit)
dropped, err := filterGetLogsResponseByDataLimit(jrr, filter)
if err != nil {
return nil, err
}
if dropped > 0 {
n.Logger().Debug().
Int("droppedLogs", dropped).
Int64("maxSize", payloadLimit).
Msg("filtered oversized eth_getLogs payloads")
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b02ade6.

The payload cap is now always enforced when non-zero: if the request filter object is missing/unparseable we fall back to size-only filtering via newGetLogsFilter(nil, nil, payloadLimit). Added regression coverage for that path in architecture/evm/eth_getLogs_test.go.

@0x666c6f 0x666c6f merged commit b002533 into morpho-main Mar 9, 2026
10 checks passed
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.

2 participants