Skip to content

fix(calldata): accept boolean enum keys instead of failing the descriptor - #6

Open
venugopalanvip wants to merge 10 commits into
sourcifyeth:mainfrom
venugopalanvip:fix-boolean-enum-ordinals
Open

venugopalanvip wants to merge 10 commits into
sourcifyeth:mainfrom
venugopalanvip:fix-boolean-enum-ordinals

Conversation

@venugopalanvip

Copy link
Copy Markdown

This is the library clear-signing-erc7730-registry installs since ethereum/clear-signing-erc7730-registry#3008, so the descriptors below are ones its CI is carrying today.

The bug

An enum key is the field value it matches — the v2 schema is explicit:

Enumeration keys are the field values and enumeration values are the displayable strings

A bool holds 0 or 1 in calldata, so true and false are the keys a descriptor writes for a bool field. JSON has no boolean object key, so they arrive as text.

convert_enums ran int(ordinal) on them. int("true") raises, and nothing catches it until the top of the conversion, where the handler discards the entire descriptor — every format, every deployment — and returns an empty list:

🟠 warning: Error processing v2 ERC-7730 file None, skipping it
🔴 error: Failed processing descriptor: invalid literal for int() with base 10: 'true'

One bool enum is enough. The descriptor still passes the registry's own CI, because neither reference test runner goes through this converter — the failure only appears when device descriptors are generated.

What it costs on registry master today

descriptor enum before after
registry/lido/calldata-WithdrawalQueueERC721.json {"true": "Allow all", "false": "Revoke all"} 0 11
registry/ekubo/calldata-MEVCaptureRouter.json {"false": "Token 0", "true": "Token 1"} 0 168
registry/flyingtulip/calldata-PftNft.json {"True": "Grant all", "False": "Deny all"} 0 10

I found them by converting all 280 calldata descriptors in the registry and looking for ones that produced nothing.

