Skip to content

Commit f02e81f

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 f02e81f

File tree

13 files changed

+657
-2
lines changed

13 files changed

+657
-2
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
.ccls-cache
1010
compile_commands.json
1111
compile_flags.txt
12+
.vscode
1213

1314
# gnu global
1415
/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/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

+33
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use super::trinary_input_string;
2222

2323
use alloc::boxed::Box;
2424
use alloc::string::String;
25+
use alloc::string::ToString;
2526
use alloc::vec::Vec;
2627
use core::cell::RefCell;
2728

@@ -107,6 +108,7 @@ async fn confirm_word(choices: &[&str], title: &str) -> Result<u8, CancelError>
107108
with_cancel("Recovery\nwords", &mut component, &result).await
108109
}
109110

111+
#[cfg(not(feature = "c-unit-testing"))]
110112
pub async fn show_and_confirm_mnemonic(words: &[&str]) -> Result<(), CancelError> {
111113
// Part 1) Scroll through words
112114
show_mnemonic(words).await?;
@@ -140,6 +142,25 @@ pub async fn show_and_confirm_mnemonic(words: &[&str]) -> Result<(), CancelError
140142
Ok(())
141143
}
142144

145+
#[cfg(feature = "c-unit-testing")]
146+
pub async fn show_and_confirm_mnemonic(words: &[&str]) -> Result<(), CancelError> {
147+
let _ = confirm::confirm(&confirm::Params {
148+
title: "",
149+
body: "Please confirm\neach word",
150+
accept_only: true,
151+
accept_is_nextarrow: true,
152+
..Default::default()
153+
})
154+
.await;
155+
156+
for word in words.iter() {
157+
bitbox02::println_stdout(word);
158+
}
159+
bitbox02::println_stdout("Words confirmed");
160+
161+
Ok(())
162+
}
163+
143164
/// Given 11/17/23 initial words, this function returns a list of candidate words for the last word,
144165
/// such that the resulting bip39 phrase has a valid checksum. There are always exactly 8 such words
145166
/// for 24 word mnemonics, 32 words for 18 word mnemonics and 128 words for 12 word mnemonics.
@@ -289,6 +310,7 @@ async fn get_12th_18th_word(
289310
}
290311
}
291312

313+
#[cfg(not(feature = "c-unit-testing"))]
292314
/// Retrieve a BIP39 mnemonic sentence of 12, 18 or 24 words from the user.
293315
pub async fn get() -> Result<zeroize::Zeroizing<String>, CancelError> {
294316
let num_words: usize = match choose("How many words?", "12", "18", "24").await {
@@ -403,6 +425,17 @@ pub async fn get() -> Result<zeroize::Zeroizing<String>, CancelError> {
403425
))
404426
}
405427

