From 509dec6e3739218e04086c19c0579caec4fd0fb4 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 16 Sep 2026 17:50:34 -0600 Subject: [PATCH] feat(ethereum): add ERC-7730 definition streaming protocol --- docs/erc7730-compiled-format.md | 136 ++++++++++++++++++++++++++++++++ messages-ethereum.options | 11 +++ messages-ethereum.proto | 66 ++++++++++++++++ messages.proto | 4 + 4 files changed, 217 insertions(+) create mode 100644 docs/erc7730-compiled-format.md diff --git a/docs/erc7730-compiled-format.md b/docs/erc7730-compiled-format.md new file mode 100644 index 00000000..7b14da0a --- /dev/null +++ b/docs/erc7730-compiled-format.md @@ -0,0 +1,136 @@ +# ERC-7730 compiled definition protocol + +Status: alpha format 1. All multi-byte integers are unsigned big endian unless +the field says otherwise. Decoders reject unknown required flags, non-minimal +integers, duplicate sections, out-of-order sections and trailing bytes. + +## Trust boundary + +The host compiles ERC-7730 v2 JSON. Firmware never trusts a host decode: the +compiled program contains types, paths and display operations, while every +displayed transaction value is read from the exact calldata or canonical +EIP-712 value stream that the device signs. + +The signed envelope is purpose-separated from every older ClearSign format. +Its signature preimage begins with the 24-byte ASCII domain +`KEEPKEY:ERC7730:CATALOG\0`. A certified failure aborts signing; it never falls +back to a less specific certified display. Runtime/self-service metadata stays +additive and cannot suppress raw review. + +## Envelope + +| Field | Size | Meaning | +|---|---:|---| +| magic | 4 | `K773` | +| envelope version | 1 | `1` | +| purpose | 1 | `1` (ERC-7730 catalog) | +| definition length | 4 | bounded canonical program length | +| definition | variable | format below | +| proof count | 1 | number of 32-byte Merkle siblings | +| proof | 32 × count | sorted-pair SHA-256 inclusion proof | +| certificate length | 2 | root-certified delegate certificate length | +| certificate | variable | KeepKey delegation certificate | +| signature | 64 | compact secp256k1 signature over the domain and leaf | +| recovery | 1 | recovery identifier | + +The leaf is `SHA256(0x00 || definition)`. An internal node is +`SHA256(0x01 || min(left,right) || max(left,right))`. The certified catalog +root and delegate certificate bind provider identity, issuance epoch, +revocation epoch and the ERC-7730-only purpose. + +## Canonical program header + +| Field | Size | +|---|---:| +| magic | 4 (`C773`) | +| compiler format version | 1 (`1`) | +| ERC-7730 schema major/minor | 1 + 1 | +| definition kind | 1 | +| flags | 2 | +| chain id | 8 | +| contract/domain address | 20 | +| selector or primary type hash | 32 | +| source JSON SHA-256 | 32 | +| compiler identity SHA-256 | 32 | +| token/network set SHA-256 | 32 | +| provider id | 4 | +| issuance epoch | 4 | +| revocation epoch | 4 | +| section count | 1 | + +Calldata definitions use the first four bytes of `selector or primary type +hash` and require its remaining bytes to be zero. EIP-712 definitions use the +complete primary type hash and bind a canonical domain-constraint section. +Address zero means that the deployment section supplies all allowed targets; +otherwise it is the single allowed target. + +Each section is `type:u8 || length:u32 || payload`. Section types are strictly +ascending. Format 1 defines: + +1. string table +2. ABI node table +3. path table +4. literal table +5. condition table +6. formatter table +7. display instruction table +8. deployment/domain constraints +9. resource declaration + +Indices are zero-based unsigned 16-bit integers. `0xffff` is the absent index. +Strings are UTF-8, length-prefixed by a minimal `u16`, contain no NUL, and are +stored in first-use order with duplicates interned. Byte literals use a `u32` +length. Tables use a `u16` entry count followed by entries. + +## Flat ABI node table + +Nodes are forward-only, so cycles are structurally impossible. A node is: + +`kind:u8 || size:u16 || first_child:u16 || child_count:u16 || array_length:u16` + +Kinds are uint, int, address, bool, fixed bytes, bytes, string, tuple and array +(values 1 through 9). Integer size is its legal Solidity bit width. Fixed bytes +size is 1–32. Tuple children are contiguous. An array has exactly one child; +array length `0xffff` means dynamic. The root is node zero and is a tuple for +calldata. EIP-712 definitions use the same flat table for their validated value +tree. + +Firmware requires canonical ABI: clean integer/address/bool/fixed-bytes +padding, valid UTF-8 strings, aligned in-bounds offsets, packed tails in member +order, no gaps/aliases/overlaps, and complete input coverage. + +## Paths and display program + +The compiler resolves every JSON `#`, `$` and `@` reference. A path entry +contains a source (`calldata`, `typed-data`, `container`, or `literal`), up to +16 signed 32-bit field/array indices, and an optional half-open slice. Negative +array indices are retained and resolved against the device-decoded length. +Full-array selection is an explicit flag, never an omitted index. Container +values are restricted to device-owned `from`, `to`, `value`, `chainId`, domain +and primary-type facts. + +Conditions are typed operations `always`, `never`, `empty`, `not-empty`, `in` +and `not-in` over paths and literal sets. Formatters cover raw values, native +and token amounts, NFT name, date, duration, unit, enum, chain id, address, +token ticker, ERC-7930 interoperable address, embedded calldata and encrypted +field fallback. Formatter operands are typed path/literal indices; a live +result has no opcode capable of replacing a decoded operand. + +Display instructions are a bounded linear program: intent text/interpolation, +field, group begin/end, array begin/end, separator and embedded call. Forward +jumps are allowed only for false conditions and loop ends. There are no +backward jumps except the bounded array iterator. Embedded calls carry a +definition lookup key and decrement the signed recursion budget. + +## Fixed firmware limits (format 1) + +- 64 ABI nodes and 8 levels of ABI nesting +- 64 aggregate decoded array elements +- 16 path components +- 64 paths, 64 fields and 32 conditions +- 96 interned strings, each at most 128 bytes +- 16 KiB canonical program and 1 KiB transport chunks +- 4 embedded-call levels and no repeated definition id in one call chain + +The resource section repeats the compiler's exact counts. Firmware recomputes +them while parsing and rejects disagreement or exhaustion. diff --git a/messages-ethereum.options b/messages-ethereum.options index fa1767f5..cb33327c 100644 --- a/messages-ethereum.options +++ b/messages-ethereum.options @@ -12,3 +12,14 @@ EthereumTypedDataStructAck.EthereumFieldType.struct_name max_size:80 EthereumTypedDataStructAck.EthereumFieldType.array_levels max_count:4 EthereumTypedDataValueRequest.member_path max_count:16 EthereumTypedDataValueAck.value max_size:1024 + +# ERC-7730 compiled definitions are streamed; no protobuf field allocates the +# complete signed catalog entry in firmware RAM. +EthereumClearSignDefinition.definition_id max_size:32 +EthereumClearSignDefinition.data max_size:1024 +EthereumClearSignDefinitionAck.definition_id max_size:32 +EthereumClearSignDefinitionRequest.definition_id max_size:32 +EthereumClearSignDefinitionRequest.contract_address max_size:20 +EthereumClearSignDefinitionRequest.selector_or_type_hash max_size:32 +EthereumClearSignDefinitionChunk.definition_id max_size:32 +EthereumClearSignDefinitionChunk.data max_size:1024 diff --git a/messages-ethereum.proto b/messages-ethereum.proto index 7a708b41..5c211509 100644 --- a/messages-ethereum.proto +++ b/messages-ethereum.proto @@ -376,3 +376,69 @@ message EthereumTypedDataValueRequest { message EthereumTypedDataValueAck { required bytes value = 1; } + +// ── ERC-7730 v2 compiled definitions ───────────────────────────────── +// +// JSON descriptors never cross the device boundary. The host resolves their +// includes, names, references, maps, groups and Solidity fragments into the +// canonical binary program documented in docs/erc7730-compiled-format.md. +// Firmware verifies the signed program and interprets values from the exact +// transaction/EIP-712 stream it signs. + +enum EthereumClearSignDefinitionKind { + ERC7730_CALLDATA = 1; + ERC7730_EIP712 = 2; + ERC7730_TOKEN = 3; + ERC7730_NETWORK = 4; +} + +/** + * Preload a signed definition before signing (offline/cache path). Chunks MUST + * be contiguous from offset zero. The first chunk supplies total_length; + * subsequent chunks bind to the same 32-byte definition_id (SHA-256 of the + * complete signed envelope). No definition becomes usable before the final + * chunk, signature, catalog proof and compiled program all verify. + * @next EthereumClearSignDefinitionAck + * @next Failure + */ +message EthereumClearSignDefinition { + required bytes definition_id = 1; + required uint32 offset = 2; + required uint32 total_length = 3; + required bytes data = 4; +} + +/** Response to a preload chunk. */ +message EthereumClearSignDefinitionAck { + required bytes definition_id = 1; + required uint32 next_offset = 2; + required bool complete = 3; +} + +/** + * Device request for a catalog definition during calldata or EIP-712 signing. + * The lookup tuple is authenticated again from the returned definition; these + * fields select an entry but never grant trust. selector_or_type_hash is four + * bytes for calldata, 32 bytes for EIP-712, and omitted for token/network + * lookups. + * @next EthereumClearSignDefinitionChunk + * @next Failure + */ +message EthereumClearSignDefinitionRequest { + required EthereumClearSignDefinitionKind kind = 1; + required uint64 chain_id = 2; + optional bytes contract_address = 3; + optional bytes selector_or_type_hash = 4; + optional bytes definition_id = 5; + required uint32 offset = 6; + required uint32 length = 7; + optional uint32 recursion_depth = 8; +} + +/** Host response to an on-demand definition request. */ +message EthereumClearSignDefinitionChunk { + required bytes definition_id = 1; + required uint32 offset = 2; + required uint32 total_length = 3; + required bytes data = 4; +} diff --git a/messages.proto b/messages.proto index 0936cebf..0af8d3c2 100644 --- a/messages.proto +++ b/messages.proto @@ -111,6 +111,10 @@ enum MessageType { MessageType_EthereumTypedDataStructAck = 1706 [ (wire_in) = true ]; MessageType_EthereumTypedDataValueRequest = 1707 [ (wire_out) = true ]; MessageType_EthereumTypedDataValueAck = 1708 [ (wire_in) = true ]; + MessageType_EthereumClearSignDefinition = 1709 [ (wire_in) = true ]; + MessageType_EthereumClearSignDefinitionAck = 1710 [ (wire_out) = true ]; + MessageType_EthereumClearSignDefinitionRequest = 1711 [ (wire_out) = true ]; + MessageType_EthereumClearSignDefinitionChunk = 1712 [ (wire_in) = true ]; // BIP-85 MessageType_GetBip85Mnemonic = 120 [ (wire_in) = true ];