Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
209 changes: 209 additions & 0 deletions .github/workflows/build-all.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
name: Build All FFI

on:
release:
types: [published]

env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"

jobs:
build-apple:
runs-on: macos-latest
defaults:
run:
working-directory: sphinx-ffi

steps:
- uses: actions/checkout@v4

- name: Configure git for private repos
run: git config --global url."https://x-access-token:${{ secrets.PRIVATE_REPO_PAT }}@github.com/".insteadOf "https://github.com/"

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-apple-ios,aarch64-apple-ios,x86_64-apple-darwin,aarch64-apple-darwin

- name: Generate Swift bindings
run: |
cargo run --features=uniffi/cli --bin uniffi-bindgen generate src/sphinxrs.udl --language swift
sed -i '' 's/module\ sphinxrsFFI/framework\ module\ sphinxrsFFI/' src/sphinxrsFFI.modulemap

- name: Build iOS (x86_64)
run: cargo build --features=uniffi/cli --target=x86_64-apple-ios --release

- name: Build iOS (aarch64)
run: cargo build --features=uniffi/cli --target=aarch64-apple-ios --release

- name: Create iOS universal lib
run: lipo -create target/x86_64-apple-ios/release/libsphinxrs.a target/aarch64-apple-ios/release/libsphinxrs.a -output target/universal-sphinxrs.a

- name: Build macOS (x86_64)
run: cargo build --features=uniffi/cli --target=x86_64-apple-darwin --release

- name: Build macOS (aarch64)
run: cargo build --features=uniffi/cli --target=aarch64-apple-darwin --release

- name: Create macOS universal lib
run: lipo -create target/x86_64-apple-darwin/release/libsphinxrs.a target/aarch64-apple-darwin/release/libsphinxrs.a -output target/universal-sphinxrs-mac.a

- name: Create macOS dylib universal
run: |
lipo -create target/x86_64-apple-darwin/release/libsphinxrs.dylib target/aarch64-apple-darwin/release/libsphinxrs.dylib -output target/universal-sphinxrs-mac.dylib || true

- name: Upload iOS universal lib
uses: actions/upload-artifact@v4
with:
name: ios-universal
path: sphinx-ffi/target/universal-sphinxrs.a

- name: Upload macOS universal lib
uses: actions/upload-artifact@v4
with:
name: macos-universal
path: sphinx-ffi/target/universal-sphinxrs-mac.a

- name: Stage macOS dylibs
run: |
mkdir -p target/dylibs/x86_64
mkdir -p target/dylibs/aarch64
cp target/x86_64-apple-darwin/release/libsphinxrs.dylib target/dylibs/x86_64/libsphinxrs.dylib
cp target/aarch64-apple-darwin/release/libsphinxrs.dylib target/dylibs/aarch64/libsphinxrs.dylib

- name: Upload macOS dylibs
uses: actions/upload-artifact@v4
with:
name: macos-dylibs
path: sphinx-ffi/target/dylibs

- name: Upload Swift bindings
uses: actions/upload-artifact@v4
with:
name: swift-bindings
path: |
sphinx-ffi/src/sphinxrs.swift
sphinx-ffi/src/sphinxrsFFI.modulemap
sphinx-ffi/src/sphinxrsFFI.h

build-android:
runs-on: ubuntu-latest
defaults:
run:
working-directory: sphinx-ffi

steps:
- uses: actions/checkout@v4

- name: Configure git for private repos
run: git config --global url."https://x-access-token:${{ secrets.PRIVATE_REPO_PAT }}@github.com/".insteadOf "https://github.com/"

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross

- name: Generate Kotlin bindings
run: |
cargo run --features=uniffi/cli --bin uniffi-bindgen generate src/sphinxrs.udl --language kotlin
sed -i 's/return "uniffi_sphinxrs"/return "sphinxrs"/' src/uniffi/sphinxrs/sphinxrs.kt

- name: Build Android targets
run: |
cross build --features=uniffi/cli --target i686-linux-android --release
cross build --features=uniffi/cli --target aarch64-linux-android --release
cross build --features=uniffi/cli --target arm-linux-androideabi --release
cross build --features=uniffi/cli --target armv7-linux-androideabi --release
cross build --features=uniffi/cli --target x86_64-linux-android --release

