feat(clear_signing): add date and raw formatters. - core tests added. - proto updated. [no changelog]
What changed, and why it matters
This commit adds two new ways to display Ethereum transaction details on a Trezor screen: a 'raw' formatter that shows values as-is (numbers, text, or hex bytes) and a 'date' formatter that turns a Unix timestamp into a readable date. It also wraps some existing parsing and display steps in try/except blocks so failures are logged and re-raised. There is no direct evidence this fixes an active security bug; it looks like a feature addition for clearer transaction signing.
No immediate action required. Treat as routine feature commit. If auditing, verify that RawFormatter's handling of untrusted strings/bytes cannot produce misleading display output and that DateFormatter's timestamp range is acceptable for the device UI.
Security signals we found
New formatter code handles bytes/string/int conversions; potential for unexpected input types
DateFormatter only accepts int; other types raise InvalidFormatDefinition
RawFormatter raises InvalidFormatDefinition on unsupported types
Exceptions during calldata parsing and field display are now logged and re-raised
No input sanitization changes beyond type checks in formatters
Evidence from the diff
The change extends the ERC-7730 clear-signing formatter enum with FORMATTER_RAW and FORMATTER_DATE, implements RawFormatter and DateFormatter classes in core/src/apps/ethereum/clear_signing.py, and propagates the enum through generated Python, Rust, and proto bindings. RawFormatter returns strings unchanged, hex-encodes bytes/bytearray, and stringifies ints (including bool subclasses). DateFormatter calls format_timestamp on integer seconds. The commit also adds exception logging around parameter parsing and field formatting, but exceptions are still re-raised, so behavior is unchanged except for debug logging.
Changed components
core/src/apps/ethereum/clear_signing.pycommon/protob/messages-definitions.protocore/src/trezor/enums/EthereumERC7730FieldFormatterType.pycore/src/trezor/enums/__init__.pypython/src/trezorlib/messages.pyrust/trezor-client/src/protos/generated/messages_definitions.rscore/tests/test_apps.ethereum.clear_signing.pyInspect captured patch +190 / −12
diff --git a/common/protob/messages-definitions.proto b/common/protob/messages-definitions.proto
index c3f0396f..2359c77b 100644
--- a/common/protob/messages-definitions.proto
+++ b/common/protob/messages-definitions.proto
@@ -132,6 +132,8 @@ enum EthereumERC7730FieldFormatterType {
FORMATTER_AMOUNT = 1;
FORMATTER_TOKEN_AMOUNT = 2;
FORMATTER_UNIT = 3;
+ FORMATTER_RAW = 4;
+ FORMATTER_DATE = 5;
}
/**
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index 547ac454..1ca60eb3 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -399,6 +399,58 @@ class UnitFormatter(FieldFormatter):
return f"{significand:g}{prefix_symbol}{self.base}", None, None
+class RawFormatter(FieldFormatter):
+ """Lazy placeholder. Simply adds label to the value and show it essentially as-is.
+ ERC-7730 `raw` format: display the decoded value with no transformation,
+ rendering by its Solidity type per the spec:
+ * int -> decimal string (natural representation)
+ * string -> the UTF-8 string as-is
+ * bytes -> hex-encoded string
+ """
+
+ async def format(
+ self,
+ value: AnyValue,
+ _msg: MsgInSignTx,
+ _definitions: Definitions,
+ _path_walker: PathWalker,
+ ) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
+ if value is None:
+ return None, None, None
+ elif isinstance(value, str):
+ return value, None, None
+ elif isinstance(value, (bytes, bytearray)):
+ from ubinascii import hexlify
+
+ return hexlify(value).decode(), None, None
+ elif isinstance(value, int):
+ # bool is an int subclass; rendered as "True"/"False".
+ return str(value), None, None
+ else:
+ raise InvalidFormatDefinition
+
+
+class DateFormatter(FieldFormatter):
+ """ERC-7730 `date` format with `encoding: timestamp` (the only encoding used
+ by the supported definitions). Renders a unix timestamp (seconds) as a
+ human-readable date."""
+
+ async def format(
+ self,
+ value: AnyValue,
+ _msg: MsgInSignTx,
+ _definitions: Definitions,
+ _path_walker: PathWalker,
+ ) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
+ from trezor.strings import format_timestamp
+
+ if value is None:
+ return None, None, None
+ if isinstance(value, int):
+ return format_timestamp(value), None, None
+ raise InvalidFormatDefinition
+
+
# https://eips.ethereum.org/EIPS/eip-7730#context-section
@@ -646,6 +698,10 @@ class FieldDefinition:
if info.prefix is not None:
formatter_params["prefix"] = info.prefix
formatter = UnitFormatter(**formatter_params)
+ elif fmt_type == FT.FORMATTER_RAW:
+ formatter = RawFormatter
+ elif fmt_type == FT.FORMATTER_DATE:
+ formatter = DateFormatter
else:
raise InvalidFormatDefinition
@@ -703,7 +759,18 @@ class DisplayFormat:
offset = 0
for parameter_definition in self.parameter_definitions:
- value, consumed = parameter_definition.parse(calldata, offset)
+ try:
+ value, consumed = parameter_definition.parse(calldata, offset)
+ except Exception as e:
+ if __debug__:
+ from trezor import log
+
+ log.debug(
+ __name__,
+ "clear signing: failed to parse calldata parameters (%s)",
+ type(e).__name__,
+ )
+ raise
parameters.append(value)
offset += consumed
@@ -762,12 +829,23 @@ class DisplayFormat:
]
] = []
for field_definition in self.field_definitions:
- value = get_value_for_path(field_definition.path)
- formatter = field_definition.get_formatter()
-
- formatted, token, token_address = await formatter.format(
- value, msg, defs, get_value_for_path
- )
+ try:
+ value = get_value_for_path(field_definition.path)
+ formatter = field_definition.get_formatter()
+ formatted, token, token_address = await formatter.format(
+ value, msg, defs, get_value_for_path
+ )
+ except Exception as e:
+ if __debug__:
+ from trezor import log
+
+ log.debug(
+ __name__,
+ 'clear signing: failed to display field "%s" (%s)',
+ field_definition.label,
+ type(e).__name__,
+ )
+ raise
fields.append(
(
diff --git a/core/src/trezor/enums/EthereumERC7730FieldFormatterType.py b/core/src/trezor/enums/EthereumERC7730FieldFormatterType.py
index 355f8854..39107afa 100644
--- a/core/src/trezor/enums/EthereumERC7730FieldFormatterType.py
+++ b/core/src/trezor/enums/EthereumERC7730FieldFormatterType.py
@@ -6,3 +6,5 @@ FORMATTER_ADDRESS_NAME = 0
FORMATTER_AMOUNT = 1
FORMATTER_TOKEN_AMOUNT = 2
FORMATTER_UNIT = 3
+FORMATTER_RAW = 4
+FORMATTER_DATE = 5
diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py
index 15c2333f..0858b779 100644
--- a/core/src/trezor/enums/__init__.py
+++ b/core/src/trezor/enums/__init__.py
@@ -327,6 +327,8 @@ if TYPE_CHECKING:
FORMATTER_AMOUNT = 1
FORMATTER_TOKEN_AMOUNT = 2
FORMATTER_UNIT = 3
+ FORMATTER_RAW = 4
+ FORMATTER_DATE = 5
class EthereumERC7730ContainerPath(IntEnum):
FROM = 1
diff --git a/core/tests/test_apps.ethereum.clear_signing.py b/core/tests/test_apps.ethereum.clear_signing.py
index d7de03f3..3c87e381 100644
--- a/core/tests/test_apps.ethereum.clear_signing.py
+++ b/core/tests/test_apps.ethereum.clear_signing.py
@@ -6,12 +6,18 @@ import unittest
if not utils.BITCOIN_ONLY:
from ethereum_common import *
+ from trezor.enums import EthereumERC7730FieldFormatterType as FT
+ from trezor.messages import EthereumERC7730FieldInfo, EthereumERC7730Path
from apps.ethereum.clear_signing import (
Array,
Atomic,
+ DateFormatter,
DirtyAddress,
+ FieldDefinition,
+ InvalidFormatDefinition,
OutOfBounds,
+ RawFormatter,
Tuple,
ValueOverflow,
parse_address,
@@ -395,6 +401,81 @@ class TestEthereumClearSigning(unittest.TestCase):
with self.assertRaises(OutOfBounds):
nested_array_parser.parse(memoryview(payload_fail), 0)
+ # --- Field formatters ---
+
+ def test_raw_formatter(self):
+ fmt = RawFormatter()
+
+ # int -> decimal, including a full-width uint256 (no float rounding / sci-notation)
+ big = 2**256 - 1
+ for value, expected in (
+ (0, "0"),
+ (291, "291"),
+ (big, str(big)),
+ ):
+ formatted, token, addr = await_result(fmt.format(value, None, None, None))
+ self.assertEqual(formatted, expected)
+ self.assertIsNone(token)
+ self.assertIsNone(addr)
+
+ # string -> passed through unchanged
+ formatted, _, _ = await_result(fmt.format("Trezor", None, None, None))
+ self.assertEqual(formatted, "Trezor")
+
+ # bytes -> hex-encoded string
+ formatted, _, _ = await_result(
+ fmt.format(b"\x12\x34\x56\x78\x9a", None, None, None)
+ )
+ self.assertEqual(formatted, "123456789a")
+
+ # None -> None
+ formatted, _, _ = await_result(fmt.format(None, None, None, None))
+ self.assertIsNone(formatted)
+
+ def test_date_formatter(self):
+ fmt = DateFormatter()
+
+ # unix timestamp (seconds) -> human-readable date
+ formatted, token, addr = await_result(fmt.format(1616051824, None, None, None))
+ self.assertEqual(formatted, "2021-03-18 07:17:04")
+ self.assertIsNone(token)
+ self.assertIsNone(addr)
+
+ formatted, _, _ = await_result(fmt.format(0, None, None, None))
+ self.assertEqual(formatted, "1970-01-01 00:00:00")
+
+ # None -> None
+ formatted, _, _ = await_result(fmt.format(None, None, None, None))
+ self.assertIsNone(formatted)
+
+ # non-int value is rejected
+ with self.assertRaises(InvalidFormatDefinition):
+ await_result(fmt.format("not-a-timestamp", None, None, None))
+
+ def test_from_proto_raw_date_dispatch(self):
+ # End-to-end from a proto enum value to a rendered string. `from_proto`
+ # maps the wire integer (FORMATTER_RAW=4 / FORMATTER_DATE=5) to a
+ # formatter class.
+ raw_info = EthereumERC7730FieldInfo(
+ path=EthereumERC7730Path(path=[0]),
+ label="Field",
+ formatter=FT.FORMATTER_RAW,
+ )
+ raw_fmt = FieldDefinition.from_proto(raw_info).get_formatter()
+ self.assertIsInstance(raw_fmt, RawFormatter)
+ formatted, _, _ = await_result(raw_fmt.format(42, None, None, None))
+ self.assertEqual(formatted, "42")
+
+ date_info = EthereumERC7730FieldInfo(
+ path=EthereumERC7730Path(path=[0]),
+ label="Field",
+ formatter=FT.FORMATTER_DATE,
+ )
+ date_fmt = FieldDefinition.from_proto(date_info).get_formatter()
+ self.assertIsInstance(date_fmt, DateFormatter)
+ formatted, _, _ = await_result(date_fmt.format(1616051824, None, None, None))
+ self.assertEqual(formatted, "2021-03-18 07:17:04")
+
if __name__ == "__main__":
unittest.main()
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index 03529f22..ad6daf63 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -370,6 +370,8 @@ class EthereumERC7730FieldFormatterType(IntEnum):
FORMATTER_AMOUNT = 1
FORMATTER_TOKEN_AMOUNT = 2
FORMATTER_UNIT = 3
+ FORMATTER_RAW = 4
+ FORMATTER_DATE = 5
class EthereumERC7730ContainerPath(IntEnum):
diff --git a/rust/trezor-client/src/protos/generated/messages_definitions.rs b/rust/trezor-client/src/protos/generated/messages_definitions.rs
index 77037655..b444b51c 100644
--- a/rust/trezor-client/src/protos/generated/messages_definitions.rs
+++ b/rust/trezor-client/src/protos/generated/messages_definitions.rs
@@ -2591,6 +2591,10 @@ pub enum EthereumERC7730FieldFormatterType {
FORMATTER_TOKEN_AMOUNT = 2,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.definitions.EthereumERC7730FieldFormatterType.FORMATTER_UNIT)
FORMATTER_UNIT = 3,
+ // @@protoc_insertion_point(enum_value:hw.trezor.messages.definitions.EthereumERC7730FieldFormatterType.FORMATTER_RAW)
+ FORMATTER_RAW = 4,
+ // @@protoc_insertion_point(enum_value:hw.trezor.messages.definitions.EthereumERC7730FieldFormatterType.FORMATTER_DATE)
+ FORMATTER_DATE = 5,
}
impl ::protobuf::Enum for EthereumERC7730FieldFormatterType {
@@ -2606,6 +2610,8 @@ impl ::protobuf::Enum for EthereumERC7730FieldFormatterType {
1 => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_AMOUNT),
2 => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_TOKEN_AMOUNT),
3 => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_UNIT),
+ 4 => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_RAW),
+ 5 => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_DATE),
_ => ::std::option::Option::None
}
}
@@ -2616,6 +2622,8 @@ impl ::protobuf::Enum for EthereumERC7730FieldFormatterType {
"FORMATTER_AMOUNT" => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_AMOUNT),
"FORMATTER_TOKEN_AMOUNT" => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_TOKEN_AMOUNT),
"FORMATTER_UNIT" => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_UNIT),
+ "FORMATTER_RAW" => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_RAW),
+ "FORMATTER_DATE" => ::std::option::Option::Some(EthereumERC7730FieldFormatterType::FORMATTER_DATE),
_ => ::std::option::Option::None
}
}
@@ -2625,6 +2633,8 @@ impl ::protobuf::Enum for EthereumERC7730FieldFormatterType {
EthereumERC7730FieldFormatterType::FORMATTER_AMOUNT,
EthereumERC7730FieldFormatterType::FORMATTER_TOKEN_AMOUNT,
EthereumERC7730FieldFormatterType::FORMATTER_UNIT,
+ EthereumERC7730FieldFormatterType::FORMATTER_RAW,
+ EthereumERC7730FieldFormatterType::FORMATTER_DATE,
];
}
@@ -2776,12 +2786,13 @@ static file_descriptor_proto_data: &'static [u8] = b"\
\x12\x0c\n\x08ABI_BOOL\x10\x10\x12\x0f\n\x0bABI_BYTES32\x10\x14\x12\x0f\
\n\x0bABI_BYTES16\x10\x15\x12\x0e\n\nABI_BYTES8\x10\x16\x12\x0e\n\nABI_B\
YTES4\x10\x17\x12\r\n\tABI_BYTES\x10\x1e\x12\x0e\n\nABI_STRING\x10\x1f*\
- \x85\x01\n!EthereumERC7730FieldFormatterType\x12\x1a\n\x16FORMATTER_ADDR\
+ \xac\x01\n!EthereumERC7730FieldFormatterType\x12\x1a\n\x16FORMATTER_ADDR\
ESS_NAME\x10\0\x12\x14\n\x10FORMATTER_AMOUNT\x10\x01\x12\x1a\n\x16FORMAT\
- TER_TOKEN_AMOUNT\x10\x02\x12\x12\n\x0eFORMATTER_UNIT\x10\x03*;\n\x1cEthe\
- reumERC7730ContainerPath\x12\x08\n\x04FROM\x10\x01\x12\t\n\x05VALUE\x10\
- \x02\x12\x06\n\x02TO\x10\x03B?\n#com.satoshilabs.trezor.lib.protobufB\
- \x18TrezorMessageDefinitions\
+ TER_TOKEN_AMOUNT\x10\x02\x12\x12\n\x0eFORMATTER_UNIT\x10\x03\x12\x11\n\r\
+ FORMATTER_RAW\x10\x04\x12\x12\n\x0eFORMATTER_DATE\x10\x05*;\n\x1cEthereu\
+ mERC7730ContainerPath\x12\x08\n\x04FROM\x10\x01\x12\t\n\x05VALUE\x10\x02\
+ \x12\x06\n\x02TO\x10\x03B?\n#com.satoshilabs.trezor.lib.protobufB\x18Tre\
+ zorMessageDefinitions\
";
/// `FileDescriptorProto` object which was a source for this generated file
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.