Skip to content

fix(rpc): handle 3-byte RLP list headers in decode_method_call_data - #1746

Open
memosr wants to merge 1 commit into
genlayerlabs:v0.123-devfrom
memosr:fix/decode-method-call-data-rlp-header
Open

fix(rpc): handle 3-byte RLP list headers in decode_method_call_data#1746
memosr wants to merge 1 commit into
genlayerlabs:v0.123-devfrom
memosr:fix/decode-method-call-data-rlp-header

Conversation

@memosr

@memosr memosr commented Aug 22, 2026

Copy link
Copy Markdown

Fixes #1745

What

  • Compute the outer RLP list header length from its prefix instead of assuming 2 bytes

  • Accept a trailing 0x01, so leader_only=True payloads are unwrapped like leader_only=False ones

  • Guard against inputs too short to be a wrapped payload

  • Add unit tests for decode_method_call_data, which previously had none

Why

decode_method_call_data unwraps the rlp([calldata, leader_only]) payload that genlayer-js sends for gen_call (type "read") and eth_call. It parsed the outer list header by hand:

if raw_bytes[0] >= 0xF8:      # Long list

    raw_bytes = raw_bytes[2:]  # Skip list prefix and length

An RLP long list header is 0xF7 + <number of length bytes>, so it is 3 bytes once the payload reaches 256 bytes, not always 2. Past that point the slice lands at the wrong offset:

rlp.exceptions.DecodingError: RLP string ends with 255 superfluous bytes

The boundary is exact, 252 bytes of calldata decode and 253 bytes fail, which is roughly 235 characters of string argument. Both callers in endpoints.py are affected, so this breaks ordinary contract reads with moderately sized arguments.

Two further defects shared the same root cause and are fixed in the same change:

  • leader_only=True encodes as a trailing 0x01, so the raw_bytes[-1] == 0 guard never fired and the entire RLP payload was returned as if it were calldata. This corrupted silently rather than raising, and leaderOnly is a public option on genlayer-js readContract.

  • Empty or single-byte input raised IndexError.

Testing done

Behaviour of the current code on v0.123-dev versus this change:

| case | before | after |

|---|---|---|

| calldata 252 bytes, leader_only=False | passes | passes |

| calldata 253 bytes, leader_only=False | DecodingError | passes |

| calldata 1000 bytes, leader_only=False | DecodingError | passes |

| any size, leader_only=True | wrong value, no error | passes |

| bare unwrapped calldata | passes | passes |

| "0x" | IndexError | passes |

  • Added tests to tests/unit/test_transactions_parser.py covering the 252/253 boundary, payloads up to 70000 bytes, the leader_only path, and degenerate inputs. All fail on v0.123-dev before the change and pass after.

  • Full unit suite: 1218 passed, 7 skipped. Baseline before the change was 1203 passed, 7 skipped, so no regressions.

  • black --check clean on both files.

Decisions made

  • Kept the existing structure and fixed the header arithmetic rather than replacing the hand-rolled unwrapping with a plain rlp.decode and a shape check. The latter is cleaner but changes behaviour for unwrapped legacy payloads that happen to decode as a 2-element list, and this PR is meant to be a surgical fix. Happy to do that refactor separately if you prefer it.

  • Widening the trigger to a trailing 0x01 means an unwrapped legacy payload ending in 0x01 would now be treated as wrapped. The same exposure already existed for 0x00. The length guard limits it, but flagging it explicitly since it is the one behavioural widening here.

  • Targeting v0.123-dev since that is where recent PRs land. The same defects are present on v0.121 and main; happy to open companion PRs.

Checks

  • I have tested this code

  • I have reviewed my own PR

  • I have created an issue for this PR

  • I have set a descriptive PR title compliant with conventional commits

Reviewing tips

The functional change is 6 lines in transactions_parser.py. The key line is the header length, 1 + (prefix - 0xF7), which follows directly from the RLP spec: for a long list the prefix encodes how many length bytes follow.

The quickest way to confirm the bug independently is to check out v0.123-dev and run the new tests, which fail there.

User facing release notes

Fixes contract read calls (gen_call and eth_call) failing with an RLP decoding error when arguments were large enough to push the request payload past 255 bytes, roughly 235 characters of string argument. Also fixes reads made with leaderOnly: true being decoded incorrectly.

`decode_method_call_data` unwraps the rlp([calldata, leader_only]) payload
that genlayer-js sends for `gen_call` (type "read") and `eth_call`. It
parsed the outer list header by hand and assumed a long list header is
always 2 bytes.

An RLP long list header is 0xF7 + the number of length bytes, so it is
3 bytes once the payload reaches 256 bytes. Any read whose calldata pushes
the payload past that point hit the wrong offset:

    rlp.exceptions.DecodingError: RLP string ends with 255 superfluous bytes

The boundary is exact: 252 bytes of calldata decode, 253 bytes fail. In
practice that is roughly 235 characters of string argument.

Two further defects shared the same root cause and are fixed here:

- `leader_only=True` encodes as a trailing 0x01, so the `raw_bytes[-1] == 0`
  guard never fired and the entire RLP payload was returned as if it were
  calldata. This failed silently rather than raising, and `leaderOnly` is a
  public option on genlayer-js `readContract`.
- Empty or single-byte input raised IndexError.

The header length is now computed from the prefix. Every existing test of
this method mocks it, so it had no real coverage; adds tests covering the
252/253 boundary, payloads up to 70000 bytes, the leader_only path, and
degenerate inputs.

Follow-up to genlayerlabs#1184, which introduced the 2-byte assumption.
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 60beeafa-0420-42b4-92ee-1e0be14a71b2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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