1use std::{
5 borrow::Cow,
6 collections::BTreeSet,
7 future::IntoFuture,
8 iter,
9 net::SocketAddr,
10 num::NonZeroU16,
11 sync::{Arc, Mutex as StdMutex},
12};
13
14use async_graphql::{
15 futures_util::Stream,
16 registry::{MetaType, MetaTypeId, Registry},
17 resolver_utils::ContainerType,
18 EmptyMutation, Error, MergedObject, OutputType, Positioned, Request, Response, ScalarType,
19 Schema, SimpleObject, Subscription,
20};
21use async_graphql_axum::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
22use axum::{extract::Path, http::StatusCode, response, response::IntoResponse, Extension, Router};
23use futures::{lock::Mutex, Future, FutureExt as _, StreamExt as _, TryStreamExt as _};
24use linera_base::{
25 crypto::{CryptoError, CryptoHash},
26 data_types::{
27 Amount, ApplicationDescription, ApplicationPermissions, BlockHeight, Bytecode, Epoch,
28 TimeDelta,
29 },
30 identifiers::{
31 Account, AccountOwner, ApplicationId, ChainId, IndexAndEvent, ModuleId, StreamId,
32 },
33 ownership::{ChainOwnership, TimeoutConfig},
34 vm::VmRuntime,
35 BcsHexParseError,
36};
37use linera_chain::{types::ConfirmedBlock, ChainStateView};
38use linera_client::chain_listener::{
39 ChainListener, ChainListenerConfig, ClientContext, ClientContextExt as _, ListenerCommand,
40};
41use linera_core::{
42 client::chain_client::{self, ChainClient},
43 data_types::ClientOutcome,
44 wallet::Wallet as _,
45 worker::{ChainStateViewReadGuard, Notification, Reason},
46};
47use linera_execution::{
48 committee::Committee, system::AdminOperation, Operation, Query, QueryOutcome, QueryResponse,
49 SystemOperation,
50};
51#[cfg(with_metrics)]
52use linera_metrics::monitoring_server;
53use linera_sdk::linera_base_types::BlobContent;
54use linera_storage::Storage;
55use lru::LruCache;
56use serde::{Deserialize, Serialize};
57use serde_json::json;
58use tokio::sync::mpsc::UnboundedReceiver;
59use tokio_util::sync::CancellationToken;
60use tower_http::cors::CorsLayer;
61use tracing::{debug, info, instrument, trace};
62
63use crate::util;
64
65#[derive(Clone)]
71struct RawJson(String);
72
73impl OutputType for RawJson {
74 fn type_name() -> Cow<'static, str> {
75 Cow::Borrowed("JSON")
76 }
77
78 fn create_type_info(registry: &mut Registry) -> String {
79 registry.create_output_type::<Self, _>(MetaTypeId::Scalar, |_| MetaType::Scalar {
80 name: "JSON".to_string(),
81 description: Some("A scalar that can represent any JSON value.".to_string()),
82 is_valid: None,
83 visible: None,
84 inaccessible: false,
85 tags: Default::default(),
86 specified_by_url: None,
87 directive_invocations: Default::default(),
88 requires_scopes: Default::default(),
89 })
90 }
91
92 async fn resolve(
93 &self,
94 _ctx: &async_graphql::ContextSelectionSet<'_>,
95 _field: &Positioned<async_graphql::parser::types::Field>,
96 ) -> async_graphql::ServerResult<async_graphql::Value> {
97 Ok(async_graphql::Value::Object(
102 std::iter::once((
103 async_graphql::Name::new(async_graphql_value::RAW_VALUE_TOKEN),
104 async_graphql::Value::String(self.0.clone()),
105 ))
106 .collect(),
107 ))
108 }
109}
110
111#[derive(SimpleObject, Serialize, Deserialize, Clone)]
113pub struct Chains {
114 pub list: Vec<ChainId>,
116 pub default: Option<ChainId>,
118}
119
120pub struct QueryRoot<C> {
122 context: Arc<Mutex<C>>,
123 port: NonZeroU16,
124 default_chain: Option<ChainId>,
125}
126
127pub struct SubscriptionRoot<C> {
129 context: Arc<Mutex<C>>,
130 query_subscriptions: Option<Arc<crate::query_subscription::QuerySubscriptionManager>>,
131 cancellation_token: CancellationToken,
132}
133
134pub struct MutationRoot<C> {
136 context: Arc<Mutex<C>>,
137}
138
139#[derive(Debug, thiserror::Error)]
140enum NodeServiceError {
141 #[error(transparent)]
142 ChainClient(#[from] chain_client::Error),
143 #[error(transparent)]
144 BcsHex(#[from] BcsHexParseError),
145 #[error(transparent)]
146 Json(#[from] serde_json::Error),
147 #[error("malformed chain ID: {0}")]
148 InvalidChainId(CryptoError),
149 #[error(transparent)]
150 Client(#[from] linera_client::Error),
151 #[error("scheduling operations from queries is disabled in read-only mode")]
152 ReadOnlyModeOperationsNotAllowed,
153}
154
155impl IntoResponse for NodeServiceError {
156 fn into_response(self) -> response::Response {
157 let status = match self {
158 NodeServiceError::InvalidChainId(_) | NodeServiceError::BcsHex(_) => {
159 StatusCode::BAD_REQUEST
160 }
161 NodeServiceError::ReadOnlyModeOperationsNotAllowed => StatusCode::FORBIDDEN,
162 _ => StatusCode::INTERNAL_SERVER_ERROR,
163 };
164 let body = json!({"error": self.to_string()}).to_string();
165 (status, body).into_response()
166 }
167}
168
169#[Subscription]
170impl<C> SubscriptionRoot<C>
171where
172 C: ClientContext + 'static,
173{
174 async fn notifications(
176 &self,
177 chain_id: ChainId,
178 ) -> Result<impl Stream<Item = Notification>, Error> {
179 let client = self
180 .context
181 .lock()
182 .await
183 .make_chain_client(chain_id)
184 .await?;
185 Ok(client.subscribe()?)
186 }
187
188 async fn query_result(
191 &self,
192 #[graphql(desc = "Name of the registered subscription query.")] name: String,
193 #[graphql(desc = "The chain to watch.")] chain_id: ChainId,
194 #[graphql(desc = "The application to query.")] application_id: ApplicationId,
195 ) -> Result<impl Stream<Item = RawJson>, Error> {
196 let manager = self
197 .query_subscriptions
198 .as_ref()
199 .ok_or_else(|| Error::new("no subscription queries registered"))?;
200
201 let key = crate::query_subscription::SubscriptionKey {
202 name,
203 chain_id,
204 application_id,
205 };
206
207 let receiver = manager
208 .subscribe(
209 &key,
210 Arc::clone(&self.context),
211 self.cancellation_token.clone(),
212 )
213 .map_err(|e| Error::new(e.to_string()))?;
214
215 let current = receiver.borrow().clone();
220 let changes = tokio_stream::wrappers::WatchStream::from_changes(receiver)
221 .filter_map(|value| async move { value });
222 Ok(futures::stream::iter(current).chain(changes).map(RawJson))
223 }
224}
225
226impl<C> MutationRoot<C>
227where
228 C: ClientContext,
229{
230 async fn execute_system_operation(
231 &self,
232 system_operation: SystemOperation,
233 chain_id: ChainId,
234 ) -> Result<CryptoHash, Error> {
235 let certificate = self
236 .apply_client_command(&chain_id, move |client| {
237 let operation = Operation::system(system_operation.clone());
238 async move {
239 let result = client
240 .execute_operation(operation)
241 .await
242 .map_err(Error::from);
243 (result, client)
244 }
245 })
246 .await?;
247 Ok(certificate.hash())
248 }
249
250 async fn maybe_auto_assign_preferred_owner(
253 &self,
254 chain_id: ChainId,
255 new_ownership: &ChainOwnership,
256 ) -> Result<(), Error> {
257 let context = self.context.lock().await;
258 let mut chain_client = context.make_chain_client(chain_id).await?;
259 context
260 .maybe_auto_assign_preferred_owner(&mut chain_client, new_ownership)
261 .await?;
262 Ok(())
263 }
264
265 async fn apply_client_command<F, Fut, T>(
269 &self,
270 chain_id: &ChainId,
271 mut f: F,
272 ) -> Result<T, Error>
273 where
274 F: FnMut(ChainClient<C::Environment>) -> Fut,
275 Fut: Future<Output = (Result<ClientOutcome<T>, Error>, ChainClient<C::Environment>)>,
276 {
277 loop {
278 let client = self
279 .context
280 .lock()
281 .await
282 .make_chain_client(*chain_id)
283 .await?;
284 let mut stream = client.subscribe()?;
285 let (result, client) = f(client).await;
286 self.context.lock().await.update_wallet(&client).await?;
287 let timeout = match result? {
288 ClientOutcome::Committed(t) => return Ok(t),
289 ClientOutcome::Conflict(certificate) => {
290 return Err(chain_client::Error::Conflict(certificate.hash()).into());
291 }
292 ClientOutcome::WaitForTimeout(timeout) => timeout,
293 };
294 drop(client);
295 util::wait_for_next_round(&mut stream, timeout).await;
296 }
297 }
298}
299
300#[async_graphql::Object(cache_control(no_cache))]
301impl<C> MutationRoot<C>
302where
303 C: ClientContext + 'static,
304{
305 async fn process_inbox(
307 &self,
308 #[graphql(desc = "The chain whose inbox is being processed.")] chain_id: ChainId,
309 ) -> Result<Vec<CryptoHash>, Error> {
310 let mut hashes = Vec::new();
311 loop {
312 let client = self
313 .context
314 .lock()
315 .await
316 .make_chain_client(chain_id)
317 .await?;
318 let result = client.process_inbox().await;
319 self.context.lock().await.update_wallet(&client).await?;
320 let (certificates, maybe_timeout) = result?;
321 hashes.extend(certificates.into_iter().map(|cert| cert.hash()));
322 match maybe_timeout {
323 None => return Ok(hashes),
324 Some(timestamp) => {
325 let mut stream = client.subscribe()?;
326 drop(client);
327 util::wait_for_next_round(&mut stream, timestamp).await;
328 }
329 }
330 }
331 }
332
333 async fn sync(
338 &self,
339 #[graphql(desc = "The chain being synchronized.")] chain_id: ChainId,
340 ) -> Result<u64, Error> {
341 let client = self
342 .context
343 .lock()
344 .await
345 .make_chain_client(chain_id)
346 .await?;
347 let info = client.synchronize_from_validators().await?;
348 self.context.lock().await.update_wallet(&client).await?;
349 Ok(info.next_block_height.0)
350 }
351
352 async fn retry_pending_block(
354 &self,
355 #[graphql(desc = "The chain on whose block is being retried.")] chain_id: ChainId,
356 ) -> Result<Option<CryptoHash>, Error> {
357 let client = self
358 .context
359 .lock()
360 .await
361 .make_chain_client(chain_id)
362 .await?;
363 let outcome = client.process_pending_block().await?;
364 self.context.lock().await.update_wallet(&client).await?;
365 match outcome {
366 ClientOutcome::Committed(Some(certificate)) => Ok(Some(certificate.hash())),
367 ClientOutcome::Committed(None) => Ok(None),
368 ClientOutcome::WaitForTimeout(timeout) => Err(Error::from(format!(
369 "Please try again at {}",
370 timeout.timestamp
371 ))),
372 ClientOutcome::Conflict(certificate) => Err(Error::from(format!(
373 "A different block was committed: {}",
374 certificate.hash()
375 ))),
376 }
377 }
378
379 async fn transfer(
382 &self,
383 #[graphql(desc = "The chain which native tokens are being transferred from.")]
384 chain_id: ChainId,
385 #[graphql(desc = "The account being debited on the chain.")] owner: AccountOwner,
386 #[graphql(desc = "The recipient of the transfer.")] recipient: Account,
387 #[graphql(desc = "The amount being transferred.")] amount: Amount,
388 ) -> Result<CryptoHash, Error> {
389 self.apply_client_command(&chain_id, move |client| async move {
390 let result = client
391 .transfer(owner, amount, recipient)
392 .await
393 .map_err(Error::from)
394 .map(|outcome| outcome.map(|certificate| certificate.hash()));
395 (result, client)
396 })
397 .await
398 }
399
400 async fn claim(
404 &self,
405 #[graphql(desc = "The chain for whom owner is one of the owner.")] chain_id: ChainId,
406 #[graphql(desc = "The owner of chain targetId being debited.")] owner: AccountOwner,
407 #[graphql(desc = "The chain whose owner is being debited.")] target_id: ChainId,
408 #[graphql(desc = "The recipient of the transfer.")] recipient: Account,
409 #[graphql(desc = "The amount being transferred.")] amount: Amount,
410 ) -> Result<CryptoHash, Error> {
411 self.apply_client_command(&chain_id, move |client| async move {
412 let result = client
413 .claim(owner, target_id, recipient, amount)
414 .await
415 .map_err(Error::from)
416 .map(|outcome| outcome.map(|certificate| certificate.hash()));
417 (result, client)
418 })
419 .await
420 }
421
422 async fn read_data_blob(
425 &self,
426 chain_id: ChainId,
427 hash: CryptoHash,
428 ) -> Result<CryptoHash, Error> {
429 self.apply_client_command(&chain_id, move |client| async move {
430 let result = client
431 .read_data_blob(hash)
432 .await
433 .map_err(Error::from)
434 .map(|outcome| outcome.map(|certificate| certificate.hash()));
435 (result, client)
436 })
437 .await
438 }
439
440 async fn open_chain(
442 &self,
443 #[graphql(desc = "The chain paying for the creation of the new chain.")] chain_id: ChainId,
444 #[graphql(desc = "The owner of the new chain.")] owner: AccountOwner,
445 #[graphql(desc = "The balance of the chain being created. Zero if `None`.")]
446 balance: Option<Amount>,
447 #[graphql(
448 desc = "The account on the new chain credited with the balance. The chain account \
449 itself if `None`."
450 )]
451 account: Option<AccountOwner>,
452 ) -> Result<ChainId, Error> {
453 let ownership = ChainOwnership::single(owner);
454 let balance = balance.unwrap_or(Amount::ZERO);
455 let account = account.unwrap_or(AccountOwner::CHAIN);
456 let description = self
457 .apply_client_command(&chain_id, move |client| {
458 let ownership = ownership.clone();
459 async move {
460 let result = client
461 .open_chain(
462 ownership,
463 ApplicationPermissions::default(),
464 account,
465 balance,
466 )
467 .await
468 .map_err(Error::from)
469 .map(|outcome| outcome.map(|(chain_id, _)| chain_id));
470 (result, client)
471 }
472 })
473 .await?;
474 Ok(description.id())
475 }
476
477 #[expect(clippy::too_many_arguments)]
479 async fn open_multi_owner_chain(
480 &self,
481 #[graphql(desc = "The chain paying for the creation of the new chain.")] chain_id: ChainId,
482 #[graphql(desc = "Permissions for applications on the new chain")]
483 application_permissions: Option<ApplicationPermissions>,
484 #[graphql(desc = "The owners of the chain")] owners: Vec<AccountOwner>,
485 #[graphql(desc = "The weights of the owners")] weights: Option<Vec<u64>>,
486 #[graphql(desc = "The number of multi-leader rounds")] multi_leader_rounds: Option<u32>,
487 #[graphql(desc = "The balance of the chain. Zero if `None`")] balance: Option<Amount>,
488 #[graphql(
489 desc = "The account on the new chain credited with the balance. The chain account \
490 itself if `None`."
491 )]
492 account: Option<AccountOwner>,
493 #[graphql(desc = "The duration of the fast round, in milliseconds; default: no timeout")]
494 fast_round_ms: Option<u64>,
495 #[graphql(
496 desc = "The duration of the first single-leader and all multi-leader rounds",
497 default = 10_000
498 )]
499 base_timeout_ms: u64,
500 #[graphql(
501 desc = "The number of milliseconds by which the timeout increases after each \
502 single-leader round",
503 default = 1_000
504 )]
505 timeout_increment_ms: u64,
506 #[graphql(
507 desc = "The age of an incoming tracked or protected message after which the \
508 validators start transitioning the chain to fallback mode, in milliseconds.",
509 default = 86_400_000
510 )]
511 fallback_duration_ms: u64,
512 ) -> Result<ChainId, Error> {
513 let owners = if let Some(weights) = weights {
514 if weights.len() != owners.len() {
515 return Err(Error::new(format!(
516 "There are {} owners but {} weights.",
517 owners.len(),
518 weights.len()
519 )));
520 }
521 owners.into_iter().zip(weights).collect::<Vec<_>>()
522 } else {
523 owners
524 .into_iter()
525 .zip(iter::repeat(100))
526 .collect::<Vec<_>>()
527 };
528 let multi_leader_rounds = multi_leader_rounds.unwrap_or(u32::MAX);
529 let timeout_config = TimeoutConfig {
530 fast_round_duration: fast_round_ms.map(TimeDelta::from_millis),
531 base_timeout: TimeDelta::from_millis(base_timeout_ms),
532 timeout_increment: TimeDelta::from_millis(timeout_increment_ms),
533 fallback_duration: TimeDelta::from_millis(fallback_duration_ms),
534 };
535 let ownership = ChainOwnership::multiple(owners, multi_leader_rounds, timeout_config);
536 let balance = balance.unwrap_or(Amount::ZERO);
537 let account = account.unwrap_or(AccountOwner::CHAIN);
538 let description = self
539 .apply_client_command(&chain_id, move |client| {
540 let ownership = ownership.clone();
541 let application_permissions = application_permissions.clone().unwrap_or_default();
542 async move {
543 let result = client
544 .open_chain(ownership, application_permissions, account, balance)
545 .await
546 .map_err(Error::from)
547 .map(|outcome| outcome.map(|(chain_id, _)| chain_id));
548 (result, client)
549 }
550 })
551 .await?;
552 Ok(description.id())
553 }
554
555 async fn close_chain(
557 &self,
558 #[graphql(desc = "The chain being closed.")] chain_id: ChainId,
559 ) -> Result<Option<CryptoHash>, Error> {
560 let maybe_cert = self
561 .apply_client_command(&chain_id, |client| async move {
562 let result = client.close_chain().await.map_err(Error::from);
563 (result, client)
564 })
565 .await?;
566 Ok(maybe_cert.as_ref().map(|cert| cert.hash()))
567 }
568
569 async fn change_owner(
571 &self,
572 #[graphql(desc = "The chain whose ownership changes")] chain_id: ChainId,
573 #[graphql(desc = "The new single owner of the chain")] new_owner: AccountOwner,
574 ) -> Result<CryptoHash, Error> {
575 let new_ownership = ChainOwnership::single_super(new_owner);
576 let operation = SystemOperation::ChangeOwnership {
577 super_owners: vec![new_owner],
578 owners: Vec::new(),
579 first_leader: None,
580 multi_leader_rounds: 5,
581 open_multi_leader_rounds: false,
582 timeout_config: TimeoutConfig::default(),
583 };
584 let hash = self.execute_system_operation(operation, chain_id).await?;
585 self.maybe_auto_assign_preferred_owner(chain_id, &new_ownership)
586 .await?;
587 Ok(hash)
588 }
589
590 #[expect(clippy::too_many_arguments)]
592 async fn change_multiple_owners(
593 &self,
594 #[graphql(desc = "The chain whose ownership changes")] chain_id: ChainId,
595 #[graphql(desc = "The new list of owners of the chain")] new_owners: Vec<AccountOwner>,
596 #[graphql(desc = "The new list of weights of the owners")] new_weights: Vec<u64>,
597 #[graphql(desc = "The multi-leader round of the chain")] multi_leader_rounds: u32,
598 #[graphql(
599 desc = "Whether multi-leader rounds are unrestricted, that is not limited to chain owners."
600 )]
601 open_multi_leader_rounds: bool,
602 #[graphql(desc = "The leader of the first single-leader round. \
603 If not set, this is random like other rounds.")]
604 first_leader: Option<AccountOwner>,
605 #[graphql(desc = "The duration of the fast round, in milliseconds; default: no timeout")]
606 fast_round_ms: Option<u64>,
607 #[graphql(
608 desc = "The duration of the first single-leader and all multi-leader rounds",
609 default = 10_000
610 )]
611 base_timeout_ms: u64,
612 #[graphql(
613 desc = "The number of milliseconds by which the timeout increases after each \
614 single-leader round",
615 default = 1_000
616 )]
617 timeout_increment_ms: u64,
618 #[graphql(
619 desc = "The age of an incoming tracked or protected message after which the \
620 validators start transitioning the chain to fallback mode, in milliseconds.",
621 default = 86_400_000
622 )]
623 fallback_duration_ms: u64,
624 ) -> Result<CryptoHash, Error> {
625 let timeout_config = TimeoutConfig {
626 fast_round_duration: fast_round_ms.map(TimeDelta::from_millis),
627 base_timeout: TimeDelta::from_millis(base_timeout_ms),
628 timeout_increment: TimeDelta::from_millis(timeout_increment_ms),
629 fallback_duration: TimeDelta::from_millis(fallback_duration_ms),
630 };
631 let owners = new_owners.into_iter().zip(new_weights).collect::<Vec<_>>();
632 let new_ownership = ChainOwnership {
633 super_owners: BTreeSet::new(),
634 owners: owners.iter().cloned().collect(),
635 first_leader,
636 multi_leader_rounds,
637 open_multi_leader_rounds,
638 timeout_config: timeout_config.clone(),
639 };
640 let operation = SystemOperation::ChangeOwnership {
641 super_owners: Vec::new(),
642 owners,
643 first_leader,
644 multi_leader_rounds,
645 open_multi_leader_rounds,
646 timeout_config,
647 };
648 let hash = self.execute_system_operation(operation, chain_id).await?;
649 self.maybe_auto_assign_preferred_owner(chain_id, &new_ownership)
650 .await?;
651 Ok(hash)
652 }
653
654 #[expect(clippy::too_many_arguments)]
656 async fn change_application_permissions(
657 &self,
658 #[graphql(desc = "The chain whose permissions are being changed")] chain_id: ChainId,
659 #[graphql(
660 desc = "These applications are allowed to manage the chain: close it, change \
661 application permissions, and change ownership."
662 )]
663 manage_chain: Vec<ApplicationId>,
664 #[graphql(
665 desc = "If this is `None`, all system operations and application operations are allowed.
666If it is `Some`, only operations from the specified applications are allowed,
667and no system operations."
668 )]
669 execute_operations: Option<Vec<ApplicationId>>,
670 #[graphql(
671 desc = "At least one operation or incoming message from each of these applications must occur in every block."
672 )]
673 mandatory_applications: Vec<ApplicationId>,
674 #[graphql(
675 desc = "These applications are allowed to perform calls to services as oracles."
676 )]
677 call_service_as_oracle: Option<Vec<ApplicationId>>,
678 #[graphql(desc = "These applications are allowed to perform HTTP requests.")]
679 make_http_requests: Option<Vec<ApplicationId>>,
680 ) -> Result<CryptoHash, Error> {
681 let operation = SystemOperation::ChangeApplicationPermissions(ApplicationPermissions {
682 execute_operations,
683 mandatory_applications,
684 manage_chain,
685 call_service_as_oracle,
686 make_http_requests,
687 });
688 self.execute_system_operation(operation, chain_id).await
689 }
690
691 async fn create_committee(
695 &self,
696 chain_id: ChainId,
697 committee: Committee,
698 ) -> Result<CryptoHash, Error> {
699 Ok(self
700 .apply_client_command(&chain_id, move |client| {
701 let committee = committee.clone();
702 async move {
703 let result = client
704 .stage_new_committee(committee)
705 .await
706 .map_err(Error::from);
707 (result, client)
708 }
709 })
710 .await?
711 .hash())
712 }
713
714 async fn remove_committee(&self, chain_id: ChainId, epoch: Epoch) -> Result<CryptoHash, Error> {
718 let operation = SystemOperation::Admin(AdminOperation::RemoveCommittee { epoch });
719 self.execute_system_operation(operation, chain_id).await
720 }
721
722 async fn publish_module(
726 &self,
727 #[graphql(desc = "The chain publishing the module")] chain_id: ChainId,
728 #[graphql(desc = "The bytecode of the contract code")] contract: Bytecode,
729 #[graphql(desc = "The bytecode of the service code (only relevant for WebAssembly)")]
730 service: Bytecode,
731 #[graphql(desc = "The virtual machine being used (either Wasm or Evm)")]
732 vm_runtime: VmRuntime,
733 #[graphql(desc = "Optional BCS-encoded `Formats` description bytes")] formats: Option<
734 Vec<u8>,
735 >,
736 ) -> Result<ModuleId, Error> {
737 self.apply_client_command(&chain_id, move |client| {
738 let contract = contract.clone();
739 let service = service.clone();
740 let formats = formats.clone();
741 async move {
742 let result = client
743 .publish_module(contract, service, vm_runtime, formats)
744 .await
745 .map_err(Error::from)
746 .map(|outcome| outcome.map(|(module_id, _)| module_id));
747 (result, client)
748 }
749 })
750 .await
751 }
752
753 async fn publish_data_blob(
755 &self,
756 #[graphql(desc = "The chain paying for the blob publication")] chain_id: ChainId,
757 #[graphql(desc = "The content of the data blob being created")] bytes: Vec<u8>,
758 ) -> Result<CryptoHash, Error> {
759 self.apply_client_command(&chain_id, |client| {
760 let bytes = bytes.clone();
761 async move {
762 let result = client.publish_data_blob(bytes).await.map_err(Error::from);
763 (result, client)
764 }
765 })
766 .await
767 .map(|_| CryptoHash::new(&BlobContent::new_data(bytes)))
768 }
769
770 async fn create_application(
772 &self,
773 #[graphql(desc = "The chain paying for the creation of the application")] chain_id: ChainId,
774 #[graphql(desc = "The module ID of the application being created")] module_id: ModuleId,
775 #[graphql(desc = "The JSON serialization of the parameters of the application")]
776 parameters: String,
777 #[graphql(
778 desc = "The JSON serialization of the instantiation argument of the application"
779 )]
780 instantiation_argument: String,
781 #[graphql(desc = "The dependencies of the application being created")]
782 required_application_ids: Vec<ApplicationId>,
783 ) -> Result<ApplicationId, Error> {
784 self.apply_client_command(&chain_id, move |client| {
785 let parameters = parameters.as_bytes().to_vec();
786 let instantiation_argument = instantiation_argument.as_bytes().to_vec();
787 let required_application_ids = required_application_ids.clone();
788 async move {
789 let result = client
790 .create_application_untyped(
791 module_id,
792 parameters,
793 instantiation_argument,
794 required_application_ids,
795 )
796 .await
797 .map_err(Error::from)
798 .map(|outcome| outcome.map(|(application_id, _)| application_id));
799 (result, client)
800 }
801 })
802 .await
803 }
804}
805
806#[async_graphql::Object(cache_control(no_cache))]
807impl<C> QueryRoot<C>
808where
809 C: ClientContext + 'static,
810{
811 async fn chain(
812 &self,
813 chain_id: ChainId,
814 ) -> Result<ChainStateExtendedView<<C::Environment as linera_core::Environment>::Storage>, Error>
815 {
816 let client = self
817 .context
818 .lock()
819 .await
820 .make_chain_client(chain_id)
821 .await?;
822 let view = client.chain_state_view().await?;
823 Ok(ChainStateExtendedView::new(view))
824 }
825
826 async fn applications(&self, chain_id: ChainId) -> Result<Vec<ApplicationOverview>, Error> {
827 let client = self
828 .context
829 .lock()
830 .await
831 .make_chain_client(chain_id)
832 .await?;
833 let applications = client
834 .chain_state_view()
835 .await?
836 .execution_state
837 .list_applications()
838 .await?;
839
840 let overviews = applications
841 .into_iter()
842 .map(|(id, description)| ApplicationOverview::new(id, description, self.port, chain_id))
843 .collect();
844
845 Ok(overviews)
846 }
847
848 async fn chains(&self) -> Result<Chains, Error> {
849 Ok(Chains {
850 list: self
851 .context
852 .lock()
853 .await
854 .wallet()
855 .chain_ids()
856 .try_collect()
857 .await?,
858 default: self.default_chain,
859 })
860 }
861
862 async fn block(
863 &self,
864 hash: Option<CryptoHash>,
865 chain_id: ChainId,
866 ) -> Result<Option<Arc<ConfirmedBlock>>, Error> {
867 let client = self
868 .context
869 .lock()
870 .await
871 .make_chain_client(chain_id)
872 .await?;
873 let hash = match hash {
874 Some(hash) => Some(hash),
875 None => client.chain_info().await?.block_hash,
876 };
877 if let Some(hash) = hash {
878 Ok(Some(client.read_confirmed_block(hash).await?))
879 } else {
880 Ok(None)
881 }
882 }
883
884 async fn events_from_index(
885 &self,
886 chain_id: ChainId,
887 stream_id: StreamId,
888 start_index: u32,
889 ) -> Result<Vec<IndexAndEvent>, Error> {
890 Ok(self
891 .context
892 .lock()
893 .await
894 .make_chain_client(chain_id)
895 .await?
896 .events_from_index(stream_id, start_index)
897 .await?)
898 }
899
900 async fn blocks(
901 &self,
902 from: Option<CryptoHash>,
903 chain_id: ChainId,
904 limit: Option<u32>,
905 ) -> Result<Vec<Arc<ConfirmedBlock>>, Error> {
906 let client = self
907 .context
908 .lock()
909 .await
910 .make_chain_client(chain_id)
911 .await?;
912 let limit = limit.unwrap_or(10);
913 let from = match from {
914 Some(from) => Some(from),
915 None => client.chain_info().await?.block_hash,
916 };
917 let Some(from) = from else {
918 return Ok(vec![]);
919 };
920 let mut hash = Some(from);
921 let mut values = Vec::new();
922 for _ in 0..limit {
923 let Some(next_hash) = hash else {
924 break;
925 };
926 let value = client.read_confirmed_block(next_hash).await?;
927 hash = value.block().header.previous_block_hash;
928 values.push(value);
929 }
930 Ok(values)
931 }
932
933 async fn version(&self) -> linera_version::VersionInfo {
935 linera_version::VersionInfo::default()
936 }
937
938 async fn application_formats(
942 &self,
943 chain_id: ChainId,
944 formats_blob_hash: CryptoHash,
945 ) -> Result<Option<Vec<u8>>, Error> {
946 let client = self
947 .context
948 .lock()
949 .await
950 .make_chain_client(chain_id)
951 .await?;
952 let blob_id = linera_base::identifiers::BlobId::new(
953 formats_blob_hash,
954 linera_base::identifiers::BlobType::ApplicationFormats,
955 );
956 let blob = client.storage_client().read_blob(blob_id).await?;
957 Ok(blob.map(|b| b.bytes().to_vec()))
958 }
959}
960
961struct ChainStateViewExtension(ChainId);
965
966#[async_graphql::Object(cache_control(no_cache))]
967impl ChainStateViewExtension {
968 async fn chain_id(&self) -> ChainId {
969 self.0
970 }
971}
972
973#[derive(MergedObject)]
974struct ChainStateExtendedView<S: Storage>(ChainStateViewExtension, ReadOnlyChainStateView<S>);
975
976pub struct ReadOnlyChainStateView<S: Storage>(ChainStateViewReadGuard<S>);
979
980impl<S: Storage> ContainerType for ReadOnlyChainStateView<S>
981where
982 ChainStateView<S::Context>: ContainerType,
983{
984 async fn resolve_field(
985 &self,
986 context: &async_graphql::Context<'_>,
987 ) -> async_graphql::ServerResult<Option<async_graphql::Value>> {
988 self.0.resolve_field(context).await
989 }
990}
991
992impl<S: Storage> OutputType for ReadOnlyChainStateView<S>
993where
994 ChainStateView<S::Context>: OutputType,
995{
996 fn type_name() -> Cow<'static, str> {
997 ChainStateView::<S::Context>::type_name()
998 }
999
1000 fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String {
1001 ChainStateView::<S::Context>::create_type_info(registry)
1002 }
1003
1004 async fn resolve(
1005 &self,
1006 context: &async_graphql::ContextSelectionSet<'_>,
1007 field: &async_graphql::Positioned<async_graphql::parser::types::Field>,
1008 ) -> async_graphql::ServerResult<async_graphql::Value> {
1009 self.0.resolve(context, field).await
1010 }
1011}
1012
1013impl<S: Storage> ChainStateExtendedView<S> {
1014 fn new(view: ChainStateViewReadGuard<S>) -> Self {
1015 Self(
1016 ChainStateViewExtension(view.chain_id()),
1017 ReadOnlyChainStateView(view),
1018 )
1019 }
1020}
1021
1022#[derive(SimpleObject)]
1024pub struct ApplicationOverview {
1025 id: ApplicationId,
1026 description: ApplicationDescription,
1027 link: String,
1028}
1029
1030impl ApplicationOverview {
1031 fn new(
1032 id: ApplicationId,
1033 description: ApplicationDescription,
1034 port: NonZeroU16,
1035 chain_id: ChainId,
1036 ) -> Self {
1037 Self {
1038 id,
1039 description,
1040 link: format!(
1041 "http://localhost:{}/chains/{}/applications/{}",
1042 port.get(),
1043 chain_id,
1044 id
1045 ),
1046 }
1047 }
1048}
1049
1050pub enum NodeServiceSchema<C>
1052where
1053 C: ClientContext + 'static,
1054{
1055 Full(Schema<QueryRoot<C>, MutationRoot<C>, SubscriptionRoot<C>>),
1057 ReadOnly(Schema<QueryRoot<C>, EmptyMutation, SubscriptionRoot<C>>),
1059}
1060
1061impl<C> NodeServiceSchema<C>
1062where
1063 C: ClientContext,
1064{
1065 pub async fn execute(&self, request: impl Into<Request>) -> Response {
1067 match self {
1068 Self::Full(schema) => schema.execute(request).await,
1069 Self::ReadOnly(schema) => schema.execute(request).await,
1070 }
1071 }
1072
1073 pub fn sdl(&self) -> String {
1075 match self {
1076 Self::Full(schema) => schema.sdl(),
1077 Self::ReadOnly(schema) => schema.sdl(),
1078 }
1079 }
1080}
1081
1082impl<C> Clone for NodeServiceSchema<C>
1083where
1084 C: ClientContext,
1085{
1086 fn clone(&self) -> Self {
1087 match self {
1088 Self::Full(schema) => Self::Full(schema.clone()),
1089 Self::ReadOnly(schema) => Self::ReadOnly(schema.clone()),
1090 }
1091 }
1092}
1093
1094#[cfg(with_metrics)]
1095pub(crate) mod query_cache_metrics {
1096 use linera_base::prometheus_util::{register_int_counter_vec, register_int_gauge};
1097 use prometheus::{IntCounterVec, IntGauge};
1098
1099 linera_base::declare_metrics! {
1100 pub static QUERY_CACHE_HIT: IntCounterVec =
1101 register_int_counter_vec("query_response_cache_hit", "Query response cache hits", &[]);
1102
1103 pub static QUERY_CACHE_MISS: IntCounterVec =
1104 register_int_counter_vec(
1105 "query_response_cache_miss",
1106 "Query response cache misses",
1107 &[],
1108 );
1109
1110 pub static QUERY_CACHE_INVALIDATION: IntCounterVec =
1111 register_int_counter_vec(
1112 "query_response_cache_invalidation",
1113 "Query response cache invalidations (per chain)",
1114 &[],
1115 );
1116
1117 pub static QUERY_CACHE_ENTRIES: IntGauge =
1118 register_int_gauge(
1119 "query_response_cache_entries",
1120 "Current number of cached query responses across all chains",
1121 );
1122 }
1123}
1124
1125struct PerChainCache {
1128 lru: LruCache<(ApplicationId, Vec<u8>), Vec<u8>>,
1129 next_block_height: BlockHeight,
1130}
1131
1132struct QueryResponseCache {
1141 chains: papaya::HashMap<ChainId, StdMutex<PerChainCache>>,
1142 subscribed: papaya::HashSet<ChainId>,
1144 notification_sender: StdMutex<Option<tokio::sync::mpsc::UnboundedSender<Notification>>>,
1146 capacity_per_chain: std::num::NonZeroUsize,
1147}
1148
1149impl QueryResponseCache {
1150 fn new(capacity_per_chain: usize) -> Self {
1151 Self {
1152 chains: papaya::HashMap::new(),
1153 subscribed: papaya::HashSet::new(),
1154 notification_sender: StdMutex::new(None),
1155 capacity_per_chain: std::num::NonZeroUsize::new(capacity_per_chain)
1156 .expect("capacity must be > 0"),
1157 }
1158 }
1159
1160 fn set_notification_sender(&self, sender: tokio::sync::mpsc::UnboundedSender<Notification>) {
1162 *self
1163 .notification_sender
1164 .lock()
1165 .expect("sender mutex poisoned") = Some(sender);
1166 }
1167
1168 fn notification_sender(&self) -> Option<tokio::sync::mpsc::UnboundedSender<Notification>> {
1170 self.notification_sender
1171 .lock()
1172 .expect("sender mutex poisoned")
1173 .clone()
1174 }
1175
1176 fn mark_subscribed(&self, chain_id: ChainId) {
1178 self.subscribed.pin().insert(chain_id);
1179 }
1180
1181 fn needs_subscription(&self, chain_id: &ChainId) -> bool {
1183 !self.subscribed.pin().contains(chain_id)
1184 }
1185
1186 fn mark_all_subscribed(&self, chain_ids: &[ChainId]) {
1188 let pinned = self.subscribed.pin();
1189 for &chain_id in chain_ids {
1190 pinned.insert(chain_id);
1191 }
1192 }
1193
1194 fn get(&self, chain_id: ChainId, app_id: &ApplicationId, request: &[u8]) -> Option<Vec<u8>> {
1197 let pinned = self.chains.pin();
1198 let result = pinned.get(&chain_id).and_then(|mutex| {
1199 mutex
1200 .lock()
1201 .expect("LRU mutex poisoned")
1202 .lru
1203 .get(&(*app_id, request.to_vec()))
1204 .cloned()
1205 });
1206 #[cfg(with_metrics)]
1207 {
1208 let metric = if result.is_some() {
1209 &query_cache_metrics::QUERY_CACHE_HIT
1210 } else {
1211 &query_cache_metrics::QUERY_CACHE_MISS
1212 };
1213 metric.with_label_values(&[]).inc();
1214 }
1215 result
1216 }
1217
1218 fn insert(
1222 &self,
1223 chain_id: ChainId,
1224 app_id: ApplicationId,
1225 request: Vec<u8>,
1226 response: Vec<u8>,
1227 next_block_height: BlockHeight,
1228 ) {
1229 let pinned = self.chains.pin();
1230 let capacity = self.capacity_per_chain;
1231 let mutex = pinned.get_or_insert_with(chain_id, || {
1232 StdMutex::new(PerChainCache {
1233 lru: LruCache::new(capacity),
1234 next_block_height,
1235 })
1236 });
1237 let mut cache = mutex.lock().expect("LRU mutex poisoned");
1238 if next_block_height < cache.next_block_height {
1239 return; }
1241 if next_block_height > cache.next_block_height {
1245 debug!(
1246 "Unexpected query cache invalidation for chain {chain_id}:\
1247 {next_block_height} > {}",
1248 cache.next_block_height
1249 );
1250 #[cfg(with_metrics)]
1251 {
1252 #[expect(
1253 clippy::cast_possible_wrap,
1254 reason = "LRU cache size fits in i64 for any realistic cache"
1255 )]
1256 let cache_len = cache.lru.len() as i64;
1257 query_cache_metrics::QUERY_CACHE_ENTRIES.sub(cache_len);
1258 query_cache_metrics::QUERY_CACHE_INVALIDATION
1259 .with_label_values(&[])
1260 .inc();
1261 }
1262 cache.lru.clear();
1263 cache.next_block_height = next_block_height;
1264 }
1265 #[cfg(with_metrics)]
1266 let prev_len = cache.lru.len();
1267 cache.lru.put((app_id, request), response);
1268 #[cfg(with_metrics)]
1269 if cache.lru.len() != prev_len {
1270 query_cache_metrics::QUERY_CACHE_ENTRIES.inc();
1271 }
1272 }
1273
1274 fn invalidate_chain(&self, chain_id: &ChainId, next_block_height: BlockHeight) {
1277 let pinned = self.chains.pin();
1278 let capacity = self.capacity_per_chain;
1279 let mutex = pinned.get_or_insert_with(*chain_id, || {
1280 StdMutex::new(PerChainCache {
1281 lru: LruCache::new(capacity),
1282 next_block_height,
1283 })
1284 });
1285 let mut cache = mutex.lock().expect("LRU mutex poisoned");
1286 if next_block_height > cache.next_block_height {
1287 #[cfg(with_metrics)]
1288 {
1289 #[expect(
1290 clippy::cast_possible_wrap,
1291 reason = "LRU cache size fits in i64 for any realistic cache"
1292 )]
1293 let cache_len = cache.lru.len() as i64;
1294 query_cache_metrics::QUERY_CACHE_ENTRIES.sub(cache_len);
1295 query_cache_metrics::QUERY_CACHE_INVALIDATION
1296 .with_label_values(&[])
1297 .inc();
1298 }
1299 cache.lru.clear();
1300 cache.next_block_height = next_block_height;
1301 } else {
1302 debug!(
1303 "Query cache for chain {chain_id} was already invalidated:\
1304 {next_block_height} <= {}",
1305 cache.next_block_height
1306 );
1307 }
1308 }
1309}
1310
1311pub struct NodeService<C>
1314where
1315 C: ClientContext + 'static,
1316{
1317 config: ChainListenerConfig,
1318 port: NonZeroU16,
1319 #[cfg(with_metrics)]
1320 metrics_port: NonZeroU16,
1321 default_chain: Option<ChainId>,
1322 context: Arc<Mutex<C>>,
1323 read_only: bool,
1325 query_cache: Option<Arc<QueryResponseCache>>,
1327 query_subscriptions: Option<Arc<crate::query_subscription::QuerySubscriptionManager>>,
1328 cancellation_token: CancellationToken,
1329 enable_memory_profiling: bool,
1330 pause: bool,
1332}
1333
1334impl<C> Clone for NodeService<C>
1335where
1336 C: ClientContext + 'static,
1337{
1338 fn clone(&self) -> Self {
1339 Self {
1340 config: self.config.clone(),
1341 port: self.port,
1342 #[cfg(with_metrics)]
1343 metrics_port: self.metrics_port,
1344 default_chain: self.default_chain,
1345 context: Arc::clone(&self.context),
1346 read_only: self.read_only,
1347 query_cache: self.query_cache.clone(),
1348 query_subscriptions: self.query_subscriptions.clone(),
1349 cancellation_token: self.cancellation_token.clone(),
1350 enable_memory_profiling: self.enable_memory_profiling,
1351 pause: self.pause,
1352 }
1353 }
1354}
1355
1356impl<C> NodeService<C>
1357where
1358 C: ClientContext,
1359{
1360 #[expect(clippy::too_many_arguments)]
1366 pub fn new(
1367 config: ChainListenerConfig,
1368 port: NonZeroU16,
1369 #[cfg(with_metrics)] metrics_port: NonZeroU16,
1370 default_chain: Option<ChainId>,
1371 context: Arc<Mutex<C>>,
1372 read_only: bool,
1373 query_cache_size: Option<usize>,
1374 query_subscriptions: Option<Arc<crate::query_subscription::QuerySubscriptionManager>>,
1375 cancellation_token: CancellationToken,
1376 enable_memory_profiling: bool,
1377 pause: bool,
1378 ) -> Self {
1379 let query_cache = query_cache_size.map(|size| Arc::new(QueryResponseCache::new(size)));
1380 Self {
1381 config,
1382 port,
1383 #[cfg(with_metrics)]
1384 metrics_port,
1385 default_chain,
1386 context,
1387 read_only,
1388 query_cache,
1389 query_subscriptions,
1390 cancellation_token,
1391 enable_memory_profiling,
1392 pause,
1393 }
1394 }
1395
1396 #[cfg(with_metrics)]
1398 pub fn metrics_address(&self) -> SocketAddr {
1399 SocketAddr::from(([0, 0, 0, 0], self.metrics_port.get()))
1400 }
1401
1402 pub fn schema(&self) -> NodeServiceSchema<C> {
1404 let query = QueryRoot {
1405 context: Arc::clone(&self.context),
1406 port: self.port,
1407 default_chain: self.default_chain,
1408 };
1409 let subscription = SubscriptionRoot {
1410 context: Arc::clone(&self.context),
1411 query_subscriptions: self.query_subscriptions.clone(),
1412 cancellation_token: self.cancellation_token.clone(),
1413 };
1414
1415 if self.read_only {
1416 NodeServiceSchema::ReadOnly(Schema::build(query, EmptyMutation, subscription).finish())
1417 } else {
1418 NodeServiceSchema::Full(
1419 Schema::build(
1420 query,
1421 MutationRoot {
1422 context: Arc::clone(&self.context),
1423 },
1424 subscription,
1425 )
1426 .finish(),
1427 )
1428 }
1429 }
1430
1431 #[instrument(name = "node_service", level = "info", skip_all, fields(port = ?self.port))]
1433 pub async fn run(
1434 self,
1435 cancellation_token: CancellationToken,
1436 command_receiver: UnboundedReceiver<ListenerCommand>,
1437 ) -> Result<(), anyhow::Error> {
1438 let port = self.port.get();
1439 let index_handler = axum::routing::get(util::graphiql).post(Self::index_handler);
1440 let application_handler =
1441 axum::routing::get(util::graphiql).post(Self::application_handler);
1442
1443 #[cfg(with_metrics)]
1444 monitoring_server::start_metrics_with_profiling(
1445 self.metrics_address(),
1446 cancellation_token.clone(),
1447 self.enable_memory_profiling,
1448 crate::init_metrics,
1449 )
1450 .await;
1451
1452 let base_router = Router::new()
1453 .route("/", index_handler)
1454 .route(
1455 "/chains/{chain_id}/applications/{application_id}",
1456 application_handler,
1457 )
1458 .route("/ready", axum::routing::get(|| async { "ready!" }));
1459
1460 let app = match self.schema() {
1462 NodeServiceSchema::Full(schema) => {
1463 base_router.route_service("/ws", GraphQLSubscription::new(schema))
1464 }
1465 NodeServiceSchema::ReadOnly(schema) => {
1466 base_router.route_service("/ws", GraphQLSubscription::new(schema))
1467 }
1468 }
1469 .layer(Extension(self.clone()))
1470 .layer(CorsLayer::permissive());
1472
1473 info!("GraphiQL IDE: http://localhost:{}", port);
1474
1475 if let Some(cache) = &self.query_cache {
1477 let guard = self.context.lock().await;
1478 let chain_ids: Vec<ChainId> = guard.wallet().chain_ids().try_collect().await?;
1479 let (tx, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1480 guard.client().subscribe_extra(chain_ids.clone(), &tx);
1481 cache.mark_all_subscribed(&chain_ids);
1482 cache.set_notification_sender(tx);
1483 drop(guard);
1484 let cache = Arc::clone(cache);
1485 tokio::spawn(async move {
1486 while let Some(notification) = receiver.recv().await {
1487 if let Reason::NewBlock { height, .. } = notification.reason {
1488 let next_block_height = height
1489 .try_add_one()
1490 .expect("block height should not overflow");
1491 cache.invalidate_chain(¬ification.chain_id, next_block_height);
1492 }
1493 }
1494 });
1495 }
1496
1497 let tcp_listener =
1498 tokio::net::TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))).await?;
1499 let server = axum::serve(tcp_listener, app)
1500 .with_graceful_shutdown(cancellation_token.clone().cancelled_owned())
1501 .into_future();
1502
1503 if self.pause {
1504 info!("Running in paused mode: chain synchronization is disabled");
1505 server.await?;
1506 } else {
1507 let storage = self.context.lock().await.storage().clone();
1508 let chain_listener = ChainListener::new(
1509 self.config,
1510 self.context,
1511 storage,
1512 cancellation_token.clone(),
1513 command_receiver,
1514 true,
1515 )
1516 .run()
1517 .await?;
1518 let mut chain_listener = Box::pin(chain_listener).fuse();
1519 futures::select! {
1520 result = chain_listener => result?,
1521 result = Box::pin(server).fuse() => result?,
1522 };
1523 }
1524
1525 Ok(())
1526 }
1527
1528 async fn handle_service_request(
1530 &self,
1531 application_id: ApplicationId,
1532 request: Vec<u8>,
1533 chain_id: ChainId,
1534 block_hash: Option<CryptoHash>,
1535 ) -> Result<Vec<u8>, NodeServiceError> {
1536 let cache = block_hash
1538 .is_none()
1539 .then_some(self.query_cache.as_ref())
1540 .flatten();
1541
1542 if let Some(cache) = cache {
1544 if let Some(cached) = cache.get(chain_id, &application_id, &request) {
1545 return Ok(cached);
1546 }
1547 }
1548
1549 let (
1550 QueryOutcome {
1551 response,
1552 operations,
1553 },
1554 block_height,
1555 ) = self
1556 .query_user_application(application_id, request.clone(), chain_id, block_hash)
1557 .await?;
1558 if operations.is_empty() {
1559 if let Some(cache) = cache {
1560 if cache.needs_subscription(&chain_id) {
1562 if let Some(sender) = cache.notification_sender() {
1563 self.context
1564 .lock()
1565 .await
1566 .client()
1567 .subscribe_extra(vec![chain_id], &sender);
1568 cache.mark_subscribed(chain_id);
1569 }
1570 }
1571 cache.insert(
1572 chain_id,
1573 application_id,
1574 request,
1575 response.clone(),
1576 block_height,
1577 );
1578 }
1579 return Ok(response);
1580 }
1581
1582 if self.read_only {
1583 return Err(NodeServiceError::ReadOnlyModeOperationsNotAllowed);
1584 }
1585
1586 trace!("Query requested a new block with operations: {operations:?}");
1587 let client = self
1588 .context
1589 .lock()
1590 .await
1591 .make_chain_client(chain_id)
1592 .await?;
1593 let hash = loop {
1594 let timeout = match client
1595 .execute_operations(operations.clone(), vec![])
1596 .await?
1597 {
1598 ClientOutcome::Committed(certificate) => break certificate.hash(),
1599 ClientOutcome::Conflict(certificate) => {
1600 return Err(chain_client::Error::Conflict(certificate.hash()).into());
1601 }
1602 ClientOutcome::WaitForTimeout(timeout) => timeout,
1603 };
1604 let mut stream = client.subscribe().map_err(|_| {
1605 chain_client::Error::InternalError("Could not subscribe to the local node.")
1606 })?;
1607 util::wait_for_next_round(&mut stream, timeout).await;
1608 };
1609 let response = async_graphql::Response::new(hash.to_value());
1610 Ok(serde_json::to_vec(&response)?)
1611 }
1612
1613 async fn query_user_application(
1616 &self,
1617 application_id: ApplicationId,
1618 bytes: Vec<u8>,
1619 chain_id: ChainId,
1620 block_hash: Option<CryptoHash>,
1621 ) -> Result<(QueryOutcome<Vec<u8>>, BlockHeight), NodeServiceError> {
1622 let query = Query::User {
1623 application_id,
1624 bytes,
1625 };
1626 let client = self
1627 .context
1628 .lock()
1629 .await
1630 .make_chain_client(chain_id)
1631 .await?;
1632 let (
1633 QueryOutcome {
1634 response,
1635 operations,
1636 },
1637 next_block_height,
1638 ) = client.query_application(query, block_hash).await?;
1639 match response {
1640 QueryResponse::System(_) => {
1641 unreachable!("cannot get a system response for a user query")
1642 }
1643 QueryResponse::User(user_response_bytes) => Ok((
1644 QueryOutcome {
1645 response: user_response_bytes,
1646 operations,
1647 },
1648 next_block_height,
1649 )),
1650 }
1651 }
1652
1653 async fn index_handler(service: Extension<Self>, request: GraphQLRequest) -> GraphQLResponse {
1655 service
1656 .0
1657 .schema()
1658 .execute(request.into_inner())
1659 .await
1660 .into()
1661 }
1662
1663 async fn application_handler(
1667 Path((chain_id, application_id)): Path<(String, String)>,
1668 service: Extension<Self>,
1669 request: String,
1670 ) -> Result<Vec<u8>, NodeServiceError> {
1671 let chain_id: ChainId = chain_id.parse().map_err(NodeServiceError::InvalidChainId)?;
1672 let application_id: ApplicationId = application_id.parse()?;
1673
1674 debug!(
1675 %chain_id,
1676 %application_id,
1677 "processing request for application:\n{:?}",
1678 &request
1679 );
1680 let response = service
1681 .0
1682 .handle_service_request(application_id, request.into_bytes(), chain_id, None)
1683 .await?;
1684
1685 Ok(response)
1686 }
1687}
1688
1689#[cfg(test)]
1690mod tests {
1691 use linera_base::{
1692 crypto::CryptoHash,
1693 data_types::BlockHeight,
1694 identifiers::{ApplicationId, ChainId},
1695 };
1696
1697 use super::QueryResponseCache;
1698
1699 fn test_chain(n: u64) -> ChainId {
1700 ChainId(CryptoHash::test_hash(format!("chain-{n}")))
1701 }
1702
1703 fn test_app(n: u64) -> ApplicationId {
1704 ApplicationId::new(CryptoHash::test_hash(format!("app-{n}")))
1705 }
1706
1707 #[test]
1708 fn cache_hit_and_miss() {
1709 let cache = QueryResponseCache::new(100);
1710 let chain = test_chain(0);
1711 let app = test_app(0);
1712 let request = b"query { balance }".to_vec();
1713 let response = b"{ \"balance\": 42 }".to_vec();
1714
1715 assert!(cache.get(chain, &app, &request).is_none());
1717
1718 cache.insert(
1720 chain,
1721 app,
1722 request.clone(),
1723 response.clone(),
1724 BlockHeight(1),
1725 );
1726
1727 assert_eq!(cache.get(chain, &app, &request), Some(response));
1729 }
1730
1731 #[test]
1732 fn per_chain_isolation() {
1733 let cache = QueryResponseCache::new(100);
1734 let chain_a = test_chain(0);
1735 let chain_b = test_chain(1);
1736 let app = test_app(0);
1737 let request = b"q".to_vec();
1738 let response = b"r".to_vec();
1739
1740 cache.insert(
1741 chain_a,
1742 app,
1743 request.clone(),
1744 response.clone(),
1745 BlockHeight(1),
1746 );
1747
1748 cache.invalidate_chain(&chain_b, BlockHeight(1));
1750 assert_eq!(cache.get(chain_a, &app, &request), Some(response));
1751 }
1752
1753 #[test]
1754 fn invalidation_clears_all_entries() {
1755 let cache = QueryResponseCache::new(100);
1756 let chain = test_chain(0);
1757 let app = test_app(0);
1758
1759 cache.insert(chain, app, b"q1".to_vec(), b"r1".to_vec(), BlockHeight(1));
1760 cache.insert(chain, app, b"q2".to_vec(), b"r2".to_vec(), BlockHeight(1));
1761
1762 cache.invalidate_chain(&chain, BlockHeight(2));
1763 assert!(cache.get(chain, &app, b"q1").is_none());
1764 assert!(cache.get(chain, &app, b"q2").is_none());
1765 }
1766
1767 #[test]
1768 fn lru_eviction() {
1769 let cache = QueryResponseCache::new(2);
1770 let chain = test_chain(0);
1771 let app = test_app(0);
1772
1773 cache.insert(chain, app, b"q1".to_vec(), b"r1".to_vec(), BlockHeight(1));
1774 cache.insert(chain, app, b"q2".to_vec(), b"r2".to_vec(), BlockHeight(1));
1775 cache.insert(chain, app, b"q3".to_vec(), b"r3".to_vec(), BlockHeight(1));
1777
1778 assert!(cache.get(chain, &app, b"q1").is_none());
1779 assert!(cache.get(chain, &app, b"q2").is_some());
1780 assert!(cache.get(chain, &app, b"q3").is_some());
1781 }
1782
1783 #[test]
1784 fn stale_insert_rejected_after_invalidation() {
1785 let cache = QueryResponseCache::new(100);
1786 let chain = test_chain(0);
1787 let app = test_app(0);
1788
1789 cache.insert(chain, app, b"q0".to_vec(), b"r0".to_vec(), BlockHeight(3));
1791 let stale_height = BlockHeight(3);
1792
1793 cache.invalidate_chain(&chain, BlockHeight(4));
1795
1796 cache.insert(chain, app, b"q".to_vec(), b"stale".to_vec(), stale_height);
1798
1799 assert!(cache.get(chain, &app, b"q").is_none());
1801 }
1802}