Skip to main content

linera_execution/
execution.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{collections::BTreeMap, vec};
5
6use allocative::Allocative;
7use futures::{FutureExt, StreamExt};
8use linera_base::{
9    crypto::CryptoHash,
10    data_types::{BlobContent, BlockHeight, StreamUpdate},
11    identifiers::{AccountOwner, BlobId, StreamId},
12    time::Instant,
13    visit_allocative_simple,
14};
15use linera_views::{
16    batch::Batch,
17    common::HasherOutput,
18    context::Context,
19    key_value_store_view::KeyValueStoreView,
20    map_view::MapView,
21    reentrant_collection_view::HashedReentrantCollectionView,
22    register_view::RegisterView,
23    views::{ClonableView, HashableView, Hasher as _, ReplaceContext, View},
24    ViewError,
25};
26#[cfg(with_testing)]
27use {
28    crate::{
29        ResourceControlPolicy, ResourceTracker, TestExecutionRuntimeContext, UserContractCode,
30    },
31    linera_base::data_types::Blob,
32    linera_views::context::MemoryContext,
33    std::sync::Arc,
34};
35
36use super::{execution_state_actor::ExecutionRequest, runtime::ServiceRuntimeRequest};
37use crate::{
38    execution_state_actor::ExecutionStateActor, resources::ResourceController,
39    system::SystemExecutionStateView, ApplicationDescription, ApplicationId, BcsHashable,
40    Deserialize, ExecutionError, ExecutionRuntimeContext, JsVec, MessageContext, OperationContext,
41    ProcessStreamsContext, Query, QueryContext, QueryOutcome, Serialize, ServiceSyncRuntime,
42    Timestamp, TransactionTracker, FLAG_HISTORICAL_HASH, FLAG_HISTORICAL_HASH_SHADOW,
43    FLAG_ZERO_HASH,
44};
45
46/// A view accessing the execution state of a chain.
47#[derive(Debug, ClonableView, View, Allocative)]
48#[allocative(bound = "C")]
49pub struct ExecutionStateView<C> {
50    /// System application.
51    pub system: SystemExecutionStateView<C>,
52    /// User applications.
53    pub users: HashedReentrantCollectionView<C, ApplicationId, KeyValueStoreView<C>>,
54    /// The number of events in the streams that this chain is writing to.
55    pub stream_event_counts: MapView<C, StreamId, u32>,
56    /// Rolling *historical* hash of the execution state, used when `FLAG_HISTORICAL_HASH`
57    /// (or its shadow variant) is active. `None` until the first block after activation, which
58    /// seeds it from the full content hash (via [`HashableView`]); subsequent blocks extend it
59    /// cheaply from the batch that block persists. Appended last so that existing chains load it
60    /// as `None` without moving the storage keys of the other fields, and deliberately excluded
61    /// from the hand-written [`HashableView`] impl below (so it never hashes itself).
62    #[allocative(visit = visit_allocative_simple)]
63    pub historical_hash: RegisterView<C, Option<HasherOutput>>,
64}
65
66/// Selects how [`ExecutionStateView::crypto_hash_mut`] computes the execution-state hash,
67/// based on the feature flags in the current committee's policy.
68#[derive(Clone, Copy, Debug)]
69enum HashingMode {
70    /// Report an all-zeros hash without hashing the state (`FLAG_ZERO_HASH`).
71    Zero,
72    /// Full content hash of the state on every block (the default, legacy behavior).
73    Legacy,
74    /// Rolling historical hash. In shadow mode (`enforce == false`) the hash is computed,
75    /// persisted and logged but the reported hash stays all-zeros.
76    Historical { enforce: bool },
77}
78
79/// Hashes only the content fields of [`ExecutionStateView`], mirroring the `HashableView`
80/// derive but omitting the appended `historical_hash` register. Keeping this by hand means the
81/// rolling hash never feeds back into the content hash that seeds it.
82impl<C> HashableView for ExecutionStateView<C>
83where
84    C: Context + Clone + 'static,
85    C::Extra: ExecutionRuntimeContext,
86{
87    type Hasher = linera_views::sha3::Sha3_256;
88
89    async fn hash_mut(&mut self) -> Result<HasherOutput, ViewError> {
90        use std::io::Write as _;
91        let Self {
92            system,
93            users,
94            stream_event_counts,
95            historical_hash: _,
96        } = self;
97        let mut hasher = Self::Hasher::default();
98        hasher.write_all(system.hash_mut().await?.as_ref())?;
99        hasher.write_all(users.hash_mut().await?.as_ref())?;
100        hasher.write_all(stream_event_counts.hash_mut().await?.as_ref())?;
101        Ok(hasher.finalize())
102    }
103
104    async fn hash(&self) -> Result<HasherOutput, ViewError> {
105        use std::io::Write as _;
106        let Self {
107            system,
108            users,
109            stream_event_counts,
110            historical_hash: _,
111        } = self;
112        let mut hasher = Self::Hasher::default();
113        hasher.write_all(system.hash().await?.as_ref())?;
114        hasher.write_all(users.hash().await?.as_ref())?;
115        hasher.write_all(stream_event_counts.hash().await?.as_ref())?;
116        Ok(hasher.finalize())
117    }
118}
119
120impl<C> ExecutionStateView<C>
121where
122    C: Context + Clone + 'static,
123    C::Extra: ExecutionRuntimeContext,
124{
125    /// Computes the cryptographic hash of the execution state.
126    pub async fn crypto_hash_mut(&mut self) -> Result<CryptoHash, ViewError> {
127        match self.hashing_mode().await? {
128            HashingMode::Zero => {
129                self.reset_historical_hash();
130                Ok(CryptoHash::from([0; 32]))
131            }
132            HashingMode::Legacy => {
133                self.reset_historical_hash();
134                let hash = self.hash_mut().await?;
135                Ok(Self::wrap_state_hash(hash))
136            }
137            HashingMode::Historical { enforce } => {
138                let hash = self.advance_historical_hash().await?;
139                let wrapped = Self::wrap_state_hash(hash);
140                if enforce {
141                    Ok(wrapped)
142                } else {
143                    // Shadow mode: populate and surface the rolling hash so it can be compared
144                    // across validators, but keep reporting an all-zeros hash so that it is not
145                    // yet enforced in consensus.
146                    tracing::info!(
147                        chain_id = %self.context().extra().chain_id(),
148                        historical_state_hash = %wrapped,
149                        "computed historical execution-state hash in shadow mode",
150                    );
151                    Ok(CryptoHash::from([0; 32]))
152                }
153            }
154        }
155    }
156
157    /// Decides which hashing scheme applies, based on the feature flags in the current
158    /// committee's content policy. With no active committee, falls back to legacy hashing
159    /// (matching the previous behavior).
160    async fn hashing_mode(&self) -> Result<HashingMode, ViewError> {
161        let Some((_epoch, committee)) = self.system.current_committee().await? else {
162            return Ok(HashingMode::Legacy);
163        };
164        let flags = &committee.policy().http_request_allow_list;
165        let mode = if flags.contains(FLAG_ZERO_HASH) {
166            HashingMode::Zero
167        } else if flags.contains(FLAG_HISTORICAL_HASH) {
168            HashingMode::Historical { enforce: true }
169        } else if flags.contains(FLAG_HISTORICAL_HASH_SHADOW) {
170            HashingMode::Historical { enforce: false }
171        } else {
172            HashingMode::Legacy
173        };
174        Ok(mode)
175    }
176
177    /// Computes the next rolling historical hash and stores it in `historical_hash`.
178    ///
179    /// On the first block after activation (`historical_hash` is `None`) the hash is seeded from
180    /// a full content hash of the state, which — because [`HashableView::hash_mut`] reflects the
181    /// in-memory state — already includes this block's changes. Every subsequent block extends
182    /// the stored hash with the batch it is about to persist, so the cost is proportional to the
183    /// block's writes rather than the whole state.
184    async fn advance_historical_hash(&mut self) -> Result<HasherOutput, ViewError> {
185        let hash = match *self.historical_hash.get() {
186            None => self.hash_mut().await?,
187            Some(stored) => {
188                // Hash only the content fields' pending writes. `historical_hash` is intentionally
189                // excluded so the rolling hash never depends on its own (possibly still pending)
190                // value. This mirrors `main`, where the wrapper's hash key sits outside the hashed
191                // inner view.
192                let Self {
193                    system,
194                    users,
195                    stream_event_counts,
196                    historical_hash: _,
197                } = self;
198                let mut batch = Batch::new();
199                system.pre_save(&mut batch)?;
200                users.pre_save(&mut batch)?;
201                stream_event_counts.pre_save(&mut batch)?;
202                make_historical_hash(Some(stored), &batch)?
203            }
204        };
205        self.historical_hash.set(Some(hash));
206        Ok(hash)
207    }
208
209    /// Drops any stored rolling hash while historical hashing is inactive. This keeps inactive
210    /// chains free of stale state and, crucially, forces a fresh full-content re-seed if historical
211    /// hashing is ever re-activated: otherwise the chain would be extended from a hash that predates
212    /// the blocks executed while it was inactive, which no longer reflects the state.
213    fn reset_historical_hash(&mut self) {
214        if self.historical_hash.get().is_some() {
215            self.historical_hash.set(None);
216        }
217    }
218
219    /// Wraps a raw 32-byte hash into a domain-separated [`CryptoHash`], matching the convention
220    /// used on `main` for the execution-state hash.
221    fn wrap_state_hash(hash: HasherOutput) -> CryptoHash {
222        #[derive(Serialize, Deserialize)]
223        struct ExecutionStateViewHash([u8; 32]);
224        impl BcsHashable<'_> for ExecutionStateViewHash {}
225        CryptoHash::new(&ExecutionStateViewHash(hash.into()))
226    }
227}
228
229/// Extends a stored hash with a batch: `SHA3-256(stored || bcs(batch))`. An empty batch leaves the
230/// hash unchanged. Mirrors `HistoricallyHashableView::make_hash`. Changing this function changes
231/// consensus-visible execution-state hashes — see `make_historical_hash_matches_recorded_value`.
232fn make_historical_hash(
233    stored: Option<HasherOutput>,
234    batch: &Batch,
235) -> Result<HasherOutput, ViewError> {
236    let stored = stored.unwrap_or_default();
237    if batch.is_empty() {
238        return Ok(stored);
239    }
240    let mut hasher = linera_views::sha3::Sha3_256::default();
241    hasher.update_with_bytes(stored.as_ref())?;
242    hasher.update_with_bcs_bytes(&batch)?;
243    Ok(hasher.finalize())
244}
245
246impl<C: Context, C2: Context> ReplaceContext<C2> for ExecutionStateView<C> {
247    type Target = ExecutionStateView<C2>;
248
249    async fn with_context(
250        &mut self,
251        ctx: impl FnOnce(&Self::Context) -> C2 + Clone,
252    ) -> Self::Target {
253        ExecutionStateView {
254            system: self.system.with_context(ctx.clone()).await,
255            users: self.users.with_context(ctx.clone()).await,
256            stream_event_counts: self.stream_event_counts.with_context(ctx.clone()).await,
257            historical_hash: self.historical_hash.with_context(ctx.clone()).await,
258        }
259    }
260}
261
262/// How to interact with a long-lived service runtime.
263pub struct ServiceRuntimeEndpoint {
264    /// How to receive requests.
265    pub incoming_execution_requests: futures::channel::mpsc::UnboundedReceiver<ExecutionRequest>,
266    /// How to query the runtime.
267    pub runtime_request_sender: std::sync::mpsc::Sender<ServiceRuntimeRequest>,
268}
269
270#[cfg(with_testing)]
271impl ExecutionStateView<MemoryContext<TestExecutionRuntimeContext>>
272where
273    MemoryContext<TestExecutionRuntimeContext>: Context + Clone + 'static,
274{
275    /// Simulates the instantiation of an application.
276    pub async fn simulate_instantiation(
277        &mut self,
278        contract: UserContractCode,
279        local_time: linera_base::data_types::Timestamp,
280        application_description: ApplicationDescription,
281        instantiation_argument: Vec<u8>,
282        contract_blob: Blob,
283        service_blob: Blob,
284    ) -> Result<(), ExecutionError> {
285        let chain_id = application_description.creator_chain_id;
286        assert_eq!(chain_id, self.context().extra().chain_id);
287        let context = OperationContext {
288            chain_id,
289            authenticated_signer: None,
290            height: application_description.block_height,
291            round: None,
292            timestamp: local_time,
293        };
294
295        let action = UserAction::Instantiate(context, instantiation_argument);
296        let next_application_index = application_description.application_index + 1;
297        let next_chain_index = 0;
298
299        let application_id = From::from(&application_description);
300        let blob = Blob::new_application_description(&application_description);
301
302        self.system.used_blobs.insert(&blob.id())?;
303        self.system.used_blobs.insert(&contract_blob.id())?;
304        self.system.used_blobs.insert(&service_blob.id())?;
305
306        self.context()
307            .extra()
308            .user_contracts()
309            .pin()
310            .insert(application_id, contract);
311
312        self.context()
313            .extra()
314            .add_blobs([
315                contract_blob,
316                service_blob,
317                Blob::new_application_description(&application_description),
318            ])
319            .await?;
320
321        let tracker = ResourceTracker::default();
322        let policy = ResourceControlPolicy::no_fees();
323        let mut resource_controller = ResourceController::new(Arc::new(policy), tracker, None);
324        let mut txn_tracker = TransactionTracker::new(
325            local_time,
326            0,
327            next_application_index,
328            next_chain_index,
329            None,
330            &[],
331        );
332        txn_tracker.add_created_blob(blob);
333        Box::pin(
334            ExecutionStateActor::new(self, &mut txn_tracker, &mut resource_controller)
335                .run_user_action(application_id, action, context.refund_grant_to(), None),
336        )
337        .await?;
338
339        Ok(())
340    }
341}
342
343pub enum UserAction {
344    Instantiate(OperationContext, Vec<u8>),
345    Operation(OperationContext, Vec<u8>),
346    Message(MessageContext, Vec<u8>),
347    ProcessStreams(ProcessStreamsContext, Vec<StreamUpdate>),
348}
349
350impl UserAction {
351    pub(crate) fn signer(&self) -> Option<AccountOwner> {
352        match self {
353            UserAction::Instantiate(context, _) => context.authenticated_signer,
354            UserAction::Operation(context, _) => context.authenticated_signer,
355            UserAction::ProcessStreams(_, _) => None,
356            UserAction::Message(context, _) => context.authenticated_signer,
357        }
358    }
359
360    pub(crate) fn height(&self) -> BlockHeight {
361        match self {
362            UserAction::Instantiate(context, _) => context.height,
363            UserAction::Operation(context, _) => context.height,
364            UserAction::ProcessStreams(context, _) => context.height,
365            UserAction::Message(context, _) => context.height,
366        }
367    }
368
369    pub(crate) fn round(&self) -> Option<u32> {
370        match self {
371            UserAction::Instantiate(context, _) => context.round,
372            UserAction::Operation(context, _) => context.round,
373            UserAction::ProcessStreams(context, _) => context.round,
374            UserAction::Message(context, _) => context.round,
375        }
376    }
377
378    pub(crate) fn timestamp(&self) -> Timestamp {
379        match self {
380            UserAction::Instantiate(context, _) => context.timestamp,
381            UserAction::Operation(context, _) => context.timestamp,
382            UserAction::ProcessStreams(context, _) => context.timestamp,
383            UserAction::Message(context, _) => context.timestamp,
384        }
385    }
386}
387
388impl<C> ExecutionStateView<C>
389where
390    C: Context + Clone + 'static,
391    C::Extra: ExecutionRuntimeContext,
392{
393    /// Runs a query against the given application and returns its response.
394    pub async fn query_application(
395        &mut self,
396        context: QueryContext,
397        query: Query,
398        endpoint: Option<&mut ServiceRuntimeEndpoint>,
399    ) -> Result<QueryOutcome, ExecutionError> {
400        assert_eq!(context.chain_id, self.context().extra().chain_id());
401        match query {
402            Query::System(query) => {
403                let outcome = self.system.handle_query(context, query)?;
404                Ok(outcome.into())
405            }
406            Query::User {
407                application_id,
408                bytes,
409            } => {
410                let outcome = match endpoint {
411                    Some(endpoint) => {
412                        self.query_user_application_with_long_lived_service(
413                            application_id,
414                            context,
415                            bytes,
416                            &mut endpoint.incoming_execution_requests,
417                            &endpoint.runtime_request_sender,
418                        )
419                        .await?
420                    }
421                    None => {
422                        self.query_user_application(application_id, context, bytes)
423                            .await?
424                    }
425                };
426                Ok(outcome.into())
427            }
428        }
429    }
430
431    async fn query_user_application(
432        &mut self,
433        application_id: ApplicationId,
434        context: QueryContext,
435        query: Vec<u8>,
436    ) -> Result<QueryOutcome<Vec<u8>>, ExecutionError> {
437        self.query_user_application_with_deadline(
438            application_id,
439            context,
440            query,
441            None,
442            BTreeMap::new(),
443        )
444        .await
445    }
446
447    pub(crate) async fn query_user_application_with_deadline(
448        &mut self,
449        application_id: ApplicationId,
450        context: QueryContext,
451        query: Vec<u8>,
452        deadline: Option<Instant>,
453        created_blobs: BTreeMap<BlobId, BlobContent>,
454    ) -> Result<QueryOutcome<Vec<u8>>, ExecutionError> {
455        let (execution_state_sender, mut execution_state_receiver) =
456            futures::channel::mpsc::unbounded();
457        let mut txn_tracker = TransactionTracker::default().with_blobs(created_blobs);
458        let mut resource_controller = ResourceController::default();
459        let thread_pool = self.context().extra().thread_pool().clone();
460        let mut actor = ExecutionStateActor::new(self, &mut txn_tracker, &mut resource_controller);
461
462        let (codes, descriptions) = actor.service_and_dependencies(application_id).await?;
463
464        let service_runtime_task = thread_pool
465            .run_send(JsVec(codes), move |codes| async move {
466                let mut runtime = ServiceSyncRuntime::new_with_deadline(
467                    execution_state_sender,
468                    context,
469                    deadline,
470                );
471
472                for (code, description) in codes.0.into_iter().zip(descriptions) {
473                    runtime.preload_service(ApplicationId::from(&description), code, description);
474                }
475
476                runtime.run_query(application_id, query)
477            })
478            .await;
479
480        while let Some(request) = execution_state_receiver.next().await {
481            actor.handle_request(request).await?;
482        }
483
484        service_runtime_task.await?
485    }
486
487    async fn query_user_application_with_long_lived_service(
488        &mut self,
489        application_id: ApplicationId,
490        context: QueryContext,
491        query: Vec<u8>,
492        incoming_execution_requests: &mut futures::channel::mpsc::UnboundedReceiver<
493            ExecutionRequest,
494        >,
495        runtime_request_sender: &std::sync::mpsc::Sender<ServiceRuntimeRequest>,
496    ) -> Result<QueryOutcome<Vec<u8>>, ExecutionError> {
497        let (outcome_sender, outcome_receiver) = oneshot::channel();
498        let mut outcome_receiver = outcome_receiver.fuse();
499
500        runtime_request_sender
501            .send(ServiceRuntimeRequest::Query {
502                application_id,
503                context,
504                query,
505                callback: outcome_sender,
506            })
507            .expect("Service runtime thread should only stop when `request_sender` is dropped");
508
509        let mut txn_tracker = TransactionTracker::default();
510        let mut resource_controller = ResourceController::default();
511        let mut actor = ExecutionStateActor::new(self, &mut txn_tracker, &mut resource_controller);
512
513        loop {
514            futures::select! {
515                maybe_request = incoming_execution_requests.next() => {
516                    if let Some(request) = maybe_request {
517                        actor.handle_request(request).await?;
518                    }
519                }
520                outcome = &mut outcome_receiver => {
521                    return outcome.map_err(|_| ExecutionError::MissingRuntimeResponse)?;
522                }
523            }
524        }
525    }
526
527    /// Returns the descriptions of all applications registered on this chain.
528    pub async fn list_applications(
529        &self,
530    ) -> Result<Vec<(ApplicationId, ApplicationDescription)>, ExecutionError> {
531        let mut applications = vec![];
532        for app_id in self.users.indices().await? {
533            let blob_id = app_id.description_blob_id();
534            let blob_content = self.system.read_blob_content(blob_id).await?;
535            let application_description = bcs::from_bytes(blob_content.bytes())?;
536            applications.push((app_id, application_description));
537        }
538        Ok(applications)
539    }
540}
541
542#[cfg(test)]
543mod historical_hash_tests {
544    use linera_views::{batch::Batch, common::HasherOutput};
545
546    use super::make_historical_hash;
547
548    const RECORDED_CHAIN_HASH: [u8; 32] = [
549        148, 130, 122, 191, 71, 219, 62, 147, 185, 157, 252, 71, 40, 90, 125, 182, 36, 55, 7, 233,
550        90, 114, 77, 56, 106, 151, 21, 246, 183, 174, 65, 74,
551    ];
552
553    /// Golden test pinning the historical-hash chaining primitive. The recorded value is the
554    /// SHA3-256 of `stored || bcs(batch)`; a change to either the algorithm or the `Batch`
555    /// serialization would alter consensus-visible execution-state hashes and break this test
556    /// deliberately.
557    #[test]
558    fn make_historical_hash_matches_recorded_value() {
559        let stored: HasherOutput = [7u8; 32].into();
560        let mut batch = Batch::new();
561        batch.put_key_value_bytes(vec![0, 1, 2], vec![3, 4, 5]);
562        batch.put_key_value_bytes(vec![6, 7], vec![8]);
563
564        let hash = make_historical_hash(Some(stored), &batch).unwrap();
565        assert_eq!(<[u8; 32]>::from(hash), RECORDED_CHAIN_HASH);
566
567        // An empty batch is a no-op: the stored hash is returned unchanged.
568        assert_eq!(
569            make_historical_hash(Some(stored), &Batch::new()).unwrap(),
570            stored,
571        );
572        // A `None` stored hash behaves like all-zeros, so the same batch over `None` differs from
573        // the same batch over a non-zero stored hash.
574        assert_ne!(
575            make_historical_hash(None, &batch).unwrap(),
576            make_historical_hash(Some(stored), &batch).unwrap(),
577        );
578    }
579}