Repository: breez/breez-sdk-greenlight (language: python)

Issue:
wrap breez services with replaceable variant
If an app hibernates, the breez sdk may still run. However, any connections to the outside world, like grpc connections will (may) stop functioning. This means after hibernation the sdk needs to reconnect. Hibernation is detected by awaiting a 'sleep' in a loop. If the sleep has taken a long time, that means the app has been in hibernation. Because many services have references to the node api, and other services than the greenlight client may have been affected by hibernation, the entire breezservices instance is recreated, reconnected. In order to allow this, an internal variant of breezservices was made. This internal instance can be replaced at any time.

Because the breezservices code was moved to another file, with edits, I took the liberty of putting all functions inside breezservices in alphabetical order.

Related: https://github.com/Blockstream/greenlight/issues/521

Tested with c-breez build https://github.com/breez/c-breez/actions/runs/11519175590
I see the app hibernating and succesfully reconnecting.

TODO: The SDK is in an unrecoverable state right now if the reconnect fails. 

Review remarks:
- breez_services was basically moved to internal_breez_services, except the static functions.
- breez_services now calls internal_breez_services for every function
- breez_services has an additional function: `detect_hibernation` that does the disconnecting/reconnecting


Discussion / hints:

Fix patch:
diff --git a/libs/sdk-bindings/src/uniffi_binding.rs b/libs/sdk-bindings/src/uniffi_binding.rs
index ec446fd29..1de3ff14d 100644
--- a/libs/sdk-bindings/src/uniffi_binding.rs
+++ b/libs/sdk-bindings/src/uniffi_binding.rs
@@ -152,7 +152,7 @@ impl BlockingBreezServices {
     }
 
     pub fn node_credentials(&self) -> SdkResult<Option<NodeCredentials>> {
-        self.breez_services.node_credentials()
+        rt().block_on(self.breez_services.node_credentials())
     }
 
     pub fn node_info(&self) -> SdkResult<NodeState> {
diff --git a/libs/sdk-common/Cargo.toml b/libs/sdk-common/Cargo.toml
index 585918449..d124fe9e8 100644
--- a/libs/sdk-common/Cargo.toml
+++ b/libs/sdk-common/Cargo.toml
@@ -22,6 +22,7 @@ serde = { workspace = true }
 serde_json = { workspace = true }
 strum_macros = { workspace = true }
 thiserror = { workspace = true }
+tokio = { workspace = true }
 tonic = { workspace = true, features = [
     "tls",
     "transport",
@@ -35,7 +36,6 @@ urlencoding = { version = "2.1.3" }
 [dev-dependencies]
 bitcoin = { workspace = true, features = ["rand"] }
 mockito = { workspace = true }
-tokio = { workspace = true }
 once_cell = { workspace = true }
 
 [build-dependencies]
diff --git a/libs/sdk-common/src/breez_server.rs b/libs/sdk-common/src/breez_server.rs
index cc7ca0b4f..f1e10b6a2 100644
--- a/libs/sdk-common/src/breez_server.rs
+++ b/libs/sdk-common/src/breez_server.rs
@@ -1,5 +1,6 @@
 use anyhow::Result;
 use log::trace;
+use tokio::sync::Mutex;
 use tonic::codegen::InterceptedService;
 use tonic::metadata::errors::InvalidMetadataValue;
 use tonic::metadata::{Ascii, MetadataValue};
@@ -20,18 +21,26 @@ pub static PRODUCTION_BREEZSERVER_URL: &str = "https://bs1.breez.technology:443"
 pub static STAGING_BREEZSERVER_URL: &str = "https://bs1-st.breez.technology:443";
 
 pub struct BreezServer {
-    grpc_channel: Channel,
+    grpc_channel: Mutex<Channel>,
     api_key: Option<String>,
+    server_url: String,
 }
 
 impl BreezServer {
     pub fn new(server_url: String, api_key: Option<String>) -> Result<Self> {
         Ok(Self {
-            grpc_channel: Endpoint::from_shared(server_url)?.connect_lazy(),
+            grpc_channel: Mutex::new(Endpoint::from_shared(server_url.clone())?.connect_lazy()),
             api_key,
+            server_url,
         })
     }
 
+    pub async fn reconnect(&self) -> Result<()> {
+        *self.grpc_channel.lock().await =
+            Endpoint::from_shared(self.server_url.clone())?.connect_lazy();
+        Ok(())
+    }
+
     fn api_key_metadata(&self) -> Result<Option<MetadataValue<Ascii>>, ServiceConnectivityError> {
         match &self.api_key {
             Some(key) => Ok(Some(format!("Bearer {key}").parse().map_err(
@@ -54,22 +63,22 @@ impl BreezServer {
     > {
         let api_key_metadata = self.api_key_metadata()?;
         let with_interceptor = ChannelOpenerClient::with_interceptor(
-            self.grpc_channel.clone(),
+            self.grpc_channel.lock().await.clone(),
             ApiKeyInterceptor { api_key_metadata },
         );
         Ok(with_interceptor)
     }
 
     pub async fn get_payment_notifier_client(&self) -> PaymentNotifierClient<Channel> {
-        PaymentNotifierClient::new(self.grpc_channel.clone())
+        PaymentNotifierClient::new(self.grpc_channel.lock().await.clone())
     }
 
     pub async fn get_information_client(&self) -> InformationClient<Channel> {
-        InformationClient::new(self.grpc_channel.clone())
+        InformationClient::new(self.grpc_channel.lock().await.clone())
     }
 
     pub async fn get_signer_client(&self) -> SignerClient<Channel> {
-        SignerClient::new(self.grpc_channel.clone())
+        SignerClient::new(self.grpc_channel.lock().await.clone())
     }
 
     pub async fn get_support_client(
@@ -80,13 +89,13 @@ impl BreezServer {
     > {
         let api_key_metadata = self.api_key_metadata()?;
         Ok(SupportClient::with_interceptor(
-            self.grpc_channel.clone(),
+            self.grpc_channel.lock().await.clone(),
             ApiKeyInterceptor { api_key_metadata },
         ))
     }
 
     pub async fn get_swapper_client(&self) -> SwapperClient<Channel> {
-        SwapperClient::new(self.grpc_channel.clone())
+        SwapperClient::new(self.grpc_channel.lock().await.clone())
     }
 
     pub async fn ping(&self) -> Result<String> {
diff --git a/libs/sdk-common/src/lnurl/specs/auth.rs b/libs/sdk-common/src/lnurl/specs/auth.rs
index e9148ad18..76ab3be36 100644
--- a/libs/sdk-common/src/lnurl/specs/auth.rs
+++ b/libs/sdk-common/src/lnurl/specs/auth.rs
@@ -6,10 +6,12 @@ use reqwest::Url;
 
 use crate::prelude::*;
 
+#[tonic::async_trait]
 pub trait LnurlAuthSigner {
-    fn derive_bip32_pub_key(&self, derivation_path: &[ChildNumber]) -> LnUrlResult<Vec<u8>>;
-    fn sign_ecdsa(&self, msg: &[u8], derivation_path: &[ChildNumber]) -> LnUrlResult<Vec<u8>>;
-    fn hmac_sha256(
+    async fn derive_bip32_pub_key(&self, derivation_path: &[ChildNumber]) -> LnUrlResult<Vec<u8>>;
+    async fn sign_ecdsa(&self, msg: &[u8], derivation_path: &[ChildNumber])
+        -> LnUrlResult<Vec<u8>>;
+    async fn hmac_sha256(
         &self,
         key_derivation_path: &[ChildNumber],
         input: &[u8],
@@ -28,13 +30,15 @@ pub async fn perform_lnurl_auth<S: LnurlAuthSigner>(
     signer: &S,
 ) -> LnUrlResult<LnUrlCallbackStatus> {
     let url = Url::from_str(&req_data.url).map_err(|e| LnUrlError::InvalidUri(e.to_string()))?;
-    let derivation_path = get_derivation_path(signer, url)?;
-    let sig = signer.sign_ecdsa(
-        &hex::decode(&req_data.k1)
-            .map_err(|e| LnUrlError::Generic(format!("Error decoding k1: {e}")))?,
-        &derivation_path,
-    )?;
-    let xpub_bytes = signer.derive_bip32_pub_key(&derivation_path)?;
+    let derivation_path = get_derivation_path(signer, url).await?;
+    let sig = signer
+        .sign_ecdsa(
+            &hex::decode(&req_data.k1)
+                .map_err(|e| LnUrlError::Generic(format!("Error decoding k1: {e}")))?,
+            &derivation_path,
+        )
+        .await?;
+    let xpub_bytes = signer.derive_bip32_pub_key(&derivation_path).await?;
     let xpub = ExtendedPubKey::decode(xpub_bytes.as_slice())?;
 
     // <LNURL_hostname_and_path>?<LNURL_existing_query_parameters>&sig=<hex(sign(utf8ToBytes(k1), linkingPrivKey))>&key=<hex(linkingKey)>
@@ -93,7 +97,7 @@ pub fn validate_request(
     })
 }
 
-pub fn get_derivation_path<S: LnurlAuthSigner>(
+pub async fn get_derivation_path<S: LnurlAuthSigner>(
     signer: &S,
     url: Url,
 ) -> LnUrlResult<Vec<ChildNumber>> {
@@ -101,10 +105,12 @@ pub fn get_derivation_path<S: LnurlAuthSigner>(
         .domain()
         .ok_or(LnUrlError::invalid_uri("Could not determine domain"))?;
 
-    let hmac = signer.hmac_sha256(
-        &[ChildNumber::from_hardened_idx(138)?, ChildNumber::from(0)],
-        domain.as_bytes(),
-    )?;
+    let hmac = signer
+        .hmac_sha256(
+            &[ChildNumber::from_hardened_idx(138)?, ChildNumber::from(0)],
+            domain.as_bytes(),
+        )
+        .await?;
 
     // m/138'/<long1>/<long2>/<long3>/<long4>
     Ok(vec![
diff --git a/libs/sdk-core/src/binding.rs b/libs/sdk-core/src/binding.rs
index 6d0f4d58a..d4636cce8 100644
--- a/libs/sdk-core/src/binding.rs
+++ b/libs/sdk-core/src/binding.rs
@@ -332,12 +332,8 @@ pub fn sync() -> Result<()> {
 
 /// See [BreezServices::node_credentials]
 pub fn node_credentials() -> Result<Option<NodeCredentials>> {
-    block_on(async {
-        get_breez_services()
-            .await?
-            .node_credentials()
-            .map_err(anyhow::Error::new::<SdkError>)
-    })
+    block_on(async { get_breez_services().await?.node_credentials().await })
+        .map_err(anyhow::Error::new::<SdkError>)
 }
 
 /// See [BreezServices::node_info]
diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs
index 9ec9c368b..24bf35988 100644
--- a/libs/sdk-core/src/breez_services.rs
+++ b/libs/sdk-core/src/breez_services.rs
@@ -47,6 +47,8 @@ use crate::swap_out::boltzswap::BoltzApi;
 use crate::swap_out::reverseswap::{BTCSendSwap, CreateReverseSwapArg};
 use crate::*;
 
+const DETECT_HIBERNATE_SLEEP_DURATION: Duration = Duration::from_secs(1);
+const DETECT_HIBERNATE_MAX_OFFSET: Duration = Duration::from_secs(2);
 pub type BreezServicesResult<T, E = ConnectError> = Result<T, E>;
 
 /// Trait that can be used to react to various [BreezEvent]s emitted by the SDK.
@@ -167,6 +169,8 @@ pub struct BreezServices {
     backup_watcher: Arc<BackupWatcher>,
     shutdown_sender: watch::Sender<()>,
     shutdown_receiver: watch::Receiver<()>,
+    hibernation_sender: watch::Sender<()>,
+    hibernation_receiver: watch::Receiver<()>,
 }
 
 impl BreezServices {
@@ -620,8 +624,8 @@ impl BreezServices {
     }
 
     /// Retrieve the decrypted credentials from the node.
-    pub fn node_credentials(&self) -> SdkResult<Option<NodeCredentials>> {
-        Ok(self.node_api.node_credentials()?)
+    pub async fn node_credentials(&self) -> SdkResult<Option<NodeCredentials>> {
+        Ok(self.node_api.node_credentials().await?)
     }
 
     /// Retrieve the node state from the persistent storage.
@@ -1458,8 +1462,11 @@ impl BreezServices {
     ///
     /// Internal method. Should only be used as part of [BreezServices::start]
     async fn start_background_tasks(self: &Arc<BreezServices>) -> SdkResult<()> {
+        // Detect hibernation
+        self.detect_hibernation();
+
         // start the signer
-        let (shutdown_signer_sender, signer_signer_receiver) = mpsc::channel(1);
+        let (shutdown_signer_sender, signer_signer_receiver) = watch::channel(());
         self.start_signer(signer_signer_receiver).await;
         self.start_node_keep_alive(self.shutdown_receiver.clone())
             .await;
@@ -1499,10 +1506,9 @@ impl BreezServices {
         // Stop signer on shutdown
         let mut shutdown_receiver = self.shutdown_receiver.clone();
         tokio::spawn(async move {
-            // start the backup watcher
             _ = shutdown_receiver.changed().await;
-            _ = shutdown_signer_sender.send(()).await;
-            debug!("Received the signal to exit event polling loop");
+            _ = shutdown_signer_sender.send(());
+            debug!("Received the signal to exit signer");
         });
 
         self.init_chainservice_urls().await?;
@@ -1510,11 +1516,72 @@ impl BreezServices {
         Ok(())
     }
 
-    async fn start_signer(self: &Arc<BreezServices>, shutdown_receiver: mpsc::Receiver<()>) {
-        let signer_api = self.clone();
+    fn detect_hibernation(self: &Arc<BreezServices>) {
+        let cloned = Arc::clone(self);
         tokio::spawn(async move {
-            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
-            signer_api.node_api.start_signer(shutdown_receiver).await;
+            loop {
+                let now = SystemTime::now();
+                tokio::time::sleep(DETECT_HIBERNATE_SLEEP_DURATION).await;
+                let elapsed = match now.elapsed() {
+                    Ok(elapsed) => elapsed,
+                    Err(e) => {
+                        error!("track_hibernation failed with: {:?}", e);
+                        continue;
+                    }
+                };
+
+                if elapsed
+                    .saturating_sub(DETECT_HIBERNATE_SLEEP_DURATION)
+                    .ge(&DETECT_HIBERNATE_MAX_OFFSET)
+                {
+                    // Reconnect node api before notifying anything else, to
+                    // ensure there are no races reconnecting dependant
+                    // services.
+                    debug!(
+                        "Hibernation detected, time diff {}s, reconnecting node api.",
+                        elapsed.as_secs_f32()
+                    );
+                    cloned.node_api.reconnect().await;
+
+                    // Now notify dependant services.
+                    debug!("Hibernation detected, notifying services.");
+                    let _ = cloned.hibernation_sender.send(());
+                }
+            }
+        });
+    }
+
+    async fn start_signer(self: &Arc<BreezServices>, mut shutdown_receiver: watch::Receiver<()>) {
+        let mut hibernation_receiver = self.hibernation_receiver.clone();
+        let node_api = self.node_api.clone();
+
+        tokio::spawn(async move {
+            loop {
+                let (tx, rx) = mpsc::channel(1);
+                let is_shutdown = tokio::select! {
+                    _ = node_api.start_signer(rx) => {
+                        tokio::time::sleep(Duration::from_secs(1)).await;
+                        false
+                    }
+
+                    _ = shutdown_receiver.changed() => {
+                        true
+                    }
+
+                    _ = hibernation_receiver.changed() => {
+                        // NOTE: The node api is reconnected already inside the
+                        //       detect_hibernation function, to avoid races.
+                        false
+                    }
+                };
+
+                debug!("shutting down signer");
+                _ = tx.send(());
+
+                if is_shutdown {
+                    return;
+                }
+            }
         });
     }
 
@@ -1606,6 +1673,7 @@ impl BreezServices {
         let cloned = self.clone();
         tokio::spawn(async move {
             let mut shutdown_receiver = cloned.shutdown_receiver.clone();
+            let mut reconnect_receiver = cloned.hibernation_receiver.clone();
             loop {
                 if shutdown_receiver.has_changed().unwrap_or(true) {
                     return;
@@ -1659,6 +1727,11 @@ impl BreezServices {
                               debug!("Invoice tracking task has completed");
                               return;
                              }
+
+                             _ = reconnect_receiver.changed() => {
+                                debug!("Reconnect hibernation: track invoices");
+                                break;
+                             }
                         }
                     }
                 }
@@ -1671,6 +1744,7 @@ impl BreezServices {
         let cloned = self.clone();
         tokio::spawn(async move {
             let mut shutdown_receiver = cloned.shutdown_receiver.clone();
+            let mut reconnect_receiver = cloned.hibernation_receiver.clone();
             loop {
                 if shutdown_receiver.has_changed().unwrap_or(true) {
                     return;
@@ -1699,6 +1773,11 @@ impl BreezServices {
                           debug!("Track logs task has completed");
                           return;
                          }
+
+                         _ = reconnect_receiver.changed() => {
+                            debug!("Reconnect hibernation: track logs");
+                            break;
+                         }
                         }
                     }
                 }
@@ -2315,6 +2394,7 @@ impl BreezServicesBuilder {
             });
         }
 
+        let (hibernation_sender, hibernation_receiver) = watch::channel(());
         // The storage is implemented via sqlite.
         let persister = self
             .persister
@@ -2349,17 +2429,21 @@ impl BreezServicesBuilder {
         let unwrapped_backup_transport = backup_transport.unwrap();
 
         // create the backup encryption key and then the backup watcher
-        let backup_encryption_key = unwrapped_node_api.derive_bip32_key(vec![
-            ChildNumber::from_hardened_idx(139)?,
-            ChildNumber::from(0),
-        ])?;
+        let backup_encryption_key = unwrapped_node_api
+            .derive_bip32_key(vec![
+                ChildNumber::from_hardened_idx(139)?,
+                ChildNumber::from(0),
+            ])
+            .await?;
 
         // We calculate the legacy key as a fallback for the case where the backup is still
         // encrypted with the old key.
-        let legacy_backup_encryption_key = unwrapped_node_api.legacy_derive_bip32_key(vec![
-            ChildNumber::from_hardened_idx(139)?,
-            ChildNumber::from(0),
-        ])?;
+        let legacy_backup_encryption_key = unwrapped_node_api
+            .legacy_derive_bip32_key(vec![
+                ChildNumber::from_hardened_idx(139)?,
+                ChildNumber::from(0),
+            ])
+            .await?;
         let backup_watcher = BackupWatcher::new(
             self.config.clone(),
             unwrapped_backup_transport.clone(),
@@ -2384,6 +2468,20 @@ impl BreezServicesBuilder {
             }
         });
 
+        // Reconnect breez server on hibernation.
+        let cloned_breez_server = breez_server.clone();
+        let mut cloned_hibernation_receiver = hibernation_receiver.clone();
+        tokio::spawn(async move {
+            loop {
+                if cloned_hibernation_receiver.changed().await.is_err() {
+                    return;
+                }
+
+                debug!("Reconnect hibernation: reconnecting breez server");
+                let _ = cloned_breez_server.reconnect().await;
+            }
+        });
+
         let current_lsp_id = persister.get_lsp_id()?;
         if current_lsp_id.is_none() && self.config.default_lsp_id.is_some() {
             persister.set_lsp(self.config.default_lsp_id.clone().unwrap(), None)?;
@@ -2474,6 +2572,8 @@ impl BreezServicesBuilder {
             backup_watcher: Arc::new(backup_watcher),
             shutdown_sender,
             shutdown_receiver,
+            hibernation_sender,
+            hibernation_receiver,
         });
 
         Ok(breez_services)
@@ -2666,7 +2766,7 @@ impl PaymentReceiver {
             let modified =
                 add_routing_hints(invoice, true, &vec![lsp_hint], parsed_invoice.amount_msat)?;
 
-            let invoice = self.node_api.sign_invoice(modified)?;
+            let invoice = self.node_api.sign_invoice(modified).await?;
             info!("Signed invoice with hint = {}", invoice);
             return Ok(invoice);
         }
@@ -2674,7 +2774,7 @@ impl PaymentReceiver {
         if parsed_invoice.routing_hints.is_empty() {
             info!("Adding custom hints: {:?}", hints);
             let modified = add_routing_hints(invoice, false, &hints, parsed_invoice.amount_msat)?;
-            let invoice = self.node_api.sign_invoice(modified)?;
+            let invoice = self.node_api.sign_invoice(modified).await?;
             info!("Signed invoice with hints = {}", invoice);
             return Ok(invoice);
         }
@@ -2707,7 +2807,7 @@ impl PaymentReceiver {
             &vec![open_channel_hint],
             Some(params.payer_amount_msat),
         )?;
-        let signed_invoice = self.node_api.sign_invoice(invoice_with_hint)?;
+        let signed_invoice = self.node_api.sign_invoice(invoice_with_hint).await?;
 
         info!("Registering payment with LSP");
         let api_key = self.config.api_key.clone().unwrap_or_default();
diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs
index 5900b84c6..658fb6d1d 100644
--- a/libs/sdk-core/src/greenlight/node_api.rs
+++ b/libs/sdk-core/src/greenlight/node_api.rs
@@ -65,8 +65,9 @@ const PAYMENT_STATE_FAILED: u8 = 4;
 
 pub(crate) struct Greenlight {
     sdk_config: Config,
-    signer: Signer,
+    signer: Mutex<Arc<Signer>>,
     device: Device,
+    seed: Vec<u8>,
     gl_client: Mutex<Option<node::Client>>,
     node_client: Mutex<Option<ClnClient>>,
     persister: Arc<SqliteStorage>,
@@ -101,7 +102,11 @@ impl Greenlight {
         persister: Arc<SqliteStorage>,
     ) -> NodeResult<Self> {
         // Derive the encryption key from the seed
-        let temp_signer = Signer::new(seed.clone(), config.network.into(), Nobody::new())?;
+        let temp_signer = Arc::new(Signer::new(
+            seed.clone(),
+            config.network.into(),
+            Nobody::new(),
+        )?);
         let encryption_key = Self::derive_bip32_key(
             config.network,
             &temp_signer,
@@ -172,12 +177,12 @@ impl Greenlight {
         persister: Arc<SqliteStorage>,
     ) -> NodeResult<Greenlight> {
         let greenlight_network = sdk_config.network.into();
-        let signer = Signer::new(seed, greenlight_network, device.clone())?;
-
+        let signer = Signer::new(seed.clone(), greenlight_network, device.clone())?;
         Ok(Greenlight {
             sdk_config,
-            signer,
+            signer: Mutex::new(Arc::new(signer)),
             device,
+            seed,
             gl_client: Mutex::new(None),
             node_client: Mutex::new(None),
             persister,
@@ -185,9 +190,13 @@ impl Greenlight {
         })
     }
 
+    async fn get_signer(&self) -> Arc<Signer> {
+        Arc::clone(&*self.signer.lock().await)
+    }
+
     fn derive_bip32_key(
         network: Network,
-        signer: &Signer,
+        signer: &Arc<Signer>,
         path: Vec<ChildNumber>,
     ) -> NodeResult<ExtendedPrivKey> {
         Ok(
@@ -198,7 +207,7 @@ impl Greenlight {
 
     fn legacy_derive_bip32_key(
         network: Network,
-        signer: &Signer,
+        signer: &Arc<Signer>,
         path: Vec<ChildNumber>,
     ) -> NodeResult<ExtendedPrivKey> {
         Ok(
@@ -272,7 +281,7 @@ impl Greenlight {
 
     fn get_node_credentials(
         network: Network,
-        signer: &Signer,
+        signer: &Arc<Signer>,
         persister: Arc<SqliteStorage>,
     ) -> NodeResult<Option<Device>> {
         // Derive the encryption key from the seed
@@ -987,10 +996,36 @@ struct SyncState {
 
 #[tonic::async_trait]
 impl NodeAPI for Greenlight {
-    fn node_credentials(&self) -> NodeResult<Option<NodeCredentials>> {
+    async fn reconnect(&self) {
+        debug!("Reconnect hibernation: request received");
+
+        // Force refresh existing grpc clients
+        *self.gl_client.lock().await = None;
+        *self.node_client.lock().await = None;
+
+        // Create a new signer
+        debug!("Reconnect hibernation: creating new signer");
+        let new_signer = match Signer::new(
+            self.seed.clone(),
+            self.sdk_config.network.into(),
+            self.device.clone(),
+        ) {
+            Ok(new_signer) => new_signer,
+            Err(e) => {
+                error!(
+                    "Reconnect hibernation: failed to create new signer after reconnect request: {:?}",
+                    e
+                );
+                return;
+            }
+        };
+        *self.signer.lock().await = Arc::new(new_signer);
+    }
+
+    async fn node_credentials(&self) -> NodeResult<Option<NodeCredentials>> {
         Ok(Self::get_node_credentials(
             self.sdk_config.network,
-            &self.signer,
+            &self.get_signer().await,
             self.persister.clone(),
         )?
         .map(|credentials| NodeCredentials::Greenlight {
@@ -1439,7 +1474,7 @@ impl NodeAPI for Greenlight {
     }
 
     async fn node_id(&self) -> NodeResult<String> {
-        Ok(hex::encode(self.signer.node_id()))
+        Ok(hex::encode(self.get_signer().await.node_id()))
     }
 
     async fn redeem_onchain_funds(
@@ -1519,7 +1554,7 @@ impl NodeAPI for Greenlight {
 
     /// Starts the signer that listens in a loop until the shutdown signal is received
     async fn start_signer(&self, shutdown: mpsc::Receiver<()>) {
-        match self.signer.run_forever(shutdown).await {
+        match self.get_signer().await.run_forever(shutdown).await {
             Ok(_) => info!("signer exited gracefully"),
             Err(e) => error!("signer exited with error: {e}"),
         }
@@ -1574,7 +1609,10 @@ impl NodeAPI for Greenlight {
     }
 
     async fn sign_message(&self, message: &str) -> NodeResult<String> {
-        let (sig, recovery_id) = self.signer.sign_message(message.as_bytes().to_vec())?;
+        let (sig, recovery_id) = self
+            .get_signer()
+            .await
+            .sign_message(message.as_bytes().to_vec())?;
         let mut complete_signature = vec![31 + recovery_id];
         complete_signature.extend_from_slice(&sig);
         Ok(zbase32::encode_full_bytes(&complete_signature))
@@ -1590,7 +1628,7 @@ impl NodeAPI for Greenlight {
         Ok(verify(message.as_bytes(), signature, &pk))
     }
 
-    fn sign_invoice(&self, invoice: RawBolt11Invoice) -> NodeResult<String> {
+    async fn sign_invoice(&self, invoice: RawBolt11Invoice) -> NodeResult<String> {
         let hrp_bytes = invoice.hrp.to_string().as_bytes().to_vec();
         let data_bytes = invoice.data.to_base32();
 
@@ -1610,7 +1648,7 @@ impl NodeAPI for Greenlight {
         buf.append(&mut hrp_len_bytes);
         buf.append(&mut hrp_buf);
         // Sign the invoice using the signer
-        let raw_result = self.signer.sign_invoice(buf)?;
+        let raw_result = self.get_signer().await.sign_invoice(buf)?;
         info!(
             "recover id: {:?} raw = {:?}",
             raw_result, raw_result[64] as i32
@@ -1852,12 +1890,12 @@ impl NodeAPI for Greenlight {
         Ok(max_channel_amounts)
     }
 
-    fn derive_bip32_key(&self, path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey> {
-        Self::derive_bip32_key(self.sdk_config.network, &self.signer, path)
+    async fn derive_bip32_key(&self, path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey> {
+        Self::derive_bip32_key(self.sdk_config.network, &self.get_signer().await, path)
     }
 
-    fn legacy_derive_bip32_key(&self, path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey> {
-        Self::legacy_derive_bip32_key(self.sdk_config.network, &self.signer, path)
+    async fn legacy_derive_bip32_key(&self, path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey> {
+        Self::legacy_derive_bip32_key(self.sdk_config.network, &self.get_signer().await, path)
     }
 
     async fn stream_custom_messages(
diff --git a/libs/sdk-core/src/lnurl/auth.rs b/libs/sdk-core/src/lnurl/auth.rs
index 9fb75299a..f56ada3ab 100644
--- a/libs/sdk-core/src/lnurl/auth.rs
+++ b/libs/sdk-core/src/lnurl/auth.rs
@@ -19,16 +19,17 @@ impl SdkLnurlAuthSigner {
     }
 }
 
+#[tonic::async_trait]
 impl LnurlAuthSigner for SdkLnurlAuthSigner {
-    fn derive_bip32_pub_key(&self, derivation_path: &[ChildNumber]) -> LnUrlResult<Vec<u8>> {
-        let xpriv = self.node_api.derive_bip32_key(derivation_path.to_vec())?;
+    async fn derive_bip32_pub_key(&self, derivation_path: &[ChildNumber]) -> LnUrlResult<Vec<u8>> {
+        let xpriv = self.node_api.derive_bip32_key(derivation_path.to_vec()).await?;
         Ok(ExtendedPubKey::from_priv(&Secp256k1::new(), &xpriv)
             .encode()
             .to_vec())
     }
 
-    fn sign_ecdsa(&self, msg: &[u8], derivation_path: &[ChildNumber]) -> LnUrlResult<Vec<u8>> {
-        let xpriv = self.node_api.derive_bip32_key(derivation_path.to_vec())?;
+    async fn sign_ecdsa(&self, msg: &[u8], derivation_path: &[ChildNumber]) -> LnUrlResult<Vec<u8>> {
+        let xpriv = self.node_api.derive_bip32_key(derivation_path.to_vec()).await?;
         let sig = Secp256k1::new().sign_ecdsa(
             &Message::from_slice(msg).map_err(|_| LnUrlError::generic("Failed to sign"))?,
             &xpriv.private_key,
@@ -36,14 +37,14 @@ impl LnurlAuthSigner for SdkLnurlAuthSigner {
         Ok(sig.serialize_der().to_vec())
     }
 
-    fn hmac_sha256(
+    async fn hmac_sha256(
         &self,
         key_derivation_path: &[ChildNumber],
         input: &[u8],
     ) -> LnUrlResult<Vec<u8>> {
         let priv_key = self
             .node_api
-            .derive_bip32_key(key_derivation_path.to_vec())?;
+            .derive_bip32_key(key_derivation_path.to_vec()).await?;
         let mut engine = HmacEngine::<sha256::Hash>::new(priv_key.encode().as_slice());
         engine.input(input);
         Ok(Hmac::<sha256::Hash>::from_engine(engine)
diff --git a/libs/sdk-core/src/node_api.rs b/libs/sdk-core/src/node_api.rs
index f005ed630..f7a3e1e00 100644
--- a/libs/sdk-core/src/node_api.rs
+++ b/libs/sdk-core/src/node_api.rs
@@ -114,7 +114,8 @@ pub struct FetchBolt11Result {
 /// Trait covering functions affecting the LN node
 #[tonic::async_trait]
 pub trait NodeAPI: Send + Sync {
-    fn node_credentials(&self) -> NodeResult<Option<NodeCredentials>>;
+    async fn reconnect(&self);
+    async fn node_credentials(&self) -> NodeResult<Option<NodeCredentials>>;
     async fn configure_node(&self, close_to_address: Option<String>) -> NodeResult<()>;
     async fn create_invoice(&self, request: CreateInvoiceRequest) -> NodeResult<String>;
     /// Fetches an existing BOLT11 invoice from the node
@@ -171,7 +172,7 @@ pub trait NodeAPI: Send + Sync {
     async fn start_signer(&self, shutdown: mpsc::Receiver<()>);
     async fn start_keep_alive(&self, shutdown: watch::Receiver<()>);
     async fn connect_peer(&self, node_id: String, addr: String) -> NodeResult<()>;
-    fn sign_invoice(&self, invoice: RawBolt11Invoice) -> NodeResult<String>;
+    async fn sign_invoice(&self, invoice: RawBolt11Invoice) -> NodeResult<String>;
     async fn close_peer_channels(&self, node_id: String) -> NodeResult<Vec<String>>;
     async fn stream_incoming_payments(
         &self,
@@ -191,8 +192,8 @@ pub trait NodeAPI: Send + Sync {
     ) -> NodeResult<Pin<Box<dyn Stream<Item = Result<CustomMessage>> + Send>>>;
 
     /// Gets the private key at the path specified
-    fn derive_bip32_key(&self, path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey>;
-    fn legacy_derive_bip32_key(&self, path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey>;
+    async fn derive_bip32_key(&self, path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey>;
+    async fn legacy_derive_bip32_key(&self, path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey>;
 
     /// Gets the routing hints related to all private channels that the node has.
     /// Also returns a boolean indicating if the node has a public channel or not.
diff --git a/tools/sdk-cli/Cargo.lock b/tools/sdk-cli/Cargo.lock
index 36d4140ca..9e5daeffa 100644
--- a/tools/sdk-cli/Cargo.lock
+++ b/tools/sdk-cli/Cargo.lock
@@ -2996,6 +2996,7 @@ dependencies = [
  "serde_json",
  "strum_macros",
  "thiserror",
+ "tokio",
  "tonic",
  "tonic-build",
  "url",
diff --git a/tools/sdk-cli/src/command_handlers.rs b/tools/sdk-cli/src/command_handlers.rs
index 90f1847b3..4534f5d02 100644
--- a/tools/sdk-cli/src/command_handlers.rs
+++ b/tools/sdk-cli/src/command_handlers.rs
@@ -381,7 +381,7 @@ pub(crate) async fn handle_command(
                 .await?;
             serde_json::to_string_pretty(&res).map_err(|e| e.into())
         }
-        Commands::NodeCredentials {} => match sdk()?.node_credentials()? {
+        Commands::NodeCredentials {} => match sdk()?.node_credentials().await? {
             Some(credentials) => serde_json::to_string_pretty(&credentials).map_err(|e| e.into()),
             None => Ok("No credentials".into()),
         },


Test patch:
diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs
index 6cfc19b54..201654529 100644
--- a/libs/sdk-core/src/test_utils.rs
+++ b/libs/sdk-core/src/test_utils.rs
@@ -330,7 +330,8 @@ pub struct MockNodeAPI {
 
 #[tonic::async_trait]
 impl NodeAPI for MockNodeAPI {
-    fn node_credentials(&self) -> NodeResult<Option<NodeCredentials>> {
+    async fn reconnect(&self) {}
+    async fn node_credentials(&self) -> NodeResult<Option<NodeCredentials>> {
         Err(NodeError::Generic("Not implemented".to_string()))
     }
 
@@ -444,7 +445,7 @@ impl NodeAPI for MockNodeAPI {
         Ok(true)
     }
 
-    fn sign_invoice(&self, invoice: RawBolt11Invoice) -> NodeResult<String> {
+    async fn sign_invoice(&self, invoice: RawBolt11Invoice) -> NodeResult<String> {
         Ok(sign_invoice(invoice))
     }
 
@@ -484,11 +485,14 @@ impl NodeAPI for MockNodeAPI {
         Err(NodeError::Generic("Not implemented".to_string()))
     }
 
-    fn derive_bip32_key(&self, _path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey> {
+    async fn derive_bip32_key(&self, _path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey> {
         Ok(ExtendedPrivKey::new_master(Network::Bitcoin, &[])?)
     }
 
-    fn legacy_derive_bip32_key(&self, _path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey> {
+    async fn legacy_derive_bip32_key(
+        &self,
+        _path: Vec<ChildNumber>,
+    ) -> NodeResult<ExtendedPrivKey> {
         Ok(ExtendedPrivKey::new_master(Network::Bitcoin, &[])?)
     }