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