From 5f31cf8360671bd9714483e3c68fe5843b2cdc71 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:35:02 +0100 Subject: [PATCH 1/9] Add 'comm' module to the app sdk This adds a simple overlay protocol on top of xsend and xrecv. receive_message receive a length-prefixed message, while always calling xrecv and xsend with fixed lengths. This allows apps to receive messages of arbitrary lengths, while avoiding allocating large buffers if not necessary. --- app-sdk/src/comm.rs | 151 +++++++++++++++++++++++++++++++++++++++++ app-sdk/src/lib.rs | 1 + client-sdk/src/comm.rs | 98 ++++++++++++++++++++++++++ client-sdk/src/lib.rs | 1 + common/src/comm.rs | 11 +++ common/src/lib.rs | 1 + 6 files changed, 263 insertions(+) create mode 100644 app-sdk/src/comm.rs create mode 100644 client-sdk/src/comm.rs create mode 100644 common/src/comm.rs diff --git a/app-sdk/src/comm.rs b/app-sdk/src/comm.rs new file mode 100644 index 0000000..83e2e93 --- /dev/null +++ b/app-sdk/src/comm.rs @@ -0,0 +1,151 @@ +//! Module for sending and receiving messages using a custom protocol with alternating acknowledgments. +//! +//! This module provides an efficient way to handle message transmission between endpoints by using +//! length-prefix ed messages. This allows applications to use a fixed-length buffer for short messages, +//! reducing the need for large preallocated buffers. Large vectors are only allocated if necessary. +//! +//! The protocol alternates between sending and receiving chunks of data, ensuring reliable communication +//! through acknowledgment control. Each chunk sent or received is acknowledged by the other endpoint, +//! maintaining synchronization and error management throughout the transmission process. +//! +//! Key features of this module include: +//! - **Chunked Data Transmission**: Messages are divided into manageable chunks, allowing for efficient +//! transmission and reception without requiring large buffers. +//! - **Length-Prefixing**: Messages are prefixed with their length, enabling dynamic buffer allocation +//! only when necessary. +//! +//! Note: This module is not thread-safe. It is designed for single-threaded execution due to the use of +//! a static mutable buffer for chunk reuse. + +use crate::{xrecv, xsend}; +use alloc::vec::Vec; +use core::cmp::min; +use core::convert::TryInto; + +use common::comm::{ACK, CHUNK_LENGTH}; + +/// Error types that can occur during message transmission. +#[derive(Debug)] +pub enum MessageError { + /// Error when the received message length does not match expectations. + InvalidLength, + /// Error when more bytes are received than expected. + TooManyBytesReceived, + /// Error when a chunk fails to be received. + FailedToReadMessage, + /// Error when the message length cannot be determined due to insufficient bytes. + FailedToReadLength, +} + +impl core::fmt::Display for MessageError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + MessageError::InvalidLength => write!(f, "Invalid message length"), + MessageError::TooManyBytesReceived => write!(f, "Too many bytes received"), + MessageError::FailedToReadMessage => write!(f, "Failed to read message"), + MessageError::FailedToReadLength => write!(f, "Failed to read message length"), + } + } +} + +impl core::error::Error for MessageError {} + +/// Receives a message, handling chunked data reception and error management. +/// +/// The function starts by attempting to read a fixed-size chunk to extract the message length. +/// It then continues reading in chunks until the entire message is received, sending an +/// acknowledgment (`ACK`) byte for each chunk received. Errors occur if any unexpected +/// conditions are encountered, such as insufficient bytes or extra bytes in a chunk. +/// +/// # Errors +/// +/// - Returns `MessageError::FailedToReadLength` if the initial chunk is too small to contain the +/// message length. +/// - Returns `MessageError::TooManyBytesReceived` if unexpected extra bytes are received. +/// - Returns `MessageError::FailedToReadMessage` if a chunk is empty or fails to be read. +/// +/// # Returns +/// +/// - On success, returns `Ok(Vec)` with the received message data. +pub fn receive_message() -> Result, MessageError> { + let first_chunk = xrecv(256); + + // Ensure we have at least 4 bytes for the length. + if first_chunk.len() < 4 { + return Err(MessageError::FailedToReadLength); + } + + // Extract the message length. + let length = u32::from_be_bytes(first_chunk[0..4].try_into().unwrap()) as usize; + + // Check for unexpected extra bytes. + if first_chunk.len() > 4 + length { + return Err(MessageError::TooManyBytesReceived); + } + + // Initialize the result with the data from the first chunk. + let mut result = Vec::with_capacity(length); + result.extend_from_slice(&first_chunk[4..]); + + // Calculate the remaining bytes to read. + let mut remaining_bytes = length - result.len(); + + while remaining_bytes > 0 { + // Send ACK to maintain the alternating protocol. + xsend(&ACK); + + let chunk = xrecv(CHUNK_LENGTH); + + if chunk.is_empty() { + return Err(MessageError::FailedToReadMessage); + } + + if chunk.len() > remaining_bytes { + return Err(MessageError::TooManyBytesReceived); + } + + result.extend_from_slice(&chunk); + remaining_bytes -= chunk.len(); + } + + Ok(result) +} + +/// Sends a message, managing chunking and acknowledgment control for transmission. +/// +/// The function begins by encoding the message length in big-endian format and sending an initial +/// chunk containing this length along with part of the message (if any). It then continues sending +/// chunks, waiting for an acknowledgment (`ACK`) byte from the receiver before each chunk is sent. +/// The process ensures that messages are transmitted sequentially and fully. +/// +/// # Parameters +/// +/// - `msg`: A reference to the message bytes (`&[u8]`) that should be sent. +/// +/// The function does not return a value, nor any error. +/// On native execution, the function will panic if the underlying calls to `xsend` or `xrecv` panic. +/// On Risc-V targets, communication failure causes the ECALL to fail, which will arrest the execution of the VM. +pub fn send_message(msg: &[u8]) { + // Encode the message length in big-endian format. + let length_be = (msg.len() as u32).to_be_bytes(); + + // Calculate how much of the message fits in the first chunk. + let first_chunk_msg_bytes = min(CHUNK_LENGTH - 4, msg.len()); + + // Send the initial chunk containing the length and part of the message. + xsend(&[&length_be, &msg[..first_chunk_msg_bytes]].concat()); + + let mut total_bytes_sent = first_chunk_msg_bytes; + + // Send the remaining chunks. + while total_bytes_sent < msg.len() { + // Wait for ACK to maintain the alternating protocol. + let _ = xrecv(CHUNK_LENGTH); + + let end_idx = min(total_bytes_sent + CHUNK_LENGTH, msg.len()); + let chunk = &msg[total_bytes_sent..end_idx]; + + xsend(chunk); + total_bytes_sent = end_idx; + } +} diff --git a/app-sdk/src/lib.rs b/app-sdk/src/lib.rs index a1d72a2..2c18ffc 100644 --- a/app-sdk/src/lib.rs +++ b/app-sdk/src/lib.rs @@ -5,6 +5,7 @@ extern crate alloc; use alloc::{vec, vec::Vec}; +pub mod comm; pub mod ux; mod ecalls; diff --git a/client-sdk/src/comm.rs b/client-sdk/src/comm.rs new file mode 100644 index 0000000..43c6492 --- /dev/null +++ b/client-sdk/src/comm.rs @@ -0,0 +1,98 @@ +//! This module provides functionality for sending messages to a V-App and receiving responses +//! according to a specific communication protocol. +//! See the documentation of the V-App SDK's `comm` module for more details. + +use crate::vanadium_client::{VAppClient, VAppExecutionError}; + +use common::comm::{ACK, CHUNK_LENGTH}; + +/// Error types that can occur during message transmission. +#[derive(Debug)] +pub enum SendMessageError { + /// Error when an ACK was expected but a different message was received. + NotAckReceived, + /// Error returned from the VM. + VAppExecutionError(VAppExecutionError), + /// Error returned when the response is less than the expected 4 bytes. + ResponseTooShort, +} + +impl core::fmt::Display for SendMessageError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + SendMessageError::NotAckReceived => write!(f, "ACK was expected but not received"), + SendMessageError::ResponseTooShort => write!(f, "Response shorter than 4 bytes"), + SendMessageError::VAppExecutionError(v) => write!(f, "Error from the VM: {}", v), + } + } +} + +impl core::error::Error for SendMessageError {} + +/// Sends a message and receives a response according to the protocol of the `comm` module of the SDK. +/// +/// This function sends a message to a V-App and waits for a response. +/// The message is length-prefixed, then sent in chunks. After each chunk, an acknowledgment +/// message is expected from the V-App. If the acknowledgment is not received, an error is returned. +/// The response must start with a 4-byte length prefix, followed by the actual response data, which +/// is also split in chunks with the same approach. +/// +/// # Arguments +/// +/// * `client` - The V-App client. +/// * `message` - A byte slice containing the message to be sent. +/// +/// # Returns +/// +/// A `Result` containing a vector of bytes representing the response data if successful, or a +/// `SendMessageError` if an error occurs during the communication protocol. +/// +/// # Errors +/// +/// This function will return a `SendMessageError` if: +/// +/// * An acknowledgment (ACK) is expected but not received. +/// * The response length is less than 4 bytes. +/// * An error occurs during the execution of the virtual application client. +/// +pub async fn send_message( + client: &mut Box, + message: &[u8], +) -> Result, SendMessageError> { + // concatenate the length of the message (as a 4-byte big-endian) and the message itself + let mut full_message: Vec = Vec::with_capacity(message.len() + 4); + full_message.extend_from_slice(&(message.len() as u32).to_be_bytes()); + full_message.extend_from_slice(message); + + let mut resp = ACK.to_vec(); + for chunk in full_message.chunks(CHUNK_LENGTH) { + if resp != ACK { + return Err(SendMessageError::NotAckReceived); + } + resp = client + .send_message(chunk.to_vec()) + .await + .map_err(SendMessageError::VAppExecutionError)?; + } + + // The first 4 bytes contain the length of the data in the response. + // All the remaining data is the response data. + if resp.len() < 4 { + return Err(SendMessageError::ResponseTooShort); + } + let response_data_len = u32::from_be_bytes( + resp[0..4] + .try_into() + .map_err(|_| SendMessageError::ResponseTooShort)?, + ) as usize; + + let mut response_data = resp[4..].to_vec(); + while response_data.len() < response_data_len { + let resp = client + .send_message(ACK.to_vec()) + .await + .map_err(SendMessageError::VAppExecutionError)?; + response_data.extend_from_slice(&resp); + } + Ok(response_data) +} diff --git a/client-sdk/src/lib.rs b/client-sdk/src/lib.rs index 0359cd3..aace9fb 100644 --- a/client-sdk/src/lib.rs +++ b/client-sdk/src/lib.rs @@ -1,4 +1,5 @@ mod apdu; +pub mod comm; pub mod elf; pub mod transport; pub mod vanadium_client; diff --git a/common/src/comm.rs b/common/src/comm.rs new file mode 100644 index 0000000..e9bd2ff --- /dev/null +++ b/common/src/comm.rs @@ -0,0 +1,11 @@ +/// This module contains the constants for a communication protocol that builds on top of the +/// xsend and xrecv calls in order to send and receive length-prefixed messages as vectors. +/// The length-prefixed messages are sent in chunks of `CHUNK_LENGTH` bytes. +/// See the comm module in the for the corresponding implementations in the V-App SDK and in +/// the V-App client SDK. + +/// ACK is a single-byte acknowledgment message. +pub const ACK: [u8; 1] = [0x42]; + +/// The length of each chunk of data to be sent or received when calling xrecv/xsend. +pub const CHUNK_LENGTH: usize = 256; diff --git a/common/src/lib.rs b/common/src/lib.rs index f871532..e7eca90 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -4,6 +4,7 @@ extern crate alloc; pub mod accumulator; pub mod client_commands; +pub mod comm; pub mod constants; pub mod ecall_constants; pub mod manifest; From 4da27cd049ccb71c6e5caa85ae6fad63a62d3c2f Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:39:27 +0100 Subject: [PATCH 2/9] Add boilerplate for the bitcoin app --- apps/bitcoin/README.md | 61 ++ apps/bitcoin/app/.cargo/config.toml | 13 + apps/bitcoin/app/.vscode/settings.json | 4 + apps/bitcoin/app/Cargo.toml | 11 + apps/bitcoin/app/src/handlers/mod.rs | 8 + apps/bitcoin/app/src/main.rs | 125 +++ apps/bitcoin/client/Cargo.toml | 25 + apps/bitcoin/client/src/README.md | 1 + apps/bitcoin/client/src/client.rs | 133 +++ apps/bitcoin/client/src/lib.rs | 5 + apps/bitcoin/client/src/main.rs | 80 ++ apps/bitcoin/common/Cargo.toml | 9 + apps/bitcoin/common/README.md | 1 + apps/bitcoin/common/src/lib.rs | 3 + apps/bitcoin/common/src/message/README.md | 4 + apps/bitcoin/common/src/message/message.proto | 118 +++ apps/bitcoin/common/src/message/message.rs | 768 ++++++++++++++++++ apps/bitcoin/common/src/message/mod.rs | 6 + vanadium.code-workspace | 19 + 19 files changed, 1394 insertions(+) create mode 100644 apps/bitcoin/README.md create mode 100644 apps/bitcoin/app/.cargo/config.toml create mode 100644 apps/bitcoin/app/.vscode/settings.json create mode 100644 apps/bitcoin/app/Cargo.toml create mode 100644 apps/bitcoin/app/src/handlers/mod.rs create mode 100644 apps/bitcoin/app/src/main.rs create mode 100644 apps/bitcoin/client/Cargo.toml create mode 100644 apps/bitcoin/client/src/README.md create mode 100644 apps/bitcoin/client/src/client.rs create mode 100644 apps/bitcoin/client/src/lib.rs create mode 100644 apps/bitcoin/client/src/main.rs create mode 100644 apps/bitcoin/common/Cargo.toml create mode 100644 apps/bitcoin/common/README.md create mode 100644 apps/bitcoin/common/src/lib.rs create mode 100644 apps/bitcoin/common/src/message/README.md create mode 100644 apps/bitcoin/common/src/message/message.proto create mode 100644 apps/bitcoin/common/src/message/message.rs create mode 100644 apps/bitcoin/common/src/message/mod.rs diff --git a/apps/bitcoin/README.md b/apps/bitcoin/README.md new file mode 100644 index 0000000..51f0351 --- /dev/null +++ b/apps/bitcoin/README.md @@ -0,0 +1,61 @@ +This is a test V-App, with a few simple computations and functionalities to test various aspects of the Vanadium VM. + +- [app](app) contains the Risc-V app, based on the V-app Sdk. +- [client](client) folder contains the client of the app, based on the V-app Client Sdk. + +The `client` is a library crate (see [lib.rs](client/src/lib.rs)), but it also has a test executable ([main.rs](client/src/main.rs)) to interact with the app from the command line. + +## Build the V-App + +### Risc-V + +In order to build the app for the Risc-V target, enter the `app` folder and run: + + ```sh + cargo build --release --target=riscv32i-unknown-none-elf + ``` + +### Native + +In order to build the app for the native target, enter the `app` folder and run: + + ```sh + cargo build --release --target=x86_64-unknown-linux-gnu + ``` + +## Run the V-App + +Make sure you built the V-App for the Risc-V target. + +Launch Vanadium on speculos. Then execute: + +From the `client` folder + + ```sh + cargo run + ``` + +If you want to run the V-app on a real device, execute instead: + + ```sh + cargo run -- --hid + ``` + +If you want to run the V-app natively, after building it for the native target, use: + + ```sh + cargo run -- --native + ``` + + +### Client commands + +Once the client is running, these are the available commands: + +- `reverse ` - Reverses the given buffer. +- `sha256 ` - Computes the sha256 hash of the given buffer. +- `b58enc ` - Computes the base58 encoding of the given buffer (the output is in hex as well). +- `addnumbers ` - Computes the sum of the numbers between `1` and `n`. +- `nprimes ` - Counts the number of primes up to `n` using the Sieve of Eratosthenes. +- `panic ` - Cause the V-App to panic. Everything written after 'panic' is the panic message. +- An empty command will exit the V-App. diff --git a/apps/bitcoin/app/.cargo/config.toml b/apps/bitcoin/app/.cargo/config.toml new file mode 100644 index 0000000..b039c25 --- /dev/null +++ b/apps/bitcoin/app/.cargo/config.toml @@ -0,0 +1,13 @@ +[build] +target = "x86_64-unknown-linux-gnu" + + +[target.riscv32i-unknown-none-elf] +rustflags = [ + # The VM expects ELF binaries with 2 segments (rx and rw). Don't put + # read-only non-executable sections in their own segment. + "-Clink-arg=--no-rosegment", +] + +[env] +RUST_TEST_THREADS = "1" \ No newline at end of file diff --git a/apps/bitcoin/app/.vscode/settings.json b/apps/bitcoin/app/.vscode/settings.json new file mode 100644 index 0000000..2fe1364 --- /dev/null +++ b/apps/bitcoin/app/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.target": "x86_64-unknown-linux-gnu", + "rust-analyzer.cargo.allTargets": false +} diff --git a/apps/bitcoin/app/Cargo.toml b/apps/bitcoin/app/Cargo.toml new file mode 100644 index 0000000..ef82833 --- /dev/null +++ b/apps/bitcoin/app/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "vnd-bitcoin" +version = "0.1.0" +edition = "2021" + +[dependencies] +common = { package = "vnd-bitcoin-common", path = "../common"} +quick-protobuf = { version = "0.8.1", default-features = false } +sdk = { package = "vanadium-app-sdk", path = "../../../app-sdk"} + +[workspace] diff --git a/apps/bitcoin/app/src/handlers/mod.rs b/apps/bitcoin/app/src/handlers/mod.rs new file mode 100644 index 0000000..0324db5 --- /dev/null +++ b/apps/bitcoin/app/src/handlers/mod.rs @@ -0,0 +1,8 @@ +use common::message::ResponseGetMasterFingerprint; + +pub fn handle_get_master_fingerprint() -> Result { + // TODO: replace with proper sdk call + Ok(ResponseGetMasterFingerprint { + fingerprint: 0xf5acc2fd, + }) +} diff --git a/apps/bitcoin/app/src/main.rs b/apps/bitcoin/app/src/main.rs new file mode 100644 index 0000000..0553c44 --- /dev/null +++ b/apps/bitcoin/app/src/main.rs @@ -0,0 +1,125 @@ +#![feature(start)] +#![cfg_attr(target_arch = "riscv32", no_std, no_main)] + +use alloc::borrow::Cow; + +#[cfg(target_arch = "riscv32")] +use sdk::fatal; + +extern crate alloc; +extern crate quick_protobuf; + +mod handlers; + +use handlers::*; + +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; +use common::message::{ + mod_Request::OneOfrequest, mod_Response::OneOfresponse, Request, Response, ResponseError, +}; +use quick_protobuf::{BytesReader, BytesWriter, MessageRead, MessageWrite, Writer}; + +// Temporary to force the creation of a data section +#[used] +#[no_mangle] +pub static mut APP_NAME: [u8; 32] = *b"Bitcoin\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + +#[cfg(target_arch = "riscv32")] +#[panic_handler] +fn my_panic(info: &core::panic::PanicInfo) -> ! { + let message = if let Some(location) = info.location() { + alloc::format!( + "Panic occurred in file '{}' at line {}: {}", + location.file(), + location.line(), + info.message() + ) + } else { + alloc::format!("Panic occurred: {}", info.message()) + }; + fatal(&message); // does not return +} + +#[cfg(target_arch = "riscv32")] +#[no_mangle] +pub fn _start(_argc: isize, _argv: *const *const u8) -> isize { + main(_argc, _argv) +} + +fn handle_req_<'a>(buffer: &'a [u8]) -> Result, &'static str> { + let mut reader = BytesReader::from_bytes(buffer); + let request: Request = + Request::from_reader(&mut reader, buffer).map_err(|_| "Failed to parse request")?; // TODO: proper error handling + + let response = Response { + response: match request.request { + OneOfrequest::get_version(_) => OneOfresponse::get_version(todo!()), + OneOfrequest::exit(_) => { + sdk::exit(0); + } + OneOfrequest::get_master_fingerprint(_) => { + OneOfresponse::get_master_fingerprint(handle_get_master_fingerprint()?) + } + OneOfrequest::get_extended_pubkey(req) => OneOfresponse::get_extended_pubkey(todo!()), + OneOfrequest::register_wallet(req) => OneOfresponse::register_wallet(todo!()), + OneOfrequest::get_wallet_address(req) => OneOfresponse::get_wallet_address(todo!()), + OneOfrequest::sign_psbt(req) => OneOfresponse::sign_psbt(todo!()), + OneOfrequest::None => OneOfresponse::error(ResponseError { + error_msg: Cow::Borrowed("Invalid command"), + }), + }, + }; + + Ok(response) +} + +fn handle_req(buffer: &[u8]) -> Vec { + let error_msg: String; + + let response = match handle_req_(buffer) { + Ok(response) => response, + Err(error) => { + error_msg = error.to_string(); + Response { + response: OneOfresponse::error(ResponseError { + error_msg: Cow::Borrowed(&error_msg), + }), + } + } + }; + + let mut out = vec![0; response.get_size()]; + let mut writer = Writer::new(BytesWriter::new(&mut out)); + response.write_message(&mut writer).unwrap(); + + out.to_vec() +} + +#[start] +pub fn main(_: isize, _: *const *const u8) -> isize { + sdk::rust_init_heap(); + + sdk::ux::ux_idle(); + loop { + let req = match sdk::comm::receive_message() { + Ok(req) => req, + Err(e) => { + let error_string = e.to_string(); + let r = Response { + response: OneOfresponse::error(ResponseError { + error_msg: Cow::Borrowed(&error_string), + }), + }; + let mut out = vec![0; r.get_size()]; + let mut writer = Writer::new(BytesWriter::new(&mut out)); + r.write_message(&mut writer).unwrap(); + + out.to_vec() + } + }; + let result = handle_req(&req); + sdk::comm::send_message(&result); + } +} diff --git a/apps/bitcoin/client/Cargo.toml b/apps/bitcoin/client/Cargo.toml new file mode 100644 index 0000000..7b1842a --- /dev/null +++ b/apps/bitcoin/client/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "vnd-bitcoin-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.17", features = ["derive"] } +common = { package = "vnd-bitcoin-common", path = "../common"} +hex = "0.4.3" +hidapi = "2.6.3" +ledger-transport-hid = "0.11.0" +quick-protobuf = { version = "0.8.1", default-features = false } +sdk = { package = "vanadium-client-sdk", path = "../../../client-sdk"} +tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread", "sync"] } + +[lib] +name = "vnd_bitcoin_client" +path = "src/lib.rs" + +[[bin]] +name = "vnd_bitcoin_cli" +path = "src/main.rs" + + +[workspace] diff --git a/apps/bitcoin/client/src/README.md b/apps/bitcoin/client/src/README.md new file mode 100644 index 0000000..88c9f63 --- /dev/null +++ b/apps/bitcoin/client/src/README.md @@ -0,0 +1 @@ +This crate contains the client code of bitcoin V-App, and a standalone executable to run the bitcoin V-App from the host. diff --git a/apps/bitcoin/client/src/client.rs b/apps/bitcoin/client/src/client.rs new file mode 100644 index 0000000..1b62119 --- /dev/null +++ b/apps/bitcoin/client/src/client.rs @@ -0,0 +1,133 @@ +use common::message::{ + mod_Request::OneOfrequest, mod_Response::OneOfresponse, Request, RequestGetMasterFingerprint, + Response, +}; +use common::message::{RequestExit, RequestGetVersion}; +use quick_protobuf::{BytesReader, BytesWriter, MessageRead, MessageWrite, Writer}; +use sdk::vanadium_client::{VAppClient, VAppExecutionError}; + +use sdk::comm::SendMessageError; + +#[derive(Debug)] +pub enum BitcoinClientError { + VAppExecutionError(VAppExecutionError), + SendMessageError(SendMessageError), + InvalidResponse(&'static str), + GenericError(&'static str), +} + +impl From for BitcoinClientError { + fn from(e: VAppExecutionError) -> Self { + Self::VAppExecutionError(e) + } +} + +impl From for BitcoinClientError { + fn from(e: SendMessageError) -> Self { + Self::SendMessageError(e) + } +} + +impl From<&'static str> for BitcoinClientError { + fn from(e: &'static str) -> Self { + Self::GenericError(e) + } +} + +impl std::fmt::Display for BitcoinClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BitcoinClientError::VAppExecutionError(e) => write!(f, "VAppExecutionError: {}", e), + BitcoinClientError::SendMessageError(e) => write!(f, "SendMessageError: {}", e), + BitcoinClientError::InvalidResponse(e) => write!(f, "InvalidResponse: {}", e), + BitcoinClientError::GenericError(e) => write!(f, "GenericError: {}", e), + } + } +} + +impl std::error::Error for BitcoinClientError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + BitcoinClientError::VAppExecutionError(e) => Some(e), + BitcoinClientError::SendMessageError(e) => Some(e), + BitcoinClientError::InvalidResponse(_) => None, + BitcoinClientError::GenericError(_) => None, + } + } +} + +pub struct BitcoinClient { + app_client: Box, +} + +impl BitcoinClient { + pub fn new(app_client: Box) -> Self { + Self { app_client } + } + + pub async fn get_version(&mut self) -> Result { + let req = Request { + request: OneOfrequest::get_version(RequestGetVersion {}), + }; + + let mut out = vec![0; req.get_size()]; + let mut writer = Writer::new(BytesWriter::new(&mut out)); + req.write_message(&mut writer).unwrap(); + + let response_raw = sdk::comm::send_message(&mut self.app_client, &out).await?; + + let mut reader = BytesReader::from_bytes(&response_raw); + let response: Response = Response::from_reader(&mut reader, &response_raw) + .map_err(|_| "Failed to parse request")?; // TODO: proper error handling + + match response.response { + OneOfresponse::get_version(resp) => Ok(String::from(resp.version)), + _ => Err(BitcoinClientError::InvalidResponse("Invalid response")), + } + } + + pub async fn exit(&mut self) -> Result { + let req = Request { + request: OneOfrequest::exit(RequestExit {}), + }; + + let mut out = vec![0; req.get_size()]; + let mut writer = Writer::new(BytesWriter::new(&mut out)); + req.write_message(&mut writer).unwrap(); + + match sdk::comm::send_message(&mut self.app_client, &out).await { + Ok(_) => Err(BitcoinClientError::InvalidResponse( + "Exit message shouldn't return!", + )), + Err(e) => match e { + SendMessageError::VAppExecutionError(VAppExecutionError::AppExited(status)) => { + Ok(status) + } + _ => Err(BitcoinClientError::InvalidResponse( + "Unexpected error on exit", + )), + }, + } + } + + pub async fn get_master_fingerprint(&mut self) -> Result { + let req = Request { + request: OneOfrequest::get_master_fingerprint(RequestGetMasterFingerprint {}), + }; + + let mut out = vec![0; req.get_size()]; + let mut writer = Writer::new(BytesWriter::new(&mut out)); + req.write_message(&mut writer).unwrap(); + + let response_raw = sdk::comm::send_message(&mut self.app_client, &out).await?; + + let mut reader = BytesReader::from_bytes(&response_raw); + let response: Response = Response::from_reader(&mut reader, &response_raw) + .map_err(|_| "Failed to parse request")?; // TODO: proper error handling + + match response.response { + OneOfresponse::get_master_fingerprint(resp) => Ok(resp.fingerprint), + _ => Err(BitcoinClientError::InvalidResponse("Invalid response")), + } + } +} diff --git a/apps/bitcoin/client/src/lib.rs b/apps/bitcoin/client/src/lib.rs new file mode 100644 index 0000000..52e8288 --- /dev/null +++ b/apps/bitcoin/client/src/lib.rs @@ -0,0 +1,5 @@ +extern crate quick_protobuf; + +mod client; + +pub use client::BitcoinClient; diff --git a/apps/bitcoin/client/src/main.rs b/apps/bitcoin/client/src/main.rs new file mode 100644 index 0000000..ba07ff7 --- /dev/null +++ b/apps/bitcoin/client/src/main.rs @@ -0,0 +1,80 @@ +use clap::Parser; +use client::BitcoinClient; +use hidapi::HidApi; +use ledger_transport_hid::TransportNativeHID; + +use sdk::transport::{Transport, TransportHID, TransportTcp, TransportWrapper}; +use sdk::vanadium_client::{NativeAppClient, VanadiumAppClient}; + +mod client; + +use std::sync::Arc; + +#[derive(Parser)] +#[command(name = "Vanadium", about = "Run a V-App on Vanadium")] +struct Args { + /// Path to the ELF file of the V-App (if not the default one) + app: Option, + + /// Use the HID interface for a real device, instead of Speculos + #[arg(long, group = "interface")] + hid: bool, + + /// Use the native interface + #[arg(long, group = "interface")] + native: bool, +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + let default_app_path = if args.native { + "../app/target/x86_64-unknown-linux-gnu/release/vnd-bitcoin" + } else { + "../app/target/riscv32i-unknown-none-elf/release/vnd-bitcoin" + }; + + let app_path_str = args.app.unwrap_or(default_app_path.to_string()); + + let mut bitcoin_client = if args.native { + BitcoinClient::new(Box::new( + NativeAppClient::new(&app_path_str) + .await + .map_err(|_| "Failed to create client")?, + )) + } else { + let transport_raw: Arc< + dyn Transport> + Send + Sync, + > = if args.hid { + Arc::new(TransportHID::new( + TransportNativeHID::new( + &HidApi::new().expect("Unable to get connect to the device"), + ) + .unwrap(), + )) + } else { + Arc::new( + TransportTcp::new() + .await + .expect("Unable to get TCP transport. Is speculos running?"), + ) + }; + let transport = TransportWrapper::new(transport_raw.clone()); + + let (client, _) = VanadiumAppClient::new(&app_path_str, Arc::new(transport), None) + .await + .map_err(|_| "Failed to create client")?; + + BitcoinClient::new(Box::new(client)) + }; + + println!( + "Master fingerprint: {:08x}", + bitcoin_client.get_master_fingerprint().await? + ); + + bitcoin_client.exit().await?; + + Ok(()) +} diff --git a/apps/bitcoin/common/Cargo.toml b/apps/bitcoin/common/Cargo.toml new file mode 100644 index 0000000..822c3d8 --- /dev/null +++ b/apps/bitcoin/common/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "vnd-bitcoin-common" +version = "0.1.0" +edition = "2021" + +[dependencies] +quick-protobuf = { version = "0.8.1", default-features = false } + +[workspace] diff --git a/apps/bitcoin/common/README.md b/apps/bitcoin/common/README.md new file mode 100644 index 0000000..fa76d7b --- /dev/null +++ b/apps/bitcoin/common/README.md @@ -0,0 +1 @@ +This crate contain the common code between the bitcoin V-App and its client library. diff --git a/apps/bitcoin/common/src/lib.rs b/apps/bitcoin/common/src/lib.rs new file mode 100644 index 0000000..4feba75 --- /dev/null +++ b/apps/bitcoin/common/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +pub mod message; diff --git a/apps/bitcoin/common/src/message/README.md b/apps/bitcoin/common/src/message/README.md new file mode 100644 index 0000000..c31b255 --- /dev/null +++ b/apps/bitcoin/common/src/message/README.md @@ -0,0 +1,4 @@ +Generate rust modules from protobuf with: + +$ cargo install pb-rs +$ pb-rs --nostd ./message.proto diff --git a/apps/bitcoin/common/src/message/message.proto b/apps/bitcoin/common/src/message/message.proto new file mode 100644 index 0000000..308192a --- /dev/null +++ b/apps/bitcoin/common/src/message/message.proto @@ -0,0 +1,118 @@ +syntax = "proto3"; + +message RequestGetVersion { +} + +message ResponseGetVersion { + string version = 1; +} + +message RequestExit { +} + +message RequestGetMasterFingerprint { +} + +message ResponseGetMasterFingerprint { + uint32 fingerprint = 1; +} + +message RequestGetExtendedPubkey { + bool display = 1; + repeated uint32 bip32_path = 2; +} + +message ResponseGetExtendedPubkey { + string pubkey = 1; +} + +message RequestRegisterWallet { + string name = 1; + string descriptor_template = 2; + repeated string keys_info = 3; +} + +message ResponseRegisterWallet { + bytes wallet_id = 1; + bytes wallet_hmac = 2; +} + +message RequestGetWalletAddress { + bool display = 1; + string name = 2; + string descriptor_template = 3; + repeated string keys_info = 4; + optional bytes wallet_hmac = 5; + bool change = 6; + uint32 address_index = 7; +} + +message ResponseGetWalletAddress { + string address = 1; +} + +message RequestSignPsbt { + bytes psbt = 1; + string name = 2; + string descriptor_template = 3; + repeated string keys_info = 4; + optional bytes wallet_hmac = 5; +} + +message PartialSignature { + uint32 input_index = 1; + bytes signature = 2; + bytes public_key = 3; + bytes leaf_hash = 4; // Optional. If it's not set, it will have the default value (empty). +} + +message MusigPublicNonce { + uint32 input_index = 1; + bytes pubnonce = 2; // 66 bytes, concatenation of two pubkeys + bytes participant_public_key = 3; + bytes xonly_key = 4; + bytes leaf_hash = 5; // Optional. If it's not set, it will have the default value (empty). +} + +message MusigPartialSignature { + uint32 input_index = 1; + bytes signature = 2; // 32-bytes partial signature + bytes participant_public_key = 3; + bytes xonly_key = 4; + bytes leaf_hash = 5; // Optional. If it's not set, it will have the default value (empty). +} + +message ResponseSignPsbt { + repeated PartialSignature partial_signatures = 1; + repeated MusigPublicNonce musig_public_nonces = 2; + repeated MusigPartialSignature musig_partial_signatures = 3; +} + + +message ResponseError { + string error_msg = 1; +} + +message Request { + oneof request { + RequestGetVersion get_version = 1; + RequestExit exit = 2; + RequestGetMasterFingerprint get_master_fingerprint = 3; + RequestGetExtendedPubkey get_extended_pubkey = 4; + RequestRegisterWallet register_wallet = 5; + RequestGetWalletAddress get_wallet_address = 6; + RequestSignPsbt sign_psbt = 7; + } +} + +message Response { + oneof response { + ResponseGetVersion get_version = 1; + ResponseGetMasterFingerprint get_master_fingerprint = 2; + ResponseGetExtendedPubkey get_extended_pubkey = 3; + ResponseRegisterWallet register_wallet = 4; + ResponseGetWalletAddress get_wallet_address = 5; + ResponseSignPsbt sign_psbt = 6; + ResponseError error = 7; + } +} diff --git a/apps/bitcoin/common/src/message/message.rs b/apps/bitcoin/common/src/message/message.rs new file mode 100644 index 0000000..e0f17b3 --- /dev/null +++ b/apps/bitcoin/common/src/message/message.rs @@ -0,0 +1,768 @@ +// Automatically generated rust module for 'message.proto' file + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(unused_imports)] +#![allow(unknown_lints)] +#![allow(clippy::all)] +#![cfg_attr(rustfmt, rustfmt_skip)] + + +use alloc::vec::Vec; +use alloc::borrow::Cow; +use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; +use quick_protobuf::sizeofs::*; +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RequestGetVersion { } + +impl<'a> MessageRead<'a> for RequestGetVersion { + fn from_reader(r: &mut BytesReader, _: &[u8]) -> Result { + r.read_to_end(); + Ok(Self::default()) + } +} + +impl MessageWrite for RequestGetVersion { } + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ResponseGetVersion<'a> { + pub version: Cow<'a, str>, +} + +impl<'a> MessageRead<'a> for ResponseGetVersion<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.version = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ResponseGetVersion<'a> { + fn get_size(&self) -> usize { + 0 + + if self.version == "" { 0 } else { 1 + sizeof_len((&self.version).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.version != "" { w.write_with_tag(10, |w| w.write_string(&**&self.version))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RequestExit { } + +impl<'a> MessageRead<'a> for RequestExit { + fn from_reader(r: &mut BytesReader, _: &[u8]) -> Result { + r.read_to_end(); + Ok(Self::default()) + } +} + +impl MessageWrite for RequestExit { } + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RequestGetMasterFingerprint { } + +impl<'a> MessageRead<'a> for RequestGetMasterFingerprint { + fn from_reader(r: &mut BytesReader, _: &[u8]) -> Result { + r.read_to_end(); + Ok(Self::default()) + } +} + +impl MessageWrite for RequestGetMasterFingerprint { } + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ResponseGetMasterFingerprint { + pub fingerprint: u32, +} + +impl<'a> MessageRead<'a> for ResponseGetMasterFingerprint { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.fingerprint = r.read_uint32(bytes)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for ResponseGetMasterFingerprint { + fn get_size(&self) -> usize { + 0 + + if self.fingerprint == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.fingerprint) as u64) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.fingerprint != 0u32 { w.write_with_tag(8, |w| w.write_uint32(*&self.fingerprint))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RequestGetExtendedPubkey { + pub display: bool, + pub bip32_path: Vec, +} + +impl<'a> MessageRead<'a> for RequestGetExtendedPubkey { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.display = r.read_bool(bytes)?, + Ok(18) => msg.bip32_path = r.read_packed(bytes, |r, bytes| Ok(r.read_uint32(bytes)?))?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for RequestGetExtendedPubkey { + fn get_size(&self) -> usize { + 0 + + if self.display == false { 0 } else { 1 + sizeof_varint(*(&self.display) as u64) } + + if self.bip32_path.is_empty() { 0 } else { 1 + sizeof_len(self.bip32_path.iter().map(|s| sizeof_varint(*(s) as u64)).sum::()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.display != false { w.write_with_tag(8, |w| w.write_bool(*&self.display))?; } + w.write_packed_with_tag(18, &self.bip32_path, |w, m| w.write_uint32(*m), &|m| sizeof_varint(*(m) as u64))?; + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ResponseGetExtendedPubkey<'a> { + pub pubkey: Cow<'a, str>, +} + +impl<'a> MessageRead<'a> for ResponseGetExtendedPubkey<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.pubkey = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ResponseGetExtendedPubkey<'a> { + fn get_size(&self) -> usize { + 0 + + if self.pubkey == "" { 0 } else { 1 + sizeof_len((&self.pubkey).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.pubkey != "" { w.write_with_tag(10, |w| w.write_string(&**&self.pubkey))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RequestRegisterWallet<'a> { + pub name: Cow<'a, str>, + pub descriptor_template: Cow<'a, str>, + pub keys_info: Vec>, +} + +impl<'a> MessageRead<'a> for RequestRegisterWallet<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.name = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(18) => msg.descriptor_template = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(26) => msg.keys_info.push(r.read_string(bytes).map(Cow::Borrowed)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for RequestRegisterWallet<'a> { + fn get_size(&self) -> usize { + 0 + + if self.name == "" { 0 } else { 1 + sizeof_len((&self.name).len()) } + + if self.descriptor_template == "" { 0 } else { 1 + sizeof_len((&self.descriptor_template).len()) } + + self.keys_info.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.name != "" { w.write_with_tag(10, |w| w.write_string(&**&self.name))?; } + if self.descriptor_template != "" { w.write_with_tag(18, |w| w.write_string(&**&self.descriptor_template))?; } + for s in &self.keys_info { w.write_with_tag(26, |w| w.write_string(&**s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ResponseRegisterWallet<'a> { + pub wallet_id: Cow<'a, [u8]>, + pub wallet_hmac: Cow<'a, [u8]>, +} + +impl<'a> MessageRead<'a> for ResponseRegisterWallet<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.wallet_id = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(18) => msg.wallet_hmac = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ResponseRegisterWallet<'a> { + fn get_size(&self) -> usize { + 0 + + if self.wallet_id == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.wallet_id).len()) } + + if self.wallet_hmac == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.wallet_hmac).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.wallet_id != Cow::Borrowed(b"") { w.write_with_tag(10, |w| w.write_bytes(&**&self.wallet_id))?; } + if self.wallet_hmac != Cow::Borrowed(b"") { w.write_with_tag(18, |w| w.write_bytes(&**&self.wallet_hmac))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RequestGetWalletAddress<'a> { + pub display: bool, + pub name: Cow<'a, str>, + pub descriptor_template: Cow<'a, str>, + pub keys_info: Vec>, + pub wallet_hmac: Cow<'a, [u8]>, + pub change: bool, + pub address_index: u32, +} + +impl<'a> MessageRead<'a> for RequestGetWalletAddress<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.display = r.read_bool(bytes)?, + Ok(18) => msg.name = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(26) => msg.descriptor_template = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(34) => msg.keys_info.push(r.read_string(bytes).map(Cow::Borrowed)?), + Ok(42) => msg.wallet_hmac = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(48) => msg.change = r.read_bool(bytes)?, + Ok(56) => msg.address_index = r.read_uint32(bytes)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for RequestGetWalletAddress<'a> { + fn get_size(&self) -> usize { + 0 + + if self.display == false { 0 } else { 1 + sizeof_varint(*(&self.display) as u64) } + + if self.name == "" { 0 } else { 1 + sizeof_len((&self.name).len()) } + + if self.descriptor_template == "" { 0 } else { 1 + sizeof_len((&self.descriptor_template).len()) } + + self.keys_info.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + + if self.wallet_hmac == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.wallet_hmac).len()) } + + if self.change == false { 0 } else { 1 + sizeof_varint(*(&self.change) as u64) } + + if self.address_index == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.address_index) as u64) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.display != false { w.write_with_tag(8, |w| w.write_bool(*&self.display))?; } + if self.name != "" { w.write_with_tag(18, |w| w.write_string(&**&self.name))?; } + if self.descriptor_template != "" { w.write_with_tag(26, |w| w.write_string(&**&self.descriptor_template))?; } + for s in &self.keys_info { w.write_with_tag(34, |w| w.write_string(&**s))?; } + if self.wallet_hmac != Cow::Borrowed(b"") { w.write_with_tag(42, |w| w.write_bytes(&**&self.wallet_hmac))?; } + if self.change != false { w.write_with_tag(48, |w| w.write_bool(*&self.change))?; } + if self.address_index != 0u32 { w.write_with_tag(56, |w| w.write_uint32(*&self.address_index))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ResponseGetWalletAddress<'a> { + pub address: Cow<'a, str>, +} + +impl<'a> MessageRead<'a> for ResponseGetWalletAddress<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.address = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ResponseGetWalletAddress<'a> { + fn get_size(&self) -> usize { + 0 + + if self.address == "" { 0 } else { 1 + sizeof_len((&self.address).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.address != "" { w.write_with_tag(10, |w| w.write_string(&**&self.address))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RequestSignPsbt<'a> { + pub psbt: Cow<'a, [u8]>, + pub name: Cow<'a, str>, + pub descriptor_template: Cow<'a, str>, + pub keys_info: Vec>, + pub wallet_hmac: Cow<'a, [u8]>, +} + +impl<'a> MessageRead<'a> for RequestSignPsbt<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.psbt = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(18) => msg.name = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(26) => msg.descriptor_template = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(34) => msg.keys_info.push(r.read_string(bytes).map(Cow::Borrowed)?), + Ok(42) => msg.wallet_hmac = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for RequestSignPsbt<'a> { + fn get_size(&self) -> usize { + 0 + + if self.psbt == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.psbt).len()) } + + if self.name == "" { 0 } else { 1 + sizeof_len((&self.name).len()) } + + if self.descriptor_template == "" { 0 } else { 1 + sizeof_len((&self.descriptor_template).len()) } + + self.keys_info.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + + if self.wallet_hmac == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.wallet_hmac).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.psbt != Cow::Borrowed(b"") { w.write_with_tag(10, |w| w.write_bytes(&**&self.psbt))?; } + if self.name != "" { w.write_with_tag(18, |w| w.write_string(&**&self.name))?; } + if self.descriptor_template != "" { w.write_with_tag(26, |w| w.write_string(&**&self.descriptor_template))?; } + for s in &self.keys_info { w.write_with_tag(34, |w| w.write_string(&**s))?; } + if self.wallet_hmac != Cow::Borrowed(b"") { w.write_with_tag(42, |w| w.write_bytes(&**&self.wallet_hmac))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct PartialSignature<'a> { + pub input_index: u32, + pub signature: Cow<'a, [u8]>, + pub public_key: Cow<'a, [u8]>, + pub leaf_hash: Cow<'a, [u8]>, +} + +impl<'a> MessageRead<'a> for PartialSignature<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.input_index = r.read_uint32(bytes)?, + Ok(18) => msg.signature = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(26) => msg.public_key = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(34) => msg.leaf_hash = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for PartialSignature<'a> { + fn get_size(&self) -> usize { + 0 + + if self.input_index == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.input_index) as u64) } + + if self.signature == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.signature).len()) } + + if self.public_key == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.public_key).len()) } + + if self.leaf_hash == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.leaf_hash).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.input_index != 0u32 { w.write_with_tag(8, |w| w.write_uint32(*&self.input_index))?; } + if self.signature != Cow::Borrowed(b"") { w.write_with_tag(18, |w| w.write_bytes(&**&self.signature))?; } + if self.public_key != Cow::Borrowed(b"") { w.write_with_tag(26, |w| w.write_bytes(&**&self.public_key))?; } + if self.leaf_hash != Cow::Borrowed(b"") { w.write_with_tag(34, |w| w.write_bytes(&**&self.leaf_hash))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct MusigPublicNonce<'a> { + pub input_index: u32, + pub pubnonce: Cow<'a, [u8]>, + pub participant_public_key: Cow<'a, [u8]>, + pub xonly_key: Cow<'a, [u8]>, + pub leaf_hash: Cow<'a, [u8]>, +} + +impl<'a> MessageRead<'a> for MusigPublicNonce<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.input_index = r.read_uint32(bytes)?, + Ok(18) => msg.pubnonce = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(26) => msg.participant_public_key = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(34) => msg.xonly_key = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(42) => msg.leaf_hash = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for MusigPublicNonce<'a> { + fn get_size(&self) -> usize { + 0 + + if self.input_index == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.input_index) as u64) } + + if self.pubnonce == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.pubnonce).len()) } + + if self.participant_public_key == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.participant_public_key).len()) } + + if self.xonly_key == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.xonly_key).len()) } + + if self.leaf_hash == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.leaf_hash).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.input_index != 0u32 { w.write_with_tag(8, |w| w.write_uint32(*&self.input_index))?; } + if self.pubnonce != Cow::Borrowed(b"") { w.write_with_tag(18, |w| w.write_bytes(&**&self.pubnonce))?; } + if self.participant_public_key != Cow::Borrowed(b"") { w.write_with_tag(26, |w| w.write_bytes(&**&self.participant_public_key))?; } + if self.xonly_key != Cow::Borrowed(b"") { w.write_with_tag(34, |w| w.write_bytes(&**&self.xonly_key))?; } + if self.leaf_hash != Cow::Borrowed(b"") { w.write_with_tag(42, |w| w.write_bytes(&**&self.leaf_hash))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct MusigPartialSignature<'a> { + pub input_index: u32, + pub signature: Cow<'a, [u8]>, + pub participant_public_key: Cow<'a, [u8]>, + pub xonly_key: Cow<'a, [u8]>, + pub leaf_hash: Cow<'a, [u8]>, +} + +impl<'a> MessageRead<'a> for MusigPartialSignature<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.input_index = r.read_uint32(bytes)?, + Ok(18) => msg.signature = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(26) => msg.participant_public_key = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(34) => msg.xonly_key = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(42) => msg.leaf_hash = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for MusigPartialSignature<'a> { + fn get_size(&self) -> usize { + 0 + + if self.input_index == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.input_index) as u64) } + + if self.signature == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.signature).len()) } + + if self.participant_public_key == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.participant_public_key).len()) } + + if self.xonly_key == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.xonly_key).len()) } + + if self.leaf_hash == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.leaf_hash).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.input_index != 0u32 { w.write_with_tag(8, |w| w.write_uint32(*&self.input_index))?; } + if self.signature != Cow::Borrowed(b"") { w.write_with_tag(18, |w| w.write_bytes(&**&self.signature))?; } + if self.participant_public_key != Cow::Borrowed(b"") { w.write_with_tag(26, |w| w.write_bytes(&**&self.participant_public_key))?; } + if self.xonly_key != Cow::Borrowed(b"") { w.write_with_tag(34, |w| w.write_bytes(&**&self.xonly_key))?; } + if self.leaf_hash != Cow::Borrowed(b"") { w.write_with_tag(42, |w| w.write_bytes(&**&self.leaf_hash))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ResponseSignPsbt<'a> { + pub partial_signatures: Vec>, + pub musig_public_nonces: Vec>, + pub musig_partial_signatures: Vec>, +} + +impl<'a> MessageRead<'a> for ResponseSignPsbt<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.partial_signatures.push(r.read_message::(bytes)?), + Ok(18) => msg.musig_public_nonces.push(r.read_message::(bytes)?), + Ok(26) => msg.musig_partial_signatures.push(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ResponseSignPsbt<'a> { + fn get_size(&self) -> usize { + 0 + + self.partial_signatures.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + + self.musig_public_nonces.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + + self.musig_partial_signatures.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + for s in &self.partial_signatures { w.write_with_tag(10, |w| w.write_message(s))?; } + for s in &self.musig_public_nonces { w.write_with_tag(18, |w| w.write_message(s))?; } + for s in &self.musig_partial_signatures { w.write_with_tag(26, |w| w.write_message(s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ResponseError<'a> { + pub error_msg: Cow<'a, str>, +} + +impl<'a> MessageRead<'a> for ResponseError<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.error_msg = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ResponseError<'a> { + fn get_size(&self) -> usize { + 0 + + if self.error_msg == "" { 0 } else { 1 + sizeof_len((&self.error_msg).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.error_msg != "" { w.write_with_tag(10, |w| w.write_string(&**&self.error_msg))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Request<'a> { + pub request: mod_Request::OneOfrequest<'a>, +} + +impl<'a> MessageRead<'a> for Request<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.request = mod_Request::OneOfrequest::get_version(r.read_message::(bytes)?), + Ok(18) => msg.request = mod_Request::OneOfrequest::exit(r.read_message::(bytes)?), + Ok(26) => msg.request = mod_Request::OneOfrequest::get_master_fingerprint(r.read_message::(bytes)?), + Ok(34) => msg.request = mod_Request::OneOfrequest::get_extended_pubkey(r.read_message::(bytes)?), + Ok(42) => msg.request = mod_Request::OneOfrequest::register_wallet(r.read_message::(bytes)?), + Ok(50) => msg.request = mod_Request::OneOfrequest::get_wallet_address(r.read_message::(bytes)?), + Ok(58) => msg.request = mod_Request::OneOfrequest::sign_psbt(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for Request<'a> { + fn get_size(&self) -> usize { + 0 + + match self.request { + mod_Request::OneOfrequest::get_version(ref m) => 1 + sizeof_len((m).get_size()), + mod_Request::OneOfrequest::exit(ref m) => 1 + sizeof_len((m).get_size()), + mod_Request::OneOfrequest::get_master_fingerprint(ref m) => 1 + sizeof_len((m).get_size()), + mod_Request::OneOfrequest::get_extended_pubkey(ref m) => 1 + sizeof_len((m).get_size()), + mod_Request::OneOfrequest::register_wallet(ref m) => 1 + sizeof_len((m).get_size()), + mod_Request::OneOfrequest::get_wallet_address(ref m) => 1 + sizeof_len((m).get_size()), + mod_Request::OneOfrequest::sign_psbt(ref m) => 1 + sizeof_len((m).get_size()), + mod_Request::OneOfrequest::None => 0, + } } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + match self.request { mod_Request::OneOfrequest::get_version(ref m) => { w.write_with_tag(10, |w| w.write_message(m))? }, + mod_Request::OneOfrequest::exit(ref m) => { w.write_with_tag(18, |w| w.write_message(m))? }, + mod_Request::OneOfrequest::get_master_fingerprint(ref m) => { w.write_with_tag(26, |w| w.write_message(m))? }, + mod_Request::OneOfrequest::get_extended_pubkey(ref m) => { w.write_with_tag(34, |w| w.write_message(m))? }, + mod_Request::OneOfrequest::register_wallet(ref m) => { w.write_with_tag(42, |w| w.write_message(m))? }, + mod_Request::OneOfrequest::get_wallet_address(ref m) => { w.write_with_tag(50, |w| w.write_message(m))? }, + mod_Request::OneOfrequest::sign_psbt(ref m) => { w.write_with_tag(58, |w| w.write_message(m))? }, + mod_Request::OneOfrequest::None => {}, + } Ok(()) + } +} + +pub mod mod_Request { + +use alloc::vec::Vec; +use super::*; + +#[derive(Debug, PartialEq, Clone)] +pub enum OneOfrequest<'a> { + get_version(RequestGetVersion), + exit(RequestExit), + get_master_fingerprint(RequestGetMasterFingerprint), + get_extended_pubkey(RequestGetExtendedPubkey), + register_wallet(RequestRegisterWallet<'a>), + get_wallet_address(RequestGetWalletAddress<'a>), + sign_psbt(RequestSignPsbt<'a>), + None, +} + +impl<'a> Default for OneOfrequest<'a> { + fn default() -> Self { + OneOfrequest::None + } +} + +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Response<'a> { + pub response: mod_Response::OneOfresponse<'a>, +} + +impl<'a> MessageRead<'a> for Response<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.response = mod_Response::OneOfresponse::get_version(r.read_message::(bytes)?), + Ok(18) => msg.response = mod_Response::OneOfresponse::get_master_fingerprint(r.read_message::(bytes)?), + Ok(26) => msg.response = mod_Response::OneOfresponse::get_extended_pubkey(r.read_message::(bytes)?), + Ok(34) => msg.response = mod_Response::OneOfresponse::register_wallet(r.read_message::(bytes)?), + Ok(42) => msg.response = mod_Response::OneOfresponse::get_wallet_address(r.read_message::(bytes)?), + Ok(50) => msg.response = mod_Response::OneOfresponse::sign_psbt(r.read_message::(bytes)?), + Ok(58) => msg.response = mod_Response::OneOfresponse::error(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for Response<'a> { + fn get_size(&self) -> usize { + 0 + + match self.response { + mod_Response::OneOfresponse::get_version(ref m) => 1 + sizeof_len((m).get_size()), + mod_Response::OneOfresponse::get_master_fingerprint(ref m) => 1 + sizeof_len((m).get_size()), + mod_Response::OneOfresponse::get_extended_pubkey(ref m) => 1 + sizeof_len((m).get_size()), + mod_Response::OneOfresponse::register_wallet(ref m) => 1 + sizeof_len((m).get_size()), + mod_Response::OneOfresponse::get_wallet_address(ref m) => 1 + sizeof_len((m).get_size()), + mod_Response::OneOfresponse::sign_psbt(ref m) => 1 + sizeof_len((m).get_size()), + mod_Response::OneOfresponse::error(ref m) => 1 + sizeof_len((m).get_size()), + mod_Response::OneOfresponse::None => 0, + } } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + match self.response { mod_Response::OneOfresponse::get_version(ref m) => { w.write_with_tag(10, |w| w.write_message(m))? }, + mod_Response::OneOfresponse::get_master_fingerprint(ref m) => { w.write_with_tag(18, |w| w.write_message(m))? }, + mod_Response::OneOfresponse::get_extended_pubkey(ref m) => { w.write_with_tag(26, |w| w.write_message(m))? }, + mod_Response::OneOfresponse::register_wallet(ref m) => { w.write_with_tag(34, |w| w.write_message(m))? }, + mod_Response::OneOfresponse::get_wallet_address(ref m) => { w.write_with_tag(42, |w| w.write_message(m))? }, + mod_Response::OneOfresponse::sign_psbt(ref m) => { w.write_with_tag(50, |w| w.write_message(m))? }, + mod_Response::OneOfresponse::error(ref m) => { w.write_with_tag(58, |w| w.write_message(m))? }, + mod_Response::OneOfresponse::None => {}, + } Ok(()) + } +} + +pub mod mod_Response { + +use alloc::vec::Vec; +use super::*; + +#[derive(Debug, PartialEq, Clone)] +pub enum OneOfresponse<'a> { + get_version(ResponseGetVersion<'a>), + get_master_fingerprint(ResponseGetMasterFingerprint), + get_extended_pubkey(ResponseGetExtendedPubkey<'a>), + register_wallet(ResponseRegisterWallet<'a>), + get_wallet_address(ResponseGetWalletAddress<'a>), + sign_psbt(ResponseSignPsbt<'a>), + error(ResponseError<'a>), + None, +} + +impl<'a> Default for OneOfresponse<'a> { + fn default() -> Self { + OneOfresponse::None + } +} + +} + diff --git a/apps/bitcoin/common/src/message/mod.rs b/apps/bitcoin/common/src/message/mod.rs new file mode 100644 index 0000000..6019076 --- /dev/null +++ b/apps/bitcoin/common/src/message/mod.rs @@ -0,0 +1,6 @@ +extern crate alloc; +extern crate quick_protobuf; + +mod message; + +pub use message::*; diff --git a/vanadium.code-workspace b/vanadium.code-workspace index 7934b60..c1b8f68 100644 --- a/vanadium.code-workspace +++ b/vanadium.code-workspace @@ -15,6 +15,18 @@ { "path": "docs", }, + { + "path": "apps/bitcoin/app", + "name": "vnd-bitcoin" + }, + { + "path": "apps/bitcoin/common", + "name": "vnd-bitcoin-common" + }, + { + "path": "apps/bitcoin/client", + "name": "vnd-bitcoin-client" + }, { "path": "apps/test/app", "name": "vnd-test" @@ -31,6 +43,13 @@ "--target", "x86_64-unknown-linux-gnu" ] + }, + "[apps/bitcoin/app]": { + "rust-analyzer.cargo-watch.allTargets": false, + "rust-analyzer.cargo-watch.arguments": [ + "--target", + "x86_64-unknown-linux-gnu" + ] } } } From 75119e8a900175f765fcfc6552e25f4fe9be438c Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:25:51 +0100 Subject: [PATCH 3/9] Nit: rust-analyzer not happy with the diverging return type otherwise --- vm/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vm/src/main.rs b/vm/src/main.rs index 9a2b8fc..a56b9a0 100644 --- a/vm/src/main.rs +++ b/vm/src/main.rs @@ -83,7 +83,8 @@ fn handle_panic(info: &core::panic::PanicInfo) -> ! { let mut comm = ledger_device_sdk::io::Comm::new(); comm.reply(ledger_device_sdk::io::StatusWords::Panic); - ledger_device_sdk::exit_app(0x01); + + ledger_device_sdk::exit_app(0x01) } ledger_device_sdk::set_panic!(handle_panic); From a36c7ddd4afbfc638a3bf3b5491b5e5bce5a011f Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:57:11 +0100 Subject: [PATCH 4/9] Modified VAppClient::send_message to take a slice instead of a Vec --- apps/test/client/src/client.rs | 14 +++++++------- client-sdk/src/comm.rs | 4 ++-- client-sdk/src/vanadium_client.rs | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/test/client/src/client.rs b/apps/test/client/src/client.rs index 38b225b..b13115e 100644 --- a/apps/test/client/src/client.rs +++ b/apps/test/client/src/client.rs @@ -51,7 +51,7 @@ impl TestClient { msg.extend_from_slice(&[Command::Reverse as u8]); msg.extend_from_slice(data); - Ok(self.app_client.send_message(msg).await?) + Ok(self.app_client.send_message(&msg).await?) } pub async fn add_numbers(&mut self, n: u32) -> Result { @@ -59,7 +59,7 @@ impl TestClient { msg.extend_from_slice(&[Command::AddNumbers as u8]); msg.extend_from_slice(&n.to_be_bytes()); - let result_raw = self.app_client.send_message(msg).await?; + let result_raw = self.app_client.send_message(&msg).await?; if result_raw.len() != 8 { return Err("Invalid response length".into()); @@ -72,7 +72,7 @@ impl TestClient { msg.extend_from_slice(&[Command::Sha256 as u8]); msg.extend_from_slice(data); - Ok(self.app_client.send_message(msg).await?) + Ok(self.app_client.send_message(&msg).await?) } pub async fn b58enc(&mut self, data: &[u8]) -> Result, TestClientError> { @@ -80,7 +80,7 @@ impl TestClient { msg.extend_from_slice(&[Command::Base58Encode as u8]); msg.extend_from_slice(data); - Ok(self.app_client.send_message(msg).await?) + Ok(self.app_client.send_message(&msg).await?) } pub async fn nprimes(&mut self, n: u32) -> Result { @@ -88,7 +88,7 @@ impl TestClient { msg.extend_from_slice(&[Command::CountPrimes as u8]); msg.extend_from_slice(&n.to_be_bytes()); - let result_raw = self.app_client.send_message(msg).await?; + let result_raw = self.app_client.send_message(&msg).await?; if result_raw.len() != 4 { return Err("Invalid response length".into()); @@ -101,13 +101,13 @@ impl TestClient { msg.extend_from_slice(&[Command::Panic as u8]); msg.extend_from_slice(panic_msg.as_bytes()); - self.app_client.send_message(msg).await?; + self.app_client.send_message(&msg).await?; Err("The app should have panicked!".into()) } pub async fn exit(&mut self) -> Result { - match self.app_client.send_message(Vec::new()).await { + match self.app_client.send_message(&[]).await { Ok(_) => Err("Exit message shouldn't return!"), Err(e) => match e { VAppExecutionError::AppExited(status) => Ok(status), diff --git a/client-sdk/src/comm.rs b/client-sdk/src/comm.rs index 43c6492..1ba6486 100644 --- a/client-sdk/src/comm.rs +++ b/client-sdk/src/comm.rs @@ -70,7 +70,7 @@ pub async fn send_message( return Err(SendMessageError::NotAckReceived); } resp = client - .send_message(chunk.to_vec()) + .send_message(chunk) .await .map_err(SendMessageError::VAppExecutionError)?; } @@ -89,7 +89,7 @@ pub async fn send_message( let mut response_data = resp[4..].to_vec(); while response_data.len() < response_data_len { let resp = client - .send_message(ACK.to_vec()) + .send_message(&ACK) .await .map_err(SendMessageError::VAppExecutionError)?; response_data.extend_from_slice(&resp); diff --git a/client-sdk/src/vanadium_client.rs b/client-sdk/src/vanadium_client.rs index ca37aae..293f252 100644 --- a/client-sdk/src/vanadium_client.rs +++ b/client-sdk/src/vanadium_client.rs @@ -566,12 +566,12 @@ impl GenericVanadiumClient { Ok(()) } - pub async fn send_message(&mut self, message: Vec) -> Result, VanadiumClientError> { + pub async fn send_message(&mut self, message: &[u8]) -> Result, VanadiumClientError> { // Send the message to VAppEngine when receive_buffer is called self.client_to_engine_sender .as_ref() .ok_or("VAppEngine not running")? - .send(ClientMessage::ReceiveBuffer(message)) + .send(ClientMessage::ReceiveBuffer(message.to_vec())) .await .map_err(|_| "Failed to send message to VAppEngine")?; @@ -633,14 +633,14 @@ pub trait VAppClient { /// /// # Parameters /// - /// - `msg`: A `Vec` containing the message to be sent. + /// - `msg`: A `&[u8]` containing the message to be sent. /// /// # Returns /// /// A `Result` containing the response message as a `Vec` if the operation is successful, /// or a `VAppExecutionError` if an error occurs. - async fn send_message(&mut self, msg: Vec) -> Result, VAppExecutionError>; + async fn send_message(&mut self, msg: &[u8]) -> Result, VAppExecutionError>; } /// Implementation of a VAppClient using the Vanadium VM. @@ -688,7 +688,7 @@ impl VanadiumAppClient { #[async_trait] impl VAppClient for VanadiumAppClient { - async fn send_message(&mut self, msg: Vec) -> Result, VAppExecutionError> { + async fn send_message(&mut self, msg: &[u8]) -> Result, VAppExecutionError> { match self.client.send_message(msg).await { Ok(response) => Ok(response), Err(VanadiumClientError::VAppExited(status)) => { @@ -728,7 +728,7 @@ impl NativeAppClient { #[async_trait] impl VAppClient for NativeAppClient { - async fn send_message(&mut self, msg: Vec) -> Result, VAppExecutionError> { + async fn send_message(&mut self, msg: &[u8]) -> Result, VAppExecutionError> { // Check if the child process has exited if let Some(status) = self .child @@ -739,7 +739,7 @@ impl VAppClient for NativeAppClient { } // Encode message as hex and append a newline - let hex_msg = hex::encode(&msg); + let hex_msg = hex::encode(msg); let hex_msg_newline = format!("{}\n", hex_msg); // Write hex-encoded message to stdin From 5a90c97d959816c03f9cb3f8810ccb8511a2e879 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:29:05 +0100 Subject: [PATCH 5/9] nit --- vm/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm/Cargo.toml b/vm/Cargo.toml index e099602..8fcfecc 100644 --- a/vm/Cargo.toml +++ b/vm/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" common = { path = "../common", features=["device_sdk"] } ledger_device_sdk = { version="1.17.4" } include_gif = "1.2.0" -serde = {version="1.0.192", default_features = false, features = ["derive"]} +serde = {version="1.0.192", default-features = false, features = ["derive"]} serde-json-core = { git = "https://github.com/rust-embedded-community/serde-json-core"} hex = { version = "0.4.3", default-features = false, features = ["serde", "alloc"] } numtoa = "0.2.4" From 4928e7ee290668400a371f77bfadb004530ad61c Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:42:33 +0100 Subject: [PATCH 6/9] Remove old print statements in ecalls --- vm/src/handlers/lib/ecall.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/vm/src/handlers/lib/ecall.rs b/vm/src/handlers/lib/ecall.rs index 4ec830f..36eadba 100644 --- a/vm/src/handlers/lib/ecall.rs +++ b/vm/src/handlers/lib/ecall.rs @@ -311,7 +311,6 @@ impl<'a> EcallHandler for CommEcallHandler<'a> { } let ecall_code = reg!(T0); - crate::println!("ecall_code: {:?}", ecall_code); match ecall_code { ECALL_EXIT => return Err(CommEcallError::Exit(reg!(A0) as i32)), ECALL_FATAL => { @@ -323,15 +322,12 @@ impl<'a> EcallHandler for CommEcallHandler<'a> { .handle_xsend(cpu, GPreg!(A0), reg!(A1) as usize) .map_err(|_| CommEcallError::GenericError("xsend failed"))?, ECALL_XRECV => { - crate::println!("Executing xrecv()"); let ret = self .handle_xrecv(cpu, GPreg!(A0), reg!(A1) as usize) .map_err(|_| CommEcallError::GenericError("xrecv failed"))?; reg!(A0) = ret as u32; } ECALL_UX_IDLE => { - crate::println!("Executing ux_idle()"); - #[cfg(not(any(target_os = "stax", target_os = "flex")))] { ledger_device_sdk::ui::gadgets::clear_screen(); @@ -366,7 +362,6 @@ impl<'a> EcallHandler for CommEcallHandler<'a> { } } - crate::println!("Done with: {:?}", ecall_code); Ok(()) } } From 24277dd25dc9f218280f944474345d3d5f49ed94 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:11:50 +0000 Subject: [PATCH 7/9] Avoid unnecessary initialization of vectors that will be overwritten --- app-sdk/src/lib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app-sdk/src/lib.rs b/app-sdk/src/lib.rs index 2c18ffc..746b9fd 100644 --- a/app-sdk/src/lib.rs +++ b/app-sdk/src/lib.rs @@ -76,7 +76,14 @@ pub fn exit(status: i32) -> ! { } pub fn xrecv(size: usize) -> Vec { - let mut buffer = vec![0; size]; + // We allocate a buffer with the requested size, but we don't initialize its content. + // xrecv guarantees that recv_size have been overwritten with the received data, and we + // do not access any further data. + let mut buffer = Vec::with_capacity(size); + unsafe { + buffer.set_len(size); + } + let recv_size = Ecall::xrecv(buffer.as_mut_ptr(), buffer.len()); buffer[0..recv_size].to_vec() } From 8b38b50056b84249e90aa9b4992275c2783c2dcd Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:05:38 +0000 Subject: [PATCH 8/9] Add xrecv_to function to app-sdk; used to avoid unnecessary allocations in comm::receive_message --- app-sdk/src/comm.rs | 41 +++++++++++++++++++++++++---------------- app-sdk/src/lib.rs | 4 ++++ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app-sdk/src/comm.rs b/app-sdk/src/comm.rs index 83e2e93..23bdac8 100644 --- a/app-sdk/src/comm.rs +++ b/app-sdk/src/comm.rs @@ -17,7 +17,7 @@ //! Note: This module is not thread-safe. It is designed for single-threaded execution due to the use of //! a static mutable buffer for chunk reuse. -use crate::{xrecv, xsend}; +use crate::{xrecv_to, xsend}; use alloc::vec::Vec; use core::cmp::min; use core::convert::TryInto; @@ -50,6 +50,9 @@ impl core::fmt::Display for MessageError { impl core::error::Error for MessageError {} +// Define a static mutable buffer for chunk reuse, in order to avoid unnecessary allocations. +static mut CHUNK_BUFFER: [u8; CHUNK_LENGTH] = [0u8; CHUNK_LENGTH]; + /// Receives a message, handling chunked data reception and error management. /// /// The function starts by attempting to read a fixed-size chunk to extract the message length. @@ -67,45 +70,47 @@ impl core::error::Error for MessageError {} /// # Returns /// /// - On success, returns `Ok(Vec)` with the received message data. +/// +/// # Safety +/// +/// This function is only safe in single-threaded execution due to the use of a static mutable buffer. pub fn receive_message() -> Result, MessageError> { - let first_chunk = xrecv(256); + let chunk = &raw mut CHUNK_BUFFER; + + let first_chunk_len = xrecv_to(unsafe { &mut *chunk }); // Ensure we have at least 4 bytes for the length. - if first_chunk.len() < 4 { + if first_chunk_len < 4 { return Err(MessageError::FailedToReadLength); } // Extract the message length. - let length = u32::from_be_bytes(first_chunk[0..4].try_into().unwrap()) as usize; + let length = u32::from_be_bytes(unsafe { &*chunk }[0..4].try_into().unwrap()) as usize; // Check for unexpected extra bytes. - if first_chunk.len() > 4 + length { + if first_chunk_len > 4 + length { return Err(MessageError::TooManyBytesReceived); } // Initialize the result with the data from the first chunk. let mut result = Vec::with_capacity(length); - result.extend_from_slice(&first_chunk[4..]); - - // Calculate the remaining bytes to read. - let mut remaining_bytes = length - result.len(); + result.extend_from_slice(&unsafe { &*chunk }[4..first_chunk_len]); - while remaining_bytes > 0 { + while result.len() < length { // Send ACK to maintain the alternating protocol. xsend(&ACK); - let chunk = xrecv(CHUNK_LENGTH); + let chunk_len = xrecv_to(unsafe { &mut *chunk }); - if chunk.is_empty() { + if chunk_len == 0 { return Err(MessageError::FailedToReadMessage); } - if chunk.len() > remaining_bytes { + if chunk_len > length - result.len() { return Err(MessageError::TooManyBytesReceived); } - result.extend_from_slice(&chunk); - remaining_bytes -= chunk.len(); + result.extend_from_slice(&unsafe { &*chunk }[0..chunk_len]); } Ok(result) @@ -137,10 +142,14 @@ pub fn send_message(msg: &[u8]) { let mut total_bytes_sent = first_chunk_msg_bytes; + let mut acc_chunk = vec![0u8]; // Send the remaining chunks. while total_bytes_sent < msg.len() { // Wait for ACK to maintain the alternating protocol. - let _ = xrecv(CHUNK_LENGTH); + let acc_len = xrecv_to(&mut acc_chunk); + if acc_len != 1 || acc_chunk != ACK { + panic!("Unexpected byte received: {}", acc_chunk[0]); + } let end_idx = min(total_bytes_sent + CHUNK_LENGTH, msg.len()); let chunk = &msg[total_bytes_sent..end_idx]; diff --git a/app-sdk/src/lib.rs b/app-sdk/src/lib.rs index 746b9fd..c03270c 100644 --- a/app-sdk/src/lib.rs +++ b/app-sdk/src/lib.rs @@ -88,6 +88,10 @@ pub fn xrecv(size: usize) -> Vec { buffer[0..recv_size].to_vec() } +pub fn xrecv_to(buf: &mut [u8]) -> usize { + Ecall::xrecv(buf.as_mut_ptr(), buf.len()) +} + pub fn xsend(buffer: &[u8]) { Ecall::xsend(buffer.as_ptr(), buffer.len() as usize) } From 8dd37c99eae372884548d6a93c57c5effec66427 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:00:05 +0000 Subject: [PATCH 9/9] Simplified boilerplate in bitcoin app client --- apps/bitcoin/client/src/client.rs | 71 ++++++++++++++++--------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/apps/bitcoin/client/src/client.rs b/apps/bitcoin/client/src/client.rs index 1b62119..7138a54 100644 --- a/apps/bitcoin/client/src/client.rs +++ b/apps/bitcoin/client/src/client.rs @@ -60,26 +60,44 @@ pub struct BitcoinClient { app_client: Box, } -impl BitcoinClient { +impl<'a> BitcoinClient { pub fn new(app_client: Box) -> Self { Self { app_client } } - pub async fn get_version(&mut self) -> Result { - let req = Request { - request: OneOfrequest::get_version(RequestGetVersion {}), - }; - + async fn create_request( + request: OneOfrequest<'_>, + ) -> Result, BitcoinClientError> { + let req = Request { request }; let mut out = vec![0; req.get_size()]; let mut writer = Writer::new(BytesWriter::new(&mut out)); - req.write_message(&mut writer).unwrap(); + req.write_message(&mut writer) + .map_err(|_| BitcoinClientError::GenericError("Failed to write message"))?; + Ok(out) + } - let response_raw = sdk::comm::send_message(&mut self.app_client, &out).await?; + async fn send_message(&mut self, out: Vec) -> Result, BitcoinClientError> { + sdk::comm::send_message(&mut self.app_client, &out) + .await + .map_err(BitcoinClientError::from) + } + async fn parse_response>( + response_raw: &'a [u8], + ) -> Result { let mut reader = BytesReader::from_bytes(&response_raw); - let response: Response = Response::from_reader(&mut reader, &response_raw) - .map_err(|_| "Failed to parse request")?; // TODO: proper error handling + T::from_reader(&mut reader, response_raw) + .map_err(|_| BitcoinClientError::GenericError("Failed to parse response")) + } + pub async fn get_version(&mut self) -> Result { + let out = Self::create_request::(OneOfrequest::get_version( + RequestGetVersion {}, + )) + .await?; + + let response_raw = self.send_message(out).await?; + let response: Response = Self::parse_response(&response_raw).await?; match response.response { OneOfresponse::get_version(resp) => Ok(String::from(resp.version)), _ => Err(BitcoinClientError::InvalidResponse("Invalid response")), @@ -87,20 +105,13 @@ impl BitcoinClient { } pub async fn exit(&mut self) -> Result { - let req = Request { - request: OneOfrequest::exit(RequestExit {}), - }; - - let mut out = vec![0; req.get_size()]; - let mut writer = Writer::new(BytesWriter::new(&mut out)); - req.write_message(&mut writer).unwrap(); - - match sdk::comm::send_message(&mut self.app_client, &out).await { + let out = Self::create_request::(OneOfrequest::exit(RequestExit {})).await?; + match self.send_message(out).await { Ok(_) => Err(BitcoinClientError::InvalidResponse( "Exit message shouldn't return!", )), Err(e) => match e { - SendMessageError::VAppExecutionError(VAppExecutionError::AppExited(status)) => { + BitcoinClientError::VAppExecutionError(VAppExecutionError::AppExited(status)) => { Ok(status) } _ => Err(BitcoinClientError::InvalidResponse( @@ -111,20 +122,12 @@ impl BitcoinClient { } pub async fn get_master_fingerprint(&mut self) -> Result { - let req = Request { - request: OneOfrequest::get_master_fingerprint(RequestGetMasterFingerprint {}), - }; - - let mut out = vec![0; req.get_size()]; - let mut writer = Writer::new(BytesWriter::new(&mut out)); - req.write_message(&mut writer).unwrap(); - - let response_raw = sdk::comm::send_message(&mut self.app_client, &out).await?; - - let mut reader = BytesReader::from_bytes(&response_raw); - let response: Response = Response::from_reader(&mut reader, &response_raw) - .map_err(|_| "Failed to parse request")?; // TODO: proper error handling - + let out = Self::create_request::( + OneOfrequest::get_master_fingerprint(RequestGetMasterFingerprint {}), + ) + .await?; + let response_raw = self.send_message(out).await?; + let response: Response = Self::parse_response(&response_raw).await?; match response.response { OneOfresponse::get_master_fingerprint(resp) => Ok(resp.fingerprint), _ => Err(BitcoinClientError::InvalidResponse("Invalid response")),