Skip to content

Introduce N4W1 backup/recovery flows#6799

Merged
romanz merged 2 commits into
mainfrom
romanz/2604/n4w1-demo
May 5, 2026
Merged

Introduce N4W1 backup/recovery flows#6799
romanz merged 2 commits into
mainfrom
romanz/2604/n4w1-demo

Conversation

@romanz

@romanz romanz commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Design: https://satoshilabs.slack.com/docs/T0J8V2YBY/F0AKE51PVFZ

Also add N4W1-enabled builds to the CI for integration tests.

I have used https://docs.pytest.org/en/stable/how-to/fixtures.html#parametrizing-fixtures in order to re-use some of the existing backup/recovery device tests in a separate CI job:

image

The rest of the test-related changes will be done in separate PRs.

@romanz romanz self-assigned this Apr 20, 2026
@trezor-bot trezor-bot Bot added this to Firmware Apr 20, 2026
@romanz romanz changed the base branch from main to romanz/2604/show-info April 20, 2026 14:02
@github-project-automation github-project-automation Bot moved this to 🔎 Needs review in Firmware Apr 20, 2026
@romanz romanz moved this from 🔎 Needs review to 🏃‍♀️ In progress in Firmware Apr 20, 2026
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 166a4094-a1e2-4734-8bc2-6d829dcba67f

📥 Commits

Reviewing files that changed from the base of the PR and between 5c7bf59 and 29e4b3e.

📒 Files selected for processing (19)
  • .github/workflows/core.yml
  • core/src/apps/management/recovery_device/layout.py
  • core/src/apps/management/reset_device/layout.py
  • core/src/trezor/ui/layouts/eckhart/recovery.py
  • tests/conftest.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_t2.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_reset_slip39_basic.py
  • tests/device_tests/test_repeated_backup.py
  • tests/input_flows.py
  • tests/input_flows_helpers.py
  • tests/ui_tests/fixtures.json
✅ Files skipped from review due to trivial changes (3)
  • tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_t2.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
🚧 Files skipped from review as they are similar to previous changes (11)
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_slip39_basic.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
  • .github/workflows/core.yml
  • core/src/apps/management/reset_device/layout.py
  • tests/input_flows_helpers.py
  • tests/input_flows.py
  • core/src/trezor/ui/layouts/eckhart/recovery.py

Walkthrough

Adds N4W1 as an alternative backup/recovery method across firmware, emulator, UI/layouts, management apps, and tests. CI matrices and artifact naming/handling were extended to produce and consume -n4w1 debuglink artifacts and to split device-test steps when N4W1 is enabled. Management/reset/recovery layouts introduce N4W1 read/write handlers with retry/confirmation and incremental validation logic. Test suites and input-flow helpers are parameterized with a backup_method fixture and updated to drive N4W1 debuglink read/write helper sequences in host-side test flows.

Sequence Diagram(s)

sequenceDiagram
    participant Device
    participant N4W1Tag as N4W1 Tag
    participant Layout as Recovery Layout
    participant UI as UI Handler

    Device->>Layout: start recovery (method=N4W1)
    Layout->>N4W1Tag: read("mnemonic")
    alt empty or damaged
        N4W1Tag-->>Layout: null or corrupted
        Layout->>UI: show danger retry warning
        UI-->>Layout: user confirms retry
        loop retry
            Layout->>N4W1Tag: read("mnemonic")
        end
    else valid share
        N4W1Tag-->>Layout: return encoded mnemonic
        Layout->>Layout: decode & incremental validate
        Layout->>UI: show progress / result
        UI-->>Layout: user proceeds
        Layout->>Device: supply share to recovery flow
    end
Loading
sequenceDiagram
    participant Device
    participant N4W1Tag as N4W1 Tag
    participant Layout as Reset Layout
    participant UI as UI Handler

    Device->>Layout: start backup (method=N4W1)
    loop for each share
        Layout->>N4W1Tag: read("mnemonic")
        alt tag contains data
            N4W1Tag-->>Layout: non-empty
            Layout->>UI: show danger overwrite warning
            UI-->>Layout: user confirms retry
            Layout->>N4W1Tag: read("mnemonic")  -- wait for empty tag
        else tag empty
            Layout->>Layout: encode share -> blob
            Layout->>N4W1Tag: write("mnemonic", blob)
            N4W1Tag-->>Layout: write ack
            Layout->>UI: show hold/footer & success
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Introduce N4W1 backup/recovery flows' clearly and concisely summarizes the main change: adding N4W1 backup/recovery functionality. It is specific, descriptive, and directly related to the changeset.
Description check ✅ Passed The PR description provides essential context: references a design document, explains the pytest fixture parametrization approach for test reuse, includes a CI test-run image demonstrating successful tests, and notes that additional test changes will follow in separate PRs. This adequately documents the PR's intent and scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch romanz/2604/n4w1-demo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown

en main(all)

model device_test click_test persistence_test
T2T1 test(all) main(all) test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all) test(all) main(all)
Translations

cs main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

de main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

es main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

fr main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

pt main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

Latest CI run: 25374842602

