Make payment-request memo names scrollable
What changed, and why it matters
This firmware update changes how long merchant or recipient names are shown when approving Bitcoin payments. Instead of breaking the name across two lines, which could be cut off or hidden, the name now appears on a single line that the user can scroll through. This helps users actually see and verify long names like 'SWAPKIT (...)' during payment approval. The change is a usability and anti-spoofing improvement, not a vulnerability fix in the traditional sense, but it does close a small security gap where truncated or wrapped names might mislead a user.
No immediate action required. Treat as a routine firmware update improving payment-request review clarity. Users who sign payment requests with long recipient names should upgrade to v9.27.2 to benefit from the scrollable display. Developers should ensure client simulators respect the new scrollable flag and updated test-vector transcripts.
Security signals we found
UI truncation/spoofing risk reduced by making long authenticated recipient names reviewable
Test vectors version-gated to preserve historical behavior
New unit test covers long recipient name scrollability
No changes to signature verification, memory allocation, or input parsing
Evidence from the diff
The commit modifies the BitBox02 payment-request confirmation screen. Previously the recipient name was formatted as ‘Memo from\n\n{recipient_name}’, which could cause long names to be displayed across multiple fixed lines and potentially truncated or obscured. The patch changes the body to ‘Memo from: {recipient_name}’ and marks the confirm dialog as scrollable (scrollable: true). It also bumps the firmware version to v9.27.2, updates test vectors to preserve pre- and post-change screen transcripts, and adds unit tests covering long SWAPKIT-style recipient names. No cryptographic, parsing, or memory-safety code is changed.
Changed components
src/rust/bitbox02-rust/src/hww/api/payment_request.rssrc/rust/bitbox02-rust/src/hal/testing/ui.rssrc/rust/bitbox-test-vectors/src/btc_transaction/cases/screens.rssrc/rust/bitbox-test-vectors/testdata/btc-transaction-test-vectors.jsonsrc/rust/bitbox-test-vectors/README.mdversions.jsonInspect captured patch +115 / −6
### src/rust/bitbox-test-vectors/README.md
@@ -12,6 +12,13 @@ express.
Version expectations and the previous-transaction requirement live on the vector.
+Client libraries use these vectors for integration tests against all supported and historical
+firmware versions. When firmware changes an observable outcome or screen transcript, preserve the
+old `VersionExpectation` by closing its range with `max_version_exclusive`, and add a new
+expectation whose `min_version` is the first firmware containing the change. Do not mutate an
+existing open-ended range in a way that changes the expected behavior of historical firmware.
+After changing the Rust constructors, regenerate the canonical JSON as described below.
+
Explicit derivation paths use canonical `m/...` strings rather than protocol-level integer arrays.
Confirm and transaction-fee screens include their `longtouch` requirement. Client simulators whose
stdout protocol omits that flag compare the remaining observable screen fields.
### src/rust/bitbox-test-vectors/src/btc_transaction/cases/screens.rs
@@ -798,7 +798,12 @@ pub fn payment_request_owned_output() -> Vec<VersionExpectation> {
success(
Some("9.24.0"),
Some("9.26.3"),
- payment_request_screens("0.30000000 TBTC", "0.10000000 TBTC", 50),
+ payment_request_screens(
+ "0.30000000 TBTC",
+ "0.10000000 TBTC",
+ 50,
+ "Memo from\n\nTest Merchant",
+ ),
),
invalid_input(Some("9.26.3"), None),
]
@@ -855,12 +860,13 @@ fn payment_request_screens(
total: &str,
transaction_fee: &str,
high_fee_percent: u32,
+ memo_from_body: &str,
) -> Vec<Screen> {
vec![
address("0.20000000 TBTC", "Test Merchant"),
Screen::Confirm {
title: "".into(),
- body: "Memo from\n\nTest Merchant".into(),
+ body: memo_from_body.into(),
longtouch: false,
},
Screen::Confirm {
@@ -884,8 +890,23 @@ pub fn payment_request() -> Vec<VersionExpectation> {
invalid_input_before("9.24.0"),
success(
Some("9.24.0"),
+ Some("9.27.2"),
+ payment_request_screens(
+ "0.50000000 TBTC",
+ "0.30000000 TBTC",
+ 150,
+ "Memo from\n\nTest Merchant",
+ ),
+ ),
+ success(
+ Some("9.27.2"),
None,
- payment_request_screens("0.50000000 TBTC", "0.30000000 TBTC", 150),
+ payment_request_screens(
+ "0.50000000 TBTC",
+ "0.30000000 TBTC",
+ 150,
+ "Memo from: Test Merchant",
+ ),
),
]
}
### src/rust/bitbox-test-vectors/testdata/btc-transaction-test-vectors.json
@@ -1335,6 +1335,7 @@
},
{
"min_version": "9.24.0",
+ "max_version_exclusive": "9.27.2",
"outcome": "success",
"screens": [
{
@@ -1378,6 +1379,52 @@
"body": "confirmed"
}
]
+ },
+ {
+ "min_version": "9.27.2",
+ "outcome": "success",
+ "screens": [
+ {
+ "type": "transaction_address",
+ "amount": "0.20000000 TBTC",
+ "address": "Test Merchant"
+ },
+ {
+ "type": "confirm",
+ "title": "",
+ "body": "Memo from: Test Merchant",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "Memo 1/2",
+ "body": "TextMemo line1",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "Memo 2/2",
+ "body": "TextMemo line2",
+ "longtouch": false
+ },
+ {
+ "type": "transaction_fee",
+ "amount": "0.50000000 TBTC",
+ "fee": "0.30000000 TBTC",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "High fee",
+ "body": "The fee is 150.0%\nthe send amount.\nProceed?",
+ "longtouch": true
+ },
+ {
+ "type": "status",
+ "title": "Transaction",
+ "body": "confirmed"
+ }
+ ]
}
],
"expected_signatures": [
### src/rust/bitbox02-rust/src/hal/testing/ui.rs
@@ -72,6 +72,7 @@ pub struct TestingUi<'a> {
_abort_nth: Option<usize>,
pub screens: Vec<Screen>,
pub confirm_display_sizes: Vec<usize>,
+ pub confirm_scrollable: Vec<bool>,
progress_screens: Rc<RefCell<Vec<ProgressScreen>>>,
_enter_string: Option<EnterStringCb<'a>>,
_menu: Option<MenuCb<'a>>,
@@ -133,6 +134,7 @@ impl Ui for TestingUi<'_> {
async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort> {
self.confirm_display_sizes.push(params.display_size);
+ self.confirm_scrollable.push(params.scrollable);
self.screens.push(Screen::Confirm {
title: params.title.into(),
body: params.body.into(),
@@ -301,6 +303,7 @@ impl<'a> TestingUi<'a> {
Self {
screens: vec![],
confirm_display_sizes: vec![],
+ confirm_scrollable: vec![],
progress_screens: Rc::new(RefCell::new(vec![])),
_abort_nth: None,
_enter_string: None,
### src/rust/bitbox02-rust/src/hww/api/payment_request.rs
@@ -152,7 +152,8 @@ pub async fn user_verify(
hal.ui()
.confirm(&ConfirmParams {
title: "",
- body: &format!("Memo from\n\n{}", payment_request.recipient_name),
+ body: &format!("Memo from: {}", payment_request.recipient_name),
+ scrollable: true,
accept_is_nextarrow: true,
..Default::default()
})
@@ -1552,7 +1553,7 @@ mod tests {
},
Screen::Confirm {
title: "".into(),
- body: "Memo from\n\nPOCKET".into(),
+ body: "Memo from: POCKET".into(),
longtouch: false,
},
Screen::Confirm {
@@ -1562,6 +1563,36 @@ mod tests {
},
]
);
+ assert_eq!(mock_hal.ui.confirm_scrollable, vec![true, true]);
+ }
+
+ #[async_test::test]
+ async fn test_user_verify_long_recipient_name_scrollable() {
+ let recipient_name = format!("SWAPKIT ({})", "a".repeat(200));
+ let mut mock_hal = TestingHal::new();
+ user_verify(
+ &mut mock_hal,
+ &pb::BtcPaymentRequestRequest {
+ recipient_name: recipient_name.clone(),
+ memos: vec![make_text_memo("Swap memo")],
+ nonce: vec![],
+ total_amount: 1234567890,
+ signature: vec![],
+ },
+ "12.34567890 BTC",
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(
+ mock_hal.ui.screens[1],
+ Screen::Confirm {
+ title: "".into(),
+ body: format!("Memo from: {recipient_name}"),
+ longtouch: false,
+ }
+ );
+ assert!(mock_hal.ui.confirm_scrollable[0]);
}
#[cfg(feature = "app-ethereum")]
### versions.json
@@ -1,5 +1,5 @@
{
- "firmware": "v9.27.1",
+ "firmware": "v9.27.2",
"bootloader": "v1.2.2",
"stage0": 1
}Why this scored 23/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.