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, Epoch, InitialChainConfig, OracleResponse, Timestamp,
21    },
22    ensure, hex_debug,
23    identifiers::{Account, AccountOwner, BlobId, BlobType, ChainId, EventId, ModuleId, StreamId},
24    ownership::{ChainOwnership, TimeoutConfig},
25};
26use linera_views::{
27    context::Context,
28    lazy_register_view::HashedLazyRegisterView,
29    map_view::HashedMapView,
30    register_view::HashedRegisterView,
31    set_view::HashedSetView,
32    views::{ClonableView, HashableView, ReplaceContext, View},
33    ViewError,
34};
35use serde::{Deserialize, Serialize};
36
37#[cfg(test)]
38use crate::test_utils::SystemExecutionState;
39use crate::{
40    committee::Committee, util::OracleResponseExt as _, ApplicationDescription, ApplicationId,
41    ExecutionError, ExecutionRuntimeContext, MessageContext, MessageKind, OperationContext,
42    OutgoingMessage, QueryContext, QueryOutcome, ResourceController, TransactionTracker,
43};
44
45/// The event stream name for new epochs and committees.
46pub static EPOCH_STREAM_NAME: &[u8] = &[0];
47/// The event stream name for removed epochs.
48pub static REMOVED_EPOCH_STREAM_NAME: &[u8] = &[1];
49
50/// The number of times the [`SystemOperation::OpenChain`] was executed.
51#[cfg(with_metrics)]
52mod metrics {
53    use std::sync::LazyLock;
54
55    use linera_base::prometheus_util::register_int_counter_vec;
56    use prometheus::IntCounterVec;
57
58    pub static OPEN_CHAIN_COUNT: LazyLock<IntCounterVec> = LazyLock::new(|| {
59        register_int_counter_vec(
60            "open_chain_count",
61            "The number of times the `OpenChain` operation was executed",
62            &[],
63        )
64    });
65}
66
67/// A view accessing the execution state of the system of a chain.
68#[derive(Debug, ClonableView, HashableView, Allocative)]
69#[allocative(bound = "C")]
70pub struct SystemExecutionStateView<C> {
71    /// How the chain was created. May be unknown for inactive chains.
72    pub description: HashedLazyRegisterView<C, Option<ChainDescription>>,
73    /// The number identifying the current configuration.
74    pub epoch: HashedRegisterView<C, Epoch>,
75    /// The admin of the chain.
76    pub admin_chain_id: HashedRegisterView<C, Option<ChainId>>,
77    /// The committees that we trust, indexed by epoch number.
78    // Not using a `MapView` because the set active of committees is supposed to be
79    // small. Plus, currently, we would create the `BTreeMap` anyway in various places
80    // (e.g. the `OpenChain` operation).
81    //
82    // Lazy: committee lookups are served from the process-global `SharedCommittees` cache
83    // (via `Storage::get_or_load_committee`), with a fall-back to this view for the
84    // mid-block case in `current_committee`. This view is only loaded when executing an
85    // operation that mutates the map (admin-chain `CreateCommittee`/`RemoveCommittee`, or
86    // `ProcessNewEpoch`/`ProcessRemovedEpoch`).
87    pub committees: HashedLazyRegisterView<C, BTreeMap<Epoch, Committee>>,
88    /// Ownership of the chain.
89    pub ownership: HashedLazyRegisterView<C, ChainOwnership>,
90    /// Balance of the chain. (Available to any user able to create blocks in the chain.)
91    pub balance: HashedRegisterView<C, Amount>,
92    /// Balances attributed to a given owner.
93    pub balances: HashedMapView<C, AccountOwner, Amount>,
94    /// The timestamp of the most recent block.
95    pub timestamp: HashedRegisterView<C, Timestamp>,
96    /// Whether this chain has been closed.
97    pub closed: HashedRegisterView<C, bool>,
98    /// Permissions for applications on this chain.
99    pub application_permissions: HashedLazyRegisterView<C, ApplicationPermissions>,
100    /// Blobs that have been used or published on this chain.
101    pub used_blobs: HashedSetView<C, BlobId>,
102    /// The event stream subscriptions of applications on this chain.
103    pub event_subscriptions: HashedMapView<C, (ChainId, StreamId), EventSubscriptions>,
104}
105
106impl<C: Context, C2: Context> ReplaceContext<C2> for SystemExecutionStateView<C> {
107    type Target = SystemExecutionStateView<C2>;
108
109    async fn with_context(
110        &mut self,
111        ctx: impl FnOnce(&Self::Context) -> C2 + Clone,
112    ) -> Self::Target {
113        SystemExecutionStateView {
114            description: self.description.with_context(ctx.clone()).await,
115            epoch: self.epoch.with_context(ctx.clone()).await,
116            admin_chain_id: self.admin_chain_id.with_context(ctx.clone()).await,
117            committees: self.committees.with_context(ctx.clone()).await,
118            ownership: self.ownership.with_context(ctx.clone()).await,
119            balance: self.balance.with_context(ctx.clone()).await,
120            balances: self.balances.with_context(ctx.clone()).await,
121            timestamp: self.timestamp.with_context(ctx.clone()).await,
122            closed: self.closed.with_context(ctx.clone()).await,
123            application_permissions: self.application_permissions.with_context(ctx.clone()).await,
124            used_blobs: self.used_blobs.with_context(ctx.clone()).await,
125            event_subscriptions: self.event_subscriptions.with_context(ctx.clone()).await,
126        }
127    }
128}
129
130/// The applications subscribing to a particular stream, and the next event index.
131#[derive(Debug, Default, Clone, Serialize, Deserialize, Allocative)]
132pub struct EventSubscriptions {
133    /// The next event index, i.e. the total number of events in this stream that have already
134    /// been processed by this chain.
135    pub next_index: u32,
136    /// The applications that are subscribed to this stream.
137    pub applications: BTreeSet<ApplicationId>,
138}
139
140/// The initial configuration for a new chain.
141#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
142pub struct OpenChainConfig {
143    /// The ownership configuration of the new chain.
144    pub ownership: ChainOwnership,
145    /// The initial chain balance.
146    pub balance: Amount,
147    /// The initial application permissions.
148    pub application_permissions: ApplicationPermissions,
149}
150
151impl OpenChainConfig {
152    /// Creates an [`InitialChainConfig`] based on this [`OpenChainConfig`] and additional
153    /// parameters.
154    pub fn init_chain_config(
155        &self,
156        epoch: Epoch,
157        min_active_epoch: Epoch,
158        max_active_epoch: Epoch,
159    ) -> InitialChainConfig {
160        InitialChainConfig {
161            application_permissions: self.application_permissions.clone(),
162            balance: self.balance,
163            epoch,
164            min_active_epoch,
165            max_active_epoch,
166            ownership: self.ownership.clone(),
167        }
168    }
169}
170
171/// A system operation.
172#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
173#[allow(missing_docs)]
174pub enum SystemOperation {
175    /// Transfers `amount` units of value from the given owner's account to the recipient.
176    /// If no owner is given, try to take the units out of the unattributed account.
177    Transfer {
178        owner: AccountOwner,
179        recipient: Account,
180        amount: Amount,
181    },
182    /// Claims `amount` units of value from the given owner's account in the remote
183    /// `target` chain. Depending on its configuration, the `target` chain may refuse to
184    /// process the message.
185    Claim {
186        owner: AccountOwner,
187        target_id: ChainId,
188        recipient: Account,
189        amount: Amount,
190    },
191    /// Creates (or activates) a new chain.
192    /// This will automatically subscribe to the future committees created by `admin_chain_id`.
193    OpenChain(OpenChainConfig),
194    /// Closes the chain.
195    CloseChain,
196    /// Changes the ownership of the chain.
197    ChangeOwnership {
198        /// Super owners can propose fast blocks in the first round, and regular blocks in any round.
199        #[debug(skip_if = Vec::is_empty)]
200        super_owners: Vec<AccountOwner>,
201        /// The regular owners, with their weights that determine how often they are round leader.
202        #[debug(skip_if = Vec::is_empty)]
203        owners: Vec<(AccountOwner, u64)>,
204        /// The number of initial rounds after 0 in which all owners are allowed to propose blocks.
205        multi_leader_rounds: u32,
206        /// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
207        /// This should only be `true` on chains with restrictive application permissions and an
208        /// application-based mechanism to select block proposers.
209        open_multi_leader_rounds: bool,
210        /// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
211        timeout_config: TimeoutConfig,
212    },
213    /// Changes the application permissions configuration on this chain.
214    ChangeApplicationPermissions(ApplicationPermissions),
215    /// Publishes a new application module.
216    PublishModule { module_id: ModuleId },
217    /// Publishes a new data blob.
218    PublishDataBlob { blob_hash: CryptoHash },
219    /// Verifies that the given blob exists. Otherwise the block fails.
220    VerifyBlob { blob_id: BlobId },
221    /// Creates a new application.
222    CreateApplication {
223        module_id: ModuleId,
224        #[serde(with = "serde_bytes")]
225        #[debug(with = "hex_debug")]
226        parameters: Vec<u8>,
227        #[serde(with = "serde_bytes")]
228        #[debug(with = "hex_debug", skip_if = Vec::is_empty)]
229        instantiation_argument: Vec<u8>,
230        #[debug(skip_if = Vec::is_empty)]
231        required_application_ids: Vec<ApplicationId>,
232    },
233    /// Operations that are only allowed on the admin chain.
234    Admin(AdminOperation),
235    /// Processes an event about a new epoch and committee.
236    ProcessNewEpoch(Epoch),
237    /// Processes an event about a removed epoch and committee.
238    ProcessRemovedEpoch(Epoch),
239    /// Updates the event stream trackers.
240    UpdateStreams(Vec<(ChainId, StreamId, u32)>),
241}
242
243/// Operations that are only allowed on the admin chain.
244#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
245#[allow(missing_docs)]
246pub enum AdminOperation {
247    /// Publishes a new committee as a blob. This can be assigned to an epoch using
248    /// [`AdminOperation::CreateCommittee`] in a later block.
249    PublishCommitteeBlob { blob_hash: CryptoHash },
250    /// Registers a new committee. Other chains can then migrate to the new epoch by executing
251    /// [`SystemOperation::ProcessNewEpoch`].
252    CreateCommittee { epoch: Epoch, blob_hash: CryptoHash },
253    /// Removes a committee. Other chains should execute [`SystemOperation::ProcessRemovedEpoch`],
254    /// so that blocks from the retired epoch will not be accepted until they are followed (hence
255    /// re-certified) by a block certified by a recent committee.
256    RemoveCommittee { epoch: Epoch },
257}
258
259/// A system message meant to be executed on a remote chain.
260#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
261#[allow(missing_docs)]
262pub enum SystemMessage {
263    /// Credits `amount` units of value to the account `target` -- unless the message is
264    /// bouncing, in which case `source` is credited instead.
265    Credit {
266        target: AccountOwner,
267        amount: Amount,
268        source: AccountOwner,
269    },
270    /// Withdraws `amount` units of value from the account and starts a transfer to credit
271    /// the recipient. The message must be properly authenticated. Receiver chains may
272    /// refuse it depending on their configuration.
273    Withdraw {
274        owner: AccountOwner,
275        amount: Amount,
276        recipient: Account,
277    },
278}
279
280/// A query to the system state.
281#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
282pub struct SystemQuery;
283
284/// The response to a system query.
285#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
286#[allow(missing_docs)]
287pub struct SystemResponse {
288    pub chain_id: ChainId,
289    pub balance: Amount,
290}
291
292/// Optional user message attached to a transfer.
293#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Default, Debug, Serialize, Deserialize)]
294pub struct UserData(pub Option<[u8; 32]>);
295
296/// The result of creating a new application.
297#[derive(Debug)]
298#[allow(missing_docs)]
299pub struct CreateApplicationResult {
300    pub app_id: ApplicationId,
301}
302
303impl<C> SystemExecutionStateView<C>
304where
305    C: Context + Clone + 'static,
306    C::Extra: ExecutionRuntimeContext,
307{
308    /// Invariant for the states of active chains.
309    pub async fn is_active(&self) -> Result<bool, ViewError> {
310        Ok(self.description.get().await?.is_some()
311            && self.ownership.get().await?.is_active()
312            && self.current_committee().await?.is_some()
313            && self.admin_chain_id.get().is_some())
314    }
315
316    /// Returns the chain's current epoch together with its committee.
317    ///
318    /// Serves lookups from the process-global [`crate::SharedCommittees`] cache, falling
319    /// back to the `NewCommittee` event on the admin chain (or the genesis committee blob
320    /// for epoch 0). If neither source has the committee — which happens when a
321    /// `CreateCommittee`/`ProcessNewEpoch` op earlier in the current block created it, but
322    /// the block hasn't been saved yet so the `NewCommittee` event isn't persisted (and
323    /// we deliberately don't populate the shared cache from an unconfirmed block) — fall
324    /// back to reading the chain's own `committees` view, where the new committee sits as
325    /// a pending update.
326    pub async fn current_committee(&self) -> Result<Option<(Epoch, Arc<Committee>)>, ViewError> {
327        let epoch = *self.epoch.get();
328        if let Some(committee) = self.context().extra().get_or_load_committee(epoch).await? {
329            return Ok(Some((epoch, committee)));
330        }
331        let Some(committee) = self.committees.get().await?.get(&epoch).cloned() else {
332            return Ok(None);
333        };
334        Ok(Some((epoch, Arc::new(committee))))
335    }
336
337    async fn get_event(&self, event_id: EventId) -> Result<Arc<Vec<u8>>, ExecutionError> {
338        match self.context().extra().get_event(event_id.clone()).await? {
339            None => Err(ExecutionError::EventsNotFound(vec![event_id])),
340            Some(vec) => Ok(vec),
341        }
342    }
343
344    /// Executes the sender's side of an operation and returns a list of actions to be
345    /// taken.
346    pub async fn execute_operation(
347        &mut self,
348        context: OperationContext,
349        operation: SystemOperation,
350        txn_tracker: &mut TransactionTracker,
351        resource_controller: &mut ResourceController<Option<AccountOwner>>,
352    ) -> Result<Option<(ApplicationId, Vec<u8>)>, ExecutionError> {
353        use SystemOperation::*;
354        let mut new_application = None;
355        match operation {
356            OpenChain(config) => {
357                let _chain_id = self
358                    .open_chain(
359                        config,
360                        context.chain_id,
361                        context.height,
362                        context.timestamp,
363                        txn_tracker,
364                    )
365                    .await?;
366                #[cfg(with_metrics)]
367                metrics::OPEN_CHAIN_COUNT.with_label_values(&[]).inc();
368            }
369            ChangeOwnership {
370                super_owners,
371                owners,
372                multi_leader_rounds,
373                open_multi_leader_rounds,
374                timeout_config,
375            } => {
376                self.ownership.set(ChainOwnership {
377                    super_owners: super_owners.into_iter().collect(),
378                    owners: owners.into_iter().collect(),
379                    multi_leader_rounds,
380                    open_multi_leader_rounds,
381                    timeout_config,
382                });
383            }
384            ChangeApplicationPermissions(application_permissions) => {
385                self.application_permissions.set(application_permissions);
386            }
387            CloseChain => self.close_chain()?,
388            Transfer {
389                owner,
390                amount,
391                recipient,
392            } => {
393                let maybe_message = self
394                    .transfer(context.authenticated_signer, None, owner, recipient, amount)
395                    .await?;
396                txn_tracker.add_outgoing_messages(maybe_message);
397            }
398            Claim {
399                owner,
400                target_id,
401                recipient,
402                amount,
403            } => {
404                let maybe_message = self
405                    .claim(
406                        context.authenticated_signer,
407                        None,
408                        owner,
409                        target_id,
410                        recipient,
411                        amount,
412                    )
413                    .await?;
414                txn_tracker.add_outgoing_messages(maybe_message);
415            }
416            Admin(admin_operation) => {
417                ensure!(
418                    *self.admin_chain_id.get() == Some(context.chain_id),
419                    ExecutionError::AdminOperationOnNonAdminChain
420                );
421                match admin_operation {
422                    AdminOperation::PublishCommitteeBlob { blob_hash } => {
423                        self.blob_published(
424                            &BlobId::new(blob_hash, BlobType::Committee),
425                            txn_tracker,
426                        )?;
427                    }
428                    AdminOperation::CreateCommittee { epoch, blob_hash } => {
429                        self.check_next_epoch(epoch)?;
430                        let blob_id = BlobId::new(blob_hash, BlobType::Committee);
431                        let committee =
432                            bcs::from_bytes(self.read_blob_content(blob_id).await?.bytes())?;
433                        self.blob_used(txn_tracker, blob_id).await?;
434                        self.committees.get_mut().await?.insert(epoch, committee);
435                        self.epoch.set(epoch);
436                        txn_tracker.add_event(
437                            StreamId::system(EPOCH_STREAM_NAME),
438                            epoch.0,
439                            bcs::to_bytes(&blob_hash)?,
440                        );
441                    }
442                    AdminOperation::RemoveCommittee { epoch } => {
443                        ensure!(
444                            self.committees.get_mut().await?.remove(&epoch).is_some(),
445                            ExecutionError::InvalidCommitteeRemoval
446                        );
447                        txn_tracker.add_event(
448                            StreamId::system(REMOVED_EPOCH_STREAM_NAME),
449                            epoch.0,
450                            vec![],
451                        );
452                    }
453                }
454            }
455            PublishModule { module_id } => {
456                for blob_id in module_id.bytecode_blob_ids() {
457                    self.blob_published(&blob_id, txn_tracker)?;
458                }
459            }
460            CreateApplication {
461                module_id,
462                parameters,
463                instantiation_argument,
464                required_application_ids,
465            } => {
466                let CreateApplicationResult { app_id } = self
467                    .create_application(
468                        context.chain_id,
469                        context.height,
470                        module_id,
471                        parameters,
472                        required_application_ids,
473                        txn_tracker,
474                    )
475                    .await?;
476                new_application = Some((app_id, instantiation_argument));
477            }
478            PublishDataBlob { blob_hash } => {
479                self.blob_published(&BlobId::new(blob_hash, BlobType::Data), txn_tracker)?;
480            }
481            VerifyBlob { blob_id } => {
482                self.assert_blob_exists(blob_id).await?;
483                resource_controller
484                    .with_state(self)
485                    .await?
486                    .track_blob_read(0)?;
487                self.blob_used(txn_tracker, blob_id).await?;
488            }
489            ProcessNewEpoch(epoch) => {
490                self.check_next_epoch(epoch)?;
491                let admin_chain_id = self.admin_chain_id.get().ok_or_else(|| {
492                    ExecutionError::InternalError(
493                        "execute_operation called for uninitialized chain",
494                    )
495                })?;
496                let event_id = EventId {
497                    chain_id: admin_chain_id,
498                    stream_id: StreamId::system(EPOCH_STREAM_NAME),
499                    index: epoch.0,
500                };
501                let bytes = txn_tracker
502                    .oracle(|| async {
503                        let bytes = self.get_event(event_id.clone()).await?;
504                        Ok(OracleResponse::Event(
505                            event_id.clone(),
506                            Arc::unwrap_or_clone(bytes),
507                        ))
508                    })
509                    .await?
510                    .to_event(&event_id)?;
511                let blob_id = BlobId::new(bcs::from_bytes(&bytes)?, BlobType::Committee);
512                let committee = bcs::from_bytes(self.read_blob_content(blob_id).await?.bytes())?;
513                self.blob_used(txn_tracker, blob_id).await?;
514                self.committees.get_mut().await?.insert(epoch, committee);
515                self.epoch.set(epoch);
516            }
517            ProcessRemovedEpoch(epoch) => {
518                ensure!(
519                    self.committees.get_mut().await?.remove(&epoch).is_some(),
520                    ExecutionError::InvalidCommitteeRemoval
521                );
522                let admin_chain_id = self.admin_chain_id.get().ok_or_else(|| {
523                    ExecutionError::InternalError(
524                        "execute_operation called for uninitialized chain",
525                    )
526                })?;
527                let event_id = EventId {
528                    chain_id: admin_chain_id,
529                    stream_id: StreamId::system(REMOVED_EPOCH_STREAM_NAME),
530                    index: epoch.0,
531                };
532                txn_tracker
533                    .oracle(|| async {
534                        let bytes = self.get_event(event_id.clone()).await?;
535                        Ok(OracleResponse::Event(event_id, Arc::unwrap_or_clone(bytes)))
536                    })
537                    .await?;
538            }
539            UpdateStreams(streams) => {
540                let mut missing_events = Vec::new();
541                for (chain_id, stream_id, next_index) in streams {
542                    let subscriptions = self
543                        .event_subscriptions
544                        .get_mut_or_default(&(chain_id, stream_id.clone()))
545                        .await?;
546                    ensure!(
547                        subscriptions.next_index < next_index,
548                        ExecutionError::OutdatedUpdateStreams
549                    );
550                    for application_id in &subscriptions.applications {
551                        txn_tracker.add_stream_to_process(
552                            *application_id,
553                            chain_id,
554                            stream_id.clone(),
555                            subscriptions.next_index,
556                            next_index,
557                        );
558                    }
559                    subscriptions.next_index = next_index;
560                    let index = next_index
561                        .checked_sub(1)
562                        .ok_or(ArithmeticError::Underflow)?;
563                    let event_id = EventId {
564                        chain_id,
565                        stream_id,
566                        index,
567                    };
568                    let extra = self.context().extra();
569                    txn_tracker
570                        .oracle(|| async {
571                            if !extra.contains_event(event_id.clone()).await? {
572                                missing_events.push(event_id.clone());
573                            }
574                            Ok(OracleResponse::EventExists(event_id))
575                        })
576                        .await?;
577                }
578                ensure!(
579                    missing_events.is_empty(),
580                    ExecutionError::EventsNotFound(missing_events)
581                );
582            }
583        }
584
585        Ok(new_application)
586    }
587
588    /// Returns an error if the `provided` epoch is not exactly one higher than the chain's current
589    /// epoch.
590    fn check_next_epoch(&self, provided: Epoch) -> Result<(), ExecutionError> {
591        let expected = self.epoch.get().try_add_one()?;
592        ensure!(
593            provided == expected,
594            ExecutionError::InvalidCommitteeEpoch { provided, expected }
595        );
596        Ok(())
597    }
598
599    async fn credit(&mut self, owner: &AccountOwner, amount: Amount) -> Result<(), ExecutionError> {
600        if owner == &AccountOwner::CHAIN {
601            let new_balance = self.balance.get().saturating_add(amount);
602            self.balance.set(new_balance);
603        } else {
604            let balance = self.balances.get_mut_or_default(owner).await?;
605            *balance = balance.saturating_add(amount);
606        }
607        Ok(())
608    }
609
610    async fn credit_or_send_message(
611        &mut self,
612        source: AccountOwner,
613        recipient: Account,
614        amount: Amount,
615    ) -> Result<Option<OutgoingMessage>, ExecutionError> {
616        let source_chain_id = self.context().extra().chain_id();
617        if recipient.chain_id == source_chain_id {
618            // Handle same-chain transfer locally.
619            let target = recipient.owner;
620            self.credit(&target, amount).await?;
621            Ok(None)
622        } else {
623            // Handle cross-chain transfer with message.
624            let message = SystemMessage::Credit {
625                amount,
626                source,
627                target: recipient.owner,
628            };
629            Ok(Some(
630                OutgoingMessage::new(recipient.chain_id, message).with_kind(MessageKind::Tracked),
631            ))
632        }
633    }
634
635    /// Transfers `amount` from `source` to `recipient`, debiting the source account.
636    pub async fn transfer(
637        &mut self,
638        authenticated_signer: Option<AccountOwner>,
639        authenticated_application_id: Option<ApplicationId>,
640        source: AccountOwner,
641        recipient: Account,
642        amount: Amount,
643    ) -> Result<Option<OutgoingMessage>, ExecutionError> {
644        if source == AccountOwner::CHAIN {
645            let authenticated_signer =
646                authenticated_signer.ok_or(ExecutionError::UnauthenticatedTransferOwner)?;
647            ensure!(
648                self.ownership
649                    .get()
650                    .await?
651                    .verify_owner(&authenticated_signer),
652                ExecutionError::UnauthenticatedTransferOwner
653            );
654        } else {
655            ensure!(
656                authenticated_signer == Some(source)
657                    || authenticated_application_id.map(AccountOwner::from) == Some(source),
658                ExecutionError::UnauthenticatedTransferOwner
659            );
660        }
661        ensure!(
662            amount > Amount::ZERO,
663            ExecutionError::IncorrectTransferAmount
664        );
665        self.debit(&source, amount).await?;
666        self.credit_or_send_message(source, recipient, amount).await
667    }
668
669    /// Claims `amount` from `source`'s account on `target_id` and transfers it to `recipient`.
670    pub async fn claim(
671        &mut self,
672        authenticated_signer: Option<AccountOwner>,
673        authenticated_application_id: Option<ApplicationId>,
674        source: AccountOwner,
675        target_id: ChainId,
676        recipient: Account,
677        amount: Amount,
678    ) -> Result<Option<OutgoingMessage>, ExecutionError> {
679        ensure!(
680            authenticated_signer == Some(source)
681                || authenticated_application_id.map(AccountOwner::from) == Some(source),
682            ExecutionError::UnauthenticatedClaimOwner
683        );
684        ensure!(amount > Amount::ZERO, ExecutionError::IncorrectClaimAmount);
685
686        let current_chain_id = self.context().extra().chain_id();
687        if target_id == current_chain_id {
688            // Handle same-chain claim locally by processing the withdraw operation directly
689            self.debit(&source, amount).await?;
690            self.credit_or_send_message(source, recipient, amount).await
691        } else {
692            // Handle cross-chain claim with Withdraw message
693            let message = SystemMessage::Withdraw {
694                amount,
695                owner: source,
696                recipient,
697            };
698            Ok(Some(
699                OutgoingMessage::new(target_id, message)
700                    .with_authenticated_signer(authenticated_signer),
701            ))
702        }
703    }
704
705    /// Debits an [`Amount`] of tokens from an account's balance.
706    async fn debit(
707        &mut self,
708        account: &AccountOwner,
709        amount: Amount,
710    ) -> Result<(), ExecutionError> {
711        let balance = if account == &AccountOwner::CHAIN {
712            self.balance.get_mut()
713        } else {
714            self.balances.get_mut(account).await?.ok_or_else(|| {
715                ExecutionError::InsufficientBalance {
716                    balance: Amount::ZERO,
717                    account: *account,
718                }
719            })?
720        };
721
722        balance
723            .try_sub_assign(amount)
724            .map_err(|_| ExecutionError::InsufficientBalance {
725                balance: *balance,
726                account: *account,
727            })?;
728
729        if account != &AccountOwner::CHAIN && balance.is_zero() {
730            self.balances.remove(account)?;
731        }
732
733        Ok(())
734    }
735
736    /// Executes a cross-chain message that represents the recipient's side of an operation.
737    pub async fn execute_message(
738        &mut self,
739        context: MessageContext,
740        message: SystemMessage,
741    ) -> Result<Vec<OutgoingMessage>, ExecutionError> {
742        let mut outcome = Vec::new();
743        use SystemMessage::*;
744        match message {
745            Credit {
746                amount,
747                source,
748                target,
749            } => {
750                let receiver = if context.is_bouncing { source } else { target };
751                self.credit(&receiver, amount).await?;
752            }
753            Withdraw {
754                amount,
755                owner,
756                recipient,
757            } => {
758                self.debit(&owner, amount).await?;
759                if let Some(message) = self
760                    .credit_or_send_message(owner, recipient, amount)
761                    .await?
762                {
763                    outcome.push(message);
764                }
765            }
766        }
767        Ok(outcome)
768    }
769
770    /// Initializes the system application state on a newly opened chain.
771    /// Returns `Ok(true)` if the chain was already initialized, `Ok(false)` if it wasn't.
772    pub async fn initialize_chain(&mut self, chain_id: ChainId) -> Result<bool, ExecutionError> {
773        if self.description.get().await?.is_some() {
774            // already initialized
775            return Ok(true);
776        }
777        let description_blob = self
778            .read_blob_content(BlobId::new(chain_id.0, BlobType::ChainDescription))
779            .await?;
780        let description: ChainDescription = bcs::from_bytes(description_blob.bytes())?;
781        let InitialChainConfig {
782            ownership,
783            epoch,
784            balance,
785            min_active_epoch,
786            max_active_epoch,
787            application_permissions,
788        } = description.config().clone();
789        self.timestamp.set(description.timestamp());
790        self.description.set(Some(description));
791        self.epoch.set(epoch);
792        let committees = self
793            .context()
794            .extra()
795            .get_committees(min_active_epoch..=max_active_epoch)
796            .await?;
797        let admin_chain_id = self
798            .context()
799            .extra()
800            .get_network_description()
801            .await?
802            .ok_or(ExecutionError::NoNetworkDescriptionFound)?
803            .admin_chain_id;
804
805        self.committees.set(committees);
806        self.admin_chain_id.set(Some(admin_chain_id));
807        self.ownership.set(ownership);
808        self.balance.set(balance);
809        self.application_permissions.set(application_permissions);
810        Ok(false)
811    }
812
813    /// Handles a query to the system state, returning the system response.
814    pub fn handle_query(
815        &mut self,
816        context: QueryContext,
817        _query: SystemQuery,
818    ) -> Result<QueryOutcome<SystemResponse>, ExecutionError> {
819        let response = SystemResponse {
820            chain_id: context.chain_id,
821            balance: *self.balance.get(),
822        };
823        Ok(QueryOutcome {
824            response,
825            operations: vec![],
826        })
827    }
828
829    /// Returns the messages to open a new chain, and subtracts the new chain's balance
830    /// from this chain's.
831    pub async fn open_chain(
832        &mut self,
833        config: OpenChainConfig,
834        parent: ChainId,
835        block_height: BlockHeight,
836        timestamp: Timestamp,
837        txn_tracker: &mut TransactionTracker,
838    ) -> Result<ChainId, ExecutionError> {
839        let chain_index = txn_tracker.next_chain_index();
840        let chain_origin = ChainOrigin::Child {
841            parent,
842            block_height,
843            chain_index,
844        };
845        let committees = self.committees.get().await?;
846        let init_chain_config = config.init_chain_config(
847            *self.epoch.get(),
848            committees.keys().min().copied().unwrap_or(Epoch::ZERO),
849            committees.keys().max().copied().unwrap_or(Epoch::ZERO),
850        );
851        let chain_description = ChainDescription::new(chain_origin, init_chain_config, timestamp);
852        let child_id = chain_description.id();
853        self.debit(&AccountOwner::CHAIN, config.balance).await?;
854        let blob = Blob::new_chain_description(&chain_description);
855        txn_tracker.add_created_blob(blob);
856        Ok(child_id)
857    }
858
859    /// Marks the chain as closed.
860    pub fn close_chain(&mut self) -> Result<(), ExecutionError> {
861        self.closed.set(true);
862        Ok(())
863    }
864
865    /// Creates a new application from the given module and arguments, returning its ID.
866    pub async fn create_application(
867        &mut self,
868        chain_id: ChainId,
869        block_height: BlockHeight,
870        module_id: ModuleId,
871        parameters: Vec<u8>,
872        required_application_ids: Vec<ApplicationId>,
873        txn_tracker: &mut TransactionTracker,
874    ) -> Result<CreateApplicationResult, ExecutionError> {
875        let application_index = txn_tracker.next_application_index();
876
877        let blob_ids = self.check_bytecode_blobs(&module_id, txn_tracker).await?;
878        // We only remember to register the blobs that aren't recorded in `used_blobs`
879        // already.
880        for blob_id in blob_ids {
881            self.blob_used(txn_tracker, blob_id).await?;
882        }
883
884        let application_description = ApplicationDescription {
885            module_id,
886            creator_chain_id: chain_id,
887            block_height,
888            application_index,
889            parameters,
890            required_application_ids,
891        };
892        self.check_required_applications(&application_description, txn_tracker)
893            .await?;
894
895        let blob = Blob::new_application_description(&application_description);
896        self.used_blobs.insert(&blob.id())?;
897        txn_tracker.add_created_blob(blob);
898
899        Ok(CreateApplicationResult {
900            app_id: ApplicationId::from(&application_description),
901        })
902    }
903
904    async fn check_required_applications(
905        &mut self,
906        application_description: &ApplicationDescription,
907        txn_tracker: &mut TransactionTracker,
908    ) -> Result<(), ExecutionError> {
909        // Make sure that referenced applications IDs have been registered.
910        for required_id in &application_description.required_application_ids {
911            Box::pin(self.describe_application(*required_id, txn_tracker)).await?;
912        }
913        Ok(())
914    }
915
916    /// Retrieves an application's description.
917    pub async fn describe_application(
918        &mut self,
919        id: ApplicationId,
920        txn_tracker: &mut TransactionTracker,
921    ) -> Result<ApplicationDescription, ExecutionError> {
922        let blob_id = id.description_blob_id();
923        let content = match txn_tracker.created_blobs().get(&blob_id) {
924            Some(content) => content.clone(),
925            None => self.read_blob_content(blob_id).await?,
926        };
927        self.blob_used(txn_tracker, blob_id).await?;
928        let description: ApplicationDescription = bcs::from_bytes(content.bytes())?;
929
930        let blob_ids = self
931            .check_bytecode_blobs(&description.module_id, txn_tracker)
932            .await?;
933        // We only remember to register the blobs that aren't recorded in `used_blobs`
934        // already.
935        for blob_id in blob_ids {
936            self.blob_used(txn_tracker, blob_id).await?;
937        }
938
939        self.check_required_applications(&description, txn_tracker)
940            .await?;
941
942        Ok(description)
943    }
944
945    /// Records a blob that is used in this block. If this is the first use on this chain, creates
946    /// an oracle response for it.
947    pub(crate) async fn blob_used(
948        &mut self,
949        txn_tracker: &mut TransactionTracker,
950        blob_id: BlobId,
951    ) -> Result<bool, ExecutionError> {
952        if self.used_blobs.contains(&blob_id).await? {
953            return Ok(false); // Nothing to do.
954        }
955        self.used_blobs.insert(&blob_id)?;
956        txn_tracker.replay_oracle_response(OracleResponse::Blob(blob_id))?;
957        Ok(true)
958    }
959
960    /// Records a blob that is published in this block. This does not create an oracle entry, and
961    /// the blob can be used without using an oracle in the future on this chain.
962    fn blob_published(
963        &mut self,
964        blob_id: &BlobId,
965        txn_tracker: &mut TransactionTracker,
966    ) -> Result<(), ExecutionError> {
967        self.used_blobs.insert(blob_id)?;
968        txn_tracker.add_published_blob(*blob_id);
969        Ok(())
970    }
971
972    /// Reads the content of the blob with the given ID.
973    pub async fn read_blob_content(&self, blob_id: BlobId) -> Result<BlobContent, ExecutionError> {
974        match self.context().extra().get_blob(blob_id).await {
975            Ok(Some(blob)) => Ok(Arc::unwrap_or_clone(blob).into()),
976            Ok(None) => Err(ExecutionError::BlobsNotFound(vec![blob_id])),
977            Err(error) => Err(error.into()),
978        }
979    }
980
981    /// Returns an error unless a blob with the given ID exists.
982    pub async fn assert_blob_exists(&mut self, blob_id: BlobId) -> Result<(), ExecutionError> {
983        if self.context().extra().contains_blob(blob_id).await? {
984            Ok(())
985        } else {
986            Err(ExecutionError::BlobsNotFound(vec![blob_id]))
987        }
988    }
989
990    async fn check_bytecode_blobs(
991        &self,
992        module_id: &ModuleId,
993        txn_tracker: &TransactionTracker,
994    ) -> Result<Vec<BlobId>, ExecutionError> {
995        let blob_ids = module_id.bytecode_blob_ids();
996
997        let mut missing_blobs = Vec::new();
998        for blob_id in &blob_ids {
999            // First check if blob is present in created_blobs
1000            if txn_tracker.created_blobs().contains_key(blob_id) {
1001                continue; // Blob found in created_blobs, it's ok
1002            }
1003            // If not in created_blobs, check storage
1004            if !self.context().extra().contains_blob(*blob_id).await? {
1005                missing_blobs.push(*blob_id);
1006            }
1007        }
1008        ensure!(
1009            missing_blobs.is_empty(),
1010            ExecutionError::BlobsNotFound(missing_blobs)
1011        );
1012
1013        Ok(blob_ids)
1014    }
1015}