From ee7471dbf7c9b6ae523b69352247251aa47ee333 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:47:41 +0200 Subject: [PATCH 01/14] Created initial module for the vanadium_client_sdk. Deleted the 'host' crate. The 'host' crate was split among the 'vanadium_client_sdk' and the vnd-test-client executable crate. --- apps/test/client/Cargo.toml | 5 + apps/test/client/src/main.rs | 130 ++++++++++++++++++-- client-sdk/Cargo.toml | 9 ++ {host => client-sdk}/src/apdu.rs | 0 {host => client-sdk}/src/elf.rs | 0 client-sdk/src/lib.rs | 18 +-- {host => client-sdk}/src/transport.rs | 2 - {host => client-sdk}/src/vanadium_client.rs | 39 +++--- host/Cargo.toml | 20 --- host/README.md | 14 --- host/src/main.rs | 88 ------------- vanadium.code-workspace | 3 - 12 files changed, 161 insertions(+), 167 deletions(-) rename {host => client-sdk}/src/apdu.rs (100%) rename {host => client-sdk}/src/elf.rs (100%) rename {host => client-sdk}/src/transport.rs (99%) rename {host => client-sdk}/src/vanadium_client.rs (95%) delete mode 100644 host/Cargo.toml delete mode 100644 host/README.md delete mode 100644 host/src/main.rs diff --git a/apps/test/client/Cargo.toml b/apps/test/client/Cargo.toml index b36919c..d22dae6 100644 --- a/apps/test/client/Cargo.toml +++ b/apps/test/client/Cargo.toml @@ -4,6 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] +clap = { version = "4.5.17", features = ["derive"] } +hex = "0.4.3" +hidapi = "2.6.3" +ledger-transport-hid = "0.11.0" sdk = { package = "vanadium-client-sdk", path = "../../../client-sdk"} +tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "sync"] } [workspace] diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index f2e6f28..9611d11 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -1,11 +1,125 @@ -fn main() { - println!("Hello, world!"); +use clap::Parser; +use hidapi::HidApi; +use ledger_transport_hid::TransportNativeHID; + +use sdk::{ + elf::ElfFile, + manifest::Manifest, + transport::{Transport, TransportHID, TransportTcp, TransportWrapper}, + vanadium_client::{Callbacks, ReceiveBufferError, VanadiumClient}, +}; + +use std::io::{stdin, Write}; +use std::sync::Arc; +use std::{io::stdout, path::Path}; +#[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) + elf: Option, + + /// Use the HID interface for a real device, instead of Speculos + #[arg(long)] + hid: bool, +} + +struct TestAppCallbacks; + +impl Callbacks for TestAppCallbacks { + fn receive_buffer(&self) -> Result, ReceiveBufferError> { + // Prompt the user to input a data buffer in hex; send it to the V-App + let mut buffer = String::new(); + let bytes = loop { + print!("Enter a data buffer in hexadecimal: "); + stdout().flush().unwrap(); + buffer.clear(); + stdin().read_line(&mut buffer).unwrap(); + buffer = buffer.trim().to_string(); + + if let Ok(bytes) = hex::decode(&buffer) { + break bytes; + } else { + println!("Invalid hexadecimal input. Please try again."); + } + }; + + Ok(bytes) + } + + fn send_buffer(&self, buffer: &[u8]) { + println!("Received buffer: {}", hex::encode(&buffer)); + } + + fn send_panic(&self, msg: &[u8]) { + println!( + "Received panic message:\n{}", + core::str::from_utf8(&msg).unwrap() + ); + } } -#[cfg(test)] -mod tests { - #[test] - fn test_placeholder() { - assert_eq!(1 + 1, 2); - } +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + let default_elf_path = "../app/target/riscv32i-unknown-none-elf/release/vnd-test"; + let elf_path_str = args.elf.unwrap_or(default_elf_path.to_string()); + let elf_path = Path::new(&elf_path_str); + let elf_file = ElfFile::new(elf_path)?; + + // println!("Entrypoint: {:?}", elf_file.entrypoint); + // println!("{:?}", elf_file); + + let transport_raw: Arc> + 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); + + let manifest = Manifest::new( + 0, + "Test", + "0.1.0", + [0u8; 32], // TODO + elf_file.entrypoint, + 65536, // TODO + elf_file.code_segment.start, + elf_file.code_segment.end, + 0xd47a2000 - 65536, // TODO + 0xd47a2000, // TODO + elf_file.data_segment.start, + elf_file.data_segment.end, + [0u8; 32], // TODO + 0, // TODO + ) + .unwrap(); + + let callbacks = TestAppCallbacks; + + let client = VanadiumClient::new(transport); + let app_hmac = client.register_vapp(&manifest).await?; + + println!("HMAC: {:?}", app_hmac); + + let result = client + .run_vapp(&manifest, &app_hmac, &elf_file, &callbacks) + .await?; + if result.len() != 4 { + return Err("The V-App exited, but did not return correctly return a status".into()); + } + let status = i32::from_be_bytes([result[0], result[1], result[2], result[3]]); + println!("App exited with status: {}", status); + + Ok(()) } diff --git a/client-sdk/Cargo.toml b/client-sdk/Cargo.toml index e71652a..bab6832 100644 --- a/client-sdk/Cargo.toml +++ b/client-sdk/Cargo.toml @@ -4,3 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] +async-trait = "0.1.81" +common = { path = "../common" } +goblin = "0.8.2" +hidapi = "2.6.3" +ledger-apdu = "0.11.0" +ledger-transport-hid = "0.11.0" +postcard = { version = "1.0.8", features = ["alloc"] } +sha2 = "0.10.8" +tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "sync"] } diff --git a/host/src/apdu.rs b/client-sdk/src/apdu.rs similarity index 100% rename from host/src/apdu.rs rename to client-sdk/src/apdu.rs diff --git a/host/src/elf.rs b/client-sdk/src/elf.rs similarity index 100% rename from host/src/elf.rs rename to client-sdk/src/elf.rs diff --git a/client-sdk/src/lib.rs b/client-sdk/src/lib.rs index b93cf3f..0359cd3 100644 --- a/client-sdk/src/lib.rs +++ b/client-sdk/src/lib.rs @@ -1,14 +1,6 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +mod apdu; +pub mod elf; +pub mod transport; +pub mod vanadium_client; -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use common::manifest; diff --git a/host/src/transport.rs b/client-sdk/src/transport.rs similarity index 99% rename from host/src/transport.rs rename to client-sdk/src/transport.rs index 3444918..d8b4872 100644 --- a/host/src/transport.rs +++ b/client-sdk/src/transport.rs @@ -16,7 +16,6 @@ use tokio::{ use crate::apdu::{APDUCommand, StatusWord}; - /// Communication layer between the bitcoin client and the Ledger device. #[async_trait] pub trait Transport { @@ -24,7 +23,6 @@ pub trait Transport { async fn exchange(&self, command: &APDUCommand) -> Result<(StatusWord, Vec), Self::Error>; } - /// Transport with the Ledger device. pub struct TransportHID(TransportNativeHID); diff --git a/host/src/vanadium_client.rs b/client-sdk/src/vanadium_client.rs similarity index 95% rename from host/src/vanadium_client.rs rename to client-sdk/src/vanadium_client.rs index 8c683ea..a8fc273 100644 --- a/host/src/vanadium_client.rs +++ b/client-sdk/src/vanadium_client.rs @@ -1,5 +1,4 @@ use std::cmp::min; -use std::io::{stdin, stdout, Write}; use common::accumulator::{HashOutput, Hasher, MerkleAccumulator, VectorAccumulator}; use common::client_commands::{ @@ -13,7 +12,7 @@ use sha2::{Digest, Sha256}; use crate::apdu::{APDUCommand, StatusWord}; use crate::elf::ElfFile; -use crate::Transport; +use crate::transport::Transport; fn apdu_continue(data: Vec) -> APDUCommand { APDUCommand { @@ -124,6 +123,7 @@ struct VAppEngine<'a> { code_seg: MemorySegment, data_seg: MemorySegment, stack_seg: MemorySegment, + callbacks: &'a dyn Callbacks, } impl<'a> VAppEngine<'a> { @@ -266,7 +266,7 @@ impl<'a> VAppEngine<'a> { remaining_len -= msg.data.len() as u32; } - println!("Received buffer: {}", hex::encode(&buf)); + self.callbacks.send_buffer(&buf); Ok(self .exchange_and_process_page_requests(transport, &apdu_continue(vec![])) @@ -282,21 +282,10 @@ impl<'a> VAppEngine<'a> { ) -> Result<(StatusWord, Vec), &'static str> { ReceiveBufferMessage::deserialize(command)?; - // Prompt the user to input a data buffer in hex; send it to the V-App - let mut buffer = String::new(); - let bytes = loop { - print!("Enter a data buffer in hexadecimal: "); - stdout().flush().unwrap(); - buffer.clear(); - stdin().read_line(&mut buffer).unwrap(); - buffer = buffer.trim().to_string(); - - if let Ok(bytes) = hex::decode(&buffer) { - break bytes; - } else { - println!("Invalid hexadecimal input. Please try again."); - } - }; + let bytes = self + .callbacks + .receive_buffer() + .map_err(|_| "receive buffer callback failed")?; let mut remaining_len = bytes.len() as u32; let mut offset: usize = 0; @@ -443,6 +432,16 @@ pub struct VanadiumClient { transport: T, } +pub enum ReceiveBufferError { + ReceiveFailed, // TODO: do we need to distinguish between more errors? +} + +pub trait Callbacks { + fn receive_buffer(&self) -> Result, ReceiveBufferError>; + fn send_buffer(&self, buffer: &[u8]); + fn send_panic(&self, msg: &[u8]); +} + impl VanadiumClient { pub fn new(transport: T) -> Self { Self { transport } @@ -479,11 +478,12 @@ impl VanadiumClient { } } - pub async fn run_vapp( + pub async fn run_vapp( &self, manifest: &Manifest, app_hmac: &[u8; 32], elf: &ElfFile, + callbacks: &C, ) -> Result, &'static str> { // concatenate the serialized manifest and the app_hmac let mut data = @@ -503,6 +503,7 @@ impl VanadiumClient { code_seg, data_seg, stack_seg, + callbacks, }; // initial APDU to start the V-App diff --git a/host/Cargo.toml b/host/Cargo.toml deleted file mode 100644 index 77b54c9..0000000 --- a/host/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "host" -version = "0.1.0" -edition = "2021" - -[dependencies] -async-trait = "0.1.81" -clap = { version = "4.5.17", features = ["derive"] } -common = { path = "../common" } -goblin = "0.8.2" -hex = "0.4.3" -hidapi = "2.6.3" -ledger-apdu = "0.11.0" -ledger-transport-hid = "0.11.0" -postcard = { version = "1.0.8", features = ["alloc"] } -serde = { version = "1.0.204", default-features = false, features = ["derive", "alloc"] } -sha2 = "0.10.8" -tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "sync"] } - -[workspace] diff --git a/host/README.md b/host/README.md deleted file mode 100644 index dce9690..0000000 --- a/host/README.md +++ /dev/null @@ -1,14 +0,0 @@ -The content in this folder is temporary; it currently contains code that was in the `/host` folder in the [vanadium-legacy](https://github.com/LedgerHQ/vanadium-legacy) repo. - -It contains a standalone executable that parses the elf file of a V-App, builds the Manifest, and starts its execution in the Vanadium VM. - -Once it's more mature, most of the code here will be moved to either the `common` crate or the `client_sdk`. - - -## Starting - -Start the VM app on speculos. - -``` -cargo run -- ../apps/test/app/target/riscv32i-unknown-none-elf/release/vnd-test -``` \ No newline at end of file diff --git a/host/src/main.rs b/host/src/main.rs deleted file mode 100644 index ac00d07..0000000 --- a/host/src/main.rs +++ /dev/null @@ -1,88 +0,0 @@ -mod apdu; -mod elf; -mod transport; -mod vanadium_client; - -use clap::Parser; -use common::manifest::Manifest; -use hidapi::HidApi; -use ledger_transport_hid::TransportNativeHID; -use transport::{Transport, TransportHID, TransportTcp, TransportWrapper}; - -use std::path::Path; -use std::sync::Arc; -use vanadium_client::VanadiumClient; - -use elf::ElfFile; - -#[derive(Parser)] -#[command(name = "Vanadium", about = "Run a V-App on Vanadium")] -struct Args { - /// Path to the ELF file of the V-App - #[arg(required = true)] - elf: String, - - /// Use the HID interface for a real device, instead of Speculos - #[arg(long)] - hid: bool, -} - -#[tokio::main(flavor = "current_thread")] -async fn main() -> Result<(), Box> { - let args = Args::parse(); - - let elf_path = Path::new(&args.elf); - let elf_file = ElfFile::new(elf_path)?; - - // println!("Entrypoint: {:?}", elf_file.entrypoint); - // println!("{:?}", elf_file); - - let transport_raw: Arc> + 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); - - let manifest = Manifest::new( - 0, - "Test", - "0.1.0", - [0u8; 32], // TODO - elf_file.entrypoint, - 65536, // TODO - elf_file.code_segment.start, - elf_file.code_segment.end, - 0xd47a2000 - 65536, // TODO - 0xd47a2000, // TODO - elf_file.data_segment.start, - elf_file.data_segment.end, - [0u8; 32], // TODO - 0, // TODO - ) - .unwrap(); - - let client = VanadiumClient::new(transport); - let app_hmac = client.register_vapp(&manifest).await?; - - println!("HMAC: {:?}", app_hmac); - - let result = client.run_vapp(&manifest, &app_hmac, &elf_file).await?; - if result.len() != 4 { - return Err("The V-App exited, but did not return correctly return a status".into()); - } - let status = i32::from_be_bytes([result[0], result[1], result[2], result[3]]); - println!("App exited with status: {}", status); - - Ok(()) -} diff --git a/vanadium.code-workspace b/vanadium.code-workspace index 1bf6787..7934b60 100644 --- a/vanadium.code-workspace +++ b/vanadium.code-workspace @@ -3,9 +3,6 @@ { "path": "vm" }, - { - "path": "host" - }, { "path": "common" }, From 5edb572517c19b844adab1fe77835fc8e446fb45 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:55:56 +0200 Subject: [PATCH 02/14] Fix CI --- .github/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 271bdac..ad9291c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,6 +43,9 @@ jobs: target: x86_64-unknown-linux-gnu components: rustfmt, clippy profile: minimal + - name: Install libudev-dev and pkg-config + run: | + sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config - name: Clone uses: actions/checkout@v4 - name: Unit tests @@ -97,6 +100,9 @@ jobs: target: x86_64-unknown-linux-gnu components: rustfmt, clippy profile: minimal + - name: Install libudev-dev and pkg-config + run: | + sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config - name: Clone uses: actions/checkout@v4 - name: Unit tests From 65376d37b0a6107a2e15863b1c4f2f7e9c6450c8 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:14:56 +0200 Subject: [PATCH 03/14] Add more test commands to the test app --- apps/test/app/Cargo.toml | 2 + apps/test/app/src/commands.rs | 23 +++++ apps/test/app/src/handlers/base58.rs | 5 + apps/test/app/src/handlers/count_primes.rs | 54 +++++++++++ apps/test/app/src/handlers/mod.rs | 7 ++ apps/test/app/src/handlers/sha256.rs | 6 ++ apps/test/app/src/main.rs | 56 ++++++----- apps/test/client/src/commands.rs | 23 +++++ apps/test/client/src/main.rs | 106 ++++++++++++++------- vm/src/handlers/start_vapp.rs | 24 +++-- 10 files changed, 241 insertions(+), 65 deletions(-) create mode 100644 apps/test/app/src/commands.rs create mode 100644 apps/test/app/src/handlers/base58.rs create mode 100644 apps/test/app/src/handlers/count_primes.rs create mode 100644 apps/test/app/src/handlers/mod.rs create mode 100644 apps/test/app/src/handlers/sha256.rs create mode 100644 apps/test/client/src/commands.rs diff --git a/apps/test/app/Cargo.toml b/apps/test/app/Cargo.toml index 31fd1a6..6ac797f 100644 --- a/apps/test/app/Cargo.toml +++ b/apps/test/app/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +bs58 = { version = "0.5.1", default-features = false, features = ["alloc"] } sdk = { package = "vanadium-app-sdk", path = "../../../app-sdk"} +sha2 = { version = "0.10.8", default-features = false } [workspace] diff --git a/apps/test/app/src/commands.rs b/apps/test/app/src/commands.rs new file mode 100644 index 0000000..63c899b --- /dev/null +++ b/apps/test/app/src/commands.rs @@ -0,0 +1,23 @@ +#[derive(Debug)] +pub enum Command { + Reverse, + Sum, + Base58Encode, + Sha256, + CountPrimes, +} + +impl TryFrom for Command { + type Error = (); + + fn try_from(byte: u8) -> Result { + match byte { + 0x00 => Ok(Command::Reverse), + 0x01 => Ok(Command::Sum), + 0x02 => Ok(Command::Base58Encode), + 0x03 => Ok(Command::Sha256), + 0x04 => Ok(Command::CountPrimes), + _ => Err(()), + } + } +} diff --git a/apps/test/app/src/handlers/base58.rs b/apps/test/app/src/handlers/base58.rs new file mode 100644 index 0000000..97bf178 --- /dev/null +++ b/apps/test/app/src/handlers/base58.rs @@ -0,0 +1,5 @@ +use alloc::vec::Vec; + +pub fn handle_base58_encode(data: &[u8]) -> Vec { + bs58::encode(data).into_vec() +} diff --git a/apps/test/app/src/handlers/count_primes.rs b/apps/test/app/src/handlers/count_primes.rs new file mode 100644 index 0000000..d565500 --- /dev/null +++ b/apps/test/app/src/handlers/count_primes.rs @@ -0,0 +1,54 @@ +use alloc::{vec, vec::Vec}; + +fn count_primes(n: u32) -> u32 { + if n < 2 { + return 0; + } + + let mut is_prime = vec![true; (n + 1) as usize]; + is_prime[0] = false; + is_prime[1] = false; + + let mut i = 2; + let mut square = 4; + loop { + if square > n { + break; + } + if is_prime[i as usize] { + let mut multiple = i * i; + while multiple <= n { + is_prime[multiple as usize] = false; + multiple += i; + } + } + square += 2 * i + 1; + i += 1; + } + + // Count the primes + is_prime.iter().filter(|&&p| p).count() as u32 +} + +pub fn handle_count_primes(data: &[u8]) -> Vec { + if data.len() != 4 { + return vec![]; + } + let n = u32::from_be_bytes(data[0..4].try_into().unwrap()); + let count = count_primes(n); + count.to_be_bytes().to_vec() +} + +#[cfg(test)] +mod tests { + #[test] + fn test_count_primes() { + assert_eq!(super::count_primes(0), 0); + assert_eq!(super::count_primes(1), 0); + assert_eq!(super::count_primes(2), 1); + assert_eq!(super::count_primes(3), 2); + assert_eq!(super::count_primes(4), 2); + assert_eq!(super::count_primes(5), 3); + assert_eq!(super::count_primes(10000), 1229); + } +} diff --git a/apps/test/app/src/handlers/mod.rs b/apps/test/app/src/handlers/mod.rs new file mode 100644 index 0000000..3920782 --- /dev/null +++ b/apps/test/app/src/handlers/mod.rs @@ -0,0 +1,7 @@ +mod base58; +mod count_primes; +mod sha256; + +pub use base58::handle_base58_encode; +pub use count_primes::handle_count_primes; +pub use sha256::handle_sha256; diff --git a/apps/test/app/src/handlers/sha256.rs b/apps/test/app/src/handlers/sha256.rs new file mode 100644 index 0000000..09bfb87 --- /dev/null +++ b/apps/test/app/src/handlers/sha256.rs @@ -0,0 +1,6 @@ +use alloc::vec::Vec; +use sha2::Digest; + +pub fn handle_sha256(data: &[u8]) -> Vec { + sha2::Sha256::digest(&data).to_vec() +} diff --git a/apps/test/app/src/main.rs b/apps/test/app/src/main.rs index ebccc98..56f5e1c 100644 --- a/apps/test/app/src/main.rs +++ b/apps/test/app/src/main.rs @@ -6,6 +6,12 @@ use sdk::fatal; extern crate alloc; +mod commands; +mod handlers; + +use commands::Command; +use handlers::*; + use alloc::vec; // Temporary to force the creation of a data section @@ -61,28 +67,34 @@ pub fn main(_: isize, _: *const *const u8) -> isize { if msg.len() == 0 { sdk::exit(0); } - if msg.len() == 1 { - panic!("Oh no, how can I reverse a single byte?"); - } - - // reverse the message - let mut reversed = msg.clone(); - reversed.reverse(); - sdk::xsend(&reversed); - - // let result = handle_req(&buffer, &mut state); - - // sdk::ux::app_loading_stop(); - // sdk::ux::ux_idle(); - - // comm::send_message(&result).unwrap(); // TODO: what to do on error? - } -} -#[cfg(test)] -mod tests { - #[test] - fn test_placeholder() { - assert_eq!(1 + 1, 2); + let Ok(command) = Command::try_from(msg[0]) else { + panic!("Unknown command"); + }; + + let response = match command { + Command::Reverse => { + let mut data = msg[1..].to_vec(); + data.reverse(); + data + } + Command::Sum => { + // sum all the numbers from 0 to n + if msg.len() != 5 { + panic!("Invalid input"); + } + let n = u32::from_be_bytes([msg[1], msg[2], msg[3], msg[4]]); + let mut result: u64 = 0; + for i in 0..=n { + result += i as u64; + } + result.to_be_bytes().to_vec() + } + Command::Base58Encode => handle_base58_encode(&msg[1..]), + Command::Sha256 => handle_sha256(&msg[1..]), + Command::CountPrimes => handle_count_primes(&msg[1..]), + }; + + sdk::xsend(&response); } } diff --git a/apps/test/client/src/commands.rs b/apps/test/client/src/commands.rs new file mode 100644 index 0000000..63c899b --- /dev/null +++ b/apps/test/client/src/commands.rs @@ -0,0 +1,23 @@ +#[derive(Debug)] +pub enum Command { + Reverse, + Sum, + Base58Encode, + Sha256, + CountPrimes, +} + +impl TryFrom for Command { + type Error = (); + + fn try_from(byte: u8) -> Result { + match byte { + 0x00 => Ok(Command::Reverse), + 0x01 => Ok(Command::Sum), + 0x02 => Ok(Command::Base58Encode), + 0x03 => Ok(Command::Sha256), + 0x04 => Ok(Command::CountPrimes), + _ => Err(()), + } + } +} diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index 9611d11..76635ee 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -9,6 +9,10 @@ use sdk::{ vanadium_client::{Callbacks, ReceiveBufferError, VanadiumClient}, }; +mod commands; + +use commands::Command; + use std::io::{stdin, Write}; use std::sync::Arc; use std::{io::stdout, path::Path}; @@ -23,6 +27,71 @@ struct Args { hid: bool, } +// TODO: maybe this can be made generic enough to move to the client-sdk +struct TestClientRunner { + client: VanadiumClient, + elf_file: ElfFile, + manifest: Manifest, + app_hmac: Option<[u8; 32]>, +} + +impl TestClientRunner { + pub fn new(transport: T, elf_path: &str) -> Result { + let elf_file = ElfFile::new(Path::new(&elf_path))?; + + let manifest = Manifest::new( + 0, + "Test", + "0.1.0", + [0u8; 32], // TODO + elf_file.entrypoint, + 65536, // TODO + elf_file.code_segment.start, + elf_file.code_segment.end, + 0xd47a2000 - 65536, // TODO + 0xd47a2000, // TODO + elf_file.data_segment.start, + elf_file.data_segment.end, + [0u8; 32], // TODO + 0, // TODO + ) + .unwrap(); + + Ok(Self { + client: VanadiumClient::new(transport), + elf_file, + manifest, + app_hmac: None, + }) + } + + pub async fn register_vapp(&mut self) -> Result<[u8; 32], Box> { + let app_hmac = self.client.register_vapp(&self.manifest).await?; + self.app_hmac = Some(app_hmac); + + Ok(app_hmac) + } + + pub async fn run_vapp( + &mut self, + callbacks: TestAppCallbacks, + ) -> Result> { + let app_hmac = self.app_hmac.ok_or("V-App not registered")?; + + let result = self + .client + .run_vapp(&self.manifest, &app_hmac, &self.elf_file, &callbacks) + .await?; + if result.len() != 4 { + panic!("The V-App exited, but did not return correctly return a status"); + } + let status = i32::from_be_bytes([result[0], result[1], result[2], result[3]]); + println!("App exited with status: {}", status); + + Ok(status) + } +} + struct TestAppCallbacks; impl Callbacks for TestAppCallbacks { @@ -64,11 +133,6 @@ async fn main() -> Result<(), Box> { let default_elf_path = "../app/target/riscv32i-unknown-none-elf/release/vnd-test"; let elf_path_str = args.elf.unwrap_or(default_elf_path.to_string()); - let elf_path = Path::new(&elf_path_str); - let elf_file = ElfFile::new(elf_path)?; - - // println!("Entrypoint: {:?}", elf_file.entrypoint); - // println!("{:?}", elf_file); let transport_raw: Arc> + Send + Sync> = if args.hid { @@ -87,38 +151,14 @@ async fn main() -> Result<(), Box> { }; let transport = TransportWrapper::new(transport_raw); - let manifest = Manifest::new( - 0, - "Test", - "0.1.0", - [0u8; 32], // TODO - elf_file.entrypoint, - 65536, // TODO - elf_file.code_segment.start, - elf_file.code_segment.end, - 0xd47a2000 - 65536, // TODO - 0xd47a2000, // TODO - elf_file.data_segment.start, - elf_file.data_segment.end, - [0u8; 32], // TODO - 0, // TODO - ) - .unwrap(); - let callbacks = TestAppCallbacks; - let client = VanadiumClient::new(transport); - let app_hmac = client.register_vapp(&manifest).await?; + let mut test_runner = TestClientRunner::new(transport, &elf_path_str)?; - println!("HMAC: {:?}", app_hmac); + test_runner.register_vapp().await?; + + let status = test_runner.run_vapp(callbacks).await?; - let result = client - .run_vapp(&manifest, &app_hmac, &elf_file, &callbacks) - .await?; - if result.len() != 4 { - return Err("The V-App exited, but did not return correctly return a status".into()); - } - let status = i32::from_be_bytes([result[0], result[1], result[2], result[3]]); println!("App exited with status: {}", status); Ok(()) diff --git a/vm/src/handlers/start_vapp.rs b/vm/src/handlers/start_vapp.rs index bb00864..f555337 100644 --- a/vm/src/handlers/start_vapp.rs +++ b/vm/src/handlers/start_vapp.rs @@ -35,21 +35,21 @@ pub fn handler_start_vapp(comm: &mut io::Comm) -> Result, AppSW> { let code_seg = MemorySegment::::new( manifest.code_start, manifest.code_end - manifest.code_start, - OutsourcedMemory::new(comm.clone(), 5, true, SectionKind::Code), + OutsourcedMemory::new(comm.clone(), 12, true, SectionKind::Code), ) .unwrap(); let data_seg = MemorySegment::::new( manifest.data_start, manifest.data_end - manifest.data_start, - OutsourcedMemory::new(comm.clone(), 10, false, SectionKind::Data), + OutsourcedMemory::new(comm.clone(), 12, false, SectionKind::Data), ) .unwrap(); let stack_seg = MemorySegment::::new( manifest.stack_start, manifest.stack_end - manifest.stack_start, - OutsourcedMemory::new(comm.clone(), 10, false, SectionKind::Stack), + OutsourcedMemory::new(comm.clone(), 12, false, SectionKind::Stack), ) .unwrap(); @@ -63,6 +63,7 @@ pub fn handler_start_vapp(comm: &mut io::Comm) -> Result, AppSW> { let mut ecall_handler = CommEcallHandler::new(comm.clone()); + let mut instr_count = 0; loop { // TODO: handle errors let instr = cpu @@ -70,21 +71,24 @@ pub fn handler_start_vapp(comm: &mut io::Comm) -> Result, AppSW> { .expect("Failed to fetch instruction"); // TODO: remove debug prints - println!("\x1b[93m{:?}\x1b[0m", cpu); + // println!("\x1b[93m{:?}\x1b[0m", cpu); - println!( - "\x1b[32m{:08x?}: {:08x?} -> {:?}\x1b[0m", - cpu.pc, - instr, - common::riscv::decode::decode(instr) - ); + // println!( + // "\x1b[32m{:08x?}: {:08x?} -> {:?}\x1b[0m", + // cpu.pc, + // instr, + // common::riscv::decode::decode(instr) + // ); let result = cpu.execute(instr, Some(&mut ecall_handler)); + instr_count += 1; + match result { Ok(_) => {} Err(common::vm::CpuExecutionError::EcallError(e)) => match e { CommEcallError::Exit(status) => { + println!("Vanadium ran {} instructions", instr_count); println!("Exiting with status {}", status); return Ok(status.to_be_bytes().to_vec()); } From 5aaa35770119d75e2a00618b54b944717c1b71d3 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:37:34 +0200 Subject: [PATCH 04/14] Threaded client --- client-sdk/src/apdu.rs | 10 ++ client-sdk/src/transport.rs | 2 +- client-sdk/src/vanadium_client.rs | 277 +++++++++++++++++------------- common/src/manifest.rs | 12 +- 4 files changed, 175 insertions(+), 126 deletions(-) diff --git a/client-sdk/src/apdu.rs b/client-sdk/src/apdu.rs index 32f9b2f..9b2ca90 100644 --- a/client-sdk/src/apdu.rs +++ b/client-sdk/src/apdu.rs @@ -73,3 +73,13 @@ impl APDUCommand { vec } } + +pub fn apdu_continue(data: Vec) -> APDUCommand { + APDUCommand { + cla: 0xE0, + ins: 0xff, + p1: 0, + p2: 0, + data, + } +} diff --git a/client-sdk/src/transport.rs b/client-sdk/src/transport.rs index d8b4872..63c6ef2 100644 --- a/client-sdk/src/transport.rs +++ b/client-sdk/src/transport.rs @@ -18,7 +18,7 @@ use crate::apdu::{APDUCommand, StatusWord}; /// Communication layer between the bitcoin client and the Ledger device. #[async_trait] -pub trait Transport { +pub trait Transport: Send + Sync { type Error: Debug; async fn exchange(&self, command: &APDUCommand) -> Result<(StatusWord, Vec), Self::Error>; } diff --git a/client-sdk/src/vanadium_client.rs b/client-sdk/src/vanadium_client.rs index a8fc273..ed49f2d 100644 --- a/client-sdk/src/vanadium_client.rs +++ b/client-sdk/src/vanadium_client.rs @@ -1,4 +1,7 @@ use std::cmp::min; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; +use tokio::task::JoinHandle; use common::accumulator::{HashOutput, Hasher, MerkleAccumulator, VectorAccumulator}; use common::client_commands::{ @@ -10,20 +13,10 @@ use common::constants::{page_start, PAGE_SIZE}; use common::manifest::Manifest; use sha2::{Digest, Sha256}; -use crate::apdu::{APDUCommand, StatusWord}; +use crate::apdu::{apdu_continue, APDUCommand, StatusWord}; use crate::elf::ElfFile; use crate::transport::Transport; -fn apdu_continue(data: Vec) -> APDUCommand { - APDUCommand { - cla: 0xE0, - ins: 0xff, - p1: 0, - p2: 0, - data, - } -} - pub struct Sha256Hasher { hasher: Sha256, } @@ -118,23 +111,61 @@ impl MemorySegment { } } -struct VAppEngine<'a> { - manifest: &'a Manifest, +enum VAppMessage { + SendBuffer(Vec), + SendPanicBuffer(String), +} + +enum ClientMessage { + ReceiveBuffer(Vec), +} + +struct VAppEngine { + manifest: Manifest, code_seg: MemorySegment, data_seg: MemorySegment, stack_seg: MemorySegment, - callbacks: &'a dyn Callbacks, + transport: Arc>, + engine_to_client_sender: mpsc::Sender, + client_to_engine_receiver: mpsc::Receiver, } -impl<'a> VAppEngine<'a> { +impl VAppEngine { + pub async fn run(mut self) -> Result<(), &'static str> { + let mut data = + postcard::to_allocvec(&self.manifest).map_err(|_| "manifest serialization failed")?; + + // TODO: need to actually get the app_hmac somehow + let app_hmac = [0x42u8; 32]; + data.extend_from_slice(&app_hmac); + + let command = APDUCommand { + cla: 0xE0, + ins: 3, + p1: 0, + p2: 0, + data, + }; + + let (status, result) = self + .transport + .exchange(&command) + .await + .map_err(|_| "exchange failed")?; + + self.busy_loop(status, result).await?; + + Ok(()) + } + // Sends and APDU and repeatedly processes the response if it's a GetPage or CommitPage client command. // Returns as soon as a different response is received. - async fn exchange_and_process_page_requests( + async fn exchange_and_process_page_requests( &mut self, - transport: &T, apdu: &APDUCommand, ) -> Result<(StatusWord, Vec), &'static str> { - let (mut status, mut result) = transport + let (mut status, mut result) = self + .transport .exchange(apdu) .await .map_err(|_| "exchange failed")?; @@ -145,18 +176,15 @@ impl<'a> VAppEngine<'a> { } let client_command_code: ClientCommandCode = result[0].try_into()?; (status, result) = match client_command_code { - ClientCommandCode::GetPage => self.process_get_page(transport, &result).await?, - ClientCommandCode::CommitPage => { - self.process_commit_page(transport, &result).await? - } + ClientCommandCode::GetPage => self.process_get_page(&result).await?, + ClientCommandCode::CommitPage => self.process_commit_page(&result).await?, _ => return Ok((status, result)), } } } - async fn process_get_page( + async fn process_get_page( &mut self, - transport: &T, command: &[u8], ) -> Result<(StatusWord, Vec), &'static str> { let GetPageMessage { @@ -176,8 +204,8 @@ impl<'a> VAppEngine<'a> { let p1 = data.pop().unwrap(); // return the content of the page - - Ok(transport + Ok(self + .transport .exchange(&APDUCommand { cla: 0xE0, ins: 0xff, @@ -189,9 +217,8 @@ impl<'a> VAppEngine<'a> { .map_err(|_| "exchange failed")?) } - async fn process_commit_page( + async fn process_commit_page( &mut self, - transport: &T, command: &[u8], ) -> Result<(StatusWord, Vec), &'static str> { let msg = CommitPageMessage::deserialize(command)?; @@ -205,7 +232,8 @@ impl<'a> VAppEngine<'a> { }; // get the next message, which contains the content of the page - let (tmp_status, tmp_result) = transport + let (tmp_status, tmp_result) = self + .transport .exchange(&apdu_continue(vec![])) .await .map_err(|_| "exchange failed")?; @@ -223,16 +251,16 @@ impl<'a> VAppEngine<'a> { // TODO: for now we ignore the update proof - Ok(transport + Ok(self + .transport .exchange(&apdu_continue(vec![])) .await .map_err(|_| "exchange failed")?) } - // receive a buffer sent by the V-App via xsend; show it in hex via stdout - async fn process_send_buffer( + // receive a buffer sent by the V-App via xsend; send it to the VappEngine + async fn process_send_buffer( &mut self, - transport: &T, command: &[u8], ) -> Result<(StatusWord, Vec), &'static str> { let SendBufferMessage { @@ -249,7 +277,7 @@ impl<'a> VAppEngine<'a> { while remaining_len > 0 { let (status, result) = self - .exchange_and_process_page_requests(transport, &apdu_continue(vec![])) + .exchange_and_process_page_requests(&apdu_continue(vec![])) .await .map_err(|_| "exchange failed")?; @@ -266,32 +294,37 @@ impl<'a> VAppEngine<'a> { remaining_len -= msg.data.len() as u32; } - self.callbacks.send_buffer(&buf); + // Send the buffer back to the client via engine_to_client_sender + self.engine_to_client_sender + .send(VAppMessage::SendBuffer(buf)) + .await + .map_err(|_| "Failed to send buffer data")?; Ok(self - .exchange_and_process_page_requests(transport, &apdu_continue(vec![])) + .exchange_and_process_page_requests(&apdu_continue(vec![])) .await .map_err(|_| "exchange failed")?) } - // the V-App is expecting a buffer via xrecv; get it in hex from standard input, and send it to the V-App - async fn process_receive_buffer( + // the V-App is expecting a buffer via xrecv; get it from the VAppEngine, and send it to the V-App + async fn process_receive_buffer( &mut self, - transport: &T, command: &[u8], ) -> Result<(StatusWord, Vec), &'static str> { ReceiveBufferMessage::deserialize(command)?; - let bytes = self - .callbacks - .receive_buffer() - .map_err(|_| "receive buffer callback failed")?; + // Wait for the message from the client + let ClientMessage::ReceiveBuffer(bytes) = self + .client_to_engine_receiver + .recv() + .await + .ok_or("Failed to receive buffer from client")?; let mut remaining_len = bytes.len() as u32; let mut offset: usize = 0; loop { - // TODO: wrong if the buffer is long + // TODO: check if correct when the buffer is long let chunk_len = min(remaining_len, 255 - 4); let data = ReceiveBufferResponse::new( remaining_len, @@ -300,7 +333,7 @@ impl<'a> VAppEngine<'a> { .serialize(); let (status, result) = self - .exchange_and_process_page_requests(transport, &apdu_continue(data)) + .exchange_and_process_page_requests(&apdu_continue(data)) .await .map_err(|_| "exchange failed")?; @@ -318,14 +351,12 @@ impl<'a> VAppEngine<'a> { ReceiveBufferMessage::deserialize(&result)?; } } - // return Ok((status, result)); } - // receive a buffer sent by the V-App during a panic; show it to stdout + // receive a buffer sent by the V-App during a panic; send it to the VAppEngine // TODO: almost identical to process_send_buffer; it might be nice to refactor - async fn process_send_panic_buffer( + async fn process_send_panic_buffer( &mut self, - transport: &T, command: &[u8], ) -> Result<(StatusWord, Vec), &'static str> { let SendPanicBufferMessage { @@ -342,7 +373,7 @@ impl<'a> VAppEngine<'a> { while remaining_len > 0 { let (status, result) = self - .exchange_and_process_page_requests(transport, &apdu_continue(vec![])) + .exchange_and_process_page_requests(&apdu_continue(vec![])) .await .map_err(|_| "exchange failed")?; @@ -359,32 +390,33 @@ impl<'a> VAppEngine<'a> { remaining_len -= msg.data.len() as u32; } - println!( - "Received panic message:\n{}", - core::str::from_utf8(&buf).unwrap() - ); + let panic_message = String::from_utf8(buf).map_err(|_| "Invalid UTF-8 in panic message")?; + // Send the panic message back to the client via engine_to_client_sender + self.engine_to_client_sender + .send(VAppMessage::SendPanicBuffer(panic_message)) + .await + .map_err(|_| "Failed to send panic message")?; + + // Continue processing Ok(self - .exchange_and_process_page_requests(transport, &apdu_continue(vec![])) + .exchange_and_process_page_requests(&apdu_continue(vec![])) .await .map_err(|_| "exchange failed")?) } - async fn busy_loop( + async fn busy_loop( &mut self, - transport: &T, first_sw: StatusWord, first_result: Vec, - ) -> Result, &'static str> { - // create the pages in the code segment. Each page is aligned to PAGE_SIZE (the first page is zero padded if the initial address is not divisible by - // PAGE_SIZE, and the last page is zero_padded if it's smaller than PAGE_SIZE).\ - + ) -> Result<(), &'static str> { let mut status = first_sw; let mut result = first_result; loop { if status == StatusWord::OK { - return Ok(result); + // TODO: how to send the status word back in response? + return Ok(()); } if status == StatusWord::VMRuntimeError { @@ -406,48 +438,42 @@ impl<'a> VAppEngine<'a> { let client_command_code: ClientCommandCode = result[0].try_into()?; (status, result) = match client_command_code { - ClientCommandCode::GetPage => self.process_get_page(transport, &result).await?, - ClientCommandCode::CommitPage => { - self.process_commit_page(transport, &result).await? - } + ClientCommandCode::GetPage => self.process_get_page(&result).await?, + ClientCommandCode::CommitPage => self.process_commit_page(&result).await?, ClientCommandCode::CommitPageContent => { // not a top-level command, part of CommitPage handling return Err("Unexpected CommitPageContent client command"); } - ClientCommandCode::SendBuffer => { - self.process_send_buffer(transport, &result).await? - } - ClientCommandCode::ReceiveBuffer => { - self.process_receive_buffer(transport, &result).await? - } + ClientCommandCode::SendBuffer => self.process_send_buffer(&result).await?, + ClientCommandCode::ReceiveBuffer => self.process_receive_buffer(&result).await?, ClientCommandCode::SendPanicBuffer => { - self.process_send_panic_buffer(transport, &result).await? + self.process_send_panic_buffer(&result).await? } } } } } -pub struct VanadiumClient { - transport: T, -} - -pub enum ReceiveBufferError { - ReceiveFailed, // TODO: do we need to distinguish between more errors? -} - -pub trait Callbacks { - fn receive_buffer(&self) -> Result, ReceiveBufferError>; - fn send_buffer(&self, buffer: &[u8]); - fn send_panic(&self, msg: &[u8]); +pub struct VanadiumClient { + client_to_engine_sender: Option>, + engine_to_client_receiver: Option>>, + vapp_engine_handle: Option>>, } -impl VanadiumClient { - pub fn new(transport: T) -> Self { - Self { transport } +impl VanadiumClient { + pub fn new() -> Self { + Self { + client_to_engine_sender: None, + engine_to_client_receiver: None, + vapp_engine_handle: None, + } } - pub async fn register_vapp(&self, manifest: &Manifest) -> Result<[u8; 32], &'static str> { + pub async fn register_vapp( + &self, + transport: &T, + manifest: &Manifest, + ) -> Result<[u8; 32], &'static str> { let command = APDUCommand { cla: 0xE0, ins: 2, @@ -456,20 +482,16 @@ impl VanadiumClient { data: postcard::to_allocvec(manifest).map_err(|_| "manifest serialization failed")?, }; - let (status, result) = self - .transport + let (status, result) = transport .exchange(&command) .await .map_err(|_| "exchange failed")?; match status { StatusWord::OK => { - // fail if the response is not exactly 32 bytes; otherwise return it as a [u8; 32] if result.len() != 32 { return Err("Invalid response length"); } - - // convert result to a [u8; 32] let mut hmac = [0u8; 32]; hmac.copy_from_slice(&result); Ok(hmac) @@ -478,14 +500,13 @@ impl VanadiumClient { } } - pub async fn run_vapp( - &self, + pub fn run_vapp( + &mut self, + transport: Arc>, manifest: &Manifest, app_hmac: &[u8; 32], elf: &ElfFile, - callbacks: &C, - ) -> Result, &'static str> { - // concatenate the serialized manifest and the app_hmac + ) -> Result<(), &'static str> { let mut data = postcard::to_allocvec(manifest).map_err(|_| "manifest serialization failed")?; data.extend_from_slice(app_hmac); @@ -498,33 +519,51 @@ impl VanadiumClient { &vec![0; (manifest.stack_end - manifest.stack_start) as usize], )?; - let mut vapp_engine = VAppEngine { - manifest, + let (client_to_engine_sender, client_to_engine_receiver) = + mpsc::channel::(10); + let (engine_to_client_sender, engine_to_client_receiver) = mpsc::channel::(10); + + let vapp_engine = VAppEngine { + manifest: manifest.clone(), code_seg, data_seg, stack_seg, - callbacks, + transport, + engine_to_client_sender, + client_to_engine_receiver, }; - // initial APDU to start the V-App - let command = APDUCommand { - cla: 0xE0, - ins: 3, - p1: 0, - p2: 0, - data, - }; + // Start the VAppEngine in a task + let vapp_engine_handle = tokio::spawn(async move { vapp_engine.run().await }); - let (status, result) = self - .transport - .exchange(&command) - .await - .map_err(|_| "exchange failed")?; + // Store the senders and receivers + self.client_to_engine_sender = Some(client_to_engine_sender); + self.engine_to_client_receiver = Some(Mutex::new(engine_to_client_receiver)); + self.vapp_engine_handle = Some(vapp_engine_handle); - let result = vapp_engine - .busy_loop(&self.transport, status, result) - .await?; + Ok(()) + } - Ok(result) + pub async fn send_message(&mut self, message: Vec) -> Result, String> { + // 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)) + .await + .map_err(|_| "Failed to send message to VAppEngine")?; + + // Wait for the response from VAppEngine + match self.engine_to_client_receiver.as_mut() { + Some(engine_to_client_receiver) => { + let mut receiver = engine_to_client_receiver.lock().await; + match receiver.recv().await { + Some(VAppMessage::SendBuffer(buf)) => Ok(buf), + Some(VAppMessage::SendPanicBuffer(panic_msg)) => Err(panic_msg), + None => Err("VAppEngine stopped".to_string()), + } + } + None => Err("VAppEngine not running".to_string()), + } } } diff --git a/common/src/manifest.rs b/common/src/manifest.rs index 2150ce5..b2883c1 100644 --- a/common/src/manifest.rs +++ b/common/src/manifest.rs @@ -1,11 +1,11 @@ -use serde::{self, Serialize, Deserialize}; +use serde::{self, Deserialize, Serialize}; const APP_NAME_LEN: usize = 32; // Define a suitable length const APP_VERSION_LEN: usize = 32; // Define a suitable length // TODO: copied from vanadium-legacy without much thought; fields are subject to change /// The manifest contains all the required info that the application needs in order to execute a V-App. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Manifest { pub manifest_version: u32, pub app_name: [u8; APP_NAME_LEN], @@ -50,13 +50,13 @@ impl Manifest { let mut app_name_arr = [0u8; APP_NAME_LEN]; let mut app_version_arr = [0u8; APP_VERSION_LEN]; - + let name_bytes = app_name.as_bytes(); let version_bytes = app_version.as_bytes(); - + app_name_arr[..name_bytes.len()].copy_from_slice(name_bytes); app_version_arr[..version_bytes.len()].copy_from_slice(version_bytes); - + Ok(Self { manifest_version, app_name: app_name_arr, @@ -74,4 +74,4 @@ impl Manifest { mt_size, }) } -} \ No newline at end of file +} From 22105639d63893cd47bafc2f89cb8dffac70e059 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:32:43 +0200 Subject: [PATCH 05/14] Refactored vnd-test-client using the new sdk code --- apps/test/app/src/commands.rs | 7 +- apps/test/app/src/main.rs | 2 +- apps/test/client/src/client.rs | 117 +++++++++++++++++ apps/test/client/src/commands.rs | 7 +- apps/test/client/src/main.rs | 208 +++++++++++++++---------------- 5 files changed, 231 insertions(+), 110 deletions(-) create mode 100644 apps/test/client/src/client.rs diff --git a/apps/test/app/src/commands.rs b/apps/test/app/src/commands.rs index 63c899b..64df67f 100644 --- a/apps/test/app/src/commands.rs +++ b/apps/test/app/src/commands.rs @@ -1,7 +1,10 @@ +// Duplicated for simplicity with the corresponding module in the client crate. +// Make sure to keep them in sync. + #[derive(Debug)] pub enum Command { Reverse, - Sum, + AddNumbers, Base58Encode, Sha256, CountPrimes, @@ -13,7 +16,7 @@ impl TryFrom for Command { fn try_from(byte: u8) -> Result { match byte { 0x00 => Ok(Command::Reverse), - 0x01 => Ok(Command::Sum), + 0x01 => Ok(Command::AddNumbers), 0x02 => Ok(Command::Base58Encode), 0x03 => Ok(Command::Sha256), 0x04 => Ok(Command::CountPrimes), diff --git a/apps/test/app/src/main.rs b/apps/test/app/src/main.rs index 56f5e1c..c45c86d 100644 --- a/apps/test/app/src/main.rs +++ b/apps/test/app/src/main.rs @@ -78,7 +78,7 @@ pub fn main(_: isize, _: *const *const u8) -> isize { data.reverse(); data } - Command::Sum => { + Command::AddNumbers => { // sum all the numbers from 0 to n if msg.len() != 5 { panic!("Invalid input"); diff --git a/apps/test/client/src/client.rs b/apps/test/client/src/client.rs new file mode 100644 index 0000000..40297e7 --- /dev/null +++ b/apps/test/client/src/client.rs @@ -0,0 +1,117 @@ +use crate::commands::Command; +use sdk::{ + elf::ElfFile, manifest::Manifest, transport::Transport, vanadium_client::VanadiumClient, +}; +use std::path::Path; +use std::sync::Arc; + +pub struct TestClient { + client: VanadiumClient, + elf_file: ElfFile, + manifest: Manifest, + app_hmac: Option<[u8; 32]>, +} + +impl TestClient { + pub fn new(elf_path: &str) -> Result { + // TODO: some of this should be moved to the client-sdk + let elf_file = ElfFile::new(Path::new(&elf_path))?; + + let manifest = Manifest::new( + 0, + "Test", + "0.1.0", + [0u8; 32], // TODO + elf_file.entrypoint, + 65536, // TODO + elf_file.code_segment.start, + elf_file.code_segment.end, + 0xd47a2000 - 65536, // TODO + 0xd47a2000, // TODO + elf_file.data_segment.start, + elf_file.data_segment.end, + [0u8; 32], // TODO + 0, // TODO + ) + .unwrap(); + + Ok(Self { + client: VanadiumClient::new(), + elf_file, + manifest, + app_hmac: None, + }) + } + + pub async fn register_vapp( + &mut self, + transport: &T, + ) -> Result<[u8; 32], Box> { + let app_hmac = self.client.register_vapp(transport, &self.manifest).await?; + self.app_hmac = Some(app_hmac); + + Ok(app_hmac) + } + + pub fn run_vapp( + &mut self, + transport: Arc>, + ) -> Result<(), Box> { + let app_hmac = self.app_hmac.ok_or("V-App not registered")?; + + self.client + .run_vapp(transport, &self.manifest, &app_hmac, &self.elf_file)?; + + Ok(()) + } + + pub async fn reverse(&mut self, data: &[u8]) -> Result, &'static str> { + let mut msg: Vec = Vec::new(); + msg.extend_from_slice(&[Command::Reverse as u8]); + msg.extend_from_slice(data); + + Ok(self.client.send_message(msg).await.map_err(|_| "Failed")?) + } + + pub async fn add_numbers(&mut self, n: u32) -> Result { + let mut msg: Vec = Vec::new(); + msg.extend_from_slice(&[Command::AddNumbers as u8]); + msg.extend_from_slice(&n.to_be_bytes()); + + let result_raw = self.client.send_message(msg).await.map_err(|_| "Failed")?; + + if result_raw.len() != 8 { + return Err("Invalid response length"); + } + Ok(u64::from_be_bytes(result_raw.try_into().unwrap())) + } + + pub async fn sha256(&mut self, data: &[u8]) -> Result, &'static str> { + let mut msg: Vec = Vec::new(); + msg.extend_from_slice(&[Command::Sha256 as u8]); + msg.extend_from_slice(data); + + Ok(self.client.send_message(msg).await.map_err(|_| "Failed")?) + } + + pub async fn b58enc(&mut self, data: &[u8]) -> Result, &'static str> { + let mut msg: Vec = Vec::new(); + msg.extend_from_slice(&[Command::Base58Encode as u8]); + msg.extend_from_slice(data); + + Ok(self.client.send_message(msg).await.map_err(|_| "Failed")?) + } + + pub async fn nprimes(&mut self, n: u32) -> Result { + let mut msg: Vec = Vec::new(); + msg.extend_from_slice(&[Command::CountPrimes as u8]); + msg.extend_from_slice(&n.to_be_bytes()); + + let result_raw = self.client.send_message(msg).await.map_err(|_| "Failed")?; + + if result_raw.len() != 4 { + return Err("Invalid response length"); + } + Ok(u32::from_be_bytes(result_raw.try_into().unwrap())) + } +} diff --git a/apps/test/client/src/commands.rs b/apps/test/client/src/commands.rs index 63c899b..8030d36 100644 --- a/apps/test/client/src/commands.rs +++ b/apps/test/client/src/commands.rs @@ -1,7 +1,10 @@ +// Duplicated for simplicity with the corresponding module in the app crate. +// Make sure to keep them in sync. + #[derive(Debug)] pub enum Command { Reverse, - Sum, + AddNumbers, Base58Encode, Sha256, CountPrimes, @@ -13,7 +16,7 @@ impl TryFrom for Command { fn try_from(byte: u8) -> Result { match byte { 0x00 => Ok(Command::Reverse), - 0x01 => Ok(Command::Sum), + 0x01 => Ok(Command::AddNumbers), 0x02 => Ok(Command::Base58Encode), 0x03 => Ok(Command::Sha256), 0x04 => Ok(Command::CountPrimes), diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index 76635ee..935a0f5 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -1,21 +1,17 @@ use clap::Parser; +use client::TestClient; use hidapi::HidApi; use ledger_transport_hid::TransportNativeHID; -use sdk::{ - elf::ElfFile, - manifest::Manifest, - transport::{Transport, TransportHID, TransportTcp, TransportWrapper}, - vanadium_client::{Callbacks, ReceiveBufferError, VanadiumClient}, -}; +use sdk::transport::{Transport, TransportHID, TransportTcp, TransportWrapper}; mod commands; -use commands::Command; +mod client; -use std::io::{stdin, Write}; +use std::io::BufRead; use std::sync::Arc; -use std::{io::stdout, path::Path}; + #[derive(Parser)] #[command(name = "Vanadium", about = "Run a V-App on Vanadium")] struct Args { @@ -27,103 +23,64 @@ struct Args { hid: bool, } -// TODO: maybe this can be made generic enough to move to the client-sdk -struct TestClientRunner { - client: VanadiumClient, - elf_file: ElfFile, - manifest: Manifest, - app_hmac: Option<[u8; 32]>, +enum CliCommand { + Reverse(Vec), + AddNumbers(u32), + Sha256(Vec), + B58Enc(Vec), + NPrimes(u32), + Exit, } -impl TestClientRunner { - pub fn new(transport: T, elf_path: &str) -> Result { - let elf_file = ElfFile::new(Path::new(&elf_path))?; - - let manifest = Manifest::new( - 0, - "Test", - "0.1.0", - [0u8; 32], // TODO - elf_file.entrypoint, - 65536, // TODO - elf_file.code_segment.start, - elf_file.code_segment.end, - 0xd47a2000 - 65536, // TODO - 0xd47a2000, // TODO - elf_file.data_segment.start, - elf_file.data_segment.end, - [0u8; 32], // TODO - 0, // TODO - ) - .unwrap(); - - Ok(Self { - client: VanadiumClient::new(transport), - elf_file, - manifest, - app_hmac: None, - }) +/// Parses a hex-encoded string into a vector of bytes. +fn parse_hex_buffer(s: &str) -> Result, String> { + if s.len() % 2 != 0 { + return Err("Hex string has an odd length".to_string()); } + (0..s.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&s[i..i + 2], 16) + .map_err(|_| format!("Invalid hex character at position {}", i)) + }) + .collect() +} - pub async fn register_vapp(&mut self) -> Result<[u8; 32], Box> { - let app_hmac = self.client.register_vapp(&self.manifest).await?; - self.app_hmac = Some(app_hmac); - - Ok(app_hmac) - } - - pub async fn run_vapp( - &mut self, - callbacks: TestAppCallbacks, - ) -> Result> { - let app_hmac = self.app_hmac.ok_or("V-App not registered")?; - - let result = self - .client - .run_vapp(&self.manifest, &app_hmac, &self.elf_file, &callbacks) - .await?; - if result.len() != 4 { - panic!("The V-App exited, but did not return correctly return a status"); - } - let status = i32::from_be_bytes([result[0], result[1], result[2], result[3]]); - println!("App exited with status: {}", status); - - Ok(status) - } +/// Parses a string into a u32 integer. +fn parse_u32(s: &str) -> Result { + s.parse::() + .map_err(|_| "Invalid u32 integer".to_string()) } -struct TestAppCallbacks; - -impl Callbacks for TestAppCallbacks { - fn receive_buffer(&self) -> Result, ReceiveBufferError> { - // Prompt the user to input a data buffer in hex; send it to the V-App - let mut buffer = String::new(); - let bytes = loop { - print!("Enter a data buffer in hexadecimal: "); - stdout().flush().unwrap(); - buffer.clear(); - stdin().read_line(&mut buffer).unwrap(); - buffer = buffer.trim().to_string(); - - if let Ok(bytes) = hex::decode(&buffer) { - break bytes; - } else { - println!("Invalid hexadecimal input. Please try again."); +fn parse_command(line: &str) -> Result { + let mut tokens = line.trim().split_whitespace(); + if let Some(command) = tokens.next() { + match command { + "reverse" | "sha256" | "b58enc" | "b58encode" => { + let arg = tokens.next().unwrap_or(""); + let buffer = parse_hex_buffer(arg).map_err(|e| e.to_string())?; + match command { + "reverse" => Ok(CliCommand::Reverse(buffer)), + "sha256" => Ok(CliCommand::Sha256(buffer)), + "b58enc" => Ok(CliCommand::B58Enc(buffer)), + _ => unreachable!(), + } } - }; - - Ok(bytes) - } - - fn send_buffer(&self, buffer: &[u8]) { - println!("Received buffer: {}", hex::encode(&buffer)); - } - - fn send_panic(&self, msg: &[u8]) { - println!( - "Received panic message:\n{}", - core::str::from_utf8(&msg).unwrap() - ); + "addnumbers" | "nprimes" => { + let arg = tokens + .next() + .ok_or_else(|| format!("'{}' requires a u32 integer argument", command))?; + let number = parse_u32(arg).map_err(|e| e.to_string())?; + match command { + "addnumbers" => Ok(CliCommand::AddNumbers(number)), + "nprimes" => Ok(CliCommand::NPrimes(number)), + _ => unreachable!(), + } + } + _ => Err(format!("Unknown command: '{}'", command)), + } + } else { + return Ok(CliCommand::Exit); } } @@ -149,17 +106,58 @@ async fn main() -> Result<(), Box> { .expect("Unable to get TCP transport. Is speculos running?"), ) }; - let transport = TransportWrapper::new(transport_raw); + let transport = TransportWrapper::new(transport_raw.clone()); + + let mut test_client = TestClient::new(&elf_path_str)?; + + println!("Registering V-App"); + test_client.register_vapp(&transport).await?; - let callbacks = TestAppCallbacks; + test_client.run_vapp(transport_raw)?; - let mut test_runner = TestClientRunner::new(transport, &elf_path_str)?; + println!("App is running"); - test_runner.register_vapp().await?; + loop { + println!("Enter a command:"); - let status = test_runner.run_vapp(callbacks).await?; + let mut line = String::new(); + std::io::stdin() + .lock() + .read_line(&mut line) + .expect("Failed to read line"); + + if line.trim().is_empty() { + break; + } + + match parse_command(&line) { + Ok(cmd) => match cmd { + CliCommand::Reverse(arg) => { + println!("{}", hex::encode(test_client.reverse(&arg).await?)); + } + CliCommand::AddNumbers(number) => { + println!("{}", test_client.add_numbers(number).await?); + } + CliCommand::Sha256(arg) => { + println!("{}", hex::encode(test_client.sha256(&arg).await?)); + } + CliCommand::B58Enc(arg) => { + println!("{}", hex::encode(test_client.b58enc(&arg).await?)); + } + CliCommand::NPrimes(n) => { + println!("{}", test_client.nprimes(n).await?); + } + CliCommand::Exit => { + break; + } + }, + Err(e) => { + println!("Error: {}", e); + } + } + } - println!("App exited with status: {}", status); + // TODO: how to cleanly close the app? It doesn't make sense to keep running if the host exits. Ok(()) } From f94af327372fab072dd30d6558078a278a0d9eda Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:49:41 +0200 Subject: [PATCH 06/14] Combine lib and binary crate for the client --- apps/test/client/Cargo.toml | 9 +++++++++ apps/test/client/src/lib.rs | 4 ++++ 2 files changed, 13 insertions(+) create mode 100644 apps/test/client/src/lib.rs diff --git a/apps/test/client/Cargo.toml b/apps/test/client/Cargo.toml index d22dae6..e609874 100644 --- a/apps/test/client/Cargo.toml +++ b/apps/test/client/Cargo.toml @@ -11,4 +11,13 @@ ledger-transport-hid = "0.11.0" sdk = { package = "vanadium-client-sdk", path = "../../../client-sdk"} tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "sync"] } +[lib] +name = "vnd_test_client" +path = "src/lib.rs" + +[[bin]] +name = "vnd_test_cli" +path = "src/main.rs" + + [workspace] diff --git a/apps/test/client/src/lib.rs b/apps/test/client/src/lib.rs new file mode 100644 index 0000000..5708251 --- /dev/null +++ b/apps/test/client/src/lib.rs @@ -0,0 +1,4 @@ +mod client; +mod commands; + +pub use client::TestClient; From 04739f00f64c43340d46d29f89996d7f34d51d7d Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:34:41 +0200 Subject: [PATCH 07/14] Correctly manage app exit and exit code --- apps/test/client/src/client.rs | 13 +++++++++++ apps/test/client/src/main.rs | 10 ++++----- client-sdk/src/vanadium_client.rs | 36 ++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/apps/test/client/src/client.rs b/apps/test/client/src/client.rs index 40297e7..6bea5cb 100644 --- a/apps/test/client/src/client.rs +++ b/apps/test/client/src/client.rs @@ -1,4 +1,5 @@ use crate::commands::Command; +use sdk::vanadium_client::VanadiumClientError; use sdk::{ elf::ElfFile, manifest::Manifest, transport::Transport, vanadium_client::VanadiumClient, }; @@ -114,4 +115,16 @@ impl TestClient { } Ok(u32::from_be_bytes(result_raw.try_into().unwrap())) } + + pub async fn exit(&mut self) -> Result { + match self.client.send_message(Vec::new()).await { + Ok(_) => { + return Err("Exit message shouldn't return!"); + } + Err(e) => match e { + VanadiumClientError::VAppExited(status) => Ok(status), + _ => Err("Unexpected error"), + }, + } + } } diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index 935a0f5..152a9fd 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -126,10 +126,6 @@ async fn main() -> Result<(), Box> { .read_line(&mut line) .expect("Failed to read line"); - if line.trim().is_empty() { - break; - } - match parse_command(&line) { Ok(cmd) => match cmd { CliCommand::Reverse(arg) => { @@ -148,6 +144,10 @@ async fn main() -> Result<(), Box> { println!("{}", test_client.nprimes(n).await?); } CliCommand::Exit => { + let status = test_client.exit().await?; + if status != 0 { + std::process::exit(status); + } break; } }, @@ -157,7 +157,5 @@ async fn main() -> Result<(), Box> { } } - // TODO: how to cleanly close the app? It doesn't make sense to keep running if the host exits. - Ok(()) } diff --git a/client-sdk/src/vanadium_client.rs b/client-sdk/src/vanadium_client.rs index ed49f2d..e60ef71 100644 --- a/client-sdk/src/vanadium_client.rs +++ b/client-sdk/src/vanadium_client.rs @@ -114,6 +114,7 @@ impl MemorySegment { enum VAppMessage { SendBuffer(Vec), SendPanicBuffer(String), + VAppExited { status: i32 }, } enum ClientMessage { @@ -415,7 +416,14 @@ impl VAppEngine { loop { if status == StatusWord::OK { - // TODO: how to send the status word back in response? + if result.len() != 4 { + return Err("The V-App should return a 4-byte exit code"); + } + let st = i32::from_be_bytes(result.try_into().unwrap()); + self.engine_to_client_sender + .send(VAppMessage::VAppExited { status: st }) + .await + .map_err(|_| "Failed to send exit code")?; return Ok(()); } @@ -460,6 +468,19 @@ pub struct VanadiumClient { vapp_engine_handle: Option>>, } +#[derive(Debug)] +pub enum VanadiumClientError { + VAppPanicked(String), + VAppExited(i32), + GenericError(String), +} + +impl From<&str> for VanadiumClientError { + fn from(s: &str) -> Self { + VanadiumClientError::GenericError(s.to_string()) + } +} + impl VanadiumClient { pub fn new() -> Self { Self { @@ -544,7 +565,7 @@ impl VanadiumClient { Ok(()) } - pub async fn send_message(&mut self, message: Vec) -> Result, String> { + pub async fn send_message(&mut self, message: Vec) -> Result, VanadiumClientError> { // Send the message to VAppEngine when receive_buffer is called self.client_to_engine_sender .as_ref() @@ -559,11 +580,16 @@ impl VanadiumClient { let mut receiver = engine_to_client_receiver.lock().await; match receiver.recv().await { Some(VAppMessage::SendBuffer(buf)) => Ok(buf), - Some(VAppMessage::SendPanicBuffer(panic_msg)) => Err(panic_msg), - None => Err("VAppEngine stopped".to_string()), + Some(VAppMessage::SendPanicBuffer(panic_msg)) => { + Err(VanadiumClientError::VAppPanicked(panic_msg)) + } + Some(VAppMessage::VAppExited { status }) => { + Err(VanadiumClientError::VAppExited(status)) + } + None => Err("VAppEngine stopped".into()), } } - None => Err("VAppEngine not running".to_string()), + None => Err("VAppEngine not running".into()), } } } From 7a10e0f7639a9b6354ce6b349b7a301adaa0807e Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:39:03 +0200 Subject: [PATCH 08/14] Add README to test V-App --- apps/test/README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/apps/test/README.md b/apps/test/README.md index f5d3ac4..24f5cc4 100644 --- a/apps/test/README.md +++ b/apps/test/README.md @@ -1,4 +1,52 @@ This will be a test V-app. - [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. \ No newline at end of file +- [client](client) folder contains the client of the app, based on the V-app Client Sdk. + +## 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 on Vanadium + +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 + ``` + +### Client commands + +Once the client is running, these are the available commands: + +- `reverse ` - Reversed 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 Erathostenes. +- An empty command will exit the V-App. From 47bb497799b10276d713f001960a15f64a801792 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:16:11 +0200 Subject: [PATCH 09/14] Separate the V-App client from the communication interface; allow execution of the app client with any interface; allow running the test app natively --- apps/test/README.md | 8 +- apps/test/client/src/client.rs | 98 ++++-------- apps/test/client/src/main.rs | 42 ++++-- client-sdk/Cargo.toml | 3 +- client-sdk/src/transport.rs | 12 +- client-sdk/src/vanadium_client.rs | 242 +++++++++++++++++++++++++++++- 6 files changed, 306 insertions(+), 99 deletions(-) diff --git a/apps/test/README.md b/apps/test/README.md index 24f5cc4..602d7b6 100644 --- a/apps/test/README.md +++ b/apps/test/README.md @@ -29,7 +29,6 @@ Launch Vanadium on speculos. Then execute: From the `client` folder - ```sh cargo run ``` @@ -40,6 +39,13 @@ If you want to run the V-app on a real device, execute instead: cargo run -- --hid ``` +If you want to run the V-app natively, use: + + ```sh + cargo run -- --native + ``` + + ### Client commands Once the client is running, these are the available commands: diff --git a/apps/test/client/src/client.rs b/apps/test/client/src/client.rs index 6bea5cb..b07d679 100644 --- a/apps/test/client/src/client.rs +++ b/apps/test/client/src/client.rs @@ -1,69 +1,13 @@ use crate::commands::Command; -use sdk::vanadium_client::VanadiumClientError; -use sdk::{ - elf::ElfFile, manifest::Manifest, transport::Transport, vanadium_client::VanadiumClient, -}; -use std::path::Path; -use std::sync::Arc; +use sdk::vanadium_client::{VAppClient, VAppExecutionError}; pub struct TestClient { - client: VanadiumClient, - elf_file: ElfFile, - manifest: Manifest, - app_hmac: Option<[u8; 32]>, + app_client: Box, } impl TestClient { - pub fn new(elf_path: &str) -> Result { - // TODO: some of this should be moved to the client-sdk - let elf_file = ElfFile::new(Path::new(&elf_path))?; - - let manifest = Manifest::new( - 0, - "Test", - "0.1.0", - [0u8; 32], // TODO - elf_file.entrypoint, - 65536, // TODO - elf_file.code_segment.start, - elf_file.code_segment.end, - 0xd47a2000 - 65536, // TODO - 0xd47a2000, // TODO - elf_file.data_segment.start, - elf_file.data_segment.end, - [0u8; 32], // TODO - 0, // TODO - ) - .unwrap(); - - Ok(Self { - client: VanadiumClient::new(), - elf_file, - manifest, - app_hmac: None, - }) - } - - pub async fn register_vapp( - &mut self, - transport: &T, - ) -> Result<[u8; 32], Box> { - let app_hmac = self.client.register_vapp(transport, &self.manifest).await?; - self.app_hmac = Some(app_hmac); - - Ok(app_hmac) - } - - pub fn run_vapp( - &mut self, - transport: Arc>, - ) -> Result<(), Box> { - let app_hmac = self.app_hmac.ok_or("V-App not registered")?; - - self.client - .run_vapp(transport, &self.manifest, &app_hmac, &self.elf_file)?; - - Ok(()) + pub fn new(app_client: Box) -> Self { + Self { app_client } } pub async fn reverse(&mut self, data: &[u8]) -> Result, &'static str> { @@ -71,7 +15,11 @@ impl TestClient { msg.extend_from_slice(&[Command::Reverse as u8]); msg.extend_from_slice(data); - Ok(self.client.send_message(msg).await.map_err(|_| "Failed")?) + Ok(self + .app_client + .send_message(msg) + .await + .map_err(|_| "Failed")?) } pub async fn add_numbers(&mut self, n: u32) -> Result { @@ -79,7 +27,11 @@ impl TestClient { msg.extend_from_slice(&[Command::AddNumbers as u8]); msg.extend_from_slice(&n.to_be_bytes()); - let result_raw = self.client.send_message(msg).await.map_err(|_| "Failed")?; + let result_raw = self + .app_client + .send_message(msg) + .await + .map_err(|_| "Failed")?; if result_raw.len() != 8 { return Err("Invalid response length"); @@ -92,7 +44,11 @@ impl TestClient { msg.extend_from_slice(&[Command::Sha256 as u8]); msg.extend_from_slice(data); - Ok(self.client.send_message(msg).await.map_err(|_| "Failed")?) + Ok(self + .app_client + .send_message(msg) + .await + .map_err(|_| "Failed")?) } pub async fn b58enc(&mut self, data: &[u8]) -> Result, &'static str> { @@ -100,7 +56,11 @@ impl TestClient { msg.extend_from_slice(&[Command::Base58Encode as u8]); msg.extend_from_slice(data); - Ok(self.client.send_message(msg).await.map_err(|_| "Failed")?) + Ok(self + .app_client + .send_message(msg) + .await + .map_err(|_| "Failed")?) } pub async fn nprimes(&mut self, n: u32) -> Result { @@ -108,7 +68,11 @@ impl TestClient { msg.extend_from_slice(&[Command::CountPrimes as u8]); msg.extend_from_slice(&n.to_be_bytes()); - let result_raw = self.client.send_message(msg).await.map_err(|_| "Failed")?; + let result_raw = self + .app_client + .send_message(msg) + .await + .map_err(|_| "Failed")?; if result_raw.len() != 4 { return Err("Invalid response length"); @@ -117,12 +81,12 @@ impl TestClient { } pub async fn exit(&mut self) -> Result { - match self.client.send_message(Vec::new()).await { + match self.app_client.send_message(Vec::new()).await { Ok(_) => { return Err("Exit message shouldn't return!"); } Err(e) => match e { - VanadiumClientError::VAppExited(status) => Ok(status), + VAppExecutionError::AppExited(status) => Ok(status), _ => Err("Unexpected error"), }, } diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index 152a9fd..080fb1e 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -4,6 +4,7 @@ use hidapi::HidApi; use ledger_transport_hid::TransportNativeHID; use sdk::transport::{Transport, TransportHID, TransportTcp, TransportWrapper}; +use sdk::vanadium_client::{NativeAppClient, VanadiumAppClient}; mod commands; @@ -16,11 +17,15 @@ use std::sync::Arc; #[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) - elf: Option, + app: Option, /// Use the HID interface for a real device, instead of Speculos - #[arg(long)] + #[arg(long, group = "interface")] hid: bool, + + /// Use the native interface + #[arg(long, group = "interface")] + native: bool, } enum CliCommand { @@ -85,14 +90,23 @@ fn parse_command(line: &str) -> Result { } #[tokio::main(flavor = "current_thread")] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Box> { let args = Args::parse(); - let default_elf_path = "../app/target/riscv32i-unknown-none-elf/release/vnd-test"; - let elf_path_str = args.elf.unwrap_or(default_elf_path.to_string()); + let default_app_path = if args.native { + "../app/target/x86_64-unknown-linux-gnu/release/vnd-test" + } else { + "../app/target/riscv32i-unknown-none-elf/release/vnd-test" + }; + + let app_path_str = args.app.unwrap_or(default_app_path.to_string()); - let transport_raw: Arc> + Send + Sync> = - if args.hid { + let mut test_client = if args.native { + TestClient::new(Box::new(NativeAppClient::new(&app_path_str).await?)) + } 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"), @@ -106,16 +120,12 @@ async fn main() -> Result<(), Box> { .expect("Unable to get TCP transport. Is speculos running?"), ) }; - let transport = TransportWrapper::new(transport_raw.clone()); - - let mut test_client = TestClient::new(&elf_path_str)?; - - println!("Registering V-App"); - test_client.register_vapp(&transport).await?; - - test_client.run_vapp(transport_raw)?; + let transport = TransportWrapper::new(transport_raw.clone()); - println!("App is running"); + TestClient::new(Box::new( + VanadiumAppClient::new(&app_path_str, Arc::new(transport)).await?, + )) + }; loop { println!("Enter a command:"); diff --git a/client-sdk/Cargo.toml b/client-sdk/Cargo.toml index bab6832..4e2f40a 100644 --- a/client-sdk/Cargo.toml +++ b/client-sdk/Cargo.toml @@ -7,9 +7,10 @@ edition = "2021" async-trait = "0.1.81" common = { path = "../common" } goblin = "0.8.2" +hex = "0.4.3" hidapi = "2.6.3" ledger-apdu = "0.11.0" ledger-transport-hid = "0.11.0" postcard = { version = "1.0.8", features = ["alloc"] } sha2 = "0.10.8" -tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "process", "rt", "sync"] } diff --git a/client-sdk/src/transport.rs b/client-sdk/src/transport.rs index 63c6ef2..8de2586 100644 --- a/client-sdk/src/transport.rs +++ b/client-sdk/src/transport.rs @@ -19,7 +19,7 @@ use crate::apdu::{APDUCommand, StatusWord}; /// Communication layer between the bitcoin client and the Ledger device. #[async_trait] pub trait Transport: Send + Sync { - type Error: Debug; + type Error: Debug + Send + Sync; async fn exchange(&self, command: &APDUCommand) -> Result<(StatusWord, Vec), Self::Error>; } @@ -34,7 +34,7 @@ impl TransportHID { #[async_trait] impl Transport for TransportHID { - type Error = Box; + type Error = Box; async fn exchange(&self, cmd: &APDUCommand) -> Result<(StatusWord, Vec), Self::Error> { self.0 .exchange(&ledger_apdu::APDUCommand { @@ -71,7 +71,7 @@ impl TransportTcp { #[async_trait] impl Transport for TransportTcp { - type Error = Box; + type Error = Box; async fn exchange(&self, command: &APDUCommand) -> Result<(StatusWord, Vec), Self::Error> { let mut stream = self.connection.lock().await; let command_bytes = command.encode(); @@ -98,17 +98,17 @@ impl Transport for TransportTcp { } /// Wrapper to handle both hid and tcp transport. -pub struct TransportWrapper(Arc> + Sync + Send>); +pub struct TransportWrapper(Arc> + Sync + Send>); impl TransportWrapper { - pub fn new(t: Arc> + Sync + Send>) -> Self { + pub fn new(t: Arc> + Sync + Send>) -> Self { Self(t) } } #[async_trait] impl Transport for TransportWrapper { - type Error = Box; + type Error = Box; async fn exchange(&self, command: &APDUCommand) -> Result<(StatusWord, Vec), Self::Error> { self.0.exchange(command).await } diff --git a/client-sdk/src/vanadium_client.rs b/client-sdk/src/vanadium_client.rs index e60ef71..3acaad2 100644 --- a/client-sdk/src/vanadium_client.rs +++ b/client-sdk/src/vanadium_client.rs @@ -1,5 +1,9 @@ +use async_trait::async_trait; use std::cmp::min; +use std::path::Path; use std::sync::Arc; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::process::{ChildStdin, ChildStdout}; use tokio::sync::{mpsc, Mutex}; use tokio::task::JoinHandle; @@ -121,7 +125,7 @@ enum ClientMessage { ReceiveBuffer(Vec), } -struct VAppEngine { +struct VAppEngine { manifest: Manifest, code_seg: MemorySegment, data_seg: MemorySegment, @@ -131,7 +135,7 @@ struct VAppEngine { client_to_engine_receiver: mpsc::Receiver, } -impl VAppEngine { +impl VAppEngine { pub async fn run(mut self) -> Result<(), &'static str> { let mut data = postcard::to_allocvec(&self.manifest).map_err(|_| "manifest serialization failed")?; @@ -462,14 +466,14 @@ impl VAppEngine { } } -pub struct VanadiumClient { +struct GenericVanadiumClient { client_to_engine_sender: Option>, engine_to_client_receiver: Option>>, vapp_engine_handle: Option>>, } #[derive(Debug)] -pub enum VanadiumClientError { +enum VanadiumClientError { VAppPanicked(String), VAppExited(i32), GenericError(String), @@ -481,7 +485,23 @@ impl From<&str> for VanadiumClientError { } } -impl VanadiumClient { +impl std::fmt::Display for VanadiumClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VanadiumClientError::VAppPanicked(msg) => write!(f, "VApp panicked: {}", msg), + VanadiumClientError::VAppExited(code) => write!(f, "VApp exited with code: {}", code), + VanadiumClientError::GenericError(msg) => write!(f, "Generic error: {}", msg), + } + } +} + +impl std::error::Error for VanadiumClientError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } +} + +impl GenericVanadiumClient { pub fn new() -> Self { Self { client_to_engine_sender: None, @@ -490,9 +510,9 @@ impl VanadiumClient { } } - pub async fn register_vapp( + pub async fn register_vapp( &self, - transport: &T, + transport: Arc>, manifest: &Manifest, ) -> Result<[u8; 32], &'static str> { let command = APDUCommand { @@ -521,7 +541,7 @@ impl VanadiumClient { } } - pub fn run_vapp( + pub fn run_vapp( &mut self, transport: Arc>, manifest: &Manifest, @@ -593,3 +613,209 @@ impl VanadiumClient { } } } + +/// Represents errors that can occur during the execution of a V-App. +#[derive(Debug)] +pub enum VAppExecutionError { + /// Indicates that the V-App has exited with the specific status code. + /// Useful to handle a graceful exit of the V-App. + AppExited(i32), + /// Any other error. + Other(Box), +} + +impl std::fmt::Display for VAppExecutionError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + VAppExecutionError::AppExited(code) => write!(f, "V-App exited with status {}", code), + VAppExecutionError::Other(e) => write!(f, "{}", e), + } + } +} + +impl std::error::Error for VAppExecutionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + VAppExecutionError::Other(e) => Some(&**e), + _ => None, + } + } +} + +/// A trait representing an application that can send messages asynchronously. +/// +/// This trait defines the behavior for sending messages to an application and +/// receiving responses. +#[async_trait] +pub trait VAppClient { + /// Sends a message to the app and returns the response asynchronously. + /// + /// # Parameters + /// + /// - `msg`: A `Vec` 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>; +} + +/// Implementation of a VAppClient using the Vanadium VM. +pub struct VanadiumAppClient { + client: GenericVanadiumClient, +} + +impl VanadiumAppClient { + pub async fn new( + elf_path: &str, + transport: Arc>, + ) -> Result> { + // Create ELF file and manifest + let elf_file = ElfFile::new(Path::new(&elf_path))?; + let manifest = Manifest::new( + 0, + "Test", + "0.1.0", + [0u8; 32], + elf_file.entrypoint, + 65536, + elf_file.code_segment.start, + elf_file.code_segment.end, + 0xd47a2000 - 65536, + 0xd47a2000, + elf_file.data_segment.start, + elf_file.data_segment.end, + [0u8; 32], + 0, + ) + .unwrap(); + + let mut client = GenericVanadiumClient::new(); + + // Register and run the V-App + let app_hmac = client.register_vapp(transport.clone(), &manifest).await?; + client.run_vapp(transport.clone(), &manifest, &app_hmac, &elf_file)?; + + Ok(Self { client }) + } +} + +#[async_trait] +impl VAppClient for VanadiumAppClient { + async fn send_message(&mut self, msg: Vec) -> Result, VAppExecutionError> { + match self.client.send_message(msg).await { + Ok(response) => Ok(response), + Err(VanadiumClientError::VAppExited(status)) => { + Err(VAppExecutionError::AppExited(status)) + } + Err(e) => Err(VAppExecutionError::Other(Box::new(e))), + } + } +} + +/// Implementation of a VAppClient for a native app running on the host, and communicating +/// via standard input and output. +pub struct NativeAppClient { + child: tokio::process::Child, + stdin: ChildStdin, + stdout: BufReader, +} + +impl NativeAppClient { + pub async fn new(bin_path: &str) -> Result> { + let mut child = tokio::process::Command::new(bin_path) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn()?; + + let stdin = child.stdin.take().ok_or("Failed to open stdin")?; + let stdout = child.stdout.take().ok_or("Failed to open stdout")?; + let stdout = BufReader::new(stdout); + + Ok(Self { + child, + stdin, + stdout, + }) + } +} + +#[async_trait] +impl VAppClient for NativeAppClient { + async fn send_message(&mut self, msg: Vec) -> Result, VAppExecutionError> { + // Check if the child process has exited + if let Some(status) = self + .child + .try_wait() + .map_err(|e| VAppExecutionError::Other(Box::new(e)))? + { + return Err(VAppExecutionError::AppExited(status.code().unwrap_or(-1))); + } + + // Encode message as hex and append a newline + let hex_msg = hex::encode(&msg); + let hex_msg_newline = format!("{}\n", hex_msg); + + // Write hex-encoded message to stdin + self.stdin + .write_all(hex_msg_newline.as_bytes()) + .await + .map_err(|e| { + if e.kind() == std::io::ErrorKind::BrokenPipe { + VAppExecutionError::AppExited(-1) + } else { + VAppExecutionError::Other(Box::new(e)) + } + })?; + + // Flush the stdin to ensure the message is sent + self.stdin.flush().await.map_err(|e| { + if e.kind() == std::io::ErrorKind::BrokenPipe { + VAppExecutionError::AppExited(-1) + } else { + VAppExecutionError::Other(Box::new(e)) + } + })?; + + // Read response from stdout until a newline + let mut response_line = String::new(); + let bytes_read = self + .stdout + .read_line(&mut response_line) + .await + .map_err(|e| { + if e.kind() == std::io::ErrorKind::UnexpectedEof { + VAppExecutionError::AppExited(-1) + } else { + VAppExecutionError::Other(Box::new(e)) + } + })?; + + if bytes_read == 0 { + println!("EOF reached"); + // read the exit code + let status = self + .child + .wait() + .await + .map_err(|e| VAppExecutionError::Other(Box::new(e)))? + .code() + .unwrap_or(-1); + + return Err(VAppExecutionError::AppExited(status)); + } + + // Remove any trailing newline or carriage return characters + response_line = response_line + .trim_end_matches(&['\r', '\n'][..]) + .to_string(); + + // Decode the hex-encoded response + let response = + hex::decode(&response_line).map_err(|e| VAppExecutionError::Other(Box::new(e)))?; + + Ok(response) + } +} From cac532cd3689a3cf51e59a34ee30c0e1ea1be84b Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:35:01 +0200 Subject: [PATCH 10/14] Added 'panic' command to test app; improved error handling --- apps/test/README.md | 1 + apps/test/app/src/commands.rs | 2 + apps/test/app/src/main.rs | 4 ++ apps/test/client/src/client.rs | 90 ++++++++++++++++++++------------ apps/test/client/src/commands.rs | 2 + apps/test/client/src/main.rs | 24 +++++++-- 6 files changed, 88 insertions(+), 35 deletions(-) diff --git a/apps/test/README.md b/apps/test/README.md index 602d7b6..cb11bc1 100644 --- a/apps/test/README.md +++ b/apps/test/README.md @@ -55,4 +55,5 @@ Once the client is running, these are the available commands: - `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 Erathostenes. +- `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/test/app/src/commands.rs b/apps/test/app/src/commands.rs index 64df67f..1556c62 100644 --- a/apps/test/app/src/commands.rs +++ b/apps/test/app/src/commands.rs @@ -8,6 +8,7 @@ pub enum Command { Base58Encode, Sha256, CountPrimes, + Panic = 0xff, } impl TryFrom for Command { @@ -20,6 +21,7 @@ impl TryFrom for Command { 0x02 => Ok(Command::Base58Encode), 0x03 => Ok(Command::Sha256), 0x04 => Ok(Command::CountPrimes), + 0xff => Ok(Command::Panic), _ => Err(()), } } diff --git a/apps/test/app/src/main.rs b/apps/test/app/src/main.rs index c45c86d..4675c71 100644 --- a/apps/test/app/src/main.rs +++ b/apps/test/app/src/main.rs @@ -93,6 +93,10 @@ pub fn main(_: isize, _: *const *const u8) -> isize { Command::Base58Encode => handle_base58_encode(&msg[1..]), Command::Sha256 => handle_sha256(&msg[1..]), Command::CountPrimes => handle_count_primes(&msg[1..]), + Command::Panic => { + let panic_msg = core::str::from_utf8(&msg[1..]).unwrap(); + panic!("{}", panic_msg); + } }; sdk::xsend(&response); diff --git a/apps/test/client/src/client.rs b/apps/test/client/src/client.rs index b07d679..49e175c 100644 --- a/apps/test/client/src/client.rs +++ b/apps/test/client/src/client.rs @@ -1,6 +1,42 @@ use crate::commands::Command; use sdk::vanadium_client::{VAppClient, VAppExecutionError}; +#[derive(Debug)] +pub enum TestClientError { + VAppExecutionError(VAppExecutionError), + GenericError(&'static str), +} + +impl From for TestClientError { + fn from(e: VAppExecutionError) -> Self { + Self::VAppExecutionError(e) + } +} + +impl From<&'static str> for TestClientError { + fn from(e: &'static str) -> Self { + Self::GenericError(e) + } +} + +impl std::fmt::Display for TestClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TestClientError::VAppExecutionError(e) => write!(f, "VAppExecutionError: {}", e), + TestClientError::GenericError(e) => write!(f, "GenericError: {}", e), + } + } +} + +impl std::error::Error for TestClientError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + TestClientError::VAppExecutionError(e) => Some(e), + TestClientError::GenericError(_) => None, + } + } +} + pub struct TestClient { app_client: Box, } @@ -10,76 +46,66 @@ impl TestClient { Self { app_client } } - pub async fn reverse(&mut self, data: &[u8]) -> Result, &'static str> { + pub async fn reverse(&mut self, data: &[u8]) -> Result, TestClientError> { let mut msg: Vec = Vec::new(); msg.extend_from_slice(&[Command::Reverse as u8]); msg.extend_from_slice(data); - Ok(self - .app_client - .send_message(msg) - .await - .map_err(|_| "Failed")?) + Ok(self.app_client.send_message(msg).await?) } - pub async fn add_numbers(&mut self, n: u32) -> Result { + pub async fn add_numbers(&mut self, n: u32) -> Result { let mut msg: Vec = Vec::new(); 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 - .map_err(|_| "Failed")?; + let result_raw = self.app_client.send_message(msg).await?; if result_raw.len() != 8 { - return Err("Invalid response length"); + return Err("Invalid response length".into()); } Ok(u64::from_be_bytes(result_raw.try_into().unwrap())) } - pub async fn sha256(&mut self, data: &[u8]) -> Result, &'static str> { + pub async fn sha256(&mut self, data: &[u8]) -> Result, TestClientError> { let mut msg: Vec = Vec::new(); msg.extend_from_slice(&[Command::Sha256 as u8]); msg.extend_from_slice(data); - Ok(self - .app_client - .send_message(msg) - .await - .map_err(|_| "Failed")?) + Ok(self.app_client.send_message(msg).await?) } - pub async fn b58enc(&mut self, data: &[u8]) -> Result, &'static str> { + pub async fn b58enc(&mut self, data: &[u8]) -> Result, TestClientError> { let mut msg: Vec = Vec::new(); msg.extend_from_slice(&[Command::Base58Encode as u8]); msg.extend_from_slice(data); - Ok(self - .app_client - .send_message(msg) - .await - .map_err(|_| "Failed")?) + Ok(self.app_client.send_message(msg).await?) } - pub async fn nprimes(&mut self, n: u32) -> Result { + pub async fn nprimes(&mut self, n: u32) -> Result { let mut msg: Vec = Vec::new(); 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 - .map_err(|_| "Failed")?; + let result_raw = self.app_client.send_message(msg).await?; if result_raw.len() != 4 { - return Err("Invalid response length"); + return Err("Invalid response length".into()); } Ok(u32::from_be_bytes(result_raw.try_into().unwrap())) } + pub async fn panic(&mut self, panic_msg: &str) -> Result<(), TestClientError> { + let mut msg: Vec = Vec::new(); + msg.extend_from_slice(&[Command::Panic as u8]); + msg.extend_from_slice(panic_msg.as_bytes()); + + 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 { Ok(_) => { diff --git a/apps/test/client/src/commands.rs b/apps/test/client/src/commands.rs index 8030d36..991c8cf 100644 --- a/apps/test/client/src/commands.rs +++ b/apps/test/client/src/commands.rs @@ -8,6 +8,7 @@ pub enum Command { Base58Encode, Sha256, CountPrimes, + Panic = 0xff, } impl TryFrom for Command { @@ -20,6 +21,7 @@ impl TryFrom for Command { 0x02 => Ok(Command::Base58Encode), 0x03 => Ok(Command::Sha256), 0x04 => Ok(Command::CountPrimes), + 0xff => Ok(Command::Panic), _ => Err(()), } } diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index 080fb1e..912d4ba 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -34,6 +34,7 @@ enum CliCommand { Sha256(Vec), B58Enc(Vec), NPrimes(u32), + Panic(String), Exit, } @@ -82,6 +83,14 @@ fn parse_command(line: &str) -> Result { _ => unreachable!(), } } + "panic" => { + // find where the word "panic" ends and the message starts + let msg = line + .find("panic") + .map(|i| line[i + 5..].trim()) + .unwrap_or(""); + Ok(CliCommand::Panic(msg.to_string())) + } _ => Err(format!("Unknown command: '{}'", command)), } } else { @@ -90,7 +99,7 @@ fn parse_command(line: &str) -> Result { } #[tokio::main(flavor = "current_thread")] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Box> { let args = Args::parse(); let default_app_path = if args.native { @@ -102,7 +111,11 @@ async fn main() -> Result<(), Box> { let app_path_str = args.app.unwrap_or(default_app_path.to_string()); let mut test_client = if args.native { - TestClient::new(Box::new(NativeAppClient::new(&app_path_str).await?)) + TestClient::new(Box::new( + NativeAppClient::new(&app_path_str) + .await + .map_err(|_| "Failed to create client")?, + )) } else { let transport_raw: Arc< dyn Transport> + Send + Sync, @@ -123,7 +136,9 @@ async fn main() -> Result<(), Box> { let transport = TransportWrapper::new(transport_raw.clone()); TestClient::new(Box::new( - VanadiumAppClient::new(&app_path_str, Arc::new(transport)).await?, + VanadiumAppClient::new(&app_path_str, Arc::new(transport)) + .await + .map_err(|_| "Failed to create client")?, )) }; @@ -153,6 +168,9 @@ async fn main() -> Result<(), Box> { CliCommand::NPrimes(n) => { println!("{}", test_client.nprimes(n).await?); } + CliCommand::Panic(msg) => { + test_client.panic(&msg).await?; + } CliCommand::Exit => { let status = test_client.exit().await?; if status != 0 { From 7ec4395f506c9ba99e3664c758640d792cdd7294 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:54:44 +0200 Subject: [PATCH 11/14] Add app_hmac argument to VanadiumAppClient::new --- apps/test/client/src/main.rs | 10 +++++----- client-sdk/src/vanadium_client.rs | 24 +++++++++++++----------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index 912d4ba..4f6b360 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -135,11 +135,11 @@ async fn main() -> Result<(), Box> { }; let transport = TransportWrapper::new(transport_raw.clone()); - TestClient::new(Box::new( - VanadiumAppClient::new(&app_path_str, Arc::new(transport)) - .await - .map_err(|_| "Failed to create client")?, - )) + let (client, _) = VanadiumAppClient::new(&app_path_str, Arc::new(transport), None) + .await + .map_err(|_| "Failed to create client")?; + + TestClient::new(Box::new(client)) }; loop { diff --git a/client-sdk/src/vanadium_client.rs b/client-sdk/src/vanadium_client.rs index 3acaad2..49a39d9 100644 --- a/client-sdk/src/vanadium_client.rs +++ b/client-sdk/src/vanadium_client.rs @@ -136,12 +136,10 @@ struct VAppEngine { } impl VAppEngine { - pub async fn run(mut self) -> Result<(), &'static str> { + pub async fn run(mut self, app_hmac: [u8; 32]) -> Result<(), &'static str> { let mut data = postcard::to_allocvec(&self.manifest).map_err(|_| "manifest serialization failed")?; - // TODO: need to actually get the app_hmac somehow - let app_hmac = [0x42u8; 32]; data.extend_from_slice(&app_hmac); let command = APDUCommand { @@ -575,7 +573,8 @@ impl GenericVanadiumClient { }; // Start the VAppEngine in a task - let vapp_engine_handle = tokio::spawn(async move { vapp_engine.run().await }); + let app_hmac_clone = *app_hmac; + let vapp_engine_handle = tokio::spawn(async move { vapp_engine.run(app_hmac_clone).await }); // Store the senders and receivers self.client_to_engine_sender = Some(client_to_engine_sender); @@ -671,7 +670,8 @@ impl VanadiumAppClient { pub async fn new( elf_path: &str, transport: Arc>, - ) -> Result> { + app_hmac: Option<[u8; 32]>, + ) -> Result<(Self, [u8; 32]), Box> { // Create ELF file and manifest let elf_file = ElfFile::new(Path::new(&elf_path))?; let manifest = Manifest::new( @@ -689,16 +689,18 @@ impl VanadiumAppClient { elf_file.data_segment.end, [0u8; 32], 0, - ) - .unwrap(); + )?; let mut client = GenericVanadiumClient::new(); - // Register and run the V-App - let app_hmac = client.register_vapp(transport.clone(), &manifest).await?; - client.run_vapp(transport.clone(), &manifest, &app_hmac, &elf_file)?; + // Register the V-App if the hmac was not given + let app_hmac = + app_hmac.unwrap_or(client.register_vapp(transport.clone(), &manifest).await?); + + // run the V-App + client.run_vapp(transport, &manifest, &app_hmac, &elf_file)?; - Ok(Self { client }) + Ok((Self { client }, app_hmac)) } } From f3ba9d982fd5d40157180ba7867cb47027cbf8e8 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:35:27 +0200 Subject: [PATCH 12/14] Refactor APDU messages in client --- client-sdk/src/apdu.rs | 32 +++++++++++++++++++++++++ client-sdk/src/vanadium_client.rs | 40 +++++++++---------------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/client-sdk/src/apdu.rs b/client-sdk/src/apdu.rs index 9b2ca90..fa6b066 100644 --- a/client-sdk/src/apdu.rs +++ b/client-sdk/src/apdu.rs @@ -83,3 +83,35 @@ pub fn apdu_continue(data: Vec) -> APDUCommand { data, } } + +pub fn apdu_continue_with_p1(data: Vec, p1: u8) -> APDUCommand { + APDUCommand { + cla: 0xE0, + ins: 0xff, + p1, + p2: 0, + data, + } +} + +pub fn apdu_register_vapp(serialized_manifest: Vec) -> APDUCommand { + APDUCommand { + cla: 0xE0, + ins: 2, + p1: 0, + p2: 0, + data: serialized_manifest, + } +} + +pub fn apdu_run_vapp(serialized_manifest: Vec, app_hmac: [u8; 32]) -> APDUCommand { + let mut data = serialized_manifest; + data.extend_from_slice(&app_hmac); + APDUCommand { + cla: 0xE0, + ins: 3, + p1: 0, + p2: 0, + data, + } +} diff --git a/client-sdk/src/vanadium_client.rs b/client-sdk/src/vanadium_client.rs index 49a39d9..ca37aae 100644 --- a/client-sdk/src/vanadium_client.rs +++ b/client-sdk/src/vanadium_client.rs @@ -17,7 +17,10 @@ use common::constants::{page_start, PAGE_SIZE}; use common::manifest::Manifest; use sha2::{Digest, Sha256}; -use crate::apdu::{apdu_continue, APDUCommand, StatusWord}; +use crate::apdu::{ + apdu_continue, apdu_continue_with_p1, apdu_register_vapp, apdu_run_vapp, APDUCommand, + StatusWord, +}; use crate::elf::ElfFile; use crate::transport::Transport; @@ -137,22 +140,12 @@ struct VAppEngine { impl VAppEngine { pub async fn run(mut self, app_hmac: [u8; 32]) -> Result<(), &'static str> { - let mut data = + let serialized_manifest = postcard::to_allocvec(&self.manifest).map_err(|_| "manifest serialization failed")?; - data.extend_from_slice(&app_hmac); - - let command = APDUCommand { - cla: 0xE0, - ins: 3, - p1: 0, - p2: 0, - data, - }; - let (status, result) = self .transport - .exchange(&command) + .exchange(&apdu_run_vapp(serialized_manifest, app_hmac)) .await .map_err(|_| "exchange failed")?; @@ -206,16 +199,10 @@ impl VAppEngine { let (mut data, _) = segment.get_page(page_index)?; let p1 = data.pop().unwrap(); - // return the content of the page + // return the content of the page (the last byte is in p1) Ok(self .transport - .exchange(&APDUCommand { - cla: 0xE0, - ins: 0xff, - p1, - p2: 0, - data, - }) + .exchange(&apdu_continue_with_p1(data, p1)) .await .map_err(|_| "exchange failed")?) } @@ -513,16 +500,11 @@ impl GenericVanadiumClient { transport: Arc>, manifest: &Manifest, ) -> Result<[u8; 32], &'static str> { - let command = APDUCommand { - cla: 0xE0, - ins: 2, - p1: 0, - p2: 0, - data: postcard::to_allocvec(manifest).map_err(|_| "manifest serialization failed")?, - }; + let serialized_manifest = + postcard::to_allocvec(manifest).map_err(|_| "manifest serialization failed")?; let (status, result) = transport - .exchange(&command) + .exchange(&apdu_register_vapp(serialized_manifest)) .await .map_err(|_| "exchange failed")?; From 7936ef90e80c70ed0d1392d2aefa32b33dbc4998 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:51:45 +0200 Subject: [PATCH 13/14] Use multi-thread runtime for tokio in vnd-test-client --- apps/test/client/Cargo.toml | 2 +- apps/test/client/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/test/client/Cargo.toml b/apps/test/client/Cargo.toml index e609874..cca9adc 100644 --- a/apps/test/client/Cargo.toml +++ b/apps/test/client/Cargo.toml @@ -9,7 +9,7 @@ hex = "0.4.3" hidapi = "2.6.3" ledger-transport-hid = "0.11.0" sdk = { package = "vanadium-client-sdk", path = "../../../client-sdk"} -tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread", "sync"] } [lib] name = "vnd_test_client" diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index 4f6b360..35d8908 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -98,7 +98,7 @@ fn parse_command(line: &str) -> Result { } } -#[tokio::main(flavor = "current_thread")] +#[tokio::main(flavor = "multi_thread")] async fn main() -> Result<(), Box> { let args = Args::parse(); From 3ea158eb919c6e29a2c4870e141e045dd86636a1 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:00:51 +0200 Subject: [PATCH 14/14] Nits + clippy --- apps/test/README.md | 12 +++++++----- apps/test/app/src/handlers/sha256.rs | 2 +- apps/test/app/src/main.rs | 6 ++++-- apps/test/client/src/client.rs | 4 +--- apps/test/client/src/main.rs | 6 +++--- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/test/README.md b/apps/test/README.md index cb11bc1..51f0351 100644 --- a/apps/test/README.md +++ b/apps/test/README.md @@ -1,8 +1,10 @@ -This will be a test V-app. +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 @@ -21,7 +23,7 @@ In order to build the app for the native target, enter the `app` folder and run: cargo build --release --target=x86_64-unknown-linux-gnu ``` -## Run the V-App on Vanadium +## Run the V-App Make sure you built the V-App for the Risc-V target. @@ -39,7 +41,7 @@ If you want to run the V-app on a real device, execute instead: cargo run -- --hid ``` -If you want to run the V-app natively, use: +If you want to run the V-app natively, after building it for the native target, use: ```sh cargo run -- --native @@ -50,10 +52,10 @@ If you want to run the V-app natively, use: Once the client is running, these are the available commands: -- `reverse ` - Reversed the given buffer. +- `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 Erathostenes. +- `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/test/app/src/handlers/sha256.rs b/apps/test/app/src/handlers/sha256.rs index 09bfb87..de1a3b1 100644 --- a/apps/test/app/src/handlers/sha256.rs +++ b/apps/test/app/src/handlers/sha256.rs @@ -2,5 +2,5 @@ use alloc::vec::Vec; use sha2::Digest; pub fn handle_sha256(data: &[u8]) -> Vec { - sha2::Sha256::digest(&data).to_vec() + sha2::Sha256::digest(data).to_vec() } diff --git a/apps/test/app/src/main.rs b/apps/test/app/src/main.rs index 4675c71..0da9b10 100644 --- a/apps/test/app/src/main.rs +++ b/apps/test/app/src/main.rs @@ -1,6 +1,8 @@ #![feature(start)] #![cfg_attr(target_arch = "riscv32", no_std, no_main)] +use core::ptr::addr_of; + #[cfg(target_arch = "riscv32")] use sdk::fatal; @@ -47,7 +49,7 @@ pub fn main(_: isize, _: *const *const u8) -> isize { // TODO: remove unsafe { - core::ptr::read_volatile(&APP_NAME); + core::ptr::read_volatile(addr_of!(APP_NAME)); } // TODO: remove @@ -64,7 +66,7 @@ pub fn main(_: isize, _: *const *const u8) -> isize { // sdk::ux::app_loading_start("Handling request...\x00"); - if msg.len() == 0 { + if msg.is_empty() { sdk::exit(0); } diff --git a/apps/test/client/src/client.rs b/apps/test/client/src/client.rs index 49e175c..38b225b 100644 --- a/apps/test/client/src/client.rs +++ b/apps/test/client/src/client.rs @@ -108,9 +108,7 @@ impl TestClient { pub async fn exit(&mut self) -> Result { match self.app_client.send_message(Vec::new()).await { - Ok(_) => { - return Err("Exit message shouldn't return!"); - } + Ok(_) => Err("Exit message shouldn't return!"), Err(e) => match e { VAppExecutionError::AppExited(status) => Ok(status), _ => Err("Unexpected error"), diff --git a/apps/test/client/src/main.rs b/apps/test/client/src/main.rs index 35d8908..bc1d559 100644 --- a/apps/test/client/src/main.rs +++ b/apps/test/client/src/main.rs @@ -59,10 +59,10 @@ fn parse_u32(s: &str) -> Result { } fn parse_command(line: &str) -> Result { - let mut tokens = line.trim().split_whitespace(); + let mut tokens = line.split_whitespace(); if let Some(command) = tokens.next() { match command { - "reverse" | "sha256" | "b58enc" | "b58encode" => { + "reverse" | "sha256" | "b58enc" => { let arg = tokens.next().unwrap_or(""); let buffer = parse_hex_buffer(arg).map_err(|e| e.to_string())?; match command { @@ -94,7 +94,7 @@ fn parse_command(line: &str) -> Result { _ => Err(format!("Unknown command: '{}'", command)), } } else { - return Ok(CliCommand::Exit); + Ok(CliCommand::Exit) } }