@romanz romanz force-pushed the romanz/2604/n4w1-demo branch from 4353da1 to 33e6532 Compare April 20, 2026 19:36
@romanz romanz force-pushed the romanz/2604/show-info branch from 099bfb0 to 14265bc Compare April 21, 2026 05:40
Base automatically changed from romanz/2604/show-info to main April 21, 2026 06:16
@romanz romanz force-pushed the romanz/2604/n4w1-demo branch 2 times, most recently from 8fe2388 to 94b032e Compare April 21, 2026 15:22
@romanz romanz changed the base branch from main to romanz/2604/n4w1-prep April 21, 2026 15:22
@romanz romanz mentioned this pull request Apr 21, 2026
@romanz romanz force-pushed the romanz/2604/n4w1-prep branch 2 times, most recently from ac4c601 to 1df8713 Compare April 22, 2026 07:01
@romanz romanz force-pushed the romanz/2604/n4w1-demo branch from 94b032e to 233d9b9 Compare April 22, 2026 07:01
Base automatically changed from romanz/2604/n4w1-prep to main April 24, 2026 17:33
@romanz romanz force-pushed the romanz/2604/n4w1-demo branch 11 times, most recently from 671ea4e to 9af64ff Compare April 29, 2026 10:04
@romanz romanz added the translations Put this label on a PR to run tests in all languages label Apr 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/src/apps/management/reset_device/layout.py (1)

237-243: Finish the missing N4W1 intro/safety screens.

intro() is still empty and backup() skips the warning shown by _DisplayBackup, so this path drops straight into writing secret shares to tags. I'd close those TODOs before enabling the flow outside demo builds.

Do you want me to sketch a minimal version that reuses the existing backup intro/warning layouts?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/apps/management/reset_device/layout.py` around lines 237 - 243, The
intro() and backup() stubs in ResetDevice.layout must present the same N4W1
intro and safety screens used elsewhere instead of being no-ops; implement
intro(self, num_of_words) to render the existing backup/introduction text/UI
(reuse the layout or components used by the prior backup flow), and update
backup(self, iter_shares) to invoke the existing _DisplayBackup
warning/confirmation sequence before proceeding to write shares (so the user
must acknowledge safety). Locate and call the same helper/layout functions or
components used by the current backup flow (e.g., the backup intro screen and
_DisplayBackup warning) inside ResetDevice.layout.intro and
ResetDevice.layout.backup, preserving the num_of_words and iter_shares
parameters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/src/apps/management/recovery_device/layout.py`:
- Around line 283-290: The current warning in choose_handler logs "Unsupported
backup method: None" for the intentional default path; change the condition so
that the warning is only emitted for explicit unsupported methods, not when
method is None. Concretely, update the conditional that checks method (in
function choose_handler) to first ensure method is not None and not
BackupMethod.Display before entering the __debug__ logging branch and calling
trezor.log.warning, so None is treated as the silent placeholder.

---

Nitpick comments:
In `@core/src/apps/management/reset_device/layout.py`:
- Around line 237-243: The intro() and backup() stubs in ResetDevice.layout must
present the same N4W1 intro and safety screens used elsewhere instead of being
no-ops; implement intro(self, num_of_words) to render the existing
backup/introduction text/UI (reuse the layout or components used by the prior
backup flow), and update backup(self, iter_shares) to invoke the existing
_DisplayBackup warning/confirmation sequence before proceeding to write shares
(so the user must acknowledge safety). Locate and call the same helper/layout
functions or components used by the current backup flow (e.g., the backup intro
screen and _DisplayBackup warning) inside ResetDevice.layout.intro and
ResetDevice.layout.backup, preserving the num_of_words and iter_shares
parameters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 758731a6-ed4f-4b4c-b9f9-5b278cb2b8c9

📥 Commits

Reviewing files that changed from the base of the PR and between e5c7f6d and 1f936e1.

