Skip to content

Commit 49f512c

Browse files
committed
simulator: implement a simulator for bitbox02 device
HWI is thinking of updating its support policy such that supported wallets must implement a simulator/emulator. See bitcoin-core/HWI#685. That's why a simulator is implemented for bitbox02, supporting functionalities of its API. This first version of the simulator is capable of nearly every functionality of a normal Bitbox02 device, without promising any security or production use. Its main aim is to be able to run unit tests for features and test the API. In addition, it will be configured to run automated tests in CI, which helps both us and HWI integration. Right now, the simulator has 3 different ways to communicate with a client: giving inputs/getting output from CLI, using pipes or opening sockets. Socket is the most convenient and reliable choice in this version. It expects the clients to open a socket on port 15432, which is selected intentionally to avoid possible conflicts. The simulator resides with C unit-tests since it uses same mocks, therefore it can be built by `make unit-test`. Lastly, Python client implemented in `py/send_message.py` is updated to support communicating with simulator with the socket configuration mentioned above. Client can be started up with `./py/send_message.py --simulator` command. To run the simulator, `build-build/bin/test_simulator` command is sufficient. Signed-off-by: asi345 <[email protected]>
1 parent 3c0e9ac commit 49f512c

15 files changed

+467
-8
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
.ccls-cache
1010
compile_commands.json
1111
compile_flags.txt
12+
.vscode
13+
.DS_Store
1214

1315
# gnu global
1416
/src/GPATH

py/send_message.py

+69
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# pylint: disable=too-many-lines
1919

2020
import argparse
21+
import socket
2122
import pprint
2223
import sys
2324
from typing import List, Any, Optional, Callable, Union, Tuple, Sequence
@@ -41,6 +42,7 @@
4142
FirmwareVersionOutdatedException,
4243
u2fhid,
4344
bitbox_api_protocol,
45+
PhysicalLayer,
4446
)
4547

4648
import u2f
@@ -1556,6 +1558,65 @@ def run(self) -> int:
15561558
return 0
15571559

15581560

