Skip to content

Commit

Permalink
Merge pull request #54 from LedgerHQ/btc-handlers
Browse files Browse the repository at this point in the history
Initial handlers for the bitcoin V-App
  • Loading branch information
bigspider authored Jan 9, 2025
2 parents a3537cc + 1e00297 commit 0a168e0
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 813 deletions.
16 changes: 10 additions & 6 deletions app-sdk/src/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ where
}
}

impl<C, const SCALAR_LENGTH: usize> AsRef<Point<C, SCALAR_LENGTH>>
for EcfpPublicKey<C, SCALAR_LENGTH>
where
C: Curve<SCALAR_LENGTH>,
{
fn as_ref(&self) -> &Point<C, SCALAR_LENGTH> {
&self.public_key
}
}

pub struct EcfpPrivateKey<C, const SCALAR_LENGTH: usize>
where
C: Curve<SCALAR_LENGTH>,
Expand Down Expand Up @@ -546,10 +556,7 @@ mod tests {

let signature = privkey.ecdsa_sign_hash(&msg_hash).unwrap();

println!("Signature: {:?}", signature);

let pubkey = privkey.to_public_key();
println!("Public key: {:?}", pubkey.public_key.to_bytes());
pubkey.ecdsa_verify_hash(&msg_hash, &signature).unwrap();
}

Expand All @@ -564,11 +571,8 @@ mod tests {
let msg = "If you don't believe me or don't get it, I don't have time to try to convince you, sorry.";

let signature = privkey.schnorr_sign(msg.as_bytes()).unwrap();
println!("Signature: {:?}", signature);

let pubkey = privkey.to_public_key();
pubkey.schnorr_verify(msg.as_bytes(), &signature).unwrap();

println!("pubkey: {:?}", pubkey.public_key.to_bytes());
}
}
17 changes: 5 additions & 12 deletions apps/bitcoin/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
This is a test V-App, with a few simple computations and functionalities to test various aspects of the Vanadium VM.
This is the Bitcoin app for Vanadium. It is composed of the following crates:

- [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.
- [app](app) contains the V-app.
- [client](client) contains the client of the V-app.
- [common](common) contains the code shared between the V-App and the client.

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.

Expand Down Expand Up @@ -50,12 +51,4 @@ If you want to run the V-app natively, after building it for the native target,

### 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.
TODO
3 changes: 3 additions & 0 deletions apps/bitcoin/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ common = { package = "vnd-bitcoin-common", path = "../common"}
quick-protobuf = { version = "0.8.1", default-features = false }
sdk = { package = "vanadium-app-sdk", path = "../../../app-sdk"}

[dev-dependencies]
bs58 = { version = "0.5.1"}

[workspace]
156 changes: 156 additions & 0 deletions apps/bitcoin/app/src/handlers/get_extended_pubkey.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use std::borrow::Cow;

use common::message::{RequestGetExtendedPubkey, ResponseGetExtendedPubkey};
use sdk::{
curve::{Curve, EcfpPrivateKey, EcfpPublicKey, Secp256k1, ToPublicKey},
hash::{Hasher, Ripemd160, Sha256},
};

const BIP32_TESTNET_PUBKEY_VERSION: u32 = 0x043587CFu32;

fn get_pubkey_fingerprint(pubkey: &EcfpPublicKey<Secp256k1, 32>) -> u32 {
let pk_bytes = pubkey.as_ref().to_bytes();
let mut sha256hasher = Sha256::new();
sha256hasher.update(&[pk_bytes[64] % 2 + 0x02]);
sha256hasher.update(&pk_bytes[1..33]);
let mut sha256 = [0u8; 32];
sha256hasher.digest(&mut sha256);

let hash = Ripemd160::hash(&sha256);

u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]])
}

pub fn handle_get_extended_pubkey<'a, 'b>(
req: &'a RequestGetExtendedPubkey,
) -> Result<ResponseGetExtendedPubkey<'b>, &'static str> {
if req.bip32_path.len() > 256 {
return Err("Derivation path is too long");
}

if req.display {
todo!("Display is not yet implemented")
}

let hd_node = sdk::curve::Secp256k1::derive_hd_node(&req.bip32_path)?;
let privkey: EcfpPrivateKey<Secp256k1, 32> = EcfpPrivateKey::new(*hd_node.privkey);
let pubkey = privkey.to_public_key();
let pubkey_bytes = pubkey.as_ref().to_bytes();

let depth = req.bip32_path.len() as u8;

let parent_fpr: u32 = if req.bip32_path.is_empty() {
0
} else {
let hd_node =
sdk::curve::Secp256k1::derive_hd_node(&req.bip32_path[..req.bip32_path.len() - 1])?;
let parent_privkey: EcfpPrivateKey<Secp256k1, 32> = EcfpPrivateKey::new(*hd_node.privkey);
let parent_pubkey = parent_privkey.to_public_key();
get_pubkey_fingerprint(&parent_pubkey)
};

let child_number: u32 = if req.bip32_path.is_empty() {
0
} else {
req.bip32_path[req.bip32_path.len() - 1]
};

let mut xpub = Vec::with_capacity(78);
xpub.extend_from_slice(&BIP32_TESTNET_PUBKEY_VERSION.to_be_bytes());
xpub.push(depth);
xpub.extend_from_slice(&parent_fpr.to_be_bytes());
xpub.extend_from_slice(&child_number.to_be_bytes());
xpub.extend_from_slice(&hd_node.chaincode);
xpub.push(pubkey_bytes[64] % 2 + 0x02);
xpub.extend_from_slice(&pubkey_bytes[1..33]);

Ok(ResponseGetExtendedPubkey {
pubkey: Cow::Owned(xpub),
})
}