The Lido entry landed on registry master on 2026-09-21 (#2989); I raised this problem on that PR before it merged and offered to rename the keys to "0"/"1", and the maintainers took it as written — which I think is right, since the schema says keys are field values and true/false are a bool field's values. The casing differs between the three because nothing constrains it.

It is also about to get more common: registry #2912 proposes a bool enum for the DAI permit allowed flag.

The fix

Boolean literals map to the value the bool holds, case-insensitively and ignoring surrounding space:

BOOLEAN_ORDINALS = {"true": 1, "false": 0}

This matches how the constraint encoder in the same package already treats a bool value:

if isinstance(value, bool):
    return bytes([1 if value else 0])

A key that is neither decimal nor boolean still raises. Silently mapping it to a number would build an enum entry the device can never match, which is worse than the loud failure.

Tests

tests/convert/calldata/v1/test_enum_ordinal.py: decimal keys including a negative, the six boolean spellings plus a padded one, and five keys that must still raise.

Also raised upstream as LedgerHQ/python-erc7730#345, where it has not been reviewed. Sending it here too since this is the copy the registry actually runs.

One thing I did not change

convert_enums takes no OutputAdder, so a bad key can only raise, and the handler that catches it cannot attribute the failure to an enum — the message never names the descriptor, the enum id, or the key. Threading out through would let a single unusable entry be reported and skipped rather than costing the whole file. Wider than this fix needs; happy to follow up.

…ptor

An enum key is the field value it matches, and a bool holds 0 or 1 in calldata,
so `true` and `false` are the keys a descriptor writes for a bool field. JSON
has no boolean object key, so they arrive as text.

`convert_enums` ran `int(ordinal)` on them. That raises, and the exception is
caught only at the top of the conversion, so one bool enum discards every
format and every deployment of the descriptor and returns an empty list.

Two descriptors in the clear-signing registry produce nothing today:

  registry/flyingtulip/calldata-PftNft.json          {"True": …, "False": …}    0 -> 6
  registry/ekubo/calldata-MEVCaptureRouter.json      {"false": …, "true": …}    0 -> 168

The casing differs because nothing constrains it: the v2 schema says
"enumeration keys are the field values" and allows any string.

Boolean literals now map to the value the bool holds, case-insensitively.
A key that is neither decimal nor boolean still raises, because silently
mapping it to a number would build an entry the device can never match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
paoun-ledger and others added 9 commits October 9, 2026 07:46
* feat(v2): support map references in field parameters

Map references ({"map": ..., "keyPath": ...}) in v2 field parameters were
dropped with a warning. They are now resolved:

- a constant key ($.…) selects the map value at conversion time
- a data/container key is kept as a ResolvedValueMap lookup in the
  resolved model (token, chainId, senderAddress, callee, selector, amount,
  spender, collection)
- maps keyed on @.chainId / @.to are checked against every deployment, and
  resolved per deployment when generating calldata descriptors
- legacy EIP-712 conversion reports map lookups as unsupported

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(v2): emit device side maps in calldata descriptors

Map lookups keyed on calldata (#.…) or on @.from / @.value were rejected
by the calldata converter. They are now looked up by the device, as
specified in app-ethereum doc/tlv_structs.md#map_entry:

- add a MAP_REF value source (VALUE tag 0x06: version, map id, key as a
  nested VALUE with a data or container path)
- emit one MAP_ENTRY instruction per map key (version 0x01, chain id,
  contract address, selector, id, key, value) in a new "maps" list next
  to "enums", for CAL to sign with the calldata key and serve
- encode keys exactly as the device reads them: 32 bytes ABI word for
  static values, raw content for dynamic ones, the slice for sliced
  paths, 20 bytes for @.from, 32 bytes for @.value
- allocate map ids per descriptor, sharing them between lookups with the
  same key and value encodings
- reject keys / values the device cannot store or match (empty or over
  32 bytes, not fitting the key type, duplicate encodings, multi-valued
  key paths, key VALUE over 48 bytes)
- keep folding @.chainId / @.to maps into constants, as a descriptor is
  bound to a single deployment
- keep the referenced map path in ResolvedValueMap to identify entries

Note MAP_ENTRY version is 0x01 as enforced by map_entry.c, the doc still
states 0x0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(v2): address review findings on map references

- map keys linter: only build the ABI tree for formats with map lookups keyed
  on transaction data, so formats using fixed point types still lint
- reject hex constants with an odd number of digits, which could not be
  decoded when serializing map values and constants
- reject maps with hex keys only differing by case, since hex keys are matched
  case-insensitively
- calldata: warn when a tokenAmount chainId differs from the deployment chain,
  as PARAM_TOKEN_AMOUNT cannot encode it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(v2): reject token amounts on another chain, simplify map references

- reject a tokenAmount chainId that differs from the deployment chain
  (plain value or map lookup): PARAM_TOKEN_AMOUNT cannot encode it, and
  the device would display the token with a wrong ticker and decimals
- reuse the values encoded at resolution for MAP_ENTRY structs
- share is_deployment_key between the converter and the map keys linter
- use a single pattern to reject optional parameters failing to convert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(v2): reject token tickers and chain id paths on another chain

* test(v2): fix token chain test field typing

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: ckorchane-ledger <104824609+ckorchane-ledger@users.noreply.github.com>
Co-authored-by: ldg-github-ci <ldg-github-ci@users.noreply.github.com>
Bumps [jdx/mise-action](https://github.com/jdx/mise-action) from 4.3.0 to 5.0.1.
- [Release notes](https://github.com/jdx/mise-action/releases)
- [Changelog](https://github.com/jdx/mise-action/blob/main/CHANGELOG.md)
- [Commits](jdx/mise-action@v4.3.0...v5.0.1)

---
updated-dependencies:
- dependency-name: jdx/mise-action
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ckorchane-ledger <104824609+ckorchane-ledger@users.noreply.github.com>
app-ethereum >= 1.22.4 rejects PARAM_UNIT structs without DECIMALS (6a80),
although its spec documents the tag as optional with a default of 0. Unit
fields without decimals (e.g. opencover "Cover duration") therefore fall
back to blind signing on released devices.

Always serialize DECIMALS, defaulting to 0, which is byte-for-byte the
value the app is specified to assume.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: ldg-github-ci <ldg-github-ci@users.noreply.github.com>
…erHQ#347)

`erc7730 schema` was the only command with no way to reach the v2 models.
`lint`, `resolve`, `calldata` and `convert` all take --v2 or detect it from
$schema; `schema` printed the v1 model either way, so the schema that
`erc7730 lint --v2` actually enforces could not be obtained from the tool.

That is the schema worth having. A descriptor is validated twice by the
clear-signing registry: against `specs/erc7730-v2.schema.json`, which is
maintained by hand, and against these models, by `erc7730 lint`. When the two
disagree the descriptor passes one check and fails the other, which is what
issue LedgerHQ#338 is. Printing the model's schema is how that comparison gets made.

The flag switches both forms, so `schema resolved --v2` works too. Tests cover
all four combinations and assert the flag reaches the v2 model rather than only
that a schema was printed: `mustMatch` is a v2 visibility rule and `excluded` a
v1 field, so each name appears in exactly one of the two schemas.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

4 participants