- name: Package Android libraries
run: |
mkdir -p target/out/x86
mkdir -p target/out/arm64-v8a
mkdir -p target/out/armeabi
mkdir -p target/out/armeabi-v7a
mkdir -p target/out/x86_64
mv target/i686-linux-android/release/libsphinxrs.so target/out/x86/libsphinxrs.so
mv target/aarch64-linux-android/release/libsphinxrs.so target/out/arm64-v8a/libsphinxrs.so
mv target/arm-linux-androideabi/release/libsphinxrs.so target/out/armeabi/libsphinxrs.so
mv target/armv7-linux-androideabi/release/libsphinxrs.so target/out/armeabi-v7a/libsphinxrs.so
mv target/x86_64-linux-android/release/libsphinxrs.so target/out/x86_64/libsphinxrs.so
cd target && zip -r kotlin-libraries.zip out

- name: Upload Android artifacts
uses: actions/upload-artifact@v4
with:
name: android-libraries
path: sphinx-ffi/target/kotlin-libraries.zip

- name: Upload Kotlin bindings
uses: actions/upload-artifact@v4
with:
name: kotlin-bindings
path: sphinx-ffi/src/uniffi/sphinxrs/sphinxrs.kt

build-windows:
runs-on: ubuntu-latest
needs: build-apple
defaults:
run:
working-directory: sphinx-ffi

steps:
- uses: actions/checkout@v4

- name: Configure git for private repos
run: git config --global url."https://x-access-token:${{ secrets.PRIVATE_REPO_PAT }}@github.com/".insteadOf "https://github.com/"

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross

- name: Build Windows targets
run: |
cross build --features=uniffi/cli --target x86_64-pc-windows-gnu --release
cross build --features=uniffi/cli --target i686-pc-windows-gnu --release

- name: Download macOS dylibs
uses: actions/download-artifact@v4
with:
name: macos-dylibs
path: sphinx-ffi/macos-dylibs

- name: Package Windows libraries
run: |
mkdir -p target/windows/x86_64
mkdir -p target/windows/i686
mkdir -p target/windows/aarch64
mv target/x86_64-pc-windows-gnu/release/sphinxrs.dll target/windows/x86_64/sphinxrs.dll
mv target/i686-pc-windows-gnu/release/sphinxrs.dll target/windows/i686/sphinxrs.dll
mv macos-dylibs/x86_64/libsphinxrs.dylib target/windows/x86_64/sphinxrs.dylib
mv macos-dylibs/aarch64/libsphinxrs.dylib target/windows/aarch64/sphinxrs.dylib
cd target && zip -r windows-libraries.zip windows

- name: Upload Windows artifacts
uses: actions/upload-artifact@v4
with:
name: windows-libraries
path: sphinx-ffi/target/windows-libraries.zip

upload-release:
runs-on: ubuntu-latest
needs: [build-apple, build-android, build-windows]
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Upload artifacts to release
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/ios-universal/universal-sphinxrs.a
artifacts/macos-universal/universal-sphinxrs-mac.a
artifacts/android-libraries/kotlin-libraries.zip
artifacts/windows-libraries/windows-libraries.zip
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ exclude = [
"sphinx-frost",
"sphinx-wasm",
"rmp-utils",
"derive",
]
17 changes: 17 additions & 0 deletions derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "sphinx-derive"
version = "0.1.0"
authors = ["Evan Feenstra <evanfeenstra@gmail.com>"]
edition = "2018"
description = "Sphinx key derivation utils"
repository = "https://github.com/stakwork/sphinx-rs"
license = "MIT"

[lib]
doctest = false

[dependencies]
bitcoin = "0.30.2"
bip39 = { version = "1.0.1", default-features = false }
anyhow = { version = "1", default-features = false }

157 changes: 157 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use bitcoin::hashes::sha256::Hash as BitcoinSha256;
use bitcoin::hashes::{Hash, HashEngine, Hmac, HmacEngine};
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use bitcoin::Network;

pub const ENTROPY_LEN: usize = 16;

/// derive a secret from another secret using HKDF-SHA256
pub fn hkdf_sha256(secret: &[u8], info: &[u8], salt: &[u8]) -> [u8; 32] {
let mut result = [0u8; 32];
hkdf_extract_expand(salt, secret, info, &mut result);
result
}

fn hkdf_extract_expand(salt: &[u8], secret: &[u8], info: &[u8], output: &mut [u8]) {
let mut hmac = HmacEngine::<BitcoinSha256>::new(salt);
hmac.input(secret);
let prk = Hmac::from_engine(hmac).to_byte_array();

let mut t = [0; 32];
let mut n: u8 = 0;

for chunk in output.chunks_mut(32) {
let mut hmac = HmacEngine::<BitcoinSha256>::new(&prk[..]);
n = n.checked_add(1).expect("HKDF size limit exceeded.");
if n != 1 {
hmac.input(&t);
}
hmac.input(&info);
hmac.input(&[n]);
t = Hmac::from_engine(hmac).to_byte_array();
chunk.copy_from_slice(&t);
}
}

