sodot_mpc/schemes/
bip340.rs

1//! Threshold BIP-340 (Schnorr) cryptographic operations and key management.
2//!
3//! This module provides types and APIs for distributed key generation, signing, key derivation,
4//! secret share refresh, export/import, and threshold resharing for BIP-340 (Schnorr signatures).
5
6use crate::{
7    ExtendedPrivateKey, ExtendedPublicKey, KeygenId, KeygenPrivateKey, PublicKey, RoomUUID, Scheme, SecretShare, SodotError,
8    ffi::{self, AsRawFFI, ConstSlice, ConstStr, FromFFI, HeapString, Room, ToFFI, async_ffi_double, async_ffi_single},
9};
10use std::num::NonZeroU16;
11
12/// A secret share for BIP-340 (Schnorr) cryptographic operations.
13pub type Bip340SecretShare = SecretShare<Bip340>;
14
15/// A public key for BIP-340 (Schnorr) cryptographic operations.
16pub type Bip340PublicKey = PublicKey<Bip340, 32>;
17
18/// A 32-byte tweak value for BIP-340 operations.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Bip340Tweak(pub(crate) [u8; 32]);
21
22impl Bip340Tweak {
23    /// Returns the tweak as a byte array reference.
24    pub fn as_bytes(&self) -> &[u8; 32] {
25        &self.0
26    }
27}
28
29impl From<[u8; 32]> for Bip340Tweak {
30    fn from(bytes: [u8; 32]) -> Self {
31        Bip340Tweak(bytes)
32    }
33}
34
35/// An extended public key for BIP-340 operations.
36pub type Bip340ExtendedPublicKey = ExtendedPublicKey<Bip340>;
37
38/// An extended private key for BIP-340 operations.
39pub type Bip340ExtendedPrivateKey = ExtendedPrivateKey<Bip340>;
40
41impl Bip340PublicKey {
42    /// Returns the public key as a byte array.
43    pub fn as_bytes(&self) -> &[u8; 32] {
44        &self.0
45    }
46}
47
48impl From<[u8; 32]> for Bip340PublicKey {
49    fn from(k: [u8; 32]) -> Self {
50        Self(k, std::marker::PhantomData)
51    }
52}
53
54/// A threshold BIP-340 (Schnorr signature) implementation.
55///
56/// BIP-340 provides Schnorr signatures for Bitcoin, offering advantages over ECDSA including
57/// simplified verification logic, better security properties, and support for key aggregation.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct Bip340 {
60    host_url: String,
61}
62
63impl Scheme for Bip340 {
64    fn new(host_url: String) -> Self {
65        Self { host_url }
66    }
67
68    fn host_url(&self) -> &str {
69        &self.host_url
70    }
71}
72
73impl Bip340 {
74    /// Creates a new BIP340 instance with the specified host URL.
75    pub fn new(host_url: String) -> Self {
76        <Self as Scheme>::new(host_url)
77    }
78
79    /// Creates a new BIP340 instance using the default production host URL.
80    pub fn with_default_host() -> Self {
81        <Self as Scheme>::with_default_host()
82    }
83
84    /// Initializes the key generation process by creating a keygen ID and private key.
85    pub fn init_keygen(&self) -> Result<(KeygenId, KeygenPrivateKey), SodotError> {
86        <Self as Scheme>::init_keygen(self)
87    }
88
89    /// Creates a room for coordinating multi-party computation protocols.
90    pub async fn create_room(&self, num_parties: NonZeroU16, api_key: &str) -> Result<RoomUUID, SodotError> {
91        <Self as Scheme>::create_room(self, num_parties, api_key).await
92    }
93
94    /// Generates a threshold BIP-340 key using distributed key generation.
95    ///
96    /// Multiple parties collaborate to create a shared public key and individual secret shares.
97    /// The resulting public key can be used for verification, while the secret shares are
98    /// required for threshold signing.
99    pub async fn keygen(
100        &self,
101        room_uuid: &RoomUUID,
102        num_parties: NonZeroU16,
103        threshold: NonZeroU16,
104        keygen_init_keypair: &KeygenPrivateKey,
105        keygen_ids: &[KeygenId],
106    ) -> Result<(Bip340PublicKey, Bip340SecretShare), SodotError> {
107        let room = Room::new(&self.host_url, room_uuid);
108        let keygen_init_keypair = keygen_init_keypair.ffi();
109
110        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
111        let keygen_slice = ConstSlice::new(&keygen_ids);
112
113        async_ffi_double(|cb| unsafe {
114            ffi::sodot_bip340_keygen(
115                room.raw_ffi(),
116                num_parties.get(),
117                threshold.get(),
118                keygen_init_keypair.raw_ffi(),
119                keygen_slice.raw_ffi(),
120                cb.callback(),
121                cb.extra(),
122            )
123        })
124        .await
125    }
126
127    /// Signs a message using the threshold BIP-340 scheme with optional Taproot tweaking.
128    ///
129    /// This method performs threshold signing where a subset of parties (at least the threshold)
130    /// collaborate to generate a valid BIP-340 Schnorr signature for a given message.
131    pub async fn sign(
132        &self,
133        room_uuid: &RoomUUID,
134        secret_share: &Bip340SecretShare,
135        message: &[u8],
136        derivation_path: &[u32],
137        tweak: Option<&Bip340Tweak>,
138    ) -> Result<[u8; 64], SodotError> {
139        let room = Room::new(&self.host_url, room_uuid);
140        let secret_share_str = secret_share.ffi();
141        let message_str = message.ffi();
142        let derivation_slice = ConstSlice::new(derivation_path);
143
144        let tweak = tweak.ffi();
145
146        async_ffi_single(|cb| unsafe {
147            ffi::sodot_bip340_sign(
148                room.raw_ffi(),
149                secret_share_str.raw_ffi(),
150                message_str.raw_ffi(),
151                derivation_slice.raw_ffi(),
152                tweak.raw_ffi(),
153                cb.callback(),
154                cb.extra(),
155            )
156        })
157        .await
158    }
159
160    /// Derives a BIP-340 public key with optional Taproot tweaking from a secret share.
161    pub fn derive_tweak_pubkey(
162        &self,
163        secret_share: &Bip340SecretShare,
164        derivation_path: &[u32],
165        tweak: Option<&Bip340Tweak>,
166    ) -> Result<Bip340PublicKey, SodotError> {
167        let secret_share_str = secret_share.ffi();
168        let derivation_slice = ConstSlice::new(derivation_path);
169
170        let tweak = tweak.ffi();
171
172        let mut pubkey_out = HeapString::NULL;
173        let result = unsafe {
174            ffi::sodot_bip340_derive_tweak_pubkey(
175                secret_share_str.raw_ffi(),
176                derivation_slice.raw_ffi(),
177                tweak.raw_ffi(),
178                pubkey_out.ffi_mut(),
179            )
180        };
181        result.ok_and(|| FromFFI::parse_ffi(pubkey_out.as_str()))
182    }
183
184    /// Returns a base58-encoded extended public key (Xpub) derived from a secret share.
185    pub fn get_xpub(&self, secret_share: &Bip340SecretShare) -> Result<Bip340ExtendedPublicKey, SodotError> {
186        let secret_share_str = secret_share.ffi();
187
188        let mut xpub_out = HeapString::NULL;
189        let result = unsafe { ffi::sodot_bip340_get_xpub(secret_share_str.raw_ffi(), xpub_out.ffi_mut()) };
190        result.ok_and(|| FromFFI::parse_ffi(xpub_out.as_str()))
191    }
192
193    /// Derives a BIP-340 public key from an extended public key (Xpub) with optional Taproot tweaking.
194    pub fn derive_pubkey_from_xpub(
195        &self,
196        xpub: &Bip340ExtendedPublicKey,
197        derivation_path: &[u32],
198        tweak: Option<&Bip340Tweak>,
199    ) -> Result<Bip340PublicKey, SodotError> {
200        let xpub = xpub.ffi();
201        let derivation_slice = ConstSlice::new(derivation_path);
202
203        let tweak = tweak.ffi();
204
205        let mut pubkey_out = HeapString::NULL;
206        let result = unsafe {
207            ffi::sodot_bip340_derive_pubkey_from_xpub(
208                xpub.raw_ffi(),
209                derivation_slice.raw_ffi(),
210                tweak.raw_ffi(),
211                pubkey_out.ffi_mut(),
212            )
213        };
214        result.ok_and(|| FromFFI::parse_ffi(pubkey_out.as_str()))
215    }
216
217    /// Derives a private key from an extended private key (xpriv) using a BIP-32 derivation path.
218    pub fn derive_private_key_from_xpriv(
219        &self,
220        xpriv: &Bip340ExtendedPrivateKey,
221        derivation_path: &[u32],
222    ) -> Result<[u8; 32], SodotError> {
223        let xpriv = xpriv.ffi();
224        let derivation_slice = ConstSlice::new(derivation_path);
225
226        let mut privkey_out = HeapString::NULL;
227        let result = unsafe {
228            ffi::sodot_bip340_derive_private_key_from_xpriv(xpriv.raw_ffi(), derivation_slice.raw_ffi(), privkey_out.ffi_mut())
229        };
230        result.ok_and(|| FromFFI::parse_ffi(privkey_out.as_str()))
231    }
232
233    /// Refreshes the secret material of all parties without changing the public key.
234    pub async fn refresh(
235        &self,
236        room_uuid: &RoomUUID,
237        secret_share: &Bip340SecretShare,
238    ) -> Result<(Bip340PublicKey, Bip340SecretShare), SodotError> {
239        let room = Room::new(&self.host_url, room_uuid);
240        let secret_share_str = secret_share.ffi();
241
242        async_ffi_double(|cb| unsafe {
243            ffi::sodot_bip340_refresh(room.raw_ffi(), secret_share_str.raw_ffi(), cb.callback(), cb.extra())
244        })
245        .await
246    }
247
248    /// Extracts the KeygenID from a secret share.
249    pub fn export_id(&self, secret_share: &Bip340SecretShare) -> Result<KeygenId, SodotError> {
250        let secret_share_str = secret_share.ffi();
251
252        let mut id_out = HeapString::NULL;
253        let result = unsafe { ffi::sodot_bip340_get_export_id(secret_share_str.raw_ffi(), id_out.ffi_mut()) };
254        result.ok_and(|| KeygenId::new(id_out.as_str().to_string()))
255    }
256
257    /// Combines threshold secret shares and exports the full private key to a single party.
258    ///
259    /// Requires a threshold number of parties to participate. The `export_to` parameter specifies
260    /// the KeygenID of the party that should receive the private key. The designated party will
261    /// receive the full xpriv, while others receive None.
262    pub async fn export_full_private_key(
263        &self,
264        room_uuid: &RoomUUID,
265        secret_share: &Bip340SecretShare,
266        export_to: &KeygenId,
267    ) -> Result<Option<Bip340ExtendedPrivateKey>, SodotError> {
268        let room = Room::new(&self.host_url, room_uuid);
269        let secret_share = ConstStr::from_str(secret_share.as_str());
270        let export_to = ConstStr::from_str(export_to.as_str());
271        async_ffi_single(|cb| unsafe {
272            ffi::sodot_bip340_export_full_private_key(
273                room.raw_ffi(),
274                secret_share.raw_ffi(),
275                export_to.raw_ffi(),
276                cb.callback(),
277                cb.extra(),
278            )
279        })
280        .await
281    }
282
283    /// Locally reconstructs the full private key from a threshold number of secret shares.
284    pub fn offline_export_full_private_key(
285        &self,
286        secret_shares: &[Bip340SecretShare],
287    ) -> Result<Bip340ExtendedPrivateKey, SodotError> {
288        let secret_share_strs: Vec<_> = secret_shares.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
289        let secret_shares_slice = ConstSlice::new(&secret_share_strs);
290
291        let mut privkey_out = HeapString::NULL;
292        let result =
293            unsafe { ffi::sodot_bip340_offline_export_full_private_key(secret_shares_slice.raw_ffi(), privkey_out.ffi_mut()) };
294        result.ok_and(|| FromFFI::parse_ffi(privkey_out.as_str()))
295    }
296
297    /// Participates as a recipient in a private key import operation.
298    pub async fn import_private_key_recipient(
299        &self,
300        room_uuid: &RoomUUID,
301        threshold: NonZeroU16,
302        keygen_init_keypair: &KeygenPrivateKey,
303        keygen_ids: &[KeygenId],
304    ) -> Result<(Bip340PublicKey, Bip340SecretShare), SodotError> {
305        let room = Room::new(&self.host_url, room_uuid);
306        let keygen_init_keypair = keygen_init_keypair.ffi();
307
308        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
309        let keygen_slice = ConstSlice::new(&keygen_ids);
310
311        async_ffi_double(|cb| unsafe {
312            ffi::sodot_bip340_import_private_key_recipient(
313                room.raw_ffi(),
314                threshold.get(),
315                keygen_init_keypair.raw_ffi(),
316                keygen_slice.raw_ffi(),
317                cb.callback(),
318                cb.extra(),
319            )
320        })
321        .await
322    }
323
324    /// Participates as the importer in a private key import operation.
325    pub async fn import_private_key_importer(
326        &self,
327        room_uuid: &RoomUUID,
328        threshold: NonZeroU16,
329        private_key: &[u8; 32],
330        keygen_init_keypair: &KeygenPrivateKey,
331        keygen_ids: &[KeygenId],
332    ) -> Result<(Bip340PublicKey, Bip340SecretShare), SodotError> {
333        let room = Room::new(&self.host_url, room_uuid);
334        let private_key_ffi = private_key.ffi();
335        let keygen_init_keypair = keygen_init_keypair.ffi();
336
337        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
338        let keygen_slice = ConstSlice::new(&keygen_ids);
339
340        async_ffi_double(|cb| unsafe {
341            ffi::sodot_bip340_import_private_key_importer(
342                room.raw_ffi(),
343                threshold.get(),
344                private_key_ffi.raw_ffi(),
345                keygen_init_keypair.raw_ffi(),
346                keygen_slice.raw_ffi(),
347                ConstStr::NULL.raw_ffi(),
348                cb.callback(),
349                cb.extra(),
350            )
351        })
352        .await
353    }
354
355    /// Participates as a new party in a resharing operation.
356    pub async fn reshare_new_party(
357        &self,
358        room_uuid: &RoomUUID,
359        new_threshold: NonZeroU16,
360        keygen_init_keypair: &KeygenPrivateKey,
361        keygen_ids: &[KeygenId],
362    ) -> Result<(Bip340PublicKey, Bip340SecretShare), SodotError> {
363        let room = Room::new(&self.host_url, room_uuid);
364        let keygen_init_keypair = keygen_init_keypair.ffi();
365
366        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
367        let keygen_slice = ConstSlice::new(&keygen_ids);
368
369        async_ffi_double(|cb| unsafe {
370            ffi::sodot_bip340_reshare_new_party(
371                room.raw_ffi(),
372                new_threshold.get(),
373                keygen_init_keypair.raw_ffi(),
374                keygen_slice.raw_ffi(),
375                cb.callback(),
376                cb.extra(),
377            )
378        })
379        .await
380    }
381
382    /// Participates as a remaining party in a resharing operation.
383    pub async fn reshare_remaining_party(
384        &self,
385        room_uuid: &RoomUUID,
386        new_threshold: NonZeroU16,
387        secret_share: &Bip340SecretShare,
388        keygen_ids: &[KeygenId],
389    ) -> Result<(Bip340PublicKey, Bip340SecretShare), SodotError> {
390        let room = Room::new(&self.host_url, room_uuid);
391        let secret_share_str = secret_share.ffi();
392
393        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
394        let keygen_slice = ConstSlice::new(&keygen_ids);
395
396        async_ffi_double(|cb| unsafe {
397            ffi::sodot_bip340_reshare_remaining_party(
398                room.raw_ffi(),
399                new_threshold.get(),
400                secret_share_str.raw_ffi(),
401                keygen_slice.raw_ffi(),
402                cb.callback(),
403                cb.extra(),
404            )
405        })
406        .await
407    }
408}