Merge commit 'refs/pull/1976/head' of https://github.com/BitBoxSwiss/bitbox02-firmware
What changed, and why it matters
This commit adds a progress bar that appears while the BitBox02 is loading large Ethereum transaction data from a connected computer. It is a user-experience improvement, not a security fix. The code only changes how progress is displayed during normal data streaming and adds tests to verify the progress bar shows the right percentages.
No security action needed. Treat as a normal feature/UX commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The merge introduces a ProgressProducer wrapper around the existing DataProducer used for Ethereum transaction data and EIP-712 typed-message bytes. It calls the device’s UI progress callback as chunks arrive, giving users feedback during long streaming operations. The change also extends the mock/testing UI so unit tests can assert which progress screens were created and what fraction values were reported. No cryptographic, parsing, authorization, or memory-safety logic is modified.
Changed components
Ethereum transaction signing UI flowEIP-712 typed-message signing UI flowTest/mock UI harnessInspect captured patch +139 / −13
### src/rust/bitbox02-rust/src/hal/testing/ui.rs
@@ -7,8 +7,10 @@ use crate::hal::ui::{
use alloc::boxed::Box;
use alloc::collections::VecDeque;
+use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec::Vec;
+use core::cell::RefCell;
use core::time::Duration;
#[derive(Debug, Eq, PartialEq, Clone)]
@@ -53,6 +55,12 @@ pub enum Screen {
More,
}
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub struct ProgressScreen {
+ pub title: String,
+ pub values: Vec<(u32, u32)>,
+}
+
type EnterStringCb<'a> = Box<dyn FnMut(&EnterStringParams<'_>) -> Result<String, UserAbort> + 'a>;
type MenuCb<'a> = Box<dyn FnMut(&[&str], Option<&str>) -> Result<u8, UserAbort> + 'a>;
type TrinaryChoiceCb<'a> =
@@ -64,16 +72,24 @@ pub struct TestingUi<'a> {
_abort_nth: Option<usize>,
pub screens: Vec<Screen>,
pub confirm_display_sizes: Vec<usize>,
+ progress_screens: Rc<RefCell<Vec<ProgressScreen>>>,
_enter_string: Option<EnterStringCb<'a>>,
_menu: Option<MenuCb<'a>>,
_trinary_choice: Option<TrinaryChoiceCb<'a>>,
_quiz_choices: VecDeque<u8>,
}
-pub struct NoopProgress;
+pub struct TestingProgress {
+ progress_screens: Rc<RefCell<Vec<ProgressScreen>>>,
+ index: usize,
+}
-impl Progress for NoopProgress {
- fn set_fraction(&mut self, _numerator: u32, _denominator: u32) {}
+impl Progress for TestingProgress {
+ fn set_fraction(&mut self, numerator: u32, denominator: u32) {
+ self.progress_screens.borrow_mut()[self.index]
+ .values
+ .push((numerator, denominator));
+ }
}
pub struct NoopEmpty;
@@ -83,12 +99,23 @@ impl Empty for NoopEmpty {}
pub struct NoopUnlockAnimation;
impl Ui for TestingUi<'_> {
- type Progress = NoopProgress;
+ type Progress = TestingProgress;
type Empty = NoopEmpty;
type UnlockAnimation = NoopUnlockAnimation;
- fn progress_create(&mut self, _title: &str) -> Self::Progress {
- NoopProgress
+ fn progress_create(&mut self, title: &str) -> Self::Progress {
+ let index = {
+ let mut progress_screens = self.progress_screens.borrow_mut();
+ progress_screens.push(ProgressScreen {
+ title: title.into(),
+ values: vec![],
+ });
+ progress_screens.len() - 1
+ };
+ TestingProgress {
+ progress_screens: self.progress_screens.clone(),
+ index,
+ }
}
fn empty_create(&mut self) -> Self::Empty {
@@ -274,6 +301,7 @@ impl<'a> TestingUi<'a> {
Self {
screens: vec![],
confirm_display_sizes: vec![],
+ progress_screens: Rc::new(RefCell::new(vec![])),
_abort_nth: None,
_enter_string: None,
_menu: None,
@@ -282,6 +310,10 @@ impl<'a> TestingUi<'a> {
}
}
+ pub fn progress_screens(&self) -> Vec<ProgressScreen> {
+ self.progress_screens.borrow().clone()
+ }
+
/// Make the `n`-th workflow (0-indexed) fail with a user abort. If that workflow cannot be
/// aborted, there will be panic.
pub fn abort_nth(&mut self, n: usize) {
### src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rs
@@ -11,6 +11,8 @@ use core::pin::Pin;
use alloc::boxed::Box;
use alloc::vec::Vec;
+use crate::hal::ui::Progress;
+
use super::Error;
/// An async producer/generator of a bytes array. This is used to be able to accumulate the RLP hash
@@ -26,6 +28,48 @@ pub trait DataProducer {
-> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Error>> + 'a>>;
}
+pub struct ProgressProducer<'a, P: Progress> {
+ producer: &'a mut dyn DataProducer,
+ progress: &'a mut P,
+ consumed: u32,
+}
+
+impl<'a, P: Progress> ProgressProducer<'a, P> {
+ pub fn new(producer: &'a mut dyn DataProducer, progress: &'a mut P) -> Self {
+ Self {
+ producer,
+ progress,
+ consumed: 0,
+ }
+ }
+}
+
+impl<P: Progress> DataProducer for ProgressProducer<'_, P> {
+ fn len(&self) -> u32 {
+ self.producer.len()
+ }
+
+ fn first_byte<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<u8, Error>> + 'a>> {
+ self.producer.first_byte()
+ }
+
+ fn next<'a>(
+ &'a mut self,
+ ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Error>> + 'a>> {
+ Box::pin(async move {
+ let chunk = self.producer.next().await?;
+ if let Some(chunk) = &chunk {
+ let total = self.producer.len();
+ self.consumed = self.consumed.saturating_add(chunk.len() as u32).min(total);
+ if total > 0 {
+ self.progress.set_fraction(self.consumed, total);
+ }
+ }
+ Ok(chunk)
+ })
+ }
+}
+
pub struct Preview {
cap: usize,
bytes: Vec<u8>,
### src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
@@ -286,18 +286,25 @@ struct PreparedStreamingStandardData {
}
async fn prepare_streaming_standard_data(
+ hal: &mut impl crate::hal::Hal,
chain_id: u64,
request: &Transaction<'_>,
) -> Result<PreparedStreamingStandardData, Error> {
let display_size = request.data_length() as usize;
let display_cap = truncating_hex_preview_byte_cap(0, display_size);
let mut producer = super::sighash::ChunkingProducer::from_host(request.data_length())
.with_preview(display_cap);
- let hash = match request {
- Transaction::Legacy(legacy) => {
- hash_legacy_with_producer(chain_id, legacy, &mut producer).await?
+ let hash = {
+ let mut progress = hal.ui().progress_create("Loading data...");
+ let mut producer = super::sighash::ProgressProducer::new(&mut producer, &mut progress);
+ match request {
+ Transaction::Legacy(legacy) => {
+ hash_legacy_with_producer(chain_id, legacy, &mut producer).await?
+ }
+ Transaction::Eip1559(eip1559) => {
+ hash_eip1559_with_producer(eip1559, &mut producer).await?
+ }
}
- Transaction::Eip1559(eip1559) => hash_eip1559_with_producer(eip1559, &mut producer).await?,
};
Ok(PreparedStreamingStandardData {
body: hex::encode(producer.preview()),
@@ -444,7 +451,7 @@ async fn verify_standard_transaction(
.await?;
let (display_size, body) = if data_length > 0 {
- let prepared = prepare_streaming_standard_data(params.chain_id, request).await?;
+ let prepared = prepare_streaming_standard_data(hal, params.chain_id, request).await?;
let display_size = prepared.display_size;
let body = prepared.body.clone();
prepared_streaming_data = Some(prepared);
@@ -663,6 +670,13 @@ mod tests {
clear_chunk_responder, setup_chunk_responder, setup_counting_chunk_responder,
};
+ fn assert_progress_screen(mock_hal: &TestingHal<'_>, expected_values: &[(u32, u32)]) {
+ let progress_screens = mock_hal.ui.progress_screens();
+ assert_eq!(progress_screens.len(), 1);
+ assert_eq!(progress_screens[0].title, "Loading data...");
+ assert_eq!(progress_screens[0].values.as_slice(), expected_values);
+ }
+
// Base payment request fixture for ETH-side swap tests.
fn make_eth_swap_payment_request() -> pb::BtcPaymentRequestRequest {
pb::BtcPaymentRequestRequest {
@@ -1967,6 +1981,7 @@ mod tests {
}
_ => panic!("unexpected screen"),
}
+ assert!(mock_hal.ui.progress_screens().is_empty());
}
#[async_test::test]
@@ -2003,6 +2018,10 @@ mod tests {
}))
);
clear_chunk_responder();
+ assert_progress_screen(
+ &mock_hal,
+ &[(4096, 10_000), (8192, 10_000), (10_000, 10_000)],
+ );
assert_eq!(mock_hal.ui.confirm_display_sizes, vec![0, 0, 0, 0, 10_000]);
assert_eq!(
mock_hal.ui.screens[0],
@@ -2133,6 +2152,10 @@ mod tests {
}))
);
clear_chunk_responder();
+ assert_progress_screen(
+ &mock_hal,
+ &[(4096, 12_000), (8192, 12_000), (12_000, 12_000)],
+ );
assert_eq!(mock_hal.ui.confirm_display_sizes, vec![0, 0, 0, 0, 12_000]);
assert_eq!(
mock_hal.ui.screens,
### src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs
@@ -460,8 +460,13 @@ async fn encode_member<U: sha3::digest::Update>(
let mut producer = super::sighash::ChunkingProducer::from_host(req.data_length)
.with_preview(display_cap);
let mut keccak = sha3::Keccak256::new();
- while let Some(chunk) = producer.next().await? {
- keccak.update(&chunk);
+ {
+ let mut progress = hal.ui().progress_create("Loading data...");
+ let mut producer =
+ super::sighash::ProgressProducer::new(&mut producer, &mut progress);
+ while let Some(chunk) = producer.next().await? {
+ keccak.update(&chunk);
+ }
}
hasher.update(&keccak.finalize());
@@ -813,6 +818,13 @@ mod tests {
use pb::eth_sign_typed_message_request::Member;
+ fn assert_progress_screen(mock_hal: &TestingHal<'_>, expected_values: &[(u32, u32)]) {
+ let progress_screens = mock_hal.ui.progress_screens();
+ assert_eq!(progress_screens.len(), 1);
+ assert_eq!(progress_screens[0].title, "Loading data...");
+ assert_eq!(progress_screens[0].values.as_slice(), expected_values);
+ }
+
fn mk_type(data_type: DataType) -> MemberType {
MemberType {
r#type: data_type as _,
@@ -1455,6 +1467,7 @@ mod tests {
let line2 = "b".repeat(MAX_CONFIRM_BODY_SIZE);
let mock_hal = run_single_string_message(format!("ok\n{line2}")).await;
+ assert!(mock_hal.ui.progress_screens().is_empty());
assert_eq!(
mock_hal.ui.screens,
vec![
@@ -1516,6 +1529,10 @@ mod tests {
let data: Vec<u8> = (0u8..=255).cycle().take(10_000).collect();
let mock_hal = run_single_streaming_bytes_message(data).await;
+ assert_progress_screen(
+ &mock_hal,
+ &[(4096, 10_000), (8192, 10_000), (10_000, 10_000)],
+ );
assert_eq!(mock_hal.ui.confirm_display_sizes, vec![0, 0, 10_000]);
assert_eq!(
mock_hal.ui.screens[1],
@@ -1534,6 +1551,16 @@ mod tests {
}
}
+ #[async_test::test]
+ async fn test_inline_bytes_no_progress() {
+ let data: &'static [u8] = Box::leak(vec![0x01, 0x02, 0x03].into_boxed_slice());
+ let mock_hal =
+ run_single_message_typed_msg("data", mk_type(DataType::Bytes), Object::Bytes(data))
+ .await;
+
+ assert!(mock_hal.ui.progress_screens().is_empty());
+ }
+
#[test]
fn test_encode_type() {
assert_eq!(Why this scored 15/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.