#[cfg(test)]
mod tests {
use super::*;
use bs58;

use std::num::ParseIntError;

// TODO: this should be implemented and tested elsewhere
/// Parse a Bitcoin-style derivation path (e.g., "m/48'/1'/4'/1'/0/7") into a list of
/// child indices as `u32`. Hardened indices are marked by an apostrophe (`'`).
pub fn parse_derivation_path(path: &str) -> Result<Vec<u32>, String> {
// Split by '/' to get each component. e.g. "m/48'/1'/4'/1'/0/7" -> ["m", "48'", "1'", "4'", "1'", "0", "7"]
let mut components = path.split('/').collect::<Vec<&str>>();

// The first component should be "m". Remove it if present.
if let Some(first) = components.first() {
if *first == "m" {
components.remove(0);
}
}

let mut indices = Vec::new();
for comp in components {
// Check if this component is hardened
let hardened = comp.ends_with('\'');

// Remove the apostrophe if hardened
let raw_index = if hardened {
&comp[..comp.len() - 1]
} else {
comp
};

// Parse the numeric portion
let index: u32 = raw_index.parse::<u32>().map_err(|e: ParseIntError| {
format!("Invalid derivation index '{}': {}", comp, e)
})?;

// If hardened, add the 0x80000000 mask
let child_number = if hardened {
0x80000000_u32
.checked_add(index)
.ok_or_else(|| format!("Invalid hardened index '{}': overflowed", comp))?
} else {
index
};

indices.push(child_number);
}

Ok(indices)
}

#[test]
fn test_handle_get_extended_pubkey() {
let testcases = vec![
("m/44'/1'/0'", "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT"),
("m/44'/1'/10'", "tpubDCwYjpDhUdPGp21gSpVay2QPJVh6WNySWMXPhbcu1DsxH31dF7mY18oibbu5RxCLBc1Szerjscuc3D5HyvfYqfRvc9mesewnFqGmPjney4d"),
("m/44'/1'/2'/1/42", "tpubDGF9YgHKv6qh777rcqVhpmDrbNzgophJM9ec7nHiSfrbss7fVBXoqhmZfohmJSvhNakDHAspPHjVVNL657tLbmTXvSeGev2vj5kzjMaeupT"),
("m/48'/1'/4'/1'/0/7", "tpubDK8WPFx4WJo1R9mEL7Wq325wBiXvkAe8ipgb9Q1QBDTDUD2YeCfutWtzY88NPokZqJyRPKHLGwTNLT7jBG59aC6VH8q47LDGQitPB6tX2d7"),
("m/49'/1'/1'/1/3", "tpubDGnetmJDCL18TyaaoyRAYbkSE9wbHktSdTS4mfsR6inC8c2r6TjdBt3wkqEQhHYPtXpa46xpxDaCXU2PRNUGVvDzAHPG6hHRavYbwAGfnFr"),
("m/84'/1'/2'/0/10", "tpubDG9YpSUwScWJBBSrhnAT47NcT4NZGLcY18cpkaiWHnkUCi19EtCh8Heeox268NaFF6o56nVeSXuTyK6jpzTvV1h68Kr3edA8AZp27MiLUNt"),
("m/86'/1'/4'/1/12", "tpubDHTZ815MvTaRmo6Qg1rnU6TEU4ZkWyA56jA1UgpmMcBGomnSsyo34EZLoctzZY9MTJ6j7bhccceUeXZZLxZj5vgkVMYfcZ7DNPsyRdFpS3f"),
];

for (path, expected_xpub) in testcases {
// decode the derivation path into a Vec<u32>

let req = RequestGetExtendedPubkey {
bip32_path: parse_derivation_path(path).unwrap(),
display: false,
};

let response = handle_get_extended_pubkey(&req).unwrap();

assert_eq!(
response.pubkey,
bs58::decode(expected_xpub)
.with_check(None)
.into_vec()
.unwrap()
);
}
}
}
19 changes: 19 additions & 0 deletions apps/bitcoin/app/src/handlers/get_master_fingerprint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use common::message::ResponseGetMasterFingerprint;
use sdk::curve::Curve;