428+
#[cfg(feature = "c-unit-testing")]
429+
pub async fn get() -> Result<zeroize::Zeroizing<String>, CancelError> {
430+
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";
431+
bitbox02::println_stdout("Restored from recovery words below:");
432+
bitbox02::println_stdout(words);
433+
return Ok(zeroize::Zeroizing::new(
434+
words
435+
.to_string()
436+
));
437+
}
438+
406439
#[cfg(test)]
407440
mod tests {
408441
use super::*;

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(feature = "c-unit-testing", path = "ui/ui_stub_c.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/ui_stub_c.rs

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright 2020 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+
//! Stubs for testing.
16+
17+
pub use super::types::{
18+
AcceptRejectCb, ConfirmParams, ContinueCancelCb, Font, MenuParams, SelectWordCb, TrinaryChoice,
19+
TrinaryChoiceCb, TrinaryInputStringParams,
20+
};
21+
22+
use crate::input::SafeInputString;
23+
24+
use core::marker::PhantomData;
25+
26+
extern crate alloc;
27+
28+
pub struct Component<'a> {
29+
is_pushed: bool,
30+
_p: PhantomData<&'a ()>,
31+
}
32+
33+
impl<'a> Component<'a> {
34+
pub fn screen_stack_push(&mut self) {
35+
if self.is_pushed {
36+
panic!("component pushed twice");
37+
}
38+
self.is_pushed = true;
39+
}
40+
}
41+
42+
impl<'a> Drop for Component<'a> {
43+
fn drop(&mut self) {
44+
if !self.is_pushed {
45+
panic!("component not pushed");
46+
}
47+
}
48+
}
49+
50+
pub fn trinary_input_string_create<'a, F>(
51+
_params: &TrinaryInputStringParams,
52+
mut confirm_callback: F,
53+
_cancel_callback: Option<ContinueCancelCb<'a>>,
54+
) -> Component<'a>
55+
where
56+
F: FnMut(SafeInputString) + 'a,
57+
{
58+
confirm_callback(SafeInputString::new());
59+
Component {
60+
is_pushed: false,
61+
_p: PhantomData,
62+
}
63+
}
64+
65+
pub fn confirm_create<'a, F>(_params: &ConfirmParams, mut result_callback: F) -> Component<'a>
66+
where
67+
F: FnMut(bool) + 'a,
68+
{
69+
result_callback(true);
70+
Component {
71+
is_pushed: false,
72+
_p: PhantomData,
73+
}
74+
}
75+
76+
pub fn screen_process() {}
77+
78+
pub fn status_create<'a, F>(_text: &str, _status_success: bool, mut callback: F) -> Component<'a>
79+
where
80+
F: FnMut() + 'a,
81+
{
82+
callback();
83+
Component {
84+
is_pushed: false,
85+
_p: PhantomData,
86+
}
87+
}
88+
89+
pub fn sdcard_create<'a, F>(_insert: bool, mut continue_callback: F) -> Component<'a>
90+
where
91+
F: FnMut() + 'a,
92+
{
93+
continue_callback();
94+
Component {
95+
is_pushed: false,
96+
_p: PhantomData,
97+
}
98+
}
99+
100+
pub fn menu_create(_params: MenuParams<'_>) -> Component<'_> {
101+
panic!("not implemented");
102+
}
103+
104+
pub fn trinary_choice_create<'a>(
105+
_message: &'a str,
106+
_label_left: &'a str,
107+
_label_middle: &'a str,
108+
_label_right: &'a str,
109+
_chosen_callback: TrinaryChoiceCb,
110+
) -> Component<'a> {
111+
panic!("not implemented")
112+
}
113+
114+
pub fn confirm_transaction_address_create<'a, 'b>(
115+
_amount: &'a str,
116+
_address: &'a str,
117+
mut callback: AcceptRejectCb<'b>,
118+
) -> Component<'b> {
119+
callback(true);
120+
Component {
121+
is_pushed: false,
122+
_p: PhantomData,
123+
}
124+
}
125+
126+
pub fn confirm_transaction_fee_create<'a, 'b>(
127+
_amount: &'a str,
128+
_fee: &'a str,
129+
_longtouch: bool,
130+
mut callback: AcceptRejectCb<'b>,
131+
) -> Component<'b> {
132+
callback(true);
133+
Component {
134+
is_pushed: false,
135+
_p: PhantomData,
136+
}
137+
}
138+
139+
pub fn trinary_input_string_set_input(_component: &mut Component, _word: &str) {
140+
panic!("not implemented")
141+
}
142+
143+
pub fn with_lock_animation<F: Fn()>(f: F) {
144+
f()
145+
}
146+
147+
pub fn screen_stack_pop_all() {}
148+
149+
pub fn progress_create<'a>(_title: &str) -> Component<'a> {
150+
Component {
151+
is_pushed: false,
152+
_p: PhantomData,
153+
}
154+
}
155+
156+
pub fn progress_set(_component: &mut Component, _progress: f32) {}
157+
158+
pub fn empty_create<'a>() -> Component<'a> {
159+
Component {
160+
is_pushed: false,
161+
_p: PhantomData,
162+
}
163+
}

test/.DS_Store

10 KB
Binary file not shown.

0 commit comments

Comments
 (0)