Skip to content

Commit

Permalink
Merge pull request #37 from LedgerHQ/btc
Browse files Browse the repository at this point in the history
Added `comm` module; added Bitcoin app boilerplate
  • Loading branch information
bigspider authored Nov 12, 2024
2 parents bb9e255 + 8dd37c9 commit 471d8f2
Show file tree
Hide file tree
Showing 30 changed files with 1,698 additions and 22 deletions.
160 changes: 160 additions & 0 deletions app-sdk/src/comm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//! Module for sending and receiving messages using a custom protocol with alternating acknowledgments.
//!
//! This module provides an efficient way to handle message transmission between endpoints by using
//! length-prefix ed messages. This allows applications to use a fixed-length buffer for short messages,
//! reducing the need for large preallocated buffers. Large vectors are only allocated if necessary.
//!
//! The protocol alternates between sending and receiving chunks of data, ensuring reliable communication
//! through acknowledgment control. Each chunk sent or received is acknowledged by the other endpoint,
//! maintaining synchronization and error management throughout the transmission process.
//!
//! Key features of this module include:
//! - **Chunked Data Transmission**: Messages are divided into manageable chunks, allowing for efficient
//! transmission and reception without requiring large buffers.
//! - **Length-Prefixing**: Messages are prefixed with their length, enabling dynamic buffer allocation
//! only when necessary.
//!
//! Note: This module is not thread-safe. It is designed for single-threaded execution due to the use of
//! a static mutable buffer for chunk reuse.
use crate::{xrecv_to, xsend};
use alloc::vec::Vec;
use core::cmp::min;
use core::convert::TryInto;

use common::comm::{ACK, CHUNK_LENGTH};

/// Error types that can occur during message transmission.
#[derive(Debug)]
pub enum MessageError {
/// Error when the received message length does not match expectations.
InvalidLength,
/// Error when more bytes are received than expected.
TooManyBytesReceived,
/// Error when a chunk fails to be received.
FailedToReadMessage,
/// Error when the message length cannot be determined due to insufficient bytes.
FailedToReadLength,
}

impl core::fmt::Display for MessageError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
MessageError::InvalidLength => write!(f, "Invalid message length"),
MessageError::TooManyBytesReceived => write!(f, "Too many bytes received"),
MessageError::FailedToReadMessage => write!(f, "Failed to read message"),
MessageError::FailedToReadLength => write!(f, "Failed to read message length"),
}
}
}

impl core::error::Error for MessageError {}

// Define a static mutable buffer for chunk reuse, in order to avoid unnecessary allocations.
static mut CHUNK_BUFFER: [u8; CHUNK_LENGTH] = [0u8; CHUNK_LENGTH];

/// Receives a message, handling chunked data reception and error management.
///
/// The function starts by attempting to read a fixed-size chunk to extract the message length.
/// It then continues reading in chunks until the entire message is received, sending an
/// acknowledgment (`ACK`) byte for each chunk received. Errors occur if any unexpected
/// conditions are encountered, such as insufficient bytes or extra bytes in a chunk.
///
/// # Errors
///
/// - Returns `MessageError::FailedToReadLength` if the initial chunk is too small to contain the
/// message length.
/// - Returns `MessageError::TooManyBytesReceived` if unexpected extra bytes are received.
/// - Returns `MessageError::FailedToReadMessage` if a chunk is empty or fails to be read.
///
/// # Returns
///
/// - On success, returns `Ok(Vec<u8>)` with the received message data.
///
/// # Safety
///
/// This function is only safe in single-threaded execution due to the use of a static mutable buffer.
pub fn receive_message() -> Result<Vec<u8>, MessageError> {
let chunk = &raw mut CHUNK_BUFFER;

let first_chunk_len = xrecv_to(unsafe { &mut *chunk });

// Ensure we have at least 4 bytes for the length.
if first_chunk_len < 4 {
return Err(MessageError::FailedToReadLength);
}

// Extract the message length.
let length = u32::from_be_bytes(unsafe { &*chunk }[0..4].try_into().unwrap()) as usize;

// Check for unexpected extra bytes.
if first_chunk_len > 4 + length {
return Err(MessageError::TooManyBytesReceived);
}

// Initialize the result with the data from the first chunk.
let mut result = Vec::with_capacity(length);
result.extend_from_slice(&unsafe { &*chunk }[4..first_chunk_len]);

while result.len() < length {
// Send ACK to maintain the alternating protocol.
xsend(&ACK);

let chunk_len = xrecv_to(unsafe { &mut *chunk });

if chunk_len == 0 {
return Err(MessageError::FailedToReadMessage);
}

if chunk_len > length - result.len() {
return Err(MessageError::TooManyBytesReceived);
}

result.extend_from_slice(&unsafe { &*chunk }[0..chunk_len]);
}

Ok(result)
}