pub fn handle_get_master_fingerprint() -> Result<ResponseGetMasterFingerprint, &'static str> {
Ok(ResponseGetMasterFingerprint {
fingerprint: sdk::curve::Secp256k1::get_master_fingerprint(),
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_handle_get_master_fingerprint() {
let response = handle_get_master_fingerprint().unwrap();
assert_eq!(response.fingerprint, 0xf5acc2fdu32);
}
}
11 changes: 4 additions & 7 deletions apps/bitcoin/app/src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use common::message::ResponseGetMasterFingerprint;
mod get_extended_pubkey;
mod get_master_fingerprint;

pub fn handle_get_master_fingerprint() -> Result<ResponseGetMasterFingerprint, &'static str> {
// TODO: replace with proper sdk call
Ok(ResponseGetMasterFingerprint {
fingerprint: 0xf5acc2fd,
})
}
pub use get_extended_pubkey::handle_get_extended_pubkey;
pub use get_master_fingerprint::handle_get_master_fingerprint;
36 changes: 18 additions & 18 deletions apps/bitcoin/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,26 +53,26 @@ fn handle_req_<'a>(buffer: &'a [u8]) -> Result<Response<'a>, &'static str> {
let request: Request =
Request::from_reader(&mut reader, buffer).map_err(|_| "Failed to parse request")?; // TODO: proper error handling

let response = Response {
response: match request.request {
OneOfrequest::get_version(_) => OneOfresponse::get_version(todo!()),
OneOfrequest::exit(_) => {
sdk::exit(0);
}
OneOfrequest::get_master_fingerprint(_) => {
OneOfresponse::get_master_fingerprint(handle_get_master_fingerprint()?)
}
OneOfrequest::get_extended_pubkey(req) => OneOfresponse::get_extended_pubkey(todo!()),
OneOfrequest::register_wallet(req) => OneOfresponse::register_wallet(todo!()),
OneOfrequest::get_wallet_address(req) => OneOfresponse::get_wallet_address(todo!()),
OneOfrequest::sign_psbt(req) => OneOfresponse::sign_psbt(todo!()),
OneOfrequest::None => OneOfresponse::error(ResponseError {
error_msg: Cow::Borrowed("Invalid command"),
}),
},
let response = match request.request {
OneOfrequest::get_version(_) => OneOfresponse::get_version(todo!()),
OneOfrequest::exit(_) => {
sdk::exit(0);
}
OneOfrequest::get_master_fingerprint(_) => {
OneOfresponse::get_master_fingerprint(handle_get_master_fingerprint()?)
}
OneOfrequest::get_extended_pubkey(req) => {
OneOfresponse::get_extended_pubkey(handle_get_extended_pubkey(&req)?)
}
OneOfrequest::register_wallet(_req) => OneOfresponse::register_wallet(todo!()),
OneOfrequest::get_wallet_address(_req) => OneOfresponse::get_wallet_address(todo!()),
OneOfrequest::sign_psbt(_req) => OneOfresponse::sign_psbt(todo!()),
OneOfrequest::None => OneOfresponse::error(ResponseError {
error_msg: Cow::Borrowed("Invalid command"),
}),
};

Ok(response)
Ok(Response { response })
}

fn handle_req(buffer: &[u8]) -> Vec<u8> {
Expand Down
2 changes: 2 additions & 0 deletions apps/bitcoin/common/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/message/message.rs

3 changes: 3 additions & 0 deletions apps/bitcoin/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ edition = "2021"
[dependencies]
quick-protobuf = { version = "0.8.1", default-features = false }

[build-dependencies]
pb-rs = "0.10.0"

[workspace]
19 changes: 19 additions & 0 deletions apps/bitcoin/common/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::process::Command;

fn main() {
let proto_files = &["src/message/message.proto"];

for proto_file in proto_files {
let output = Command::new("pb-rs")
.args(&["--nostd", proto_file])
.output()
.expect("Failed to execute pb-rs");

if !output.status.success() {
panic!(
"pb-rs failed with error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
}
4 changes: 3 additions & 1 deletion apps/bitcoin/common/src/message/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Generate rust modules from protobuf with:
The module corresponding to `message.proto` is automatically generated at build time via [build.rs](../../build.rs).

You can generate it manually with:

$ cargo install pb-rs
$ pb-rs --nostd ./message.proto
2 changes: 1 addition & 1 deletion apps/bitcoin/common/src/message/message.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ message RequestGetExtendedPubkey {
}

message ResponseGetExtendedPubkey {
string pubkey = 1;
bytes pubkey = 1;
}

message RequestRegisterWallet {
Expand Down
Loading

0 comments on commit 0a168e0

Please sign in to comment.