⛔ Files ignored due to path filters (1)
  • core/embed/rust/src/translations/generated/translated_string.rs is excluded by !**/generated/**
📒 Files selected for processing (9)
  • .github/workflows/core.yml
  • core/embed/rust/librust_qstr.h
  • core/mocks/trezortranslate_keys.pyi
  • core/src/apps/management/recovery_device/layout.py
  • core/src/apps/management/reset_device/layout.py
  • core/translations/en.json
  • core/translations/order.json
  • core/translations/signatures.json
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic.py
✅ Files skipped from review due to trivial changes (5)
  • core/embed/rust/librust_qstr.h
  • core/translations/signatures.json
  • core/translations/en.json
  • core/translations/order.json
  • core/mocks/trezortranslate_keys.pyi
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic.py

Comment thread core/src/apps/management/recovery_device/layout.py
@romanz romanz force-pushed the romanz/2604/n4w1-demo branch from 1f936e1 to c5d047b Compare April 29, 2026 15:43
@romanz romanz marked this pull request as ready for review April 29, 2026 15:45
@romanz romanz moved this from 🏃‍♀️ In progress to 🔎 Needs review in Firmware Apr 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/core.yml (1)

305-356: ⚠️ Potential issue | 🟠 Major

Keep N4W1 coverage separate from the default T3W1 report.

This include row adds a second T3W1/universal/noasan binary with N4W1=1, but the coverage pipeline still reuses only the standard core-emu-T3W1-universal-debuglink-noasan build for .i sources. That mixes coverage from two different compiled variants and can skew or break the final T3W1 coverage report. Either skip upload-coverage for matrix.n4w1 for now, or make the coverage artifacts/report job variant-aware (-n4w1 sources + separate merge/reporting).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/core.yml around lines 305 - 356, The pipeline is mixing
coverage from the N4W1 variant into the standard T3W1 report because matrix.n4w1
produces a second core-emu artifact but upload-coverage always processes the
default build; either prevent uploading coverage for N4W1 by adding a condition
to the upload-coverage step (if: ${{ !matrix.n4w1 }}) or make the coverage flow
variant-aware by including the n4w1 suffix in the core-emu download/upload
artifact names (core-emu-${{ matrix.model }}-${{ matrix.coins }}-debuglink-${{
matrix.asan }}${{ matrix.n4w1 && '-n4w1' || '' }}) and updating the
upload-coverage job to merge/report per-variant artifacts instead of a single
default artifact.
♻️ Duplicate comments (1)
core/src/apps/management/recovery_device/layout.py (1)

283-291: ⚠️ Potential issue | 🟡 Minor

Don't warn on the intentional method=None path.

When USE_N4W1 is off, this still logs Unsupported backup method: None for the normal default flow. Limit the warning to explicit non-Display values.

Proposed fix
-        if method is not BackupMethod.Display and __debug__:
+        if method not in (None, BackupMethod.Display) and __debug__:
             from trezor import log

             log.warning(__name__, "Unsupported backup method: %s", method)

Based on learnings, method=None passed to choose_backup_method() is intentionally distinct from BackupMethod.Display, and debug-mode unsupported-method warnings should be suppressed for None as well as for BackupMethod.Display.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/apps/management/recovery_device/layout.py` around lines 283 - 291,
The warning in choose_handler currently triggers for method==None; change the
condition so the debug warning only runs when method is neither None nor
BackupMethod.Display: update the if to check "method is not None and method is
not BackupMethod.Display and __debug__" (keep the import of BackupMethod and
return of _DisplayHandler unchanged) so intentional None flows do not log an
unsupported-method warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/src/apps/management/reset_device/layout.py`:
- Around line 241-259: The branch in _backup_share misinterprets ShareInfo.index
as a global position; change backup to materialize iter_shares into a flattened
list (so you can compute global_index and total_shares across groups) and pass
those global values into _backup_share (or adjust _backup_share signature to
accept global_index and total_shares) so the description/button decision uses
global_index == 0 and global_index == total_shares - 1; update any callers
(e.g., slip39_advanced_show_and_confirm_shares) to rely on the flattened order
instead of group-local ShareInfo.index/num_of_shares.

In `@core/src/trezor/ui/layouts/eckhart/recovery.py`:
- Around line 224-236: The choose_method function currently indexes a two-item
tuple with the result of trezorui_api.select_word(), which can return 0..3 and
will raise IndexError for values >=2; replace the tuple lookup with an explicit
conditional mapping in choose_method: call trezorui_api.select_word(...) as
before, then if index == 0 return BackupMethod.N4W1, elif index == 1 return
BackupMethod.Display, elif index in (2, 3) return a safe default (e.g.,
BackupMethod.Display) or raise a clear ValueError for unexpected slots;
reference the choose_method function, trezorui_api.select_word, BackupMethod and
the TR.backup__type_* symbols when making the change.

---

Outside diff comments:
In @.github/workflows/core.yml:
- Around line 305-356: The pipeline is mixing coverage from the N4W1 variant
into the standard T3W1 report because matrix.n4w1 produces a second core-emu
artifact but upload-coverage always processes the default build; either prevent
uploading coverage for N4W1 by adding a condition to the upload-coverage step
(if: ${{ !matrix.n4w1 }}) or make the coverage flow variant-aware by including
the n4w1 suffix in the core-emu download/upload artifact names (core-emu-${{
matrix.model }}-${{ matrix.coins }}-debuglink-${{ matrix.asan }}${{ matrix.n4w1
&& '-n4w1' || '' }}) and updating the upload-coverage job to merge/report
per-variant artifacts instead of a single default artifact.

---

Duplicate comments:
In `@core/src/apps/management/recovery_device/layout.py`:
- Around line 283-291: The warning in choose_handler currently triggers for
method==None; change the condition so the debug warning only runs when method is
neither None nor BackupMethod.Display: update the if to check "method is not
None and method is not BackupMethod.Display and __debug__" (keep the import of
BackupMethod and return of _DisplayHandler unchanged) so intentional None flows
do not log an unsupported-method warning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71009308-ea25-4473-9716-294aed30b1e5

📥 Commits

Reviewing files that changed from the base of the PR and between 1f936e1 and c5d047b.

⛔ Files ignored due to path filters (1)
  • core/embed/rust/src/translations/generated/translated_string.rs is excluded by !**/generated/**
📒 Files selected for processing (24)
  • .github/workflows/core.yml
  • core/embed/rust/librust_qstr.h
  • core/mocks/trezortranslate_keys.pyi
  • core/src/apps/management/recovery_device/layout.py
  • core/src/apps/management/reset_device/layout.py
  • core/src/trezor/ui/layouts/eckhart/recovery.py
  • core/translations/en.json
  • core/translations/order.json
  • core/translations/signatures.json
  • tests/conftest.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_t2.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_reset_slip39_basic.py
  • tests/device_tests/test_repeated_backup.py
  • tests/input_flows.py
  • tests/input_flows_helpers.py
  • tests/ui_tests/fixtures.json
✅ Files skipped from review due to trivial changes (6)
  • tests/device_tests/reset_recovery/test_recovery_bip39_t2.py
  • core/translations/signatures.json
  • core/translations/en.json
  • core/embed/rust/librust_qstr.h
  • core/mocks/trezortranslate_keys.pyi
  • core/translations/order.json
🚧 Files skipped from review as they are similar to previous changes (12)
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_reset_slip39_basic.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic.py
  • tests/input_flows_helpers.py
  • tests/device_tests/test_repeated_backup.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py
  • tests/input_flows.py

Comment thread core/src/apps/management/reset_device/layout.py
Comment thread core/src/trezor/ui/layouts/eckhart/recovery.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/core.yml (1)

334-339: Don't silently drop UI checks on the N4W1 universal/noasan path.

For this matrix entry ACTIONS_DO_UI_TEST is still true, but the N4W1 branch switches to test_emu_multicore instead of test_emu_ui_multicore. That loses the --ui=test --ui-check-missing --do-master-diff coverage the standard path gets for the same job shape.

Possible fix
       - name: Run device tests (N4W1)
         if: ${{ matrix.n4w1 }}  # TODO: test N4W1 UI fixtures as well
-        run: nix-shell --run "uv run make -C core ${{ env.ACTIONS_DO_UI_TEST == 'true' && 'test_emu_multicore' || 'test_emu' }}"
+        run: nix-shell --run "uv run make -C core ${{ env.ACTIONS_DO_UI_TEST == 'true' && 'test_emu_ui_multicore' || 'test_emu' }}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/core.yml around lines 334 - 339, The N4W1 matrix branch
incorrectly uses test_emu_multicore instead of the UI-aware target, which drops
UI checks when ACTIONS_DO_UI_TEST is true; update the N4W1 run line to mirror
the non-N4W1 conditional expression so it selects test_emu_ui_multicore when
ACTIONS_DO_UI_TEST is 'true' (i.e., use the same ternary logic with
ACTIONS_DO_UI_TEST -> 'test_emu_ui_multicore' || 'test_emu' instead of switching
to 'test_emu_multicore'), ensuring the UI flags (--ui=test, --ui-check-missing,
--do-master-diff) are preserved for N4W1 runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/core.yml:
- Around line 334-339: The N4W1 matrix branch incorrectly uses
test_emu_multicore instead of the UI-aware target, which drops UI checks when
ACTIONS_DO_UI_TEST is true; update the N4W1 run line to mirror the non-N4W1
conditional expression so it selects test_emu_ui_multicore when
ACTIONS_DO_UI_TEST is 'true' (i.e., use the same ternary logic with
ACTIONS_DO_UI_TEST -> 'test_emu_ui_multicore' || 'test_emu' instead of switching
to 'test_emu_multicore'), ensuring the UI flags (--ui=test, --ui-check-missing,
--do-master-diff) are preserved for N4W1 runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ab51a135-d1d8-47a2-b5d1-53e25c5308bb

📥 Commits

Reviewing files that changed from the base of the PR and between c5d047b and b8e841b.

📒 Files selected for processing (1)
  • .github/workflows/core.yml

@romanz romanz force-pushed the romanz/2604/n4w1-demo branch from b8e841b to 53dcb05 Compare April 29, 2026 17:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
core/src/trezor/ui/layouts/eckhart/recovery.py (1)

224-236: ⚠️ Potential issue | 🟠 Major

Handle unexpected select_word() values explicitly.

This still relies on a 2-item tuple lookup, so any unexpected index turns into a generic IndexError. Given the UI contract here, it should fail deliberately and readably instead.

Proposed fix
         index = await interact(
             trezorui_api.select_word(
                 title=title,
                 description=description,
                 words=(TR.backup__type_n4w1, TR.backup__type_wordlist, ""),
             ),
             br_name="choose_method",
         )
-        return (BackupMethod.N4W1, BackupMethod.Display)[index]
+        if index == 0:
+            return BackupMethod.N4W1
+        if index == 1:
+            return BackupMethod.Display
+        raise ValueError(f"Unexpected select_word index: {index}")

Based on learnings, trezorui_api.select_word() called from choose_method will only return 0 or 1 on Eckhart because the empty third word slot is disabled at the UI level. Unexpected return values (index >= 2) should trigger an explicit failure (e.g., raise ValueError) rather than silently defaulting to BackupMethod.Display.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/trezor/ui/layouts/eckhart/recovery.py` around lines 224 - 236, The
choose_method function uses the index from trezorui_api.select_word to pick from
a two-item tuple, which will raise a generic IndexError for unexpected values;
change choose_method (the async function calling trezorui_api.select_word) to
validate the returned index explicitly: accept 0 -> BackupMethod.N4W1, 1 ->
BackupMethod.Display, and if index is any other value raise a clear ValueError
(or custom error) with a descriptive message referencing the unexpected index
and the select_word call so failures are deliberate and readable.
core/src/apps/management/recovery_device/layout.py (1)

283-290: ⚠️ Potential issue | 🟡 Minor

Suppress the debug warning for method=None here as well.

None is the intentional default path, so this branch still logs Unsupported backup method: None on ordinary recovery calls when USE_N4W1 is off.

Proposed fix
-        if method is not BackupMethod.Display and __debug__:
+        if method not in (None, BackupMethod.Display) and __debug__:
             from trezor import log
 
             log.warning(__name__, "Unsupported backup method: %s", method)

Based on learnings, method=None passed to choose_backup_method() is intentionally distinct from BackupMethod.Display. None is reserved for a future flow where the user is prompted to choose between supported backup methods interactively. It should NOT be treated as equivalent to BackupMethod.Display, and debug-mode unsupported-method warnings should be suppressed for None (as well as for BackupMethod.Display).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/apps/management/recovery_device/layout.py` around lines 283 - 290,
The debug warning in choose_handler currently fires for method=None; update the
condition so it only logs when method is neither None nor BackupMethod.Display
(i.e., skip logging for the intentional default None path as well as the Display
enum). Modify the branch that checks BackupMethod to require method is not None
and method is not BackupMethod.Display before calling log.warning (refer to
function choose_handler, symbol BackupMethod, __debug__, and log.warning).
🧹 Nitpick comments (2)
.github/workflows/core.yml (1)

55-69: Consider adding a btconly N4W1 lane too.

All new N4W1 matrix rows are coins: universal, so CI still won’t build or exercise the btconly variant. If N4W1 is expected to ship on both firmware types, this leaves a blind spot for btconly-only regressions.

Based on learnings, when advertising the Capability.N4W1 capability, reviewers should ensure it is added for all firmware types (both Bitcoin-only and universal builds). Do not wrap f.capabilities.append(Capability.N4W1) with utils.BITCOIN_ONLY. The only acceptable conditional guard is utils.USE_N4W1.

Also applies to: 133-138, 306-311

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/core.yml around lines 55 - 69, The CI matrix is missing a
btconly N4W1 lane and some code conditionally guards N4W1 with
utils.BITCOIN_ONLY; add a matrix row mirroring the existing N4W1 universal entry
but with coins: btconly and n4w1: true (e.g., alongside the T3W1 n4w1 entry) so
btconly builds are exercised, and remove any conditional guards that append
Capability.N4W1 under utils.BITCOIN_ONLY — ensure Capability.N4W1 is only gated
by utils.USE_N4W1; apply the same change to the other similar blocks that add
N4W1 (the other matrix sections and any code paths that call
f.capabilities.append(Capability.N4W1)).
tests/input_flows.py (1)

2227-2242: Add an explicit assert for method validation to match InputFlowSlip39BasicBackup.

In InputFlowSlip39BasicBackup.input_flow_eckhart, there's an explicit assert self.method in (Display, N4W1). Here, the dict lookup implicitly validates valid methods, but an invalid method would raise a cryptic KeyError instead of a clear AssertionError.

♻️ Suggested fix for consistency
     def input_flow_eckhart(self) -> BRGeneratorType:
+        assert self.method in (
+            messages.BackupMethod.Display,
+            messages.BackupMethod.N4W1,
+        )
         num_screens = {
             messages.BackupMethod.Display: 10,
             messages.BackupMethod.N4W1: 8,
         }[self.method]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/input_flows.py` around lines 2227 - 2242, Add an explicit assertion to
validate self.method at the start of input_flow_eckhart so it mirrors
InputFlowSlip39BasicBackup: assert self.method in
(messages.BackupMethod.Display, messages.BackupMethod.N4W1). This makes method
validation explicit and raises an AssertionError with a clear failure mode
before using the dict lookup that currently raises a KeyError; place the assert
immediately before the num_screens assignment in the input_flow_eckhart method.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/src/apps/management/reset_device/layout.py`:
- Around line 237-246: The intro() method is currently a no-op and backup()
immediately writes shares, skipping the pre-export safety screens; implement
intro() to reuse the existing _DisplayBackup intro/warning flow (e.g., await
self._display_backup.intro(num_of_words) or delegate to the _DisplayBackup.intro
method) and ensure backup() calls await self.intro(num_of_words) (or awaits the
same _DisplayBackup warning) before iterating and calling _backup_share(share)
so the N4W1 path shows the same safety/acknowledgement screens as
_DisplayBackup.

---

Duplicate comments:
In `@core/src/apps/management/recovery_device/layout.py`:
- Around line 283-290: The debug warning in choose_handler currently fires for
method=None; update the condition so it only logs when method is neither None
nor BackupMethod.Display (i.e., skip logging for the intentional default None
path as well as the Display enum). Modify the branch that checks BackupMethod to
require method is not None and method is not BackupMethod.Display before calling
log.warning (refer to function choose_handler, symbol BackupMethod, __debug__,
and log.warning).

In `@core/src/trezor/ui/layouts/eckhart/recovery.py`:
- Around line 224-236: The choose_method function uses the index from
trezorui_api.select_word to pick from a two-item tuple, which will raise a
generic IndexError for unexpected values; change choose_method (the async
function calling trezorui_api.select_word) to validate the returned index
explicitly: accept 0 -> BackupMethod.N4W1, 1 -> BackupMethod.Display, and if
index is any other value raise a clear ValueError (or custom error) with a
descriptive message referencing the unexpected index and the select_word call so
failures are deliberate and readable.

---

Nitpick comments:
In @.github/workflows/core.yml:
- Around line 55-69: The CI matrix is missing a btconly N4W1 lane and some code
conditionally guards N4W1 with utils.BITCOIN_ONLY; add a matrix row mirroring
the existing N4W1 universal entry but with coins: btconly and n4w1: true (e.g.,
alongside the T3W1 n4w1 entry) so btconly builds are exercised, and remove any
conditional guards that append Capability.N4W1 under utils.BITCOIN_ONLY — ensure
Capability.N4W1 is only gated by utils.USE_N4W1; apply the same change to the
other similar blocks that add N4W1 (the other matrix sections and any code paths
that call f.capabilities.append(Capability.N4W1)).

In `@tests/input_flows.py`:
- Around line 2227-2242: Add an explicit assertion to validate self.method at
the start of input_flow_eckhart so it mirrors InputFlowSlip39BasicBackup: assert
self.method in (messages.BackupMethod.Display, messages.BackupMethod.N4W1). This
makes method validation explicit and raises an AssertionError with a clear
failure mode before using the dict lookup that currently raises a KeyError;
place the assert immediately before the num_screens assignment in the
input_flow_eckhart method.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a039282f-6b92-43a3-9da0-5db463dc2c7d

📥 Commits

Reviewing files that changed from the base of the PR and between b8e841b and 53dcb05.

📒 Files selected for processing (19)
  • .github/workflows/core.yml
  • core/src/apps/management/recovery_device/layout.py
  • core/src/apps/management/reset_device/layout.py
  • core/src/trezor/ui/layouts/eckhart/recovery.py
  • tests/conftest.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_t2.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_reset_slip39_basic.py
  • tests/device_tests/test_repeated_backup.py
  • tests/input_flows.py
  • tests/input_flows_helpers.py
  • tests/ui_tests/fixtures.json
✅ Files skipped from review due to trivial changes (3)
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_t2.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/conftest.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
  • tests/device_tests/reset_recovery/test_reset_slip39_basic.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic.py

Comment on lines +237 to +246
async def intro(self, num_of_words: int | None = None) -> None:
# TODO: design/copy
pass

async def backup(self, iter_shares: Iterable[ShareInfo]) -> None:
# TODO: warn user about safety

# backup all shares
for share in iter_shares:
await self._backup_share(share)

@coderabbitai coderabbitai Bot Apr 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t skip the backup intro/safety screens on the N4W1 path.

intro() is a no-op and backup() jumps straight into writing shares, so this path misses the same pre-export safety acknowledgment that _DisplayBackup shows before exposing the seed.

Possible stopgap
     class _N4W1Backup:
 
         async def intro(self, num_of_words: int | None = None) -> None:
-            # TODO: design/copy
-            pass
+            from trezor.ui.layouts.reset import show_intro_backup
+
+            await show_intro_backup(num_of_words=num_of_words)
 
         async def backup(self, iter_shares: Iterable[ShareInfo]) -> None:
-            # TODO: warn user about safety
+            from trezor.ui.layouts.reset import show_warning_backup
+
+            await show_warning_backup()
 
             # backup all shares
             for share in iter_shares:
                 await self._backup_share(share)

I can help sketch a minimal patch that reuses the existing intro/warning screens until the N4W1-specific copy lands.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/apps/management/reset_device/layout.py` around lines 237 - 246, The
intro() method is currently a no-op and backup() immediately writes shares,
skipping the pre-export safety screens; implement intro() to reuse the existing
_DisplayBackup intro/warning flow (e.g., await
self._display_backup.intro(num_of_words) or delegate to the _DisplayBackup.intro
method) and ensure backup() calls await self.intro(num_of_words) (or awaits the
same _DisplayBackup warning) before iterating and calling _backup_share(share)
so the N4W1 path shows the same safety/acknowledgement screens as
_DisplayBackup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
core/src/apps/management/reset_device/layout.py (1)

237-246: ⚠️ Potential issue | 🟠 Major

Reuse the existing backup disclosure flow before N4W1 writes.

intro() is a no-op and backup() starts exporting shares immediately, so the N4W1 path skips the intro/safety acknowledgement that _DisplayBackup shows before exposing mnemonic material.

Possible stopgap
         async def intro(self, num_of_words: int | None = None) -> None:
-            # TODO: design/copy
-            pass
+            from trezor.ui.layouts.reset import show_intro_backup
+
+            await show_intro_backup(num_of_words=num_of_words)

         async def backup(self, iter_shares: Iterable[ShareInfo]) -> None:
-            # TODO: warn user about safety
+            from trezor.ui.layouts.reset import show_warning_backup
+
+            await show_warning_backup()

             # backup all shares
             for share in iter_shares:
                 await self._backup_share(share)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/apps/management/reset_device/layout.py` around lines 237 - 246, The
intro() method is a no-op and backup() immediately calls _backup_share, which
skips the existing _DisplayBackup disclosure/safety acknowledgement used in the
N4W1 flow; update intro() to present the same disclosure/acknowledgement flow
used by _DisplayBackup (or call the common method/component that shows the
copy/safety notice) and modify backup() to ensure it awaits/uses intro() before
iterating shares (i.e., call await self.intro(...) or invoke the shared
disclosure helper before calling self._backup_share for each ShareInfo) so the
safety intro is always shown prior to exposing mnemonic material.
core/src/apps/management/recovery_device/layout.py (1)

283-291: ⚠️ Potential issue | 🟡 Minor

Don't warn on the default method=None path.

In non-N4W1 builds this still logs Unsupported backup method: None for the ordinary recovery flow. Restrict the warning to explicit non-None unsupported values.

Proposed patch
-        if method is not BackupMethod.Display and __debug__:
+        if method not in (None, BackupMethod.Display) and __debug__:
             from trezor import log

             log.warning(__name__, "Unsupported backup method: %s", method)

Based on learnings, method=None is an intentional placeholder and should not trigger unsupported-method warnings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/apps/management/recovery_device/layout.py` around lines 283 - 291,
The warning should not fire for the intentional placeholder value None; update
choose_handler so the log.warning inside the __debug__ block only runs when
method is not None and not BackupMethod.Display. Locate the choose_handler
function (and the log.warning call that references Unsupported backup method)
and change the condition to require method is not None in addition to method is
not BackupMethod.Display so only explicit unsupported enum values trigger the
warning; keep returning _DisplayHandler unchanged.
🧹 Nitpick comments (1)
tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py (1)

38-39: Thread backup_method through the reset leg too.

The new fixture only affects the recovery half of this roundtrip. reset(session) still uses the default display backup flow, so the N4W1 matrix here never exercises reset-time backup.

Proposed patch
-def test_reset_recovery(client: Client, backup_method: BackupMethod):
+def test_reset_recovery(client: Client, backup_method: BackupMethod):
     session = client.get_seedless_session()

-    mnemonics = reset(session)
+    mnemonics = reset(session, method=backup_method)
     session = client.get_session()
     address_before = btc.get_address(session, "Bitcoin", parse_path("m/44h/0h/0h/0/0"))
-def reset(session: Session, strength: int = 128) -> list[str]:
+def reset(
+    session: Session,
+    strength: int = 128,
+    method: BackupMethod = BackupMethod.Display,
+) -> list[str]:
     with session.test_ctx as client:
-        IF = InputFlowSlip39BasicResetRecovery(session)
+        IF = InputFlowSlip39BasicResetRecovery(session, method)
         client.set_input_flow(IF.get())

         device.setup(
             session,
             strength=strength,
             passphrase_protection=False,
             pin_protection=False,
             label="test",
             backup_type=BackupType.Slip39_Basic,
+            backup_method=method,
             entropy_check_count=0,
             _get_entropy=MOCK_GET_ENTROPY,
         )

Also applies to: 52-52

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py` around
lines 38 - 39, The test only applies the backup_method fixture to the recovery
half; thread the backup_method through the reset leg so reset uses the same
backup flow. Update the test_reset_recovery code paths that call
client.get_seedless_session() and reset(session) to pass the backup_method
fixture into the reset flow (for example by calling reset(session,
backup_method=backup_method) or by creating the session with
client.get_seedless_session(backup_method=backup_method) so both
get_seedless_session and reset() use the same backup_method); do the same change
for the other occurrence referenced (around line 52) so both reset and recovery
phases exercise the selected backup_method.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@core/src/apps/management/recovery_device/layout.py`:
- Around line 283-291: The warning should not fire for the intentional
placeholder value None; update choose_handler so the log.warning inside the
__debug__ block only runs when method is not None and not BackupMethod.Display.
Locate the choose_handler function (and the log.warning call that references
Unsupported backup method) and change the condition to require method is not
None in addition to method is not BackupMethod.Display so only explicit
unsupported enum values trigger the warning; keep returning _DisplayHandler
unchanged.

In `@core/src/apps/management/reset_device/layout.py`:
- Around line 237-246: The intro() method is a no-op and backup() immediately
calls _backup_share, which skips the existing _DisplayBackup disclosure/safety
acknowledgement used in the N4W1 flow; update intro() to present the same
disclosure/acknowledgement flow used by _DisplayBackup (or call the common
method/component that shows the copy/safety notice) and modify backup() to
ensure it awaits/uses intro() before iterating shares (i.e., call await
self.intro(...) or invoke the shared disclosure helper before calling
self._backup_share for each ShareInfo) so the safety intro is always shown prior
to exposing mnemonic material.

---

Nitpick comments:
In `@tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py`:
- Around line 38-39: The test only applies the backup_method fixture to the
recovery half; thread the backup_method through the reset leg so reset uses the
same backup flow. Update the test_reset_recovery code paths that call
client.get_seedless_session() and reset(session) to pass the backup_method
fixture into the reset flow (for example by calling reset(session,
backup_method=backup_method) or by creating the session with
client.get_seedless_session(backup_method=backup_method) so both
get_seedless_session and reset() use the same backup_method); do the same change
for the other occurrence referenced (around line 52) so both reset and recovery
phases exercise the selected backup_method.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e18c8a3-1132-4528-869b-a022ee1ab303

📥 Commits

Reviewing files that changed from the base of the PR and between b8e841b and 53dcb05.

📒 Files selected for processing (19)
  • .github/workflows/core.yml
  • core/src/apps/management/recovery_device/layout.py
  • core/src/apps/management/reset_device/layout.py
  • core/src/trezor/ui/layouts/eckhart/recovery.py
  • tests/conftest.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_t2.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
  • tests/device_tests/reset_recovery/test_reset_slip39_basic.py
  • tests/device_tests/test_repeated_backup.py
  • tests/input_flows.py
  • tests/input_flows_helpers.py
  • tests/ui_tests/fixtures.json
✅ Files skipped from review due to trivial changes (4)
  • tests/device_tests/reset_recovery/test_recovery_bip39_t2.py
  • tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/device_tests/reset_recovery/test_recovery_slip39_advanced_dryrun.py
  • tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
  • tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py
  • tests/input_flows_helpers.py
  • tests/conftest.py
  • tests/input_flows.py

@romanz romanz mentioned this pull request Apr 30, 2026
@romanz romanz changed the base branch from main to romanz/2604/n4w1-prep April 30, 2026 17:04
@romanz romanz force-pushed the romanz/2604/n4w1-demo branch from 53dcb05 to c9d57c0 Compare April 30, 2026 17:04
@romanz

romanz commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Rebased over #6861.

Base automatically changed from romanz/2604/n4w1-prep to main April 30, 2026 18:16
@romanz romanz force-pushed the romanz/2604/n4w1-demo branch from c9d57c0 to 563284b Compare April 30, 2026 19:03

@mmilata mmilata left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
Perhaps consider s/TODO/TODO(N4W1)/ so we can easily search & make sure the relevant TODOs are resolved before we enable the feature by default?

raise RetryRead(TR.n4w1__err_empty)

# TODO: use protobuf?
blob = bytes(blob)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the bytes constructor needed? I assume we don't have enough information for now to resolve the TODO.

@romanz romanz May 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the bytes constructor needed?

memoryview doesn't have .decode() method - so I couldn't call it on blob (which is AnyBytes after c3e6e7a).

I can switch to use str(blob, "utf-8") as done in:
https://github.com/trezor/micropython/blob/e8d89a02609259ad4b5cbaf785255f36bfd3d8dc/py/objstr.c#L1896-L1910

OTOH, assert isinstance(blob, bytes) works as well, since our protobuf codec allocates bytes:

FieldType::Bytes => {
let buf_len = num.try_into()?;
let buf = stream.read(buf_len)?;
buf.try_into()
}

@romanz romanz May 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we don't have enough information for now to resolve the TODO.

We can use https://www.notion.so/satoshilabs/Firmware-workpage-WIP-4e58d783efbb41748d599df35053a40a?source=copy_link#28aa30d2d75049bfadcc6e92ad09f2ed for encoding the mnemonic - WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memoryview doesn't have .decode() method

ah, didn't realize - blob = bytes(blob) is fine unless it's huge

We can use https://www.notion.so/... for encoding the mnemonic - WDYT?

sounds good!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blob should be small (currently it's only the mnemonic, so at most 33 words).

Opened an issue for defining the storage format -> #6876

@romanz

romanz commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

s/TODO/TODO(N4W1)/ in 5c7bf59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
core/src/apps/management/recovery_device/layout.py (2)

347-360: 💤 Low value

Drop the redundant super().__init__() call.

_N4W1Handler does not inherit from any explicit base class, so super().__init__() on line 353 just invokes object.__init__(). It's harmless but adds noise; _DisplayHandler.__init__ does not include it either, so removing keeps the two handlers consistent.

♻️ Proposed nit
         def __init__(
             self,
             recovery_type: RecoveryType,
             slip39_state: Slip39State | None,
         ) -> None:
-            super().__init__()
             self.recovery_type = recovery_type
             # `slip39_state is None` indicates that we are (re)starting the first recovery step.
             self.slip39_state = slip39_state
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/apps/management/recovery_device/layout.py` around lines 347 - 360,
Remove the redundant super().__init__() call inside the _N4W1Handler.__init__
method: delete the line calling super().__init__() so the constructor simply
assigns self.recovery_type and self.slip39_state (keeping the load classmethod
as-is which returns cls(recovery_type, load_slip39_state())). This makes
_N4W1Handler consistent with _DisplayHandler by avoiding the unnecessary
object.__init__ invocation.

393-418: 💤 Low value

Remove redundant prefix-checking loop.

check_words calls word_validity.check for each prefix length 1..N (lines 408-410), but check only examines the last word of the partial mnemonic (via len(partial_mnemonic) - 1), making each iteration O(1). The outer loop validates at specific index boundaries (0–3) to mirror per-word entry behavior, but since the share is read atomically rather than typed word-by-word, a single call with the full mnemonic may be sufficient. Verify whether validation errors must trigger at intermediate prefix boundaries for correctness or UX before simplifying to a single post-loop check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/apps/management/recovery_device/layout.py` around lines 393 - 418,
The prefix-checking loop in check_words unnecessarily calls check(backup_type,
partial_mnemonic=...) for each prefix length; simplify by calling check once
with the full share_words (i.e. check(backup_type,
partial_mnemonic=share_words)) and remove the for loop and per-prefix progress
reports, keeping the same try/except/finally structure and
progress_obj.start()/stop(); ensure you still await exc.show_error() on
WordValidityResult and preserve use of backup_type and share_words for the
single check call so behavior and error handling remain identical.
core/src/apps/management/reset_device/layout.py (1)

281-305: 💤 Low value

Consider starting progress at report(0) for consistency.

The report() method accepts values from 0 to 1000. Starting with report(100) displays ~10% progress before any I/O, then jumps to 100% after ctx.write() completes. Other code in the codebase (e.g., core/src/trezor/pin.py) initializes progress at report(0). Aligning with this pattern and the existing TODO comment would support proper progress animation during I/O operations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/apps/management/reset_device/layout.py` around lines 281 - 305, In
_write_share, change the initial progress report to start at 0 instead of 100:
after creating progress_obj = progress(description=TR.n4w1__writing) and
progress_obj.start(), call progress_obj.report(0) to match the project's
convention (see core/src/trezor/pin.py) so the progress animation covers the
I/O; leave the post-write report calls (report(1000) or final values) and the
progress_obj.stop() intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@core/src/apps/management/recovery_device/layout.py`:
- Around line 347-360: Remove the redundant super().__init__() call inside the
_N4W1Handler.__init__ method: delete the line calling super().__init__() so the
constructor simply assigns self.recovery_type and self.slip39_state (keeping the
load classmethod as-is which returns cls(recovery_type, load_slip39_state())).
This makes _N4W1Handler consistent with _DisplayHandler by avoiding the
unnecessary object.__init__ invocation.
- Around line 393-418: The prefix-checking loop in check_words unnecessarily
calls check(backup_type, partial_mnemonic=...) for each prefix length; simplify
by calling check once with the full share_words (i.e. check(backup_type,
partial_mnemonic=share_words)) and remove the for loop and per-prefix progress
reports, keeping the same try/except/finally structure and
progress_obj.start()/stop(); ensure you still await exc.show_error() on
WordValidityResult and preserve use of backup_type and share_words for the
single check call so behavior and error handling remain identical.

In `@core/src/apps/management/reset_device/layout.py`:
- Around line 281-305: In _write_share, change the initial progress report to
start at 0 instead of 100: after creating progress_obj =
progress(description=TR.n4w1__writing) and progress_obj.start(), call
progress_obj.report(0) to match the project's convention (see
core/src/trezor/pin.py) so the progress animation covers the I/O; leave the
post-write report calls (report(1000) or final values) and the
progress_obj.stop() intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bbe16c9-a656-4371-984b-56f758c6e186

📥 Commits

Reviewing files that changed from the base of the PR and between 563284b and 5c7bf59.

📒 Files selected for processing (3)
  • .github/workflows/core.yml
  • core/src/apps/management/recovery_device/layout.py
  • core/src/apps/management/reset_device/layout.py

romanz added 2 commits May 5, 2026 13:53
Can be manually tested by invoking:
```
$ core/emu.py -ea -c trezorctl device setup -b shamir   # will run multi-share backup
$ core/tools/n1w1-emu.py run 127.0.0.1:21325 /tmp/tagN  # simulate tag connection and I/O
```

Enabled N4W1-based backup/recovery device tests for SLIP-39 single group scenarios.
Other device & click tests will be added in subsequent PRs.

[no changelog]
@romanz

romanz commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Squashing before merge.

@romanz romanz force-pushed the romanz/2604/n4w1-demo branch from 5c7bf59 to 29e4b3e Compare May 5, 2026 11:55
@romanz romanz merged commit 4627ebe into main May 5, 2026
169 checks passed
@romanz romanz deleted the romanz/2604/n4w1-demo branch May 5, 2026 12:50
@trezor-bot trezor-bot Bot moved this from 🔎 Needs review to 🤝 Needs QA in Firmware May 5, 2026
@romanz romanz linked an issue May 26, 2026 that may be closed by this pull request
@romanz romanz moved this from 🤝 Needs QA to ✅ Done (no QA) in Firmware May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

translations Put this label on a PR to run tests in all languages

Projects

Status: ✅ Done (no QA)

Development

Successfully merging this pull request may close these issues.

Backup/recovery flows re-testing

2 participants