What changed, and why it matters
This commit changes how Trezor handles Ethereum transaction data (calldata). Previously, clear signing only worked if the entire calldata fit in the first chunk sent by the host. Now the device actively requests and stores up to 4 KB of calldata so it can try clear signing on larger transactions. If clear signing succeeds, the user sees a human-readable confirmation instead of raw hex data. The change refactors the code to collect initial data before deciding whether to clear sign or fall back to blind signing. There is no direct evidence in the commit of a security vulnerability being fixed; it reads as a feature improvement.
Treat as a normal feature/refactor commit. Reviewers should verify that request_initial_data correctly bounds the collected data to MAX_DATA_STORED, that HashWriter state remains consistent across initial and subsequent chunks, and that falling back from clear signing to blind signing cannot be triggered by malformed calldata in a way that bypasses user confirmation. No immediate security response is indicated by the supplied materials.
Security signals we found
Behavioral change in transaction confirmation flow: device now requests up to 4 KB of calldata before deciding on clear vs blind signing
Clear signing previously limited to data_length <= len(data_initial_chunk); now supports multi-chunk calldata up to 4 KB
New exception base class ClearSigningFailed unifies InvalidFunctionCall and InvalidFormatDefinition handling
Removal of ParsingContext and its truncated flag; calldata truncation logic moved into request_initial_data
No explicit security bug, CVE, or advisory referenced in commit message or diff
Evidence from the diff
The patch refactors Ethereum signing in core/src/apps/ethereum/sign_tx.py and sign_tx_eip1559.py. A new helper request_initial_data() collects up to MAX_DATA_STORED (4096) bytes of transaction data by requesting additional chunks from the host if the initial chunk is smaller than the total data_length. The collected initial_data is then passed to confirm_tx_data(), which calls clear_signing.try_parse(). If try_parse succeeds it returns (None, None), meaning no further blind chunk confirmation or summary is needed. If it fails with ClearSigningFailed, the code falls back to the previous blind-signing path. The clear_signing module was simplified: ParsingContext was removed, DisplayFormat.parse now operates directly on a memoryview of the calldata (after the 4-byte selector), and the old MAX_CALLDATA_STORED limit was removed from clear_signing.py. Tests were updated to expect the new request/response sequence on non-legacy models.
Changed components
core/src/apps/ethereum/clear_signing.pycore/src/apps/ethereum/sign_tx.pycore/src/apps/ethereum/sign_tx_eip1559.pytests/device_tests/ethereum/test_signtx.pyInspect captured patch +210 / −275
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index 197e1a80..765e4330 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -2,13 +2,12 @@ from micropython import const
from typing import TYPE_CHECKING
from trezor import TR
-from trezor.utils import BufferReader
from .helpers import address_from_bytes, format_ethereum_amount, get_account_and_path
if TYPE_CHECKING:
from buffer_types import AnyBytes
- from typing import Any, Callable, Coroutine, Iterable
+ from typing import Callable, Iterable
from trezor.messages import EthereumTokenInfo
from trezor.ui.layouts import StrPropertyType
@@ -16,7 +15,6 @@ if TYPE_CHECKING:
from apps.common.payment_request import PaymentRequestVerifier
from .definitions import Definitions
- from .helpers import ConfirmDataFn
from .keychain import MsgInSignTx
# Represents values that have been parsed from the calldata
@@ -35,10 +33,13 @@ if TYPE_CHECKING:
SC_FUNC_SIG_BYTES = const(4)
-MAX_CALLDATA_STORED = const(4096)
-class InvalidFunctionCall(Exception):
+class ClearSigningFailed(Exception):
+ pass
+
+
+class InvalidFunctionCall(ClearSigningFailed):
"""Raised when the calldata encoding of a function call,
including its parameters, is invalid."""
@@ -62,7 +63,7 @@ class OutOfBounds(InvalidFunctionCall):
pass
-class InvalidFormatDefinition(Exception):
+class InvalidFormatDefinition(ClearSigningFailed):
"""Raised when we fail to format data according to the definitions,
if for example the parsed calldata has other types than what
the format definition expects."""
@@ -494,37 +495,9 @@ class DisplayFormat:
return self.binding_context.matches(chain_id, address)
-
-class ParsingContext:
- def __init__(self, display_format: DisplayFormat) -> None:
- self.data = bytes()
- self.display_format = display_format
- self.truncated = False
-
- def process_data_chunk(self, offset: int, chunk: memoryview) -> None:
- if offset == 0:
- # skip function signature
- chunk = chunk[SC_FUNC_SIG_BYTES:]
-
- if not chunk:
- # nothing to process after skipping function signature
- return
-
- current_len = len(self.data)
- if current_len >= MAX_CALLDATA_STORED:
- self.truncated = True
- # reached the storage limit. ignore further chunks.
- return
-
- remaining = MAX_CALLDATA_STORED - current_len
- if len(chunk) > remaining:
- self.truncated = True
- chunk = chunk[:remaining]
- if chunk:
- self.data += bytes(chunk)
-
- def get_parameters_and_fields(
+ def parse(
self,
+ calldata: memoryview,
address_n: list[int],
tx_value: AnyBytes,
definitions: Definitions,
@@ -533,17 +506,11 @@ class ParsingContext:
list[AnyValue],
list[tuple[StrPropertyType, EthereumTokenInfo | None, AnyBytes | None]],
]:
- if self.truncated:
- # this will not happen, because we already checked the data_length
- # in the very beginning and bailed from clear signing
- raise OutOfBounds
-
parameters: list[AnyValue] = []
- data = memoryview(self.data)
offset = 0
- for parameter_definition in self.display_format.parameter_definitions:
- value, consumed = parameter_definition.parse(data, offset)
+ for parameter_definition in self.parameter_definitions:
+ value, consumed = parameter_definition.parse(calldata, offset)
parameters.append(value)
offset += consumed
@@ -595,7 +562,7 @@ class ParsingContext:
fields: list[
tuple[StrPropertyType, EthereumTokenInfo | None, AnyBytes | None]
] = []
- for field_definition in self.display_format.field_definitions:
+ for field_definition in self.field_definitions:
(
formatted_value,
actual_token,
@@ -621,38 +588,28 @@ class ParsingContext:
return parameters, fields
-def get_approver(
+async def try_parse(
+ data: AnyBytes,
+ address_bytes: bytes,
msg: MsgInSignTx,
definitions: Definitions,
- address_bytes: bytes,
- value: int,
maximum_fee: str,
fee_items: Iterable[StrPropertyType],
payment_request_verifier: PaymentRequestVerifier | None,
-) -> tuple[ConfirmDataFn, Coroutine[Any, Any, None]] | None:
- from .clear_signing_definitions import ALL_DISPLAY_FORMATS
-
- # local_cache_attribute
- chain_id = msg.chain_id
+) -> bool:
+ from .clear_signing_definitions import (
+ ALL_DISPLAY_FORMATS,
+ APPROVE_DISPLAY_FORMAT,
+ TRANSFER_DISPLAY_FORMAT,
+ )
if not address_bytes:
- return None
-
- if msg.data_length > len(msg.data_initial_chunk):
- # we only support clear signing one chunk for now
- return None
-
- if msg.data_length > MAX_CALLDATA_STORED:
- # skip clear signing if the calldata is longer than what we can process
- return None
-
- data_reader = BufferReader(msg.data_initial_chunk)
- if data_reader.remaining_count() < SC_FUNC_SIG_BYTES:
- return None
+ return False
- token = definitions.get_token(address_bytes)
+ if len(data) < SC_FUNC_SIG_BYTES:
+ return False
- func_sig = data_reader.read_memoryview(SC_FUNC_SIG_BYTES)
+ func_sig = data[0:SC_FUNC_SIG_BYTES]
display_format = None
for f in ALL_DISPLAY_FORMATS:
@@ -660,58 +617,19 @@ def get_approver(
display_format = f
break
else:
- return None
-
- if not display_format.matches_context(chain_id, address_bytes):
- return None
- parser, parsing_context = _get_data_chunk_parser(display_format)
-
- return parser, _get_summary_handler(
- parsing_context,
- address_bytes,
- msg,
- definitions,
- token,
- maximum_fee,
- fee_items,
- payment_request_verifier,
- )
-
-
-def _get_data_chunk_parser(
- display_format: DisplayFormat,
-) -> tuple[ConfirmDataFn, ParsingContext]:
- offset = 0
- context = ParsingContext(display_format)
-
- async def confirm_fn(chunk: AnyBytes) -> None:
- nonlocal offset
- context.process_data_chunk(offset, memoryview(chunk))
- offset += len(chunk)
-
- return confirm_fn, context
+ return False
+ if not display_format.matches_context(msg.chain_id, address_bytes):
+ return False
-def _get_summary_handler(
- context: ParsingContext,
- address_bytes: bytes,
- msg: MsgInSignTx,
- definitions: Definitions,
- token: EthereumTokenInfo,
- maximum_fee: str,
- fee_items: Iterable[StrPropertyType],
- payment_request_verifier: PaymentRequestVerifier | None,
-) -> Coroutine[Any, Any, None]:
- from .clear_signing_definitions import (
- APPROVE_DISPLAY_FORMAT,
- TRANSFER_DISPLAY_FORMAT,
- )
+ calldata = memoryview(data)[SC_FUNC_SIG_BYTES:]
+ token = definitions.get_token(address_bytes)
# custom treatment of certain functions (APPROVE, TRANSFER)
-
- if context.display_format.func_sig == APPROVE_DISPLAY_FORMAT.func_sig:
- return _handle_approve(
- context,
+ if display_format.func_sig == APPROVE_DISPLAY_FORMAT.func_sig:
+ await _handle_approve(
+ calldata,
+ display_format,
address_bytes,
msg,
definitions,
@@ -719,9 +637,10 @@ def _get_summary_handler(
maximum_fee,
fee_items,
)
- elif context.display_format.func_sig == TRANSFER_DISPLAY_FORMAT.func_sig:
- return _handle_transfer(
- context,
+ elif display_format.func_sig == TRANSFER_DISPLAY_FORMAT.func_sig:
+ await _handle_transfer(
+ calldata,
+ display_format,
address_bytes,
msg,
definitions,
@@ -730,16 +649,22 @@ def _get_summary_handler(
fee_items,
payment_request_verifier,
)
-
- # generic UI for any function that has a `DisplayFormat`
-
- return _handle_generic_ui(
- context, msg, definitions, address_bytes, token, maximum_fee
- )
+ else:
+ # generic UI for any function that has a `DisplayFormat`
+ await _handle_generic_ui(
+ calldata,
+ display_format,
+ msg,
+ definitions,
+ token,
+ maximum_fee,
+ )
+ return True
async def _handle_approve(
- context: ParsingContext,
+ calldata: memoryview,
+ display_format: DisplayFormat,
address_bytes: bytes,
msg: MsgInSignTx,
definitions: Definitions,
@@ -753,8 +678,8 @@ async def _handle_approve(
)
from .layout import require_confirm_approve
- args, fields = context.get_parameters_and_fields(
- msg.address_n, msg.value, definitions, token
+ args, fields = display_format.parse(
+ calldata, msg.address_n, msg.value, definitions, token
)
assert len(args) == 2
@@ -792,7 +717,8 @@ async def _handle_approve(
async def _handle_transfer(
- context: ParsingContext,
+ calldata: memoryview,
+ display_format: DisplayFormat,
address_bytes: bytes,
msg: MsgInSignTx,
definitions: Definitions,
@@ -803,8 +729,8 @@ async def _handle_transfer(
) -> None:
from .layout import require_confirm_payment_request, require_confirm_tx
- args, fields = context.get_parameters_and_fields(
- msg.address_n, msg.value, definitions, token
+ args, fields = display_format.parse(
+ calldata, msg.address_n, msg.value, definitions, token
)
assert len(args) == 2
@@ -853,10 +779,10 @@ async def _handle_transfer(
async def _handle_generic_ui(
- context: ParsingContext,
+ calldata: memoryview,
+ display_format: DisplayFormat,
msg: MsgInSignTx,
definitions: Definitions,
- address_bytes: bytes,
token: EthereumTokenInfo,
maximum_fee: str,
) -> None:
@@ -865,8 +791,8 @@ async def _handle_generic_ui(
from .helpers import bytes_from_address
from .layout import require_confirm_clear_signing
- _, fields = context.get_parameters_and_fields(
- msg.address_n, msg.value, definitions, token
+ _, fields = display_format.parse(
+ calldata, msg.address_n, msg.value, definitions, token
)
properties_to_confirm = []
@@ -888,5 +814,5 @@ async def _handle_generic_ui(
recipient_str = KNOWN_ADDRESSES.get(bytes_from_address(msg.to), msg.to)
await require_confirm_clear_signing(
- recipient_str, context.display_format.intent, properties_to_confirm, maximum_fee
+ recipient_str, display_format.intent, properties_to_confirm, maximum_fee
)
diff --git a/core/src/apps/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py
index 819f1e2c..151cc76d 100644
--- a/core/src/apps/ethereum/sign_tx.py
+++ b/core/src/apps/ethereum/sign_tx.py
@@ -5,6 +5,7 @@ from ubinascii import unhexlify
from trezor import TR
from trezor.crypto import rlp
from trezor.messages import EthereumTxRequest
+from trezor.utils import HashWriter
from trezor.wire import DataError
from .helpers import (
@@ -16,6 +17,7 @@ from .helpers import (
from .keychain import with_keychain_from_chain_id
if TYPE_CHECKING:
+ from buffer_types import AnyBytes
from typing import Any, Coroutine, Iterable
from trezor.messages import EthereumSignTx, EthereumTxAck
@@ -61,11 +63,10 @@ async def sign_tx(
from apps.common import paths, safety_checks
- from .clear_signing import InvalidFormatDefinition, InvalidFunctionCall
from .helpers import format_ethereum_amount, get_fee_items_regular
# local_cache_attribute
- data_total = msg.data_length
+ data_length = msg.data_length
tx_type = msg.tx_type
network = defs.network
@@ -107,11 +108,8 @@ async def sign_tx(
amount_size_bytes=32,
)
- # digest
- total_length = _get_total_length(msg, data_total)
-
sha = HashWriter(sha3_256(keccak=True))
- rlp.write_header(sha, total_length, rlp.LIST_HEADER_BYTE)
+ rlp.write_header(sha, _get_digest_length(msg, data_length), rlp.LIST_HEADER_BYTE)
if tx_type is not None:
rlp.write(sha, tx_type)
@@ -119,7 +117,10 @@ async def sign_tx(
for field in (msg.nonce, msg.gas_price, msg.gas_limit, address_bytes, msg.value):
rlp.write(sha, field)
+ initial_data = await request_initial_data(msg, sha)
+
confirm_data_chunk, confirm_summary = await confirm_tx_data(
+ initial_data,
msg,
defs,
tx_type,
@@ -127,45 +128,25 @@ async def sign_tx(
maximum_fee,
fee_items,
payment_req_verifier,
- try_clear_signing=True,
)
- await confirm_data_chunk(msg.data_initial_chunk)
-
- data_left = data_total - len(msg.data_initial_chunk)
-
- summary_confirmed = False
- if data_left == 0: # Note: clear signing only works with the 1st chunk for now
- try:
- await confirm_summary
- summary_confirmed = True
- except (InvalidFunctionCall, InvalidFormatDefinition):
- # parsing calldata by the clear signer failed,
- # retry without clear signing
- confirm_data_chunk, confirm_summary = await confirm_tx_data(
- msg,
- defs,
- tx_type,
- address_bytes,
- maximum_fee,
- fee_items,
- payment_req_verifier,
- try_clear_signing=False,
- )
-
- # we can safely assume that the initial data chunk was not confirmed
- # because we are currently handling clear signer's exception
- # so let's finally confirm the initial data chunk!
- await confirm_data_chunk(msg.data_initial_chunk)
-
- rlp.write_header(sha, data_total, rlp.STRING_HEADER_BYTE, msg.data_initial_chunk)
- sha.extend(msg.data_initial_chunk)
-
- while data_left > 0:
- resp = await send_request_chunk(data_left)
- await confirm_data_chunk(resp.data_chunk)
- data_left -= len(resp.data_chunk)
- sha.extend(resp.data_chunk)
+ # `confirm_data_chunk` and `confirm_summary` can be `None`
+ # if we clear signed so there is nothing more to confirm
+
+ if confirm_data_chunk is not None:
+ await confirm_data_chunk(initial_data)
+
+ data_left = data_length - len(initial_data)
+ while data_left > 0:
+ resp = await send_request_chunk(data_left)
+ chunk = resp.data_chunk
+ await confirm_data_chunk(chunk)
+ data_left -= len(chunk)
+ sha.extend(chunk)
+
+ if confirm_summary is not None:
+ # blind signer's summary
+ await confirm_summary
# eip 155 replay protection
rlp.write(sha, msg.chain_id)
@@ -174,9 +155,6 @@ async def sign_tx(
digest = sha.get_digest()
- if not summary_confirmed:
- await confirm_summary
-
# transaction data confirmed, proceed with signing
result = _sign_digest(msg, keychain, digest)
@@ -184,7 +162,48 @@ async def sign_tx(
return result
+MAX_DATA_STORED = const(4096)
+DATA_CHUNK_SIZE = const(1024)
+
+
+async def request_initial_data(msg: MsgInSignTx, sha: HashWriter) -> AnyBytes:
+ """Request at most `MAX_DATA_STORED` which we keep locally"""
+
+ data_length = msg.data_length
+ if data_length > len(msg.data_initial_chunk):
+ # pre-allocate memory
+ initial_data = bytearray(min(data_length, MAX_DATA_STORED))
+
+ chunk = msg.data_initial_chunk
+ initial_data[0 : len(chunk)] = chunk
+ initial_data_length = len(chunk)
+ rlp.write_header(sha, data_length, rlp.STRING_HEADER_BYTE, chunk)
+ sha.extend(chunk)
+ data_left = data_length - initial_data_length
+ while (
+ data_left > 0 and initial_data_length + DATA_CHUNK_SIZE <= MAX_DATA_STORED
+ ):
+ resp = await send_request_chunk(data_left)
+ chunk = resp.data_chunk
+ initial_data[
+ initial_data_length : initial_data_length + len(resp.data_chunk)
+ ] = chunk
+ data_left -= len(chunk)
+ initial_data_length += len(chunk)
+ sha.extend(chunk)
+ else:
+ initial_data = msg.data_initial_chunk
+ initial_data_length = len(msg.data_initial_chunk)
+ rlp.write_header(
+ sha, data_length, rlp.STRING_HEADER_BYTE, msg.data_initial_chunk
+ )
+ sha.extend(msg.data_initial_chunk)
+
+ return initial_data
+
+
async def confirm_tx_data(
+ initial_data: AnyBytes,
msg: MsgInSignTx,
defs: Definitions,
tx_type: int | None,
@@ -192,9 +211,9 @@ async def confirm_tx_data(
maximum_fee: str,
fee_items: Iterable[StrPropertyType],
payment_request_verifier: PaymentRequestVerifier | None,
- try_clear_signing: bool,
-) -> tuple[ConfirmDataFn, Coroutine[Any, Any, None]]:
- """Returns data chunk callback and transaction summary layout to be awaited."""
+) -> tuple[ConfirmDataFn | None, Coroutine[Any, Any, None] | None]:
+ """Returns data chunk callback and transaction summary layout to be awaited.
+ [None, None] implies clear signing attempted and succeeded."""
from trezor.ui.layouts import confirm_value
@@ -226,18 +245,18 @@ async def confirm_tx_data(
value = int.from_bytes(msg.value, "big")
- if try_clear_signing:
- clear_signing_approver = clear_signing.get_approver(
+ try:
+ clear_signed = await clear_signing.try_parse(
+ initial_data,
+ address_bytes,
msg,
defs,
- address_bytes,
- value,
maximum_fee,
fee_items,
payment_request_verifier,
)
- if clear_signing_approver is not None:
- return clear_signing_approver
+ except clear_signing.ClearSigningFailed:
+ clear_signed = False
recipient_str = (
address_from_bytes(address_bytes, network) if address_bytes else None
@@ -263,16 +282,15 @@ async def confirm_tx_data(
None,
None,
)
- else:
+ elif not clear_signed:
if data_length > 0:
- # blind signing: we have data but `clear_signing` did not recognize the function
confirm_data_chunk = get_data_confirmer(data_length)
else:
confirm_data_chunk = get_progress_indicator(data_length)
-
token = (
None # what we want to confirm here is the ETH amount being sent on-chain
)
+
return confirm_data_chunk, require_confirm_tx(
recipient_str,
format_ethereum_amount(value, token, network),
@@ -284,9 +302,11 @@ async def confirm_tx_data(
is_send=(data_length == 0 and tx_type != EIP_7702_TX_TYPE),
chunkify=bool(msg.chunkify),
)
+ else:
+ return None, None
-def _get_total_length(msg: EthereumSignTx, data_total: int) -> int:
+def _get_digest_length(msg: EthereumSignTx, data_total: int) -> int:
length = 0
if msg.tx_type is not None:
length += rlp.length(msg.tx_type)
@@ -316,7 +336,7 @@ async def send_request_chunk(data_left: int) -> EthereumTxAck:
from trezor.wire.context import call
req = EthereumTxRequest()
- req.data_length = min(data_left, 1024)
+ req.data_length = min(data_left, DATA_CHUNK_SIZE)
return await call(req, EthereumTxAck)
diff --git a/core/src/apps/ethereum/sign_tx_eip1559.py b/core/src/apps/ethereum/sign_tx_eip1559.py
index e6485eb7..4769c427 100644
--- a/core/src/apps/ethereum/sign_tx_eip1559.py
+++ b/core/src/apps/ethereum/sign_tx_eip1559.py
@@ -43,12 +43,16 @@ async def sign_tx_eip1559(
from apps.common import paths
- from .clear_signing import InvalidFormatDefinition, InvalidFunctionCall
from .helpers import format_ethereum_amount, get_fee_items_eip1559
- from .sign_tx import check_common_fields, confirm_tx_data, send_request_chunk
+ from .sign_tx import (
+ check_common_fields,
+ confirm_tx_data,
+ request_initial_data,
+ send_request_chunk,
+ )
gas_limit = msg.gas_limit # local_cache_attribute
- data_total = msg.data_length # local_cache_attribute
+ data_length = msg.data_length # local_cache_attribute
# check
if len(msg.max_gas_fee) + len(gas_limit) > 30:
@@ -81,54 +85,10 @@ async def sign_tx_eip1559(
msg.payment_req, slip44_id, keychain, amount_size_bytes=32
)
- # digest
- total_length = _get_total_length(msg, data_total)
-
sha = HashWriter(sha3_256(keccak=True))
rlp.write(sha, _TX_TYPE)
- rlp.write_header(sha, total_length, rlp.LIST_HEADER_BYTE)
-
- # data chunks will be confirmed during digest (see below)
- # tx summary will approved before signing the digest (see below)
- confirm_data_chunk, confirm_summary = await confirm_tx_data(
- msg,
- defs,
- None,
- address_bytes,
- maximum_fee,
- fee_items,
- payment_req_verifier,
- try_clear_signing=True,
- )
-
- await confirm_data_chunk(msg.data_initial_chunk)
-
- data_left = data_total - len(msg.data_initial_chunk)
-
- summary_confirmed = False
- if data_left == 0:
- try:
- await confirm_summary
- summary_confirmed = True
- except (InvalidFunctionCall, InvalidFormatDefinition):
- # parsing calldata by the clear signer failed,
- # retry without clear signing
- confirm_data_chunk, confirm_summary = await confirm_tx_data(
- msg,
- defs,
- None,
- address_bytes,
- maximum_fee,
- fee_items,
- payment_req_verifier,
- try_clear_signing=False,
- )
-
- # we can safely assume that the initial data chunk was not confirmed
- # because we are currently handling clear signer's exception
- # so let's finally confirm the initial data chunk!
- await confirm_data_chunk(msg.data_initial_chunk)
+ rlp.write_header(sha, _get_digest_length(msg, data_length), rlp.LIST_HEADER_BYTE)
fields: tuple[rlp.RLPItem, ...] = (
msg.chain_id,
@@ -142,14 +102,33 @@ async def sign_tx_eip1559(
for field in fields:
rlp.write(sha, field)
- rlp.write_header(sha, data_total, rlp.STRING_HEADER_BYTE, msg.data_initial_chunk)
- sha.extend(msg.data_initial_chunk)
+ initial_data = await request_initial_data(msg, sha)
+
+ confirm_data_chunk, confirm_summary = await confirm_tx_data(
+ initial_data,
+ msg,
+ defs,
+ None,
+ address_bytes,
+ maximum_fee,
+ fee_items,
+ payment_req_verifier,
+ )
- while data_left > 0:
- resp = await send_request_chunk(data_left)
- await confirm_data_chunk(resp.data_chunk)
- data_left -= len(resp.data_chunk)
- sha.extend(resp.data_chunk)
+ if confirm_data_chunk is not None:
+ await confirm_data_chunk(initial_data)
+
+ data_left = data_length - len(initial_data)
+ while data_left > 0:
+ resp = await send_request_chunk(data_left)
+ chunk = resp.data_chunk
+ await confirm_data_chunk(chunk)
+ data_left -= len(chunk)
+ sha.extend(chunk)
+
+ if confirm_summary is not None:
+ # blind signer's summary
+ await confirm_summary
# write_access_list
payload_length = sum(access_list_item_length(i) for i in msg.access_list)
@@ -164,9 +143,6 @@ async def sign_tx_eip1559(
digest = sha.get_digest()
- if not summary_confirmed:
- await confirm_summary
-
# transaction data confirmed, proceed with signing
result = _sign_digest(msg, keychain, digest)
@@ -174,7 +150,7 @@ async def sign_tx_eip1559(
return result
-def _get_total_length(msg: EthereumSignTxEIP1559, data_total: int) -> int:
+def _get_digest_length(msg: EthereumSignTxEIP1559, data_length: int) -> int:
length = 0
fields: tuple[rlp.RLPItem, ...] = (
@@ -189,8 +165,8 @@ def _get_total_length(msg: EthereumSignTxEIP1559, data_total: int) -> int:
for field in fields:
length += rlp.length(field)
- length += rlp.header_length(data_total, msg.data_initial_chunk)
- length += data_total
+ length += rlp.header_length(data_length, msg.data_initial_chunk)
+ length += data_length
# access_list_length
payload_length = sum(access_list_item_length(i) for i in msg.access_list)
diff --git a/tests/device_tests/ethereum/test_signtx.py b/tests/device_tests/ethereum/test_signtx.py
index 501664f2..fe70f62d 100644
--- a/tests/device_tests/ethereum/test_signtx.py
+++ b/tests/device_tests/ethereum/test_signtx.py
@@ -288,30 +288,43 @@ def test_data_streaming(session: Session):
client.set_input_flow(flow.get())
is_legacy = client.model in models.LEGACY_MODELS
- br_sign_tx = messages.ButtonRequest(code=messages.ButtonRequestType.SignTx)
+ def br_sign_tx(n):
+ return messages.ButtonRequest(
+ code=messages.ButtonRequestType.SignTx, name=n
+ )
+
br_protect = messages.ButtonRequest(code=messages.ButtonRequestType.ProtectCall)
- expected_responses: list[ExpectedResponse] = [br_sign_tx]
+ def tx_request(l):
+ return message_filters.EthereumTxRequest(
+ data_length=l, signature_r=None, signature_s=None, signature_v=None
+ )
+
if is_legacy:
- expected_responses += [br_sign_tx] * LEGACY_MAX_DATA_PAGES + [
+ expected_responses: list[ExpectedResponse] = []
+ expected_responses += [br_sign_tx(None)]
+ expected_responses += [br_sign_tx(None)] * LEGACY_MAX_DATA_PAGES
+ expected_responses += [
br_protect,
- br_sign_tx,
+ br_sign_tx(None),
]
+ expected_responses += [tx_request(l) for l in (1024, 1024, 1024, 3)]
+ expected_responses += [message_filters.EthereumTxRequest(data_length=None)]
+ else:
+ expected_responses: list[ExpectedResponse] = []
+ expected_responses += [tx_request(l) for l in (1024, 1024, 1024)]
+ expected_responses += [br_sign_tx("confirm_data")]
+ expected_responses += [tx_request(3)]
+ if client.model is models.T3T1 or client.model is models.T3W1:
+ # related issue: https://github.com/trezor/trezor-firmware/issues/6490
+ # TODO: make these consistent!
+ expected_responses += [br_sign_tx("confirm_output")]
+ expected_responses += [br_sign_tx("confirm_total")]
+ else:
+ expected_responses += [br_sign_tx("confirm_ethereum_tx")]
+ expected_responses += [br_sign_tx("confirm_ethereum_tx")]
+ expected_responses += [message_filters.EthereumTxRequest(data_length=None)]
- expected_responses.extend(
- message_filters.EthereumTxRequest(
- data_length=data_length,
- signature_r=None,
- signature_s=None,
- signature_v=None,
- )
- for data_length in (1_024, 1_024, 1_024, 3)
- )
-
- if not is_legacy:
- expected_responses += [br_sign_tx, br_sign_tx]
-
- expected_responses += [message_filters.EthereumTxRequest(data_length=None)]
client.set_expected_responses(expected_responses)
ethereum.sign_tx(
Why this scored 35/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.