What changed, and why it matters
This commit adds a new feature to the Trezor Ethereum clear-signing system: a formatter that converts a duration in seconds into a human-readable HH:MM:ss display. It is a straightforward feature addition with no apparent security relevance.
No security action required. This is a benign feature addition.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces DurationFormatter in core/src/apps/ethereum/clear_signing.py to support the ERC-7730 FORMATTER_DURATION type. It handles None, int, and bytes (big-endian sliced word) inputs, rejects negative durations and non-numeric values, and returns a formatted string. The corresponding test file adds unit tests covering normal, edge, and error cases. No existing behavior is changed.
Changed components
core/src/apps/ethereum/clear_signing.pycore/tests/test_apps.ethereum.clear_signing.pyInspect captured patch +64 / −0
### core/src/apps/ethereum/clear_signing.py
@@ -505,6 +505,31 @@ async def format(
raise InvalidFormatDefinition
+class DurationFormatter(FieldFormatter):
+ """Duration in seconds. Formatted as HH:MM:ss"""
+
+ 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
+ if isinstance(value, bytes):
+ # a sliced word, e.g. `deadline.[-4:]`: big-endian seconds
+ value = int.from_bytes(value, "big")
+ if isinstance(value, int):
+ if value < 0:
+ # a negative duration has no sensible rendering
+ raise InvalidFormatDefinition
+ minutes, seconds = divmod(value, 60)
+ hours, minutes = divmod(minutes, 60)
+ return f"{hours:02d}:{minutes:02d}:{seconds:02d}", None, None
+ raise InvalidFormatDefinition
+
+
class CalldataFormatter(RawFormatter):
"""ERC-7730 `calldata` format: the field's value is the embedded calldata
of a nested call, rendered with the display format of the called contract
@@ -863,6 +888,8 @@ def decode_path(p: EthereumERC7730Path) -> Path:
formatter = RawFormatter
elif fmt_type == FT.FORMATTER_DATE:
formatter = DateFormatter
+ elif fmt_type == FT.FORMATTER_DURATION:
+ formatter = DurationFormatter
elif fmt_type == FT.FORMATTER_CALLDATA:
if info.callee_path is None:
raise InvalidFormatDefinition
### core/tests/test_apps.ethereum.clear_signing.py
@@ -27,6 +27,7 @@
DateFormatter,
DirtyAddress,
DisplayFormat,
+ DurationFormatter,
DynamicLeaf,
EnumFormatter,
FieldDefinition,
@@ -712,6 +713,42 @@ def test_date_formatter(self):
with self.assertRaises(InvalidFormatDefinition):
await_result(fmt.format("not-a-timestamp", None, None, None))
+ def test_duration_formatter(self):
+ fmt = DurationFormatter()
+
+ # seconds -> HH:MM:ss (example from the ERC-7730 spec)
+ formatted, token, addr = await_result(fmt.format(8250, None, None, None))
+ self.assertEqual(formatted, "02:17:30")
+ self.assertIsNone(token)
+ self.assertIsNone(addr)
+
+ formatted, _, _ = await_result(fmt.format(0, None, None, None))
+ self.assertEqual(formatted, "00:00:00")
+
+ # durations longer than a day keep counting hours
+ formatted, _, _ = await_result(
+ fmt.format(100 * 3600 + 59 * 60 + 59, None, None, None)
+ )
+ self.assertEqual(formatted, "100:59:59")
+
+ # None -> None
+ formatted, _, _ = await_result(fmt.format(None, None, None, None))
+ self.assertIsNone(formatted)
+
+ # a sliced word, e.g. `deadline.[-4:]`: big-endian seconds
+ formatted, _, _ = await_result(
+ fmt.format((8250).to_bytes(4, "big"), None, None, None)
+ )
+ self.assertEqual(formatted, "02:17:30")
+
+ # a negative duration is rejected
+ with self.assertRaises(InvalidFormatDefinition):
+ await_result(fmt.format(-1, None, None, None))
+
+ # a non-numeric value is rejected
+ with self.assertRaises(InvalidFormatDefinition):
+ await_result(fmt.format("not-a-duration", 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 aWhy this scored 15/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.