1561+
def connect_to_simulator_bitbox(debug: bool) -> int:
1562+
"""
1563+
Connects and runs the main menu on host computer,
1564+
simulating a BitBox02 connected over USB.
1565+
"""
1566+
1567+
class Simulator(PhysicalLayer):
1568+
"""
1569+
Simulator class handles the communication
1570+
with the firmware simulator
1571+
"""
1572+
1573+
def __init__(self) -> None:
1574+
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1575+
port = 15423
1576+
self.client_socket.bind(("", port))
1577+
self.client_socket.listen(50)
1578+
print(f"Waiting for connection on port {port}")
1579+
self.connection, addr = self.client_socket.accept()
1580+
print(f"Connected to {addr}")
1581+
1582+
def write(self, data: bytes) -> None:
1583+
self.connection.send(data[1:])
1584+
if debug:
1585+
print(f"Written to the simulator:\n{data.hex()[2:]}")
1586+
1587+
def read(self, size: int, timeout_ms: int) -> bytes:
1588+
res = self.connection.recv(64)
1589+
if debug:
1590+
print(f"Read from the simulator:\n{res.hex()}")
1591+
return res
1592+
1593+
def __del__(self) -> None:
1594+
print("Simulator quit")
1595+
if self.connection:
1596+
self.connection.shutdown(socket.SHUT_RDWR)
1597+
self.connection.close()
1598+
1599+
simulator = Simulator()
1600+
1601+
device_info: devices.DeviceInfo = {
1602+
"serial_number": "v9.16.0",
1603+
"path": b"",
1604+
"product_string": "BitBox02BTC",
1605+
}
1606+
noise_config = bitbox_api_protocol.BitBoxNoiseConfig()
1607+
bitbox_connection = bitbox02.BitBox02(
1608+
transport=u2fhid.U2FHid(simulator),
1609+
device_info=device_info,
1610+
noise_config=noise_config,
1611+
)
1612+
try:
1613+
bitbox_connection.check_min_version()
1614+
except FirmwareVersionOutdatedException as exc:
1615+
print("WARNING: ", exc)
1616+
1617+
return SendMessage(bitbox_connection, debug).run()
1618+
1619+
15591620
def connect_to_usb_bitbox(debug: bool, use_cache: bool) -> int:
15601621
"""
15611622
Connects and runs the main menu on a BitBox02 connected
@@ -1643,6 +1704,11 @@ def main() -> int:
16431704
parser = argparse.ArgumentParser(description="Tool for communicating with bitbox device")
16441705
parser.add_argument("--debug", action="store_true", help="Print messages sent and received")
16451706
parser.add_argument("--u2f", action="store_true", help="Use u2f menu instead")
1707+
parser.add_argument(
1708+
"--simulator",
1709+
action="store_true",
1710+
help="Connect to the BitBox02 simulator instead of a real BitBox02",
1711+
)
16461712
parser.add_argument(
16471713
"--no-cache", action="store_true", help="Don't use cached or store noise keys"
16481714
)
@@ -1663,6 +1729,9 @@ def main() -> int:
16631729
return u2fapp.run()
16641730
return 1
16651731

1732+
if args.simulator:
1733+
return connect_to_simulator_bitbox(args.debug)
1734+
16661735
return connect_to_usb_bitbox(args.debug, not args.no_cache)
16671736

16681737

src/rust/bitbox02-rust/src/workflow.rs

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
pub mod cancel;
1616
pub mod confirm;
1717
pub mod menu;
18+
#[cfg_attr(feature = "c-unit-testing", path = "workflow/mnemonic_c_unit_tests.rs")]
1819
pub mod mnemonic;
1920
pub mod pairing;
2021
pub mod password;

src/rust/bitbox02-rust/src/workflow/confirm.rs

+2
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,7 @@ pub async fn confirm(params: &Params<'_>) -> Result<(), UserAbort> {
3131
};
3232
});
3333
component.screen_stack_push();
34+
#[cfg(feature = "c-unit-testing")]
35+
bitbox02::print_stdout(&format!("CONFIRM SCREEN START\nTITLE: {}\nBODY: {}\nCONFIRM SCREEN END\n", params.title, params.body));
3436
option_no_screensaver(&result).await
3537
}

src/rust/bitbox02-rust/src/workflow/mnemonic.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -502,4 +502,4 @@ mod tests {
502502
&bruteforce_lastword(&mnemonic)
503503
);
504504
}
505-
}
505+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2024 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
pub use super::cancel::Error as CancelError;
16+
use super::confirm;
17+
18+
use alloc::string::String;
19+
use alloc::string::ToString;
20+
21+
pub async fn show_and_confirm_mnemonic(words: &[&str]) -> Result<(), CancelError> {
22+
let _ = confirm::confirm(&confirm::Params {
23+
title: "",
24+
body: "Please confirm\neach word",
25+
accept_only: true,
26+
accept_is_nextarrow: true,
27+
..Default::default()
28+
})
29+
.await;
30+
31+
for word in words.iter() {
32+
bitbox02::println_stdout(word);
33+
}
34+
bitbox02::println_stdout("Words confirmed");
35+
36+
Ok(())
37+
}
38+
39+
pub async fn get() -> Result<zeroize::Zeroizing<String>, CancelError> {
40+
let words = "boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide";
41+
bitbox02::println_stdout("Restored from recovery words below:");
42+
bitbox02::println_stdout(words);
43+
44+
Ok(zeroize::Zeroizing::new(
45+
words
46+
.to_string()
47+
))
48+
}

src/rust/bitbox02-rust/src/workflow/status.rs

+2
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ pub async fn status(title: &str, status_success: bool) {
2121
*result.borrow_mut() = Some(());
2222
});
2323
component.screen_stack_push();
24+
#[cfg(feature = "c-unit-testing")]
25+
bitbox02::print_stdout(&format!("STATUS SCREEN START\nTITLE: {}\nSTATUS SCREEN END\n", title));
2426
option_no_screensaver(&result).await
2527
}

src/rust/bitbox02-rust/src/workflow/trinary_input_string.rs

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub async fn enter(
4949
bitbox02::ui::trinary_input_string_set_input(&mut component, preset);
5050
}
5151
component.screen_stack_push();
52+
#[cfg(feature = "c-unit-testing")]
53+
bitbox02::print_stdout(&format!("ENTER SCREEN START\nTITLE: {}\nENTER SCREEN END\n", params.title));
5254
option(&result)
5355
.await
5456
.or(Err(super::cancel::Error::Cancelled))

src/rust/bitbox02/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,14 @@ pub fn print_stdout(msg: &str) {
183183
}
184184
}
185185

186+
#[cfg(any(feature = "testing", feature = "c-unit-testing"))]
187+
pub fn println_stdout(msg: &str) {
188+
unsafe {
189+
bitbox02_sys::printf(crate::util::str_to_cstr_vec(msg).unwrap().as_ptr());
190+
bitbox02_sys::printf(crate::util::str_to_cstr_vec("\n").unwrap().as_ptr());
191+
}
192+
}
193+
186194
#[cfg(test)]
187195
mod tests {
188196
use super::*;

src/rust/bitbox02/src/ui.rs

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
mod types;
1717

1818
#[cfg_attr(feature = "testing", path = "ui/ui_stub.rs")]
19+
#[cfg_attr(not(feature = "testing"), cfg_attr(feature = "c-unit-testing", path = "ui/ui_stub_c_unit_tests.rs"))]
1920
// We don't actually use ui::ui anywhere, we re-export below.
2021
#[allow(clippy::module_inception)]
2122
mod ui;

src/rust/bitbox02/src/ui/types.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use util::Survive;
2121
pub use bitbox02_sys::trinary_choice_t as TrinaryChoice;
2222

2323
// Taking the constant straight from C, as it's excluding the null terminator.
24-
#[cfg_attr(feature = "testing", allow(dead_code))]
24+
#[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))]
2525
pub(crate) const MAX_LABEL_SIZE: usize = bitbox02_sys::MAX_LABEL_SIZE as _;
2626

2727
#[derive(Default)]
@@ -33,7 +33,7 @@ pub enum Font {
3333
}
3434

3535
impl Font {
36-
#[cfg_attr(feature = "testing", allow(dead_code))]
36+
#[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))]
3737
pub(crate) fn as_ptr(&self) -> *const bitbox02_sys::UG_FONT {
3838
match self {
3939
Font::Default => core::ptr::null() as *const _,
@@ -65,7 +65,7 @@ pub struct ConfirmParams<'a> {
6565
}
6666

6767
impl<'a> ConfirmParams<'a> {
68-
#[cfg_attr(feature = "testing", allow(dead_code))]
68+
#[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))]
6969
/// `title_scratch` and `body_scratch` exist to keep the data
7070
/// alive for as long as the C params live.
7171
pub(crate) fn to_c_params(
@@ -110,7 +110,7 @@ pub struct TrinaryInputStringParams<'a> {
110110
}
111111

112112
impl<'a> TrinaryInputStringParams<'a> {
113-
#[cfg_attr(feature = "testing", allow(dead_code))]
113+
#[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))]
114114
pub(crate) fn to_c_params(
115115
&self,
116116
title_scratch: &'a mut Vec<u8>,

0 commit comments

Comments
 (0)