Skip to content

BitBox02 follow-ups: USB re-enumeration crash during pairing, crash-reporting rollout, device context, chainId registration coordination #879

Description

@TaprootFreak

Tracking issue for the remaining work from the two BitBox02 customer cases reported on 23.07. and the crash-reporting groundwork in #878. Four workstreams, ordered by customer impact.


1. USB connect: crash/hang when the BitBox02 re-enumerates during initial pairing

This is the crash/loop case explicitly declared out of scope in #866 ("The unexplained crash/loop reported alongside this customer case") — it now has a root-cause analysis and gets its tracked home here.

Symptoms (customer on a Samsung Galaxy Tab S4, BitBox02 Nova, current app):

  • Tapping "BitBox hardware wallet" raises the system dialog "Open BitBoxApp?"; cancelling it crashes the app to the home screen.
  • With the vendor BitBoxApp uninstalled, connecting still aborts/crashes.
  • Best run: after "Tap this side", the logo spins forever and the password prompt never appears.
  • The Android dialog "Allow RealUnit to access the USB device" never appeared in any attempt.

Verified root causes (code review of the app plus the pinned bitbox_flutter v0.0.10 plugin; the BitBox02 re-enumerates on the USB bus after firmware boot/unlock):

  1. Unstable device identifier. Devices are addressed by the Android deviceName bus path (GetDevicesOperation.kt:17-19, key of UsbManager.getDeviceList()), which does not survive re-enumeration; BitboxManager.kt:18-22 then fails with "Device with identifier not found". GoDeviceInfo additionally holds the UsbDevice/connection from open() time — after re-attach the file descriptor is dead and read/write run into 10 s bulkTransfer timeouts (GoDeviceInfo.kt:35-55).
  2. No re-attach recovery during pairing. After starting the connect, the cubit only polls getChannelHash() for 90 s against the old handle (connect_bitbox_cubit.dart:112-136) — no rescan, no new permission request. The only recovery observer starts after successful pairing (connect_bitbox_cubit.dart:232), i.e. after the exact point where the customer hangs. Result: endless spinner, then a silent restart of the same loop — symptom 3.
  3. Permission flow breaks silently on a stale identifier. RequestPermissionOperation returns success(false) without ever showing a dialog when the identifier no longer resolves (explains symptom 4), and the Dart connect() ignores the requestPermission result and calls open() anyway (bitbox_manager.dart:15-20).
  4. Uncaught native error paths. usbManager.openDevice() returns null without permission and the next line NPEs (GoDeviceInfo.kt:35-37); ConnectBitBoxOperation.kt:16-23 catches only BitBoxException and force-unwraps identifier!!; MethodCallRegistry.kt:16-28 has no try/catch; RequestPermissionOperation answers the Dart future from a BroadcastReceiver — if the broadcast never fires the future never completes (hang), if it fires twice a "Reply already submitted" crash threatens. Together these explain symptoms 1 and 2.
  5. No attach intent-filter of our own. Our manifest only declares uses-feature usb.host; the "Open BitBoxApp?" resolver comes from the vendor app's USB_DEVICE_ATTACHED filter (VID 0x03eb / PID 0x2403). We never appear there and depend entirely on the runtime permission that (3) prevents.

Fix direction, in priority order (a–b are the substance, c–d are supporting):
a. Resolve devices by VID/PID (+ serial where available) instead of the bus path, and add re-attach recovery during pairing: on "device gone / identifier invalid", rescan, adopt the new device, request permission again — instead of polling a dead handle for 90 s.
b. Crash-guard the native path in the bitbox_flutter plugin (broad catch in the operations and registry, explicit openDevice() == null handling, evaluate the requestPermission result, single-reply/timeout guard in the permission receiver). Requires a plugin release + version bump here, since the plugin is pinned.
c. Register our own USB_DEVICE_ATTACHED intent-filter with a device filter for the BitBox VID/PID so attach events reach the app and the resolver offers it alongside the vendor app.
d. Concrete error states/texts in the connect flow instead of the generic failure snackbar (overlaps with the UX escalation already specced in #866).

Reproduction with real hardware is required before landing a fix — the re-enumeration is well evidenced in code, but which crash path fires on the customer device is timing-dependent.

2. Turn on crash reporting in releases (follow-up to #878)

#878 ships the reporter fully gated: without an injected DSN nothing starts. To make it effective:

  • Release workflow injects SENTRY_DSN (repo/environment secret + --dart-define), optionally SENTRY_ENVIRONMENT for testnet builds
  • First tagged build afterwards smoke-checked once on device (PR CI runs analyze + tests only; the native SDK parts join at Gradle/pod build time)
  • One deliberate test event verified end-to-end in the crash-reporting backend

3. Attach BitBox device context to crash reporting (follow-up to #878)

So hardware cases stop depending on asking the customer for device facts:

  • After connect, read product and firmware version from the device and attach them as reporter context
  • Breadcrumbs for the connect ceremony (scan result, permission result, init, pairing steps, disconnects) — within the CONTRIBUTING § API Access data-minimisation rule from feat(crash-reporting): opt-in crash reporting behind a build-time SENTRY_DSN gate #878 (widening what is sent is review-blocking)
  • Unit tests per CONTRIBUTING; no PII in context values

4. Registration signing on BitBox: rollout coordination for the chainId pair PRs

DFXswiss/api#4354 (verification accepts both EIP-712 domain variants) and #877 (BitBox signs with a chainId-extended domain) are drafts. Remaining before rollout:

  • Confirm with Aktionariat that their signature re-verification accepts the chainId-extended domain (their check being pinned to the legacy domain would fail BitBox registrations at forward time)
  • Correct both PR descriptions/commit messages on one point: current firmware does not hard-reject a chainId-less domain — it shows a confirmable warning ("Typed data has no chain ID. Message is valid for every chain."). The fix is still right (removes the scary prompt and binds the signature to the chain), but the stated mechanism should match the firmware behaviour; affected customers may simply be cancelling at that warning screen.
  • Have support verify the interim workaround with the affected customer: confirming that warning on the device should let the registration proceed with the current app version
  • Merge order enforced: API PR first (backward compatible), app PR second; app release only after the Aktionariat confirmation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions