Skip to content

Commit

Permalink
Merge pull request #4 from spacemeshos/cffi
Browse files Browse the repository at this point in the history
Add remote-wallet crate for talking to hardware wallets
  • Loading branch information
lrettig authored May 25, 2023
2 parents 20161f4 + 312d74d commit 61e8b9f
Show file tree
Hide file tree
Showing 17 changed files with 313 additions and 255 deletions.
73 changes: 29 additions & 44 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,79 +4,64 @@ on:
release:
types: [created]

env:
ARTIFACT_NAME: spacemesh-sdk

jobs:
release:
strategy:
matrix:
include:
- image: macos-latest
name: macos-amd64
extension: dylib
artifact: libed25519_bip32_macos-amd64.tar.gz
toolchain: x86_64-apple-darwin
target: x86_64-apple-darwin
- image: ubuntu-latest
name: linux-amd64
extension: so
artifact: libed25519_bip32_linux-amd64.tar.gz
toolchain: x86_64-unknown-linux-musl
target: x86_64-unknown-linux-gnu
- image: windows-latest
name: windows-amd64
extension: dll
artifact: libed25519_bip32_windows-amd64.zip
toolchain: x86_64-pc-windows-gnu
# On Windows we use the GNU target (not MSVC, the default)
target: x86_64-pc-windows-gnu
- image: [self-hosted, macos, arm64]
name: macos-arm64
extension: dylib
artifact: libed25519_bip32_macos-arm64.tar.gz
toolchain: aarch64-apple-darwin
target: aarch64-apple-darwin
- image: [self-hosted, linux, arm64]
name: linux-arm64
extension: so
artifact: libed25519_bip32_linux-arm64.tar.gz
toolchain: aarch64-unknown-linux-musl
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.image }}
name: Release ${{ matrix.name }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up toolchain
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
target: ${{ matrix.toolchain }}
target: ${{ matrix.target }}
- name: Install required packages
# libudev and pkgconfig are required for the hidapi crate
if: matrix.name == 'linux-amd64'
run: sudo apt-get install -y libudev-dev pkg-config
- name: Regenerate C Header and Check
run: make diff
- name: Compile
if: matrix.name != 'windows-amd64'
run: cargo build --release
- name: Compile Windows
if: matrix.name == 'windows-amd64'
# On Windows we only build using the GNU toolchain (not MSVC, the default)
run: cargo build --release --target ${{ matrix.toolchain }}
- name: Compile staticlib
if: contains(matrix.toolchain, 'musl')
# Linux requires a different toolchain for static lib generation
run: |
cargo build --release --target ${{ matrix.toolchain }}
# overwrite static glibc static lib
mv target/${{ matrix.toolchain }}/release/libed25519_bip32.a target/release
run: cargo build --release --target ${{ matrix.target }}
- name: Prepare files
if: matrix.name != 'windows-amd64'
shell: bash
run: |
mkdir artifacts
mv LICENSE ed25519_bip32.h target/release/libed25519_bip32.${{ matrix.extension }} target/release/libed25519_bip32.a artifacts
# copy all relevant headers and static and dynamic libs into the new directory
find . -maxdepth 3 -type f \( \
-path './LICENSE' -o \
-path '*.h' -o \
-path './target/${{ matrix.target }}/release/*.a' -o \
-path './target/${{ matrix.target }}/release/*.dll' -o \
-path './target/${{ matrix.target }}/release/*.dylib' -o \
-path './target/${{ matrix.target }}/release/*.so' \
\) -print0 | xargs -0 -I {} mv {} artifacts
cd artifacts
tar -czf libed25519_bip32_${{ matrix.name }}.tar.gz *
mv libed25519_bip32_${{ matrix.name }}.tar.gz ..
- name: Prepare files Windows
if: matrix.name == 'windows-amd64'
run: |
mkdir artifacts
Move-Item -Path LICENSE, ed25519_bip32.h -Destination artifacts
Move-Item -Path target/${{ matrix.toolchain }}/release/ed25519_bip32.dll -Destination artifacts
Move-Item -Path target/${{ matrix.toolchain }}/release/libed25519_bip32.a -Destination artifacts
Compress-Archive -Path artifacts/* -Destination ${{ matrix.artifact }}
tar -czf ${{ env.ARTIFACT_NAME }}_${{ matrix.name }}.tar.gz *
mv ${{ env.ARTIFACT_NAME }}_${{ matrix.name }}.tar.gz ..
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ${{ matrix.artifact }}
files: ${{ env.ARTIFACT_NAME }}_${{ matrix.name }}.tar.gz
6 changes: 6 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Set up toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install required packages
run: sudo apt-get install -y libudev-dev pkg-config
- name: Regenerate C Header and Check
run: make diff
- name: Build
run: cargo build --verbose
- name: Run tests
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = [
"derivation-path",
"ed25519-bip32",
"remote-wallet",
"wallet-test",
"sdkutils",
]

[workspace.package]
Expand All @@ -21,5 +21,6 @@ qstring = "0.7.2"
solana-sdk = "=1.14.17"
spacemesh-derivation-path = { path = "derivation-path", version = "=0.0.1" }
spacemesh-remote-wallet = { path = "remote-wallet", version = "=0.0.1" }
spacemesh-sdkutils = { path = "sdkutils", version = "=0.0.1" }
thiserror = "1.0.40"
uriparse = "0.6.4"
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
HEADERFN := ed25519_bip32.h

.PHONY: build
build:
Expand All @@ -15,15 +14,17 @@ wasm:
.PHONY: cheader
cheader:
cargo install cbindgen
cd ed25519-bip32 && cbindgen -c ../cbindgen.toml -o $(HEADERFN)
cd ed25519-bip32 && cbindgen -c ../cbindgen.toml -o ed25519_bip32.h
cd remote-wallet && cbindgen -c ../cbindgen.toml -o remote_wallet.h

# Regenerate the C Header and complain if it's changed
.PHONY: diff
diff: cheader
@cd ed25519-bip32 && git diff --name-only --diff-filter=AM --exit-code $(HEADERFN) \
@cd ed25519-bip32 && git diff --name-only --diff-filter=AM --exit-code ed25519_bip32.h \
|| { echo "C header has changed"; exit 1; }
@cd remote-wallet && git diff --name-only --diff-filter=AM --exit-code remote_wallet.h \
|| { echo "C header has changed"; exit 1; }

.PHONY: clean
clean:
rm -rf ./ed25519-bip32/lib/gen
rm -rf ./target
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
# spacemesh-sdk
Low-level Rust SDK

This repository contains a low-level Rust SDK for the Spacemesh protocol and associated tooling. Various crates implement utilities such as key derivation and communication with Ledger hardware wallets (see inline Rust documentation for more information). Certain functions are externalized via Wasm bindings and CFFI bindings for use in upstream applications including [Smapp](https://github.com/spacemeshos/smapp/) and [Smcli](https://github.com/spacemeshos/smcli).

See the Github workflow files for information on how to build on various platforms as a dynamic or static library.

Portions of the codebase are forked from [Solana](https://github.com/solana-labs/solana/) with gratitude.
2 changes: 1 addition & 1 deletion cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@



language = "C++"
language = "C"



Expand Down
1 change: 1 addition & 0 deletions ed25519-bip32/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ crate-type = ["staticlib", "cdylib", "rlib"]

[dependencies]
ed25519-dalek-bip32 = "0.2.0"
spacemesh-sdkutils = { workspace = true }
wasm-bindgen = "0.2.84"
41 changes: 17 additions & 24 deletions ed25519-bip32/ed25519_bip32.h
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */

#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <ostream>
#include <new>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>



extern "C" {

/// derive_c generates a keypair from a 64-byte BIP39-compatible seed and BIP32 hierarchical
/// derivation path. it returns 64 bytes. the first 32 bytes are the secret key and the second 32
/// bytes are the public key.
/// this function does the same thing as derive_key, which is bound for wasm rather than CFFI.
/// it adds error handling in order to be friendlier to the FFI caller: in case of an error, it
/// prints the error and returns a null pointer.
/// note that the caller must call derive_free_c() to free the returned memory as ownership is
/// transferred to the caller.
uint8_t *derive_c(const uint8_t *seed, size_t seedlen, const uint8_t *path, size_t pathlen);

/// free the memory allocated and returned by the derive functions by transferring ownership back to
/// Rust. must be called on each pointer returned by the functions precisely once to ensure safety.
void derive_free_c(uint8_t *ptr);

} // extern "C"
/**
* derive_c generates a keypair from a 64-byte BIP39-compatible seed and BIP32 hierarchical
* derivation path. It writes the keypair bytes to result, which must be at least 64 bytes long.
* It returns a status code, with a return value of zero indicating success.
* This function does the same thing as derive_key, which is bound for wasm rather than CFFI.
* it adds error handling in order to be friendlier to the FFI caller: in case of an error, it
* prints the error and returns a nonzero value.
*/
uint16_t derive_c(const uint8_t *seed,
size_t seedlen,
const char *derivation_path_ptr,
uint8_t *result);
130 changes: 51 additions & 79 deletions ed25519-bip32/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
extern crate ed25519_dalek_bip32;
extern crate wasm_bindgen;
use ed25519_dalek_bip32::{ed25519_dalek::{Keypair}, DerivationPath, ExtendedSecretKey};

use std::ffi::{c_char, CStr};
use ed25519_dalek_bip32::{ed25519_dalek::{Keypair, KEYPAIR_LENGTH, SECRET_KEY_LENGTH}, DerivationPath, ExtendedSecretKey};
use spacemesh_sdkutils::{check_err, err};
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
Expand All @@ -20,92 +23,61 @@ pub fn derive_key(
Box::new(keypair.to_bytes())
}

// check for error. if no error, do nothing. if there is an error, print it and return a null ptr.
macro_rules! check_err {
($ptr:expr, $str:expr) => {
match ($ptr) {
Ok(ref _v) => (),
Err(e) => {
// TODO: return error message rather than printing it
eprint!($str);
eprintln!(": {e}");
return std::ptr::null_mut();
},
}
};
}

macro_rules! err {
($str:expr) => {
eprintln!($str);
return std::ptr::null_mut();
};
}

/// derive_c generates a keypair from a 64-byte BIP39-compatible seed and BIP32 hierarchical
/// derivation path. it returns 64 bytes. the first 32 bytes are the secret key and the second 32
/// bytes are the public key.
/// this function does the same thing as derive_key, which is bound for wasm rather than CFFI.
/// derivation path. It writes the keypair bytes to result, which must be at least 64 bytes long.
/// It returns a status code, with a return value of zero indicating success.
/// This function does the same thing as derive_key, which is bound for wasm rather than CFFI.
/// it adds error handling in order to be friendlier to the FFI caller: in case of an error, it
/// prints the error and returns a null pointer.
/// note that the caller must call derive_free_c() to free the returned memory as ownership is
/// transferred to the caller.
/// prints the error and returns a nonzero value.
#[no_mangle]
pub extern "C" fn derive_c(
seed: *const u8,
seedlen: usize,
path: *const u8,
pathlen: usize,
) -> *mut u8 {
unsafe {
let seed_slice = std::slice::from_raw_parts(seed, seedlen);
let path_str = std::str::from_utf8(std::slice::from_raw_parts(path, pathlen));
check_err!(path_str, "failed to convert string from raw parts");
let derivation_path = path_str.unwrap().parse();
check_err!(derivation_path, "failed to parse derivation path");
let derivation_path_inner: DerivationPath = derivation_path.unwrap();

// for now we are rather strict with which types of paths we accept, to avoid errors and to
// be as compatible as possible with BIP-44. the path must be of the format
// "m/44'/540'/...", i.e., it must have purpose 44 and coin type
// 540 and all path elements must be hardened. we expect it to contain between 2 and 5
// elements.
if derivation_path_inner.path().len() < 2 {
err!("path too short");
}
if derivation_path_inner.path().len() > 5 {
err!("path too long");
}
if derivation_path_inner.path()[0].to_u32() != 44 {
err!("bad path purpose");
}
if derivation_path_inner.path()[1].to_u32() != 540 {
err!("bad path coin type");
}
for p in derivation_path_inner.path() {
if !p.is_hardened() {
err!("path isn't fully hardened");
}
}

let extended = ExtendedSecretKey::from_seed(seed_slice)
.and_then(|extended| extended.derive(&derivation_path_inner));
check_err!(extended, "failed to derive secret key from seed");
let extended_inner = extended.unwrap();
let extended_public_key = extended_inner.public_key();
let keypair = Keypair{secret: extended_inner.secret_key, public: extended_public_key};
let boxed_keypair = Box::new(keypair.to_bytes());
Box::into_raw(boxed_keypair) as *mut u8
derivation_path_ptr: *const c_char,
result: *mut u8,
) -> u16 {
// Seed must be at least 32 bytes
if seedlen < SECRET_KEY_LENGTH {
err!("seed must be at least 32 bytes");
}
}
let seed_slice = unsafe { std::slice::from_raw_parts(seed, seedlen) };
let derivation_path_str = unsafe { CStr::from_ptr(derivation_path_ptr) };
let derivation_path_str = derivation_path_str.to_str();
check_err!(derivation_path_str, "failed to convert path string from raw parts");
let derivation_path_str = derivation_path_str.unwrap().parse();
check_err!(derivation_path_str, "failed to parse derivation path");
let derivation_path: DerivationPath = derivation_path_str.unwrap();

/// free the memory allocated and returned by the derive functions by transferring ownership back to
/// Rust. must be called on each pointer returned by the functions precisely once to ensure safety.
#[no_mangle]
pub extern "C" fn derive_free_c(ptr: *mut u8) {
unsafe {
if !ptr.is_null() {
let _ = Box::from_raw(ptr);
// for now we are rather strict with which types of paths we accept, to avoid errors and to
// be as compatible as possible with BIP-44. the path must be of the format
// "m/44'/540'/...", i.e., it must have purpose 44 and coin type
// 540 and all path elements must be hardened. we expect it to contain between 2 and 5
// elements.
if derivation_path.path().len() < 2 {
err!("path too short");
}
if derivation_path.path().len() > 5 {
err!("path too long");
}
if derivation_path.path()[0].to_u32() != 44 {
err!("bad path purpose");
}
if derivation_path.path()[1].to_u32() != 540 {
err!("bad path coin type");
}
for p in derivation_path.path() {
if !p.is_hardened() {
err!("path isn't fully hardened");
}
}

let extended = ExtendedSecretKey::from_seed(seed_slice)
.and_then(|extended| extended.derive(&derivation_path));
check_err!(extended, "failed to derive secret key from seed");
let extended_inner = extended.unwrap();
let extended_public_key = extended_inner.public_key();
let keypair = Keypair{secret: extended_inner.secret_key, public: extended_public_key};
let result_slice = unsafe { std::slice::from_raw_parts_mut(result, KEYPAIR_LENGTH) };
result_slice.copy_from_slice(&keypair.to_bytes());
0
}
Loading

0 comments on commit 61e8b9f

Please sign in to comment.