Skip to content

Commit 3ca9525

Browse files
committed
feat(core): reimplement THP retransmissions
[no changelog]
1 parent 5bb49f1 commit 3ca9525

File tree

2 files changed

+35
-21
lines changed

2 files changed

+35
-21
lines changed

core/src/trezor/wire/thp/channel.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ustruct
2+
from micropython import const
23
from typing import TYPE_CHECKING
34

45
from storage.cache_common import (
@@ -20,6 +21,7 @@
2021
is_there_a_channel_to_replace,
2122
)
2223
from trezor import protobuf, utils, workflow
24+
from trezor.loop import Timeout
2325

2426
from ..protocol_common import Message
2527
from . import (
@@ -53,6 +55,10 @@
5355
from .session_context import GenericSessionContext
5456

5557

58+
_MAX_RETRANSMISSION_COUNT = const(50)
59+
_MIN_RETRANSMISSION_COUNT = const(2)
60+
61+
5662
class Reassembler:
5763
def __init__(self, cid: int, read_buf: ThpBuffer) -> None:
5864
self.cid = cid
@@ -209,7 +215,9 @@ def is_channel_to_replace(self) -> bool:
209215
# READ and DECRYPT
210216

211217
async def recv_payload(
212-
self, expected_ctrl_byte: Callable[[int], bool] | None
218+
self,
219+
expected_ctrl_byte: Callable[[int], bool] | None,
220+
timeout_ms: int | None = None,
213221
) -> memoryview:
214222
"""
215223
Receive and return a valid THP payload from this channel & its control byte.
@@ -219,10 +227,11 @@ async def recv_payload(
219227
220228
If `expected_ctrl_byte` is `None`, returns after the first received ACK.
221229
"""
230+
222231
while True:
223232
# Handle an existing message (if already reassembled).
224233
# Otherwise, receive and reassemble a new one.
225-
msg = await self._get_reassembled_message()
234+
msg = await self._get_reassembled_message(timeout_ms=timeout_ms)
226235

227236
# Synchronization process
228237
ctrl_byte = msg[0]
@@ -237,6 +246,8 @@ async def recv_payload(
237246
continue
238247

239248
if expected_ctrl_byte is None or not expected_ctrl_byte(ctrl_byte):
249+
if __debug__:
250+
self._log("Unexpected control byte", utils.hexlify_if_bytes(msg))
240251
raise ThpError("Unexpected control byte")
241252

242253
# 2: Handle message with unexpected sequential bit
@@ -255,11 +266,13 @@ async def recv_payload(
255266

256267
return payload
257268

258-
async def _get_reassembled_message(self) -> memoryview:
269+
async def _get_reassembled_message(
270+
self, timeout_ms: int | None = None
271+
) -> memoryview:
259272
"""Doesn't block if a message has been already reassembled."""
260273
while self.reassembler.message is None:
261274
# receive and reassemble a new message from this channel
262-
channel = await self.ctx.get_next_message()
275+
channel = await self.ctx.get_next_message(timeout_ms=timeout_ms)
263276
if channel is self:
264277
break
265278

@@ -377,23 +390,23 @@ async def write_encrypted_payload(self, ctrl_byte: int, payload: bytes) -> None:
377390
# ACK is needed before sending more data
378391
ABP.set_sending_allowed(self.channel_cache, False)
379392

380-
# TODO implement retransmissions:
381-
# sender = loop.spawn(self._retransmit(header, payload)) # will raise on timeout
382-
# receiver = loop.spawn(self._wait_for_ack()) # will return on success
383-
# await loop.race(sender, receiver)
384-
await self.ctx.write_payload(header, payload)
385-
await self._wait_for_ack()
386-
387-
# `ABP.set_sending_allowed()` will be called after a valid ACK
388-
assert ABP.is_sending_allowed(self.channel_cache)
389-
390-
ABP.set_send_seq_bit_to_opposite(self.channel_cache)
393+
for i in range(_MAX_RETRANSMISSION_COUNT):
394+
await self.ctx.write_payload(header, payload)
395+
# starting from 100ms till ~3.42s
396+
timeout_ms = round(10200 - 1010000 / (100 + i))
397+
try:
398+
# wait and return after receiving an ACK, or raise in case of an unexpected message.
399+
await self.recv_payload(expected_ctrl_byte=None, timeout_ms=timeout_ms)
400+
except Timeout:
401+
if __debug__:
402+
log.warning(__name__, "Retransmit after %d ms", timeout_ms)
403+
continue
404+
# `ABP.set_sending_allowed()` will be called after a valid ACK
405+
if ABP.is_sending_allowed(self.channel_cache):
406+
ABP.set_send_seq_bit_to_opposite(self.channel_cache)
407+
return
391408

392-
async def _wait_for_ack(self) -> None:
393-
# `ABP.set_sending_allowed()` will be called after a valid ACK
394-
while not ABP.is_sending_allowed(self.channel_cache):
395-
# wait and return after receiving an ACK, or raise in case of an unexpected message.
396-
await self.recv_payload(expected_ctrl_byte=None)
409+
raise ThpError("Retransmission timeout")
397410

398411
def _encrypt(self, buffer: utils.BufferType, noise_payload_len: int) -> None:
399412
if __debug__:

core/src/trezor/wire/thp/interface_context.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def __init__(self, iface: WireInterface) -> None:
4747
self._write = loop.wait(iface.iface_num() | io.POLL_WRITE)
4848
self._channels: dict[int, Channel] = {}
4949

50-
async def get_next_message(self) -> Channel:
50+
async def get_next_message(self, timeout_ms: int | None = None) -> Channel:
5151
"""
5252
Reassemble a valid THP payload and return its channel.
5353
@@ -57,6 +57,7 @@ async def get_next_message(self) -> Channel:
5757

5858
packet = bytearray(self._iface.RX_PACKET_LEN)
5959
while True:
60+
self._read.timeout_ms = timeout_ms
6061
packet_len = await self._read
6162
assert packet_len is not None
6263
assert packet_len == len(packet)

0 commit comments

Comments
 (0)