/// CLN compatible node key derivation
pub fn node_keys(network: &Network, seed: &[u8]) -> (PublicKey, SecretKey) {
let _ = network; // CLN native derivation doesn't use network for node keys
let secp_ctx = Secp256k1::new();
let node_private_bytes = hkdf_sha256(seed, "nodeid".as_bytes(), &[]);
let node_secret_key = SecretKey::from_slice(&node_private_bytes).unwrap();
let node_id = PublicKey::from_secret_key(&secp_ctx, &node_secret_key);
(node_id, node_secret_key)
}

pub fn mnemonic_from_entropy(entropy: &[u8]) -> anyhow::Result<String> {
let mn = bip39::Mnemonic::from_entropy(entropy)
.map_err(|e| anyhow::anyhow!("Mnemonic::from_entropy failed {:?}", e))?;
let mut ret = Vec::new();
mn.word_iter().for_each(|w| ret.push(w.to_string()));
Ok(ret.join(" "))
}

pub fn entropy_from_mnemonic(mn: &str) -> anyhow::Result<Vec<u8>> {
let mn = bip39::Mnemonic::parse_normalized(mn)
.map_err(|e| anyhow::anyhow!("Mnemonic::parse_normalized failed {:?}", e))?;
match mn.word_count() {
12 => (),
len => {
return Err(anyhow::anyhow!(
"Mnemonic is length {}, should be 12 words long.",
len
))
}
}
let (array, len) = mn.to_entropy_array();
if len != 16 {
return Err(anyhow::anyhow!("Should never happen, 12 words didn't convert to 16 bytes of entropy. Please try again."));
}
Ok(array[..len].to_vec())
}

pub fn mnemonic_to_seed(mn: &str) -> anyhow::Result<Vec<u8>> {
let mn = bip39::Mnemonic::parse_normalized(mn)
.map_err(|e| anyhow::anyhow!("Mnemonic::parse_normalized failed {:?}", e))?;
match mn.word_count() {
12 => (),
len => {
return Err(anyhow::anyhow!(
"Mnemonic is length {}, should be 12 words long.",
len
))
}
}
// BIP39 seed is 64 bytes. Do like CLN does, chop off the last 32 bytes.
let e = mn.to_seed_normalized("")[..32].to_vec();
Ok(e)
}

pub fn entropy_to_seed(entropy: &[u8]) -> anyhow::Result<Vec<u8>> {
match entropy.len() {
16 => (),
len => {
return Err(anyhow::anyhow!(
"Entropy is length {}, should be 16 bytes.",
len
))
}
}
let mn = bip39::Mnemonic::from_entropy(entropy)
.map_err(|e| anyhow::anyhow!("Mnemonic::from_entropy failed {:?}", e))?;
if mn.word_count() != 12 {
return Err(anyhow::anyhow!("Should never happen, 16 bytes of entropy didn't convert to 12 words. Please try again."));
}
// Do like CLN does, chop off the last 32 bytes
let e = mn.to_seed_normalized("")[..32].to_vec();
Ok(e)
}

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

fn entropy() -> [u8; 16] {
[1; 16]
}

fn seed() -> [u8; 32] {
[1; 32]
}

#[test]
fn test_mnemonic() {
let entropy = entropy();
let mn = mnemonic_from_entropy(&entropy).expect("nope");
assert_eq!(
mn,
"absurd amount doctor acoustic avoid letter advice cage absurd amount doctor adjust"
);
let en = entropy_from_mnemonic(&mn).expect("fail");
assert_eq!(en, entropy);
}

#[test]
fn test_mnemonic_to_seed() {
let seed = mnemonic_to_seed(
"absurd amount doctor acoustic avoid letter advice cage absurd amount doctor adjust",
)
.expect("fail");
let vector = [
2, 89, 45, 66, 60, 78, 124, 109, 24, 148, 119, 19, 180, 127, 121, 87, 201, 241, 221,
208, 161, 150, 214, 73, 215, 119, 205, 145, 70, 156, 15, 179,
];
assert_eq!(seed, vector);
}

#[test]
fn test_derive() {
let net = Network::Regtest;
let ks = node_keys(&net, &seed());
let hexpk = ks.0.to_string();
assert_eq!(
hexpk,
"026f61d7ee82f937f9697f4f3e44bfaaa25849cc4f526b3a57326130eba6346002"
);
}
}
Loading