feat(core): introduce DebugLink-based N4W1 emulator
What changed, and why it matters
This commit adds a developer-only mock/emulator for a new hardware component called N4W1. It is gated behind debug builds (PYOPT='0') and only for the T3W1 device model. It does not change normal user-facing wallet behavior; it only provides a way for engineers to simulate the N4W1 chip during testing.
No immediate action required. Treat as normal development/testing infrastructure. Ensure the N4W1 mock remains excluded from production (PYOPT != '0') builds and that DebugLink is disabled in release firmware, consistent with existing Trezor practice.
Security signals we found
New debug-only feature with no production impact
Gated by PYOPT='0' and T3W1 model checks
Uses existing DebugLink infrastructure (developer/test interface)
No privilege escalation, cryptographic, or storage bypass changes observed
Host-side emulator stores data in a local dbm file, not device secure storage
Evidence from the diff
The patch introduces a DebugLink-based N4W1 mock: a new n4w1_mock.py module exposing N4W1Context with read/write methods, a new DebugLinkN4W1Connected workflow handler in apps.debug, build-system exclusions so the mock is only frozen into T3W1 debug firmware, and a host-side n4w1-emu.py tool backed by a dbm file. The code is wrapped in if __debug__: and if PYOPT == '0' checks, and the handler is registered only when utils.INTERNAL_MODEL == 'T3W1'.
Changed components
core/src/apps/debug/__init__.pycore/src/apps/debug/n4w1_mock.pycore/tools/n4w1-emu.pycore/SConscript.firmwarecore/SConscript.unixcore/embed/upymod/qstrdefsport.hInspect captured patch +150 / −2
diff --git a/core/SConscript.firmware b/core/SConscript.firmware
index d4ef0896..c15246ca 100644
--- a/core/SConscript.firmware
+++ b/core/SConscript.firmware
@@ -741,7 +741,8 @@ if FROZEN:
)
))
if PYOPT == '0':
- SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py'))
+ exclude_n4w1 = [SOURCE_PY_DIR + 'apps/debug/n4w1_mock.py'] if TREZOR_MODEL != "T3W1" else []
+ SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py', exclude=exclude_n4w1))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/homescreen/device_menu.py',
diff --git a/core/SConscript.unix b/core/SConscript.unix
index 0803a965..e710153e 100644
--- a/core/SConscript.unix
+++ b/core/SConscript.unix
@@ -756,7 +756,8 @@ if FROZEN:
)
))
if PYOPT == '0':
- SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py'))
+ exclude_n4w1 = [SOURCE_PY_DIR + 'apps/debug/n4w1_mock.py'] if TREZOR_MODEL != "T3W1" else []
+ SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py', exclude=exclude_n4w1))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/homescreen/device_menu.py',
diff --git a/core/embed/upymod/qstrdefsport.h b/core/embed/upymod/qstrdefsport.h
index 6d50a0a8..390e888e 100644
--- a/core/embed/upymod/qstrdefsport.h
+++ b/core/embed/upymod/qstrdefsport.h
@@ -828,8 +828,10 @@ Q(DebugWaitType)
Q(__main__)
Q(apps.debug)
Q(apps.debug.load_device)
+Q(apps.debug.n4w1_mock)
Q(debug)
Q(load_device)
+Q(n4w1_mock)
Q(prof)
Q(prof.__main__)
Q(storage.debug)
diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py
index 35ef1e03..de39441c 100644
--- a/core/src/apps/debug/__init__.py
+++ b/core/src/apps/debug/__init__.py
@@ -28,6 +28,7 @@ if __debug__:
DebugLinkGetGcInfo,
DebugLinkGetPairingInfo,
DebugLinkGetState,
+ DebugLinkN4W1Connected,
DebugLinkOptigaSetSecMax,
DebugLinkPairingInfo,
DebugLinkRecordScreen,
@@ -446,6 +447,16 @@ if __debug__:
finally:
raise RestartEventLoop
+ if utils.INTERNAL_MODEL == "T3W1": # TODO utils.USE_N4W1
+
+ async def dispatch_DebugLinkConnected(msg: DebugLinkN4W1Connected) -> Success:
+ """Exchange a sequence of N4W1 messages."""
+ from .n4w1_mock import ctx
+
+ assert DEBUG_CONTEXT is not None
+ await ctx.handle(DEBUG_CONTEXT)
+ return Success()
+
async def _no_op(_msg: Any) -> Success:
return Success()
@@ -565,6 +576,11 @@ if __debug__:
MessageType.WipeDevice: dispatch_WipeDevice,
}
+ if utils.INTERNAL_MODEL == "T3W1": # TODO utils.USE_N4W1
+ WORKFLOW_HANDLERS[MessageType.DebugLinkN4W1Connected] = (
+ dispatch_DebugLinkConnected
+ )
+
def boot() -> None:
import usb
diff --git a/core/src/apps/debug/n4w1_mock.py b/core/src/apps/debug/n4w1_mock.py
new file mode 100644
index 00000000..842b9b65
--- /dev/null
+++ b/core/src/apps/debug/n4w1_mock.py
@@ -0,0 +1,58 @@
+from typing import TYPE_CHECKING
+
+from trezor import log, loop
+from trezor.messages import DebugLinkN4W1Read, DebugLinkN4W1Response, DebugLinkN4W1Write
+
+if TYPE_CHECKING:
+ from buffer_types import AnyBytes
+ from typing import Any
+
+ from trezor.wire.context import Context
+ from typing_extensions import Self
+
+ DebugLinkN4W1Request = DebugLinkN4W1Read | DebugLinkN4W1Write
+
+
+class N4W1Context:
+ def __init__(self) -> None:
+ self.tx: loop.mailbox[DebugLinkN4W1Request | None] = loop.mailbox()
+ self.rx: loop.mailbox[DebugLinkN4W1Response] = loop.mailbox()
+
+ # Invoked by the application (to communicate via N4W1)
+
+ def __enter__(self) -> Self:
+ log.debug(__name__, "N4W1 exchange start")
+ return self
+
+ def __exit__(self, exc_type: Any, exc_val: Any, tb: Any) -> None:
+ log.debug(__name__, "N4W1 exchange done")
+ self.tx.put(None)
+
+ async def read(self, key: str) -> AnyBytes | None:
+ """Read a specific entry from N4W1."""
+ log.debug(__name__, "N4W1 read: %s", key)
+ self.tx.put(DebugLinkN4W1Read(key=key))
+ # blocks until N4W1 connection + response
+ resp = await self.rx
+ log.debug(__name__, "N4W1 response: %s", resp.value)
+ return resp.value
+
+ async def write(self, key: str, value: AnyBytes | None) -> AnyBytes | None:
+ """Write/delete a specific entry from N4W1."""
+ log.debug(__name__, "N4W1 write: %s %s", key, value)
+ self.tx.put(DebugLinkN4W1Write(key=key, value=value))
+ # blocks until N4W1 connection + response
+ resp = await self.rx
+ log.debug(__name__, "N4W1 response: %s", resp.value)
+ return resp.value
+
+ # Invoked to communicate with N4W1 emulator over apps.debug.DEBUG_CONTEXT
+
+ async def handle(self, ctx: Context) -> None:
+ """Called from `apps.debug.dispatch_DebugLinkConnected()`."""
+ while (req := await self.tx) is not None:
+ res = await ctx.call(req, DebugLinkN4W1Response)
+ self.rx.put(res)
+
+
+ctx = N4W1Context()
diff --git a/core/tools/n4w1-emu.py b/core/tools/n4w1-emu.py
new file mode 100755
index 00000000..afa1020b
--- /dev/null
+++ b/core/tools/n4w1-emu.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+
+import dbm
+
+import click
+
+from trezorlib.debuglink import DebugLink
+from trezorlib.messages import (
+ DebugLinkN4W1Connected,
+ DebugLinkN4W1Read,
+ DebugLinkN4W1Response,
+ DebugLinkN4W1Write,
+ Success,
+)
+from trezorlib.transport.udp import UdpTransport
+from trezorlib.transport.webusb import WebUsbTransport
+
+
+@click.group()
+def cli() -> None:
+ pass
+
+
+@cli.command
+@click.argument("device_path")
+@click.argument("db_file")
+def run(device_path: str, db_file: str) -> None:
+ """Run basic N4W1 emulator over DebugLink transport."""
+ with dbm.open(db_file, "c") as db:
+ for k, v in db.items():
+ print(f"{k} => {v}")
+
+ if device_path.startswith("webusb"):
+ # first matching USB device
+ transport = next(
+ t.find_debug()
+ for t in WebUsbTransport.enumerate()
+ if t.get_path().startswith(device_path)
+ )
+ else:
+ # directly open debuglink (without interfering with wirelink UDP port)
+ transport = UdpTransport(device_path)
+
+ debug = DebugLink(transport)
+ req = debug._call(DebugLinkN4W1Connected())
+ while not isinstance(req, Success):
+ print(req)
+ value = None
+ if isinstance(req, DebugLinkN4W1Write):
+ if req.key is not None:
+ # fetch the existing item
+ value = db.get(req.key, None)
+ # insert a new one, or delete (if the new value is None)
+ if req.value is not None:
+ db[req.key] = req.value
+ elif value is not None:
+ del db[req.key]
+ elif isinstance(req, DebugLinkN4W1Read):
+ if req.key is not None:
+ value = db.get(req.key, None)
+ else:
+ raise NotImplementedError(req)
+
+ resp = DebugLinkN4W1Response(value=value)
+ print(resp)
+ req = debug._call(resp)
+
+
+if __name__ == "__main__":
+ cli()
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.