Skip to content

Commit 79971ba

Browse files
committed
feat: add derive_store_id function
1 parent 931d287 commit 79971ba

File tree

7 files changed

+161
-17
lines changed

7 files changed

+161
-17
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "vss-rust-client-ffi"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
edition = "2021"
55
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
66

README.md

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,19 @@ cargo build --release
3232
```swift
3333
import vss_rust_client_ffi
3434

35-
// Initialize VSS client with LNURL-auth
35+
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
36+
let passphrase: String? = nil
37+
let storeId = try vssDeriveStoreId(
38+
prefix: "bitkit_v1_regtest",
39+
mnemonic: mnemonic,
40+
passphrase: passphrase
41+
)
42+
3643
try await vssNewClientWithLnurlAuth(
3744
baseUrl: "https://vss.example.com",
38-
storeId: "my-app-store",
39-
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
40-
passphrase: nil,
45+
storeId: storeId,
46+
mnemonic: mnemonic,
47+
passphrase: passphrase,
4148
lnurlAuthServerUrl: "https://auth.example.com/lnurl"
4249
)
4350

@@ -73,12 +80,19 @@ vssShutdownClient()
7380
```python
7481
from vss_rust_client_ffi import *
7582

76-
# Initialize VSS client with LNURL-auth
83+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
84+
passphrase = None
85+
store_id = vss_derive_store_id(
86+
prefix="bitkit_v1_regtest",
87+
mnemonic=mnemonic,
88+
passphrase=passphrase
89+
)
90+
7791
await vss_new_client_with_lnurl_auth(
7892
"https://vss.example.com",
79-
"my-app-store",
80-
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
81-
None, # passphrase
93+
store_id,
94+
mnemonic,
95+
passphrase,
8296
"https://auth.example.com/lnurl"
8397
)
8498

