Skip to main content

linera_core/
local_node.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2// Copyright (c) Zefchain Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::{BTreeMap, HashMap, HashSet, VecDeque},
7    sync::Arc,
8};
9
10use futures::{stream::FuturesUnordered, TryStreamExt as _};
11use linera_base::{
12    crypto::{CryptoHash, ValidatorPublicKey},
13    data_types::{ArithmeticError, Blob, BlockHeight},
14    identifiers::{BlobId, ChainId, EventId, StreamId},
15};
16use linera_chain::{
17    data_types::{BlockProposal, BundleExecutionPolicy, ProposedBlock},
18    types::{Block, ConfirmedBlockCertificate, GenericCertificate},
19};
20use linera_execution::{BlobState, Query, QueryOutcome, ResourceTracker};
21use linera_storage::{Arc as CacheArc, Storage};
22use linera_views::ViewError;
23use thiserror::Error;
24use tracing::{instrument, warn};
25
26use crate::{
27    chain_worker::ProcessConfirmedBlockMode,
28    data_types::{ChainInfo, ChainInfoQuery, ChainInfoResponse},
29    notifier::Notifier,
30    worker::{ProcessableCertificate, WorkerError, WorkerState},
31};
32
33/// A local node with a single worker, typically used by clients.
34pub struct LocalNode<S>
35where
36    S: Storage,
37{
38    state: WorkerState<S>,
39}
40
41/// A client to a local node.
42#[derive(Clone)]
43pub struct LocalNodeClient<S>
44where
45    S: Storage,
46{
47    node: Arc<LocalNode<S>>,
48}
49
50/// Error type for the operations on a local node.
51#[derive(Debug, Error, strum::IntoStaticStr)]
52#[allow(missing_docs)]
53pub enum LocalNodeError {
54    #[error(transparent)]
55    ArithmeticError(#[from] ArithmeticError),
56
57    #[error(transparent)]
58    ViewError(#[from] ViewError),
59
60    #[error("Worker operation failed: {0}")]
61    WorkerError(WorkerError),
62
63    #[error("The local node doesn't have an active chain {0}")]
64    InactiveChain(ChainId),
65
66    #[error("The chain info response received from the local node is invalid")]
67    InvalidChainInfoResponse,
68
69    #[error("Blobs not found: {0:?}")]
70    BlobsNotFound(Vec<BlobId>),
71
72    #[error("Events not found: {0:?}")]
73    EventsNotFound(Vec<EventId>),
74}
75
76impl LocalNodeError {
77    /// Returns the qualified error variant name for the `error_type` metric label,
78    /// delegating to [`WorkerError::error_type`] for wrapped worker errors.
79    pub fn error_type(&self) -> String {
80        match self {
81            LocalNodeError::WorkerError(worker_error) => worker_error.error_type(),
82            other => {
83                let variant: &'static str = other.into();
84                format!("LocalNodeError::{variant}")
85            }
86        }
87    }
88}
89
90impl From<WorkerError> for LocalNodeError {
91    fn from(error: WorkerError) -> Self {
92        match error {
93            WorkerError::BlobsNotFound(blob_ids) => LocalNodeError::BlobsNotFound(blob_ids),
94            WorkerError::EventsNotFound(event_ids) => LocalNodeError::EventsNotFound(event_ids),
95            error => LocalNodeError::WorkerError(error),
96        }
97    }
98}
99
100impl<S> LocalNodeClient<S>
101where
102    S: Storage + Clone + 'static,
103{
104    #[instrument(level = "trace", skip_all)]
105    pub async fn handle_block_proposal(
106        &self,
107        proposal: BlockProposal,
108    ) -> Result<ChainInfoResponse, LocalNodeError> {
109        // In local nodes, cross-chain actions will be handled internally, so we discard them.
110        let (response, _actions) = Box::pin(self.node.state.handle_block_proposal(proposal)).await;
111        Ok(response?)
112    }
113
114    #[instrument(level = "trace", skip_all)]
115    pub async fn handle_certificate<T>(
116        &self,
117        certificate: GenericCertificate<T>,
118        notifier: &impl Notifier,
119    ) -> Result<ChainInfoResponse, LocalNodeError>
120    where
121        T: ProcessableCertificate,
122    {
123        Ok(Box::pin(
124            self.node
125                .state
126                .fully_handle_certificate_with_notifications(certificate, notifier),
127        )
128        .await?)
129    }
130
131    /// Same as [`Self::handle_certificate`] but for a confirmed block certificate
132    /// and with an explicit [`ProcessConfirmedBlockMode`]. The generic variant
133    /// always uses [`ProcessConfirmedBlockMode::Auto`].
134    #[instrument(level = "trace", skip_all)]
135    pub async fn handle_confirmed_certificate(
136        &self,
137        certificate: ConfirmedBlockCertificate,
138        mode: ProcessConfirmedBlockMode,
139        notifier: &impl Notifier,
140    ) -> Result<ChainInfoResponse, LocalNodeError> {
141        Ok(Box::pin(
142            self.node
143                .state
144                .fully_handle_confirmed_certificate_with_notifications(certificate, mode, notifier),
145        )
146        .await?)
147    }
148
149    #[instrument(level = "trace", skip_all)]
150    pub async fn handle_chain_info_query(
151        &self,
152        query: ChainInfoQuery,
153    ) -> Result<ChainInfoResponse, LocalNodeError> {
154        // In local nodes, cross-chain actions will be handled internally, so we discard them.
155        let (response, _actions) = self.node.state.handle_chain_info_query(query).await?;
156        Ok(response)
157    }
158
159    #[instrument(level = "trace", skip_all)]
160    pub fn new(state: WorkerState<S>) -> Self {
161        Self {
162            node: Arc::new(LocalNode { state }),
163        }
164    }
165
166    #[instrument(level = "trace", skip_all)]
167    pub(crate) fn storage_client(&self) -> S {
168        self.node.state.storage_client().clone()
169    }
170
171    /// Executes a block with a policy for handling bundle failures.
172    ///
173    /// Returns the modified block (bundles may be rejected/removed based on the policy),
174    /// the executed block, chain info response, and resource tracker.
175    #[instrument(level = "trace", skip_all)]
176    pub async fn stage_block_execution(
177        &self,
178        block: ProposedBlock,
179        round: Option<u32>,
180        published_blobs: Vec<Blob>,
181        policy: BundleExecutionPolicy,
182    ) -> Result<
183        (
184            ProposedBlock,
185            Block,
186            ChainInfoResponse,
187            ResourceTracker,
188            HashSet<ChainId>,
189        ),
190        LocalNodeError,
191    > {
192        Ok(self
193            .node
194            .state
195            .stage_block_execution(block, round, published_blobs, policy)
196            .await?)
197    }
198
199    /// Reads blobs from storage.
200    pub async fn read_blobs_from_storage(
201        &self,
202        blob_ids: &[BlobId],
203    ) -> Result<Option<Vec<CacheArc<Blob>>>, LocalNodeError> {
204        let storage = self.storage_client();
205        Ok(storage.read_blobs(blob_ids).await?.into_iter().collect())
206    }
207
208    /// Reads blob states from storage.
209    pub async fn read_blob_states_from_storage(
210        &self,
211        blob_ids: &[BlobId],
212    ) -> Result<Vec<BlobState>, LocalNodeError> {
213        let storage = self.storage_client();
214        let mut blobs_not_found = Vec::new();
215        let mut blob_states = Vec::new();
216        for (blob_state, blob_id) in storage
217            .read_blob_states(blob_ids)
218            .await?
219            .into_iter()
220            .zip(blob_ids)
221        {
222            match blob_state {
223                None => blobs_not_found.push(*blob_id),
224                Some(blob_state) => blob_states.push(blob_state),
225            }
226        }
227        if !blobs_not_found.is_empty() {
228            return Err(LocalNodeError::BlobsNotFound(blobs_not_found));
229        }
230        Ok(blob_states)
231    }
232
233    /// Looks for the specified blobs in the local chain manager's locking blobs.
234    /// Returns `Ok(None)` if any of the blobs is not found.
235    pub async fn get_locking_blobs(
236        &self,
237        blob_ids: impl IntoIterator<Item = &BlobId>,
238        chain_id: ChainId,
239    ) -> Result<Option<Vec<Blob>>, LocalNodeError> {
240        let blob_ids_vec: Vec<_> = blob_ids.into_iter().copied().collect();
241        Ok(self
242            .node
243            .state
244            .get_locking_blobs(chain_id, blob_ids_vec)
245            .await?)
246    }
247
248    /// Writes the given blobs to storage if there is an appropriate blob state.
249    pub async fn store_blobs(&self, blobs: &[Blob]) -> Result<(), LocalNodeError> {
250        let storage = self.storage_client();
251        storage.maybe_write_blobs(blobs).await?;
252        Ok(())
253    }
254
255    pub async fn handle_pending_blobs(
256        &self,
257        chain_id: ChainId,
258        blobs: Vec<Blob>,
259    ) -> Result<(), LocalNodeError> {
260        for blob in blobs {
261            self.node.state.handle_pending_blob(chain_id, blob).await?;
262        }
263        Ok(())
264    }
265
266    /// Returns a read-only view of the [`ChainStateView`] of a chain referenced by its
267    /// [`ChainId`].
268    ///
269    /// The returned view holds a lock on the chain state, which prevents the local node from
270    /// changing the state of that chain.
271    #[instrument(level = "trace", skip(self))]
272    pub async fn chain_state_view(
273        &self,
274        chain_id: ChainId,
275    ) -> Result<crate::worker::ChainStateViewReadGuard<S>, LocalNodeError> {
276        Ok(self.node.state.chain_state_view(chain_id).await?)
277    }
278
279    #[instrument(level = "trace", skip(self))]
280    pub(crate) async fn chain_info(
281        &self,
282        chain_id: ChainId,
283    ) -> Result<Box<ChainInfo>, LocalNodeError> {
284        let query = ChainInfoQuery::new(chain_id);
285        Ok(self.handle_chain_info_query(query).await?.info)
286    }
287
288    #[instrument(level = "trace", skip(self, query))]
289    pub async fn query_application(
290        &self,
291        chain_id: ChainId,
292        query: Query,
293        block_hash: Option<CryptoHash>,
294    ) -> Result<(QueryOutcome, BlockHeight), LocalNodeError> {
295        let result = self
296            .node
297            .state
298            .query_application(chain_id, query, block_hash)
299            .await?;
300        Ok(result)
301    }
302
303    /// Handles any pending local cross-chain requests.
304    ///
305    /// Does not initialize the sender chain's execution state, so it is safe to
306    /// call even when the sender's `ChainDescription` blob is not in local storage.
307    /// Previously this went through `handle_chain_info_query`, which unconditionally
308    /// initialized the worker and therefore forced a `ChainDescription` download on
309    /// every call.
310    #[instrument(level = "trace", skip(self, notifier))]
311    pub async fn retry_pending_cross_chain_requests(
312        &self,
313        sender_chain: ChainId,
314        notifier: &impl Notifier,
315    ) -> Result<(), LocalNodeError> {
316        let actions = self
317            .node
318            .state
319            .cross_chain_network_actions(sender_chain)
320            .await?;
321        let mut requests = VecDeque::from_iter(actions.cross_chain_requests);
322        while let Some(request) = requests.pop_front() {
323            let new_actions = self.node.state.handle_cross_chain_request(request).await?;
324            notifier.notify(&new_actions.notifications);
325            requests.extend(new_actions.cross_chain_requests);
326        }
327        Ok(())
328    }
329
330    /// Given a list of chain IDs, returns a map that assigns to each of them the next block
331    /// height to schedule, i.e. the lowest block height for which we haven't added the messages
332    /// to `receiver_id` to the outbox yet.
333    pub async fn next_outbox_heights(
334        &self,
335        chain_ids: impl IntoIterator<Item = &ChainId>,
336        receiver_id: ChainId,
337    ) -> Result<BTreeMap<ChainId, BlockHeight>, LocalNodeError> {
338        let futures = chain_ids
339            .into_iter()
340            .map(|chain_id| async move {
341                let (next_block_height, next_height_to_schedule) = match self
342                    .get_tip_state_and_outbox_info(*chain_id, receiver_id)
343                    .await
344                {
345                    Ok(info) => info,
346                    Err(LocalNodeError::BlobsNotFound(_) | LocalNodeError::InactiveChain(_)) => {
347                        return Ok((*chain_id, BlockHeight::ZERO))
348                    }
349                    Err(err) => Err(err)?,
350                };
351                let next_height = if let Some(scheduled_height) = next_height_to_schedule {
352                    next_block_height.max(scheduled_height)
353                } else {
354                    next_block_height
355                };
356                Ok::<_, LocalNodeError>((*chain_id, next_height))
357            })
358            .collect::<FuturesUnordered<_>>();
359        futures.try_collect().await
360    }
361
362    pub async fn update_received_certificate_trackers(
363        &self,
364        chain_id: ChainId,
365        new_trackers: BTreeMap<ValidatorPublicKey, u64>,
366    ) -> Result<(), LocalNodeError> {
367        self.node
368            .state
369            .update_received_certificate_trackers(chain_id, new_trackers)
370            .await?;
371        Ok(())
372    }
373
374    pub async fn get_preprocessed_block_hashes(
375        &self,
376        chain_id: ChainId,
377        start: BlockHeight,
378        end: BlockHeight,
379    ) -> Result<Vec<linera_base::crypto::CryptoHash>, LocalNodeError> {
380        Ok(self
381            .node
382            .state
383            .get_preprocessed_block_hashes(chain_id, start, end)
384            .await?)
385    }
386
387    pub async fn get_inbox_next_height(
388        &self,
389        chain_id: ChainId,
390        origin: ChainId,
391    ) -> Result<BlockHeight, LocalNodeError> {
392        Ok(self
393            .node
394            .state
395            .get_inbox_next_height(chain_id, origin)
396            .await?)
397    }
398
399    /// Gets block hashes for the given heights.
400    pub async fn get_block_hashes(
401        &self,
402        chain_id: ChainId,
403        heights: Vec<BlockHeight>,
404    ) -> Result<Vec<CryptoHash>, LocalNodeError> {
405        Ok(self.node.state.get_block_hashes(chain_id, heights).await?)
406    }
407
408    /// Gets proposed blobs from the manager for specified blob IDs.
409    pub async fn get_proposed_blobs(
410        &self,
411        chain_id: ChainId,
412        blob_ids: Vec<BlobId>,
413    ) -> Result<Vec<Blob>, LocalNodeError> {
414        Ok(self
415            .node
416            .state
417            .get_proposed_blobs(chain_id, blob_ids)
418            .await?)
419    }
420
421    /// Gets event subscriptions from the chain.
422    pub async fn get_event_subscriptions(
423        &self,
424        chain_id: ChainId,
425    ) -> Result<crate::worker::EventSubscriptionsResult, LocalNodeError> {
426        Ok(self.node.state.get_event_subscriptions(chain_id).await?)
427    }
428
429    /// Gets the `next_expected_events` indices for the given streams.
430    pub async fn next_expected_events(
431        &self,
432        chain_id: ChainId,
433        stream_ids: Vec<StreamId>,
434    ) -> Result<BTreeMap<StreamId, u32>, LocalNodeError> {
435        Ok(self
436            .node
437            .state
438            .next_expected_events(chain_id, stream_ids)
439            .await?)
440    }
441
442    /// Gets the stream event count for a stream.
443    pub async fn get_stream_event_count(
444        &self,
445        chain_id: ChainId,
446        stream_id: StreamId,
447    ) -> Result<Option<u32>, LocalNodeError> {
448        Ok(self
449            .node
450            .state
451            .get_stream_event_count(chain_id, stream_id)
452            .await?)
453    }
454
455    /// Gets received certificate trackers.
456    pub async fn get_received_certificate_trackers(
457        &self,
458        chain_id: ChainId,
459    ) -> Result<HashMap<ValidatorPublicKey, u64>, LocalNodeError> {
460        Ok(self
461            .node
462            .state
463            .get_received_certificate_trackers(chain_id)
464            .await?)
465    }
466
467    /// Gets tip state and outbox info for next_outbox_heights calculation.
468    pub async fn get_tip_state_and_outbox_info(
469        &self,
470        chain_id: ChainId,
471        receiver_id: ChainId,
472    ) -> Result<(BlockHeight, Option<BlockHeight>), LocalNodeError> {
473        Ok(self
474            .node
475            .state
476            .get_tip_state_and_outbox_info(chain_id, receiver_id)
477            .await?)
478    }
479
480    /// Gets the next height to preprocess.
481    pub async fn get_next_height_to_preprocess(
482        &self,
483        chain_id: ChainId,
484    ) -> Result<BlockHeight, LocalNodeError> {
485        Ok(self
486            .node
487            .state
488            .get_next_height_to_preprocess(chain_id)
489            .await?)
490    }
491
492    /// Gets the chain manager's seed for leader election.
493    pub async fn get_manager_seed(&self, chain_id: ChainId) -> Result<u64, LocalNodeError> {
494        Ok(self.node.state.get_manager_seed(chain_id).await?)
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn error_type_delegates_to_worker_error() {
504        assert_eq!(
505            LocalNodeError::WorkerError(WorkerError::InvalidOwner).error_type(),
506            "WorkerError::InvalidOwner"
507        );
508    }
509
510    #[test]
511    fn error_type_falls_back_to_local_node_variant() {
512        assert_eq!(
513            LocalNodeError::InvalidChainInfoResponse.error_type(),
514            "LocalNodeError::InvalidChainInfoResponse"
515        );
516    }
517}