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(with_metrics)]
6use std::sync::LazyLock;
7use std::{
8    collections::BTreeMap,
9    fmt::{self, Display, Formatter},
10    iter,
11};
12
13use async_graphql::Enum;
14use custom_debug_derive::Debug;
15use linera_base::{
16    crypto::{CryptoHash, PublicKey},
17    data_types::{
18        Amount, ApplicationPermissions, ArithmeticError, BlobContent, OracleResponse, Timestamp,
19    },
20    ensure, hex_debug,
21    identifiers::{
22        Account, BlobId, BlobType, BytecodeId, ChainDescription, ChainId, MessageId, Owner,
23    },
24    ownership::{ChainOwnership, TimeoutConfig},
25};
26use linera_views::{
27    context::Context,
28    map_view::HashedMapView,
29    register_view::HashedRegisterView,
30    set_view::HashedSetView,
31    views::{ClonableView, HashableView, View, ViewError},
32};
33use serde::{Deserialize, Serialize};
34use thiserror::Error;
35#[cfg(with_metrics)]
36use {linera_base::prometheus_util, prometheus::IntCounterVec};
37
38#[cfg(test)]
39use crate::test_utils::SystemExecutionState;
40use crate::{
41    committee::{Committee, Epoch},
42    ApplicationRegistryView, ChannelName, ChannelSubscription, Destination,
43    ExecutionRuntimeContext, MessageContext, MessageKind, OperationContext, QueryContext,
44    RawExecutionOutcome, RawOutgoingMessage, TransactionTracker, UserApplicationDescription,
45    UserApplicationId,
46};
47
48/// The relative index of the `OpenChain` message created by the `OpenChain` operation.
49pub static OPEN_CHAIN_MESSAGE_INDEX: u32 = 0;
50/// The relative index of the `ApplicationCreated` message created by the `CreateApplication`
51/// operation.
52pub static CREATE_APPLICATION_MESSAGE_INDEX: u32 = 0;
53
54/// The number of times the [`SystemOperation::OpenChain`] was executed.
55#[cfg(with_metrics)]
56static OPEN_CHAIN_COUNT: LazyLock<IntCounterVec> = LazyLock::new(|| {
57    prometheus_util::register_int_counter_vec(
58        "open_chain_count",
59        "The number of times the `OpenChain` operation was executed",
60        &[],
61    )
62    .expect("Counter creation should not fail")
63});
64
65/// A view accessing the execution state of the system of a chain.
66#[derive(Debug, ClonableView, HashableView)]
67pub struct SystemExecutionStateView<C> {
68    /// How the chain was created. May be unknown for inactive chains.
69    pub description: HashedRegisterView<C, Option<ChainDescription>>,
70    /// The number identifying the current configuration.
71    pub epoch: HashedRegisterView<C, Option<Epoch>>,
72    /// The admin of the chain.
73    pub admin_id: HashedRegisterView<C, Option<ChainId>>,
74    /// Track the channels that we have subscribed to.
75    pub subscriptions: HashedSetView<C, ChannelSubscription>,
76    /// The committees that we trust, indexed by epoch number.
77    // Not using a `MapView` because the set active of committees is supposed to be
78    // small. Plus, currently, we would create the `BTreeMap` anyway in various places
79    // (e.g. the `OpenChain` operation).
80    pub committees: HashedRegisterView<C, BTreeMap<Epoch, Committee>>,
81    /// Ownership of the chain.
82    pub ownership: HashedRegisterView<C, ChainOwnership>,
83    /// Balance of the chain. (Available to any user able to create blocks in the chain.)
84    pub balance: HashedRegisterView<C, Amount>,
85    /// Balances attributed to a given owner.
86    pub balances: HashedMapView<C, Owner, Amount>,
87    /// The timestamp of the most recent block.
88    pub timestamp: HashedRegisterView<C, Timestamp>,
89    /// Track the locations of known bytecodes as well as the descriptions of known applications.
90    pub registry: ApplicationRegistryView<C>,
91    /// Whether this chain has been closed.
92    pub closed: HashedRegisterView<C, bool>,
93    /// Permissions for applications on this chain.
94    pub application_permissions: HashedRegisterView<C, ApplicationPermissions>,
95}
96
97/// The configuration for a new chain.
98#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
99pub struct OpenChainConfig {
100    pub ownership: ChainOwnership,
101    pub admin_id: ChainId,
102    pub epoch: Epoch,
103    pub committees: BTreeMap<Epoch, Committee>,
104    pub balance: Amount,
105    pub application_permissions: ApplicationPermissions,
106}
107
108/// A system operation.
109#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
110pub enum SystemOperation {
111    /// Transfers `amount` units of value from the given owner's account to the recipient.
112    /// If no owner is given, try to take the units out of the unattributed account.
113    Transfer {
114        owner: Option<Owner>,
115        recipient: Recipient,
116        amount: Amount,
117    },
118    /// Claims `amount` units of value from the given owner's account in the remote
119    /// `target` chain. Depending on its configuration, the `target` chain may refuse to
120    /// process the message.
121    Claim {
122        owner: Owner,
123        target_id: ChainId,
124        recipient: Recipient,
125        amount: Amount,
126    },
127    /// Creates (or activates) a new chain.
128    /// This will automatically subscribe to the future committees created by `admin_id`.
129    OpenChain(OpenChainConfig),
130    /// Closes the chain.
131    CloseChain,
132    /// Changes the ownership of the chain.
133    ChangeOwnership {
134        /// Super owners can propose fast blocks in the first round, and regular blocks in any round.
135        super_owners: Vec<PublicKey>,
136        /// The regular owners, with their weights that determine how often they are round leader.
137        owners: Vec<(PublicKey, u64)>,
138        /// The number of initial rounds after 0 in which all owners are allowed to propose blocks.
139        multi_leader_rounds: u32,
140        /// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
141        timeout_config: TimeoutConfig,
142    },
143    /// Changes the application permissions configuration on this chain.
144    ChangeApplicationPermissions(ApplicationPermissions),
145    /// Subscribes to a system channel.
146    Subscribe {
147        chain_id: ChainId,
148        channel: SystemChannel,
149    },
150    /// Unsubscribes from a system channel.
151    Unsubscribe {
152        chain_id: ChainId,
153        channel: SystemChannel,
154    },
155    /// Publishes a new application bytecode.
156    PublishBytecode { bytecode_id: BytecodeId },
157    /// Publishes a new data blob.
158    PublishDataBlob { blob_hash: CryptoHash },
159    /// Reads a blob and discards the result.
160    // TODO(#2490): Consider removing this.
161    ReadBlob { blob_id: BlobId },
162    /// Creates a new application.
163    CreateApplication {
164        bytecode_id: BytecodeId,
165        #[serde(with = "serde_bytes")]
166        #[debug(with = "hex_debug")]
167        parameters: Vec<u8>,
168        #[serde(with = "serde_bytes")]
169        #[debug(with = "hex_debug")]
170        instantiation_argument: Vec<u8>,
171        required_application_ids: Vec<UserApplicationId>,
172    },
173    /// Requests a message from another chain to register a user application on this chain.
174    RequestApplication {
175        chain_id: ChainId,
176        application_id: UserApplicationId,
177    },
178    /// Operations that are only allowed on the admin chain.
179    Admin(AdminOperation),
180}
181
182/// Operations that are only allowed on the admin chain.
183#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
184pub enum AdminOperation {
185    /// Registers a new committee. This will notify the subscribers of the admin chain so that they
186    /// can migrate to the new epoch by accepting the resulting `CreateCommittee` as an incoming
187    /// message in a block.
188    CreateCommittee { epoch: Epoch, committee: Committee },
189    /// Removes a committee. Once the resulting `RemoveCommittee` message is accepted by a chain,
190    /// blocks from the retired epoch will not be accepted until they are followed (hence
191    /// re-certified) by a block certified by a recent committee.
192    RemoveCommittee { epoch: Epoch },
193}
194
195/// A system message meant to be executed on a remote chain.
196#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
197pub enum SystemMessage {
198    /// Credits `amount` units of value to the account `target` -- unless the message is
199    /// bouncing, in which case `source` is credited instead.
200    Credit {
201        target: Option<Owner>,
202        amount: Amount,
203        source: Option<Owner>,
204    },
205    /// Withdraws `amount` units of value from the account and starts a transfer to credit
206    /// the recipient. The message must be properly authenticated. Receiver chains may
207    /// refuse it depending on their configuration.
208    Withdraw {
209        owner: Owner,
210        amount: Amount,
211        recipient: Recipient,
212    },
213    /// Creates (or activates) a new chain.
214    OpenChain(OpenChainConfig),
215    /// Adds a new epoch and committee.
216    CreateCommittee { epoch: Epoch, committee: Committee },
217    /// Removes an old committee.
218    RemoveCommittee { epoch: Epoch },
219    /// Subscribes to a channel.
220    Subscribe {
221        id: ChainId,
222        subscription: ChannelSubscription,
223    },
224    /// Unsubscribes from a channel.
225    Unsubscribe {
226        id: ChainId,
227        subscription: ChannelSubscription,
228    },
229    /// Notifies that a new application was created.
230    ApplicationCreated,
231    /// Shares information about some applications to help the recipient use them.
232    /// Applications must be registered after their dependencies.
233    RegisterApplications {
234        applications: Vec<UserApplicationDescription>,
235    },
236    /// Requests a `RegisterApplication` message from the target chain to register the specified
237    /// application on the sender chain.
238    RequestApplication(UserApplicationId),
239}
240
241/// A query to the system state.
242#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
243pub struct SystemQuery;
244
245/// The response to a system query.
246#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
247pub struct SystemResponse {
248    pub chain_id: ChainId,
249    pub balance: Amount,
250}
251
252/// The channels available in the system application.
253#[derive(
254    Enum, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, clap::ValueEnum,
255)]
256pub enum SystemChannel {
257    /// Channel used to broadcast reconfigurations.
258    Admin,
259}
260
261impl SystemChannel {
262    /// The [`ChannelName`] of this [`SystemChannel`].
263    pub fn name(&self) -> ChannelName {
264        bcs::to_bytes(self)
265            .expect("`SystemChannel` can be serialized")
266            .into()
267    }
268}
269
270impl Display for SystemChannel {
271    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
272        let display_name = match self {
273            SystemChannel::Admin => "Admin",
274        };
275
276        write!(formatter, "{display_name}")
277    }
278}
279
280/// The recipient of a transfer.
281#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
282pub enum Recipient {
283    /// This is mainly a placeholder for future extensions.
284    Burn,
285    /// Transfers to the balance of the given account.
286    Account(Account),
287}
288
289impl Recipient {
290    /// Returns the default recipient for the given chain (no owner).
291    pub fn chain(chain_id: ChainId) -> Recipient {
292        Recipient::Account(Account::chain(chain_id))
293    }
294
295    /// Returns the default recipient for the root chain with the given index.
296    #[cfg(with_testing)]
297    pub fn root(index: u32) -> Recipient {
298        Recipient::chain(ChainId::root(index))
299    }
300}
301
302/// Optional user message attached to a transfer.
303#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Default, Debug, Serialize, Deserialize)]
304pub struct UserData(pub Option<[u8; 32]>);
305
306impl UserData {
307    pub fn from_option_string(opt_str: Option<String>) -> Result<Self, usize> {
308        // Convert the Option<String> to Option<[u8; 32]>
309        let option_array = match opt_str {
310            Some(s) => {
311                // Convert the String to a Vec<u8>
312                let vec = s.into_bytes();
313                if vec.len() <= 32 {
314                    // Create an array from the Vec<u8>
315                    let mut array = [b' '; 32];
316
317                    // Copy bytes from the vector into the array
318                    let len = vec.len().min(32);
319                    array[..len].copy_from_slice(&vec[..len]);
320
321                    Some(array)
322                } else {
323                    return Err(vec.len());
324                }
325            }
326            None => None,
327        };
328
329        // Return the UserData with the converted Option<[u8; 32]>
330        Ok(UserData(option_array))
331    }
332}
333
334#[derive(Error, Debug)]
335pub enum SystemExecutionError {
336    #[error(transparent)]
337    ArithmeticError(#[from] ArithmeticError),
338    #[error(transparent)]
339    ViewError(#[from] ViewError),
340
341    #[error("Invalid admin ID in new chain: {0}")]
342    InvalidNewChainAdminId(ChainId),
343    #[error("Invalid committees")]
344    InvalidCommittees,
345    #[error("{epoch:?} is not recognized by chain {chain_id:}")]
346    InvalidEpoch { chain_id: ChainId, epoch: Epoch },
347    #[error("Transfer must have positive amount")]
348    IncorrectTransferAmount,
349    #[error("Transfer from owned account must be authenticated by the right signer")]
350    UnauthenticatedTransferOwner,
351    #[error("The transferred amount must not exceed the current chain balance: {balance}")]
352    InsufficientFunding { balance: Amount },
353    #[error("Required execution fees exceeded the total funding available: {balance}")]
354    InsufficientFundingForFees { balance: Amount },
355    #[error("Claim must have positive amount")]
356    IncorrectClaimAmount,
357    #[error("Claim must be authenticated by the right signer")]
358    UnauthenticatedClaimOwner,
359    #[error("Admin operations are only allowed on the admin chain.")]
360    AdminOperationOnNonAdminChain,
361    #[error("Failed to create new committee")]
362    InvalidCommitteeCreation,
363    #[error("Failed to remove committee")]
364    InvalidCommitteeRemoval,
365    #[error(
366        "Chain {0} tried to subscribe to the admin channel ({1}) of a chain that is not the admin chain"
367    )]
368    InvalidAdminSubscription(ChainId, SystemChannel),
369    #[error("Cannot subscribe to a channel ({1}) on the same chain ({0})")]
370    SelfSubscription(ChainId, SystemChannel),
371    #[error("Chain {0} tried to subscribe to channel {1} but it is already subscribed")]
372    AlreadySubscribedToChannel(ChainId, SystemChannel),
373    #[error("Invalid unsubscription request to channel {1} on chain {0}")]
374    InvalidUnsubscription(ChainId, SystemChannel),
375    #[error("Amount overflow")]
376    AmountOverflow,
377    #[error("Amount underflow")]
378    AmountUnderflow,
379    #[error("Chain balance overflow")]
380    BalanceOverflow,
381    #[error("Chain balance underflow")]
382    BalanceUnderflow,
383    #[error("Cannot set epoch to a lower value")]
384    CannotRewindEpoch,
385    #[error("Cannot decrease the chain's timestamp")]
386    TicksOutOfOrder,
387    #[error("Application {0:?} is not registered by the chain")]
388    UnknownApplicationId(Box<UserApplicationId>),
389    #[error("Chain is not active yet.")]
390    InactiveChain,
391
392    #[error("Blob not found on storage read: {0}")]
393    BlobNotFoundOnRead(BlobId),
394    #[error("Oracle response mismatch")]
395    OracleResponseMismatch,
396    #[error("No recorded response for oracle query")]
397    MissingOracleResponse,
398}
399
400impl<C> SystemExecutionStateView<C>
401where
402    C: Context + Clone + Send + Sync + 'static,
403    C::Extra: ExecutionRuntimeContext,
404{
405    /// Invariant for the states of active chains.
406    pub fn is_active(&self) -> bool {
407        self.description.get().is_some()
408            && self.ownership.get().is_active()
409            && self.current_committee().is_some()
410            && self.admin_id.get().is_some()
411    }
412
413    /// Returns the current committee, if any.
414    pub fn current_committee(&self) -> Option<(Epoch, &Committee)> {
415        let epoch = self.epoch.get().as_ref()?;
416        let committee = self.committees.get().get(epoch)?;
417        Some((*epoch, committee))
418    }
419
420    /// Executes the sender's side of an operation and returns a list of actions to be
421    /// taken.
422    pub async fn execute_operation(
423        &mut self,
424        context: OperationContext,
425        operation: SystemOperation,
426        txn_tracker: &mut TransactionTracker,
427    ) -> Result<Option<(UserApplicationId, Vec<u8>)>, SystemExecutionError> {
428        use SystemOperation::*;
429        let mut outcome = RawExecutionOutcome {
430            authenticated_signer: context.authenticated_signer,
431            refund_grant_to: context.refund_grant_to(),
432            ..RawExecutionOutcome::default()
433        };
434        let mut new_application = None;
435        match operation {
436            OpenChain(config) => {
437                let next_message_id = context.next_message_id(txn_tracker.next_message_index());
438                let messages = self.open_chain(config, next_message_id)?;
439                outcome.messages.extend(messages);
440                #[cfg(with_metrics)]
441                OPEN_CHAIN_COUNT.with_label_values(&[]).inc();
442            }
443            ChangeOwnership {
444                super_owners,
445                owners,
446                multi_leader_rounds,
447                timeout_config,
448            } => {
449                self.ownership.set(ChainOwnership {
450                    super_owners: super_owners
451                        .into_iter()
452                        .map(|public_key| (Owner::from(public_key), public_key))
453                        .collect(),
454                    owners: owners
455                        .into_iter()
456                        .map(|(public_key, weight)| (Owner::from(public_key), (public_key, weight)))
457                        .collect(),
458                    multi_leader_rounds,
459                    timeout_config,
460                });
461            }
462            ChangeApplicationPermissions(application_permissions) => {
463                self.application_permissions.set(application_permissions);
464            }
465            CloseChain => {
466                let messages = self.close_chain(context.chain_id).await?;
467                outcome.messages.extend(messages);
468            }
469            Transfer {
470                owner,
471                amount,
472                recipient,
473                ..
474            } => {
475                let message = self
476                    .transfer(context.authenticated_signer, owner, recipient, amount)
477                    .await?;
478
479                if let Some(message) = message {
480                    outcome.messages.push(message)
481                }
482            }
483            Claim {
484                owner,
485                target_id,
486                recipient,
487                amount,
488            } => {
489                let message = self
490                    .claim(
491                        context.authenticated_signer,
492                        owner,
493                        target_id,
494                        recipient,
495                        amount,
496                    )
497                    .await?;
498
499                outcome.messages.push(message)
500            }
501            Admin(admin_operation) => {
502                ensure!(
503                    *self.admin_id.get() == Some(context.chain_id),
504                    SystemExecutionError::AdminOperationOnNonAdminChain
505                );
506                match admin_operation {
507                    AdminOperation::CreateCommittee { epoch, committee } => {
508                        ensure!(
509                            epoch == self.epoch.get().expect("chain is active").try_add_one()?,
510                            SystemExecutionError::InvalidCommitteeCreation
511                        );
512                        self.committees.get_mut().insert(epoch, committee.clone());
513                        self.epoch.set(Some(epoch));
514                        let message = RawOutgoingMessage {
515                            destination: Destination::Subscribers(SystemChannel::Admin.name()),
516                            authenticated: false,
517                            grant: Amount::ZERO,
518                            kind: MessageKind::Protected,
519                            message: SystemMessage::CreateCommittee { epoch, committee },
520                        };
521                        outcome.messages.push(message);
522                    }
523                    AdminOperation::RemoveCommittee { epoch } => {
524                        ensure!(
525                            self.committees.get_mut().remove(&epoch).is_some(),
526                            SystemExecutionError::InvalidCommitteeRemoval
527                        );
528                        let message = RawOutgoingMessage {
529                            destination: Destination::Subscribers(SystemChannel::Admin.name()),
530                            authenticated: false,
531                            grant: Amount::ZERO,
532                            kind: MessageKind::Protected,
533                            message: SystemMessage::RemoveCommittee { epoch },
534                        };
535                        outcome.messages.push(message);
536                    }
537                }
538            }
539            Subscribe { chain_id, channel } => {
540                ensure!(
541                    context.chain_id != chain_id,
542                    SystemExecutionError::SelfSubscription(context.chain_id, channel)
543                );
544                if channel == SystemChannel::Admin {
545                    ensure!(
546                        self.admin_id.get().as_ref() == Some(&chain_id),
547                        SystemExecutionError::InvalidAdminSubscription(context.chain_id, channel)
548                    );
549                }
550                let subscription = ChannelSubscription {
551                    chain_id,
552                    name: channel.name(),
553                };
554                ensure!(
555                    !self.subscriptions.contains(&subscription).await?,
556                    SystemExecutionError::AlreadySubscribedToChannel(context.chain_id, channel)
557                );
558                self.subscriptions.insert(&subscription)?;
559                let message = RawOutgoingMessage {
560                    destination: Destination::Recipient(chain_id),
561                    authenticated: false,
562                    grant: Amount::ZERO,
563                    kind: MessageKind::Protected,
564                    message: SystemMessage::Subscribe {
565                        id: context.chain_id,
566                        subscription,
567                    },
568                };
569                outcome.messages.push(message);
570            }
571            Unsubscribe { chain_id, channel } => {
572                let subscription = ChannelSubscription {
573                    chain_id,
574                    name: channel.name(),
575                };
576                ensure!(
577                    self.subscriptions.contains(&subscription).await?,
578                    SystemExecutionError::InvalidUnsubscription(context.chain_id, channel)
579                );
580                self.subscriptions.remove(&subscription)?;
581                let message = RawOutgoingMessage {
582                    destination: Destination::Recipient(chain_id),
583                    authenticated: false,
584                    grant: Amount::ZERO,
585                    kind: MessageKind::Protected,
586                    message: SystemMessage::Unsubscribe {
587                        id: context.chain_id,
588                        subscription,
589                    },
590                };
591                outcome.messages.push(message);
592            }
593            PublishBytecode { bytecode_id } => {
594                txn_tracker.replay_oracle_response(OracleResponse::Blob(BlobId::new(
595                    bytecode_id.contract_blob_hash,
596                    BlobType::ContractBytecode,
597                )))?;
598                txn_tracker.replay_oracle_response(OracleResponse::Blob(BlobId::new(
599                    bytecode_id.service_blob_hash,
600                    BlobType::ServiceBytecode,
601                )))?;
602            }
603            CreateApplication {
604                bytecode_id,
605                parameters,
606                instantiation_argument,
607                required_application_ids,
608            } => {
609                let id = UserApplicationId {
610                    bytecode_id,
611                    creation: context.next_message_id(txn_tracker.next_message_index()),
612                };
613                for application in required_application_ids.iter().chain(iter::once(&id)) {
614                    self.check_and_record_bytecode_blobs(&application.bytecode_id, txn_tracker)
615                        .await?;
616                }
617                self.registry
618                    .register_new_application(
619                        id,
620                        parameters.clone(),
621                        required_application_ids.clone(),
622                    )
623                    .await?;
624                // Send a message to ourself to increment the message ID.
625                let message = RawOutgoingMessage {
626                    destination: Destination::Recipient(context.chain_id),
627                    authenticated: false,
628                    grant: Amount::ZERO,
629                    kind: MessageKind::Protected,
630                    message: SystemMessage::ApplicationCreated,
631                };
632                outcome.messages.push(message);
633                new_application = Some((id, instantiation_argument.clone()));
634            }
635            RequestApplication {
636                chain_id,
637                application_id,
638            } => {
639                let message = RawOutgoingMessage {
640                    destination: Destination::Recipient(chain_id),
641                    authenticated: false,
642                    grant: Amount::ZERO,
643                    kind: MessageKind::Simple,
644                    message: SystemMessage::RequestApplication(application_id),
645                };
646                outcome.messages.push(message);
647            }
648            PublishDataBlob { blob_hash } => {
649                txn_tracker.replay_oracle_response(OracleResponse::Blob(BlobId::new(
650                    blob_hash,
651                    BlobType::Data,
652                )))?;
653            }
654            ReadBlob { blob_id } => {
655                txn_tracker.replay_oracle_response(OracleResponse::Blob(blob_id))?;
656                self.read_blob_content(blob_id).await?;
657            }
658        }
659
660        txn_tracker.add_system_outcome(outcome)?;
661        Ok(new_application)
662    }
663
664    pub async fn transfer(
665        &mut self,
666        authenticated_signer: Option<Owner>,
667        owner: Option<Owner>,
668        recipient: Recipient,
669        amount: Amount,
670    ) -> Result<Option<RawOutgoingMessage<SystemMessage, Amount>>, SystemExecutionError> {
671        if owner.is_some() {
672            ensure!(
673                authenticated_signer == owner,
674                SystemExecutionError::UnauthenticatedTransferOwner
675            );
676        }
677        ensure!(
678            amount > Amount::ZERO,
679            SystemExecutionError::IncorrectTransferAmount
680        );
681        let balance = match &owner {
682            Some(owner) => self.balances.get_mut_or_default(owner).await?,
683            None => self.balance.get_mut(),
684        };
685        balance
686            .try_sub_assign(amount)
687            .map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
688        match recipient {
689            Recipient::Account(account) => {
690                let message = RawOutgoingMessage {
691                    destination: Destination::Recipient(account.chain_id),
692                    authenticated: false,
693                    grant: Amount::ZERO,
694                    kind: MessageKind::Tracked,
695                    message: SystemMessage::Credit {
696                        amount,
697                        source: owner,
698                        target: account.owner,
699                    },
700                };
701
702                Ok(Some(message))
703            }
704            Recipient::Burn => Ok(None),
705        }
706    }
707
708    pub async fn claim(
709        &self,
710        authenticated_signer: Option<Owner>,
711        owner: Owner,
712        target_id: ChainId,
713        recipient: Recipient,
714        amount: Amount,
715    ) -> Result<RawOutgoingMessage<SystemMessage, Amount>, SystemExecutionError> {
716        ensure!(
717            authenticated_signer.as_ref() == Some(&owner),
718            SystemExecutionError::UnauthenticatedClaimOwner
719        );
720        ensure!(
721            amount > Amount::ZERO,
722            SystemExecutionError::IncorrectClaimAmount
723        );
724
725        Ok(RawOutgoingMessage {
726            destination: Destination::Recipient(target_id),
727            authenticated: true,
728            grant: Amount::ZERO,
729            kind: MessageKind::Simple,
730            message: SystemMessage::Withdraw {
731                amount,
732                owner,
733                recipient,
734            },
735        })
736    }
737
738    /// Executes a cross-chain message that represents the recipient's side of an operation.
739    pub async fn execute_message(
740        &mut self,
741        context: MessageContext,
742        message: SystemMessage,
743        txn_tracker: &mut TransactionTracker,
744    ) -> Result<RawExecutionOutcome<SystemMessage, Amount>, SystemExecutionError> {
745        let mut outcome = RawExecutionOutcome::default();
746        use SystemMessage::*;
747        match message {
748            Credit {
749                amount,
750                source,
751                target,
752            } => {
753                let receiver = if context.is_bouncing { source } else { target };
754                match receiver {
755                    None => {
756                        let new_balance = self.balance.get().saturating_add(amount);
757                        self.balance.set(new_balance);
758                    }
759                    Some(owner) => {
760                        let balance = self.balances.get_mut_or_default(&owner).await?;
761                        *balance = balance.saturating_add(amount);
762                    }
763                }
764            }
765            Withdraw {
766                amount,
767                owner,
768                recipient,
769            } => {
770                ensure!(
771                    context.authenticated_signer == Some(owner),
772                    SystemExecutionError::UnauthenticatedClaimOwner
773                );
774
775                let balance = self.balances.get_mut_or_default(&owner).await?;
776                balance
777                    .try_sub_assign(amount)
778                    .map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
779                match recipient {
780                    Recipient::Account(account) => {
781                        let message = RawOutgoingMessage {
782                            destination: Destination::Recipient(account.chain_id),
783                            authenticated: false,
784                            grant: Amount::ZERO,
785                            kind: MessageKind::Tracked,
786                            message: SystemMessage::Credit {
787                                amount,
788                                source: Some(owner),
789                                target: account.owner,
790                            },
791                        };
792                        outcome.messages.push(message);
793                    }
794                    Recipient::Burn => (),
795                }
796            }
797            CreateCommittee { epoch, committee } => {
798                let chain_next_epoch = self.epoch.get().expect("chain is active").try_add_one()?;
799                ensure!(
800                    epoch <= chain_next_epoch,
801                    SystemExecutionError::InvalidCommitteeCreation
802                );
803                if epoch == chain_next_epoch {
804                    self.committees.get_mut().insert(epoch, committee.clone());
805                    self.epoch.set(Some(epoch));
806                }
807            }
808            RemoveCommittee { epoch } => {
809                self.committees.get_mut().remove(&epoch);
810            }
811            RegisterApplications { applications } => {
812                for application in applications {
813                    self.check_and_record_bytecode_blobs(&application.bytecode_id, txn_tracker)
814                        .await?;
815                    self.registry
816                        .register_application(application.clone())
817                        .await?;
818                }
819            }
820            RequestApplication(application_id) => {
821                let applications = self
822                    .registry
823                    .describe_applications_with_dependencies(
824                        vec![application_id],
825                        &Default::default(),
826                    )
827                    .await?;
828                let message = RawOutgoingMessage {
829                    destination: Destination::Recipient(context.message_id.chain_id),
830                    authenticated: false,
831                    grant: Amount::ZERO,
832                    kind: MessageKind::Simple,
833                    message: SystemMessage::RegisterApplications { applications },
834                };
835                outcome.messages.push(message);
836            }
837            // These messages are executed immediately when cross-chain requests are received.
838            Subscribe { .. } | Unsubscribe { .. } | OpenChain(_) => {}
839            // This message is only a placeholder: Its ID is part of the application ID.
840            ApplicationCreated => {}
841        }
842        Ok(outcome)
843    }
844
845    /// Initializes the system application state on a newly opened chain.
846    pub fn initialize_chain(
847        &mut self,
848        message_id: MessageId,
849        timestamp: Timestamp,
850        config: OpenChainConfig,
851    ) {
852        // Guaranteed under BFT assumptions.
853        assert!(self.description.get().is_none());
854        assert!(!self.ownership.get().is_active());
855        assert!(self.committees.get().is_empty());
856        let OpenChainConfig {
857            ownership,
858            admin_id,
859            epoch,
860            committees,
861            balance,
862            application_permissions,
863        } = config;
864        let description = ChainDescription::Child(message_id);
865        self.description.set(Some(description));
866        self.epoch.set(Some(epoch));
867        self.committees.set(committees);
868        self.admin_id.set(Some(admin_id));
869        self.subscriptions
870            .insert(&ChannelSubscription {
871                chain_id: admin_id,
872                name: SystemChannel::Admin.name(),
873            })
874            .expect("serialization failed");
875        self.ownership.set(ownership);
876        self.timestamp.set(timestamp);
877        self.balance.set(balance);
878        self.application_permissions.set(application_permissions);
879    }
880
881    pub async fn handle_query(
882        &mut self,
883        context: QueryContext,
884        _query: SystemQuery,
885    ) -> Result<SystemResponse, SystemExecutionError> {
886        let response = SystemResponse {
887            chain_id: context.chain_id,
888            balance: *self.balance.get(),
889        };
890        Ok(response)
891    }
892
893    /// Returns the messages to open a new chain, and subtracts the new chain's balance
894    /// from this chain's.
895    pub fn open_chain(
896        &mut self,
897        config: OpenChainConfig,
898        next_message_id: MessageId,
899    ) -> Result<[RawOutgoingMessage<SystemMessage, Amount>; 2], SystemExecutionError> {
900        let child_id = ChainId::child(next_message_id);
901        ensure!(
902            self.admin_id.get().as_ref() == Some(&config.admin_id),
903            SystemExecutionError::InvalidNewChainAdminId(child_id)
904        );
905        let admin_id = config.admin_id;
906        ensure!(
907            self.committees.get() == &config.committees,
908            SystemExecutionError::InvalidCommittees
909        );
910        ensure!(
911            self.epoch.get().as_ref() == Some(&config.epoch),
912            SystemExecutionError::InvalidEpoch {
913                chain_id: child_id,
914                epoch: config.epoch,
915            }
916        );
917        let balance = self.balance.get_mut();
918        balance
919            .try_sub_assign(config.balance)
920            .map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
921        let open_chain_message = RawOutgoingMessage {
922            destination: Destination::Recipient(child_id),
923            authenticated: false,
924            grant: Amount::ZERO,
925            kind: MessageKind::Protected,
926            message: SystemMessage::OpenChain(config),
927        };
928        let subscription = ChannelSubscription {
929            chain_id: admin_id,
930            name: SystemChannel::Admin.name(),
931        };
932        let subscribe_message = RawOutgoingMessage {
933            destination: Destination::Recipient(admin_id),
934            authenticated: false,
935            grant: Amount::ZERO,
936            kind: MessageKind::Protected,
937            message: SystemMessage::Subscribe {
938                id: child_id,
939                subscription,
940            },
941        };
942        Ok([open_chain_message, subscribe_message])
943    }
944
945    pub async fn close_chain(
946        &mut self,
947        id: ChainId,
948    ) -> Result<Vec<RawOutgoingMessage<SystemMessage, Amount>>, SystemExecutionError> {
949        let mut messages = Vec::new();
950        // Unsubscribe from all channels.
951        self.subscriptions
952            .for_each_index(|subscription| {
953                let message = RawOutgoingMessage {
954                    destination: Destination::Recipient(subscription.chain_id),
955                    authenticated: false,
956                    grant: Amount::ZERO,
957                    kind: MessageKind::Protected,
958                    message: SystemMessage::Unsubscribe { id, subscription },
959                };
960                messages.push(message);
961                Ok(())
962            })
963            .await?;
964        self.subscriptions.clear();
965        self.closed.set(true);
966        Ok(messages)
967    }
968
969    pub async fn read_blob_content(
970        &mut self,
971        blob_id: BlobId,
972    ) -> Result<BlobContent, SystemExecutionError> {
973        self.context()
974            .extra()
975            .get_blob(blob_id)
976            .await
977            .map_err(|_| SystemExecutionError::BlobNotFoundOnRead(blob_id))
978            .map(Into::into)
979    }
980
981    pub async fn assert_blob_exists(
982        &mut self,
983        blob_id: BlobId,
984    ) -> Result<(), SystemExecutionError> {
985        if self.context().extra().contains_blob(blob_id).await? {
986            Ok(())
987        } else {
988            Err(SystemExecutionError::BlobNotFoundOnRead(blob_id))
989        }
990    }
991
992    async fn check_and_record_bytecode_blobs(
993        &mut self,
994        bytecode_id: &BytecodeId,
995        txn_tracker: &mut TransactionTracker,
996    ) -> Result<(), SystemExecutionError> {
997        let contract_bytecode_blob_id =
998            BlobId::new(bytecode_id.contract_blob_hash, BlobType::ContractBytecode);
999        ensure!(
1000            self.context()
1001                .extra()
1002                .contains_blob(contract_bytecode_blob_id)
1003                .await?,
1004            SystemExecutionError::BlobNotFoundOnRead(contract_bytecode_blob_id)
1005        );
1006        txn_tracker.replay_oracle_response(OracleResponse::Blob(contract_bytecode_blob_id))?;
1007        let service_bytecode_blob_id =
1008            BlobId::new(bytecode_id.service_blob_hash, BlobType::ServiceBytecode);
1009        ensure!(
1010            self.context()
1011                .extra()
1012                .contains_blob(service_bytecode_blob_id)
1013                .await?,
1014            SystemExecutionError::BlobNotFoundOnRead(service_bytecode_blob_id)
1015        );
1016        txn_tracker.replay_oracle_response(OracleResponse::Blob(service_bytecode_blob_id))?;
1017        Ok(())
1018    }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023    use linera_base::{
1024        data_types::{Blob, BlockHeight, Bytecode},
1025        identifiers::ApplicationId,
1026    };
1027    use linera_views::context::MemoryContext;
1028
1029    use super::*;
1030    use crate::{ExecutionOutcome, ExecutionStateView, TestExecutionRuntimeContext};
1031
1032    /// Returns an execution state view and a matching operation context, for epoch 1, with root
1033    /// chain 0 as the admin ID and one empty committee.
1034    async fn new_view_and_context() -> (
1035        ExecutionStateView<MemoryContext<TestExecutionRuntimeContext>>,
1036        OperationContext,
1037    ) {
1038        let description = ChainDescription::Root(5);
1039        let context = OperationContext {
1040            chain_id: ChainId::from(description),
1041            authenticated_signer: None,
1042            authenticated_caller_id: None,
1043            height: BlockHeight::from(7),
1044            index: Some(2),
1045        };
1046        let state = SystemExecutionState {
1047            description: Some(description),
1048            epoch: Some(Epoch(1)),
1049            admin_id: Some(ChainId::root(0)),
1050            committees: BTreeMap::new(),
1051            ..SystemExecutionState::default()
1052        };
1053        let view = state.into_view().await;
1054        (view, context)
1055    }
1056
1057    #[tokio::test]
1058    async fn application_message_index() {
1059        let (mut view, context) = new_view_and_context().await;
1060        let contract = Bytecode::new(b"contract".into());
1061        let service = Bytecode::new(b"service".into());
1062        let contract_blob = Blob::new_contract_bytecode(contract.compress());
1063        let service_blob = Blob::new_service_bytecode(service.compress());
1064        let bytecode_id = BytecodeId::new(contract_blob.id().hash, service_blob.id().hash);
1065
1066        let operation = SystemOperation::CreateApplication {
1067            bytecode_id,
1068            parameters: vec![],
1069            instantiation_argument: vec![],
1070            required_application_ids: vec![],
1071        };
1072        let mut txn_tracker = TransactionTracker::default();
1073        view.context()
1074            .extra()
1075            .add_blobs(vec![contract_blob, service_blob]);
1076        let new_application = view
1077            .system
1078            .execute_operation(context, operation, &mut txn_tracker)
1079            .await
1080            .unwrap();
1081        let [ExecutionOutcome::System(result)] = &txn_tracker.destructure().unwrap().0[..] else {
1082            panic!("Unexpected outcome");
1083        };
1084        assert_eq!(
1085            result.messages[CREATE_APPLICATION_MESSAGE_INDEX as usize].message,
1086            SystemMessage::ApplicationCreated
1087        );
1088        let creation = MessageId {
1089            chain_id: context.chain_id,
1090            height: context.height,
1091            index: CREATE_APPLICATION_MESSAGE_INDEX,
1092        };
1093        let id = ApplicationId {
1094            bytecode_id,
1095            creation,
1096        };
1097        assert_eq!(new_application, Some((id, vec![])));
1098    }
1099
1100    #[tokio::test]
1101    async fn open_chain_message_index() {
1102        let (mut view, context) = new_view_and_context().await;
1103        let epoch = view.system.epoch.get().unwrap();
1104        let admin_id = view.system.admin_id.get().unwrap();
1105        let committees = view.system.committees.get().clone();
1106        let ownership = ChainOwnership::single(PublicKey::test_key(0));
1107        let config = OpenChainConfig {
1108            ownership,
1109            committees,
1110            epoch,
1111            admin_id,
1112            balance: Amount::ZERO,
1113            application_permissions: Default::default(),
1114        };
1115        let mut txn_tracker = TransactionTracker::default();
1116        let operation = SystemOperation::OpenChain(config.clone());
1117        let new_application = view
1118            .system
1119            .execute_operation(context, operation, &mut txn_tracker)
1120            .await
1121            .unwrap();
1122        assert_eq!(new_application, None);
1123        let [ExecutionOutcome::System(result)] = &txn_tracker.destructure().unwrap().0[..] else {
1124            panic!("Unexpected outcome");
1125        };
1126        assert_eq!(
1127            result.messages[OPEN_CHAIN_MESSAGE_INDEX as usize].message,
1128            SystemMessage::OpenChain(config)
1129        );
1130    }
1131}