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