gl_client/
scheduler.rs

1use crate::credentials::{self, RuneProvider, NodeIdProvider, TlsConfigProvider};
2use crate::node::{self, GrpcClient};
3use crate::pb::scheduler::scheduler_client::SchedulerClient;
4use crate::tls::{self};
5use crate::utils::scheduler_uri;
6use crate::{pb, signer::Signer};
7use anyhow::{Result};
8use lightning_signer::bitcoin::Network;
9use log::debug;
10use runeauth;
11use tonic::transport::Channel;
12
13type Client = SchedulerClient<Channel>;
14
15/// A scheduler client to interact with the scheduler service. It has
16/// different implementations depending on the implementations
17#[derive(Clone)]
18pub struct Scheduler<Creds> {
19    client: Client,
20    network: Network,
21    grpc_uri: String,
22    creds: Creds,
23    ca: Vec<u8>,
24}
25
26impl<Creds> Scheduler<Creds>
27where
28    Creds: TlsConfigProvider,
29{
30    /// Creates a new scheduler client with the provided parameters.
31    /// A scheduler created this way is considered unauthenticated and
32    /// limited in its scope.
33    ///
34    /// # Example
35    ///
36    /// ```rust
37    /// # use gl_client::credentials::Nobody;
38    /// # use gl_client::scheduler::Scheduler;
39    /// # use lightning_signer::bitcoin::Network;
40    /// # async fn example() {
41    /// let node_id = vec![0, 1, 2, 3];
42    /// let network = Network::Regtest;
43    /// let creds = Nobody::new();
44    /// let scheduler = Scheduler::new(network, creds).await.unwrap();
45    /// # }
46    /// ```
47    pub async fn new(network: Network, creds: Creds) -> Result<Scheduler<Creds>> {
48        let grpc_uri = scheduler_uri();
49        Self::with(network, creds, grpc_uri).await
50    }
51
52    /// Creates a new scheduler client with the provided parameters and
53    /// custom URI.
54    /// A scheduler created this way is considered unauthenticated and
55    /// limited in its scope.
56    ///
57    /// # Example
58    ///
59    /// ```rust
60    /// # use gl_client::credentials::Nobody;
61    /// # use gl_client::scheduler::Scheduler;
62    /// # use lightning_signer::bitcoin::Network;
63    /// # async fn example() {
64    /// let node_id = vec![0, 1, 2, 3];
65    /// let network = Network::Regtest;
66    /// let creds = Nobody::new();
67    /// let uri = "https://example.com".to_string();
68    /// let scheduler = Scheduler::with(network, creds, uri).await.unwrap();
69    /// # }
70    /// ```
71    pub async fn with(
72        network: Network,
73        creds: Creds,
74        uri: impl Into<String>,
75    ) -> Result<Scheduler<Creds>> {
76        let uri = uri.into();
77        debug!("Connecting to scheduler at {}", uri);
78        let channel = tonic::transport::Endpoint::from_shared(uri.clone())?
79            .tls_config(creds.tls_config().inner.clone())?
80            .tcp_keepalive(Some(crate::TCP_KEEPALIVE))
81            .http2_keep_alive_interval(crate::TCP_KEEPALIVE)
82            .keep_alive_timeout(crate::TCP_KEEPALIVE_TIMEOUT)
83            .keep_alive_while_idle(true)
84            .connect_lazy();
85
86        let client = SchedulerClient::new(channel);
87        let ca = creds.tls_config().ca.clone();
88
89        Ok(Scheduler {
90            client,
91            network,
92            creds,
93            grpc_uri: uri,
94            ca,
95        })
96    }
97}
98
99impl<Creds> Scheduler<Creds> {
100    /// Registers a new node with the scheduler service.
101    ///
102    /// # Arguments
103    ///
104    /// * `signer` - The signer instance bound to the node.
105    /// * `invite_code` - Optional invite code to register the node.
106    ///
107    /// # Example
108    ///
109    /// ```rust
110    /// # use gl_client::credentials::Nobody;
111    /// # use gl_client::{scheduler::Scheduler, signer::Signer};
112    /// # use lightning_signer::bitcoin::Network;
113    /// # async fn example() {
114    /// let node_id = vec![0, 1, 2, 3];
115    /// let network = Network::Regtest;
116    /// let creds = Nobody::new();
117    /// let scheduler = Scheduler::new(network, creds.clone()).await.unwrap();
118    /// let secret = vec![0, 0, 0, 0];
119    /// let signer = Signer::new(secret, network, creds).unwrap(); // Create or obtain a signer instance
120    /// let registration_response = scheduler.register(&signer, None).await.unwrap();
121    /// # }
122    /// ```
123    pub async fn register(
124        &self,
125        signer: &Signer,
126        invite_code: Option<String>,
127    ) -> Result<pb::scheduler::RegistrationResponse> {
128        let code = invite_code.unwrap_or_default();
129        self.inner_register(signer, code).await
130    }
131
132    /// We split the register method into one with an invite code and one
133    /// without an invite code in order to keep the api stable. We might want to
134    /// remove the invite system in the future and so it does not make sense to
135    /// change the signature of the register method.
136    async fn inner_register(
137        &self,
138        signer: &Signer,
139        invite_code: impl Into<String>,
140    ) -> Result<pb::scheduler::RegistrationResponse> {
141        log::debug!("Retrieving challenge for registration");
142        let challenge = self
143            .client
144            .clone()
145            .get_challenge(pb::scheduler::ChallengeRequest {
146                scope: pb::scheduler::ChallengeScope::Register as i32,
147                node_id: signer.node_id(),
148            })
149            .await?
150            .into_inner();
151
152        log::trace!("Got a challenge: {}", hex::encode(&challenge.challenge));
153
154        let signature = signer.sign_challenge(challenge.challenge.clone())?;
155        let device_cert = tls::generate_self_signed_device_cert(
156            &hex::encode(signer.node_id()),
157            "default",
158            vec!["localhost".into()],
159            None,
160        );
161        let device_csr = device_cert.serialize_request_pem()?;
162        debug!("Requesting registration with csr:\n{}", device_csr);
163
164        let startupmsgs = signer
165            .get_startup_messages()
166            .into_iter()
167            .map(|m| m.into())
168            .collect();
169
170        let mut res = self
171            .client
172            .clone()
173            .register(pb::scheduler::RegistrationRequest {
174                node_id: signer.node_id(),
175                bip32_key: signer.bip32_ext_key(),
176                network: self.network.to_string(),
177                challenge: challenge.challenge,
178                signer_proto: signer.version().to_owned(),
179                init_msg: signer.get_init(),
180                signature,
181                csr: device_csr.into_bytes(),
182                invite_code: invite_code.into(),
183                startupmsgs,
184            })
185            .await?
186            .into_inner();
187
188        // This step ensures backwards compatibility with the backend. If we did
189        // receive a device key, the backend did not sign the csr and we need to
190        // return the response as it is. If the device key is empty, the csr was
191        // signed and we return the client side generated private key.
192        if res.device_key.is_empty() {
193            debug!("Received signed certificate:\n{}", &res.device_cert);
194            // We intercept the response and replace the private key with the
195            // private key of the device_cert. This private key has been generated
196            // on and has never left the client device.
197            res.device_key = device_cert.serialize_private_key_pem();
198        }
199
200        let public_key = device_cert.get_key_pair().public_key_raw();
201        debug!(
202            "Asking signer to create a rune for public key {}",
203            hex::encode(public_key)
204        );
205
206        // Create a new rune for the tls certs public key and append it to the
207        // grpc response. Restricts the rune to the public key used for mTLS
208        // authentication.
209        let alt = runeauth::Alternative::new(
210            "pubkey".to_string(),
211            runeauth::Condition::Equal,
212            hex::encode(public_key),
213            false,
214        )?;
215        res.rune = signer.create_rune(None, vec![vec![&alt.encode()]])?;
216
217        // Create a `credentials::Device` struct and serialize it into byte format to
218        // return. This can than be stored on the device.
219        let creds = credentials::Device::with(
220            res.device_cert.clone().into_bytes(),
221            res.device_key.clone().into_bytes(),
222            res.rune.clone(),
223        );
224        res.creds = creds.to_bytes();
225
226        Ok(res)
227    }
228
229    /// Recovers a previously registered node with the scheduler service.
230    ///
231    /// # Arguments
232    ///
233    /// * `signer` - The signer instance used to sign the recovery challenge.
234    ///
235    /// # Example
236    ///
237    /// ```rust
238    /// # use gl_client::credentials::Nobody;
239    /// # use gl_client::{scheduler::Scheduler, signer::Signer};
240    /// # use lightning_signer::bitcoin::Network;
241    /// # async fn example() {
242    /// let node_id = vec![0, 1, 2, 3];
243    /// let network = Network::Regtest;
244    /// let creds = Nobody::new();
245    /// let scheduler = Scheduler::new(network, creds.clone()).await.unwrap();
246    /// let secret = vec![0, 0, 0, 0];
247    /// let signer = Signer::new(secret, network, creds).unwrap(); // Create or obtain a signer instance
248    /// let recovery_response = scheduler.recover(&signer).await.unwrap();
249    /// # }
250    /// ```
251    pub async fn recover(&self, signer: &Signer) -> Result<pb::scheduler::RecoveryResponse> {
252        let challenge = self
253            .client
254            .clone()
255            .get_challenge(pb::scheduler::ChallengeRequest {
256                scope: pb::scheduler::ChallengeScope::Recover as i32,
257                node_id: signer.node_id(),
258            })
259            .await?
260            .into_inner();
261
262        let signature = signer.sign_challenge(challenge.challenge.clone())?;
263        let name = format!("recovered-{}", hex::encode(&challenge.challenge[0..8]));
264        let device_cert = tls::generate_self_signed_device_cert(
265            &hex::encode(signer.node_id()),
266            &name,
267            vec!["localhost".into()],
268            None,
269        );
270        let device_csr = device_cert.serialize_request_pem()?;
271        debug!("Requesting recovery with csr:\n{}", device_csr);
272
273        let mut res = self
274            .client
275            .clone()
276            .recover(pb::scheduler::RecoveryRequest {
277                node_id: signer.node_id(),
278                challenge: challenge.challenge,
279                signature,
280                csr: device_csr.into_bytes(),
281            })
282            .await?
283            .into_inner();
284
285        // This step ensures backwards compatibility with the backend. If we did
286        // receive a device key, the backend did not sign the csr and we need to
287        // return the response as it is. If the device key is empty, the csr was
288        // signed and we return the client side generated private key.
289        if res.device_key.is_empty() {
290            debug!("Received signed certificate:\n{}", &res.device_cert);
291            // We intercept the response and replace the private key with the
292            // private key of the device_cert. This private key has been generated
293            // on and has never left the client device.
294            res.device_key = device_cert.serialize_private_key_pem();
295        }
296
297        let public_key = device_cert.get_key_pair().public_key_raw();
298        debug!(
299            "Asking signer to create a rune for public key {}",
300            hex::encode(public_key)
301        );
302
303        // Create a new rune for the tls certs public key and append it to the
304        // grpc response. Restricts the rune to the public key used for mTLS
305        // authentication.
306        let alt = runeauth::Alternative::new(
307            "pubkey".to_string(),
308            runeauth::Condition::Equal,
309            hex::encode(public_key),
310            false,
311        )?;
312        res.rune = signer.create_rune(None, vec![vec![&alt.encode()]])?;
313
314        // Create a `credentials::Device` struct and serialize it into byte format to
315        // return. This can than be stored on the device.
316        let creds = credentials::Device::with(
317            res.device_cert.clone().into_bytes(),
318            res.device_key.clone().into_bytes(),
319            res.rune.clone(),
320        );
321        res.creds = creds.to_bytes();
322
323        Ok(res)
324    }
325
326    /// Elevates the scheduler client to an authenticated scheduler client
327    /// that is able to schedule a node for example.
328    ///
329    /// # Arguments
330    ///
331    /// * `creds` - Credentials that carry a TlsConfig and a Rune. These
332    /// are credentials returned during registration or recovery.
333    ///
334    /// # Example
335    ///
336    /// ```rust
337    /// # use gl_client::credentials::{Device, Nobody};
338    /// # use gl_client::{scheduler::Scheduler, signer::Signer};
339    /// # use lightning_signer::bitcoin::Network;
340    /// # async fn example() {
341    /// let node_id = vec![0, 1, 2, 3];
342    /// let network = Network::Regtest;
343    /// let creds = Nobody::new();
344    /// let scheduler_unauthed = Scheduler::new(network, creds.clone()).await.unwrap();
345    /// let secret = vec![0, 0, 0, 0];
346    /// let signer = Signer::new(secret, network, creds).unwrap(); // Create or obtain a signer instance
347    /// let registration_response = scheduler_unauthed.register(&signer, None).await.unwrap();
348    /// let creds = Device::from_bytes(registration_response.creds);
349    /// let scheduler_authed = scheduler_unauthed.authenticate(creds);
350    /// # }
351    /// ```
352    pub async fn authenticate<Auth>(&self, creds: Auth) -> Result<Scheduler<Auth>>
353    where
354        Auth: TlsConfigProvider + RuneProvider,
355    {
356        debug!("Connecting to scheduler at {}", self.grpc_uri);
357        let channel = tonic::transport::Endpoint::from_shared(self.grpc_uri.clone())?
358            .tls_config(creds.tls_config().inner.clone())?
359            .tcp_keepalive(Some(crate::TCP_KEEPALIVE))
360            .http2_keep_alive_interval(crate::TCP_KEEPALIVE)
361            .keep_alive_timeout(crate::TCP_KEEPALIVE_TIMEOUT)
362            .keep_alive_while_idle(true)
363            .connect_lazy();
364
365        let client = SchedulerClient::new(channel);
366
367        Ok(Scheduler {
368            client,
369            network: self.network,
370            creds,
371            grpc_uri: self.grpc_uri.clone(),
372            ca: self.ca.clone(),
373        })
374    }
375}
376
377impl<Creds> Scheduler<Creds>
378where
379    Creds: TlsConfigProvider + RuneProvider + NodeIdProvider + Clone,
380{
381    /// Schedules a node at the scheduler service. Once a node is
382    /// scheduled one can access it through the node client.
383    ///
384    /// # Example
385    ///
386    /// ```rust
387    /// # use gl_client::credentials::Device;
388    /// # use gl_client::{scheduler::Scheduler, node::{Node, Client}};
389    /// # use lightning_signer::bitcoin::Network;
390    /// # async fn example() {
391    /// let node_id = vec![0, 1, 2, 3];
392    /// let network = Network::Regtest;
393    /// let creds = Device::from_path("my/path/to/credentials.glc");
394    /// let scheduler = Scheduler::new(network, creds.clone()).await.unwrap();
395    /// let info = scheduler.schedule().await.unwrap();
396    /// let node_client: Client  = Node::new(node_id, creds).unwrap().connect(info.grpc_uri).await.unwrap();
397    /// # }
398    /// ```
399    pub async fn schedule(&self) -> Result<pb::scheduler::NodeInfoResponse> {
400        let res = self
401            .client
402            .clone()
403            .schedule(pb::scheduler::ScheduleRequest {
404                node_id: self.creds.node_id()?,
405            })
406            .await?;
407        Ok(res.into_inner())
408    }
409
410    /// Schedules a node at the scheduler service and returns a node
411    /// client.
412    ///
413    /// # Example
414    ///
415    /// ```rust
416    /// # use gl_client::credentials::Device;
417    /// # use gl_client::scheduler::Scheduler;
418    /// # use gl_client::node::Client;
419    /// # use lightning_signer::bitcoin::Network;
420    /// # async fn example() {
421    /// let node_id = vec![0, 1, 2, 3];
422    /// let network = Network::Regtest;
423    /// let creds = Device::from_path("my/path/to/credentials.glc");
424    /// let scheduler = Scheduler::new(network, creds.clone()).await.unwrap();
425    /// let node_client: Client  = scheduler.node().await.unwrap();
426    /// # }
427    /// ```
428    pub async fn node<T>(&self) -> Result<T>
429    where
430        T: GrpcClient,
431    {
432        let res = self.schedule().await?;
433        node::Node::new(self.creds.node_id()?, self.creds.clone())?
434            .connect(res.grpc_uri)
435            .await
436    }
437
438    pub async fn get_node_info(&self, wait: bool) -> Result<pb::scheduler::NodeInfoResponse> {
439        Ok(self
440            .client
441            .clone()
442            .get_node_info(pb::scheduler::NodeInfoRequest {
443                node_id: self.creds.node_id()?,
444                wait: wait,
445            })
446            .await?
447            .into_inner())
448    }
449
450    pub async fn export_node(&self) -> Result<pb::scheduler::ExportNodeResponse> {
451        Ok(self
452            .client
453            .clone()
454            .export_node(pb::scheduler::ExportNodeRequest {})
455            .await?
456            .into_inner())
457    }
458
459    pub async fn get_invite_codes(&self) -> Result<pb::scheduler::ListInviteCodesResponse> {
460        let res = self
461            .client
462            .clone()
463            .list_invite_codes(pb::scheduler::ListInviteCodesRequest {})
464            .await?;
465        Ok(res.into_inner())
466    }
467
468    pub async fn add_outgoing_webhook(
469        &self,
470        uri: String,
471    ) -> Result<pb::scheduler::AddOutgoingWebhookResponse> {
472        let node_id = self.creds.node_id()?;
473        let res = self
474            .client
475            .clone()
476            .add_outgoing_webhook(pb::scheduler::AddOutgoingWebhookRequest { node_id, uri })
477            .await?;
478        Ok(res.into_inner())
479    }
480
481    pub async fn list_outgoing_webhooks(
482        &self,
483    ) -> Result<pb::scheduler::ListOutgoingWebhooksResponse> {
484        let node_id = self.creds.node_id()?;
485        let res = self
486            .client
487            .clone()
488            .list_outgoing_webhooks(pb::scheduler::ListOutgoingWebhooksRequest { node_id })
489            .await?;
490        Ok(res.into_inner())
491    }
492
493    pub async fn delete_webhooks(&self, webhook_ids: Vec<i64>) -> Result<pb::greenlight::Empty> {
494        let node_id = self.creds.node_id()?;
495        let res = self
496            .client
497            .clone()
498            .delete_webhooks(pb::scheduler::DeleteOutgoingWebhooksRequest {
499                node_id,
500                ids: webhook_ids,
501            })
502            .await?;
503        Ok(res.into_inner())
504    }
505
506    pub async fn rotate_outgoing_webhook_secret(
507        &self,
508        webhook_id: i64,
509    ) -> Result<pb::scheduler::WebhookSecretResponse> {
510        let node_id = self.creds.node_id()?;
511        let res = self
512            .client
513            .clone()
514            .rotate_outgoing_webhook_secret(pb::scheduler::RotateOutgoingWebhookSecretRequest {
515                node_id,
516                webhook_id,
517            })
518            .await?;
519        Ok(res.into_inner())
520    }
521}