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 ) -> Result<ChainId, Error> {
448 let ownership = ChainOwnership::single(owner);
449 let balance = balance.unwrap_or(Amount::ZERO);
450 let description = self
451 .apply_client_command(&chain_id, move |client| {
452 let ownership = ownership.clone();
453 async move {
454 let result = client
455 .open_chain(ownership, ApplicationPermissions::default(), balance)
456 .await
457 .map_err(Error::from)
458 .map(|outcome| outcome.map(|(chain_id, _)| chain_id));
459 (result, client)
460 }
461 })
462 .await?;
463 Ok(description.id())
464 }
465
466 #[expect(clippy::too_many_arguments)]
468 async fn open_multi_owner_chain(
469 &self,
470 #[graphql(desc = "The chain paying for the creation of the new chain.")] chain_id: ChainId,
471 #[graphql(desc = "Permissions for applications on the new chain")]
472 application_permissions: Option<ApplicationPermissions>,
473 #[graphql(desc = "The owners of the chain")] owners: Vec<AccountOwner>,
474 #[graphql(desc = "The weights of the owners")] weights: Option<Vec<u64>>,
475 #[graphql(desc = "The number of multi-leader rounds")] multi_leader_rounds: Option<u32>,
476 #[graphql(desc = "The balance of the chain. Zero if `None`")] balance: Option<Amount>,
477 #[graphql(desc = "The duration of the fast round, in milliseconds; default: no timeout")]
478 fast_round_ms: Option<u64>,
479 #[graphql(
480 desc = "The duration of the first single-leader and all multi-leader rounds",
481 default = 10_000
482 )]
483 base_timeout_ms: u64,
484 #[graphql(
485 desc = "The number of milliseconds by which the timeout increases after each \
486 single-leader round",
487 default = 1_000
488 )]
489 timeout_increment_ms: u64,
490 #[graphql(
491 desc = "The age of an incoming tracked or protected message after which the \
492 validators start transitioning the chain to fallback mode, in milliseconds.",
493 default = 86_400_000
494 )]
495 fallback_duration_ms: u64,
496 ) -> Result<ChainId, Error> {
497 let owners = if let Some(weights) = weights {
498 if weights.len() != owners.len() {
499 return Err(Error::new(format!(
500 "There are {} owners but {} weights.",
501 owners.len(),
502 weights.len()
503 )));
504 }
505 owners.into_iter().zip(weights).collect::<Vec<_>>()
506 } else {
507 owners
508 .into_iter()
509 .zip(iter::repeat(100))
510 .collect::<Vec<_>>()
511 };
512 let multi_leader_rounds = multi_leader_rounds.unwrap_or(u32::MAX);
513 let timeout_config = TimeoutConfig {
514 fast_round_duration: fast_round_ms.map(TimeDelta::from_millis),
515 base_timeout: TimeDelta::from_millis(base_timeout_ms),
516 timeout_increment: TimeDelta::from_millis(timeout_increment_ms),
517 fallback_duration: TimeDelta::from_millis(fallback_duration_ms),
518 };
519 let ownership = ChainOwnership::multiple(owners, multi_leader_rounds, timeout_config);
520 let balance = balance.unwrap_or(Amount::ZERO);
521 let description = self
522 .apply_client_command(&chain_id, move |client| {
523 let ownership = ownership.clone();
524 let application_permissions = application_permissions.clone().unwrap_or_default();
525 async move {
526 let result = client
527 .open_chain(ownership, application_permissions, balance)
528 .await
529 .map_err(Error::from)
530 .map(|outcome| outcome.map(|(chain_id, _)| chain_id));
531 (result, client)
532 }
533 })
534 .await?;
535 Ok(description.id())
536 }
537
538 async fn close_chain(
540 &self,
541 #[graphql(desc = "The chain being closed.")] chain_id: ChainId,
542 ) -> Result<Option<CryptoHash>, Error> {
543 let maybe_cert = self
544 .apply_client_command(&chain_id, |client| async move {
545 let result = client.close_chain().await.map_err(Error::from);
546 (result, client)
547 })
548 .await?;
549 Ok(maybe_cert.as_ref().map(|cert| cert.hash()))
550 }
551
552 async fn change_owner(
554 &self,
555 #[graphql(desc = "The chain whose ownership changes")] chain_id: ChainId,
556 #[graphql(desc = "The new single owner of the chain")] new_owner: AccountOwner,
557 ) -> Result<CryptoHash, Error> {
558 let new_ownership = ChainOwnership::single_super(new_owner);
559 let operation = SystemOperation::ChangeOwnership {
560 super_owners: vec![new_owner],
561 owners: Vec::new(),
562 first_leader: None,
563 multi_leader_rounds: 5,
564 open_multi_leader_rounds: false,
565 timeout_config: TimeoutConfig::default(),
566 };
567 let hash = self.execute_system_operation(operation, chain_id).await?;
568 self.maybe_auto_assign_preferred_owner(chain_id, &new_ownership)
569 .await?;
570 Ok(hash)
571 }
572
573 #[expect(clippy::too_many_arguments)]
575 async fn change_multiple_owners(
576 &self,
577 #[graphql(desc = "The chain whose ownership changes")] chain_id: ChainId,
578 #[graphql(desc = "The new list of owners of the chain")] new_owners: Vec<AccountOwner>,
579 #[graphql(desc = "The new list of weights of the owners")] new_weights: Vec<u64>,
580 #[graphql(desc = "The multi-leader round of the chain")] multi_leader_rounds: u32,
581 #[graphql(
582 desc = "Whether multi-leader rounds are unrestricted, that is not limited to chain owners."
583 )]
584 open_multi_leader_rounds: bool,
585 #[graphql(desc = "The leader of the first single-leader round. \
586 If not set, this is random like other rounds.")]
587 first_leader: Option<AccountOwner>,
588 #[graphql(desc = "The duration of the fast round, in milliseconds; default: no timeout")]
589 fast_round_ms: Option<u64>,
590 #[graphql(
591 desc = "The duration of the first single-leader and all multi-leader rounds",
592 default = 10_000
593 )]
594 base_timeout_ms: u64,
595 #[graphql(
596 desc = "The number of milliseconds by which the timeout increases after each \
597 single-leader round",
598 default = 1_000
599 )]
600 timeout_increment_ms: u64,
601 #[graphql(
602 desc = "The age of an incoming tracked or protected message after which the \
603 validators start transitioning the chain to fallback mode, in milliseconds.",
604 default = 86_400_000
605 )]
606 fallback_duration_ms: u64,
607 ) -> Result<CryptoHash, Error> {
608 let timeout_config = TimeoutConfig {
609 fast_round_duration: fast_round_ms.map(TimeDelta::from_millis),
610 base_timeout: TimeDelta::from_millis(base_timeout_ms),
611 timeout_increment: TimeDelta::from_millis(timeout_increment_ms),
612 fallback_duration: TimeDelta::from_millis(fallback_duration_ms),
613 };
614 let owners = new_owners.into_iter().zip(new_weights).collect::<Vec<_>>();
615 let new_ownership = ChainOwnership {
616 super_owners: BTreeSet::new(),
617 owners: owners.iter().cloned().collect(),
618 first_leader,
619 multi_leader_rounds,
620 open_multi_leader_rounds,
621 timeout_config: timeout_config.clone(),
622 };
623 let operation = SystemOperation::ChangeOwnership {
624 super_owners: Vec::new(),
625 owners,
626 first_leader,
627 multi_leader_rounds,
628 open_multi_leader_rounds,
629 timeout_config,
630 };
631 let hash = self.execute_system_operation(operation, chain_id).await?;
632 self.maybe_auto_assign_preferred_owner(chain_id, &new_ownership)
633 .await?;
634 Ok(hash)
635 }
636
637 #[expect(clippy::too_many_arguments)]
639 async fn change_application_permissions(
640 &self,
641 #[graphql(desc = "The chain whose permissions are being changed")] chain_id: ChainId,
642 #[graphql(
643 desc = "These applications are allowed to manage the chain: close it, change \
644 application permissions, and change ownership."
645 )]
646 manage_chain: Vec<ApplicationId>,
647 #[graphql(
648 desc = "If this is `None`, all system operations and application operations are allowed.
649If it is `Some`, only operations from the specified applications are allowed,
650and no system operations."
651 )]
652 execute_operations: Option<Vec<ApplicationId>>,
653 #[graphql(
654 desc = "At least one operation or incoming message from each of these applications must occur in every block."
655 )]
656 mandatory_applications: Vec<ApplicationId>,
657 #[graphql(
658 desc = "These applications are allowed to perform calls to services as oracles."
659 )]
660 call_service_as_oracle: Option<Vec<ApplicationId>>,
661 #[graphql(desc = "These applications are allowed to perform HTTP requests.")]
662 make_http_requests: Option<Vec<ApplicationId>>,
663 ) -> Result<CryptoHash, Error> {
664 let operation = SystemOperation::ChangeApplicationPermissions(ApplicationPermissions {
665 execute_operations,
666 mandatory_applications,
667 manage_chain,
668 call_service_as_oracle,
669 make_http_requests,
670 });
671 self.execute_system_operation(operation, chain_id).await
672 }
673
674 async fn create_committee(
678 &self,
679 chain_id: ChainId,
680 committee: Committee,
681 ) -> Result<CryptoHash, Error> {
682 Ok(self
683 .apply_client_command(&chain_id, move |client| {
684 let committee = committee.clone();
685 async move {
686 let result = client
687 .stage_new_committee(committee)
688 .await
689 .map_err(Error::from);
690 (result, client)
691 }
692 })
693 .await?
694 .hash())
695 }
696
697 async fn remove_committee(&self, chain_id: ChainId, epoch: Epoch) -> Result<CryptoHash, Error> {
701 let operation = SystemOperation::Admin(AdminOperation::RemoveCommittee { epoch });
702 self.execute_system_operation(operation, chain_id).await
703 }
704
705 async fn publish_module(
709 &self,
710 #[graphql(desc = "The chain publishing the module")] chain_id: ChainId,
711 #[graphql(desc = "The bytecode of the contract code")] contract: Bytecode,
712 #[graphql(desc = "The bytecode of the service code (only relevant for WebAssembly)")]
713 service: Bytecode,
714 #[graphql(desc = "The virtual machine being used (either Wasm or Evm)")]
715 vm_runtime: VmRuntime,
716 #[graphql(desc = "Optional BCS-encoded `Formats` description bytes")] formats: Option<
717 Vec<u8>,
718 >,
719 ) -> Result<ModuleId, Error> {
720 self.apply_client_command(&chain_id, move |client| {
721 let contract = contract.clone();
722 let service = service.clone();
723 let formats = formats.clone();
724 async move {
725 let result = client
726 .publish_module(contract, service, vm_runtime, formats)
727 .await
728 .map_err(Error::from)
729 .map(|outcome| outcome.map(|(module_id, _)| module_id));
730 (result, client)
731 }
732 })
733 .await
734 }
735
736 async fn publish_data_blob(
738 &self,
739 #[graphql(desc = "The chain paying for the blob publication")] chain_id: ChainId,
740 #[graphql(desc = "The content of the data blob being created")] bytes: Vec<u8>,
741 ) -> Result<CryptoHash, Error> {
742 self.apply_client_command(&chain_id, |client| {
743 let bytes = bytes.clone();
744 async move {
745 let result = client.publish_data_blob(bytes).await.map_err(Error::from);
746 (result, client)
747 }
748 })
749 .await
750 .map(|_| CryptoHash::new(&BlobContent::new_data(bytes)))
751 }
752
753 async fn create_application(
755 &self,
756 #[graphql(desc = "The chain paying for the creation of the application")] chain_id: ChainId,
757 #[graphql(desc = "The module ID of the application being created")] module_id: ModuleId,
758 #[graphql(desc = "The JSON serialization of the parameters of the application")]
759 parameters: String,
760 #[graphql(
761 desc = "The JSON serialization of the instantiation argument of the application"
762 )]
763 instantiation_argument: String,
764 #[graphql(desc = "The dependencies of the application being created")]
765 required_application_ids: Vec<ApplicationId>,
766 ) -> Result<ApplicationId, Error> {
767 self.apply_client_command(&chain_id, move |client| {
768 let parameters = parameters.as_bytes().to_vec();
769 let instantiation_argument = instantiation_argument.as_bytes().to_vec();
770 let required_application_ids = required_application_ids.clone();
771 async move {
772 let result = client
773 .create_application_untyped(
774 module_id,
775 parameters,
776 instantiation_argument,
777 required_application_ids,
778 )
779 .await
780 .map_err(Error::from)
781 .map(|outcome| outcome.map(|(application_id, _)| application_id));
782 (result, client)
783 }
784 })
785 .await
786 }
787}
788
789#[async_graphql::Object(cache_control(no_cache))]
790impl<C> QueryRoot<C>
791where
792 C: ClientContext + 'static,
793{
794 async fn chain(
795 &self,
796 chain_id: ChainId,
797 ) -> Result<ChainStateExtendedView<<C::Environment as linera_core::Environment>::Storage>, Error>
798 {
799 let client = self
800 .context
801 .lock()
802 .await
803 .make_chain_client(chain_id)
804 .await?;
805 let view = client.chain_state_view().await?;
806 Ok(ChainStateExtendedView::new(view))
807 }
808
809 async fn applications(&self, chain_id: ChainId) -> Result<Vec<ApplicationOverview>, Error> {
810 let client = self
811 .context
812 .lock()
813 .await
814 .make_chain_client(chain_id)
815 .await?;
816 let applications = client
817 .chain_state_view()
818 .await?
819 .execution_state
820 .list_applications()
821 .await?;
822
823 let overviews = applications
824 .into_iter()
825 .map(|(id, description)| ApplicationOverview::new(id, description, self.port, chain_id))
826 .collect();
827
828 Ok(overviews)
829 }
830
831 async fn chains(&self) -> Result<Chains, Error> {
832 Ok(Chains {
833 list: self
834 .context
835 .lock()
836 .await
837 .wallet()
838 .chain_ids()
839 .try_collect()
840 .await?,
841 default: self.default_chain,
842 })
843 }
844
845 async fn block(
846 &self,
847 hash: Option<CryptoHash>,
848 chain_id: ChainId,
849 ) -> Result<Option<Arc<ConfirmedBlock>>, Error> {
850 let client = self
851 .context
852 .lock()
853 .await
854 .make_chain_client(chain_id)
855 .await?;
856 let hash = match hash {
857 Some(hash) => Some(hash),
858 None => client.chain_info().await?.block_hash,
859 };
860 if let Some(hash) = hash {
861 Ok(Some(client.read_confirmed_block(hash).await?))
862 } else {
863 Ok(None)
864 }
865 }
866
867 async fn events_from_index(
868 &self,
869 chain_id: ChainId,
870 stream_id: StreamId,
871 start_index: u32,
872 ) -> Result<Vec<IndexAndEvent>, Error> {
873 Ok(self
874 .context
875 .lock()
876 .await
877 .make_chain_client(chain_id)
878 .await?
879 .events_from_index(stream_id, start_index)
880 .await?)
881 }
882
883 async fn blocks(
884 &self,
885 from: Option<CryptoHash>,
886 chain_id: ChainId,
887 limit: Option<u32>,
888 ) -> Result<Vec<Arc<ConfirmedBlock>>, Error> {
889 let client = self
890 .context
891 .lock()
892 .await
893 .make_chain_client(chain_id)
894 .await?;
895 let limit = limit.unwrap_or(10);
896 let from = match from {
897 Some(from) => Some(from),
898 None => client.chain_info().await?.block_hash,
899 };
900 let Some(from) = from else {
901 return Ok(vec![]);
902 };
903 let mut hash = Some(from);
904 let mut values = Vec::new();
905 for _ in 0..limit {
906 let Some(next_hash) = hash else {
907 break;
908 };
909 let value = client.read_confirmed_block(next_hash).await?;
910 hash = value.block().header.previous_block_hash;
911 values.push(value);
912 }
913 Ok(values)
914 }
915
916 async fn version(&self) -> linera_version::VersionInfo {
918 linera_version::VersionInfo::default()
919 }
920
921 async fn application_formats(
925 &self,
926 chain_id: ChainId,
927 formats_blob_hash: CryptoHash,
928 ) -> Result<Option<Vec<u8>>, Error> {
929 let client = self
930 .context
931 .lock()
932 .await
933 .make_chain_client(chain_id)
934 .await?;
935 let blob_id = linera_base::identifiers::BlobId::new(
936 formats_blob_hash,
937 linera_base::identifiers::BlobType::ApplicationFormats,
938 );
939 let blob = client.storage_client().read_blob(blob_id).await?;
940 Ok(blob.map(|b| b.bytes().to_vec()))
941 }
942}
943
944struct ChainStateViewExtension(ChainId);
948
949#[async_graphql::Object(cache_control(no_cache))]
950impl ChainStateViewExtension {
951 async fn chain_id(&self) -> ChainId {
952 self.0
953 }
954}
955
956#[derive(MergedObject)]
957struct ChainStateExtendedView<S: Storage>(ChainStateViewExtension, ReadOnlyChainStateView<S>);
958
959pub struct ReadOnlyChainStateView<S: Storage>(ChainStateViewReadGuard<S>);
962
963impl<S: Storage> ContainerType for ReadOnlyChainStateView<S>
964where
965 ChainStateView<S::Context>: ContainerType,
966{
967 async fn resolve_field(
968 &self,
969 context: &async_graphql::Context<'_>,
970 ) -> async_graphql::ServerResult<Option<async_graphql::Value>> {
971 self.0.resolve_field(context).await
972 }
973}
974
975impl<S: Storage> OutputType for ReadOnlyChainStateView<S>
976where
977 ChainStateView<S::Context>: OutputType,
978{
979 fn type_name() -> Cow<'static, str> {
980 ChainStateView::<S::Context>::type_name()
981 }
982
983 fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String {
984 ChainStateView::<S::Context>::create_type_info(registry)
985 }
986
987 async fn resolve(
988 &self,
989 context: &async_graphql::ContextSelectionSet<'_>,
990 field: &async_graphql::Positioned<async_graphql::parser::types::Field>,
991 ) -> async_graphql::ServerResult<async_graphql::Value> {
992 self.0.resolve(context, field).await
993 }
994}
995
996impl<S: Storage> ChainStateExtendedView<S> {
997 fn new(view: ChainStateViewReadGuard<S>) -> Self {
998 Self(
999 ChainStateViewExtension(view.chain_id()),
1000 ReadOnlyChainStateView(view),
1001 )
1002 }
1003}
1004
1005#[derive(SimpleObject)]
1007pub struct ApplicationOverview {
1008 id: ApplicationId,
1009 description: ApplicationDescription,
1010 link: String,
1011}
1012
1013impl ApplicationOverview {
1014 fn new(
1015 id: ApplicationId,
1016 description: ApplicationDescription,
1017 port: NonZeroU16,
1018 chain_id: ChainId,
1019 ) -> Self {
1020 Self {
1021 id,
1022 description,
1023 link: format!(
1024 "http://localhost:{}/chains/{}/applications/{}",
1025 port.get(),
1026 chain_id,
1027 id
1028 ),
1029 }
1030 }
1031}
1032
1033pub enum NodeServiceSchema<C>
1035where
1036 C: ClientContext + 'static,
1037{
1038 Full(Schema<QueryRoot<C>, MutationRoot<C>, SubscriptionRoot<C>>),
1040 ReadOnly(Schema<QueryRoot<C>, EmptyMutation, SubscriptionRoot<C>>),
1042}
1043
1044impl<C> NodeServiceSchema<C>
1045where
1046 C: ClientContext,
1047{
1048 pub async fn execute(&self, request: impl Into<Request>) -> Response {
1050 match self {
1051 Self::Full(schema) => schema.execute(request).await,
1052 Self::ReadOnly(schema) => schema.execute(request).await,
1053 }
1054 }
1055
1056 pub fn sdl(&self) -> String {
1058 match self {
1059 Self::Full(schema) => schema.sdl(),
1060 Self::ReadOnly(schema) => schema.sdl(),
1061 }
1062 }
1063}
1064
1065impl<C> Clone for NodeServiceSchema<C>
1066where
1067 C: ClientContext,
1068{
1069 fn clone(&self) -> Self {
1070 match self {
1071 Self::Full(schema) => Self::Full(schema.clone()),
1072 Self::ReadOnly(schema) => Self::ReadOnly(schema.clone()),
1073 }
1074 }
1075}
1076
1077#[cfg(with_metrics)]
1078mod query_cache_metrics {
1079 use std::sync::LazyLock;
1080
1081 use linera_base::prometheus_util::{register_int_counter_vec, register_int_gauge};
1082 use prometheus::{IntCounterVec, IntGauge};
1083
1084 pub static QUERY_CACHE_HIT: LazyLock<IntCounterVec> = LazyLock::new(|| {
1085 register_int_counter_vec("query_response_cache_hit", "Query response cache hits", &[])
1086 });
1087
1088 pub static QUERY_CACHE_MISS: LazyLock<IntCounterVec> = LazyLock::new(|| {
1089 register_int_counter_vec(
1090 "query_response_cache_miss",
1091 "Query response cache misses",
1092 &[],
1093 )
1094 });
1095
1096 pub static QUERY_CACHE_INVALIDATION: LazyLock<IntCounterVec> = LazyLock::new(|| {
1097 register_int_counter_vec(
1098 "query_response_cache_invalidation",
1099 "Query response cache invalidations (per chain)",
1100 &[],
1101 )
1102 });
1103
1104 pub static QUERY_CACHE_ENTRIES: LazyLock<IntGauge> = LazyLock::new(|| {
1105 register_int_gauge(
1106 "query_response_cache_entries",
1107 "Current number of cached query responses across all chains",
1108 )
1109 });
1110}
1111
1112struct PerChainCache {
1115 lru: LruCache<(ApplicationId, Vec<u8>), Vec<u8>>,
1116 next_block_height: BlockHeight,
1117}
1118
1119struct QueryResponseCache {
1128 chains: papaya::HashMap<ChainId, StdMutex<PerChainCache>>,
1129 subscribed: papaya::HashSet<ChainId>,
1131 notification_sender: StdMutex<Option<tokio::sync::mpsc::UnboundedSender<Notification>>>,
1133 capacity_per_chain: std::num::NonZeroUsize,
1134}
1135
1136impl QueryResponseCache {
1137 fn new(capacity_per_chain: usize) -> Self {
1138 Self {
1139 chains: papaya::HashMap::new(),
1140 subscribed: papaya::HashSet::new(),
1141 notification_sender: StdMutex::new(None),
1142 capacity_per_chain: std::num::NonZeroUsize::new(capacity_per_chain)
1143 .expect("capacity must be > 0"),
1144 }
1145 }
1146
1147 fn set_notification_sender(&self, sender: tokio::sync::mpsc::UnboundedSender<Notification>) {
1149 *self
1150 .notification_sender
1151 .lock()
1152 .expect("sender mutex poisoned") = Some(sender);
1153 }
1154
1155 fn notification_sender(&self) -> Option<tokio::sync::mpsc::UnboundedSender<Notification>> {
1157 self.notification_sender
1158 .lock()
1159 .expect("sender mutex poisoned")
1160 .clone()
1161 }
1162
1163 fn mark_subscribed(&self, chain_id: ChainId) {
1165 self.subscribed.pin().insert(chain_id);
1166 }
1167
1168 fn needs_subscription(&self, chain_id: &ChainId) -> bool {
1170 !self.subscribed.pin().contains(chain_id)
1171 }
1172
1173 fn mark_all_subscribed(&self, chain_ids: &[ChainId]) {
1175 let pinned = self.subscribed.pin();
1176 for &chain_id in chain_ids {
1177 pinned.insert(chain_id);
1178 }
1179 }
1180
1181 fn get(&self, chain_id: ChainId, app_id: &ApplicationId, request: &[u8]) -> Option<Vec<u8>> {
1184 let pinned = self.chains.pin();
1185 let result = pinned.get(&chain_id).and_then(|mutex| {
1186 mutex
1187 .lock()
1188 .expect("LRU mutex poisoned")
1189 .lru
1190 .get(&(*app_id, request.to_vec()))
1191 .cloned()
1192 });
1193 #[cfg(with_metrics)]
1194 {
1195 let metric = if result.is_some() {
1196 &query_cache_metrics::QUERY_CACHE_HIT
1197 } else {
1198 &query_cache_metrics::QUERY_CACHE_MISS
1199 };
1200 metric.with_label_values(&[]).inc();
1201 }
1202 result
1203 }
1204
1205 fn insert(
1209 &self,
1210 chain_id: ChainId,
1211 app_id: ApplicationId,
1212 request: Vec<u8>,
1213 response: Vec<u8>,
1214 next_block_height: BlockHeight,
1215 ) {
1216 let pinned = self.chains.pin();
1217 let capacity = self.capacity_per_chain;
1218 let mutex = pinned.get_or_insert_with(chain_id, || {
1219 StdMutex::new(PerChainCache {
1220 lru: LruCache::new(capacity),
1221 next_block_height,
1222 })
1223 });
1224 let mut cache = mutex.lock().expect("LRU mutex poisoned");
1225 if next_block_height < cache.next_block_height {
1226 return; }
1228 if next_block_height > cache.next_block_height {
1232 debug!(
1233 "Unexpected query cache invalidation for chain {chain_id}:\
1234 {next_block_height} > {}",
1235 cache.next_block_height
1236 );
1237 #[cfg(with_metrics)]
1238 {
1239 #[expect(
1240 clippy::cast_possible_wrap,
1241 reason = "LRU cache size fits in i64 for any realistic cache"
1242 )]
1243 let cache_len = cache.lru.len() as i64;
1244 query_cache_metrics::QUERY_CACHE_ENTRIES.sub(cache_len);
1245 query_cache_metrics::QUERY_CACHE_INVALIDATION
1246 .with_label_values(&[])
1247 .inc();
1248 }
1249 cache.lru.clear();
1250 cache.next_block_height = next_block_height;
1251 }
1252 #[cfg(with_metrics)]
1253 let prev_len = cache.lru.len();
1254 cache.lru.put((app_id, request), response);
1255 #[cfg(with_metrics)]
1256 if cache.lru.len() != prev_len {
1257 query_cache_metrics::QUERY_CACHE_ENTRIES.inc();
1258 }
1259 }
1260
1261 fn invalidate_chain(&self, chain_id: &ChainId, next_block_height: BlockHeight) {
1264 let pinned = self.chains.pin();
1265 let capacity = self.capacity_per_chain;
1266 let mutex = pinned.get_or_insert_with(*chain_id, || {
1267 StdMutex::new(PerChainCache {
1268 lru: LruCache::new(capacity),
1269 next_block_height,
1270 })
1271 });
1272 let mut cache = mutex.lock().expect("LRU mutex poisoned");
1273 if next_block_height > cache.next_block_height {
1274 #[cfg(with_metrics)]
1275 {
1276 #[expect(
1277 clippy::cast_possible_wrap,
1278 reason = "LRU cache size fits in i64 for any realistic cache"
1279 )]
1280 let cache_len = cache.lru.len() as i64;
1281 query_cache_metrics::QUERY_CACHE_ENTRIES.sub(cache_len);
1282 query_cache_metrics::QUERY_CACHE_INVALIDATION
1283 .with_label_values(&[])
1284 .inc();
1285 }
1286 cache.lru.clear();
1287 cache.next_block_height = next_block_height;
1288 } else {
1289 debug!(
1290 "Query cache for chain {chain_id} was already invalidated:\
1291 {next_block_height} <= {}",
1292 cache.next_block_height
1293 );
1294 }
1295 }
1296}
1297
1298pub struct NodeService<C>
1301where
1302 C: ClientContext + 'static,
1303{
1304 config: ChainListenerConfig,
1305 port: NonZeroU16,
1306 #[cfg(with_metrics)]
1307 metrics_port: NonZeroU16,
1308 default_chain: Option<ChainId>,
1309 context: Arc<Mutex<C>>,
1310 read_only: bool,
1312 query_cache: Option<Arc<QueryResponseCache>>,
1314 query_subscriptions: Option<Arc<crate::query_subscription::QuerySubscriptionManager>>,
1315 cancellation_token: CancellationToken,
1316 enable_memory_profiling: bool,
1317 pause: bool,
1319}
1320
1321impl<C> Clone for NodeService<C>
1322where
1323 C: ClientContext + 'static,
1324{
1325 fn clone(&self) -> Self {
1326 Self {
1327 config: self.config.clone(),
1328 port: self.port,
1329 #[cfg(with_metrics)]
1330 metrics_port: self.metrics_port,
1331 default_chain: self.default_chain,
1332 context: Arc::clone(&self.context),
1333 read_only: self.read_only,
1334 query_cache: self.query_cache.clone(),
1335 query_subscriptions: self.query_subscriptions.clone(),
1336 cancellation_token: self.cancellation_token.clone(),
1337 enable_memory_profiling: self.enable_memory_profiling,
1338 pause: self.pause,
1339 }
1340 }
1341}
1342
1343impl<C> NodeService<C>
1344where
1345 C: ClientContext,
1346{
1347 #[expect(clippy::too_many_arguments)]
1353 pub fn new(
1354 config: ChainListenerConfig,
1355 port: NonZeroU16,
1356 #[cfg(with_metrics)] metrics_port: NonZeroU16,
1357 default_chain: Option<ChainId>,
1358 context: Arc<Mutex<C>>,
1359 read_only: bool,
1360 query_cache_size: Option<usize>,
1361 query_subscriptions: Option<Arc<crate::query_subscription::QuerySubscriptionManager>>,
1362 cancellation_token: CancellationToken,
1363 enable_memory_profiling: bool,
1364 pause: bool,
1365 ) -> Self {
1366 let query_cache = query_cache_size.map(|size| Arc::new(QueryResponseCache::new(size)));
1367 Self {
1368 config,
1369 port,
1370 #[cfg(with_metrics)]
1371 metrics_port,
1372 default_chain,
1373 context,
1374 read_only,
1375 query_cache,
1376 query_subscriptions,
1377 cancellation_token,
1378 enable_memory_profiling,
1379 pause,
1380 }
1381 }
1382
1383 #[cfg(with_metrics)]
1385 pub fn metrics_address(&self) -> SocketAddr {
1386 SocketAddr::from(([0, 0, 0, 0], self.metrics_port.get()))
1387 }
1388
1389 pub fn schema(&self) -> NodeServiceSchema<C> {
1391 let query = QueryRoot {
1392 context: Arc::clone(&self.context),
1393 port: self.port,
1394 default_chain: self.default_chain,
1395 };
1396 let subscription = SubscriptionRoot {
1397 context: Arc::clone(&self.context),
1398 query_subscriptions: self.query_subscriptions.clone(),
1399 cancellation_token: self.cancellation_token.clone(),
1400 };
1401
1402 if self.read_only {
1403 NodeServiceSchema::ReadOnly(Schema::build(query, EmptyMutation, subscription).finish())
1404 } else {
1405 NodeServiceSchema::Full(
1406 Schema::build(
1407 query,
1408 MutationRoot {
1409 context: Arc::clone(&self.context),
1410 },
1411 subscription,
1412 )
1413 .finish(),
1414 )
1415 }
1416 }
1417
1418 #[instrument(name = "node_service", level = "info", skip_all, fields(port = ?self.port))]
1420 pub async fn run(
1421 self,
1422 cancellation_token: CancellationToken,
1423 command_receiver: UnboundedReceiver<ListenerCommand>,
1424 ) -> Result<(), anyhow::Error> {
1425 let port = self.port.get();
1426 let index_handler = axum::routing::get(util::graphiql).post(Self::index_handler);
1427 let application_handler =
1428 axum::routing::get(util::graphiql).post(Self::application_handler);
1429
1430 #[cfg(with_metrics)]
1431 monitoring_server::start_metrics_with_profiling(
1432 self.metrics_address(),
1433 cancellation_token.clone(),
1434 self.enable_memory_profiling,
1435 )
1436 .await;
1437
1438 let base_router = Router::new()
1439 .route("/", index_handler)
1440 .route(
1441 "/chains/{chain_id}/applications/{application_id}",
1442 application_handler,
1443 )
1444 .route("/ready", axum::routing::get(|| async { "ready!" }));
1445
1446 let app = match self.schema() {
1448 NodeServiceSchema::Full(schema) => {
1449 base_router.route_service("/ws", GraphQLSubscription::new(schema))
1450 }
1451 NodeServiceSchema::ReadOnly(schema) => {
1452 base_router.route_service("/ws", GraphQLSubscription::new(schema))
1453 }
1454 }
1455 .layer(Extension(self.clone()))
1456 .layer(CorsLayer::permissive());
1458
1459 info!("GraphiQL IDE: http://localhost:{}", port);
1460
1461 if let Some(cache) = &self.query_cache {
1463 let guard = self.context.lock().await;
1464 let chain_ids: Vec<ChainId> = guard.wallet().chain_ids().try_collect().await?;
1465 let (tx, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1466 guard.client().subscribe_extra(chain_ids.clone(), &tx);
1467 cache.mark_all_subscribed(&chain_ids);
1468 cache.set_notification_sender(tx);
1469 drop(guard);
1470 let cache = Arc::clone(cache);
1471 tokio::spawn(async move {
1472 while let Some(notification) = receiver.recv().await {
1473 if let Reason::NewBlock { height, .. } = notification.reason {
1474 let next_block_height = height
1475 .try_add_one()
1476 .expect("block height should not overflow");
1477 cache.invalidate_chain(¬ification.chain_id, next_block_height);
1478 }
1479 }
1480 });
1481 }
1482
1483 let tcp_listener =
1484 tokio::net::TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))).await?;
1485 let server = axum::serve(tcp_listener, app)
1486 .with_graceful_shutdown(cancellation_token.clone().cancelled_owned())
1487 .into_future();
1488
1489 if self.pause {
1490 info!("Running in paused mode: chain synchronization is disabled");
1491 server.await?;
1492 } else {
1493 let storage = self.context.lock().await.storage().clone();
1494 let chain_listener = ChainListener::new(
1495 self.config,
1496 self.context,
1497 storage,
1498 cancellation_token.clone(),
1499 command_receiver,
1500 true,
1501 )
1502 .run()
1503 .await?;
1504 let mut chain_listener = Box::pin(chain_listener).fuse();
1505 futures::select! {
1506 result = chain_listener => result?,
1507 result = Box::pin(server).fuse() => result?,
1508 };
1509 }
1510
1511 Ok(())
1512 }
1513
1514 async fn handle_service_request(
1516 &self,
1517 application_id: ApplicationId,
1518 request: Vec<u8>,
1519 chain_id: ChainId,
1520 block_hash: Option<CryptoHash>,
1521 ) -> Result<Vec<u8>, NodeServiceError> {
1522 let cache = block_hash
1524 .is_none()
1525 .then_some(self.query_cache.as_ref())
1526 .flatten();
1527
1528 if let Some(cache) = cache {
1530 if let Some(cached) = cache.get(chain_id, &application_id, &request) {
1531 return Ok(cached);
1532 }
1533 }
1534
1535 let (
1536 QueryOutcome {
1537 response,
1538 operations,
1539 },
1540 block_height,
1541 ) = self
1542 .query_user_application(application_id, request.clone(), chain_id, block_hash)
1543 .await?;
1544 if operations.is_empty() {
1545 if let Some(cache) = cache {
1546 if cache.needs_subscription(&chain_id) {
1548 if let Some(sender) = cache.notification_sender() {
1549 self.context
1550 .lock()
1551 .await
1552 .client()
1553 .subscribe_extra(vec![chain_id], &sender);
1554 cache.mark_subscribed(chain_id);
1555 }
1556 }
1557 cache.insert(
1558 chain_id,
1559 application_id,
1560 request,
1561 response.clone(),
1562 block_height,
1563 );
1564 }
1565 return Ok(response);
1566 }
1567
1568 if self.read_only {
1569 return Err(NodeServiceError::ReadOnlyModeOperationsNotAllowed);
1570 }
1571
1572 trace!("Query requested a new block with operations: {operations:?}");
1573 let client = self
1574 .context
1575 .lock()
1576 .await
1577 .make_chain_client(chain_id)
1578 .await?;
1579 let hash = loop {
1580 let timeout = match client
1581 .execute_operations(operations.clone(), vec![])
1582 .await?
1583 {
1584 ClientOutcome::Committed(certificate) => break certificate.hash(),
1585 ClientOutcome::Conflict(certificate) => {
1586 return Err(chain_client::Error::Conflict(certificate.hash()).into());
1587 }
1588 ClientOutcome::WaitForTimeout(timeout) => timeout,
1589 };
1590 let mut stream = client.subscribe().map_err(|_| {
1591 chain_client::Error::InternalError("Could not subscribe to the local node.")
1592 })?;
1593 util::wait_for_next_round(&mut stream, timeout).await;
1594 };
1595 let response = async_graphql::Response::new(hash.to_value());
1596 Ok(serde_json::to_vec(&response)?)
1597 }
1598
1599 async fn query_user_application(
1602 &self,
1603 application_id: ApplicationId,
1604 bytes: Vec<u8>,
1605 chain_id: ChainId,
1606 block_hash: Option<CryptoHash>,
1607 ) -> Result<(QueryOutcome<Vec<u8>>, BlockHeight), NodeServiceError> {
1608 let query = Query::User {
1609 application_id,
1610 bytes,
1611 };
1612 let client = self
1613 .context
1614 .lock()
1615 .await
1616 .make_chain_client(chain_id)
1617 .await?;
1618 let (
1619 QueryOutcome {
1620 response,
1621 operations,
1622 },
1623 next_block_height,
1624 ) = client.query_application(query, block_hash).await?;
1625 match response {
1626 QueryResponse::System(_) => {
1627 unreachable!("cannot get a system response for a user query")
1628 }
1629 QueryResponse::User(user_response_bytes) => Ok((
1630 QueryOutcome {
1631 response: user_response_bytes,
1632 operations,
1633 },
1634 next_block_height,
1635 )),
1636 }
1637 }
1638
1639 async fn index_handler(service: Extension<Self>, request: GraphQLRequest) -> GraphQLResponse {
1641 service
1642 .0
1643 .schema()
1644 .execute(request.into_inner())
1645 .await
1646 .into()
1647 }
1648
1649 async fn application_handler(
1653 Path((chain_id, application_id)): Path<(String, String)>,
1654 service: Extension<Self>,
1655 request: String,
1656 ) -> Result<Vec<u8>, NodeServiceError> {
1657 let chain_id: ChainId = chain_id.parse().map_err(NodeServiceError::InvalidChainId)?;
1658 let application_id: ApplicationId = application_id.parse()?;
1659
1660 debug!(
1661 %chain_id,
1662 %application_id,
1663 "processing request for application:\n{:?}",
1664 &request
1665 );
1666 let response = service
1667 .0
1668 .handle_service_request(application_id, request.into_bytes(), chain_id, None)
1669 .await?;
1670
1671 Ok(response)
1672 }
1673}
1674
1675#[cfg(test)]
1676mod tests {
1677 use linera_base::{
1678 crypto::CryptoHash,
1679 data_types::BlockHeight,
1680 identifiers::{ApplicationId, ChainId},
1681 };
1682
1683 use super::QueryResponseCache;
1684
1685 fn test_chain(n: u64) -> ChainId {
1686 ChainId(CryptoHash::test_hash(format!("chain-{n}")))
1687 }
1688
1689 fn test_app(n: u64) -> ApplicationId {
1690 ApplicationId::new(CryptoHash::test_hash(format!("app-{n}")))
1691 }
1692
1693 #[test]
1694 fn cache_hit_and_miss() {
1695 let cache = QueryResponseCache::new(100);
1696 let chain = test_chain(0);
1697 let app = test_app(0);
1698 let request = b"query { balance }".to_vec();
1699 let response = b"{ \"balance\": 42 }".to_vec();
1700
1701 assert!(cache.get(chain, &app, &request).is_none());
1703
1704 cache.insert(
1706 chain,
1707 app,
1708 request.clone(),
1709 response.clone(),
1710 BlockHeight(1),
1711 );
1712
1713 assert_eq!(cache.get(chain, &app, &request), Some(response));
1715 }
1716
1717 #[test]
1718 fn per_chain_isolation() {
1719 let cache = QueryResponseCache::new(100);
1720 let chain_a = test_chain(0);
1721 let chain_b = test_chain(1);
1722 let app = test_app(0);
1723 let request = b"q".to_vec();
1724 let response = b"r".to_vec();
1725
1726 cache.insert(
1727 chain_a,
1728 app,
1729 request.clone(),
1730 response.clone(),
1731 BlockHeight(1),
1732 );
1733
1734 cache.invalidate_chain(&chain_b, BlockHeight(1));
1736 assert_eq!(cache.get(chain_a, &app, &request), Some(response));
1737 }
1738
1739 #[test]
1740 fn invalidation_clears_all_entries() {
1741 let cache = QueryResponseCache::new(100);
1742 let chain = test_chain(0);
1743 let app = test_app(0);
1744
1745 cache.insert(chain, app, b"q1".to_vec(), b"r1".to_vec(), BlockHeight(1));
1746 cache.insert(chain, app, b"q2".to_vec(), b"r2".to_vec(), BlockHeight(1));
1747
1748 cache.invalidate_chain(&chain, BlockHeight(2));
1749 assert!(cache.get(chain, &app, b"q1").is_none());
1750 assert!(cache.get(chain, &app, b"q2").is_none());
1751 }
1752
1753 #[test]
1754 fn lru_eviction() {
1755 let cache = QueryResponseCache::new(2);
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 cache.insert(chain, app, b"q3".to_vec(), b"r3".to_vec(), BlockHeight(1));
1763
1764 assert!(cache.get(chain, &app, b"q1").is_none());
1765 assert!(cache.get(chain, &app, b"q2").is_some());
1766 assert!(cache.get(chain, &app, b"q3").is_some());
1767 }
1768
1769 #[test]
1770 fn stale_insert_rejected_after_invalidation() {
1771 let cache = QueryResponseCache::new(100);
1772 let chain = test_chain(0);
1773 let app = test_app(0);
1774
1775 cache.insert(chain, app, b"q0".to_vec(), b"r0".to_vec(), BlockHeight(3));
1777 let stale_height = BlockHeight(3);
1778
1779 cache.invalidate_chain(&chain, BlockHeight(4));
1781
1782 cache.insert(chain, app, b"q".to_vec(), b"stale".to_vec(), stale_height);
1784
1785 assert!(cache.get(chain, &app, b"q").is_none());
1787 }
1788}