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}