use std::{
collections::BTreeMap,
fmt::{self, Display, Formatter},
iter,
};
use async_graphql::Enum;
use custom_debug_derive::Debug;
use linera_base::{
crypto::{CryptoHash, PublicKey},
data_types::{Amount, ApplicationPermissions, ArithmeticError, Timestamp},
ensure, hex_debug,
identifiers::{Account, BytecodeId, ChainDescription, ChainId, MessageId, Owner},
ownership::{ChainOwnership, TimeoutConfig},
};
use linera_views::{
common::Context,
map_view::HashedMapView,
register_view::HashedRegisterView,
set_view::HashedSetView,
views::{HashableView, View, ViewError},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(with_metrics)]
use {
linera_base::{prometheus_util, sync::Lazy},
prometheus::IntCounterVec,
};
#[cfg(test)]
use crate::test_utils::SystemExecutionState;
use crate::{
committee::{Committee, Epoch},
ApplicationRegistryView, Bytecode, BytecodeLocation, ChannelName, ChannelSubscription,
Destination, MessageContext, MessageKind, OperationContext, QueryContext, RawExecutionOutcome,
RawOutgoingMessage, UserApplicationDescription, UserApplicationId,
};
pub static OPEN_CHAIN_MESSAGE_INDEX: u32 = 0;
pub static CREATE_APPLICATION_MESSAGE_INDEX: u32 = 0;
pub static PUBLISH_BYTECODE_MESSAGE_INDEX: u32 = 0;
#[cfg(with_metrics)]
static OPEN_CHAIN_COUNT: Lazy<IntCounterVec> = Lazy::new(|| {
prometheus_util::register_int_counter_vec(
"open_chain_count",
"The number of times the `OpenChain` operation was executed",
&[],
)
.expect("Counter creation should not fail")
});
#[derive(Debug, HashableView)]
pub struct SystemExecutionStateView<C> {
pub description: HashedRegisterView<C, Option<ChainDescription>>,
pub epoch: HashedRegisterView<C, Option<Epoch>>,
pub admin_id: HashedRegisterView<C, Option<ChainId>>,
pub subscriptions: HashedSetView<C, ChannelSubscription>,
pub committees: HashedRegisterView<C, BTreeMap<Epoch, Committee>>,
pub ownership: HashedRegisterView<C, ChainOwnership>,
pub balance: HashedRegisterView<C, Amount>,
pub balances: HashedMapView<C, Owner, Amount>,
pub timestamp: HashedRegisterView<C, Timestamp>,
pub registry: ApplicationRegistryView<C>,
pub closed: HashedRegisterView<C, bool>,
pub application_permissions: HashedRegisterView<C, ApplicationPermissions>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub struct OpenChainConfig {
pub ownership: ChainOwnership,
pub admin_id: ChainId,
pub epoch: Epoch,
pub committees: BTreeMap<Epoch, Committee>,
pub balance: Amount,
pub application_permissions: ApplicationPermissions,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub enum SystemOperation {
Transfer {
owner: Option<Owner>,
recipient: Recipient,
amount: Amount,
user_data: UserData,
},
Claim {
owner: Owner,
target_id: ChainId,
recipient: Recipient,
amount: Amount,
user_data: UserData,
},
OpenChain(OpenChainConfig),
CloseChain,
ChangeOwnership {
super_owners: Vec<PublicKey>,
owners: Vec<(PublicKey, u64)>,
multi_leader_rounds: u32,
timeout_config: TimeoutConfig,
},
ChangeApplicationPermissions(ApplicationPermissions),
Subscribe {
chain_id: ChainId,
channel: SystemChannel,
},
Unsubscribe {
chain_id: ChainId,
channel: SystemChannel,
},
PublishBytecode {
contract: Bytecode,
service: Bytecode,
},
CreateApplication {
bytecode_id: BytecodeId,
#[serde(with = "serde_bytes")]
#[debug(with = "hex_debug")]
parameters: Vec<u8>,
#[serde(with = "serde_bytes")]
#[debug(with = "hex_debug")]
initialization_argument: Vec<u8>,
required_application_ids: Vec<UserApplicationId>,
},
RequestApplication {
chain_id: ChainId,
application_id: UserApplicationId,
},
Admin(AdminOperation),
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub enum AdminOperation {
CreateCommittee { epoch: Epoch, committee: Committee },
RemoveCommittee { epoch: Epoch },
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub enum SystemMessage {
Credit {
target: Option<Owner>,
amount: Amount,
source: Option<Owner>,
},
Withdraw {
owner: Owner,
amount: Amount,
recipient: Recipient,
user_data: UserData,
},
OpenChain(OpenChainConfig),
SetCommittees {
epoch: Epoch,
committees: BTreeMap<Epoch, Committee>,
},
Subscribe {
id: ChainId,
subscription: ChannelSubscription,
},
Unsubscribe {
id: ChainId,
subscription: ChannelSubscription,
},
BytecodePublished { operation_index: u32 },
ApplicationCreated,
BytecodeLocations {
locations: Vec<(BytecodeId, BytecodeLocation)>,
},
RegisterApplications {
applications: Vec<UserApplicationDescription>,
},
Notify { id: ChainId },
RequestApplication(UserApplicationId),
}
impl SystemMessage {
pub fn bytecode_locations(
&self,
certificate_hash: CryptoHash,
) -> Box<dyn Iterator<Item = BytecodeLocation> + '_> {
match self {
SystemMessage::BytecodePublished { operation_index } => {
Box::new(iter::once(BytecodeLocation {
certificate_hash,
operation_index: *operation_index,
}))
}
SystemMessage::BytecodeLocations {
locations: new_locations,
} => Box::new(new_locations.iter().map(|(_id, location)| *location)),
SystemMessage::RegisterApplications { applications } => {
Box::new(applications.iter().map(|app| app.bytecode_location))
}
SystemMessage::Credit { .. }
| SystemMessage::Withdraw { .. }
| SystemMessage::OpenChain(_)
| SystemMessage::SetCommittees { .. }
| SystemMessage::Subscribe { .. }
| SystemMessage::Unsubscribe { .. }
| SystemMessage::ApplicationCreated
| SystemMessage::Notify { .. }
| SystemMessage::RequestApplication(_) => Box::new(iter::empty()),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub struct SystemQuery;
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub struct SystemResponse {
pub chain_id: ChainId,
pub balance: Amount,
}
#[derive(
Enum, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, clap::ValueEnum,
)]
pub enum SystemChannel {
Admin,
PublishedBytecodes,
}
impl SystemChannel {
pub fn name(&self) -> ChannelName {
bcs::to_bytes(self)
.expect("`SystemChannel` can be serialized")
.into()
}
}
impl Display for SystemChannel {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
let display_name = match self {
SystemChannel::Admin => "Admin",
SystemChannel::PublishedBytecodes => "PublishedBytecodes",
};
write!(formatter, "{display_name}")
}
}
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
pub enum Recipient {
Burn,
Account(Account),
}
impl Recipient {
pub fn chain(chain_id: ChainId) -> Recipient {
Recipient::Account(Account::chain(chain_id))
}
#[cfg(with_testing)]
pub fn root(index: u32) -> Recipient {
Recipient::chain(ChainId::root(index))
}
}
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Default, Debug, Serialize, Deserialize)]
pub struct UserData(pub Option<[u8; 32]>);
#[derive(Error, Debug)]
pub enum SystemExecutionError {
#[error(transparent)]
ArithmeticError(#[from] ArithmeticError),
#[error(transparent)]
ViewError(#[from] ViewError),
#[error("Incorrect chain ID: {0}")]
IncorrectChainId(ChainId),
#[error("Invalid admin ID in new chain: {0}")]
InvalidNewChainAdminId(ChainId),
#[error("Invalid committees")]
InvalidCommittees,
#[error("{epoch:?} is not recognized by chain {chain_id:}")]
InvalidEpoch { chain_id: ChainId, epoch: Epoch },
#[error("Transfer must have positive amount")]
IncorrectTransferAmount,
#[error("Transfer from owned account must be authenticated by the right signer")]
UnauthenticatedTransferOwner,
#[error("The transferred amount must not exceed the current chain balance: {balance}")]
InsufficientFunding { balance: Amount },
#[error("Required execution fees exceeded the total funding available: {balance}")]
InsufficientFundingForFees { balance: Amount },
#[error("Claim must have positive amount")]
IncorrectClaimAmount,
#[error("Claim must be authenticated by the right signer")]
UnauthenticatedClaimOwner,
#[error("Admin operations are only allowed on the admin chain.")]
AdminOperationOnNonAdminChain,
#[error("Failed to create new committee")]
InvalidCommitteeCreation,
#[error("Failed to remove committee")]
InvalidCommitteeRemoval,
#[error(
"Chain {0} tried to subscribe to the admin channel ({1}) of a chain that is not the admin chain"
)]
InvalidAdminSubscription(ChainId, SystemChannel),
#[error("Cannot subscribe to a channel ({1}) on the same chain ({0})")]
SelfSubscription(ChainId, SystemChannel),
#[error("Chain {0} tried to subscribe to channel {1} but it is already subscribed")]
AlreadySubscribedToChannel(ChainId, SystemChannel),
#[error("Invalid unsubscription request to channel {1} on chain {0}")]
InvalidUnsubscription(ChainId, SystemChannel),
#[error("Amount overflow")]
AmountOverflow,
#[error("Amount underflow")]
AmountUnderflow,
#[error("Chain balance overflow")]
BalanceOverflow,
#[error("Chain balance underflow")]
BalanceUnderflow,
#[error("Cannot set epoch to a lower value")]
CannotRewindEpoch,
#[error("Cannot decrease the chain's timestamp")]
TicksOutOfOrder,
#[error("Attempt to create an application using unregistered bytecode identifier {0:?}")]
UnknownBytecodeId(BytecodeId),
#[error("Application {0:?} is not registered by the chain")]
UnknownApplicationId(Box<UserApplicationId>),
#[error("Chain is not active yet.")]
InactiveChain,
}
impl<C> SystemExecutionStateView<C>
where
C: Context + Clone + Send + Sync + 'static,
ViewError: From<C::Error>,
{
pub fn is_active(&self) -> bool {
self.description.get().is_some()
&& self.ownership.get().is_active()
&& self.current_committee().is_some()
&& self.admin_id.get().is_some()
}
pub fn current_committee(&self) -> Option<(Epoch, &Committee)> {
let epoch = self.epoch.get().as_ref()?;
let committee = self.committees.get().get(epoch)?;
Some((*epoch, committee))
}
pub async fn execute_operation(
&mut self,
context: OperationContext,
operation: SystemOperation,
) -> Result<
(
RawExecutionOutcome<SystemMessage, Amount>,
Option<(UserApplicationId, Vec<u8>)>,
),
SystemExecutionError,
> {
use SystemOperation::*;
let mut outcome = RawExecutionOutcome::default();
let mut new_application = None;
match operation {
OpenChain(config) => {
let next_message_id = context.next_message_id();
let messages = self.open_chain(config, next_message_id)?;
outcome.messages.extend(messages);
#[cfg(with_metrics)]
OPEN_CHAIN_COUNT.with_label_values(&[]).inc();
}
ChangeOwnership {
super_owners,
owners,
multi_leader_rounds,
timeout_config,
} => {
self.ownership.set(ChainOwnership {
super_owners: super_owners
.into_iter()
.map(|public_key| (Owner::from(public_key), public_key))
.collect(),
owners: owners
.into_iter()
.map(|(public_key, weight)| (Owner::from(public_key), (public_key, weight)))
.collect(),
multi_leader_rounds,
timeout_config,
});
}
ChangeApplicationPermissions(application_permissions) => {
self.application_permissions.set(application_permissions);
}
CloseChain => {
let messages = self.close_chain(context.chain_id).await?;
outcome.messages.extend(messages);
}
Transfer {
owner,
amount,
recipient,
..
} => {
let message = self
.transfer(context.authenticated_signer, owner, recipient, amount)
.await?;
if let Some(message) = message {
outcome.messages.push(message)
}
}
Claim {
owner,
target_id,
recipient,
amount,
user_data,
} => {
let message = self
.claim(
context.authenticated_signer,
owner,
target_id,
recipient,
amount,
user_data,
)
.await?;
outcome.messages.push(message)
}
Admin(admin_operation) => {
ensure!(
*self.admin_id.get() == Some(context.chain_id),
SystemExecutionError::AdminOperationOnNonAdminChain
);
match admin_operation {
AdminOperation::CreateCommittee { epoch, committee } => {
ensure!(
epoch == self.epoch.get().expect("chain is active").try_add_one()?,
SystemExecutionError::InvalidCommitteeCreation
);
self.committees.get_mut().insert(epoch, committee);
self.epoch.set(Some(epoch));
let message = RawOutgoingMessage {
destination: Destination::Subscribers(SystemChannel::Admin.name()),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::SetCommittees {
epoch,
committees: self.committees.get().clone(),
},
};
outcome.messages.push(message);
}
AdminOperation::RemoveCommittee { epoch } => {
ensure!(
self.committees.get_mut().remove(&epoch).is_some(),
SystemExecutionError::InvalidCommitteeRemoval
);
let message = RawOutgoingMessage {
destination: Destination::Subscribers(SystemChannel::Admin.name()),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::SetCommittees {
epoch: self.epoch.get().expect("chain is active"),
committees: self.committees.get().clone(),
},
};
outcome.messages.push(message);
}
}
}
Subscribe { chain_id, channel } => {
ensure!(
context.chain_id != chain_id,
SystemExecutionError::SelfSubscription(context.chain_id, channel)
);
if channel == SystemChannel::Admin {
ensure!(
self.admin_id.get().as_ref() == Some(&chain_id),
SystemExecutionError::InvalidAdminSubscription(context.chain_id, channel)
);
}
let subscription = ChannelSubscription {
chain_id,
name: channel.name(),
};
ensure!(
!self.subscriptions.contains(&subscription).await?,
SystemExecutionError::AlreadySubscribedToChannel(context.chain_id, channel)
);
self.subscriptions.insert(&subscription)?;
let message = RawOutgoingMessage {
destination: Destination::Recipient(chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::Subscribe {
id: context.chain_id,
subscription,
},
};
outcome.messages.push(message);
}
Unsubscribe { chain_id, channel } => {
let subscription = ChannelSubscription {
chain_id,
name: channel.name(),
};
ensure!(
self.subscriptions.contains(&subscription).await?,
SystemExecutionError::InvalidUnsubscription(context.chain_id, channel)
);
self.subscriptions.remove(&subscription)?;
let message = RawOutgoingMessage {
destination: Destination::Recipient(chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::Unsubscribe {
id: context.chain_id,
subscription,
},
};
outcome.messages.push(message);
}
PublishBytecode { .. } => {
let message = RawOutgoingMessage {
destination: Destination::Recipient(context.chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::BytecodePublished {
operation_index: context
.index
.expect("System application can not be called by other applications"),
},
};
outcome.messages.push(message);
}
CreateApplication {
bytecode_id,
parameters,
initialization_argument,
required_application_ids,
} => {
let id = UserApplicationId {
bytecode_id,
creation: context.next_message_id(),
};
self.registry
.register_new_application(
id,
parameters.clone(),
required_application_ids.clone(),
)
.await?;
let message = RawOutgoingMessage {
destination: Destination::Recipient(context.chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::ApplicationCreated,
};
outcome.messages.push(message);
new_application = Some((id, initialization_argument.clone()));
}
RequestApplication {
chain_id,
application_id,
} => {
let message = RawOutgoingMessage {
destination: Destination::Recipient(chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Simple,
message: SystemMessage::RequestApplication(application_id),
};
outcome.messages.push(message);
}
}
Ok((outcome, new_application))
}
pub async fn transfer(
&mut self,
authenticated_signer: Option<Owner>,
owner: Option<Owner>,
recipient: Recipient,
amount: Amount,
) -> Result<Option<RawOutgoingMessage<SystemMessage, Amount>>, SystemExecutionError> {
if owner.is_some() {
ensure!(
authenticated_signer == owner,
SystemExecutionError::UnauthenticatedTransferOwner
);
}
ensure!(
amount > Amount::ZERO,
SystemExecutionError::IncorrectTransferAmount
);
let balance = match &owner {
Some(owner) => self.balances.get_mut_or_default(owner).await?,
None => self.balance.get_mut(),
};
balance
.try_sub_assign(amount)
.map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
match recipient {
Recipient::Account(account) => {
let message = RawOutgoingMessage {
destination: Destination::Recipient(account.chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Tracked,
message: SystemMessage::Credit {
amount,
source: owner,
target: account.owner,
},
};
Ok(Some(message))
}
Recipient::Burn => Ok(None),
}
}
pub async fn claim(
&self,
authenticated_signer: Option<Owner>,
owner: Owner,
target_id: ChainId,
recipient: Recipient,
amount: Amount,
user_data: UserData,
) -> Result<RawOutgoingMessage<SystemMessage, Amount>, SystemExecutionError> {
ensure!(
authenticated_signer.as_ref() == Some(&owner),
SystemExecutionError::UnauthenticatedClaimOwner
);
ensure!(
amount > Amount::ZERO,
SystemExecutionError::IncorrectClaimAmount
);
Ok(RawOutgoingMessage {
destination: Destination::Recipient(target_id),
authenticated: true,
grant: Amount::ZERO,
kind: MessageKind::Simple,
message: SystemMessage::Withdraw {
amount,
owner,
user_data,
recipient,
},
})
}
pub async fn execute_message(
&mut self,
context: MessageContext,
message: SystemMessage,
) -> Result<RawExecutionOutcome<SystemMessage, Amount>, SystemExecutionError> {
let mut outcome = RawExecutionOutcome::default();
use SystemMessage::*;
match message {
Credit {
amount,
source,
target,
} => {
let receiver = if context.is_bouncing { source } else { target };
match receiver {
None => {
let new_balance = self.balance.get().saturating_add(amount);
self.balance.set(new_balance);
}
Some(owner) => {
let balance = self.balances.get_mut_or_default(&owner).await?;
*balance = balance.saturating_add(amount);
}
}
}
Withdraw {
amount,
owner,
user_data: _,
recipient,
} => {
ensure!(
context.authenticated_signer == Some(owner),
SystemExecutionError::UnauthenticatedClaimOwner
);
let balance = self.balances.get_mut_or_default(&owner).await?;
balance
.try_sub_assign(amount)
.map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
match recipient {
Recipient::Account(account) => {
let message = RawOutgoingMessage {
destination: Destination::Recipient(account.chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Tracked,
message: SystemMessage::Credit {
amount,
source: Some(owner),
target: account.owner,
},
};
outcome.messages.push(message);
}
Recipient::Burn => (),
}
}
SetCommittees { epoch, committees } => {
ensure!(
epoch >= self.epoch.get().expect("chain is active"),
SystemExecutionError::CannotRewindEpoch
);
self.epoch.set(Some(epoch));
self.committees.set(committees);
}
Subscribe { id, subscription } => {
ensure!(
subscription.chain_id == context.chain_id,
SystemExecutionError::IncorrectChainId(subscription.chain_id)
);
let message = RawOutgoingMessage {
destination: Destination::Recipient(id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::Notify { id },
};
outcome.messages.push(message);
outcome.subscribe.push((subscription.name.clone(), id));
}
Unsubscribe { id, subscription } => {
ensure!(
subscription.chain_id == context.chain_id,
SystemExecutionError::IncorrectChainId(subscription.chain_id)
);
let message = RawOutgoingMessage {
destination: Destination::Recipient(id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::Notify { id },
};
outcome.messages.push(message);
outcome.unsubscribe.push((subscription.name.clone(), id));
}
BytecodePublished { operation_index } => {
let bytecode_id = BytecodeId::new(context.message_id);
let bytecode_location = BytecodeLocation {
certificate_hash: context.certificate_hash,
operation_index,
};
self.registry
.register_published_bytecode(bytecode_id, bytecode_location)?;
let locations = self.registry.bytecode_locations().await?;
let message = RawOutgoingMessage {
destination: Destination::Subscribers(SystemChannel::PublishedBytecodes.name()),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Simple,
message: SystemMessage::BytecodeLocations { locations },
};
outcome.messages.push(message);
}
BytecodeLocations { locations } => {
for (id, location) in locations {
self.registry.register_published_bytecode(id, location)?;
}
}
RegisterApplications { applications } => {
for application in applications {
self.registry
.register_application(application.clone())
.await?;
}
}
RequestApplication(application_id) => {
let applications = self
.registry
.describe_applications_with_dependencies(
vec![application_id],
&Default::default(),
)
.await?;
let message = RawOutgoingMessage {
destination: Destination::Recipient(context.message_id.chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Simple,
message: SystemMessage::RegisterApplications { applications },
};
outcome.messages.push(message);
}
OpenChain(_) => {
}
ApplicationCreated | Notify { .. } => (),
}
Ok(outcome)
}
pub fn initialize_chain(
&mut self,
message_id: MessageId,
timestamp: Timestamp,
config: OpenChainConfig,
) {
assert!(self.description.get().is_none());
assert!(!self.ownership.get().is_active());
assert!(self.committees.get().is_empty());
let OpenChainConfig {
ownership,
admin_id,
epoch,
committees,
balance,
application_permissions,
} = config;
let description = ChainDescription::Child(message_id);
self.description.set(Some(description));
self.epoch.set(Some(epoch));
self.committees.set(committees);
self.admin_id.set(Some(admin_id));
self.subscriptions
.insert(&ChannelSubscription {
chain_id: admin_id,
name: SystemChannel::Admin.name(),
})
.expect("serialization failed");
self.ownership.set(ownership);
self.timestamp.set(timestamp);
self.balance.set(balance);
self.application_permissions.set(application_permissions);
}
pub async fn handle_query(
&mut self,
context: QueryContext,
_query: SystemQuery,
) -> Result<SystemResponse, SystemExecutionError> {
let response = SystemResponse {
chain_id: context.chain_id,
balance: *self.balance.get(),
};
Ok(response)
}
pub fn open_chain(
&mut self,
config: OpenChainConfig,
next_message_id: MessageId,
) -> Result<[RawOutgoingMessage<SystemMessage, Amount>; 2], SystemExecutionError> {
let child_id = ChainId::child(next_message_id);
ensure!(
self.admin_id.get().as_ref() == Some(&config.admin_id),
SystemExecutionError::InvalidNewChainAdminId(child_id)
);
let admin_id = config.admin_id;
ensure!(
self.committees.get() == &config.committees,
SystemExecutionError::InvalidCommittees
);
ensure!(
self.epoch.get().as_ref() == Some(&config.epoch),
SystemExecutionError::InvalidEpoch {
chain_id: child_id,
epoch: config.epoch,
}
);
let balance = self.balance.get_mut();
balance
.try_sub_assign(config.balance)
.map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
let open_chain_message = RawOutgoingMessage {
destination: Destination::Recipient(child_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::OpenChain(config),
};
let subscription = ChannelSubscription {
chain_id: admin_id,
name: SystemChannel::Admin.name(),
};
let subscribe_message = RawOutgoingMessage {
destination: Destination::Recipient(admin_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::Subscribe {
id: child_id,
subscription,
},
};
Ok([open_chain_message, subscribe_message])
}
pub async fn close_chain(
&mut self,
id: ChainId,
) -> Result<Vec<RawOutgoingMessage<SystemMessage, Amount>>, SystemExecutionError> {
let mut messages = Vec::new();
self.subscriptions
.for_each_index(|subscription| {
let message = RawOutgoingMessage {
destination: Destination::Recipient(subscription.chain_id),
authenticated: false,
grant: Amount::ZERO,
kind: MessageKind::Protected,
message: SystemMessage::Unsubscribe { id, subscription },
};
messages.push(message);
Ok(())
})
.await?;
self.subscriptions.clear();
self.closed.set(true);
Ok(messages)
}
}
#[cfg(test)]
mod tests {
use linera_base::{data_types::BlockHeight, identifiers::ApplicationId};
use linera_views::memory::MemoryContext;
use super::*;
use crate::{ExecutionStateView, TestExecutionRuntimeContext};
async fn new_view_and_context() -> (
ExecutionStateView<MemoryContext<TestExecutionRuntimeContext>>,
OperationContext,
) {
let description = ChainDescription::Root(5);
let context = OperationContext {
chain_id: ChainId::from(description),
authenticated_signer: None,
authenticated_caller_id: None,
height: BlockHeight::from(7),
index: Some(2),
next_message_index: 3,
};
let state = SystemExecutionState {
description: Some(description),
epoch: Some(Epoch(1)),
admin_id: Some(ChainId::root(0)),
committees: BTreeMap::new(),
..SystemExecutionState::default()
};
let view = state.into_view().await;
(view, context)
}
#[tokio::test]
async fn bytecode_message_index() {
let (mut view, context) = new_view_and_context().await;
let operation = SystemOperation::PublishBytecode {
contract: Bytecode::new(vec![]),
service: Bytecode::new(vec![]),
};
let (result, new_application) = view
.system
.execute_operation(context, operation)
.await
.unwrap();
assert_eq!(new_application, None);
let operation_index = context
.index
.expect("Missing operation index in dummy context");
assert_eq!(
result.messages[PUBLISH_BYTECODE_MESSAGE_INDEX as usize].message,
SystemMessage::BytecodePublished { operation_index }
);
}
#[tokio::test]
async fn application_message_index() {
let (mut view, context) = new_view_and_context().await;
let bytecode_id = BytecodeId::new(MessageId {
chain_id: context.chain_id,
height: BlockHeight::from(5),
index: 0,
});
let location = BytecodeLocation {
certificate_hash: CryptoHash::test_hash("certificate"),
operation_index: 1,
};
view.system
.registry
.register_published_bytecode(bytecode_id, location)
.unwrap();
let operation = SystemOperation::CreateApplication {
bytecode_id,
parameters: vec![],
initialization_argument: vec![],
required_application_ids: vec![],
};
let (result, new_application) = view
.system
.execute_operation(context, operation)
.await
.unwrap();
assert_eq!(
result.messages[CREATE_APPLICATION_MESSAGE_INDEX as usize].message,
SystemMessage::ApplicationCreated
);
let creation = MessageId {
chain_id: context.chain_id,
height: context.height,
index: context.next_message_index + CREATE_APPLICATION_MESSAGE_INDEX,
};
let id = ApplicationId {
bytecode_id,
creation,
};
assert_eq!(new_application, Some((id, vec![])));
}
#[tokio::test]
async fn open_chain_message_index() {
let (mut view, context) = new_view_and_context().await;
let epoch = view.system.epoch.get().unwrap();
let admin_id = view.system.admin_id.get().unwrap();
let committees = view.system.committees.get().clone();
let ownership = ChainOwnership::single(PublicKey::test_key(0));
let config = OpenChainConfig {
ownership,
committees,
epoch,
admin_id,
balance: Amount::ZERO,
application_permissions: Default::default(),
};
let operation = SystemOperation::OpenChain(config.clone());
let (result, new_application) = view
.system
.execute_operation(context, operation)
.await
.unwrap();
assert_eq!(new_application, None);
assert_eq!(
result.messages[OPEN_CHAIN_MESSAGE_INDEX as usize].message,
SystemMessage::OpenChain(config)
);
}
}