Skip to content

Clear signing: readable parser rework — reinterpret instead of skip - #68

Draft
PrisionMike wants to merge 6 commits into
mainfrom
prisionmike/cs-parser-readable
Draft

Clear signing: readable parser rework — reinterpret instead of skip#68
PrisionMike wants to merge 6 commits into
mainfrom
prisionmike/cs-parser-readable

Conversation

@PrisionMike

@PrisionMike PrisionMike commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Based on main (#66 landed). The registry-ingestion half is stacked on top as #69.

Reviewer's guide

The parser file is rewritten top-to-bottom, so review it as a fresh read, not a diff. The module docstring is the map: it draws the pipeline and defines the two outcomes every descriptor field has — DROP (raise UnsupportedFeature with a distinct feature tag; the whole display format for that signature is dropped, we never emit a format with a field silently missing) and ADJUST (the field is kept but deliberately bent into something the firmware can render, and every bend is logged). Every function serves one of those two verdicts, so it is always clear when a feature was seen and when a field was rejected and why.

One commit per concern, in review order:

  1. refactor: restructure parser around DROP/ADJUST + reinterpretations — sections 1-5 follow the pipeline; _FormatContext replaces parameter threading; _build_one_format returns (candidate, issues, adjustments) (issues non-empty ⇒ dropped), replacing the old nonlocal had_issue flag. Includes the uint→address ABI retype for addressName/tokenPath over packed uints (uintN and address share the one-word static layout; dirty values degrade to blind signing on-device) and const-value handling superseding build_const_field.
  2. bytes20/int160 + trailing byte slices — reconcile with the bytesN-types and sliced-paths firmware branches. A statically 20-byte slice (token.[-20:]) classifies as address, so 1inch packed-address fields need no retype and no adjustment. Recovers tBTC (bytes20) and all 1inch V5/V6 slice fields.
  3. FORMATTER_CALLDATA — from the calldata-formatter firmware branch: callee_path (required; resolves like tokenPath, incl. packed-uint retype) and 4-byte selector for args-only embedded calldata. Registry extras (amountPath, spenderPath) have no proto representation and are logged as calldata-params-ignored.
  4. FORMATTER_ENUM — from the enum-support firmware branch: params.$ref → $.metadata.enums.* resolved into enum_values (uint32 key → display string). True/False keys map to 1/0 (enum-bool-keys adjustment); every accepted enum field is logged (enum-field).
  5. optional visibility hidden, rule objects rejectedvisible: optional means "wallets MAY display this field if possible or sensible", so it is hidden like never and logged (optional-field-hidden); the spec's rule objects (ifNotIn/mustMatch) are true conditionals the firmware cannot honor and drop the format (conditional-visibility).
  6. provider_namemetadata.owner, falling back to <registry subdir>: <contractName> or the subdir, emitted on every record.

Requires

A trezorlib carrying the union of the calldata-formatter and enum-support firmware branches (FORMATTER_CALLDATA/callee_path/selector + FORMATTER_ENUM/enum_values, plus slice_start/slice_end and provider_name). The fail-fast guard in common.py names exactly what's missing.

Numbers (with #69's ingestion, registry 5a67786)

3590 records emitted, all serializing — 32 calldata fields, 162 enum fields, 709 slice-bearing field defs, provider_name on every record, 28 optional fields hidden (logged). Remaining drops: nftName and misc formatters (8), nested field groups, deep ABI shapes.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 55c8d4c5-22bd-4d3b-a02d-6b0123a71495

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch prisionmike/cs-parser-readable

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.

@PrisionMike PrisionMike left a comment

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.

Self-review: inline notes on the parts worth the most reviewer attention. Suggested reading order: module docstring -> section 5 (_build_one_format) -> section 4b (the retype) -> the per-formatter helpers.

# raises on-device, which degrades safely to blind signing).
# ---------------------------------------------------------------------


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.

This block is the crux of the PR — why a uint256 leaf may be retyped to address.

The short version: the descriptor author says "render this uint as an address" (addressName), the firmware formatter raises on int, and the only lever the parser has is the declared ABI type. Since uintN and address are both one static 32-byte word, the retype cannot move or corrupt any other decoded value — worst case the word isn't a clean address at runtime and the firmware's parse_address padding check aborts clear-signing into the ordinary blind-signing flow.

Real registry cases this recovers: 1inch uniswapV3Swap(..., uint256[] pools) ("Last pool" field), 1inch NativeOrderFactory makerOrder.receiver.

represent, so the caller skips the whole descriptor file rather than emit a
display format with a missing field.
A path may also point at a whole array (trailing `.[]` iteration, no
index appended): the firmware formats each element, so the element type

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.

_abi_leaf_at is the mechanical half (pure tree walk, no mutation); _retype_uint_leaf_as_address below is the policy half. The docstring's worked example is the fastest way to convince yourself the index semantics match path_to_dict: names/tuple fields consume an index into fields, array indices step into the shared element type, and a trailing .[] iteration leaves the path on the array so the final unwrap loop lands on the element.

`pools.[-1]` retypes every `pools` element — and any other field reading
the same leaf sees an address from then on. That matches the author's
intent (the whole array holds packed addresses) and is logged by the
caller as an adjustment.

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.

Note the deliberate side effect called out here: arrays have ONE shared element type, so retyping pools.[-1] retypes every pools element, and any other field reading the same leaf (e.g. a raw view of pools.[0]) now sees an address. The early-return for ABI_ADDRESS is what makes two addressName fields on the same leaf idempotent.


Returns `(candidate, issues, adjustments)`:
* `candidate` — the buildable display format, or None;
* `issues` — every unsupported feature hit, as (feature, detail).

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.

The contract that replaced nonlocal had_issue: _build_one_format returns (candidate, issues, adjustments) and issues non-empty means the format is dropped. Two properties worth checking:

  1. The field loop keeps scanning after the first issue, so the log lists everything wrong with a signature, not just the first find.
  2. adjustments are returned only when issues is empty — a dropped format's retypes die with its parameter_definitions, so the log never reports an adjustment for something that wasn't emitted.



@dataclasses.dataclass
class _FormatContext:

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.

_FormatContext is scoped to ONE display format (one signature): parameter_definitions is rebuilt per signature, so an in-place retype can never leak across signatures or files. This replaces the six-parameter threading through the old build_field_dict.

@PrisionMike

Copy link
Copy Markdown
Collaborator Author

Second commit added: trailing byte-slice support, reconciling with the prisionmike/clear-signing/sliced-paths firmware branch (slice_start/slice_end on EthereumERC7730Path; the firmware views a sliced word as 32 big-endian bytes).

Highlights: token.[-20:]-style 20-byte slices classify as address — so 1inch's packed-address fields and token paths work with no ABI retype and no adjustment (the slice is exact semantics, unlike the uint→address retype which remains for unsliced packed values). goodUntil.[-4:] + date is allowed (DateFormatter converts sliced bytes). Slices of arrays, non-trailing slices, and slices after .[] stay dropped with their own reasons.

Note for reviewers: this makes the PR require a trezorlib built from the sliced-paths firmware branch (the common.py fail-fast guard now checks slice_start). 162 tests pass; 1inch AggregationRouterV6 alone recovers 247 records.

@PrisionMike

Copy link
Copy Markdown
Collaborator Author

Amended the firmware-reconcile commit (force-pushed): it now also covers the bytesN-types branch — bytes20 and int160 added to the ABI type map (ABI_BYTES20/ABI_INT160). bytes20 classifies bytes-like, int160 numeric; the uint→address retype deliberately does not apply to the signed int160. This recovers the tBTC/threshold descriptors — all 7 remaining unrepresentable-params drops were bytes20. 127 parser tests pass.

@PrisionMike
PrisionMike marked this pull request as draft July 26, 2026 22:52
PrisionMike and others added 4 commits July 27, 2026 23:40
…tations

Rework the descriptor parser for reviewability and fold in the reinterpret-
instead-of-skip semantics:

Structure (behavior-preserving):
* Module docstring states the pipeline and the two outcomes every field has:
  DROP (UnsupportedFeature, format dropped whole) or ADJUST (kept, bent,
  logged). Sections 1-5 follow the pipeline order.
* `_FormatContext` replaces six threaded parameters; `ctx.adjust()` records
  accepted-but-modified fields.
* `_build_one_format` returns (candidate, issues, adjustments) — the
  `nonlocal had_issue` closure flag is gone; "issues non-empty means drop"
  is now the visible contract.
* The 265-line build_field_dict is split: `_build_path_field` orchestrates,
  with `_check_kind_or_reinterpret`, `_apply_token_amount_params`,
  `_apply_threshold`, `_apply_unit_params`, `_apply_date_params` and
  `_build_non_path_field`/`_const_value_field` each owning one concern.
* Deployment expansion extracted to `_expand_deployments`.

Semantics (each logged as an adjustment or a distinct drop reason):
* addressName on a uint leaf retypes the ABI leaf to ABI_ADDRESS in place
  (`_abi_leaf_at` + `_retype_uint_leaf_as_address`; uintN and address share
  the one-word static layout, dirty values degrade to blind signing).
  Same for tokenAmount's tokenPath at a packed uint. addressName on bytes
  passes through (firmware renders hex).
* Constant fields extend build_const_field (which this replaces): non-string
  values stringify, $.metadata.constants.* works as a field path, and each
  failure mode has its own drop reason instead of a silent None.
* path_to_dict raises a distinct UnsupportedFeature per drop reason
  (per-element-field-path, array-slice-path, unknown-path-segment,
  iteration-over-non-array, unparseable-path, descriptor-path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconcile with the prisionmike/clear-signing/bytesN-types and sliced-paths
firmware branches.

bytesN-types (ABI_BYTES20 / ABI_INT160 in the proto enum):
* _ABI_TYPE_MAP emits bytes20 and int160. bytes20 classifies as a bytes-like
  scalar; int160 as numeric (decodes to int on-device). The uint->address
  retype deliberately does NOT apply to the signed int160.
* Recovers the tBTC/threshold descriptors (all 7 remaining
  unrepresentable-params drops were bytes20).

sliced-paths (slice_start/slice_end sint32 on EthereumERC7730Path; the
walker views a sliced word as 32 big-endian bytes, DateFormatter accepts
sliced bytes as a big-endian timestamp):
* path_to_dict accepts one trailing slice on a scalar leaf. A statically
  20-byte slice (`token.[-20:]`, the 1inch packed-address pattern) classifies
  KIND_ADDRESS — addressName and tokenPath work with no retype and no
  adjustment; other slices classify KIND_BYTES.
* `date` over a sliced word is allowed (exact firmware semantics).
* Still dropped, each with a distinct reason: slices of arrays/tuples
  (selects elements, not bytes), non-trailing slices, slices after `.[]`
  (the firmware would slice the array rather than each element), and
  out-of-sint32 bounds.
* common.py carries the slice bounds on the data-path variant and the
  fail-fast proto guard now checks for slice_start.

1inch AggregationRouterV6 alone recovers 247 records (364 sliced fields);
its only remaining drops are `enum` formatters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The calldata-formatter firmware branch renders a `calldata` field as the
nested call it embeds: it resolves the callee address from `callee_path`,
fetches that contract's display format, and expands the field into the
nested call's rows. `selector` (4 bytes) marks args-only embedded calldata.

* `calleePath` is required (the firmware raises without it) and resolves
  like tokenPath — address kind, or a packed uint retyped to address
  (logged as callee-address-in-numeric).
* Registry extras with no proto representation (amountPath, spenderPath)
  are dropped from the field and logged as calldata-params-ignored; the
  nested call still renders in full via the callee's display format.
* Distinct drop reasons: calldata-missing-callee, unresolvable-callee-path,
  invalid-calldata-selector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The enum-support firmware branch adds FORMATTER_ENUM and an on-device
EnumFormatter: the decoded calldata value is a key into a descriptor-
supplied mapping (EthereumERC7730EnumEntry, uint32 key -> display string);
a key missing from the mapping fails clear signing into blind signing
rather than risk showing a wrong value.

* `params.$ref` resolves against `$.metadata.enums.<name>`; keys must fit
  uint32. The JSON-boolean variant (True/False keys over a bool value,
  flyingtulip) maps to 1/0 — on-device the decoded bool compares equal —
  logged as enum-bool-keys.
* Every accepted enum field is logged (enum-field adjustment) so use of the
  feature is visible while firmware support is fresh.
* Distinct drop reasons: enum-missing-ref, unresolvable-enum-ref,
  invalid-enum-entry.

Recovers the aave interest-rate-mode, okx swap-direction, celo governance
and flyingtulip enum fields (19 unsupported-formatter drops).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PrisionMike
PrisionMike force-pushed the prisionmike/cs-parser-readable branch from e138757 to f119fe8 Compare July 27, 2026 21:48
@PrisionMike
PrisionMike changed the base branch from prisionmike/const-value-path-main to main July 27, 2026 22:17
@PrisionMike

Copy link
Copy Markdown
Collaborator Author

Rebuilt the branch history (force-push): rebased onto main (#66 landed), and per review feedback the calldata-as-raw stopgap is edited out of history — commit 3 introduces FORMATTER_CALLDATA directly. New commits: enum support (FORMATTER_ENUM/enum_values), conditional-visibility rejection, and provider_name. PR body rewritten as the reviewer's guide; the earlier inline self-review comments still describe commits 1–2.

PrisionMike and others added 2 commits July 28, 2026 06:49
The registry's `visible` vocabulary is always / never / optional, plus the
spec's rule objects (ifNotIn / mustMatch).

`optional` means "wallets MAY display this field if possible or sensible"
(ERC-7730) — hiding it is spec-compliant, so it is skipped like `never`
and the choice is logged (optional-field-hidden adjustment).

The rule objects are true conditionals the firmware cannot honor either
way — mustMatch even carries a validation duty — so they drop the display
format with their own reason (conditional-visibility) instead of being
displayed or hidden unconditionally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The firmware shows the descriptor's provider next to the rendered call
(EthereumDisplayFormatInfo.provider_name, externalise-provider-name).
`metadata.owner` is authoritative; ownerless descriptors (1inch, safe, …)
fall back to "<registry subdirectory>: <metadata.contractName>", or just
the subdirectory. Unset when nothing is known.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PrisionMike
PrisionMike force-pushed the prisionmike/cs-parser-readable branch from f119fe8 to 723a9e3 Compare July 28, 2026 04:49
@PrisionMike

Copy link
Copy Markdown
Collaborator Author

Amended the visibility commit (force-pushed): visible: optional is no longer penalized — per ERC-7730 it means "wallets MAY display this field if possible or sensible", so it's now hidden like never and logged as an optional-field-hidden adjustment. The conditional-visibility drop is reserved for the spec's rule objects (ifNotIn/mustMatch), which the firmware can't honor either way. Recovers the 29 affected display formats → 3590 records.

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