diff --git a/src/rust/bitbox02-rust/src/hal/testing/ui.rs b/src/rust/bitbox02-rust/src/hal/testing/ui.rs index 6437fe459a..3bd118c52d 100644 --- a/src/rust/bitbox02-rust/src/hal/testing/ui.rs +++ b/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, PartialEq, Clone)] +pub struct ProgressScreen { + pub title: String, + pub values: Vec, +} + type EnterStringCb<'a> = Box) -> Result + 'a>; type MenuCb<'a> = Box) -> Result + 'a>; type TrinaryChoiceCb<'a> = @@ -64,16 +72,24 @@ pub struct TestingUi<'a> { _abort_nth: Option, pub screens: Vec, pub confirm_display_sizes: Vec, + progress_screens: Rc>>, _enter_string: Option>, _menu: Option>, _trinary_choice: Option>, _quiz_choices: VecDeque, } -pub struct NoopProgress; +pub struct TestingProgress { + progress_screens: Rc>>, + index: usize, +} -impl Progress for NoopProgress { - fn set(&mut self, _progress: f32) {} +impl Progress for TestingProgress { + fn set(&mut self, progress: f32) { + self.progress_screens.borrow_mut()[self.index] + .values + .push(progress); + } } 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 { + 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) { diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rs index 2e10622d76..be2cc1271e 100644 --- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rs +++ b/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>, 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 DataProducer for ProgressProducer<'_, P> { + fn len(&self) -> u32 { + self.producer.len() + } + + fn first_byte<'a>(&'a mut self) -> Pin> + 'a>> { + self.producer.first_byte() + } + + fn next<'a>( + &'a mut self, + ) -> Pin>, 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(self.consumed as f32 / total as f32); + } + } + Ok(chunk) + }) + } +} + pub struct Preview { cap: usize, bytes: Vec, diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs index d28995a41e..324a7f10a8 100644 --- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs +++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs @@ -281,6 +281,7 @@ struct PreparedStreamingStandardData { } async fn prepare_streaming_standard_data( + hal: &mut impl crate::hal::Hal, chain_id: u64, request: &Transaction<'_>, ) -> Result { @@ -288,11 +289,17 @@ async fn prepare_streaming_standard_data( 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()), @@ -439,7 +446,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,16 @@ mod tests { clear_chunk_responder, setup_chunk_responder, setup_counting_chunk_responder, }; + fn assert_progress_screen(mock_hal: &TestingHal<'_>, expected_values: &[f32]) { + 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.len(), expected_values.len()); + for (actual, expected) in progress_screens[0].values.iter().zip(expected_values) { + assert!((actual - expected).abs() < 1e-6); + } + } + // Base payment request fixture for ETH-side swap tests. fn make_eth_swap_payment_request() -> pb::BtcPaymentRequestRequest { pb::BtcPaymentRequestRequest { @@ -2034,6 +2051,7 @@ mod tests { } _ => panic!("unexpected screen"), } + assert!(mock_hal.ui.progress_screens().is_empty()); } #[async_test::test] @@ -2070,6 +2088,7 @@ mod tests { })) ); clear_chunk_responder(); + assert_progress_screen(&mock_hal, &[4096.0 / 10_000.0, 8192.0 / 10_000.0, 1.0]); assert_eq!(mock_hal.ui.confirm_display_sizes, vec![0, 0, 0, 0, 10_000]); assert_eq!( mock_hal.ui.screens[0], @@ -2167,6 +2186,7 @@ mod tests { } other => panic!("expected Ok(Sign), got {:?}", other), } + assert_progress_screen(&mock_hal, &[1.0]); } #[async_test::test] @@ -2203,6 +2223,7 @@ mod tests { })) ); clear_chunk_responder(); + assert_progress_screen(&mock_hal, &[4096.0 / 12_000.0, 8192.0 / 12_000.0, 1.0]); assert_eq!(mock_hal.ui.confirm_display_sizes, vec![0, 0, 0, 0, 12_000]); assert_eq!( mock_hal.ui.screens, diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs index 76f6dbc75f..154832e27a 100644 --- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs +++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs @@ -352,8 +352,13 @@ async fn encode_member( 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()); @@ -672,6 +677,16 @@ mod tests { use pb::eth_sign_typed_message_request::Member; + fn assert_progress_screen(mock_hal: &TestingHal<'_>, expected_values: &[f32]) { + 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.len(), expected_values.len()); + for (actual, expected) in progress_screens[0].values.iter().zip(expected_values) { + assert!((actual - expected).abs() < 1e-6); + } + } + fn mk_type(data_type: DataType) -> MemberType { MemberType { r#type: data_type as _, @@ -1183,6 +1198,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![ @@ -1215,6 +1231,7 @@ mod tests { let data: Vec = (0u8..=255).cycle().take(10_000).collect(); let mock_hal = run_single_streaming_bytes_message(data).await; + assert_progress_screen(&mock_hal, &[4096.0 / 10_000.0, 8192.0 / 10_000.0, 1.0]); assert_eq!(mock_hal.ui.confirm_display_sizes, vec![0, 0, 10_000]); assert_eq!( mock_hal.ui.screens[1], @@ -1233,6 +1250,15 @@ 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(mk_type(DataType::Bytes), Object::Bytes(data)).await; + + assert!(mock_hal.ui.progress_screens().is_empty()); + } + #[test] fn test_encode_type() { assert_eq!(