@@ -117,12 +131,19 @@ vss_shutdown_client()
117131
```kotlin
118132
import uniffi.vss_rust_client_ffi.*
119133

120-
// Initialize VSS client with LNURL-auth
134+
val mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
135+
val passphrase: String? = null
136+
val storeId = vssDeriveStoreId(
137+
prefix = "bitkit_v1_regtest",
138+
mnemonic = mnemonic,
139+
passphrase = passphrase
140+
)
141+
121142
vssNewClientWithLnurlAuth(
122143
baseUrl = "https://vss.example.com",
123-
storeId = "my-app-store",
124-
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
125-
passphrase = null,
144+
storeId = storeId,
145+
mnemonic = mnemonic,
146+
passphrase = passphrase,
126147
lnurlAuthServerUrl = "https://auth.example.com/lnurl"
127148
)
128149

@@ -171,6 +192,15 @@ Initialize the global VSS client connection with LNURL-auth authentication. Prov
171192
#### `vssShutdownClient() -> Void`
172193
Shutdown the VSS client and clean up resources. Optional but recommended for clean application shutdown.
173194

195+
### Utility Functions
196+
197+
#### `vssDeriveStoreId(prefix: String, mnemonic: String, passphrase: String?) -> String`
198+
Derives a deterministic VSS store ID from a mnemonic and optional passphrase using BIP32 key derivation.
199+
200+
- `prefix`: A prefix to include in the store ID (e.g., "bitkit_v1_regtest")
201+
- `mnemonic`: BIP39 mnemonic phrase (12 or 24 words)
202+
- `passphrase`: Optional BIP39 passphrase
203+
174204
### Data Operations
175205

176206
#### `vssStore(key: String, value: Data) -> VssItem`
@@ -294,4 +324,4 @@ This project is licensed under the MIT License - see the LICENSE file for detail
294324

295325
- [VSS Server](https://github.com/lightningdevkit/vss-server) - The VSS server implementation
296326
- [vss-client](https://crates.io/crates/vss-client) - The underlying Rust client library
297-
- [UniFFI](https://mozilla.github.io/uniffi-rs/) - The FFI binding generator
327+
- [UniFFI](https://mozilla.github.io/uniffi-rs/) - The FFI binding generator

bindings/android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ android.useAndroidX=true
33
android.nonTransitiveRClass=true
44
kotlin.code.style=official
55
# project settings:
6-
version=0.2.0
6+
version=0.3.0

src/implementation.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,70 @@ use vss_client::util::retry::{
2121
MaxAttemptsRetryPolicy, MaxTotalDelayRetryPolicy, RetryPolicy,
2222
};
2323
use vss_client::util::storable_builder::{EntropySource, StorableBuilder};
24+
use bip39::Mnemonic;
25+
use std::str::FromStr;
2426

2527
const VSS_HARDENED_CHILD_INDEX: u32 = 877;
2628
const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138;
29+
const VSS_STORE_ID_HARDENED_CHILD_INDEX: u32 = 118;
30+
const VSS_STORE_ID_HASH_LENGTH: usize = 36;
31+
32+
/// Derives a deterministic VSS store ID from a mnemonic and optional passphrase.
33+
///
34+
/// # Parameters
35+
/// - `prefix`: A prefix to include in the store ID
36+
/// - `mnemonic`: BIP39 mnemonic phrase (12 or 24 words)
37+
/// - `passphrase`: Optional BIP39 passphrase
38+
///
39+
/// # Returns
40+
/// A store ID string or VssError on failure
41+
pub fn derive_vss_store_id(
42+
prefix: String,
43+
mnemonic: String,
44+
passphrase: Option<String>,
45+
) -> Result<String, VssError> {
46+
let mnemonic = Mnemonic::from_str(&mnemonic).map_err(|e| VssError::ConnectionError {
47+
error_details: format!("Invalid mnemonic: {}", e),
48+
})?;
49+
50+
let seed = match passphrase {
51+
Some(passphrase) => mnemonic.to_seed(&passphrase),
52+
None => mnemonic.to_seed(""),
53+
};
54+
let seed_array: [u8; 32] = seed[..32]
55+
.try_into()
56+
.map_err(|_| VssError::ConnectionError {
57+
error_details: "Failed to extract seed from mnemonic".to_string(),
58+
})?;
59+
60+
let secp = Secp256k1::new();
61+
let master_xprv = Xpriv::new_master(Network::Bitcoin, &seed_array).map_err(|e| {
62+
VssError::ConnectionError {
63+
error_details: format!("Failed to create master key: {}", e),
64+
}
65+
})?;
66+
67+
let vss_store_id_xprv = master_xprv
68+
.derive_priv(
69+
&secp,
70+
&[
71+
ChildNumber::Hardened { index: VSS_HARDENED_CHILD_INDEX },
72+
ChildNumber::Hardened { index: VSS_STORE_ID_HARDENED_CHILD_INDEX },
73+
],
74+
)
75+
.map_err(|e| VssError::ConnectionError {
76+
error_details: format!("Failed to derive VSS store ID key: {}", e),
77+
})?;
78+
79+
let store_id_key = vss_store_id_xprv.private_key.secret_bytes();
80+
let hash = sha256::Hash::hash(&store_id_key);
81+
let hash_hex = hash.to_string();
82+
83+
let store_id_suffix = &hash_hex[..VSS_STORE_ID_HASH_LENGTH];
84+
let store_id = format!("{}_{}", prefix, store_id_suffix);
85+
86+
Ok(store_id)
87+
}
2788

2889
type CustomRetryPolicy = FilteredRetryPolicy<
2990
JitteredRetryPolicy<

src/lib.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod tests;
66
mod types;
77

88
pub use errors::*;
9-
pub use implementation::VssClient;
9+
pub use implementation::{VssClient, derive_vss_store_id};
1010
pub use types::*;
1111

1212
uniffi::setup_scaffolding!();
@@ -338,6 +338,38 @@ pub async fn vss_delete(
338338
})
339339
}
340340

341+
/// Derives a deterministic VSS store ID from a mnemonic and optional passphrase.
342+
///
343+
/// This function creates a consistent store ID that can be used across devices for the same wallet.
344+
/// The store ID is derived using BIP32 key derivation at a specific VSS path, ensuring it's
345+
/// cryptographically secure and deterministic.
346+
///
347+
/// # Parameters
348+
/// - `prefix`: A prefix to include in the store ID (e.g., "bitkit_v1_regtest")
349+
/// - `mnemonic`: BIP39 mnemonic phrase (12 or 24 words)
350+
/// - `passphrase`: Optional BIP39 passphrase
351+
///
352+
/// # Returns
353+
/// A deterministic store ID string that combines the prefix with a derived identifier.
354+
///
355+
/// # Example
356+
/// ```
357+
/// let store_id = vss_derive_store_id(
358+
/// "bitkit_v1_regtest".to_string(),
359+
/// "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
360+
/// None
361+
/// )?;
362+
/// println!("Store ID: {}", store_id);
363+
/// ```
364+
#[uniffi::export]
365+
pub fn vss_derive_store_id(
366+
prefix: String,
367+
mnemonic: String,
368+
passphrase: Option<String>,
369+
) -> Result<String, VssError> {
370+
derive_vss_store_id(prefix, mnemonic, passphrase)
371+
}
372+
341373
/// Shuts down the VSS client and clears the global client state.
342374
///
343375
/// This function is optional but recommended for clean shutdown in applications

src/tests.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,27 @@ mod tests {
7777
assert!(format!("{}", get_err).contains("Test get error"));
7878
}
7979

80+
#[test]
81+
fn test_vss_derive_store_id() {
82+
use crate::vss_derive_store_id;
83+
84+
let prefix = "test".to_string();
85+
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
86+
87+
// Test deterministic output
88+
let store_id1 = vss_derive_store_id(prefix.clone(), mnemonic.clone(), None).unwrap();
89+
let store_id2 = vss_derive_store_id(prefix.clone(), mnemonic.clone(), None).unwrap();
90+
assert_eq!(store_id1, store_id2);
91+
assert!(store_id1.starts_with("test_"));
92+
93+
// Test passphrase handling
94+
let with_passphrase = vss_derive_store_id(prefix.clone(), mnemonic.clone(), Some("pass".to_string())).unwrap();
95+
assert_ne!(store_id1, with_passphrase);
96+
97+
// Test invalid mnemonic
98+
assert!(vss_derive_store_id(prefix, "invalid".to_string(), None).is_err());
99+
}
100+
80101
#[test]
81102
fn test_types_creation() {
82103
use crate::{VssItem, KeyValue, KeyVersion};

0 commit comments

Comments
 (0)