Skip to main content

linera_execution/
system.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2// Copyright (c) Zefchain Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5#[cfg(test)]
6#[path = "./unit_tests/system_tests.rs"]
7mod tests;
8
9use std::{
10    collections::{BTreeMap, BTreeSet},
11    sync::Arc,
12};
13
14use allocative::Allocative;
15use custom_debug_derive::Debug;
16use linera_base::{
17    crypto::CryptoHash,
18    data_types::{
19        Amount, ApplicationPermissions, ArithmeticError, Blob, BlobContent, BlockHeight,
20        ChainDescription, ChainOrigin, Cursor, Epoch, InitialChainConfig, OracleResponse,
21        Timestamp,
22    },
23    ensure, hex_debug,
24    identifiers::{
25        Account, AccountOwner, BlobId, BlobType, ChainId, EventId, ModuleId, OwnerSpender, StreamId,
26    },
27    ownership::{ChainOwnership, TimeoutConfig},
28};
29use linera_views::{
30    context::Context,
31    lazy_register_view::LazyRegisterView,
32    map_view::MapView,
33    register_view::RegisterView,
34    set_view::SetView,
35    views::{ClonableView, ReplaceContext, View},
36    ViewError,
37};
38use serde::{Deserialize, Serialize};
39
40#[cfg(test)]
41use crate::test_utils::SystemExecutionState;
42use crate::{
43    committee::Committee, util::OracleResponseExt as _, ApplicationDescription, ApplicationId,
44    ExecutionError, ExecutionRuntimeContext, MessageContext, MessageKind, OperationContext,
45    OutgoingMessage, QueryContext, QueryOutcome, ResourceController, TransactionTracker,
46};
47
48/// The event stream name for new epochs and committees.
49pub static EPOCH_STREAM_NAME: &[u8] = &[0];
50/// The event stream name for removed epochs.
51pub static REMOVED_EPOCH_STREAM_NAME: &[u8] = &[1];
52
53/// The data stored in an epoch creation event.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct EpochEventData {
56    /// The hash of the committee blob for this epoch.
57    pub blob_hash: CryptoHash,
58    /// The timestamp when the epoch was created on the admin chain.
59    pub timestamp: Timestamp,
60}
61
62/// The number of times the [`SystemOperation::OpenChain`] was executed.
63#[cfg(with_metrics)]
64mod metrics {
65    use std::sync::LazyLock;
66
67    use linera_base::prometheus_util::register_int_counter_vec;
68    use prometheus::IntCounterVec;
69
70    pub static OPEN_CHAIN_COUNT: LazyLock<IntCounterVec> = LazyLock::new(|| {
71        register_int_counter_vec(
72            "open_chain_count",
73            "The number of times the `OpenChain` operation was executed",
74            &[],
75        )
76    });
77}
78
79/// Per-block state of a chain: the timestamp of its most recent block together with
80/// cumulative counts of the transactions and messages processed so far. Stored as a
81/// single value so that each block updates only one key.
82#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Allocative)]
83pub struct ChainProgress {
84    /// The timestamp of the most recent block.
85    pub timestamp: Timestamp,
86    /// Number of incoming message bundles executed so far.
87    pub num_incoming_bundles: u32,
88    /// Number of operations executed so far.
89    pub num_operations: u32,
90    /// Number of outgoing messages sent so far.
91    pub num_outgoing_messages: u32,
92}
93
94/// A view accessing the execution state of the system of a chain.
95#[derive(Debug, ClonableView, View, Allocative)]
96#[allocative(bound = "C")]
97pub struct SystemExecutionStateView<C> {
98    /// How the chain was created. May be unknown for inactive chains.
99    pub description: LazyRegisterView<C, Option<ChainDescription>>,
100    /// The number identifying the current configuration.
101    pub epoch: RegisterView<C, Epoch>,
102    /// The admin of the chain.
103    pub admin_chain_id: RegisterView<C, Option<ChainId>>,
104    /// The blob hash of the committee that is allowed to sign the next block on this chain.
105    /// `None` until the chain is initialized.
106    pub committee_hash: RegisterView<C, Option<CryptoHash>>,
107    /// Ownership of the chain.
108    pub ownership: LazyRegisterView<C, ChainOwnership>,
109    /// Balance of the chain. (Available to any user able to create blocks in the chain.)
110    pub balance: RegisterView<C, Amount>,
111    /// Balances attributed to a given owner.
112    pub balances: MapView<C, AccountOwner, Amount>,
113    /// Allowances for spending from one account by another.
114    pub allowances: MapView<C, OwnerSpender, Amount>,
115    /// Whether this chain has been closed.
116    pub closed: RegisterView<C, bool>,
117    /// Permissions for applications on this chain.
118    pub application_permissions: LazyRegisterView<C, ApplicationPermissions>,
119    /// Blobs that have been used or published on this chain.
120    pub used_blobs: SetView<C, BlobId>,
121    /// The event stream subscriptions of applications on this chain.
122    pub event_subscriptions: MapView<C, (ChainId, StreamId), EventSubscriptions>,
123    /// The number of events in the streams that this chain is writing to.
124    pub stream_event_counts: MapView<C, StreamId, u32>,
125    /// For each recipient chain, the cursors `(block_height, transaction_index)` of
126    /// our outgoing bundles that haven't yet been acknowledged via
127    /// [`SystemMessage::CheckpointAck`]. Maintained on-chain (as opposed to the local
128    /// off-chain outbox in chain state) so it is identical across validators and can
129    /// feed the checkpoint oracle response's `outbox_block_hashes` (the unique heights
130    /// across all cursors). We store cursors rather than heights so that an ack at a
131    /// finer-grained cursor than the last bundle in a block can fully evict the entry
132    /// — important for high-fanout chains whose recipients only interact once.
133    ///
134    /// Excludes bundles whose only messages to a given recipient were
135    /// `SystemMessage::CheckpointAck`: those don't trigger a return notification
136    /// from the recipient, so tracking them would accumulate forever.
137    pub unfinalized_message_blocks: MapView<C, ChainId, BTreeSet<Cursor>>,
138    /// Chains from which we've received at least one non-`CheckpointAck` message
139    /// since our last `SystemOperation::Checkpoint`. Determines whom to notify with a
140    /// `SystemMessage::CheckpointAck` at the next checkpoint operation. Excluding
141    /// `CheckpointAck` messages here is what breaks the otherwise-perpetual
142    /// notification ping-pong between two chains that ever exchanged a real message.
143    pub pending_checkpoint_ack_targets: SetView<C, ChainId>,
144    /// The most recent block's timestamp and cumulative transaction/message counts.
145    pub progress: RegisterView<C, ChainProgress>,
146}
147
148impl<C: Context, C2: Context> ReplaceContext<C2> for SystemExecutionStateView<C> {
149    type Target = SystemExecutionStateView<C2>;
150
151    async fn with_context(
152        &mut self,
153        ctx: impl FnOnce(&Self::Context) -> C2 + Clone,
154    ) -> Self::Target {
155        SystemExecutionStateView {
156            description: self.description.with_context(ctx.clone()).await,
157            epoch: self.epoch.with_context(ctx.clone()).await,
158            admin_chain_id: self.admin_chain_id.with_context(ctx.clone()).await,
159            committee_hash: self.committee_hash.with_context(ctx.clone()).await,
160            ownership: self.ownership.with_context(ctx.clone()).await,
161            balance: self.balance.with_context(ctx.clone()).await,
162            balances: self.balances.with_context(ctx.clone()).await,
163            allowances: self.allowances.with_context(ctx.clone()).await,
164            closed: self.closed.with_context(ctx.clone()).await,
165            application_permissions: self.application_permissions.with_context(ctx.clone()).await,
166            used_blobs: self.used_blobs.with_context(ctx.clone()).await,
167            event_subscriptions: self.event_subscriptions.with_context(ctx.clone()).await,
168            stream_event_counts: self.stream_event_counts.with_context(ctx.clone()).await,
169            unfinalized_message_blocks: self
170                .unfinalized_message_blocks
171                .with_context(ctx.clone())
172                .await,
173            pending_checkpoint_ack_targets: self
174                .pending_checkpoint_ack_targets
175                .with_context(ctx.clone())
176                .await,
177            progress: self.progress.with_context(ctx.clone()).await,
178        }
179    }
180}
181
182/// The applications subscribing to a particular stream, and their per-application event indices.
183#[derive(Debug, Clone, Serialize, Deserialize, Allocative)]
184pub struct EventSubscriptions {
185    /// Cached minimum of all per-application `next_index` values. Used for short-circuit
186    /// filtering: if the next available event index is <= this value, no application needs
187    /// processing. Set to `u32::MAX` when no applications are subscribed.
188    pub min_next_index: u32,
189    /// The applications that are subscribed to this stream, each mapped to the next event
190    /// index that they need to process.
191    pub applications: BTreeMap<ApplicationId, u32>,
192}
193
194impl Default for EventSubscriptions {
195    fn default() -> Self {
196        Self {
197            min_next_index: u32::MAX,
198            applications: BTreeMap::new(),
199        }
200    }
201}
202
203impl EventSubscriptions {
204    pub(crate) fn recalculate_min(&mut self) {
205        self.min_next_index = self
206            .applications
207            .values()
208            .copied()
209            .min()
210            .unwrap_or(u32::MAX);
211    }
212}
213
214/// The initial configuration for a new chain.
215#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
216pub struct OpenChainConfig {
217    /// The ownership configuration of the new chain.
218    pub ownership: ChainOwnership,
219    /// The initial chain balance.
220    pub balance: Amount,
221    /// The initial application permissions.
222    pub application_permissions: ApplicationPermissions,
223}
224
225impl OpenChainConfig {
226    /// Creates an [`InitialChainConfig`] based on this [`OpenChainConfig`] and additional
227    /// parameters.
228    pub fn init_chain_config(&self, epoch: Epoch) -> InitialChainConfig {
229        InitialChainConfig {
230            application_permissions: self.application_permissions.clone(),
231            balance: self.balance,
232            epoch,
233            ownership: self.ownership.clone(),
234        }
235    }
236}
237
238/// A system operation.
239#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
240#[allow(missing_docs)]
241pub enum SystemOperation {
242    /// Transfers `amount` units of value from the given owner's account to the recipient.
243    /// If no owner is given, try to take the units out of the unattributed account.
244    Transfer {
245        owner: AccountOwner,
246        recipient: Account,
247        amount: Amount,
248    },
249    /// Claims `amount` units of value from the given owner's account in the remote
250    /// `target` chain. Depending on its configuration, the `target` chain may refuse to
251    /// process the message.
252    Claim {
253        owner: AccountOwner,
254        target_id: ChainId,
255        recipient: Account,
256        amount: Amount,
257    },
258    /// Creates (or activates) a new chain.
259    /// This will automatically subscribe to the future committees created by `admin_chain_id`.
260    OpenChain(OpenChainConfig),
261    /// Closes the chain.
262    CloseChain,
263    /// Changes the ownership of the chain.
264    ChangeOwnership {
265        /// Super owners can propose fast blocks in the first round, and regular blocks in any round.
266        #[debug(skip_if = Vec::is_empty)]
267        super_owners: Vec<AccountOwner>,
268        /// The regular owners, with their weights that determine how often they are round leader.
269        #[debug(skip_if = Vec::is_empty)]
270        owners: Vec<(AccountOwner, u64)>,
271        /// The leader of the first single-leader round. If not set, this is random like other rounds.
272        #[debug(skip_if = Option::is_none)]
273        first_leader: Option<AccountOwner>,
274        /// The number of initial rounds after 0 in which all owners are allowed to propose blocks.
275        multi_leader_rounds: u32,
276        /// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
277        /// This should only be `true` on chains with restrictive application permissions and an
278        /// application-based mechanism to select block proposers.
279        open_multi_leader_rounds: bool,
280        /// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
281        timeout_config: TimeoutConfig,
282    },
283    /// Changes the application permissions configuration on this chain.
284    ChangeApplicationPermissions(ApplicationPermissions),
285    /// Publishes a new application module.
286    PublishModule { module_id: ModuleId },
287    /// Publishes a new data blob.
288    PublishDataBlob { blob_hash: CryptoHash },
289    /// Verifies that the given blob exists. Otherwise the block fails.
290    VerifyBlob { blob_id: BlobId },
291    /// Creates a new application.
292    CreateApplication {
293        module_id: ModuleId,
294        #[serde(with = "serde_bytes")]
295        #[debug(with = "hex_debug")]
296        parameters: Vec<u8>,
297        #[serde(with = "serde_bytes")]
298        #[debug(with = "hex_debug", skip_if = Vec::is_empty)]
299        instantiation_argument: Vec<u8>,
300        #[debug(skip_if = Vec::is_empty)]
301        required_application_ids: Vec<ApplicationId>,
302    },
303    /// Operations that are only allowed on the admin chain.
304    Admin(AdminOperation),
305    /// Processes an event about a new epoch and committee.
306    ProcessNewEpoch(Epoch),
307    /// Updates the event stream trackers.
308    UpdateStream {
309        application_id: ApplicationId,
310        chain_id: ChainId,
311        stream_id: StreamId,
312        /// The lowest readable index in the publishing stream, i.e. the index of the first
313        /// event published since the publisher's most recent checkpoint.
314        first_index: u32,
315        next_index: u32,
316    },
317    /// Publishes a canonical snapshot of the chain's execution state as a blob,
318    /// resetting the execution-state hash to the hash of that content. This allows
319    /// future nodes to bootstrap from the snapshot instead of replaying the chain's
320    /// history. Subject to a strict set of preconditions on the chain's state.
321    Checkpoint,
322}
323
324/// Operations that are only allowed on the admin chain.
325#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
326#[allow(missing_docs)]
327pub enum AdminOperation {
328    /// Publishes a new committee as a blob. This can be assigned to an epoch using
329    /// [`AdminOperation::CreateCommittee`] in a later block.
330    PublishCommitteeBlob { blob_hash: CryptoHash },
331    /// Registers a new committee. Other chains can then migrate to the new epoch by executing
332    /// [`SystemOperation::ProcessNewEpoch`].
333    CreateCommittee { epoch: Epoch, blob_hash: CryptoHash },
334    /// Removes a committee. Blocks signed by this committee will only be accepted once they
335    /// have been followed (hence re-certified) by a block certified by a recent committee.
336    RemoveCommittee { epoch: Epoch },
337}
338
339/// A system message meant to be executed on a remote chain.
340#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
341#[allow(missing_docs)]
342pub enum SystemMessage {
343    /// Credits `amount` units of value to the account `target` -- unless the message is
344    /// bouncing, in which case `source` is credited instead.
345    Credit {
346        target: AccountOwner,
347        amount: Amount,
348        source: AccountOwner,
349    },
350    /// Withdraws `amount` units of value from the account and starts a transfer to credit
351    /// the recipient. The message must be properly authenticated. Receiver chains may
352    /// refuse it depending on their configuration.
353    Withdraw {
354        owner: AccountOwner,
355        amount: Amount,
356        recipient: Account,
357    },
358    /// Sent by a chain that just executed `SystemOperation::Checkpoint` to each chain
359    /// it has received at least one non-`CheckpointAck` message from since its
360    /// previous checkpoint. `latest_received_cursor` is the position past the last
361    /// bundle from the recipient that the sender has consumed. The recipient trims
362    /// its `unfinalized_message_blocks` accordingly, so that its next checkpoint
363    /// drops already-delivered outgoing messages from its outbox dump.
364    CheckpointAck { latest_received_cursor: Cursor },
365}
366
367/// A query to the system state.
368#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
369pub struct SystemQuery;
370
371/// The response to a system query.
372#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
373#[allow(missing_docs)]
374pub struct SystemResponse {
375    pub chain_id: ChainId,
376    pub balance: Amount,
377}
378
379/// Optional user message attached to a transfer.
380#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Default, Debug, Serialize, Deserialize)]
381pub struct UserData(pub Option<[u8; 32]>);
382
383/// The result of creating a new application.
384#[derive(Debug)]
385#[allow(missing_docs)]
386pub struct CreateApplicationResult {
387    pub app_id: ApplicationId,
388}
389
390impl<C> SystemExecutionStateView<C>
391where
392    C: Context + Clone + 'static,
393    C::Extra: ExecutionRuntimeContext,
394{
395    /// Invariant for the states of active chains.
396    pub async fn is_active(&self) -> Result<bool, ViewError> {
397        Ok(self.description.get().await?.is_some()
398            && self.ownership.get().await?.is_active()
399            && self.admin_chain_id.get().is_some())
400    }
401
402    /// Returns the current committee, if the chain has been initialized.
403    pub async fn current_committee(
404        &self,
405    ) -> Result<Option<(Epoch, Arc<Committee>)>, ExecutionError> {
406        let Some(hash) = *self.committee_hash.get() else {
407            return Ok(None);
408        };
409        let epoch = *self.epoch.get();
410        let committee = self
411            .context()
412            .extra()
413            .get_or_load_committee_by_hash(hash)
414            .await?;
415        Ok(Some((epoch, committee)))
416    }
417
418    async fn get_event(&self, event_id: EventId) -> Result<Arc<Vec<u8>>, ExecutionError> {
419        match self.context().extra().get_event(event_id.clone()).await? {
420            None => Err(ExecutionError::EventsNotFound(vec![event_id])),
421            Some(vec) => Ok(vec),
422        }
423    }
424
425    /// Executes the sender's side of an operation and returns a list of actions to be
426    /// taken.
427    pub async fn execute_operation(
428        &mut self,
429        context: OperationContext,
430        operation: SystemOperation,
431        txn_tracker: &mut TransactionTracker,
432        resource_controller: &mut ResourceController<Option<AccountOwner>>,
433    ) -> Result<Option<(ApplicationId, Vec<u8>)>, ExecutionError> {
434        use SystemOperation::*;
435        let mut new_application = None;
436        match operation {
437            OpenChain(config) => {
438                let _chain_id = self
439                    .open_chain(
440                        config,
441                        context.chain_id,
442                        context.height,
443                        context.timestamp,
444                        txn_tracker,
445                    )
446                    .await?;
447                #[cfg(with_metrics)]
448                metrics::OPEN_CHAIN_COUNT.with_label_values(&[]).inc();
449            }
450            ChangeOwnership {
451                super_owners,
452                owners,
453                first_leader,
454                multi_leader_rounds,
455                open_multi_leader_rounds,
456                timeout_config,
457            } => {
458                self.ownership.set(ChainOwnership {
459                    super_owners: super_owners.into_iter().collect(),
460                    owners: owners.into_iter().collect(),
461                    first_leader,
462                    multi_leader_rounds,
463                    open_multi_leader_rounds,
464                    timeout_config,
465                });
466            }
467            ChangeApplicationPermissions(application_permissions) => {
468                self.application_permissions.set(application_permissions);
469            }
470            CloseChain => self.close_chain(),
471            Transfer {
472                owner,
473                amount,
474                recipient,
475            } => {
476                let maybe_message = self
477                    .transfer(context.authenticated_owner, None, owner, recipient, amount)
478                    .await?;
479                txn_tracker.add_outgoing_messages(maybe_message);
480            }
481            Claim {
482                owner,
483                target_id,
484                recipient,
485                amount,
486            } => {
487                let maybe_message = self
488                    .claim(
489                        context.authenticated_owner,
490                        None,
491                        owner,
492                        target_id,
493                        recipient,
494                        amount,
495                    )
496                    .await?;
497                txn_tracker.add_outgoing_messages(maybe_message);
498            }
499            Admin(admin_operation) => {
500                ensure!(
501                    *self.admin_chain_id.get() == Some(context.chain_id),
502                    ExecutionError::AdminOperationOnNonAdminChain
503                );
504                match admin_operation {
505                    AdminOperation::PublishCommitteeBlob { blob_hash } => {
506                        self.blob_published(
507                            &BlobId::new(blob_hash, BlobType::Committee),
508                            txn_tracker,
509                        )?;
510                    }
511                    AdminOperation::CreateCommittee { epoch, blob_hash } => {
512                        self.check_next_epoch(epoch)?;
513                        let blob_id = BlobId::new(blob_hash, BlobType::Committee);
514                        // Validate that the blob exists and deserializes as a Committee.
515                        self.context()
516                            .extra()
517                            .get_or_load_committee_by_hash(blob_hash)
518                            .await?;
519                        self.blob_used(txn_tracker, blob_id).await?;
520                        self.committee_hash.set(Some(blob_hash));
521                        self.epoch.set(epoch);
522                        let event_data = EpochEventData {
523                            blob_hash,
524                            timestamp: context.timestamp,
525                        };
526                        let stream_id = StreamId::system(EPOCH_STREAM_NAME);
527                        let next_index = epoch.0.checked_add(1).ok_or(ArithmeticError::Overflow)?;
528                        self.stream_event_counts.insert(&stream_id, next_index)?;
529                        txn_tracker.add_event(stream_id, epoch.0, bcs::to_bytes(&event_data)?);
530                    }
531                    AdminOperation::RemoveCommittee { epoch } => {
532                        let stream_id = StreamId::system(REMOVED_EPOCH_STREAM_NAME);
533                        let count = self.stream_event_counts.get(&stream_id).await?.unwrap_or(0);
534                        // Revocations must happen in increasing epoch order, so the stream's
535                        // indices stay sequential.
536                        ensure!(
537                            count == epoch.0 && epoch < *self.epoch.get(),
538                            ExecutionError::InvalidCommitteeRemoval
539                        );
540                        let next_index = epoch.0.checked_add(1).ok_or(ArithmeticError::Overflow)?;
541                        self.stream_event_counts.insert(&stream_id, next_index)?;
542                        txn_tracker.add_event(stream_id, epoch.0, vec![]);
543                    }
544                }
545            }
546            PublishModule { module_id } => {
547                for blob_id in module_id.bytecode_blob_ids() {
548                    self.blob_published(&blob_id, txn_tracker)?;
549                }
550            }
551            CreateApplication {
552                module_id,
553                parameters,
554                instantiation_argument,
555                required_application_ids,
556            } => {
557                let CreateApplicationResult { app_id } = self
558                    .create_application(
559                        context.chain_id,
560                        context.height,
561                        module_id,
562                        parameters,
563                        required_application_ids,
564                        txn_tracker,
565                    )
566                    .await?;
567                new_application = Some((app_id, instantiation_argument));
568            }
569            PublishDataBlob { blob_hash } => {
570                self.blob_published(&BlobId::new(blob_hash, BlobType::Data), txn_tracker)?;
571            }
572            VerifyBlob { blob_id } => {
573                self.assert_blob_exists(blob_id).await?;
574                resource_controller
575                    .with_state(self)
576                    .await?
577                    .track_blob_read(0)?;
578                self.blob_used(txn_tracker, blob_id).await?;
579            }
580            ProcessNewEpoch(epoch) => {
581                self.check_next_epoch(epoch)?;
582                let admin_chain_id = self.admin_chain_id.get().ok_or_else(|| {
583                    ExecutionError::InternalError(
584                        "execute_operation called for uninitialized chain",
585                    )
586                })?;
587                let event_id = EventId {
588                    chain_id: admin_chain_id,
589                    stream_id: StreamId::system(EPOCH_STREAM_NAME),
590                    index: epoch.0,
591                };
592                let bytes = txn_tracker
593                    .oracle(|| async {
594                        let bytes = self.get_event(event_id.clone()).await?;
595                        Ok(OracleResponse::Event(
596                            event_id.clone(),
597                            Arc::unwrap_or_clone(bytes),
598                        ))
599                    })
600                    .await?
601                    .to_event(&event_id)?;
602                let event_data: EpochEventData = bcs::from_bytes(&bytes)?;
603                let blob_id = BlobId::new(event_data.blob_hash, BlobType::Committee);
604                // Validate that the blob exists and deserializes as a Committee.
605                self.context()
606                    .extra()
607                    .get_or_load_committee_by_hash(event_data.blob_hash)
608                    .await?;
609                self.blob_used(txn_tracker, blob_id).await?;
610                self.committee_hash.set(Some(event_data.blob_hash));
611                self.epoch.set(epoch);
612            }
613            UpdateStream {
614                application_id,
615                chain_id,
616                stream_id,
617                first_index,
618                next_index,
619            } => {
620                let subscriptions = self
621                    .event_subscriptions
622                    .get_mut_or_default(&(chain_id, stream_id.clone()))
623                    .await?;
624                let app_next_index = *subscriptions
625                    .applications
626                    .get(&application_id)
627                    .ok_or(ExecutionError::UnsubscribedUpdateStream)?;
628                ensure!(
629                    app_next_index < next_index,
630                    ExecutionError::OutdatedUpdateStream
631                );
632                txn_tracker.add_stream_to_process(
633                    application_id,
634                    chain_id,
635                    stream_id.clone(),
636                    app_next_index,
637                    first_index,
638                    next_index,
639                );
640                subscriptions
641                    .applications
642                    .insert(application_id, next_index);
643                subscriptions.recalculate_min();
644                let index = next_index
645                    .checked_sub(1)
646                    .ok_or(ArithmeticError::Underflow)?;
647                let event_id = EventId {
648                    chain_id,
649                    stream_id,
650                    index,
651                };
652                let context = self.context();
653                let extra = context.extra();
654                let mut missing_events = Vec::new();
655                txn_tracker
656                    .oracle(|| async {
657                        if !extra.contains_event(event_id.clone()).await? {
658                            missing_events.push(event_id.clone());
659                        }
660                        Ok(OracleResponse::EventExists(event_id))
661                    })
662                    .await?;
663                ensure!(
664                    missing_events.is_empty(),
665                    ExecutionError::EventsNotFound(missing_events)
666                );
667            }
668            Checkpoint => {
669                return Err(ExecutionError::InternalError(
670                    "SystemOperation::Checkpoint must be dispatched at ExecutionStateView level",
671                ));
672            }
673        }
674
675        Ok(new_application)
676    }
677
678    /// Returns an error if the `provided` epoch is not exactly one higher than the chain's current
679    /// epoch.
680    fn check_next_epoch(&self, provided: Epoch) -> Result<(), ExecutionError> {
681        let expected = self.epoch.get().try_add_one()?;
682        ensure!(
683            provided == expected,
684            ExecutionError::InvalidCommitteeEpoch { provided, expected }
685        );
686        Ok(())
687    }
688
689    async fn credit(&mut self, owner: &AccountOwner, amount: Amount) -> Result<(), ExecutionError> {
690        if owner == &AccountOwner::CHAIN {
691            let new_balance = self.balance.get().saturating_add(amount);
692            self.balance.set(new_balance);
693        } else {
694            let balance = self.balances.get_mut_or_default(owner).await?;
695            *balance = balance.saturating_add(amount);
696        }
697        Ok(())
698    }
699
700    async fn credit_or_send_message(
701        &mut self,
702        source: AccountOwner,
703        recipient: Account,
704        amount: Amount,
705    ) -> Result<Option<OutgoingMessage>, ExecutionError> {
706        let source_chain_id = self.context().extra().chain_id();
707        if recipient.chain_id == source_chain_id {
708            // Handle same-chain transfer locally.
709            let target = recipient.owner;
710            self.credit(&target, amount).await?;
711            Ok(None)
712        } else {
713            // Handle cross-chain transfer with message.
714            let message = SystemMessage::Credit {
715                amount,
716                source,
717                target: recipient.owner,
718            };
719            Ok(Some(
720                OutgoingMessage::new(recipient.chain_id, message).with_kind(MessageKind::Tracked),
721            ))
722        }
723    }
724
725    /// Transfers `amount` from `source` to `recipient`, debiting the source account.
726    pub async fn transfer(
727        &mut self,
728        authenticated_owner: Option<AccountOwner>,
729        authenticated_application_id: Option<ApplicationId>,
730        source: AccountOwner,
731        recipient: Account,
732        amount: Amount,
733    ) -> Result<Option<OutgoingMessage>, ExecutionError> {
734        if source == AccountOwner::CHAIN {
735            let authenticated_owner =
736                authenticated_owner.ok_or(ExecutionError::UnauthenticatedTransferOwner)?;
737            ensure!(
738                self.ownership.get().await?.is_owner(&authenticated_owner),
739                ExecutionError::UnauthenticatedTransferOwner
740            );
741        } else {
742            ensure!(
743                authenticated_owner == Some(source)
744                    || authenticated_application_id.map(AccountOwner::from) == Some(source),
745                ExecutionError::UnauthenticatedTransferOwner
746            );
747        }
748        ensure!(
749            amount > Amount::ZERO,
750            ExecutionError::IncorrectTransferAmount
751        );
752        self.debit(&source, amount).await?;
753        self.credit_or_send_message(source, recipient, amount).await
754    }
755
756    /// Claims `amount` from `source`'s account on `target_id` and transfers it to `recipient`.
757    pub async fn claim(
758        &mut self,
759        authenticated_owner: Option<AccountOwner>,
760        authenticated_application_id: Option<ApplicationId>,
761        source: AccountOwner,
762        target_id: ChainId,
763        recipient: Account,
764        amount: Amount,
765    ) -> Result<Option<OutgoingMessage>, ExecutionError> {
766        ensure!(
767            authenticated_owner == Some(source)
768                || authenticated_application_id.map(AccountOwner::from) == Some(source),
769            ExecutionError::UnauthenticatedClaimOwner
770        );
771        ensure!(amount > Amount::ZERO, ExecutionError::IncorrectClaimAmount);
772
773        let current_chain_id = self.context().extra().chain_id();
774        if target_id == current_chain_id {
775            // Handle same-chain claim locally by processing the withdraw operation directly
776            self.debit(&source, amount).await?;
777            self.credit_or_send_message(source, recipient, amount).await
778        } else {
779            // Handle cross-chain claim with Withdraw message
780            let message = SystemMessage::Withdraw {
781                amount,
782                owner: source,
783                recipient,
784            };
785            Ok(Some(
786                OutgoingMessage::new(target_id, message)
787                    .with_authenticated_owner(authenticated_owner),
788            ))
789        }
790    }
791
792    /// Sets the allowance that `spender` may transfer on behalf of `owner` to `amount`.
793    pub async fn approve(
794        &mut self,
795        authenticated_owner: Option<AccountOwner>,
796        authenticated_application_id: Option<ApplicationId>,
797        owner: AccountOwner,
798        spender: AccountOwner,
799        amount: Amount,
800    ) -> Result<(), ExecutionError> {
801        ensure!(
802            authenticated_owner == Some(owner)
803                || authenticated_application_id.map(AccountOwner::from) == Some(owner),
804            ExecutionError::UnauthenticatedTransferOwner
805        );
806
807        let owner_spender = OwnerSpender::new(owner, spender);
808        if amount == Amount::ZERO {
809            self.allowances.remove(&owner_spender)?;
810            return Ok(());
811        }
812        let allowance = self.allowances.get_mut_or_default(&owner_spender).await?;
813        *allowance = amount;
814
815        Ok(())
816    }
817
818    /// Transfers `amount` from `owner` to `recipient`, debiting the spender's allowance.
819    pub async fn transfer_from(
820        &mut self,
821        authenticated_owner: Option<AccountOwner>,
822        authenticated_application_id: Option<ApplicationId>,
823        owner: AccountOwner,
824        spender: AccountOwner,
825        recipient: Account,
826        amount: Amount,
827    ) -> Result<Option<OutgoingMessage>, ExecutionError> {
828        ensure!(
829            authenticated_owner == Some(spender)
830                || authenticated_application_id.map(AccountOwner::from) == Some(spender),
831            ExecutionError::UnauthenticatedTransferOwner
832        );
833        ensure!(
834            amount > Amount::ZERO,
835            ExecutionError::IncorrectTransferAmount
836        );
837
838        // Debit from allowance
839        let owner_spender = OwnerSpender::new(owner, spender);
840        let allowance = self.allowances.get_mut_or_default(&owner_spender).await?;
841
842        allowance
843            .try_sub_assign(amount)
844            .map_err(|_| ExecutionError::InsufficientAllowance {
845                allowance: *allowance,
846                owner,
847                spender,
848            })?;
849
850        if allowance.is_zero() {
851            self.allowances.remove(&owner_spender)?;
852        }
853
854        // Debit from owner's balance
855        self.debit(&owner, amount).await?;
856
857        // Credit or send message
858        self.credit_or_send_message(owner, recipient, amount).await
859    }
860
861    /// Debits an [`Amount`] of tokens from an account's balance.
862    async fn debit(
863        &mut self,
864        account: &AccountOwner,
865        amount: Amount,
866    ) -> Result<(), ExecutionError> {
867        let balance = if account == &AccountOwner::CHAIN {
868            self.balance.get_mut()
869        } else {
870            self.balances.get_mut(account).await?.ok_or_else(|| {
871                ExecutionError::InsufficientBalance {
872                    balance: Amount::ZERO,
873                    account: *account,
874                }
875            })?
876        };
877
878        balance
879            .try_sub_assign(amount)
880            .map_err(|_| ExecutionError::InsufficientBalance {
881                balance: *balance,
882                account: *account,
883            })?;
884
885        if account != &AccountOwner::CHAIN && balance.is_zero() {
886            self.balances.remove(account)?;
887        }
888
889        Ok(())
890    }
891
892    /// Executes a cross-chain message that represents the recipient's side of an operation.
893    pub async fn execute_message(
894        &mut self,
895        context: MessageContext,
896        message: SystemMessage,
897    ) -> Result<Vec<OutgoingMessage>, ExecutionError> {
898        let mut outcome = Vec::new();
899        use SystemMessage::*;
900        match message {
901            Credit {
902                amount,
903                source,
904                target,
905            } => {
906                let receiver = if context.is_bouncing { source } else { target };
907                self.credit(&receiver, amount).await?;
908            }
909            Withdraw {
910                amount,
911                owner,
912                recipient,
913            } => {
914                self.debit(&owner, amount).await?;
915                if let Some(message) = self
916                    .credit_or_send_message(owner, recipient, amount)
917                    .await?
918                {
919                    outcome.push(message);
920                }
921            }
922            CheckpointAck {
923                latest_received_cursor,
924            } => {
925                // Drop every cursor the recipient has consumed. `split_off(&k)` on a
926                // `BTreeSet<Cursor>` returns the entries `>= k`, so this trims the
927                // strict prefix below `latest_received_cursor` and leaves any
928                // still-unfinalized bundles in place. A recipient that has consumed
929                // everything we ever sent ends up with an empty set and is evicted.
930                if let Some(mut cursors) =
931                    self.unfinalized_message_blocks.get(&context.origin).await?
932                {
933                    let retained = cursors.split_off(&latest_received_cursor);
934                    if retained.is_empty() {
935                        self.unfinalized_message_blocks.remove(&context.origin)?;
936                    } else {
937                        self.unfinalized_message_blocks
938                            .insert(&context.origin, retained)?;
939                    }
940                }
941            }
942        }
943        Ok(outcome)
944    }
945
946    /// Initializes the system application state on a newly opened chain.
947    /// Returns `Ok(true)` if the chain was already initialized, `Ok(false)` if it wasn't.
948    pub async fn initialize_chain(&mut self, chain_id: ChainId) -> Result<bool, ExecutionError> {
949        if self.description.get().await?.is_some() {
950            // already initialized
951            return Ok(true);
952        }
953        let description_blob = self
954            .read_blob_content(BlobId::new(chain_id.0, BlobType::ChainDescription))
955            .await?;
956        let description: ChainDescription = bcs::from_bytes(description_blob.bytes())?;
957        let InitialChainConfig {
958            ownership,
959            epoch,
960            balance,
961            application_permissions,
962        } = description.config().clone();
963        self.progress.get_mut().timestamp = description.timestamp();
964        self.description.set(Some(description));
965        self.epoch.set(epoch);
966
967        let committee_hash = *self
968            .context()
969            .extra()
970            .get_committee_hashes(epoch..=epoch)
971            .await?
972            .get(&epoch)
973            .expect("get_committee_hashes returns the requested epoch on success");
974        let admin_chain_id = self
975            .context()
976            .extra()
977            .get_network_description()
978            .await?
979            .ok_or(ExecutionError::NoNetworkDescriptionFound)?
980            .admin_chain_id;
981
982        self.committee_hash.set(Some(committee_hash));
983        self.admin_chain_id.set(Some(admin_chain_id));
984        self.ownership.set(ownership);
985        self.balance.set(balance);
986        self.application_permissions.set(application_permissions);
987        Ok(false)
988    }
989
990    /// Handles a query to the system state, returning the system response.
991    pub fn handle_query(
992        &mut self,
993        context: QueryContext,
994        _query: SystemQuery,
995    ) -> QueryOutcome<SystemResponse> {
996        let response = SystemResponse {
997            chain_id: context.chain_id,
998            balance: *self.balance.get(),
999        };
1000        QueryOutcome {
1001            response,
1002            operations: vec![],
1003        }
1004    }
1005
1006    /// Returns the messages to open a new chain, and subtracts the new chain's balance
1007    /// from this chain's.
1008    pub async fn open_chain(
1009        &mut self,
1010        config: OpenChainConfig,
1011        parent: ChainId,
1012        block_height: BlockHeight,
1013        timestamp: Timestamp,
1014        txn_tracker: &mut TransactionTracker,
1015    ) -> Result<ChainId, ExecutionError> {
1016        let chain_index = txn_tracker.next_chain_index();
1017        let chain_origin = ChainOrigin::Child {
1018            parent,
1019            block_height,
1020            chain_index,
1021        };
1022        let init_chain_config = config.init_chain_config(*self.epoch.get());
1023        let chain_description = ChainDescription::new(chain_origin, init_chain_config, timestamp);
1024        let child_id = chain_description.id();
1025        self.debit(&AccountOwner::CHAIN, config.balance).await?;
1026        let blob = Blob::new_chain_description(&chain_description);
1027        txn_tracker.add_created_blob(blob);
1028        Ok(child_id)
1029    }
1030
1031    /// Marks the chain as closed.
1032    pub fn close_chain(&mut self) {
1033        self.closed.set(true);
1034    }
1035
1036    /// Creates a new application from the given module and arguments, returning its ID.
1037    pub async fn create_application(
1038        &mut self,
1039        chain_id: ChainId,
1040        block_height: BlockHeight,
1041        module_id: ModuleId,
1042        parameters: Vec<u8>,
1043        required_application_ids: Vec<ApplicationId>,
1044        txn_tracker: &mut TransactionTracker,
1045    ) -> Result<CreateApplicationResult, ExecutionError> {
1046        let application_index = txn_tracker.next_application_index();
1047
1048        let blob_ids = self.check_bytecode_blobs(&module_id, txn_tracker).await?;
1049        // We only remember to register the blobs that aren't recorded in `used_blobs`
1050        // already.
1051        for blob_id in blob_ids {
1052            self.blob_used(txn_tracker, blob_id).await?;
1053        }
1054
1055        let application_description = ApplicationDescription {
1056            module_id,
1057            creator_chain_id: chain_id,
1058            block_height,
1059            application_index,
1060            parameters,
1061            required_application_ids,
1062        };
1063        self.check_required_applications(&application_description, txn_tracker)
1064            .await?;
1065
1066        let blob = Blob::new_application_description(&application_description);
1067        self.used_blobs.insert(&blob.id())?;
1068        txn_tracker.add_created_blob(blob);
1069
1070        Ok(CreateApplicationResult {
1071            app_id: ApplicationId::from(&application_description),
1072        })
1073    }
1074
1075    async fn check_required_applications(
1076        &mut self,
1077        application_description: &ApplicationDescription,
1078        txn_tracker: &mut TransactionTracker,
1079    ) -> Result<(), ExecutionError> {
1080        // Make sure that referenced applications IDs have been registered.
1081        for required_id in &application_description.required_application_ids {
1082            Box::pin(self.describe_application(*required_id, txn_tracker)).await?;
1083        }
1084        Ok(())
1085    }
1086
1087    /// Retrieves an application's description.
1088    pub async fn describe_application(
1089        &mut self,
1090        id: ApplicationId,
1091        txn_tracker: &mut TransactionTracker,
1092    ) -> Result<ApplicationDescription, ExecutionError> {
1093        let blob_id = id.description_blob_id();
1094        let content = match txn_tracker.created_blobs().get(&blob_id) {
1095            Some(content) => content.clone(),
1096            None => self.read_blob_content(blob_id).await?,
1097        };
1098        self.blob_used(txn_tracker, blob_id).await?;
1099        let description: ApplicationDescription = bcs::from_bytes(content.bytes())?;
1100
1101        let blob_ids = self
1102            .check_bytecode_blobs(&description.module_id, txn_tracker)
1103            .await?;
1104        // We only remember to register the blobs that aren't recorded in `used_blobs`
1105        // already.
1106        for blob_id in blob_ids {
1107            self.blob_used(txn_tracker, blob_id).await?;
1108        }
1109
1110        self.check_required_applications(&description, txn_tracker)
1111            .await?;
1112
1113        Ok(description)
1114    }
1115
1116    /// Records a blob that is used in this block. If this is the first use on this chain, creates
1117    /// an oracle response for it.
1118    pub(crate) async fn blob_used(
1119        &mut self,
1120        txn_tracker: &mut TransactionTracker,
1121        blob_id: BlobId,
1122    ) -> Result<bool, ExecutionError> {
1123        if self.used_blobs.contains(&blob_id).await? {
1124            return Ok(false); // Nothing to do.
1125        }
1126        self.used_blobs.insert(&blob_id)?;
1127        txn_tracker.replay_oracle_response(OracleResponse::Blob(blob_id))?;
1128        Ok(true)
1129    }
1130
1131    /// Records a blob that is published in this block. This does not create an oracle entry, and
1132    /// the blob can be used without using an oracle in the future on this chain.
1133    fn blob_published(
1134        &mut self,
1135        blob_id: &BlobId,
1136        txn_tracker: &mut TransactionTracker,
1137    ) -> Result<(), ExecutionError> {
1138        self.used_blobs.insert(blob_id)?;
1139        txn_tracker.add_published_blob(*blob_id);
1140        Ok(())
1141    }
1142
1143    /// Reads the content of the blob with the given ID.
1144    pub async fn read_blob_content(&self, blob_id: BlobId) -> Result<BlobContent, ExecutionError> {
1145        match self.context().extra().get_blob(blob_id).await {
1146            Ok(Some(blob)) => Ok(Arc::unwrap_or_clone(blob).into()),
1147            Ok(None) => Err(ExecutionError::BlobsNotFound(vec![blob_id])),
1148            Err(error) => Err(error.into()),
1149        }
1150    }
1151
1152    /// Returns an error unless a blob with the given ID exists.
1153    pub async fn assert_blob_exists(&mut self, blob_id: BlobId) -> Result<(), ExecutionError> {
1154        if self.context().extra().contains_blob(blob_id).await? {
1155            Ok(())
1156        } else {
1157            Err(ExecutionError::BlobsNotFound(vec![blob_id]))
1158        }
1159    }
1160
1161    async fn check_bytecode_blobs(
1162        &self,
1163        module_id: &ModuleId,
1164        txn_tracker: &TransactionTracker,
1165    ) -> Result<Vec<BlobId>, ExecutionError> {
1166        let blob_ids = module_id.bytecode_blob_ids();
1167
1168        let mut missing_blobs = Vec::new();
1169        for blob_id in &blob_ids {
1170            // First check if blob is present in created_blobs
1171            if txn_tracker.created_blobs().contains_key(blob_id) {
1172                continue; // Blob found in created_blobs, it's ok
1173            }
1174            // If not in created_blobs, check storage
1175            if !self.context().extra().contains_blob(*blob_id).await? {
1176                missing_blobs.push(*blob_id);
1177            }
1178        }
1179        ensure!(
1180            missing_blobs.is_empty(),
1181            ExecutionError::BlobsNotFound(missing_blobs)
1182        );
1183
1184        Ok(blob_ids)
1185    }
1186}