diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ad9291c..b6ae3e2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,4 @@ -name: Run unit tests +name: CI on: push: @@ -14,6 +14,8 @@ env: CARGO_TERM_COLOR: always jobs: + # Native tests + test_app_sdk: name: Run V-App SDK tests runs-on: ubuntu-latest @@ -109,3 +111,89 @@ jobs: working-directory: apps/test/client run: | cargo +nightly test --target x86_64-unknown-linux-gnu + + # build Vanadium VM app + + build_application: + name: Build application using the reusable workflow + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1 + with: + upload_app_binaries_artifact: "vanadium_binaries" + builder: ledger-app-builder + + ### Risc-V tests + + # build test V-App + build_vapp_test: + name: Build vnd-test on Risc-V target + runs-on: ubuntu-latest + steps: + - name: Set up Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + target: riscv32i-unknown-none-elf + components: rustfmt, clippy + profile: minimal + - name: Clone + uses: actions/checkout@v4 + - name: Build app + working-directory: apps/test/app + run: | + cargo +nightly build --release --target riscv32i-unknown-none-elf + - name: Upload binaries + uses: actions/upload-artifact@v4 + with: + name: vnd_test_riscv_binary + path: apps/test/app/target/riscv32i-unknown-none-elf/release/vnd-test + + run_speculos_integration_tests: + name: Integration tests on Speculos + + strategy: + matrix: + include: + # - model: nanox # TODO: reenable once compilation is fixed + - model: nanosplus + - model: flex + - model: stax + + runs-on: ubuntu-latest + needs: [build_application, build_vapp_test] + steps: + - name: Set up Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + target: x86_64-unknown-linux-gnu + components: rustfmt, clippy + profile: minimal + - name: Install Speculos + run: | + pip install speculos + - name: Install dependencies + run: | + sudo apt-get update && \ + sudo apt-get install -y libudev-dev pkg-config && \ + sudo apt-get install -y qemu-user-static libvncserver-dev + - name: Clone + uses: actions/checkout@v4 + - name: Download Vanadium binaries + uses: actions/download-artifact@v4 + with: + name: vanadium_binaries + path: ./vanadium_binaries + + - name: Download vnd_test_riscv_binary + uses: actions/download-artifact@v4 + with: + name: vnd_test_riscv_binary + path: ./vnd_test_riscv_binary + + - name: Run integration tests + working-directory: apps/test/client + env: + VANADIUM_BINARY: ../../../vanadium_binaries/${{ matrix.model }}/release/app-vanadium + VAPP_BINARY: ../../../vnd_test_riscv_binary/vnd-test + run: | + cargo test --features speculos-tests \ No newline at end of file diff --git a/app-sdk/src/bignum.rs b/app-sdk/src/bignum.rs index c2f1009..dd063cd 100644 --- a/app-sdk/src/bignum.rs +++ b/app-sdk/src/bignum.rs @@ -192,7 +192,7 @@ impl<'a, const N: usize> BigNumMod<'a, N> { } // reduce the buffer my the modulus let mut buffer = buffer; - if !Ecall::bn_modm( + if 1 != Ecall::bn_modm( buffer.as_mut_ptr(), buffer.as_ptr(), N, @@ -220,7 +220,7 @@ impl<'a, const N: usize> BigNumMod<'a, N> { let mut buffer = [0u8; N]; buffer[N - 4..N].copy_from_slice(&value.to_be_bytes()); - if !Ecall::bn_modm( + if 1 != Ecall::bn_modm( buffer.as_mut_ptr(), buffer.as_ptr(), N, @@ -251,7 +251,7 @@ impl<'a, const N: usize> BigNumMod<'a, N> { self.modulus.m.as_ptr(), N, ); - if !res { + if res != 1 { panic!("Exponentiation failed"); } Self::from_be_bytes(result, self.modulus) @@ -298,7 +298,7 @@ impl<'a, const N: usize> Add for &BigNumMod<'a, N> { self.modulus.m.as_ptr(), N, ); - if !res { + if res != 1 { panic!("Addition failed"); } BigNumMod::from_be_bytes(result, self.modulus) @@ -318,7 +318,7 @@ impl<'a, const N: usize> AddAssign for BigNumMod<'a, N> { self.modulus.m.as_ptr(), N, ); - if !res { + if res != 1 { panic!("Addition failed"); } } @@ -362,7 +362,7 @@ impl<'a, const N: usize> Sub for &BigNumMod<'a, N> { self.modulus.m.as_ptr(), N, ); - if !res { + if res != 1 { panic!("Subtraction failed"); } BigNumMod::from_be_bytes(result, self.modulus) @@ -382,7 +382,7 @@ impl<'a, const N: usize> SubAssign for BigNumMod<'a, N> { self.modulus.m.as_ptr(), N, ); - if !res { + if res != 1 { panic!("Subtraction failed"); } } @@ -418,7 +418,7 @@ impl<'a, const N: usize> core::ops::Mul for &BigNumMod<'a, N> { self.modulus.m.as_ptr(), N, ); - if !res { + if res != 1 { panic!("Multiplication failed"); } BigNumMod::from_be_bytes(result, self.modulus) @@ -438,7 +438,7 @@ impl<'a, const N: usize> MulAssign<&Self> for BigNumMod<'a, N> { self.modulus.m.as_ptr(), N, ); - if !res { + if res != 1 { panic!("Multiplication failed"); } } diff --git a/app-sdk/src/ecalls.rs b/app-sdk/src/ecalls.rs index ae2dfd6..37fa6b5 100644 --- a/app-sdk/src/ecalls.rs +++ b/app-sdk/src/ecalls.rs @@ -55,8 +55,8 @@ pub(crate) trait EcallsInterface { /// - `len_m`: Length of `m`. /// /// # Returns - /// `true` on success, `false` on error. - fn bn_modm(r: *mut u8, n: *const u8, len: usize, m: *const u8, len_m: usize) -> bool; + /// 1 on success, 0 on error. + fn bn_modm(r: *mut u8, n: *const u8, len: usize, m: *const u8, len_m: usize) -> u32; /// Adds two big numbers `a` and `b` modulo `m`, storing the result in `r`. /// @@ -69,7 +69,7 @@ pub(crate) trait EcallsInterface { /// /// # Returns /// `true` on success, `false` on error. - fn bn_addm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> bool; + fn bn_addm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> u32; /// Subtracts two big numbers `a` and `b` modulo `m`, storing the result in `r`. /// @@ -82,7 +82,7 @@ pub(crate) trait EcallsInterface { /// /// # Returns /// `true` on success, `false` on error. - fn bn_subm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> bool; + fn bn_subm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> u32; /// Multiplies two big numbers `a` and `b` modulo `m`, storing the result in `r`. /// @@ -95,7 +95,7 @@ pub(crate) trait EcallsInterface { /// /// # Returns /// `true` on success, `false` on error. - fn bn_multm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> bool; + fn bn_multm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> u32; /// Computes `a` to the power of `e` modulo `m`, storing the result in `r`. /// @@ -116,7 +116,7 @@ pub(crate) trait EcallsInterface { len_e: usize, m: *const u8, len: usize, - ) -> bool; + ) -> u32; } pub(crate) use ecalls_module::*; diff --git a/app-sdk/src/ecalls_native.rs b/app-sdk/src/ecalls_native.rs index c6894be..eea908b 100644 --- a/app-sdk/src/ecalls_native.rs +++ b/app-sdk/src/ecalls_native.rs @@ -64,39 +64,39 @@ impl EcallsInterface for Ecall { return n_bytes_to_copy; } - fn bn_modm(r: *mut u8, n: *const u8, len: usize, m: *const u8, len_m: usize) -> bool { + fn bn_modm(r: *mut u8, n: *const u8, len: usize, m: *const u8, len_m: usize) -> u32 { if len > MAX_BIGNUMBER_SIZE || len_m > MAX_BIGNUMBER_SIZE { - return false; + return 0; } if len_m > len_m { - return false; + return 0; } let n = unsafe { Self::to_bigint(n, len) }; let m = unsafe { Self::to_bigint(m, len_m) }; if m.is_zero() { - return false; + return 0; } let result = n % &m; let result_bytes = result.to_bytes_be(); if result_bytes.len() > len { - return false; + return 0; } unsafe { Self::copy_result(r, &result_bytes, len); } - true + 1 } - fn bn_addm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> bool { + fn bn_addm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> u32 { if len > MAX_BIGNUMBER_SIZE { - return false; + return 0; } let a = unsafe { Self::to_bigint(a, len) }; @@ -104,30 +104,30 @@ impl EcallsInterface for Ecall { let m = unsafe { Self::to_bigint(m, len) }; if a >= m || b >= m { - return false; + return 0; } if m.is_zero() { - return false; + return 0; } let result = (a + b) % &m; let result_bytes = result.to_bytes_be(); if result_bytes.len() > len { - return false; + return 0; } unsafe { Self::copy_result(r, &result_bytes, len); } - true + 1 } - fn bn_subm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> bool { + fn bn_subm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> u32 { if len > MAX_BIGNUMBER_SIZE { - return false; + return 0; } let a = unsafe { Self::to_bigint(a, len) }; @@ -135,11 +135,11 @@ impl EcallsInterface for Ecall { let m = unsafe { Self::to_bigint(m, len) }; if a >= m || b >= m { - return false; + return 0; } if m.is_zero() { - return false; + return 0; } // the `+ &m` is to avoid negative numbers, since BigUints must be non-negative @@ -147,19 +147,19 @@ impl EcallsInterface for Ecall { let result_bytes = result.to_bytes_be(); if result_bytes.len() > len { - return false; + return 0; } unsafe { Self::copy_result(r, &result_bytes, len); } - true + 1 } - fn bn_multm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> bool { + fn bn_multm(r: *mut u8, a: *const u8, b: *const u8, m: *const u8, len: usize) -> u32 { if len > MAX_BIGNUMBER_SIZE { - return false; + return 0; } let a = unsafe { Self::to_bigint(a, len) }; @@ -167,25 +167,25 @@ impl EcallsInterface for Ecall { let m = unsafe { Self::to_bigint(m, len) }; if a >= m || b >= m { - return false; + return 0; } if m.is_zero() { - return false; + return 0; } let result = (a * b) % &m; let result_bytes = result.to_bytes_be(); if result_bytes.len() > len { - return false; + return 0; } unsafe { Self::copy_result(r, &result_bytes, len); } - true + 1 } fn bn_powm( @@ -195,9 +195,9 @@ impl EcallsInterface for Ecall { len_e: usize, m: *const u8, len: usize, - ) -> bool { + ) -> u32 { if len > MAX_BIGNUMBER_SIZE || len_e > MAX_BIGNUMBER_SIZE { - return false; + return 0; } let a = unsafe { Self::to_bigint(a, len) }; @@ -205,25 +205,25 @@ impl EcallsInterface for Ecall { let m = unsafe { Self::to_bigint(m, len) }; if a >= m { - return false; + return 0; } if m.is_zero() { - return false; + return 0; } let result = a.modpow(&e, &m); let result_bytes = result.to_bytes_be(); if result_bytes.len() > len { - return false; + return 0; } unsafe { Self::copy_result(r, &result_bytes, len); } - true + 1 } } diff --git a/app-sdk/src/ecalls_riscv.rs b/app-sdk/src/ecalls_riscv.rs index e51b3fc..4502e86 100644 --- a/app-sdk/src/ecalls_riscv.rs +++ b/app-sdk/src/ecalls_riscv.rs @@ -169,7 +169,7 @@ macro_rules! ecall5 { }; } -macro_rules! ecall5 { +macro_rules! ecall6 { // ECALL with 6 arguments and returning a value ($fn_name:ident, $syscall_number:expr, ($arg1:ident: $arg1_type:ty), @@ -230,9 +230,9 @@ impl EcallsInterface for Ecall { ecall2v!(xsend, ECALL_XSEND, (buffer: *const u8), (size: usize)); ecall2!(xrecv, ECALL_XRECV, (buffer: *mut u8), (size: usize), usize); - ecall5!(bn_modm, ECALL_MODM, (r: *mut u8), (n: *const u8), (len: usize), (m: *const u8), (len_m: usize), bool); - ecall5!(bn_addm, ECALL_ADDM, (r: *mut u8), (a: *const u8), (b: *const u8), (m: *const u8), (len: usize), bool); - ecall5!(bn_subm, ECALL_SUBM, (r: *mut u8), (a: *const u8), (b: *const u8), (m: *const u8), (len: usize), bool); - ecall5!(bn_multm, ECALL_MULTM, (r: *mut u8), (a: *const u8), (b: *const u8), (m: *const u8), (len: usize), bool); - ecall6!(bn_powm, ECALL_POWM, (r: *mut u8), (a: *const u8), (e: *const u8), (len_e: usize), (m: *const u8), (len: usize), bool); + ecall5!(bn_modm, ECALL_MODM, (r: *mut u8), (n: *const u8), (len: usize), (m: *const u8), (len_m: usize), u32); + ecall5!(bn_addm, ECALL_ADDM, (r: *mut u8), (a: *const u8), (b: *const u8), (m: *const u8), (len: usize), u32); + ecall5!(bn_subm, ECALL_SUBM, (r: *mut u8), (a: *const u8), (b: *const u8), (m: *const u8), (len: usize), u32); + ecall5!(bn_multm, ECALL_MULTM, (r: *mut u8), (a: *const u8), (b: *const u8), (m: *const u8), (len: usize), u32); + ecall6!(bn_powm, ECALL_POWM, (r: *mut u8), (a: *const u8), (e: *const u8), (len_e: usize), (m: *const u8), (len: usize), u32); } diff --git a/apps/test/client/.cargo/config.toml b/apps/test/client/.cargo/config.toml new file mode 100644 index 0000000..8af59dd --- /dev/null +++ b/apps/test/client/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +RUST_TEST_THREADS = "1" diff --git a/apps/test/client/Cargo.toml b/apps/test/client/Cargo.toml index cca9adc..52c769a 100644 --- a/apps/test/client/Cargo.toml +++ b/apps/test/client/Cargo.toml @@ -3,6 +3,9 @@ name = "vnd-test-client" version = "0.1.0" edition = "2021" +[features] +speculos-tests = [] + [dependencies] clap = { version = "4.5.17", features = ["derive"] } hex = "0.4.3" @@ -11,6 +14,9 @@ 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", "rt-multi-thread", "sync"] } +[dev-dependencies] +hex-literal = "0.4.1" + [lib] name = "vnd_test_client" path = "src/lib.rs" diff --git a/apps/test/client/tests/common/mod.rs b/apps/test/client/tests/common/mod.rs new file mode 100644 index 0000000..fd556d2 --- /dev/null +++ b/apps/test/client/tests/common/mod.rs @@ -0,0 +1,61 @@ +use std::process::{Child, Command}; +use std::sync::Arc; +use std::thread::sleep; +use std::time::Duration; +use vnd_test_client::TestClient; + +use sdk::{ + transport::{Transport, TransportTcp, TransportWrapper}, + vanadium_client::VanadiumAppClient, +}; + +pub struct TestSetup { + pub client: TestClient, + child: Child, +} + +impl TestSetup { + async fn new() -> Self { + let vanadium_binary = std::env::var("VANADIUM_BINARY") + .unwrap_or_else(|_| "../../../vm/build/nanos2/bin/app.elf".to_string()); + let vapp_binary = std::env::var("VAPP_BINARY").unwrap_or_else(|_| { + "../app/target/riscv32i-unknown-none-elf/release/vnd-test".to_string() + }); + + let child = Command::new("speculos") + .arg(vanadium_binary) + .arg("--display") + .arg("headless") + .spawn() + .expect("Failed to start speculos process"); + + sleep(Duration::from_secs(1)); + + let transport_raw: Arc< + dyn Transport> + Send + Sync, + > = Arc::new( + TransportTcp::new() + .await + .expect("Unable to get TCP transport. Is speculos running?"), + ); + let transport = TransportWrapper::new(transport_raw.clone()); + + let (vanadium_client, _) = VanadiumAppClient::new(&vapp_binary, Arc::new(transport), None) + .await + .expect("Failed to create client"); + + let client = TestClient::new(Box::new(vanadium_client)); + + TestSetup { client, child } + } +} + +impl Drop for TestSetup { + fn drop(&mut self) { + self.child.kill().expect("Failed to kill speculos process"); + } +} + +pub async fn setup() -> TestSetup { + TestSetup::new().await +} diff --git a/apps/test/client/tests/integration_test.rs b/apps/test/client/tests/integration_test.rs new file mode 100644 index 0000000..49e3d51 --- /dev/null +++ b/apps/test/client/tests/integration_test.rs @@ -0,0 +1,88 @@ +#![cfg(feature = "speculos-tests")] + +mod common; + +use hex_literal::hex; + +#[tokio::test] +async fn test_reverse() { + let mut setup = common::setup().await; + + #[rustfmt::skip] + let testcases: Vec<(Vec, Vec)> = vec![ + (hex!("1122334455").to_vec(), hex!("5544332211").to_vec()), + ]; + + for (input, expected) in testcases { + assert_eq!(setup.client.reverse(&input).await.unwrap(), expected); + } +} + +#[tokio::test] +async fn test_add_numbers() { + let mut setup = common::setup().await; + + #[rustfmt::skip] + let testcases: Vec<(u32, u64)> = vec![ + (0, 0), + (1, 1), + (100, 5050), + ]; + + for (input, expected) in testcases { + assert_eq!(setup.client.add_numbers(input).await.unwrap(), expected); + } +} + +#[tokio::test] +async fn test_b58enc() { + let mut setup = common::setup().await; + + #[rustfmt::skip] + let testcases: Vec<(&str, &str)> = vec![ + ("Hello World!", "2NEpo7TZRRrLZSi2U"), + ("The quick brown fox jumps over the lazy dog.", "USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z"), + ]; + + for (input, expected) in testcases { + assert_eq!( + setup.client.b58enc(&input.as_bytes()).await.unwrap(), + expected.as_bytes().to_vec() + ); + } +} + +#[tokio::test] +async fn test_sha256() { + let mut setup = common::setup().await; + + #[rustfmt::skip] + let testcases: Vec<(&str, Vec)> = vec![ + ("", hex!("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855").to_vec()), + ("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", hex!("248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1").to_vec()), + ]; + + for (input, expected) in testcases { + assert_eq!( + setup.client.sha256(&input.as_bytes()).await.unwrap(), + expected + ); + } +} + +#[tokio::test] +async fn test_nprimes() { + let mut setup = common::setup().await; + + #[rustfmt::skip] + let testcases: Vec<(u32, u32)> = vec![ + (0, 0), + (5, 3), + (100, 25), + (1000, 168), + ]; + + for (input, expected) in testcases { + assert_eq!(setup.client.nprimes(input).await.unwrap(), expected); + } +} diff --git a/ledger_app.toml b/ledger_app.toml new file mode 100644 index 0000000..3fbfa28 --- /dev/null +++ b/ledger_app.toml @@ -0,0 +1,4 @@ +[app] +build_directory = "./vm/" +sdk = "Rust" +devices = ["nanos+", "stax", "flex"] diff --git a/vm/ledger_app.toml b/vm/ledger_app.toml deleted file mode 100644 index cb24deb..0000000 --- a/vm/ledger_app.toml +++ /dev/null @@ -1,7 +0,0 @@ -[app] -build_directory = "./" -sdk = "Rust" -devices = ["nanox", "nanos+", "stax", "flex"] - -[tests] -pytest_directory = "./tests/"