/// Sends a message, managing chunking and acknowledgment control for transmission.
///
/// The function begins by encoding the message length in big-endian format and sending an initial
/// chunk containing this length along with part of the message (if any). It then continues sending
/// chunks, waiting for an acknowledgment (`ACK`) byte from the receiver before each chunk is sent.
/// The process ensures that messages are transmitted sequentially and fully.
///
/// # Parameters
///
/// - `msg`: A reference to the message bytes (`&[u8]`) that should be sent.
///
/// The function does not return a value, nor any error.
/// On native execution, the function will panic if the underlying calls to `xsend` or `xrecv` panic.
/// On Risc-V targets, communication failure causes the ECALL to fail, which will arrest the execution of the VM.
pub fn send_message(msg: &[u8]) {
// Encode the message length in big-endian format.
let length_be = (msg.len() as u32).to_be_bytes();

// Calculate how much of the message fits in the first chunk.
let first_chunk_msg_bytes = min(CHUNK_LENGTH - 4, msg.len());

// Send the initial chunk containing the length and part of the message.
xsend(&[&length_be, &msg[..first_chunk_msg_bytes]].concat());

let mut total_bytes_sent = first_chunk_msg_bytes;

let mut acc_chunk = vec![0u8];
// Send the remaining chunks.
while total_bytes_sent < msg.len() {
// Wait for ACK to maintain the alternating protocol.
let acc_len = xrecv_to(&mut acc_chunk);
if acc_len != 1 || acc_chunk != ACK {
panic!("Unexpected byte received: {}", acc_chunk[0]);
}

let end_idx = min(total_bytes_sent + CHUNK_LENGTH, msg.len());
let chunk = &msg[total_bytes_sent..end_idx];

xsend(chunk);
total_bytes_sent = end_idx;
}
}
14 changes: 13 additions & 1 deletion app-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ extern crate alloc;

use alloc::{vec, vec::Vec};

pub mod comm;
pub mod ux;

mod ecalls;
Expand Down Expand Up @@ -75,11 +76,22 @@ pub fn exit(status: i32) -> ! {
}

pub fn xrecv(size: usize) -> Vec<u8> {
let mut buffer = vec![0; size];
// We allocate a buffer with the requested size, but we don't initialize its content.
// xrecv guarantees that recv_size have been overwritten with the received data, and we
// do not access any further data.
let mut buffer = Vec::with_capacity(size);
unsafe {
buffer.set_len(size);
}

let recv_size = Ecall::xrecv(buffer.as_mut_ptr(), buffer.len());
buffer[0..recv_size].to_vec()
}

pub fn xrecv_to(buf: &mut [u8]) -> usize {
Ecall::xrecv(buf.as_mut_ptr(), buf.len())
}

pub fn xsend(buffer: &[u8]) {
Ecall::xsend(buffer.as_ptr(), buffer.len() as usize)
}
Expand Down
61 changes: 61 additions & 0 deletions apps/bitcoin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
This is a test V-App, with a few simple computations and functionalities to test various aspects of the Vanadium VM.

- [app](app) contains the Risc-V app, based on the V-app Sdk.
- [client](client) folder contains the client of the app, based on the V-app Client Sdk.

The `client` is a library crate (see [lib.rs](client/src/lib.rs)), but it also has a test executable ([main.rs](client/src/main.rs)) to interact with the app from the command line.

## Build the V-App

### Risc-V

In order to build the app for the Risc-V target, enter the `app` folder and run:

```sh
cargo build --release --target=riscv32i-unknown-none-elf
```

### Native

In order to build the app for the native target, enter the `app` folder and run:

```sh
cargo build --release --target=x86_64-unknown-linux-gnu
```

## Run the V-App

Make sure you built the V-App for the Risc-V target.

Launch Vanadium on speculos. Then execute:

From the `client` folder

```sh
cargo run
```

If you want to run the V-app on a real device, execute instead:

```sh
cargo run -- --hid
```

If you want to run the V-app natively, after building it for the native target, use:

```sh
cargo run -- --native
```


### Client commands

Once the client is running, these are the available commands:

- `reverse <hex_buffer>` - Reverses the given buffer.
- `sha256 <hex_buffer>` - Computes the sha256 hash of the given buffer.
- `b58enc <hex_buffer>` - Computes the base58 encoding of the given buffer (the output is in hex as well).
- `addnumbers <n>` - Computes the sum of the numbers between `1` and `n`.
- `nprimes <n>` - Counts the number of primes up to `n` using the Sieve of Eratosthenes.
- `panic <panic message>` - Cause the V-App to panic. Everything written after 'panic' is the panic message.
- An empty command will exit the V-App.
13 changes: 13 additions & 0 deletions apps/bitcoin/app/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[build]
target = "x86_64-unknown-linux-gnu"


[target.riscv32i-unknown-none-elf]
rustflags = [
# The VM expects ELF binaries with 2 segments (rx and rw). Don't put
# read-only non-executable sections in their own segment.
"-Clink-arg=--no-rosegment",
]

[env]
RUST_TEST_THREADS = "1"
4 changes: 4 additions & 0 deletions apps/bitcoin/app/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"rust-analyzer.cargo.target": "x86_64-unknown-linux-gnu",
"rust-analyzer.cargo.allTargets": false
}
11 changes: 11 additions & 0 deletions apps/bitcoin/app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "vnd-bitcoin"
version = "0.1.0"
edition = "2021"

[dependencies]
common = { package = "vnd-bitcoin-common", path = "../common"}
quick-protobuf = { version = "0.8.1", default-features = false }
sdk = { package = "vanadium-app-sdk", path = "../../../app-sdk"}

[workspace]
8 changes: 8 additions & 0 deletions apps/bitcoin/app/src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use common::message::ResponseGetMasterFingerprint;

pub fn handle_get_master_fingerprint() -> Result<ResponseGetMasterFingerprint, &'static str> {
// TODO: replace with proper sdk call
Ok(ResponseGetMasterFingerprint {
fingerprint: 0xf5acc2fd,
})
}
Loading

0 comments on commit 471d8f2

Please sign in to comment.