Skip to main content

breez_sdk_spark/signer/
rtsync.rs

1use anyhow::anyhow;
2use bitcoin::bip32::DerivationPath;
3use breez_sdk_common::sync::SyncSigner;
4use std::sync::Arc;
5
6use crate::{
7    Network,
8    signer::{BreezSigner, EciesSigner},
9};
10
11const SIGNING_DERIVATION_PATH: &str = "m/1220588449'/0'/0'/0/0";
12const SIGNING_DERIVATION_PATH_TEST: &str = "m/1220588449'/1'/0'/0/0";
13const SIGNING_DERIVATION_PATH_SIGNET: &str = "m/1220588449'/1'/1'/0/0";
14const ENCRYPTION_DERIVATION_PATH: &str = "m/1782705014'/0'/0'/0/0";
15const ENCRYPTION_DERIVATION_PATH_TEST: &str = "m/1782705014'/1'/0'/0/0";
16const ENCRYPTION_DERIVATION_PATH_SIGNET: &str = "m/1782705014'/1'/1'/0/0";
17
18pub struct RTSyncSigner {
19    signer: Arc<dyn BreezSigner>,
20    ecies: Arc<dyn EciesSigner>,
21    signing_path: DerivationPath,
22    encryption_path: DerivationPath,
23}
24
25impl RTSyncSigner {
26    pub fn new(
27        signer: Arc<dyn BreezSigner>,
28        ecies: Arc<dyn EciesSigner>,
29        network: Network,
30    ) -> Result<Self, bitcoin::bip32::Error> {
31        let signing_path: DerivationPath = match network {
32            Network::Mainnet => SIGNING_DERIVATION_PATH,
33            Network::Regtest => SIGNING_DERIVATION_PATH_TEST,
34            Network::Signet => SIGNING_DERIVATION_PATH_SIGNET,
35        }
36        .parse()?;
37        let encryption_path: DerivationPath = match network {
38            Network::Mainnet => ENCRYPTION_DERIVATION_PATH,
39            Network::Regtest => ENCRYPTION_DERIVATION_PATH_TEST,
40            Network::Signet => ENCRYPTION_DERIVATION_PATH_SIGNET,
41        }
42        .parse()?;
43
44        Ok(Self {
45            signer,
46            ecies,
47            signing_path,
48            encryption_path,
49        })
50    }
51}
52
53#[macros::async_trait]
54impl SyncSigner for RTSyncSigner {
55    async fn sign_ecdsa_recoverable(&self, data: &[u8]) -> anyhow::Result<Vec<u8>> {
56        use bitcoin::hashes::{Hash, sha256};
57        use bitcoin::secp256k1::Message;
58
59        // Real-time sync requires double SHA256 hash
60        let hash = sha256::Hash::hash(sha256::Hash::hash(data).as_ref());
61        let message = Message::from_digest(hash.to_byte_array());
62        let sig = self
63            .signer
64            .sign_ecdsa_recoverable(message, &self.signing_path)
65            .await
66            .map_err(|e| anyhow!(e.to_string()))?;
67
68        // Serialize the recoverable signature: recovery_id + 64 bytes
69        let (recovery_id, sig_bytes) = sig.serialize_compact();
70        let mut complete_signature = vec![31u8.saturating_add(
71            u8::try_from(recovery_id.to_i32()).map_err(|e| anyhow!(e.to_string()))?,
72        )];
73        complete_signature.extend_from_slice(&sig_bytes);
74        Ok(complete_signature)
75    }
76
77    async fn encrypt_ecies(&self, msg: Vec<u8>) -> anyhow::Result<Vec<u8>> {
78        self.ecies
79            .encrypt_ecies(&msg, &self.encryption_path)
80            .await
81            .map_err(|e| anyhow!(e.to_string()))
82    }
83
84    async fn decrypt_ecies(&self, msg: Vec<u8>) -> anyhow::Result<Vec<u8>> {
85        self.ecies
86            .decrypt_ecies(&msg, &self.encryption_path)
87            .await
88            .map_err(|e| anyhow!(e.to_string()))
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::signer::breez::BreezSignerImpl;
96    use bitcoin::bip32::Xpriv;
97
98    #[test]
99    fn sync_derivation_paths() {
100        let master = Xpriv::new_master(bitcoin::Network::Signet, &[7; 32]).unwrap();
101        let signer = Arc::new(BreezSignerImpl::new(master));
102        for (network, signing, encryption) in [
103            (
104                Network::Mainnet,
105                "m/1220588449'/0'/0'/0/0",
106                "m/1782705014'/0'/0'/0/0",
107            ),
108            (
109                Network::Regtest,
110                "m/1220588449'/1'/0'/0/0",
111                "m/1782705014'/1'/0'/0/0",
112            ),
113            (
114                Network::Signet,
115                "m/1220588449'/1'/1'/0/0",
116                "m/1782705014'/1'/1'/0/0",
117            ),
118        ] {
119            let sync = RTSyncSigner::new(signer.clone(), signer.clone(), network).unwrap();
120            assert_eq!(sync.signing_path, signing.parse().unwrap());
121            assert_eq!(sync.encryption_path, encryption.parse().unwrap());
122        }
123    }
124
125    #[macros::async_test_all]
126    async fn signet_sync_is_isolated_from_regtest_with_the_same_signer() {
127        let master = Xpriv::new_master(bitcoin::Network::Signet, &[7; 32]).unwrap();
128        let shared_signer = Arc::new(BreezSignerImpl::new(master));
129        let signet = RTSyncSigner::new(
130            shared_signer.clone(),
131            shared_signer.clone(),
132            Network::Signet,
133        )
134        .unwrap();
135        let regtest =
136            RTSyncSigner::new(shared_signer.clone(), shared_signer, Network::Regtest).unwrap();
137        let message = b"sync data".to_vec();
138        assert_ne!(
139            signet.sign_ecdsa_recoverable(&message).await.unwrap(),
140            regtest.sign_ecdsa_recoverable(&message).await.unwrap()
141        );
142        for (source, other) in [(&signet, &regtest), (&regtest, &signet)] {
143            let encrypted = source.encrypt_ecies(message.clone()).await.unwrap();
144            assert_eq!(
145                source.decrypt_ecies(encrypted.clone()).await.unwrap(),
146                message
147            );
148            assert!(other.decrypt_ecies(encrypted).await.is_err());
149        }
150    }
151}