Skip to content

Add remote-wallet crate for talking to hardware wallets #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 44 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8f7c24c
Initial test is working
lrettig May 6, 2023
395fcde
Add sdkutils crate and refactor
lrettig May 7, 2023
4f83e6c
Add confirmKey arg to ledger fn
lrettig May 8, 2023
62668f4
Update build process
lrettig May 8, 2023
882c732
Update command to build artifact
lrettig May 8, 2023
f7fe013
Update command again
lrettig May 8, 2023
1f885e2
Add libudev
lrettig May 8, 2023
1a2063e
Add pkg-config
lrettig May 8, 2023
8a5afbb
Fix env commands
lrettig May 8, 2023
7559cee
Correctly set C_INCLUDE_PATH
lrettig May 8, 2023
4c0f635
Add musl-tools
lrettig May 8, 2023
d100800
Attempt to fix Windows build
lrettig May 8, 2023
7bc0abf
Don't try to mkdir on Windows
lrettig May 8, 2023
6b3f88d
Try using bash on Windows
lrettig May 8, 2023
dc5983f
CI release flow is working!
lrettig May 9, 2023
c3c3288
Update rust.yml
lrettig May 9, 2023
9d88ad4
Add steps to build workflow
lrettig May 9, 2023
5cfec67
Merge branch 'cffi' of github.com:spacemeshos/spacemesh-sdk into cffi
lrettig May 9, 2023
0f7e9fd
Add rust toolchain
lrettig May 9, 2023
7be429d
Skip ledger hardware wallet test
lrettig May 9, 2023
9aa73c2
Update README.md
lrettig May 9, 2023
35844ea
Modify CFFI function signatures
lrettig May 9, 2023
7893c02
Merge branch 'cffi' of github.com:spacemeshos/spacemesh-sdk into cffi
lrettig May 9, 2023
607efda
Switch headers C++ -> C
lrettig May 10, 2023
2ca30e9
Temporarily re-enable release workflow
lrettig May 10, 2023
94812e3
Refactor FFI functions
lrettig May 10, 2023
1e56b5c
Update C headers
lrettig May 10, 2023
45cd867
Fix release workflow again
lrettig May 10, 2023
ce404ee
Use same naming format for C header files
lrettig May 10, 2023
28ca5c2
Remove musl toolchain
lrettig May 10, 2023
276bbfc
Rename C header
lrettig May 14, 2023
e704fe4
Merge branch 'main' into cffi
lrettig May 24, 2023
3527c51
Fix dependency
lrettig May 24, 2023
761cc8d
Pin solana-sdk version, fix release workflow
lrettig May 24, 2023
7dab526
Merge branch 'main' into cffi
lrettig May 24, 2023
bdfedbf
Update C header
lrettig May 24, 2023
bbb7a1f
Update .github/workflows/rust.yml
lrettig May 25, 2023
acfe2de
Clean up cargo target selection in CI workflow
lrettig May 25, 2023
aebb77c
Update remote-wallet/src/lib.rs
lrettig May 25, 2023
1cd4e61
Minor cleanup per review comments
lrettig May 25, 2023
f443041
Merge branch 'cffi' of github.com:spacemeshos/spacemesh-sdk into cffi
lrettig May 25, 2023
e529a7f
Misc cleanup
lrettig May 25, 2023
d003cbc
Add comment
lrettig May 25, 2023
312d74d
Update C header
lrettig May 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

@fasmat fasmat May 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix the rpath issue on macOS this file I think needs the following configuration:

[profile.release-clib]
inherits = "release"
strip = true
lto = true
rpath = true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to go ahead and merge without this. If it's still causing issues we can open a new PR to fix it (as you did with #5)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if related but I created a new issue: #12

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. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this header file required to be written by hand?

I think https://github.com/spacemeshos/post-rs generates the C headers when it builds the library, maybe @poszu can help here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the question. It's not written by hand. There's a makefile recipe for this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah my bad. It seems like https://github.com/spacemeshos/post-rs also generates the headers using a build.rs file, but doesn't commit them to the repository. I misunderstood the files to be manually edited 😞

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why check it in the repo though? Cbindgen can be used with build.rs to automatically generate a header when the rust project is being built. This is the approach taken by post-rs (see https://github.com/spacemeshos/post-rs/blob/755f5c2432dd7493b4c6457dce1556955fe9a94d/ffi/build.rs). It has a few advantages:

  • no need for extra make targets,
  • no risk of forgetting to check the header in,
  • no need to manually install cbindgen (it is declared as a build dependency and fetched automatically by cargo).

The header is later bundled together with the library in release artifacts: https://github.com/spacemeshos/post-rs/blob/fbf1b886253beec659527262499d1392490ea14d/.github/workflows/ci.yml#L155-L162

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no strong opinion. personally I kind of like manually regenerating it, and being alerted when it changes.


#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