pub mod committee;
pub mod evm;
mod execution;
pub mod execution_state_actor;
#[cfg(with_graphql)]
mod graphql;
mod policy;
mod resources;
mod runtime;
pub mod system;
#[cfg(with_testing)]
pub mod test_utils;
mod transaction_tracker;
mod util;
mod wasm;
use std::{any::Any, collections::BTreeMap, fmt, ops::RangeInclusive, str::FromStr, sync::Arc};
use allocative::Allocative;
use async_graphql::SimpleObject;
use async_trait::async_trait;
use custom_debug_derive::Debug;
use derive_more::Display;
#[cfg(web)]
use js_sys::wasm_bindgen::JsValue;
use linera_base::{
abi::Abi,
crypto::{BcsHashable, CryptoHash},
data_types::{
Amount, ApplicationDescription, ApplicationPermissions, ArithmeticError, Blob, BlockHeight,
Bytecode, DecompressionError, Epoch, NetworkDescription, SendMessageRequest, StreamUpdate,
Timestamp,
},
doc_scalar, hex_debug, http,
identifiers::{
Account, AccountOwner, ApplicationId, BlobId, BlobType, ChainId, DataBlobHash, EventId,
GenericApplicationId, ModuleId, StreamId, StreamName,
},
ownership::ChainOwnership,
vm::VmRuntime,
};
use linera_views::{batch::Batch, ViewError};
use serde::{Deserialize, Serialize};
use system::AdminOperation;
use thiserror::Error;
pub use web_thread_pool::Pool as ThreadPool;
use web_thread_select as web_thread;
#[cfg(with_revm)]
use crate::evm::EvmExecutionError;
use crate::system::EPOCH_STREAM_NAME;
#[cfg(with_testing)]
use crate::test_utils::dummy_chain_description;
#[cfg(all(with_testing, with_wasm_runtime))]
pub use crate::wasm::test as wasm_test;
#[cfg(with_wasm_runtime)]
pub use crate::wasm::{
BaseRuntimeApi, ContractEntrypoints, ContractRuntimeApi, RuntimeApiData, ServiceEntrypoints,
ServiceRuntimeApi, WasmContractModule, WasmExecutionError, WasmServiceModule,
};
pub use crate::{
committee::{Committee, SharedCommittees},
execution::{ExecutionStateView, ServiceRuntimeEndpoint},
execution_state_actor::{ExecutionRequest, ExecutionStateActor},
policy::ResourceControlPolicy,
resources::{BalanceHolder, ResourceController, ResourceTracker},
runtime::{
ContractSyncRuntimeHandle, ServiceRuntimeRequest, ServiceSyncRuntime,
ServiceSyncRuntimeHandle,
},
system::{
SystemExecutionStateView, SystemMessage, SystemOperation, SystemQuery, SystemResponse,
},
transaction_tracker::{TransactionOutcome, TransactionTracker},
};
pub const LINERA_SOL: &str = include_str!("../solidity/Linera.sol");
pub const LINERA_TYPES_SOL: &str = include_str!("../solidity/LineraTypes.sol");
const MAX_STREAM_NAME_LEN: usize = 64;
pub const FLAG_ZERO_HASH: &str = "FLAG_ZERO_HASH.linera.network";
pub const FLAG_FREE_REJECT: &str = "FLAG_FREE_REJECT.linera.network";
pub const FLAG_MANDATORY_APPS_NEED_ACCEPTED_MESSAGE: &str =
"FLAG_MANDATORY_APPS_NEED_ACCEPTED_MESSAGE.linera.network";
pub const FLAG_FREE_APPLICATION_ID_PREFIX: &str = "FLAG_FREE_APPLICATION_ID_";
pub const FLAG_FREE_APPLICATION_ID_SUFFIX: &str = ".linera.network";
#[derive(Clone)]
pub struct UserContractCode(Box<dyn UserContractModule>);
#[derive(Clone)]
pub struct UserServiceCode(Box<dyn UserServiceModule>);
pub type UserContractInstance = Box<dyn UserContract>;
pub type UserServiceInstance = Box<dyn UserService>;
pub trait UserContractModule: dyn_clone::DynClone + Any + web_thread::Post + Send + Sync {
fn instantiate(
&self,
runtime: ContractSyncRuntimeHandle,
) -> Result<UserContractInstance, ExecutionError>;
}
impl<T: UserContractModule + Send + Sync + 'static> From<T> for UserContractCode {
fn from(module: T) -> Self {
Self(Box::new(module))
}
}
dyn_clone::clone_trait_object!(UserContractModule);
pub trait UserServiceModule: dyn_clone::DynClone + Any + web_thread::Post + Send + Sync {
fn instantiate(
&self,
runtime: ServiceSyncRuntimeHandle,
) -> Result<UserServiceInstance, ExecutionError>;
}
impl<T: UserServiceModule + Send + Sync + 'static> From<T> for UserServiceCode {
fn from(module: T) -> Self {
Self(Box::new(module))
}
}
dyn_clone::clone_trait_object!(UserServiceModule);
impl UserServiceCode {
fn instantiate(
&self,
runtime: ServiceSyncRuntimeHandle,
) -> Result<UserServiceInstance, ExecutionError> {
self.0.instantiate(runtime)
}
}
impl UserContractCode {
fn instantiate(
&self,
runtime: ContractSyncRuntimeHandle,
) -> Result<UserContractInstance, ExecutionError> {
self.0.instantiate(runtime)
}
}
pub struct JsVec<T>(pub Vec<T>);
#[cfg(web)]
const _: () = {
impl web_thread::AsJs for UserContractCode {
fn to_js(&self) -> Result<JsValue, JsValue> {
((&*self.0) as &dyn Any)
.downcast_ref::<WasmContractModule>()
.expect("we only support Wasm modules on the Web for now")
.to_js()
}
fn from_js(value: JsValue) -> Result<Self, JsValue> {
WasmContractModule::from_js(value).map(Into::into)
}
}
impl web_thread::Post for UserContractCode {
fn transferables(&self) -> js_sys::Array {
self.0.transferables()
}
}
impl web_thread::AsJs for UserServiceCode {
fn to_js(&self) -> Result<JsValue, JsValue> {
((&*self.0) as &dyn Any)
.downcast_ref::<WasmServiceModule>()
.expect("we only support Wasm modules on the Web for now")
.to_js()
}
fn from_js(value: JsValue) -> Result<Self, JsValue> {
WasmServiceModule::from_js(value).map(Into::into)
}
}
impl web_thread::Post for UserServiceCode {
fn transferables(&self) -> js_sys::Array {
self.0.transferables()
}
}
impl<T: web_thread::AsJs> web_thread::AsJs for JsVec<T> {
fn to_js(&self) -> Result<JsValue, JsValue> {
let array = self
.0
.iter()
.map(T::to_js)
.collect::<Result<js_sys::Array, _>>()?;
Ok(array.into())
}
fn from_js(value: JsValue) -> Result<Self, JsValue> {
let array = js_sys::Array::from(&value);
let v = array
.into_iter()
.map(T::from_js)
.collect::<Result<Vec<_>, _>>()?;
Ok(JsVec(v))
}
}
impl<T: web_thread::Post> web_thread::Post for JsVec<T> {
fn transferables(&self) -> js_sys::Array {
let mut array = js_sys::Array::new();
for x in &self.0 {
array = array.concat(&x.transferables());
}
array
}
}
};
#[derive(Error, Debug, strum::IntoStaticStr)]
pub enum ExecutionError {
#[error(transparent)]
ViewError(#[from] ViewError),
#[error(transparent)]
ArithmeticError(#[from] ArithmeticError),
#[error("User application reported an error: {0}")]
UserError(String),
#[cfg(with_wasm_runtime)]
#[error(transparent)]
WasmError(#[from] WasmExecutionError),
#[cfg(with_revm)]
#[error(transparent)]
EvmError(#[from] EvmExecutionError),
#[error(transparent)]
DecompressionError(#[from] DecompressionError),
#[error("The given promise is invalid or was polled once already")]
InvalidPromise,
#[error("Attempted to perform a reentrant call to application {0}")]
ReentrantCall(ApplicationId),
#[error(
"Application {caller_id} attempted to perform a cross-application to {callee_id} call \
from `finalize`"
)]
CrossApplicationCallInFinalize {
caller_id: Box<ApplicationId>,
callee_id: Box<ApplicationId>,
},
#[error("Failed to load bytecode from storage {0:?}")]
ApplicationBytecodeNotFound(Box<ApplicationDescription>),
#[error("Unsupported dynamic application load: {0:?}")]
UnsupportedDynamicApplicationLoad(Box<ApplicationId>),
#[error("Excessive number of bytes read from storage")]
ExcessiveRead,
#[error("Excessive number of bytes written to storage")]
ExcessiveWrite,
#[error("Block execution required too much fuel for VM {0}")]
MaximumFuelExceeded(VmRuntime),
#[error("Services running as oracles in block took longer than allowed")]
MaximumServiceOracleExecutionTimeExceeded,
#[error("Service running as an oracle produced a response that's too large")]
ServiceOracleResponseTooLarge,
#[error("Serialized size of the block exceeds limit")]
BlockTooLarge,
#[error("HTTP response exceeds the size limit of {limit} bytes, having at least {size} bytes")]
HttpResponseSizeLimitExceeded { limit: u64, size: u64 },
#[error("Runtime failed to respond to application")]
MissingRuntimeResponse,
#[error("Application is not authorized to perform system operations on this chain: {0:}")]
UnauthorizedApplication(ApplicationId),
#[error("Failed to make network reqwest: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("Encountered I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("More recorded oracle responses than expected")]
UnexpectedOracleResponse,
#[error("Invalid JSON: {0}")]
JsonError(#[from] serde_json::Error),
#[error(transparent)]
BcsError(#[from] bcs::Error),
#[error("Recorded response for oracle query has the wrong type")]
OracleResponseMismatch,
#[error("Service oracle query tried to create operations: {0:?}")]
ServiceOracleQueryOperations(Vec<Operation>),
#[error("Assertion failed: local time {local_time} is not earlier than {timestamp}")]
AssertBefore {
timestamp: Timestamp,
local_time: Timestamp,
},
#[error("Stream names can be at most {MAX_STREAM_NAME_LEN} bytes.")]
StreamNameTooLong,
#[error("Blob exceeds size limit")]
BlobTooLarge,
#[error("Bytecode exceeds size limit")]
BytecodeTooLarge,
#[error("Attempt to perform an HTTP request to an unauthorized host: {0:?}")]
UnauthorizedHttpRequest(reqwest::Url),
#[error("Attempt to perform an HTTP request to an invalid URL")]
InvalidUrlForHttpRequest(#[from] url::ParseError),
#[error("Worker thread failure: {0:?}")]
Thread(#[from] web_thread::Error),
#[error("The chain being queried is not active {0}")]
InactiveChain(ChainId),
#[error("Blobs not found: {0:?}")]
BlobsNotFound(Vec<BlobId>),
#[error("Events not found: {0:?}")]
EventsNotFound(Vec<EventId>),
#[error("Invalid HTTP header name used for HTTP request")]
InvalidHeaderName(#[from] reqwest::header::InvalidHeaderName),
#[error("Invalid HTTP header value used for HTTP request")]
InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
#[error("No NetworkDescription found in storage")]
NoNetworkDescriptionFound,
#[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 balance of the current account {account}: {balance}")]
InsufficientBalance {
balance: Amount,
account: AccountOwner,
},
#[error("Required execution fees exceeded the total funding available. Fees {fees}, available balance: {balance}")]
FeesExceedFunding { fees: Amount, 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: expected {expected}, but got {provided}")]
InvalidCommitteeEpoch { expected: Epoch, provided: Epoch },
#[error("Failed to remove committee")]
InvalidCommitteeRemoval,
#[error("No recorded response for oracle query")]
MissingOracleResponse,
#[error("process_streams was not called for all stream updates")]
UnprocessedStreams,
#[error("Internal error: {0}")]
InternalError(&'static str),
#[error("UpdateStreams is outdated")]
OutdatedUpdateStreams,
}
impl ExecutionError {
pub fn is_local(&self) -> bool {
match self {
ExecutionError::ArithmeticError(_)
| ExecutionError::UserError(_)
| ExecutionError::DecompressionError(_)
| ExecutionError::InvalidPromise
| ExecutionError::CrossApplicationCallInFinalize { .. }
| ExecutionError::ReentrantCall(_)
| ExecutionError::ApplicationBytecodeNotFound(_)
| ExecutionError::UnsupportedDynamicApplicationLoad(_)
| ExecutionError::ExcessiveRead
| ExecutionError::ExcessiveWrite
| ExecutionError::MaximumFuelExceeded(_)
| ExecutionError::MaximumServiceOracleExecutionTimeExceeded
| ExecutionError::ServiceOracleResponseTooLarge
| ExecutionError::BlockTooLarge
| ExecutionError::HttpResponseSizeLimitExceeded { .. }
| ExecutionError::UnauthorizedApplication(_)
| ExecutionError::UnexpectedOracleResponse
| ExecutionError::JsonError(_)
| ExecutionError::BcsError(_)
| ExecutionError::OracleResponseMismatch
| ExecutionError::ServiceOracleQueryOperations(_)
| ExecutionError::AssertBefore { .. }
| ExecutionError::StreamNameTooLong
| ExecutionError::BlobTooLarge
| ExecutionError::BytecodeTooLarge
| ExecutionError::UnauthorizedHttpRequest(_)
| ExecutionError::InvalidUrlForHttpRequest(_)
| ExecutionError::InactiveChain(_)
| ExecutionError::BlobsNotFound(_)
| ExecutionError::EventsNotFound(_)
| ExecutionError::InvalidHeaderName(_)
| ExecutionError::InvalidHeaderValue(_)
| ExecutionError::InvalidEpoch { .. }
| ExecutionError::IncorrectTransferAmount
| ExecutionError::UnauthenticatedTransferOwner
| ExecutionError::InsufficientBalance { .. }
| ExecutionError::FeesExceedFunding { .. }
| ExecutionError::IncorrectClaimAmount
| ExecutionError::UnauthenticatedClaimOwner
| ExecutionError::AdminOperationOnNonAdminChain
| ExecutionError::InvalidCommitteeEpoch { .. }
| ExecutionError::InvalidCommitteeRemoval
| ExecutionError::MissingOracleResponse
| ExecutionError::UnprocessedStreams
| ExecutionError::OutdatedUpdateStreams
| ExecutionError::ViewError(ViewError::NotFound(_)) => false,
#[cfg(with_wasm_runtime)]
ExecutionError::WasmError(_) => false,
#[cfg(with_revm)]
ExecutionError::EvmError(..) => false,
ExecutionError::MissingRuntimeResponse
| ExecutionError::ViewError(_)
| ExecutionError::ReqwestError(_)
| ExecutionError::Thread(_)
| ExecutionError::NoNetworkDescriptionFound
| ExecutionError::InternalError(_)
| ExecutionError::IoError(_) => true,
}
}
pub fn error_type(&self) -> String {
let variant: &'static str = self.into();
format!("ExecutionError::{variant}")
}
pub fn is_limit_error(&self) -> bool {
matches!(
self,
ExecutionError::ExcessiveRead
| ExecutionError::ExcessiveWrite
| ExecutionError::MaximumFuelExceeded(_)
| ExecutionError::MaximumServiceOracleExecutionTimeExceeded
| ExecutionError::BlockTooLarge
)
}
pub fn is_transient_error(&self) -> bool {
matches!(
self,
ExecutionError::BlobsNotFound(_) | ExecutionError::EventsNotFound(_)
)
}
}
pub trait UserContract {
fn instantiate(&mut self, argument: Vec<u8>) -> Result<(), ExecutionError>;
fn execute_operation(&mut self, operation: Vec<u8>) -> Result<Vec<u8>, ExecutionError>;
fn execute_message(&mut self, message: Vec<u8>) -> Result<(), ExecutionError>;
fn process_streams(&mut self, updates: Vec<StreamUpdate>) -> Result<(), ExecutionError>;
fn finalize(&mut self) -> Result<(), ExecutionError>;
}
pub trait UserService {
fn handle_query(&mut self, argument: Vec<u8>) -> Result<Vec<u8>, ExecutionError>;
}
#[derive(Clone, Copy)]
pub struct ExecutionRuntimeConfig {
pub allow_application_logs: bool,
}
impl Default for ExecutionRuntimeConfig {
fn default() -> Self {
Self {
allow_application_logs: true,
}
}
}
#[cfg_attr(not(web), async_trait)]
#[cfg_attr(web, async_trait(?Send))]
pub trait ExecutionRuntimeContext {
fn chain_id(&self) -> ChainId;
fn thread_pool(&self) -> &Arc<ThreadPool>;
fn execution_runtime_config(&self) -> ExecutionRuntimeConfig;
fn user_contracts(&self) -> &Arc<papaya::HashMap<ApplicationId, UserContractCode>>;
fn user_services(&self) -> &Arc<papaya::HashMap<ApplicationId, UserServiceCode>>;
async fn get_user_contract(
&self,
description: &ApplicationDescription,
txn_tracker: &TransactionTracker,
) -> Result<UserContractCode, ExecutionError>;
async fn get_user_service(
&self,
description: &ApplicationDescription,
txn_tracker: &TransactionTracker,
) -> Result<UserServiceCode, ExecutionError>;
async fn get_blob(&self, blob_id: BlobId) -> Result<Option<Arc<Blob>>, ViewError>;
async fn get_event(&self, event_id: EventId) -> Result<Option<Arc<Vec<u8>>>, ViewError>;
async fn get_network_description(&self) -> Result<Option<NetworkDescription>, ViewError>;
async fn get_committees(
&self,
epoch_range: RangeInclusive<Epoch>,
) -> Result<BTreeMap<Epoch, Committee>, ExecutionError> {
let mut committees = BTreeMap::new();
let mut missing = Vec::new();
for index in epoch_range.start().0..=epoch_range.end().0 {
let epoch = Epoch(index);
match self.get_or_load_committee(epoch).await? {
Some(committee) => {
committees.insert(epoch, (*committee).clone());
}
None => missing.push(epoch),
}
}
if !missing.is_empty() {
let net_description = self
.get_network_description()
.await?
.ok_or(ExecutionError::NoNetworkDescriptionFound)?;
let event_ids = missing
.into_iter()
.map(|epoch| EventId {
chain_id: net_description.admin_chain_id,
stream_id: StreamId::system(EPOCH_STREAM_NAME),
index: epoch.0,
})
.collect();
return Err(ExecutionError::EventsNotFound(event_ids));
}
Ok(committees)
}
async fn get_or_load_committee(
&self,
epoch: Epoch,
) -> Result<Option<Arc<Committee>>, ViewError>;
async fn contains_blob(&self, blob_id: BlobId) -> Result<bool, ViewError>;
async fn contains_event(&self, event_id: EventId) -> Result<bool, ViewError>;
#[cfg(with_testing)]
async fn add_blobs(
&self,
blobs: impl IntoIterator<Item = Blob> + Send,
) -> Result<(), ViewError>;
#[cfg(with_testing)]
async fn add_events(
&self,
events: impl IntoIterator<Item = (EventId, Vec<u8>)> + Send,
) -> Result<(), ViewError>;
}
#[derive(Clone, Copy, Debug)]
pub struct OperationContext {
pub chain_id: ChainId,
#[debug(skip_if = Option::is_none)]
pub authenticated_signer: Option<AccountOwner>,
pub height: BlockHeight,
pub round: Option<u32>,
pub timestamp: Timestamp,
}
#[derive(Clone, Copy, Debug)]
pub struct MessageContext {
pub chain_id: ChainId,
pub origin: ChainId,
pub is_bouncing: bool,
#[debug(skip_if = Option::is_none)]
pub authenticated_signer: Option<AccountOwner>,
#[debug(skip_if = Option::is_none)]
pub refund_grant_to: Option<Account>,
pub height: BlockHeight,
pub round: Option<u32>,
pub timestamp: Timestamp,
}
#[derive(Clone, Copy, Debug)]
pub struct ProcessStreamsContext {
pub chain_id: ChainId,
pub height: BlockHeight,
pub round: Option<u32>,
pub timestamp: Timestamp,
}
impl From<MessageContext> for ProcessStreamsContext {
fn from(context: MessageContext) -> Self {
Self {
chain_id: context.chain_id,
height: context.height,
round: context.round,
timestamp: context.timestamp,
}
}
}
impl From<OperationContext> for ProcessStreamsContext {
fn from(context: OperationContext) -> Self {
Self {
chain_id: context.chain_id,
height: context.height,
round: context.round,
timestamp: context.timestamp,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct FinalizeContext {
pub chain_id: ChainId,
#[debug(skip_if = Option::is_none)]
pub authenticated_signer: Option<AccountOwner>,
pub height: BlockHeight,
pub round: Option<u32>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct QueryContext {
pub chain_id: ChainId,
pub next_block_height: BlockHeight,
pub local_time: Timestamp,
}
pub trait BaseRuntime {
type Read: fmt::Debug + Send + Sync;
type ContainsKey: fmt::Debug + Send + Sync;
type ContainsKeys: fmt::Debug + Send + Sync;
type ReadMultiValuesBytes: fmt::Debug + Send + Sync;
type ReadValueBytes: fmt::Debug + Send + Sync;
type FindKeysByPrefix: fmt::Debug + Send + Sync;
type FindKeyValuesByPrefix: fmt::Debug + Send + Sync;
fn chain_id(&mut self) -> Result<ChainId, ExecutionError>;
fn block_height(&mut self) -> Result<BlockHeight, ExecutionError>;
fn application_id(&mut self) -> Result<ApplicationId, ExecutionError>;
fn application_creator_chain_id(&mut self) -> Result<ChainId, ExecutionError>;
fn read_application_description(
&mut self,
application_id: ApplicationId,
) -> Result<ApplicationDescription, ExecutionError>;
fn application_parameters(&mut self) -> Result<Vec<u8>, ExecutionError>;
fn read_system_timestamp(&mut self) -> Result<Timestamp, ExecutionError>;
fn read_chain_balance(&mut self) -> Result<Amount, ExecutionError>;
fn read_owner_balance(&mut self, owner: AccountOwner) -> Result<Amount, ExecutionError>;
fn read_owner_balances(&mut self) -> Result<Vec<(AccountOwner, Amount)>, ExecutionError>;
fn read_balance_owners(&mut self) -> Result<Vec<AccountOwner>, ExecutionError>;
fn chain_ownership(&mut self) -> Result<ChainOwnership, ExecutionError>;
fn application_permissions(&mut self) -> Result<ApplicationPermissions, ExecutionError>;
#[cfg(feature = "test")]
fn contains_key(&mut self, key: Vec<u8>) -> Result<bool, ExecutionError> {
let promise = self.contains_key_new(key)?;
self.contains_key_wait(&promise)
}
fn contains_key_new(&mut self, key: Vec<u8>) -> Result<Self::ContainsKey, ExecutionError>;
fn contains_key_wait(&mut self, promise: &Self::ContainsKey) -> Result<bool, ExecutionError>;
#[cfg(feature = "test")]
fn contains_keys(&mut self, keys: Vec<Vec<u8>>) -> Result<Vec<bool>, ExecutionError> {
let promise = self.contains_keys_new(keys)?;
self.contains_keys_wait(&promise)
}
fn contains_keys_new(
&mut self,
keys: Vec<Vec<u8>>,
) -> Result<Self::ContainsKeys, ExecutionError>;
fn contains_keys_wait(
&mut self,
promise: &Self::ContainsKeys,
) -> Result<Vec<bool>, ExecutionError>;
#[cfg(feature = "test")]
fn read_multi_values_bytes(
&mut self,
keys: Vec<Vec<u8>>,
) -> Result<Vec<Option<Vec<u8>>>, ExecutionError> {
let promise = self.read_multi_values_bytes_new(keys)?;
self.read_multi_values_bytes_wait(&promise)
}
fn read_multi_values_bytes_new(
&mut self,
keys: Vec<Vec<u8>>,
) -> Result<Self::ReadMultiValuesBytes, ExecutionError>;
fn read_multi_values_bytes_wait(
&mut self,
promise: &Self::ReadMultiValuesBytes,
) -> Result<Vec<Option<Vec<u8>>>, ExecutionError>;
#[cfg(feature = "test")]
fn read_value_bytes(&mut self, key: Vec<u8>) -> Result<Option<Vec<u8>>, ExecutionError> {
let promise = self.read_value_bytes_new(key)?;
self.read_value_bytes_wait(&promise)
}
fn read_value_bytes_new(
&mut self,
key: Vec<u8>,
) -> Result<Self::ReadValueBytes, ExecutionError>;
fn read_value_bytes_wait(
&mut self,
promise: &Self::ReadValueBytes,
) -> Result<Option<Vec<u8>>, ExecutionError>;
fn find_keys_by_prefix_new(
&mut self,
key_prefix: Vec<u8>,
) -> Result<Self::FindKeysByPrefix, ExecutionError>;
fn find_keys_by_prefix_wait(
&mut self,
promise: &Self::FindKeysByPrefix,
) -> Result<Vec<Vec<u8>>, ExecutionError>;
#[cfg(feature = "test")]
#[expect(clippy::type_complexity)]
fn find_key_values_by_prefix(
&mut self,
key_prefix: Vec<u8>,
) -> Result<Vec<(Vec<u8>, Vec<u8>)>, ExecutionError> {
let promise = self.find_key_values_by_prefix_new(key_prefix)?;
self.find_key_values_by_prefix_wait(&promise)
}
fn find_key_values_by_prefix_new(
&mut self,
key_prefix: Vec<u8>,
) -> Result<Self::FindKeyValuesByPrefix, ExecutionError>;
#[expect(clippy::type_complexity)]
fn find_key_values_by_prefix_wait(
&mut self,
promise: &Self::FindKeyValuesByPrefix,
) -> Result<Vec<(Vec<u8>, Vec<u8>)>, ExecutionError>;
fn perform_http_request(
&mut self,
request: http::Request,
) -> Result<http::Response, ExecutionError>;
fn assert_before(&mut self, timestamp: Timestamp) -> Result<(), ExecutionError>;
fn read_data_blob(&mut self, hash: DataBlobHash) -> Result<Vec<u8>, ExecutionError>;
fn assert_data_blob_exists(&mut self, hash: DataBlobHash) -> Result<(), ExecutionError>;
fn allow_application_logs(&mut self) -> Result<bool, ExecutionError>;
#[cfg(web)]
fn send_log(&mut self, message: String, level: tracing::log::Level);
}
pub trait ServiceRuntime: BaseRuntime {
fn try_query_application(
&mut self,
queried_id: ApplicationId,
argument: Vec<u8>,
) -> Result<Vec<u8>, ExecutionError>;
fn schedule_operation(&mut self, operation: Vec<u8>) -> Result<(), ExecutionError>;
fn check_execution_time(&mut self) -> Result<(), ExecutionError>;
}
pub trait ContractRuntime: BaseRuntime {
fn authenticated_signer(&mut self) -> Result<Option<AccountOwner>, ExecutionError>;
fn message_is_bouncing(&mut self) -> Result<Option<bool>, ExecutionError>;
fn message_origin_chain_id(&mut self) -> Result<Option<ChainId>, ExecutionError>;
fn authenticated_caller_id(&mut self) -> Result<Option<ApplicationId>, ExecutionError>;
fn maximum_fuel_per_block(&mut self, vm_runtime: VmRuntime) -> Result<u64, ExecutionError>;
fn remaining_fuel(&mut self, vm_runtime: VmRuntime) -> Result<u64, ExecutionError>;
fn consume_fuel(&mut self, fuel: u64, vm_runtime: VmRuntime) -> Result<(), ExecutionError>;
fn send_message(&mut self, message: SendMessageRequest<Vec<u8>>) -> Result<(), ExecutionError>;
fn transfer(
&mut self,
source: AccountOwner,
destination: Account,
amount: Amount,
) -> Result<(), ExecutionError>;
fn claim(
&mut self,
source: Account,
destination: Account,
amount: Amount,
) -> Result<(), ExecutionError>;
fn try_call_application(
&mut self,
authenticated: bool,
callee_id: ApplicationId,
argument: Vec<u8>,
) -> Result<Vec<u8>, ExecutionError>;
fn emit(&mut self, name: StreamName, value: Vec<u8>) -> Result<u32, ExecutionError>;
fn read_event(
&mut self,
chain_id: ChainId,
stream_name: StreamName,
index: u32,
) -> Result<Vec<u8>, ExecutionError>;
fn subscribe_to_events(
&mut self,
chain_id: ChainId,
application_id: ApplicationId,
stream_name: StreamName,
) -> Result<(), ExecutionError>;
fn unsubscribe_from_events(
&mut self,
chain_id: ChainId,
application_id: ApplicationId,
stream_name: StreamName,
) -> Result<(), ExecutionError>;
fn query_service(
&mut self,
application_id: ApplicationId,
query: Vec<u8>,
) -> Result<Vec<u8>, ExecutionError>;
fn open_chain(
&mut self,
ownership: ChainOwnership,
application_permissions: ApplicationPermissions,
balance: Amount,
) -> Result<ChainId, ExecutionError>;
fn close_chain(&mut self) -> Result<(), ExecutionError>;
fn change_ownership(&mut self, ownership: ChainOwnership) -> Result<(), ExecutionError>;
fn change_application_permissions(
&mut self,
application_permissions: ApplicationPermissions,
) -> Result<(), ExecutionError>;
fn create_application(
&mut self,
module_id: ModuleId,
parameters: Vec<u8>,
argument: Vec<u8>,
required_application_ids: Vec<ApplicationId>,
) -> Result<ApplicationId, ExecutionError>;
fn create_data_blob(&mut self, bytes: Vec<u8>) -> Result<DataBlobHash, ExecutionError>;
fn publish_module(
&mut self,
contract: Bytecode,
service: Bytecode,
vm_runtime: VmRuntime,
) -> Result<ModuleId, ExecutionError>;
fn validation_round(&mut self) -> Result<Option<u32>, ExecutionError>;
fn write_batch(&mut self, batch: Batch) -> Result<(), ExecutionError>;
}
#[derive(
Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative, strum::AsRefStr,
)]
pub enum Operation {
System(Box<SystemOperation>),
User {
application_id: ApplicationId,
#[serde(with = "serde_bytes")]
#[debug(with = "hex_debug")]
bytes: Vec<u8>,
},
}
impl BcsHashable<'_> for Operation {}
#[derive(
Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative, strum::AsRefStr,
)]
pub enum Message {
System(SystemMessage),
User {
application_id: ApplicationId,
#[serde(with = "serde_bytes")]
#[debug(with = "hex_debug")]
bytes: Vec<u8>,
},
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub enum Query {
System(SystemQuery),
User {
application_id: ApplicationId,
#[serde(with = "serde_bytes")]
#[debug(with = "hex_debug")]
bytes: Vec<u8>,
},
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub struct QueryOutcome<Response = QueryResponse> {
pub response: Response,
pub operations: Vec<Operation>,
}
impl From<QueryOutcome<SystemResponse>> for QueryOutcome {
fn from(system_outcome: QueryOutcome<SystemResponse>) -> Self {
let QueryOutcome {
response,
operations,
} = system_outcome;
QueryOutcome {
response: QueryResponse::System(response),
operations,
}
}
}
impl From<QueryOutcome<Vec<u8>>> for QueryOutcome {
fn from(user_service_outcome: QueryOutcome<Vec<u8>>) -> Self {
let QueryOutcome {
response,
operations,
} = user_service_outcome;
QueryOutcome {
response: QueryResponse::User(response),
operations,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub enum QueryResponse {
System(SystemResponse),
User(
#[serde(with = "serde_bytes")]
#[debug(with = "hex_debug")]
Vec<u8>,
),
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Copy, Allocative)]
pub enum MessageKind {
Simple,
Protected,
Tracked,
Bouncing,
}
impl Display for MessageKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MessageKind::Simple => write!(f, "Simple"),
MessageKind::Protected => write!(f, "Protected"),
MessageKind::Tracked => write!(f, "Tracked"),
MessageKind::Bouncing => write!(f, "Bouncing"),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
pub struct OutgoingMessage {
pub destination: ChainId,
#[debug(skip_if = Option::is_none)]
pub authenticated_signer: Option<AccountOwner>,
#[debug(skip_if = Amount::is_zero)]
pub grant: Amount,
#[debug(skip_if = Option::is_none)]
pub refund_grant_to: Option<Account>,
pub kind: MessageKind,
pub message: Message,
}
impl BcsHashable<'_> for OutgoingMessage {}
impl OutgoingMessage {
pub fn new(recipient: ChainId, message: impl Into<Message>) -> Self {
OutgoingMessage {
destination: recipient,
authenticated_signer: None,
grant: Amount::ZERO,
refund_grant_to: None,
kind: MessageKind::Simple,
message: message.into(),
}
}
pub fn with_kind(mut self, kind: MessageKind) -> Self {
self.kind = kind;
self
}
pub fn with_authenticated_signer(mut self, authenticated_signer: Option<AccountOwner>) -> Self {
self.authenticated_signer = authenticated_signer;
self
}
}
impl OperationContext {
fn refund_grant_to(&self) -> Option<Account> {
self.authenticated_signer.map(|owner| Account {
chain_id: self.chain_id,
owner,
})
}
}
#[cfg(with_testing)]
#[derive(Clone)]
pub struct TestExecutionRuntimeContext {
chain_id: ChainId,
thread_pool: Arc<ThreadPool>,
execution_runtime_config: ExecutionRuntimeConfig,
user_contracts: Arc<papaya::HashMap<ApplicationId, UserContractCode>>,
user_services: Arc<papaya::HashMap<ApplicationId, UserServiceCode>>,
blobs: Arc<papaya::HashMap<BlobId, Blob>>,
events: Arc<papaya::HashMap<EventId, Vec<u8>>>,
}
#[cfg(with_testing)]
impl TestExecutionRuntimeContext {
pub fn new(chain_id: ChainId, execution_runtime_config: ExecutionRuntimeConfig) -> Self {
Self {
chain_id,
thread_pool: Arc::new(ThreadPool::new(20)),
execution_runtime_config,
user_contracts: Arc::default(),
user_services: Arc::default(),
blobs: Arc::default(),
events: Arc::default(),
}
}
}
#[cfg(with_testing)]
#[cfg_attr(not(web), async_trait)]
#[cfg_attr(web, async_trait(?Send))]
impl ExecutionRuntimeContext for TestExecutionRuntimeContext {
fn chain_id(&self) -> ChainId {
self.chain_id
}
fn thread_pool(&self) -> &Arc<ThreadPool> {
&self.thread_pool
}
fn execution_runtime_config(&self) -> ExecutionRuntimeConfig {
self.execution_runtime_config
}
fn user_contracts(&self) -> &Arc<papaya::HashMap<ApplicationId, UserContractCode>> {
&self.user_contracts
}
fn user_services(&self) -> &Arc<papaya::HashMap<ApplicationId, UserServiceCode>> {
&self.user_services
}
async fn get_user_contract(
&self,
description: &ApplicationDescription,
_txn_tracker: &TransactionTracker,
) -> Result<UserContractCode, ExecutionError> {
let application_id: ApplicationId = description.into();
let pinned = self.user_contracts().pin();
Ok(pinned
.get(&application_id)
.ok_or_else(|| {
ExecutionError::ApplicationBytecodeNotFound(Box::new(description.clone()))
})?
.clone())
}
async fn get_user_service(
&self,
description: &ApplicationDescription,
_txn_tracker: &TransactionTracker,
) -> Result<UserServiceCode, ExecutionError> {
let application_id: ApplicationId = description.into();
let pinned = self.user_services().pin();
Ok(pinned
.get(&application_id)
.ok_or_else(|| {
ExecutionError::ApplicationBytecodeNotFound(Box::new(description.clone()))
})?
.clone())
}
async fn get_blob(&self, blob_id: BlobId) -> Result<Option<Arc<Blob>>, ViewError> {
Ok(self.blobs.pin().get(&blob_id).cloned().map(Arc::new))
}
async fn get_event(&self, event_id: EventId) -> Result<Option<Arc<Vec<u8>>>, ViewError> {
Ok(self.events.pin().get(&event_id).cloned().map(Arc::new))
}
async fn get_network_description(&self) -> Result<Option<NetworkDescription>, ViewError> {
let pinned = self.blobs.pin();
let genesis_committee_blob_hash = pinned
.iter()
.find(|(_, blob)| blob.content().blob_type() == BlobType::Committee)
.map_or_else(
|| CryptoHash::test_hash("genesis committee"),
|(_, blob)| blob.id().hash,
);
Ok(Some(NetworkDescription {
admin_chain_id: dummy_chain_description(0).id(),
genesis_config_hash: CryptoHash::test_hash("genesis config"),
genesis_timestamp: Timestamp::from(0),
genesis_committee_blob_hash,
name: "dummy network description".to_string(),
}))
}
async fn get_or_load_committee(
&self,
epoch: Epoch,
) -> Result<Option<Arc<Committee>>, ViewError> {
let Some(net_description) = self.get_network_description().await? else {
return Ok(None);
};
let blob_hash = if epoch.0 == 0 {
net_description.genesis_committee_blob_hash
} else {
let event_id = EventId {
chain_id: net_description.admin_chain_id,
stream_id: StreamId::system(EPOCH_STREAM_NAME),
index: epoch.0,
};
match self.get_event(event_id).await? {
Some(bytes) => bcs::from_bytes(&bytes)?,
None => return Ok(None),
}
};
let blob_id = BlobId::new(blob_hash, BlobType::Committee);
let Some(blob) = self.get_blob(blob_id).await? else {
return Ok(None);
};
let committee: Committee = bcs::from_bytes(blob.bytes())?;
Ok(Some(Arc::new(committee)))
}
async fn contains_blob(&self, blob_id: BlobId) -> Result<bool, ViewError> {
Ok(self.blobs.pin().contains_key(&blob_id))
}
async fn contains_event(&self, event_id: EventId) -> Result<bool, ViewError> {
Ok(self.events.pin().contains_key(&event_id))
}
#[cfg(with_testing)]
async fn add_blobs(
&self,
blobs: impl IntoIterator<Item = Blob> + Send,
) -> Result<(), ViewError> {
let pinned = self.blobs.pin();
for blob in blobs {
pinned.insert(blob.id(), blob);
}
Ok(())
}
#[cfg(with_testing)]
async fn add_events(
&self,
events: impl IntoIterator<Item = (EventId, Vec<u8>)> + Send,
) -> Result<(), ViewError> {
let pinned = self.events.pin();
for (event_id, bytes) in events {
pinned.insert(event_id, bytes);
}
Ok(())
}
}
impl From<SystemOperation> for Operation {
fn from(operation: SystemOperation) -> Self {
Operation::System(Box::new(operation))
}
}
impl Operation {
pub fn system(operation: SystemOperation) -> Self {
Operation::System(Box::new(operation))
}
#[cfg(with_testing)]
pub fn user<A: Abi>(
application_id: ApplicationId<A>,
operation: &A::Operation,
) -> Result<Self, bcs::Error> {
Self::user_without_abi(application_id.forget_abi(), operation)
}
#[cfg(with_testing)]
pub fn user_without_abi(
application_id: ApplicationId,
operation: &impl Serialize,
) -> Result<Self, bcs::Error> {
Ok(Operation::User {
application_id,
bytes: bcs::to_bytes(&operation)?,
})
}
pub fn as_system_operation(&self) -> Option<&SystemOperation> {
match self {
Operation::System(system_operation) => Some(system_operation),
Operation::User { .. } => None,
}
}
pub fn application_id(&self) -> GenericApplicationId {
match self {
Self::System(_) => GenericApplicationId::System,
Self::User { application_id, .. } => GenericApplicationId::User(*application_id),
}
}
pub fn published_blob_ids(&self) -> Vec<BlobId> {
match self.as_system_operation() {
Some(SystemOperation::PublishDataBlob { blob_hash }) => {
vec![BlobId::new(*blob_hash, BlobType::Data)]
}
Some(SystemOperation::Admin(AdminOperation::PublishCommitteeBlob { blob_hash })) => {
vec![BlobId::new(*blob_hash, BlobType::Committee)]
}
Some(SystemOperation::PublishModule { module_id }) => module_id.bytecode_blob_ids(),
_ => vec![],
}
}
pub fn is_exempt_from_permissions(&self) -> bool {
let Operation::System(system_op) = self else {
return false;
};
matches!(
**system_op,
SystemOperation::ProcessNewEpoch(_)
| SystemOperation::ProcessRemovedEpoch(_)
| SystemOperation::UpdateStreams(_)
)
}
}
impl From<SystemMessage> for Message {
fn from(message: SystemMessage) -> Self {
Message::System(message)
}
}
impl Message {
pub fn system(message: SystemMessage) -> Self {
Message::System(message)
}
pub fn user<A, M: Serialize>(
application_id: ApplicationId<A>,
message: &M,
) -> Result<Self, bcs::Error> {
let application_id = application_id.forget_abi();
let bytes = bcs::to_bytes(&message)?;
Ok(Message::User {
application_id,
bytes,
})
}
pub fn application_id(&self) -> GenericApplicationId {
match self {
Self::System(_) => GenericApplicationId::System,
Self::User { application_id, .. } => GenericApplicationId::User(*application_id),
}
}
}
impl From<SystemQuery> for Query {
fn from(query: SystemQuery) -> Self {
Query::System(query)
}
}
impl Query {
pub fn system(query: SystemQuery) -> Self {
Query::System(query)
}
pub fn user<A: Abi>(
application_id: ApplicationId<A>,
query: &A::Query,
) -> Result<Self, serde_json::Error> {
Self::user_without_abi(application_id.forget_abi(), query)
}
pub fn user_without_abi(
application_id: ApplicationId,
query: &impl Serialize,
) -> Result<Self, serde_json::Error> {
Ok(Query::User {
application_id,
bytes: serde_json::to_vec(&query)?,
})
}
pub fn application_id(&self) -> GenericApplicationId {
match self {
Self::System(_) => GenericApplicationId::System,
Self::User { application_id, .. } => GenericApplicationId::User(*application_id),
}
}
}
impl From<SystemResponse> for QueryResponse {
fn from(response: SystemResponse) -> Self {
QueryResponse::System(response)
}
}
impl From<Vec<u8>> for QueryResponse {
fn from(response: Vec<u8>) -> Self {
QueryResponse::User(response)
}
}
#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize, Deserialize)]
pub struct BlobState {
pub last_used_by: Option<CryptoHash>,
pub chain_id: ChainId,
pub block_height: BlockHeight,
pub epoch: Option<Epoch>,
}
#[derive(Clone, Copy, Display)]
#[cfg_attr(with_wasm_runtime, derive(Debug, Default))]
pub enum WasmRuntime {
#[cfg(with_wasmer)]
#[default]
#[display("wasmer")]
Wasmer,
#[cfg(with_wasmtime)]
#[cfg_attr(not(with_wasmer), default)]
#[display("wasmtime")]
Wasmtime,
}
#[derive(Clone, Copy, Display)]
#[cfg_attr(with_revm, derive(Debug, Default))]
pub enum EvmRuntime {
#[cfg(with_revm)]
#[default]
#[display("revm")]
Revm,
}
pub trait WithWasmDefault {
fn with_wasm_default(self) -> Self;
}
impl WithWasmDefault for Option<WasmRuntime> {
fn with_wasm_default(self) -> Self {
#[cfg(with_wasm_runtime)]
{
Some(self.unwrap_or_default())
}
#[cfg(not(with_wasm_runtime))]
{
None
}
}
}
impl FromStr for WasmRuntime {
type Err = InvalidWasmRuntime;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
#[cfg(with_wasmer)]
"wasmer" => Ok(WasmRuntime::Wasmer),
#[cfg(with_wasmtime)]
"wasmtime" => Ok(WasmRuntime::Wasmtime),
unknown => Err(InvalidWasmRuntime(unknown.to_owned())),
}
}
}
#[derive(Clone, Debug, Error)]
#[error("{0:?} is not a valid WebAssembly runtime")]
pub struct InvalidWasmRuntime(String);
doc_scalar!(Operation, "An operation to be executed in a block");
doc_scalar!(
Message,
"A message to be sent and possibly executed in the receiver's block."
);
doc_scalar!(MessageKind, "The kind of outgoing message being sent");