diff --git a/Cargo.lock b/Cargo.lock index 8de18c6d49..a08a260793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,6 +689,7 @@ dependencies = [ "blake2", "criterion", "datasize", + "derive_more", "derp", "ed25519-dalek", "getrandom", @@ -936,6 +937,30 @@ dependencies = [ "casper-types", ] +[[package]] +name = "contract-messages-emitter" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-types", +] + +[[package]] +name = "contract-messages-from-account" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-types", +] + +[[package]] +name = "contract-messages-upgrader" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-types", +] + [[package]] name = "convert_case" version = "0.4.0" diff --git a/execution_engine/src/engine_state/execution_result.rs b/execution_engine/src/engine_state/execution_result.rs index b75086ec4f..58b742886b 100644 --- a/execution_engine/src/engine_state/execution_result.rs +++ b/execution_engine/src/engine_state/execution_result.rs @@ -4,6 +4,7 @@ use std::collections::VecDeque; use casper_types::{ bytesrepr::FromBytes, + contract_messages::Messages, execution::{Effects, ExecutionResultV2 as TypesExecutionResult, Transform, TransformKind}, CLTyped, CLValue, Gas, Key, Motes, StoredValue, TransferAddr, }; @@ -25,6 +26,8 @@ pub enum ExecutionResult { cost: Gas, /// Execution effects. effects: Effects, + /// Messages emitted during execution. + messages: Messages, }, /// Execution was finished successfully Success { @@ -34,6 +37,8 @@ pub enum ExecutionResult { cost: Gas, /// Execution effects. effects: Effects, + /// Messages emitted during execution. + messages: Messages, }, } @@ -60,6 +65,7 @@ impl ExecutionResult { transfers: Vec::default(), cost: Gas::default(), effects: Effects::new(), + messages: Vec::default(), } } @@ -127,19 +133,25 @@ impl ExecutionResult { error, transfers, effects, + messages, .. } => ExecutionResult::Failure { error, transfers, cost, effects, + messages, }, ExecutionResult::Success { - transfers, effects, .. + transfers, + effects, + messages, + .. } => ExecutionResult::Success { transfers, cost, effects, + messages, }, } } @@ -154,17 +166,25 @@ impl ExecutionResult { error, cost, effects, + messages, .. } => ExecutionResult::Failure { error, transfers, cost, effects, + messages, }, - ExecutionResult::Success { cost, effects, .. } => ExecutionResult::Success { + ExecutionResult::Success { + cost, + effects, + messages, + .. + } => ExecutionResult::Success { transfers, cost, effects, + messages, }, } } @@ -180,20 +200,24 @@ impl ExecutionResult { transfers, cost, effects: _, + messages, } => ExecutionResult::Failure { error, transfers, cost, effects, + messages, }, ExecutionResult::Success { transfers, cost, effects: _, + messages, } => ExecutionResult::Success { transfers, cost, effects, + messages, }, } } @@ -291,6 +315,7 @@ impl ExecutionResult { effects, transfers, cost: gas_cost, + messages: Vec::default(), }) } @@ -305,28 +330,44 @@ impl ExecutionResult { } } -impl From for TypesExecutionResult { +/// A versioned execution result and the messages produced by that execution. +pub struct ExecutionResultAndMessages { + /// Execution result + pub execution_result: TypesExecutionResult, + /// Messages emitted during execution + pub messages: Messages, +} + +impl From for ExecutionResultAndMessages { fn from(execution_result: ExecutionResult) -> Self { match execution_result { ExecutionResult::Success { transfers, cost, effects, - } => TypesExecutionResult::Success { - effects, - transfers, - cost: cost.value(), + messages, + } => ExecutionResultAndMessages { + execution_result: TypesExecutionResult::Success { + effects, + transfers, + cost: cost.value(), + }, + messages, }, ExecutionResult::Failure { error, transfers, cost, effects, - } => TypesExecutionResult::Failure { - effects, - transfers, - cost: cost.value(), - error_message: error.to_string(), + messages, + } => ExecutionResultAndMessages { + execution_result: TypesExecutionResult::Failure { + effects, + transfers, + cost: cost.value(), + error_message: error.to_string(), + }, + messages, }, } } @@ -422,9 +463,11 @@ impl ExecutionResultBuilder { let mut transfers = self.transfers(); let cost = self.total_cost(); - let mut all_effects = match self.payment_execution_result { + let (mut all_effects, mut all_messages) = match self.payment_execution_result { Some(result @ ExecutionResult::Failure { .. }) => return Ok(result), - Some(ExecutionResult::Success { effects, .. }) => effects, + Some(ExecutionResult::Success { + effects, messages, .. + }) => (effects, messages), None => return Err(ExecutionResultBuilderError::MissingPaymentExecutionResult), }; @@ -436,11 +479,18 @@ impl ExecutionResultBuilder { transfers: session_transfers, effects: _, cost: _, + messages, }) => { error = Some(session_error); transfers = session_transfers; + all_messages.extend(messages); + } + Some(ExecutionResult::Success { + effects, messages, .. + }) => { + all_effects.append(effects); + all_messages.extend(messages); } - Some(ExecutionResult::Success { effects, .. }) => all_effects.append(effects), None => return Err(ExecutionResultBuilderError::MissingSessionExecutionResult), }; @@ -451,7 +501,12 @@ impl ExecutionResultBuilder { error::Error::Finalization, )); } - Some(ExecutionResult::Success { effects, .. }) => all_effects.append(effects), + Some(ExecutionResult::Success { + effects, messages, .. + }) => { + all_effects.append(effects); + all_messages.extend(messages); + } None => return Err(ExecutionResultBuilderError::MissingFinalizeExecutionResult), } @@ -460,12 +515,14 @@ impl ExecutionResultBuilder { transfers, cost, effects: all_effects, + messages: all_messages, }), Some(error) => Ok(ExecutionResult::Failure { error, transfers, cost, effects: all_effects, + messages: all_messages, }), } } diff --git a/execution_engine/src/engine_state/genesis.rs b/execution_engine/src/engine_state/genesis.rs index 6bbc6841ab..db595510ad 100644 --- a/execution_engine/src/engine_state/genesis.rs +++ b/execution_engine/src/engine_state/genesis.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; use casper_storage::global_state::state::StateProvider; use casper_types::{ - addressable_entity::{ActionThresholds, NamedKeys}, + addressable_entity::{ActionThresholds, MessageTopics, NamedKeys}, execution::Effects, package::{EntityVersions, Groups, PackageKind, PackageStatus}, system::{ @@ -1152,6 +1152,7 @@ where main_purse, associated_keys, ActionThresholds::default(), + MessageTopics::default(), ); let access_key = self diff --git a/execution_engine/src/engine_state/mod.rs b/execution_engine/src/engine_state/mod.rs index 0e313e7226..bcccf294c6 100644 --- a/execution_engine/src/engine_state/mod.rs +++ b/execution_engine/src/engine_state/mod.rs @@ -47,7 +47,7 @@ use casper_storage::{ use casper_types::{ account::{Account, AccountHash}, - addressable_entity::{AssociatedKeys, NamedKeys}, + addressable_entity::{AssociatedKeys, MessageTopics, NamedKeys}, bytesrepr::ToBytes, execution::Effects, package::{EntityVersions, Groups, PackageKind, PackageKindTag, PackageStatus}, @@ -822,6 +822,7 @@ where account.main_purse(), associated_keys, account.action_thresholds().clone().into(), + MessageTopics::default(), ); let access_key = generator.new_uref(AccessRights::READ_ADD_WRITE); @@ -2630,11 +2631,13 @@ fn log_execution_result(preamble: &'static str, result: &ExecutionResult) { transfers, cost, effects, + messages, } => { debug!( %cost, transfer_count = %transfers.len(), transforms_count = %effects.len(), + messages_count = %messages.len(), "{}: execution success", preamble ); @@ -2644,12 +2647,14 @@ fn log_execution_result(preamble: &'static str, result: &ExecutionResult) { transfers, cost, effects, + messages, } => { debug!( %error, %cost, transfer_count = %transfers.len(), transforms_count = %effects.len(), + messages_count = %messages.len(), "{}: execution failure", preamble ); @@ -2664,6 +2669,7 @@ fn should_charge_for_errors_in_wasm(execution_result: &ExecutionResult) -> bool transfers: _, cost: _, effects: _, + messages: _, } => match error { Error::Exec(err) => match err { ExecError::WasmPreprocessing(_) | ExecError::UnsupportedWasmStart => true, @@ -2718,7 +2724,9 @@ fn should_charge_for_errors_in_wasm(execution_result: &ExecutionResult) -> bool | ExecError::UnexpectedKeyVariant(_) | ExecError::InvalidPackageKind(_) | ExecError::Transform(_) - | ExecError::InvalidEntryPointType => false, + | ExecError::InvalidEntryPointType + | ExecError::InvalidMessageTopicOperation + | ExecError::InvalidUtf8Encoding(_) => false, ExecError::DisabledUnrestrictedTransfers => false, }, Error::WasmPreprocessing(_) => true, diff --git a/execution_engine/src/engine_state/upgrade.rs b/execution_engine/src/engine_state/upgrade.rs index e63d037483..b4725fdbed 100644 --- a/execution_engine/src/engine_state/upgrade.rs +++ b/execution_engine/src/engine_state/upgrade.rs @@ -5,7 +5,7 @@ use thiserror::Error; use casper_storage::global_state::state::StateProvider; use casper_types::{ - addressable_entity::{ActionThresholds, AssociatedKeys, NamedKeys, Weight}, + addressable_entity::{ActionThresholds, AssociatedKeys, MessageTopics, NamedKeys, Weight}, bytesrepr::{self, ToBytes}, execution::Effects, package::{EntityVersions, Groups, PackageKind, PackageKindTag, PackageStatus}, @@ -151,6 +151,7 @@ where URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), ); let byte_code_key = @@ -297,6 +298,7 @@ where main_purse, associated_keys, ActionThresholds::default(), + MessageTopics::default(), ); let access_key = address_generator.new_uref(AccessRights::READ_ADD_WRITE); diff --git a/execution_engine/src/execution/error.rs b/execution_engine/src/execution/error.rs index c4fe45e367..38a540de20 100644 --- a/execution_engine/src/execution/error.rs +++ b/execution_engine/src/execution/error.rs @@ -1,4 +1,6 @@ //! Execution error and supporting code. +use std::str::Utf8Error; + use casper_storage::global_state; use parity_wasm::elements; use thiserror::Error; @@ -197,6 +199,12 @@ pub enum Error { /// The EntryPoints contains an invalid entry. #[error("The EntryPoints contains an invalid entry")] InvalidEntryPointType, + /// Invalid message topic operation. + #[error("The requested operation is invalid for a message topic")] + InvalidMessageTopicOperation, + /// Invalid string encoding. + #[error("Invalid UTF-8 string encoding: {0}")] + InvalidUtf8Encoding(Utf8Error), } impl From for Error { diff --git a/execution_engine/src/execution/executor.rs b/execution_engine/src/execution/executor.rs index e1a2fd97ff..99ca076aa6 100644 --- a/execution_engine/src/execution/executor.rs +++ b/execution_engine/src/execution/executor.rs @@ -126,12 +126,14 @@ impl Executor { effects: runtime.context().effects(), transfers: runtime.context().transfers().to_owned(), cost: runtime.context().gas_counter(), + messages: runtime.context().messages(), }, Err(error) => ExecutionResult::Failure { error: error.into(), effects: runtime.context().effects(), transfers: runtime.context().transfers().to_owned(), cost: runtime.context().gas_counter(), + messages: runtime.context().messages(), }, } } @@ -177,6 +179,7 @@ impl Executor { // Snapshot of effects before execution, so in case of error only nonce update // can be returned. let effects = tracking_copy.borrow().effects(); + let messages = tracking_copy.borrow().messages(); let entry_point_name = direct_system_contract_call.entry_point_name(); @@ -250,6 +253,7 @@ impl Executor { effects: runtime.context().effects(), transfers: runtime.context().transfers().to_owned(), cost: runtime.context().gas_counter(), + messages: runtime.context().messages(), } .take_with_ret(ret), Err(error) => ExecutionResult::Failure { @@ -257,6 +261,7 @@ impl Executor { error: Error::CLValue(error).into(), transfers: runtime.context().transfers().to_owned(), cost: runtime.context().gas_counter(), + messages, } .take_without_ret(), }, @@ -265,6 +270,7 @@ impl Executor { error: error.into(), transfers: runtime.context().transfers().to_owned(), cost: runtime.context().gas_counter(), + messages, } .take_without_ret(), } diff --git a/execution_engine/src/resolvers/v1_function_index.rs b/execution_engine/src/resolvers/v1_function_index.rs index d6ff7be557..47a5522ec1 100644 --- a/execution_engine/src/resolvers/v1_function_index.rs +++ b/execution_engine/src/resolvers/v1_function_index.rs @@ -61,6 +61,8 @@ pub(crate) enum FunctionIndex { DictionaryReadFuncIndex, EnableContractVersion, AddSessionVersion, + ManageMessageTopic, + EmitMessage, } impl From for usize { diff --git a/execution_engine/src/resolvers/v1_resolver.rs b/execution_engine/src/resolvers/v1_resolver.rs index 31d49b4987..f9591d5647 100644 --- a/execution_engine/src/resolvers/v1_resolver.rs +++ b/execution_engine/src/resolvers/v1_resolver.rs @@ -249,6 +249,14 @@ impl ModuleImportResolver for RuntimeModuleImportResolver { Signature::new(&[ValueType::I32; 2][..], Some(ValueType::I32)), FunctionIndex::AddSessionVersion.into(), ), + "casper_manage_message_topic" => FuncInstance::alloc_host( + Signature::new(&[ValueType::I32; 4][..], Some(ValueType::I32)), + FunctionIndex::ManageMessageTopic.into(), + ), + "casper_emit_message" => FuncInstance::alloc_host( + Signature::new(&[ValueType::I32; 4][..], Some(ValueType::I32)), + FunctionIndex::EmitMessage.into(), + ), _ => { return Err(InterpreterError::Function(format!( "host module doesn't export function with name {}", diff --git a/execution_engine/src/runtime/externals.rs b/execution_engine/src/runtime/externals.rs index 544a3bee37..40474be14c 100644 --- a/execution_engine/src/runtime/externals.rs +++ b/execution_engine/src/runtime/externals.rs @@ -8,6 +8,7 @@ use casper_types::{ addressable_entity::{EntryPoints, NamedKeys}, api_error, bytesrepr::{self, ToBytes}, + contract_messages::MessageTopicOperation, crypto, package::{PackageKind, PackageStatus}, system::auction::EraInfo, @@ -1124,6 +1125,107 @@ where Ok(Some(RuntimeValue::I32(api_error::i32_from(result)))) } + FunctionIndex::ManageMessageTopic => { + // args(0) = pointer to the serialized topic name string in wasm memory + // args(1) = size of the serialized topic name string in wasm memory + // args(2) = pointer to the operation to be performed for the specified topic + // args(3) = size of the operation + let (topic_name_ptr, topic_name_size, operation_ptr, operation_size) = + Args::parse(args)?; + self.charge_host_function_call( + &host_function_costs.manage_message_topic, + [ + topic_name_ptr, + topic_name_size, + operation_ptr, + operation_size, + ], + )?; + + let limits = self.context.engine_config().wasm_config().messages_limits(); + + if topic_name_size > limits.max_topic_name_size() { + return Ok(Some(RuntimeValue::I32(api_error::i32_from(Err( + ApiError::MaxTopicNameSizeExceeded, + ))))); + } + + let topic_name_bytes = + self.bytes_from_mem(topic_name_ptr, topic_name_size as usize)?; + let topic_name = std::str::from_utf8(&topic_name_bytes) + .map_err(|e| Trap::from(Error::InvalidUtf8Encoding(e)))?; + + if operation_size as usize > MessageTopicOperation::max_serialized_len() { + return Err(Trap::from(Error::InvalidMessageTopicOperation)); + } + let topic_operation = self + .t_from_mem(operation_ptr, operation_size) + .map_err(|_e| Trap::from(Error::InvalidMessageTopicOperation))?; + + // only allow managing messages from stored contracts + if !self.context.get_entity_key().is_smart_contract_key() { + return Err(Trap::from(Error::InvalidContext)); + } + + let result = match topic_operation { + MessageTopicOperation::Add => { + self.add_message_topic(topic_name).map_err(Trap::from)? + } + }; + + Ok(Some(RuntimeValue::I32(api_error::i32_from(result)))) + } + FunctionIndex::EmitMessage => { + // args(0) = pointer to the serialized topic name string in wasm memory + // args(1) = size of the serialized name string in wasm memory + // args(2) = pointer to the serialized message payload in wasm memory + // args(3) = size of the serialized message payload in wasm memory + let (topic_name_ptr, topic_name_size, message_ptr, message_size) = + Args::parse(args)?; + + // Charge for the call to emit message. This increases for every message emitted + // within an execution so we're not using the static value from the wasm config. + self.context + .charge_gas(Gas::new(self.context.emit_message_cost()))?; + // Charge for parameter weights. + self.charge_host_function_call( + &HostFunction::new(0, host_function_costs.emit_message.arguments()), + &[topic_name_ptr, topic_name_size, message_ptr, message_size], + )?; + + let limits = self.context.engine_config().wasm_config().messages_limits(); + + if topic_name_size > limits.max_topic_name_size() { + return Ok(Some(RuntimeValue::I32(api_error::i32_from(Err( + ApiError::MaxTopicNameSizeExceeded, + ))))); + } + + if message_size > limits.max_message_size() { + return Ok(Some(RuntimeValue::I32(api_error::i32_from(Err( + ApiError::MessageTooLarge, + ))))); + } + + let topic_name_bytes = + self.bytes_from_mem(topic_name_ptr, topic_name_size as usize)?; + let topic_name = std::str::from_utf8(&topic_name_bytes) + .map_err(|e| Trap::from(Error::InvalidUtf8Encoding(e)))?; + + let message = self.t_from_mem(message_ptr, message_size)?; + + let result = self.emit_message(topic_name, message)?; + if result.is_ok() { + // Increase the cost for the next call to emit a message. + let new_cost = self + .context + .emit_message_cost() + .checked_add(host_function_costs.cost_increase_per_message.into()) + .ok_or(Error::GasLimit)?; + self.context.set_emit_message_cost(new_cost); + } + Ok(Some(RuntimeValue::I32(api_error::i32_from(result)))) + } } } } diff --git a/execution_engine/src/runtime/mod.rs b/execution_engine/src/runtime/mod.rs index a85db2acaf..cd0ae2fc0a 100644 --- a/execution_engine/src/runtime/mod.rs +++ b/execution_engine/src/runtime/mod.rs @@ -24,13 +24,17 @@ use casper_storage::global_state::state::StateReader; use casper_types::{ account::{Account, AccountHash}, addressable_entity::{ - self, ActionThresholds, ActionType, AddKeyFailure, AddressableEntity, - AddressableEntityHash, AssociatedKeys, EntryPoint, EntryPointAccess, EntryPointType, - EntryPoints, NamedKeys, Parameter, RemoveKeyFailure, SetThresholdFailure, UpdateKeyFailure, - Weight, DEFAULT_ENTRY_POINT_NAME, + self, ActionThresholds, ActionType, AddKeyFailure, AddressableEntity, AssociatedKeys, + EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, MessageTopics, NamedKeys, + Parameter, RemoveKeyFailure, SetThresholdFailure, UpdateKeyFailure, Weight, + DEFAULT_ENTRY_POINT_NAME, }, bytesrepr::{self, Bytes, FromBytes, ToBytes}, + contract_messages::{ + Message, MessageAddr, MessageChecksum, MessagePayload, MessageTopicSummary, + }, contracts::ContractPackage, + crypto, package::{PackageKind, PackageKindTag, PackageStatus}, system::{ self, @@ -38,11 +42,11 @@ use casper_types::{ handle_payment, mint, CallStackElement, SystemEntityType, AUCTION, HANDLE_PAYMENT, MINT, STANDARD_PAYMENT, }, - AccessRights, ApiError, ByteCode, ByteCodeHash, ByteCodeKind, CLTyped, CLValue, - ContextAccessRights, ContractWasm, DeployHash, EntityVersion, EntityVersionKey, EntityVersions, - Gas, GrantedAccess, Group, Groups, HostFunction, HostFunctionCost, Key, NamedArg, Package, - PackageHash, Phase, PublicKey, RuntimeArgs, StoredValue, Tagged, Transfer, TransferResult, - TransferredTo, URef, DICTIONARY_ITEM_KEY_MAX_LENGTH, U512, + AccessRights, AddressableEntityHash, ApiError, ByteCode, ByteCodeHash, ByteCodeKind, CLTyped, + CLValue, ContextAccessRights, ContractWasm, DeployHash, EntityVersion, EntityVersionKey, + EntityVersions, Gas, GrantedAccess, Group, Groups, HostFunction, HostFunctionCost, Key, + NamedArg, Package, PackageHash, Phase, PublicKey, RuntimeArgs, StoredValue, Tagged, Transfer, + TransferResult, TransferredTo, URef, DICTIONARY_ITEM_KEY_MAX_LENGTH, U512, }; use crate::{ @@ -1414,8 +1418,10 @@ where let result = instance.invoke_export(entry_point.name(), &[], runtime); // The `runtime`'s context was initialized with our counter from before the call and any gas // charged by the sub-call was added to its counter - so let's copy the correct value of the - // counter from there to our counter. + // counter from there to our counter. Do the same for the message cost tracking. self.context.set_gas_counter(runtime.context.gas_counter()); + self.context + .set_emit_message_cost(runtime.context.emit_message_cost()); let transfers = self.context.transfers_mut(); *transfers = runtime.context.transfers().to_owned(); @@ -1752,7 +1758,7 @@ where return Err(Error::LockedEntity(package_hash)); } - let (main_purse, previous_named_keys, action_thresholds, associated_keys) = + let (main_purse, previous_named_keys, action_thresholds, associated_keys, message_topics) = self.new_version_entity_parts(&package)?; // TODO: EE-1032 - Implement different ways of carrying on existing named keys. @@ -1788,12 +1794,22 @@ where main_purse, associated_keys, action_thresholds, + message_topics.clone(), ); self.context.metered_write_gs_unsafe(entity_key, entity)?; self.context .metered_write_gs_unsafe(package_hash, package)?; + for (_, topic_hash) in message_topics.iter() { + let topic_key = Key::message_topic(entity_hash.into(), *topic_hash); + let summary = StoredValue::MessageTopic(MessageTopicSummary::new( + 0, + self.context.get_blocktime(), + )); + self.context.metered_write_gs_unsafe(topic_key, summary)?; + } + // set return values to buffer { let hash_bytes = match entity_hash.to_bytes() { @@ -1820,7 +1836,16 @@ where fn new_version_entity_parts( &mut self, package: &Package, - ) -> Result<(URef, NamedKeys, ActionThresholds, AssociatedKeys), Error> { + ) -> Result< + ( + URef, + NamedKeys, + ActionThresholds, + AssociatedKeys, + MessageTopics, + ), + Error, + > { if let Some(previous_entity_key) = package.previous_entity_key() { let (mut previous_entity, requires_purse_creation) = self.context.get_contract_entity(previous_entity_key)?; @@ -1854,6 +1879,8 @@ where let associated_keys = previous_entity.associated_keys().clone(); + let previous_message_topics = previous_entity.message_topics().clone(); + let previous_named_keys = previous_entity.take_named_keys(); return Ok(( @@ -1861,6 +1888,7 @@ where previous_named_keys, action_thresholds, associated_keys, + previous_message_topics, )); } @@ -1869,6 +1897,7 @@ where NamedKeys::new(), ActionThresholds::default(), AssociatedKeys::new(self.context.get_caller(), Weight::new(1)), + MessageTopics::default(), )) } @@ -2402,6 +2431,7 @@ where let associated_keys = AssociatedKeys::new(target, Weight::new(1)); let named_keys = NamedKeys::new(); let entry_points = EntryPoints::new(); + let message_topics = MessageTopics::default(); let entity = AddressableEntity::new( package_hash, @@ -2412,6 +2442,7 @@ where main_purse, associated_keys, ActionThresholds::default(), + message_topics, ); let access_key = self.context.new_unit_uref()?; @@ -3289,6 +3320,7 @@ where entity_main_purse, associated_keys, ActionThresholds::default(), + MessageTopics::default(), ); let previous_wasm = self.context.read_gs_typed::(&Key::Hash( @@ -3313,4 +3345,69 @@ where None => Err(Error::InvalidEntity(contract_hash)), } } + + fn add_message_topic(&mut self, topic_name: &str) -> Result, Error> { + let topic_hash = crypto::blake2b(topic_name).into(); + + self.context + .add_message_topic(topic_name, topic_hash) + .map(|ret| ret.map_err(ApiError::from)) + } + + fn emit_message( + &mut self, + topic_name: &str, + message: MessagePayload, + ) -> Result, Trap> { + let entity_addr = self + .context + .get_entity_key() + .into_entity_hash() + .ok_or(Error::InvalidContext)?; + + let topic_name_hash = crypto::blake2b(topic_name).into(); + let topic_key = Key::Message(MessageAddr::new_topic_addr(entity_addr, topic_name_hash)); + + // Check if the topic exists and get the summary. + let Some(StoredValue::MessageTopic(prev_topic_summary)) = + self.context.read_gs(&topic_key)? else { + return Ok(Err(ApiError::MessageTopicNotRegistered)); + }; + + let current_blocktime = self.context.get_blocktime(); + let message_index = if prev_topic_summary.blocktime() != current_blocktime { + for index in 1..prev_topic_summary.message_count() { + self.context + .prune_gs_unsafe(Key::message(entity_addr, topic_name_hash, index)); + } + 0 + } else { + prev_topic_summary.message_count() + }; + + let Some(message_count) = message_index.checked_add(1) else { + return Ok(Err(ApiError::MessageTopicFull)); + }; + let new_topic_summary = MessageTopicSummary::new(message_count, current_blocktime); + + let message_key = Key::message(entity_addr, topic_name_hash, message_index); + let message_checksum = MessageChecksum(crypto::blake2b( + message.to_bytes().map_err(Error::BytesRepr)?, + )); + + self.context.metered_emit_message( + topic_key, + new_topic_summary, + message_key, + message_checksum, + Message::new( + entity_addr, + message, + topic_name.to_string(), + topic_name_hash, + message_index, + ), + )?; + Ok(Ok(())) + } } diff --git a/execution_engine/src/runtime_context/mod.rs b/execution_engine/src/runtime_context/mod.rs index b5458180b7..a020e468df 100644 --- a/execution_engine/src/runtime_context/mod.rs +++ b/execution_engine/src/runtime_context/mod.rs @@ -18,10 +18,13 @@ use casper_storage::global_state::state::StateReader; use casper_types::{ account::{Account, AccountHash}, addressable_entity::{ - ActionType, AddKeyFailure, NamedKeys, RemoveKeyFailure, SetThresholdFailure, - UpdateKeyFailure, Weight, + ActionType, AddKeyFailure, MessageTopicError, NamedKeys, RemoveKeyFailure, + SetThresholdFailure, UpdateKeyFailure, Weight, }, bytesrepr::ToBytes, + contract_messages::{ + Message, MessageAddr, MessageChecksum, MessageTopicSummary, Messages, TopicNameHash, + }, execution::Effects, package::{PackageKind, PackageKindTag}, system::auction::EraInfo, @@ -69,6 +72,7 @@ pub struct RuntimeContext<'a, R> { entity_key: Key, package_kind: PackageKind, account_hash: AccountHash, + emit_message_cost: U512, } impl<'a, R> RuntimeContext<'a, R> @@ -102,6 +106,12 @@ where remaining_spending_limit: U512, entry_point_type: EntryPointType, ) -> Self { + let emit_message_cost = engine_config + .wasm_config() + .take_host_function_costs() + .emit_message + .cost() + .into(); RuntimeContext { tracking_copy, entry_point_type, @@ -123,6 +133,7 @@ where transfers, remaining_spending_limit, package_kind, + emit_message_cost, } } @@ -177,6 +188,7 @@ where transfers, remaining_spending_limit, package_kind, + emit_message_cost: self.emit_message_cost, } } @@ -561,6 +573,21 @@ where self.tracking_copy.borrow().effects() } + /// Returns a copy of the current messages of a tracking copy. + pub fn messages(&self) -> Messages { + self.tracking_copy.borrow().messages() + } + + /// Returns the cost charged for the last emitted message. + pub fn emit_message_cost(&self) -> U512 { + self.emit_message_cost + } + + /// Sets the cost charged for the last emitted message. + pub fn set_emit_message_cost(&mut self, cost: U512) { + self.emit_message_cost = cost + } + /// Returns list of transfers. pub fn transfers(&self) -> &Vec { &self.transfers @@ -631,6 +658,8 @@ where StoredValue::Unbonding(_) => Ok(()), StoredValue::ContractPackage(_) => Ok(()), StoredValue::ContractWasm(_) => Ok(()), + StoredValue::MessageTopic(_) => Ok(()), + StoredValue::Message(_) => Ok(()), } } @@ -712,7 +741,8 @@ where | Key::BidAddr(_) | Key::Package(_) | Key::AddressableEntity(..) - | Key::ByteCode(..) => true, + | Key::ByteCode(..) + | Key::Message(_) => true, } } @@ -742,7 +772,8 @@ where | Key::ChecksumRegistry | Key::BidAddr(_) | Key::Package(_) - | Key::ByteCode(..) => false, + | Key::ByteCode(..) + | Key::Message(_) => false, } } @@ -767,7 +798,8 @@ where | Key::BidAddr(_) | Key::Package(_) | Key::AddressableEntity(..) - | Key::ByteCode(..) => false, + | Key::ByteCode(..) + | Key::Message(_) => false, } } @@ -867,6 +899,32 @@ where Ok(()) } + /// Emits message and writes message summary to global state with a measurement. + pub(crate) fn metered_emit_message( + &mut self, + topic_key: Key, + topic_value: MessageTopicSummary, + message_key: Key, + message_value: MessageChecksum, + message: Message, + ) -> Result<(), Error> { + let topic_value = StoredValue::MessageTopic(topic_value); + let message_value = StoredValue::Message(message_value); + + // Charge for amount as measured by serialized length + let bytes_count = topic_value.serialized_length() + message_value.serialized_length(); + self.charge_gas_storage(bytes_count)?; + + self.tracking_copy.borrow_mut().emit_message( + topic_key, + topic_value, + message_key, + message_value, + message, + ); + Ok(()) + } + /// Writes data to a global state and charges for bytes stored. /// /// This method performs full validation of the key to be written. @@ -1296,4 +1354,44 @@ where pub(crate) fn set_remaining_spending_limit(&mut self, amount: U512) { self.remaining_spending_limit = amount; } + + /// Adds new message topic. + pub(crate) fn add_message_topic( + &mut self, + topic_name: &str, + topic_name_hash: TopicNameHash, + ) -> Result, Error> { + let entity_key: Key = self.get_entity_key(); + let entity_addr = entity_key.into_entity_hash().ok_or(Error::InvalidContext)?; + + // Take the addressable entity out of the global state + let entity = { + let mut entity: AddressableEntity = self.read_gs_typed(&entity_key)?; + + let max_topics_per_contract = self + .engine_config + .wasm_config() + .messages_limits() + .max_topics_per_contract(); + + if entity.message_topics().len() >= max_topics_per_contract as usize { + return Ok(Err(MessageTopicError::MaxTopicsExceeded)); + } + + if let Err(e) = entity.add_message_topic(topic_name, topic_name_hash) { + return Ok(Err(e)); + } + entity + }; + + let topic_key = Key::Message(MessageAddr::new_topic_addr(entity_addr, topic_name_hash)); + let summary = StoredValue::MessageTopic(MessageTopicSummary::new(0, self.get_blocktime())); + + let entity_value = self.addressable_entity_to_validated_value(entity)?; + + self.metered_write_gs_unsafe(entity_key, entity_value)?; + self.metered_write_gs_unsafe(topic_key, summary)?; + + Ok(Ok(())) + } } diff --git a/execution_engine/src/runtime_context/tests.rs b/execution_engine/src/runtime_context/tests.rs index a301792a07..c6889d8806 100644 --- a/execution_engine/src/runtime_context/tests.rs +++ b/execution_engine/src/runtime_context/tests.rs @@ -13,8 +13,8 @@ use casper_storage::global_state::state::{self, lmdb::LmdbGlobalStateView, State use casper_types::{ account::{AccountHash, ACCOUNT_HASH_LENGTH}, addressable_entity::{ - ActionThresholds, ActionType, AddKeyFailure, AssociatedKeys, NamedKeys, RemoveKeyFailure, - SetThresholdFailure, Weight, + ActionThresholds, ActionType, AddKeyFailure, AssociatedKeys, MessageTopics, NamedKeys, + RemoveKeyFailure, SetThresholdFailure, Weight, }, bytesrepr::ToBytes, execution::TransformKind, @@ -84,6 +84,7 @@ fn new_addressable_entity_with_purse( URef::new(purse, AccessRights::READ_ADD_WRITE), associated_keys, Default::default(), + MessageTopics::default(), ); let account_key = Key::Account(account_hash); let contract_key = Key::addressable_entity_key(package_kind.tag(), entity_hash); @@ -460,6 +461,7 @@ fn contract_key_addable_valid() { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), )); let read_contract = runtime_context.read_gs(&contract_key).unwrap(); diff --git a/execution_engine/src/tracking_copy/byte_size.rs b/execution_engine/src/tracking_copy/byte_size.rs index 39890843af..60caa03ef7 100644 --- a/execution_engine/src/tracking_copy/byte_size.rs +++ b/execution_engine/src/tracking_copy/byte_size.rs @@ -42,6 +42,10 @@ impl ByteSize for StoredValue { StoredValue::Withdraw(withdraw_purses) => withdraw_purses.serialized_length(), StoredValue::Unbonding(unbonding_purses) => unbonding_purses.serialized_length(), StoredValue::ByteCode(byte_code) => byte_code.serialized_length(), + StoredValue::MessageTopic(message_topic_summary) => { + message_topic_summary.serialized_length() + } + StoredValue::Message(message_summary) => message_summary.serialized_length(), } } } diff --git a/execution_engine/src/tracking_copy/ext.rs b/execution_engine/src/tracking_copy/ext.rs index cd4a468719..938705cc60 100644 --- a/execution_engine/src/tracking_copy/ext.rs +++ b/execution_engine/src/tracking_copy/ext.rs @@ -6,6 +6,7 @@ use std::{ use casper_storage::global_state::{state::StateReader, trie::merkle_proof::TrieMerkleProof}; use casper_types::{ account::AccountHash, + addressable_entity::MessageTopics, bytesrepr, package::{EntityVersions, Groups, PackageKind, PackageKindTag, PackageStatus}, AccessRights, AddressableEntity, AddressableEntityHash, CLValue, EntryPoints, Key, Motes, @@ -148,6 +149,7 @@ where account.main_purse(), account.associated_keys().clone().into(), account.action_thresholds().clone().into(), + MessageTopics::default(), ); let access_key = generator.new_uref(AccessRights::READ_ADD_WRITE); diff --git a/execution_engine/src/tracking_copy/mod.rs b/execution_engine/src/tracking_copy/mod.rs index 8d027bf26b..7b27926cca 100644 --- a/execution_engine/src/tracking_copy/mod.rs +++ b/execution_engine/src/tracking_copy/mod.rs @@ -19,6 +19,7 @@ use casper_storage::global_state::{state::StateReader, trie::merkle_proof::TrieM use casper_types::{ addressable_entity::NamedKeys, bytesrepr::{self}, + contract_messages::{Message, Messages}, execution::{Effects, Transform, TransformError, TransformInstruction, TransformKind}, CLType, CLValue, CLValueError, Digest, Key, KeyTag, StoredValue, StoredValueTypeMismatch, Tagged, U512, @@ -223,6 +224,7 @@ pub struct TrackingCopy { reader: R, cache: TrackingCopyCache, effects: Effects, + messages: Messages, } /// Result of executing an "add" operation on a value in the state. @@ -261,6 +263,7 @@ impl> TrackingCopy { // TODO: Should `max_cache_size` be a fraction of wasm memory limit? cache: TrackingCopyCache::new(1024 * 16, HeapSize), effects: Effects::new(), + messages: Vec::new(), } } @@ -342,6 +345,24 @@ impl> TrackingCopy { self.effects.push(transform); } + /// Caches the emitted message and writes the message topic summary under the specified key. + /// + /// This function does not check the types for the key and the value so the caller should + /// correctly set the type. The `message_topic_key` should be of the `Key::MessageTopic` + /// variant and the `message_topic_summary` should be of the `StoredValue::Message` variant. + pub fn emit_message( + &mut self, + message_topic_key: Key, + message_topic_summary: StoredValue, + message_key: Key, + message_value: StoredValue, + message: Message, + ) { + self.write(message_key, message_value); + self.write(message_topic_key, message_topic_summary); + self.messages.push(message); + } + /// Prunes a `key`. pub(crate) fn prune(&mut self, key: Key) { let normalized_key = key.normalize(); @@ -435,6 +456,11 @@ impl> TrackingCopy { self.effects.clone() } + /// Returns a copy of the messages cached by this instance. + pub fn messages(&self) -> Messages { + self.messages.clone() + } + /// Calling `query()` avoids calling into `self.cache`, so this will not return any values /// written or mutated in this `TrackingCopy` via previous calls to `write()` or `add()`, since /// these updates are only held in `self.cache`. @@ -569,6 +595,12 @@ impl> TrackingCopy { StoredValue::Unbonding(_) => { return Ok(query.into_not_found_result("UnbondingPurses value found.")); } + StoredValue::MessageTopic(_) => { + return Ok(query.into_not_found_result("MessageTopic value found.")); + } + StoredValue::Message(_) => { + return Ok(query.into_not_found_result("Message value found.")); + } } } } diff --git a/execution_engine/src/tracking_copy/tests.rs b/execution_engine/src/tracking_copy/tests.rs index 23bfee8cd9..c571a2fe8e 100644 --- a/execution_engine/src/tracking_copy/tests.rs +++ b/execution_engine/src/tracking_copy/tests.rs @@ -10,7 +10,7 @@ use casper_storage::global_state::{ use casper_types::{ account::{AccountHash, ACCOUNT_HASH_LENGTH}, addressable_entity::{ - ActionThresholds, AddressableEntityHash, AssociatedKeys, NamedKeys, Weight, + ActionThresholds, AddressableEntityHash, AssociatedKeys, MessageTopics, NamedKeys, Weight, }, execution::{Effects, Transform, TransformKind}, gens::*, @@ -198,6 +198,7 @@ fn tracking_copy_add_named_key() { URef::new([0u8; 32], AccessRights::READ_ADD_WRITE), associated_keys, Default::default(), + MessageTopics::default(), ); let db = CountingDb::new_init(StoredValue::AddressableEntity(contract)); @@ -357,6 +358,7 @@ proptest! { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), )); let contract_key = Key::Hash(hash); @@ -400,7 +402,8 @@ proptest! { ProtocolVersion::V1_0_0, purse, associated_keys, - ActionThresholds::default() + ActionThresholds::default(), + MessageTopics::default(), ); @@ -450,6 +453,7 @@ proptest! { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), )); let contract_key = Key::AddressableEntity(PackageKindTag::SmartContract,hash); @@ -553,6 +557,7 @@ fn query_for_circular_references_should_fail() { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), )); let (global_state, root_hash, _tempdir) = state::lmdb::make_temporary_global_state([ @@ -607,6 +612,7 @@ fn validate_query_proof_should_work() { fake_purse, AssociatedKeys::new(account_hash, Weight::new(1)), ActionThresholds::default(), + MessageTopics::default(), )); // create contract that refers to that account @@ -626,6 +632,7 @@ fn validate_query_proof_should_work() { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), )); let contract_key = Key::Hash([5; 32]); @@ -653,6 +660,7 @@ fn validate_query_proof_should_work() { fake_purse, AssociatedKeys::new(account_hash, Weight::new(1)), ActionThresholds::default(), + MessageTopics::default(), )); let main_account_value = StoredValue::CLValue(cl_value_2); @@ -1074,6 +1082,7 @@ fn query_with_large_depth_with_fixed_path_should_fail() { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), )); pairs.push((contract_key, contract)); contract_keys.push(contract_key); @@ -1136,6 +1145,7 @@ fn query_with_large_depth_with_urefs_should_fail() { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), )); let contract_key = Key::Hash([0; 32]); pairs.push((contract_key, contract)); diff --git a/execution_engine_testing/tests/fixtures/call_stack_fixture/global_state/data.lmdb b/execution_engine_testing/tests/fixtures/call_stack_fixture/global_state/data.lmdb index bf1a45fa0e..73ee47cfbe 100644 Binary files a/execution_engine_testing/tests/fixtures/call_stack_fixture/global_state/data.lmdb and b/execution_engine_testing/tests/fixtures/call_stack_fixture/global_state/data.lmdb differ diff --git a/execution_engine_testing/tests/fixtures/call_stack_fixture/global_state/data.lmdb-lock b/execution_engine_testing/tests/fixtures/call_stack_fixture/global_state/data.lmdb-lock index 25594bc32f..d261577952 100644 Binary files a/execution_engine_testing/tests/fixtures/call_stack_fixture/global_state/data.lmdb-lock and b/execution_engine_testing/tests/fixtures/call_stack_fixture/global_state/data.lmdb-lock differ diff --git a/execution_engine_testing/tests/fixtures/call_stack_fixture/state.json b/execution_engine_testing/tests/fixtures/call_stack_fixture/state.json index deb85db9cd..6220b3011f 100644 --- a/execution_engine_testing/tests/fixtures/call_stack_fixture/state.json +++ b/execution_engine_testing/tests/fixtures/call_stack_fixture/state.json @@ -2,5 +2,5 @@ "genesis_request": { "protocol_version": "1.0.0" }, - "post_state_hash": "6d2d9f1fadfbdc9396251fc2c9325ff7e08c70040a51bc6519d70a0dd86b7575" + "post_state_hash": "86a85a3b0cbe3a2f883c12fc901ebfc4889b1cd997aef353bcf9f4b5e889eead" } \ No newline at end of file diff --git a/execution_engine_testing/tests/fixtures/groups/global_state/data.lmdb b/execution_engine_testing/tests/fixtures/groups/global_state/data.lmdb index ca43002242..9120908c44 100644 Binary files a/execution_engine_testing/tests/fixtures/groups/global_state/data.lmdb and b/execution_engine_testing/tests/fixtures/groups/global_state/data.lmdb differ diff --git a/execution_engine_testing/tests/fixtures/groups/global_state/data.lmdb-lock b/execution_engine_testing/tests/fixtures/groups/global_state/data.lmdb-lock index 08525ff39c..241358ccb8 100644 Binary files a/execution_engine_testing/tests/fixtures/groups/global_state/data.lmdb-lock and b/execution_engine_testing/tests/fixtures/groups/global_state/data.lmdb-lock differ diff --git a/execution_engine_testing/tests/fixtures/groups/state.json b/execution_engine_testing/tests/fixtures/groups/state.json index 4e201414b8..ef25459d4e 100644 --- a/execution_engine_testing/tests/fixtures/groups/state.json +++ b/execution_engine_testing/tests/fixtures/groups/state.json @@ -2,5 +2,5 @@ "genesis_request": { "protocol_version": "1.0.0" }, - "post_state_hash": "f009952b02f61ba14c6ead8deade5f5ab1110a9442ddc7146677f1c0fadd76d4" + "post_state_hash": "ac1507f439601e71b62b258196d7d322b78356d5efd466db592c259c9225c56a" } \ No newline at end of file diff --git a/execution_engine_testing/tests/src/test/contract_messages.rs b/execution_engine_testing/tests/src/test/contract_messages.rs new file mode 100644 index 0000000000..c545362608 --- /dev/null +++ b/execution_engine_testing/tests/src/test/contract_messages.rs @@ -0,0 +1,794 @@ +use num_traits::Zero; +use std::cell::RefCell; + +use casper_engine_test_support::{ + ExecuteRequestBuilder, LmdbWasmTestBuilder, DEFAULT_ACCOUNT_ADDR, DEFAULT_BLOCK_TIME, + PRODUCTION_RUN_GENESIS_REQUEST, +}; +use casper_execution_engine::engine_state::EngineConfigBuilder; +use casper_types::{ + bytesrepr::ToBytes, + contract_messages::{MessageChecksum, MessagePayload, MessageTopicSummary, TopicNameHash}, + crypto, runtime_args, AddressableEntity, AddressableEntityHash, BlockTime, Digest, + HostFunction, HostFunctionCosts, Key, MessageLimits, OpcodeCosts, RuntimeArgs, StorageCosts, + StoredValue, WasmConfig, DEFAULT_MAX_STACK_HEIGHT, DEFAULT_WASM_MAX_MEMORY, U512, +}; + +const MESSAGE_EMITTER_INSTALLER_WASM: &str = "contract_messages_emitter.wasm"; +const MESSAGE_EMITTER_UPGRADER_WASM: &str = "contract_messages_upgrader.wasm"; +const MESSAGE_EMITTER_FROM_ACCOUNT: &str = "contract_messages_from_account.wasm"; +const MESSAGE_EMITTER_PACKAGE_HASH_KEY_NAME: &str = "messages_emitter_package_hash"; +const MESSAGE_EMITTER_GENERIC_TOPIC: &str = "generic_messages"; +const MESSAGE_EMITTER_UPGRADED_TOPIC: &str = "new_topic_after_upgrade"; +const ENTRY_POINT_EMIT_MESSAGE: &str = "emit_message"; +const ENTRY_POINT_EMIT_MULTIPLE_MESSAGES: &str = "emit_multiple_messages"; +const ENTRY_POINT_EMIT_MESSAGE_FROM_EACH_VERSION: &str = "emit_message_from_each_version"; +const ARG_NUM_MESSAGES_TO_EMIT: &str = "num_messages_to_emit"; +const ARG_TOPIC_NAME: &str = "topic_name"; +const ENTRY_POINT_ADD_TOPIC: &str = "add_topic"; +const ARG_MESSAGE_SUFFIX_NAME: &str = "message_suffix"; + +const EMITTER_MESSAGE_PREFIX: &str = "generic message: "; + +// Number of messages that will be emitted when calling `ENTRY_POINT_EMIT_MESSAGE_FROM_EACH_VERSION` +const EMIT_MESSAGE_FROM_EACH_VERSION_NUM_MESSAGES: u32 = 3; + +fn install_messages_emitter_contract( + builder: &RefCell, +) -> AddressableEntityHash { + // Request to install the contract that will be emitting messages. + let install_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MESSAGE_EMITTER_INSTALLER_WASM, + RuntimeArgs::default(), + ) + .build(); + + // Execute the request to install the message emitting contract. + // This will also register a topic for the contract to emit messages on. + builder + .borrow_mut() + .exec(install_request) + .expect_success() + .commit(); + + // Get the contract package for the messages_emitter. + let query_result = builder + .borrow_mut() + .query( + None, + Key::from(*DEFAULT_ACCOUNT_ADDR), + &[MESSAGE_EMITTER_PACKAGE_HASH_KEY_NAME.into()], + ) + .expect("should query"); + + let message_emitter_package = if let StoredValue::Package(package) = query_result { + package + } else { + panic!("Stored value is not a contract package: {:?}", query_result); + }; + + // Get the contract hash of the messages_emitter contract. + *message_emitter_package + .versions() + .contract_hashes() + .last() + .expect("Should have contract hash") +} + +fn upgrade_messages_emitter_contract( + builder: &RefCell, +) -> AddressableEntityHash { + let upgrade_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MESSAGE_EMITTER_UPGRADER_WASM, + RuntimeArgs::default(), + ) + .build(); + + // Execute the request to upgrade the message emitting contract. + // This will also register a new topic for the contract to emit messages on. + builder + .borrow_mut() + .exec(upgrade_request) + .expect_success() + .commit(); + + // Get the contract package for the upgraded messages emitter contract. + let query_result = builder + .borrow_mut() + .query( + None, + Key::from(*DEFAULT_ACCOUNT_ADDR), + &[MESSAGE_EMITTER_PACKAGE_HASH_KEY_NAME.into()], + ) + .expect("should query"); + + let message_emitter_package = if let StoredValue::Package(package) = query_result { + package + } else { + panic!("Stored value is not a contract package: {:?}", query_result); + }; + + // Get the contract hash of the latest version of the messages emitter contract. + *message_emitter_package + .versions() + .contract_hashes() + .last() + .expect("Should have contract hash") +} + +fn emit_message_with_suffix( + builder: &RefCell, + suffix: &str, + contract_hash: &AddressableEntityHash, + block_time: u64, +) { + let emit_message_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + *contract_hash, + ENTRY_POINT_EMIT_MESSAGE, + runtime_args! { + ARG_MESSAGE_SUFFIX_NAME => suffix, + }, + ) + .with_block_time(block_time) + .build(); + + builder + .borrow_mut() + .exec(emit_message_request) + .expect_success() + .commit(); +} + +struct ContractQueryView<'a> { + builder: &'a RefCell, + contract_hash: AddressableEntityHash, +} + +impl<'a> ContractQueryView<'a> { + fn new( + builder: &'a RefCell, + contract_hash: AddressableEntityHash, + ) -> Self { + Self { + builder, + contract_hash, + } + } + + fn entity(&self) -> AddressableEntity { + let query_result = self + .builder + .borrow_mut() + .query(None, Key::contract_entity_key(self.contract_hash), &[]) + .expect("should query"); + + let entity = if let StoredValue::AddressableEntity(entity) = query_result { + entity + } else { + panic!( + "Stored value is not an adressable entity: {:?}", + query_result + ); + }; + + entity + } + + fn message_topic(&self, topic_name_hash: TopicNameHash) -> MessageTopicSummary { + let query_result = self + .builder + .borrow_mut() + .query( + None, + Key::message_topic(self.contract_hash, topic_name_hash), + &[], + ) + .expect("should query"); + + match query_result { + StoredValue::MessageTopic(summary) => summary, + _ => { + panic!( + "Stored value is not a message topic summary: {:?}", + query_result + ); + } + } + } + + fn message_summary( + &self, + topic_name_hash: TopicNameHash, + message_index: u32, + state_hash: Option, + ) -> Result { + let query_result = self.builder.borrow_mut().query( + state_hash, + Key::message(self.contract_hash, topic_name_hash, message_index), + &[], + )?; + + match query_result { + StoredValue::Message(summary) => Ok(summary), + _ => panic!("Stored value is not a message summary: {:?}", query_result), + } + } +} + +#[ignore] +#[test] +fn should_emit_messages() { + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + let contract_hash = install_messages_emitter_contract(&builder); + let query_view = ContractQueryView::new(&builder, contract_hash); + let entity = query_view.entity(); + + let (topic_name, message_topic_hash) = entity + .message_topics() + .iter() + .next() + .expect("should have at least one topic"); + + assert_eq!(topic_name, &MESSAGE_EMITTER_GENERIC_TOPIC.to_string()); + // Check that the topic exists for the installed contract. + assert_eq!( + query_view + .message_topic(*message_topic_hash) + .message_count(), + 0 + ); + + // Now call the entry point to emit some messages. + emit_message_with_suffix(&builder, "test", &contract_hash, DEFAULT_BLOCK_TIME); + let expected_message = MessagePayload::from(format!("{}{}", EMITTER_MESSAGE_PREFIX, "test")); + let expected_message_hash = crypto::blake2b(expected_message.to_bytes().unwrap()); + let queried_message_summary = query_view + .message_summary(*message_topic_hash, 0, None) + .expect("should have value") + .value(); + assert_eq!(expected_message_hash, queried_message_summary); + assert_eq!( + query_view + .message_topic(*message_topic_hash) + .message_count(), + 1 + ); + + // call again to emit a new message and check that the index in the topic incremented. + emit_message_with_suffix(&builder, "test", &contract_hash, DEFAULT_BLOCK_TIME); + let queried_message_summary = query_view + .message_summary(*message_topic_hash, 1, None) + .expect("should have value") + .value(); + assert_eq!(expected_message_hash, queried_message_summary); + assert_eq!( + query_view + .message_topic(*message_topic_hash) + .message_count(), + 2 + ); + + let first_block_state_hash = builder.borrow().get_post_state_hash(); + + // call to emit a new message but in another block. + emit_message_with_suffix( + &builder, + "new block time", + &contract_hash, + DEFAULT_BLOCK_TIME + 1, + ); + let expected_message = + MessagePayload::from(format!("{}{}", EMITTER_MESSAGE_PREFIX, "new block time")); + let expected_message_hash = crypto::blake2b(expected_message.to_bytes().unwrap()); + let queried_message_summary = query_view + .message_summary(*message_topic_hash, 0, None) + .expect("should have value") + .value(); + assert_eq!(expected_message_hash, queried_message_summary); + assert_eq!( + query_view + .message_topic(*message_topic_hash) + .message_count(), + 1 + ); + + // old messages should be pruned from tip and inaccessible at the latest state hash. + assert!(query_view + .message_summary(*message_topic_hash, 1, None) + .is_err()); + + // old messages should still be discoverable at a state hash before pruning. + assert!(query_view + .message_summary(*message_topic_hash, 1, Some(first_block_state_hash)) + .is_ok()); +} + +#[ignore] +#[test] +fn should_emit_message_on_empty_topic_in_new_block() { + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + let contract_hash = install_messages_emitter_contract(&builder); + let query_view = ContractQueryView::new(&builder, contract_hash); + let entity = query_view.entity(); + + let (_, message_topic_hash) = entity + .message_topics() + .iter() + .next() + .expect("should have at least one topic"); + + assert_eq!( + query_view + .message_topic(*message_topic_hash) + .message_count(), + 0 + ); + + emit_message_with_suffix( + &builder, + "new block time", + &contract_hash, + DEFAULT_BLOCK_TIME + 1, + ); + assert_eq!( + query_view + .message_topic(*message_topic_hash) + .message_count(), + 1 + ); +} + +#[ignore] +#[test] +fn should_add_topics() { + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let contract_hash = install_messages_emitter_contract(&builder); + let query_view = ContractQueryView::new(&builder, contract_hash); + + let add_topic_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_ADD_TOPIC, + runtime_args! { + ARG_TOPIC_NAME => "topic_1", + }, + ) + .build(); + + builder + .borrow_mut() + .exec(add_topic_request) + .expect_success() + .commit(); + + let topic_1_hash = *query_view + .entity() + .message_topics() + .get("topic_1") + .expect("should have added topic `topic_1"); + assert_eq!(query_view.message_topic(topic_1_hash).message_count(), 0); + + let add_topic_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_ADD_TOPIC, + runtime_args! { + ARG_TOPIC_NAME => "topic_2", + }, + ) + .build(); + + builder + .borrow_mut() + .exec(add_topic_request) + .expect_success() + .commit(); + + let topic_2_hash = *query_view + .entity() + .message_topics() + .get("topic_2") + .expect("should have added topic `topic_2"); + + assert!(query_view + .entity() + .message_topics() + .get("topic_1") + .is_some()); + assert_eq!(query_view.message_topic(topic_1_hash).message_count(), 0); + assert_eq!(query_view.message_topic(topic_2_hash).message_count(), 0); +} + +#[ignore] +#[test] +fn should_not_add_duplicate_topics() { + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + let contract_hash = install_messages_emitter_contract(&builder); + let query_view = ContractQueryView::new(&builder, contract_hash); + + let entity = query_view.entity(); + let (first_topic_name, _) = entity + .message_topics() + .iter() + .next() + .expect("should have at least one topic"); + + let add_topic_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_ADD_TOPIC, + runtime_args! { + ARG_TOPIC_NAME => first_topic_name, + }, + ) + .build(); + + builder + .borrow_mut() + .exec(add_topic_request) + .expect_failure() + .commit(); +} + +#[ignore] +#[test] +fn should_not_exceed_configured_limits() { + let default_wasm_config = WasmConfig::default(); + let custom_engine_config = EngineConfigBuilder::default() + .with_wasm_config(WasmConfig::new( + default_wasm_config.max_memory, + default_wasm_config.max_stack_height, + default_wasm_config.opcode_costs(), + default_wasm_config.storage_costs(), + default_wasm_config.take_host_function_costs(), + MessageLimits { + max_topic_name_size: 32, + max_message_size: 100, + max_topics_per_contract: 2, + }, + )) + .build(); + + let builder = RefCell::new(LmdbWasmTestBuilder::new_temporary_with_config( + custom_engine_config, + )); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + let contract_hash = install_messages_emitter_contract(&builder); + + // if the topic larger than the limit, registering should fail. + // string is 33 bytes > limit established above + let too_large_topic_name = std::str::from_utf8(&[0x4du8; 33]).unwrap(); + let add_topic_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_ADD_TOPIC, + runtime_args! { + ARG_TOPIC_NAME => too_large_topic_name, + }, + ) + .build(); + + builder + .borrow_mut() + .exec(add_topic_request) + .expect_failure() + .commit(); + + // if the topic name is equal to the limit, registering should work. + // string is 32 bytes == limit established above + let topic_name_at_limit = std::str::from_utf8(&[0x4du8; 32]).unwrap(); + let add_topic_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_ADD_TOPIC, + runtime_args! { + ARG_TOPIC_NAME => topic_name_at_limit, + }, + ) + .build(); + + builder + .borrow_mut() + .exec(add_topic_request) + .expect_success() + .commit(); + + // Check that the max number of topics limit is enforced. + // 2 topics are already registered, so registering another topic should + // fail since the limit is already reached. + let add_topic_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_ADD_TOPIC, + runtime_args! { + ARG_TOPIC_NAME => "topic_1", + }, + ) + .build(); + + builder + .borrow_mut() + .exec(add_topic_request) + .expect_failure() + .commit(); + + // Check message size limit + let large_message = std::str::from_utf8(&[0x4du8; 128]).unwrap(); + let emit_message_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_EMIT_MESSAGE, + runtime_args! { + ARG_MESSAGE_SUFFIX_NAME => large_message, + }, + ) + .build(); + + builder + .borrow_mut() + .exec(emit_message_request) + .expect_failure() + .commit(); +} + +#[ignore] +#[test] +fn should_carry_message_topics_on_upgraded_contract() { + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + let _ = install_messages_emitter_contract(&builder); + let contract_hash = upgrade_messages_emitter_contract(&builder); + let query_view = ContractQueryView::new(&builder, contract_hash); + + let entity = query_view.entity(); + assert_eq!(entity.message_topics().len(), 2); + let mut expected_topic_names = 0; + for (topic_name, topic_hash) in entity.message_topics().iter() { + if topic_name == MESSAGE_EMITTER_GENERIC_TOPIC + || topic_name == MESSAGE_EMITTER_UPGRADED_TOPIC + { + expected_topic_names += 1; + } + + assert_eq!(query_view.message_topic(*topic_hash).message_count(), 0); + } + assert_eq!(expected_topic_names, 2); +} + +#[ignore] +#[test] +fn should_not_emit_messages_from_account() { + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + // Request to run a deploy that tries to register a message topic without a stored contract. + let install_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MESSAGE_EMITTER_FROM_ACCOUNT, + RuntimeArgs::default(), + ) + .build(); + + // Expect to fail since topics can only be registered by stored contracts. + builder + .borrow_mut() + .exec(install_request) + .expect_failure() + .commit(); +} + +#[ignore] +#[test] +fn should_charge_expected_gas_for_storage() { + const GAS_PER_BYTE_COST: u32 = 100; + + let wasm_config = WasmConfig::new( + DEFAULT_WASM_MAX_MEMORY, + DEFAULT_MAX_STACK_HEIGHT, + OpcodeCosts::zero(), + StorageCosts::new(GAS_PER_BYTE_COST), + HostFunctionCosts::zero(), + MessageLimits::default(), + ); + let custom_engine_config = EngineConfigBuilder::default() + .with_wasm_config(wasm_config) + .build(); + + let builder = RefCell::new(LmdbWasmTestBuilder::new_temporary_with_config( + custom_engine_config, + )); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + let contract_hash = install_messages_emitter_contract(&builder); + let query_view = ContractQueryView::new(&builder, contract_hash); + + // check the cost of adding a new topic + let add_topic_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_ADD_TOPIC, + runtime_args! { + ARG_TOPIC_NAME => "cost_topic", + }, + ) + .build(); + + builder + .borrow_mut() + .exec(add_topic_request) + .expect_success() + .commit(); + + let add_topic_cost = builder.borrow().last_exec_gas_cost().value(); + + // cost depends on the entity size since we store the topic names in the entity record. + let entity = query_view.entity(); + let default_topic_summary = MessageTopicSummary::new(0, BlockTime::new(0)); + let written_size_expected = StoredValue::MessageTopic(default_topic_summary.clone()) + .serialized_length() + + StoredValue::AddressableEntity(entity).serialized_length(); + assert_eq!( + U512::from(written_size_expected * GAS_PER_BYTE_COST as usize), + add_topic_cost + ); + + // check that the storage cost charged is invariable with message size that is emitted. + let written_size_expected = StoredValue::Message(MessageChecksum([0; 32])).serialized_length() + + StoredValue::MessageTopic(default_topic_summary).serialized_length(); + + emit_message_with_suffix(&builder, "test", &contract_hash, DEFAULT_BLOCK_TIME); + let emit_message_gas_cost = builder.borrow().last_exec_gas_cost().value(); + assert_eq!( + U512::from(written_size_expected * GAS_PER_BYTE_COST as usize), + emit_message_gas_cost + ); + + emit_message_with_suffix(&builder, "test 12345", &contract_hash, DEFAULT_BLOCK_TIME); + let emit_message_gas_cost = builder.borrow().last_exec_gas_cost().value(); + assert_eq!( + U512::from(written_size_expected * GAS_PER_BYTE_COST as usize), + emit_message_gas_cost + ); + + // emitting messages in a different block will also prune the old entries so check the cost. + emit_message_with_suffix( + &builder, + "message in different block", + &contract_hash, + DEFAULT_BLOCK_TIME + 1, + ); + let emit_message_gas_cost = builder.borrow().last_exec_gas_cost().value(); + assert_eq!( + U512::from(written_size_expected * GAS_PER_BYTE_COST as usize), + emit_message_gas_cost + ); +} + +#[ignore] +#[test] +fn should_charge_increasing_gas_cost_for_multiple_messages_emitted() { + const FIRST_MESSAGE_EMIT_COST: u32 = 100; + const COST_INCREASE_PER_MESSAGE: u32 = 50; + const fn emit_cost_per_execution(num_messages: u32) -> u32 { + FIRST_MESSAGE_EMIT_COST * num_messages + + (num_messages - 1) * num_messages / 2 * COST_INCREASE_PER_MESSAGE + } + + const MESSAGES_TO_EMIT: u32 = 4; + const EMIT_MULTIPLE_EXPECTED_COST: u32 = emit_cost_per_execution(MESSAGES_TO_EMIT); + const EMIT_MESSAGES_FROM_MULTIPLE_CONTRACTS: u32 = + emit_cost_per_execution(EMIT_MESSAGE_FROM_EACH_VERSION_NUM_MESSAGES); + + let wasm_config = WasmConfig::new( + DEFAULT_WASM_MAX_MEMORY, + DEFAULT_MAX_STACK_HEIGHT, + OpcodeCosts::zero(), + StorageCosts::zero(), + HostFunctionCosts { + emit_message: HostFunction::fixed(FIRST_MESSAGE_EMIT_COST), + cost_increase_per_message: COST_INCREASE_PER_MESSAGE, + ..Zero::zero() + }, + MessageLimits::default(), + ); + + let custom_engine_config = EngineConfigBuilder::default() + .with_wasm_config(wasm_config) + .build(); + + let builder = RefCell::new(LmdbWasmTestBuilder::new_temporary_with_config( + custom_engine_config, + )); + builder + .borrow_mut() + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + let contract_hash = install_messages_emitter_contract(&builder); + + // Emit one message in this execution. Cost should be `FIRST_MESSAGE_EMIT_COST`. + emit_message_with_suffix(&builder, "test", &contract_hash, DEFAULT_BLOCK_TIME); + let emit_message_gas_cost = builder.borrow().last_exec_gas_cost().value(); + assert_eq!(emit_message_gas_cost, FIRST_MESSAGE_EMIT_COST.into()); + + // Emit multiple messages in this execution. Cost should increase for each message emitted. + let emit_messages_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_EMIT_MULTIPLE_MESSAGES, + runtime_args! { + ARG_NUM_MESSAGES_TO_EMIT => MESSAGES_TO_EMIT, + }, + ) + .build(); + builder + .borrow_mut() + .exec(emit_messages_request) + .expect_success() + .commit(); + + let emit_multiple_messages_cost = builder.borrow().last_exec_gas_cost().value(); + assert_eq!( + emit_multiple_messages_cost, + EMIT_MULTIPLE_EXPECTED_COST.into() + ); + + // Try another execution where we emit a single message. + // Cost should be `FIRST_MESSAGE_EMIT_COST` + emit_message_with_suffix(&builder, "test", &contract_hash, DEFAULT_BLOCK_TIME); + let emit_message_gas_cost = builder.borrow().last_exec_gas_cost().value(); + assert_eq!(emit_message_gas_cost, FIRST_MESSAGE_EMIT_COST.into()); + + // Check gas cost when multiple messages are emitted from different contracts. + let contract_hash = upgrade_messages_emitter_contract(&builder); + let emit_message_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + contract_hash, + ENTRY_POINT_EMIT_MESSAGE_FROM_EACH_VERSION, + runtime_args! { + ARG_MESSAGE_SUFFIX_NAME => "test message", + }, + ) + .build(); + + builder + .borrow_mut() + .exec(emit_message_request) + .expect_success() + .commit(); + + // 3 messages are emitted by this execution so the cost would be: + // `EMIT_MESSAGES_FROM_MULTIPLE_CONTRACTS` + let emit_message_gas_cost = builder.borrow().last_exec_gas_cost().value(); + assert_eq!( + emit_message_gas_cost, + U512::from(EMIT_MESSAGES_FROM_MULTIPLE_CONTRACTS) + ); +} diff --git a/execution_engine_testing/tests/src/test/explorer/faucet.rs b/execution_engine_testing/tests/src/test/explorer/faucet.rs index c0f43148fe..f1d5431c9b 100644 --- a/execution_engine_testing/tests/src/test/explorer/faucet.rs +++ b/execution_engine_testing/tests/src/test/explorer/faucet.rs @@ -863,11 +863,12 @@ fn should_allow_funding_by_an_authorized_account() { #[test] fn faucet_costs() { // This test will fail if execution costs vary. The expected costs should not be updated - // without understanding why the cost has changed. - const EXPECTED_FAUCET_INSTALL_COST: u64 = 84_201_467_720; - const EXPECTED_FAUCET_SET_VARIABLES_COST: u64 = 650_487_100; - const EXPECTED_FAUCET_CALL_BY_INSTALLER_COST: u64 = 3_247_573_380; - const EXPECTED_FAUCET_CALL_BY_USER_COST: u64 = 3_368_370_660; + // without understanding why the cost has changed. If the costs do change, it should be + // reflected in the "Costs by Entry Point" section of the faucet crate's README.md. + const EXPECTED_FAUCET_INSTALL_COST: u64 = 87_073_018_080; + const EXPECTED_FAUCET_SET_VARIABLES_COST: u64 = 650_909_620; + const EXPECTED_FAUCET_CALL_BY_INSTALLER_COST: u64 = 3_250_976_180; + const EXPECTED_FAUCET_CALL_BY_USER_COST: u64 = 3_368_796_020; let installer_account = AccountHash::new([1u8; 32]); let user_account: AccountHash = AccountHash::new([2u8; 32]); diff --git a/execution_engine_testing/tests/src/test/mod.rs b/execution_engine_testing/tests/src/test/mod.rs index f3f5394f83..8e0dd2cc05 100644 --- a/execution_engine_testing/tests/src/test/mod.rs +++ b/execution_engine_testing/tests/src/test/mod.rs @@ -3,6 +3,7 @@ mod chainspec_registry; mod check_transfer_success; mod contract_api; mod contract_context; +mod contract_messages; mod counter_factory; mod deploy; mod explorer; diff --git a/execution_engine_testing/tests/src/test/private_chain.rs b/execution_engine_testing/tests/src/test/private_chain.rs index 0f1c5746d9..e428d24bf4 100644 --- a/execution_engine_testing/tests/src/test/private_chain.rs +++ b/execution_engine_testing/tests/src/test/private_chain.rs @@ -21,8 +21,8 @@ use once_cell::sync::Lazy; use casper_types::{ account::AccountHash, system::auction::DELEGATION_RATE_DENOMINATOR, AdministratorAccount, - FeeHandling, GenesisAccount, GenesisValidator, HostFunction, HostFunctionCosts, Motes, - OpcodeCosts, PublicKey, RefundHandling, SecretKey, StorageCosts, WasmConfig, + FeeHandling, GenesisAccount, GenesisValidator, HostFunction, HostFunctionCosts, MessageLimits, + Motes, OpcodeCosts, PublicKey, RefundHandling, SecretKey, StorageCosts, WasmConfig, DEFAULT_MAX_STACK_HEIGHT, DEFAULT_WASM_MAX_MEMORY, U512, }; use tempfile::TempDir; @@ -208,6 +208,7 @@ fn make_wasm_config() -> WasmConfig { OpcodeCosts::default(), StorageCosts::default(), host_functions, + MessageLimits::default(), ) } diff --git a/execution_engine_testing/tests/src/test/regression/ee_966.rs b/execution_engine_testing/tests/src/test/regression/ee_966.rs index 644d20192b..4c92f52d87 100644 --- a/execution_engine_testing/tests/src/test/regression/ee_966.rs +++ b/execution_engine_testing/tests/src/test/regression/ee_966.rs @@ -13,8 +13,8 @@ use casper_execution_engine::{ }; use casper_types::{ addressable_entity::DEFAULT_ENTRY_POINT_NAME, runtime_args, ApiError, EraId, HostFunctionCosts, - OpcodeCosts, ProtocolVersion, RuntimeArgs, StorageCosts, WasmConfig, DEFAULT_MAX_STACK_HEIGHT, - DEFAULT_WASM_MAX_MEMORY, + MessageLimits, OpcodeCosts, ProtocolVersion, RuntimeArgs, StorageCosts, WasmConfig, + DEFAULT_MAX_STACK_HEIGHT, DEFAULT_WASM_MAX_MEMORY, }; const CONTRACT_EE_966_REGRESSION: &str = "ee_966_regression.wasm"; @@ -28,6 +28,7 @@ static DOUBLED_WASM_MEMORY_LIMIT: Lazy = Lazy::new(|| { OpcodeCosts::default(), StorageCosts::default(), HostFunctionCosts::default(), + MessageLimits::default(), ) }); static NEW_PROTOCOL_VERSION: Lazy = Lazy::new(|| { diff --git a/execution_engine_testing/tests/src/test/regression/gh_2280.rs b/execution_engine_testing/tests/src/test/regression/gh_2280.rs index 0a70f7adf7..ad27868e30 100644 --- a/execution_engine_testing/tests/src/test/regression/gh_2280.rs +++ b/execution_engine_testing/tests/src/test/regression/gh_2280.rs @@ -758,6 +758,7 @@ fn make_wasm_config( old_wasm_config.opcode_costs(), old_wasm_config.storage_costs(), new_host_function_costs, + old_wasm_config.messages_limits(), ) } diff --git a/execution_engine_testing/tests/src/test/regression/host_function_metrics_size_and_gas_cost.rs b/execution_engine_testing/tests/src/test/regression/host_function_metrics_size_and_gas_cost.rs index ade932341b..723d09c6f3 100644 --- a/execution_engine_testing/tests/src/test/regression/host_function_metrics_size_and_gas_cost.rs +++ b/execution_engine_testing/tests/src/test/regression/host_function_metrics_size_and_gas_cost.rs @@ -22,7 +22,7 @@ const CONTRACT_TRANSFER_TO_ACCOUNT_U512: &str = "transfer_to_account_u512.wasm"; // This value is not systemic, as code is added the size of WASM will increase, // you can change this value to reflect the increase in WASM size. const HOST_FUNCTION_METRICS_STANDARD_SIZE: usize = 97_569; -const HOST_FUNCTION_METRICS_STANDARD_GAS_COST: u64 = 364_682_740_480; +const HOST_FUNCTION_METRICS_STANDARD_GAS_COST: u64 = 364_434_284_571; /// Acceptable size regression/improvement in percentage. const SIZE_MARGIN: usize = 5; diff --git a/execution_engine_testing/tests/src/test/storage_costs.rs b/execution_engine_testing/tests/src/test/storage_costs.rs index 0ab78b84c4..30c4b09388 100644 --- a/execution_engine_testing/tests/src/test/storage_costs.rs +++ b/execution_engine_testing/tests/src/test/storage_costs.rs @@ -1,4 +1,5 @@ use num_rational::Ratio; +use num_traits::Zero; use once_cell::sync::Lazy; #[cfg(not(feature = "use-as-wasm"))] @@ -12,8 +13,8 @@ use casper_execution_engine::engine_state::EngineConfigBuilder; use casper_types::DEFAULT_ADD_BID_COST; use casper_types::{ bytesrepr::{Bytes, ToBytes}, - AddressableEntityHash, BrTableCost, CLValue, ControlFlowCosts, EraId, HostFunction, - HostFunctionCosts, OpcodeCosts, ProtocolVersion, RuntimeArgs, StorageCosts, StoredValue, + AddressableEntityHash, BrTableCost, CLValue, ControlFlowCosts, EraId, HostFunctionCosts, + MessageLimits, OpcodeCosts, ProtocolVersion, RuntimeArgs, StorageCosts, StoredValue, WasmConfig, DEFAULT_MAX_STACK_HEIGHT, DEFAULT_WASM_MAX_MEMORY, U512, }; #[cfg(not(feature = "use-as-wasm"))] @@ -88,53 +89,7 @@ const NEW_OPCODE_COSTS: OpcodeCosts = OpcodeCosts { grow_memory: 0, }; -static NEW_HOST_FUNCTION_COSTS: Lazy = Lazy::new(|| HostFunctionCosts { - read_value: HostFunction::fixed(0), - dictionary_get: HostFunction::fixed(0), - write: HostFunction::fixed(0), - dictionary_put: HostFunction::fixed(0), - add: HostFunction::fixed(0), - new_uref: HostFunction::fixed(0), - load_named_keys: HostFunction::fixed(0), - ret: HostFunction::fixed(0), - get_key: HostFunction::fixed(0), - has_key: HostFunction::fixed(0), - put_key: HostFunction::fixed(0), - remove_key: HostFunction::fixed(0), - revert: HostFunction::fixed(0), - is_valid_uref: HostFunction::fixed(0), - add_associated_key: HostFunction::fixed(0), - remove_associated_key: HostFunction::fixed(0), - update_associated_key: HostFunction::fixed(0), - set_action_threshold: HostFunction::fixed(0), - get_caller: HostFunction::fixed(0), - get_blocktime: HostFunction::fixed(0), - create_purse: HostFunction::fixed(0), - transfer_to_account: HostFunction::fixed(0), - transfer_from_purse_to_account: HostFunction::fixed(0), - transfer_from_purse_to_purse: HostFunction::fixed(0), - get_balance: HostFunction::fixed(0), - get_phase: HostFunction::fixed(0), - get_system_contract: HostFunction::fixed(0), - get_main_purse: HostFunction::fixed(0), - read_host_buffer: HostFunction::fixed(0), - create_contract_package_at_hash: HostFunction::fixed(0), - create_contract_user_group: HostFunction::fixed(0), - add_contract_version: HostFunction::fixed(0), - disable_contract_version: HostFunction::fixed(0), - call_contract: HostFunction::fixed(0), - call_versioned_contract: HostFunction::fixed(0), - get_named_arg_size: HostFunction::fixed(0), - get_named_arg: HostFunction::fixed(0), - remove_contract_user_group: HostFunction::fixed(0), - provision_contract_user_group_uref: HostFunction::fixed(0), - remove_contract_user_group_urefs: HostFunction::fixed(0), - print: HostFunction::fixed(0), - blake2b: HostFunction::fixed(0), - random_bytes: HostFunction::fixed(0), - enable_contract_version: HostFunction::fixed(0), - add_session_version: HostFunction::fixed(0), -}); +static NEW_HOST_FUNCTION_COSTS: Lazy = Lazy::new(HostFunctionCosts::zero); static STORAGE_COSTS_ONLY: Lazy = Lazy::new(|| { WasmConfig::new( DEFAULT_WASM_MAX_MEMORY, @@ -142,6 +97,7 @@ static STORAGE_COSTS_ONLY: Lazy = Lazy::new(|| { NEW_OPCODE_COSTS, StorageCosts::default(), *NEW_HOST_FUNCTION_COSTS, + MessageLimits::default(), ) }); diff --git a/execution_engine_testing/tests/src/test/system_contracts/upgrade.rs b/execution_engine_testing/tests/src/test/system_contracts/upgrade.rs index fb1b9927b4..7386d35399 100644 --- a/execution_engine_testing/tests/src/test/system_contracts/upgrade.rs +++ b/execution_engine_testing/tests/src/test/system_contracts/upgrade.rs @@ -17,9 +17,9 @@ use casper_types::{ }, mint::ROUND_SEIGNIORAGE_RATE_KEY, }, - BrTableCost, CLValue, ControlFlowCosts, EraId, HostFunctionCosts, OpcodeCosts, ProtocolVersion, - StorageCosts, StoredValue, WasmConfig, DEFAULT_ADD_COST, DEFAULT_BIT_COST, DEFAULT_CONST_COST, - DEFAULT_CONTROL_FLOW_BLOCK_OPCODE, DEFAULT_CONTROL_FLOW_BR_IF_OPCODE, + BrTableCost, CLValue, ControlFlowCosts, EraId, HostFunctionCosts, MessageLimits, OpcodeCosts, + ProtocolVersion, StorageCosts, StoredValue, WasmConfig, DEFAULT_ADD_COST, DEFAULT_BIT_COST, + DEFAULT_CONST_COST, DEFAULT_CONTROL_FLOW_BLOCK_OPCODE, DEFAULT_CONTROL_FLOW_BR_IF_OPCODE, DEFAULT_CONTROL_FLOW_BR_OPCODE, DEFAULT_CONTROL_FLOW_BR_TABLE_MULTIPLIER, DEFAULT_CONTROL_FLOW_BR_TABLE_OPCODE, DEFAULT_CONTROL_FLOW_CALL_INDIRECT_OPCODE, DEFAULT_CONTROL_FLOW_CALL_OPCODE, DEFAULT_CONTROL_FLOW_DROP_OPCODE, @@ -74,12 +74,14 @@ fn get_upgraded_wasm_config() -> WasmConfig { }; let storage_costs = StorageCosts::default(); let host_function_costs = HostFunctionCosts::default(); + let messages_limits = MessageLimits::default(); WasmConfig::new( DEFAULT_WASM_MAX_MEMORY, DEFAULT_MAX_STACK_HEIGHT * 2, opcode_cost, storage_costs, host_function_costs, + messages_limits, ) } diff --git a/execution_engine_testing/tests/src/test/system_costs.rs b/execution_engine_testing/tests/src/test/system_costs.rs index 80b47a3d92..e321258567 100644 --- a/execution_engine_testing/tests/src/test/system_costs.rs +++ b/execution_engine_testing/tests/src/test/system_costs.rs @@ -16,10 +16,11 @@ use casper_types::{ handle_payment, mint, AUCTION, }, AuctionCosts, BrTableCost, ControlFlowCosts, EraId, Gas, GenesisAccount, GenesisValidator, - HandlePaymentCosts, HostFunction, HostFunctionCost, HostFunctionCosts, MintCosts, Motes, - OpcodeCosts, ProtocolVersion, PublicKey, RuntimeArgs, SecretKey, StandardPaymentCosts, - StorageCosts, SystemConfig, WasmConfig, DEFAULT_ADD_BID_COST, DEFAULT_MAX_STACK_HEIGHT, - DEFAULT_TRANSFER_COST, DEFAULT_WASMLESS_TRANSFER_COST, DEFAULT_WASM_MAX_MEMORY, U512, + HandlePaymentCosts, HostFunction, HostFunctionCost, HostFunctionCosts, MessageLimits, + MintCosts, Motes, OpcodeCosts, ProtocolVersion, PublicKey, RuntimeArgs, SecretKey, + StandardPaymentCosts, StorageCosts, SystemConfig, WasmConfig, DEFAULT_ADD_BID_COST, + DEFAULT_MAX_STACK_HEIGHT, DEFAULT_TRANSFER_COST, DEFAULT_WASMLESS_TRANSFER_COST, + DEFAULT_WASM_MAX_MEMORY, U512, }; use crate::wasm_utils; @@ -924,51 +925,8 @@ fn should_verify_wasm_add_bid_wasm_cost_is_not_recursive() { // This will verify that user pays for the transfer host function _only_ while host does not // additionally charge for calling mint's "transfer" entrypoint under the hood. let new_host_function_costs = HostFunctionCosts { - read_value: HostFunction::fixed(0), - dictionary_get: HostFunction::fixed(0), - write: HostFunction::fixed(0), - dictionary_put: HostFunction::fixed(0), - add: HostFunction::fixed(0), - new_uref: HostFunction::fixed(0), - load_named_keys: HostFunction::fixed(0), - ret: HostFunction::fixed(0), - get_key: HostFunction::fixed(0), - has_key: HostFunction::fixed(0), - put_key: HostFunction::fixed(0), - remove_key: HostFunction::fixed(0), - revert: HostFunction::fixed(0), - is_valid_uref: HostFunction::fixed(0), - add_associated_key: HostFunction::fixed(0), - remove_associated_key: HostFunction::fixed(0), - update_associated_key: HostFunction::fixed(0), - set_action_threshold: HostFunction::fixed(0), - get_caller: HostFunction::fixed(0), - get_blocktime: HostFunction::fixed(0), - create_purse: HostFunction::fixed(0), - transfer_to_account: HostFunction::fixed(0), - transfer_from_purse_to_account: HostFunction::fixed(0), - transfer_from_purse_to_purse: HostFunction::fixed(0), - get_balance: HostFunction::fixed(0), - get_phase: HostFunction::fixed(0), - get_system_contract: HostFunction::fixed(0), - get_main_purse: HostFunction::fixed(0), - read_host_buffer: HostFunction::fixed(0), - create_contract_package_at_hash: HostFunction::fixed(0), - create_contract_user_group: HostFunction::fixed(0), - add_contract_version: HostFunction::fixed(0), - disable_contract_version: HostFunction::fixed(0), call_contract: HostFunction::fixed(UPDATED_CALL_CONTRACT_COST), - call_versioned_contract: HostFunction::fixed(0), - get_named_arg_size: HostFunction::fixed(0), - get_named_arg: HostFunction::fixed(0), - remove_contract_user_group: HostFunction::fixed(0), - provision_contract_user_group_uref: HostFunction::fixed(0), - remove_contract_user_group_urefs: HostFunction::fixed(0), - print: HostFunction::fixed(0), - blake2b: HostFunction::fixed(0), - random_bytes: HostFunction::fixed(0), - enable_contract_version: HostFunction::fixed(0), - add_session_version: HostFunction::fixed(0), + ..Zero::zero() }; let new_wasm_config = WasmConfig::new( @@ -977,6 +935,7 @@ fn should_verify_wasm_add_bid_wasm_cost_is_not_recursive() { new_opcode_costs, new_storage_costs, new_host_function_costs, + MessageLimits::default(), ); let new_wasmless_transfer_cost = 0; diff --git a/node/src/components/contract_runtime.rs b/node/src/components/contract_runtime.rs index f62465534d..676d3ebe68 100644 --- a/node/src/components/contract_runtime.rs +++ b/node/src/components/contract_runtime.rs @@ -75,7 +75,7 @@ pub(crate) use operations::compute_execution_results_checksum; pub use operations::execute_finalized_block; use operations::execute_only; pub(crate) use types::{ - BlockAndExecutionResults, EraValidatorsRequest, RoundSeigniorageRateRequest, + BlockAndExecutionResults, EraValidatorsRequest, ExecutionArtifact, RoundSeigniorageRateRequest, StepEffectsAndUpcomingEraValidators, TotalSupplyRequest, }; @@ -984,7 +984,7 @@ impl ContractRuntime { let execution_results_map: HashMap<_, _> = execution_results .iter() .cloned() - .map(|(deploy_hash, _, execution_result)| (deploy_hash, execution_result)) + .map(|artifact| (artifact.deploy_hash, artifact.execution_result)) .collect(); if meta_block_state.register_as_stored().was_updated() { effect_builder diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index b1e5273d16..2569158179 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -5,10 +5,12 @@ use tracing::{debug, error, info, trace, warn}; use casper_execution_engine::{ engine_state::{ - self, execution_result::ExecutionResults, step::EvictItem, ChecksumRegistry, DeployItem, - EngineState, ExecuteRequest, ExecutionResult as EngineExecutionResult, - GetEraValidatorsRequest, PruneConfig, PruneResult, QueryRequest, QueryResult, StepError, - StepRequest, StepSuccess, + self, + execution_result::{ExecutionResultAndMessages, ExecutionResults}, + step::EvictItem, + ChecksumRegistry, DeployItem, EngineState, ExecuteRequest, + ExecutionResult as EngineExecutionResult, GetEraValidatorsRequest, PruneConfig, + PruneResult, QueryRequest, QueryResult, StepError, StepRequest, StepSuccess, }, execution, }; @@ -19,6 +21,7 @@ use casper_storage::{ use casper_types::{ account::AccountHash, bytesrepr::{self, ToBytes, U32_SERIALIZED_LENGTH}, + contract_messages::Messages, execution::{Effects, ExecutionResult, ExecutionResultV2, Transform, TransformKind}, AddressableEntity, AddressableEntityHash, BlockV2, CLValue, DeployHash, Digest, EraEndV2, EraId, HashAddr, Key, ProtocolVersion, PublicKey, StoredValue, Transaction, U512, @@ -36,6 +39,8 @@ use crate::{ types::{self, ApprovalsHashes, Chunkable, ExecutableBlock, InternalEraReport}, }; +use super::ExecutionArtifact; + fn generate_range_by_index( highest_era: u64, batch_size: u64, @@ -111,7 +116,8 @@ pub fn execute_finalized_block( next_block_height, } = execution_pre_state; let mut state_root_hash = pre_state_root_hash; - let mut execution_results = Vec::with_capacity(executable_block.transactions.len()); + let mut execution_results: Vec = + Vec::with_capacity(executable_block.transactions.len()); // Run any deploys that must be executed let block_time = executable_block.timestamp.millis(); let start = Instant::now(); @@ -163,20 +169,28 @@ pub fn execute_finalized_block( trace!(?deploy_hash, ?result, "deploy execution result"); // As for now a given state is expected to exist. - let (state_hash, execution_result) = commit_execution_results( + let (state_hash, execution_result, messages) = commit_execution_results( &scratch_state, metrics.clone(), state_root_hash, deploy_hash, result, )?; - execution_results.push((deploy_hash, deploy_header, execution_result)); + execution_results.push(ExecutionArtifact::new( + deploy_hash, + deploy_header, + execution_result, + messages, + )); state_root_hash = state_hash; } // Write the deploy approvals' and execution results' checksums to global state. - let execution_results_checksum = - compute_execution_results_checksum(execution_results.iter().map(|(_, _, result)| result))?; + let execution_results_checksum = compute_execution_results_checksum( + execution_results + .iter() + .map(|artifact| &artifact.execution_result), + )?; let mut checksum_registry = ChecksumRegistry::new(); checksum_registry.insert(APPROVALS_CHECKSUM_NAME, approvals_checksum); @@ -382,7 +396,7 @@ fn commit_execution_results( state_root_hash: Digest, deploy_hash: DeployHash, execution_results: ExecutionResults, -) -> Result<(Digest, ExecutionResult), BlockExecutionError> +) -> Result<(Digest, ExecutionResult, Messages), BlockExecutionError> where S: StateProvider + CommitProvider, S::Error: Into, @@ -413,9 +427,12 @@ where } }; let new_state_root = commit_transforms(engine_state, metrics, state_root_hash, effects)?; - let versioned_execution_result = - ExecutionResult::from(ExecutionResultV2::from(ee_execution_result)); - Ok((new_state_root, versioned_execution_result)) + let ExecutionResultAndMessages { + execution_result, + messages, + } = ExecutionResultAndMessages::from(ee_execution_result); + let versioned_execution_result = ExecutionResult::from(execution_result); + Ok((new_state_root, versioned_execution_result, messages)) } fn commit_transforms( @@ -446,7 +463,7 @@ pub fn execute_only( engine_state: &EngineState, execution_state: SpeculativeExecutionState, deploy: DeployItem, -) -> Result, engine_state::Error> +) -> Result, engine_state::Error> where S: StateProvider + CommitProvider, S::Error: Into, @@ -478,7 +495,14 @@ where // with `Some(_)` but `pop_front` already returns an `Option`. // We need to transform the `engine_state::ExecutionResult` into // `casper_types::ExecutionResult` as well. - execution_results.pop_front().map(Into::into) + execution_results.pop_front().map(|result| { + let ExecutionResultAndMessages { + execution_result, + messages, + } = result.into(); + + (execution_result, messages) + }) } }) } diff --git a/node/src/components/contract_runtime/types.rs b/node/src/components/contract_runtime/types.rs index ed326aef58..da6b4c284b 100644 --- a/node/src/components/contract_runtime/types.rs +++ b/node/src/components/contract_runtime/types.rs @@ -4,9 +4,11 @@ use datasize::DataSize; use casper_execution_engine::engine_state::GetEraValidatorsRequest; use casper_types::{ + contract_messages::Messages, execution::{Effects, ExecutionResult}, BlockV2, DeployHash, DeployHeader, Digest, EraId, ProtocolVersion, PublicKey, U512, }; +use serde::Serialize; use crate::types::ApprovalsHashes; @@ -114,6 +116,30 @@ pub(crate) struct StepEffectsAndUpcomingEraValidators { pub(crate) step_effects: Effects, } +#[derive(Clone, Debug, DataSize, PartialEq, Eq, Serialize)] +pub(crate) struct ExecutionArtifact { + pub(crate) deploy_hash: DeployHash, + pub(crate) deploy_header: DeployHeader, + pub(crate) execution_result: ExecutionResult, + pub(crate) messages: Messages, +} + +impl ExecutionArtifact { + pub(crate) fn new( + deploy_hash: DeployHash, + deploy_header: DeployHeader, + execution_result: ExecutionResult, + messages: Messages, + ) -> Self { + Self { + deploy_hash, + deploy_header, + execution_result, + messages, + } + } +} + #[doc(hidden)] /// A [`Block`] that was the result of execution in the `ContractRuntime` along with any execution /// effects it may have. @@ -124,7 +150,7 @@ pub struct BlockAndExecutionResults { /// The [`ApprovalsHashes`] for the deploys in this block. pub(crate) approvals_hashes: Box, /// The results from executing the deploys in the block. - pub(crate) execution_results: Vec<(DeployHash, DeployHeader, ExecutionResult)>, + pub(crate) execution_results: Vec, /// The [`Effects`] and the upcoming validator sets determined by the `step` pub(crate) maybe_step_effects_and_upcoming_era_validators: Option, diff --git a/node/src/components/event_stream_server.rs b/node/src/components/event_stream_server.rs index e15fc9d022..1ceda03d9b 100644 --- a/node/src/components/event_stream_server.rs +++ b/node/src/components/event_stream_server.rs @@ -296,6 +296,7 @@ where transaction_header, block_hash, execution_result, + messages, } => { let (account, timestamp, ttl) = match *transaction_header { TransactionHeader::Deploy(deploy_header) => ( @@ -316,6 +317,7 @@ where ttl, block_hash: Box::new(block_hash), execution_result, + messages, }) } Event::TransactionsExpired(transaction_hashes) => transaction_hashes diff --git a/node/src/components/event_stream_server/event.rs b/node/src/components/event_stream_server/event.rs index 2913fae7e2..7eec8d1b68 100644 --- a/node/src/components/event_stream_server/event.rs +++ b/node/src/components/event_stream_server/event.rs @@ -6,6 +6,7 @@ use std::{ use itertools::Itertools; use casper_types::{ + contract_messages::Messages, execution::{Effects, ExecutionResult}, Block, BlockHash, EraId, FinalitySignature, PublicKey, Timestamp, Transaction, TransactionHash, TransactionHeader, @@ -21,6 +22,7 @@ pub enum Event { transaction_header: Box, block_hash: BlockHash, execution_result: Box, + messages: Messages, }, TransactionsExpired(Vec), Fault { diff --git a/node/src/components/event_stream_server/sse_server.rs b/node/src/components/event_stream_server/sse_server.rs index a721cc6de8..980c9e1811 100644 --- a/node/src/components/event_stream_server/sse_server.rs +++ b/node/src/components/event_stream_server/sse_server.rs @@ -32,16 +32,17 @@ use warp::{ Filter, Reply, }; -#[cfg(test)] -use casper_types::{ - execution::ExecutionResultV2, testing::TestRng, Deploy, TestBlockBuilder, - TestTransactionV1Builder, -}; use casper_types::{ + contract_messages::Messages, execution::{Effects, ExecutionResult}, Block, BlockHash, EraId, FinalitySignature, ProtocolVersion, PublicKey, TimeDiff, Timestamp, Transaction, TransactionHash, }; +#[cfg(test)] +use casper_types::{ + execution::ExecutionResultV2, testing::TestRng, Deploy, TestBlockBuilder, + TestTransactionV1Builder, +}; /// The URL root path. pub const SSE_API_PATH: &str = "events"; @@ -75,8 +76,9 @@ pub enum SseData { timestamp: Timestamp, ttl: TimeDiff, block_hash: Box, - #[data_size(skip)] + //#[data_size(skip)] execution_result: Box, + messages: Messages, }, /// The given transaction has expired. TransactionExpired { transaction_hash: TransactionHash }, @@ -124,6 +126,11 @@ impl SseData { Transaction::Deploy(deploy) => (deploy.timestamp(), deploy.ttl()), Transaction::V1(txn) => (txn.timestamp(), txn.ttl()), }; + let message_count = rng.gen_range(0..6); + let messages = std::iter::repeat_with(|| rng.gen()) + .take(message_count) + .collect(); + SseData::TransactionProcessed { transaction_hash: Box::new(txn.hash()), account: Box::new(txn.account().clone()), @@ -131,6 +138,7 @@ impl SseData { ttl, block_hash: Box::new(BlockHash::random(rng)), execution_result: Box::new(ExecutionResult::from(ExecutionResultV2::random(rng))), + messages, } } diff --git a/node/src/components/rpc_server/rpcs/speculative_exec.rs b/node/src/components/rpc_server/rpcs/speculative_exec.rs index 81b3297f1f..fce8fddb34 100644 --- a/node/src/components/rpc_server/rpcs/speculative_exec.rs +++ b/node/src/components/rpc_server/rpcs/speculative_exec.rs @@ -9,7 +9,10 @@ use serde::{Deserialize, Serialize}; use casper_execution_engine::engine_state::Error as EngineStateError; use casper_json_rpc::ReservedErrorCode; -use casper_types::{execution::ExecutionResultV2, BlockHash, Deploy, ProtocolVersion, Transaction}; +use casper_types::{ + contract_messages::Messages, execution::ExecutionResultV2, BlockHash, Deploy, ProtocolVersion, + Transaction, +}; use super::{ chain::BlockIdentifier, @@ -29,6 +32,7 @@ static SPECULATIVE_EXEC_TXN_RESULT: Lazy = api_version: DOCS_EXAMPLE_PROTOCOL_VERSION, block_hash: *BlockHash::example(), execution_result: ExecutionResultV2::example().clone(), + messages: Vec::new(), }); static SPECULATIVE_EXEC_PARAMS: Lazy = Lazy::new(|| SpeculativeExecParams { block_identifier: Some(BlockIdentifier::Hash(*BlockHash::example())), @@ -62,6 +66,8 @@ pub struct SpeculativeExecTxnResult { pub block_hash: BlockHash, /// Result of the execution. pub execution_result: ExecutionResultV2, + /// Messages emitted during execution. + pub messages: Messages, } impl DocExample for SpeculativeExecTxnResult { @@ -168,11 +174,12 @@ async fn handle_request( .await; match result { - Ok(Some(execution_result)) => { + Ok(Some((execution_result, messages))) => { let result = SpeculativeExecTxnResult { api_version, block_hash, execution_result, + messages, }; Ok(result) } diff --git a/node/src/effect.rs b/node/src/effect.rs index 28910cd24e..56f466bb9b 100644 --- a/node/src/effect.rs +++ b/node/src/effect.rs @@ -122,6 +122,7 @@ use casper_execution_engine::engine_state::{ use casper_storage::global_state::trie::TrieRaw; use casper_types::{ bytesrepr::Bytes, + contract_messages::Messages, execution::{Effects as ExecutionEffects, ExecutionResult, ExecutionResultV2}, package::Package, system::auction::EraValidators, @@ -2248,7 +2249,7 @@ impl EffectBuilder { self, execution_prestate: SpeculativeExecutionState, transaction: Box, - ) -> Result, engine_state::Error> + ) -> Result, engine_state::Error> where REv: From, { diff --git a/node/src/effect/requests.rs b/node/src/effect/requests.rs index fb9f13c4fa..9af9516928 100644 --- a/node/src/effect/requests.rs +++ b/node/src/effect/requests.rs @@ -27,6 +27,7 @@ use casper_storage::global_state::trie::TrieRaw; use casper_types::{ addressable_entity::AddressableEntity, bytesrepr::Bytes, + contract_messages::Messages, execution::{ExecutionResult, ExecutionResultV2}, system::auction::EraValidators, Block, BlockHash, BlockHeader, BlockSignatures, BlockV2, ChainspecRawBytes, DeployHash, @@ -965,7 +966,7 @@ pub(crate) enum ContractRuntimeRequest { /// Transaction to execute. transaction: Box, /// Results - responder: Responder, engine_state::Error>>, + responder: Responder, engine_state::Error>>, }, } diff --git a/node/src/reactor/main_reactor.rs b/node/src/reactor/main_reactor.rs index 3d81f8f389..4441d114bd 100644 --- a/node/src/reactor/main_reactor.rs +++ b/node/src/reactor/main_reactor.rs @@ -1452,6 +1452,7 @@ impl MainReactor { if let MetaBlock::Forward(forward_meta_block) = &meta_block { let block = forward_meta_block.block.clone(); + let execution_results = forward_meta_block.execution_results.clone(); if meta_block .mut_state() @@ -1465,7 +1466,7 @@ impl MainReactor { ); let meta_block = ForwardMetaBlock { block, - execution_results: meta_block.execution_results().clone(), + execution_results, state: *meta_block.state(), }; effects.extend(reactor::wrap_effects( @@ -1579,19 +1580,46 @@ impl MainReactor { ), )); - for (deploy_hash, deploy_header, execution_result) in meta_block.execution_results().clone() - { - let event = event_stream_server::Event::TransactionProcessed { - transaction_hash: TransactionHash::Deploy(deploy_hash), - transaction_header: Box::new(TransactionHeader::Deploy(deploy_header)), - block_hash: meta_block.hash(), - execution_result: Box::new(execution_result), - }; - effects.extend(reactor::wrap_effects( - MainEvent::EventStreamServer, - self.event_stream_server - .handle_event(effect_builder, rng, event), - )); + match &meta_block { + MetaBlock::Forward(fwd_meta_block) => { + for exec_artifact in fwd_meta_block.execution_results.iter() { + let event = event_stream_server::Event::TransactionProcessed { + transaction_hash: TransactionHash::Deploy(exec_artifact.deploy_hash), + transaction_header: Box::new(TransactionHeader::Deploy( + exec_artifact.deploy_header.clone(), + )), + block_hash: *fwd_meta_block.block.hash(), + execution_result: Box::new(exec_artifact.execution_result.clone()), + messages: exec_artifact.messages.clone(), + }; + + effects.extend(reactor::wrap_effects( + MainEvent::EventStreamServer, + self.event_stream_server + .handle_event(effect_builder, rng, event), + )); + } + } + MetaBlock::Historical(historical_meta_block) => { + for (deploy_hash, deploy_header, execution_result) in + historical_meta_block.execution_results.iter() + { + let event = event_stream_server::Event::TransactionProcessed { + transaction_hash: TransactionHash::Deploy(*deploy_hash), + transaction_header: Box::new(TransactionHeader::Deploy( + deploy_header.clone(), + )), + block_hash: *historical_meta_block.block.hash(), + execution_result: Box::new(execution_result.clone()), + messages: Vec::new(), + }; + effects.extend(reactor::wrap_effects( + MainEvent::EventStreamServer, + self.event_stream_server + .handle_event(effect_builder, rng, event), + )); + } + } } debug!( diff --git a/node/src/types/block/meta_block.rs b/node/src/types/block/meta_block.rs index a22da43e05..b6e0ac52c8 100644 --- a/node/src/types/block/meta_block.rs +++ b/node/src/types/block/meta_block.rs @@ -14,6 +14,8 @@ use casper_types::{ pub(crate) use merge_mismatch_error::MergeMismatchError; pub(crate) use state::State; +use crate::contract_runtime::ExecutionArtifact; + /// A block along with its execution results and state recording which actions have been taken /// related to the block. /// @@ -30,7 +32,7 @@ pub(crate) enum MetaBlock { impl MetaBlock { pub(crate) fn new_forward( block: Arc, - execution_results: Vec<(DeployHash, DeployHeader, ExecutionResult)>, + execution_results: Vec, state: State, ) -> Self { Self::Forward(ForwardMetaBlock { @@ -93,19 +95,12 @@ impl MetaBlock { MetaBlock::Historical(meta_block) => &meta_block.state, } } - - pub(crate) fn execution_results(&self) -> &Vec<(DeployHash, DeployHeader, ExecutionResult)> { - match &self { - MetaBlock::Forward(meta_block) => &meta_block.execution_results, - MetaBlock::Historical(meta_block) => &meta_block.execution_results, - } - } } #[derive(Clone, Eq, PartialEq, Serialize, Debug, DataSize)] pub(crate) struct ForwardMetaBlock { pub(crate) block: Arc, - pub(crate) execution_results: Vec<(DeployHash, DeployHeader, ExecutionResult)>, + pub(crate) execution_results: Vec, pub(crate) state: State, } @@ -186,10 +181,11 @@ mod tests { let block = Arc::new(TestBlockBuilder::new().build(rng)); let deploy = Deploy::random(rng); - let execution_results = vec![( + let execution_results = vec![ExecutionArtifact::new( *deploy.hash(), deploy.take_header(), ExecutionResult::from(ExecutionResultV2::random(rng)), + Vec::new(), )]; let state = State::new_already_stored(); @@ -240,10 +236,11 @@ mod tests { let block = Arc::new(TestBlockBuilder::new().build(rng)); let deploy = Deploy::random(rng); - let execution_results = vec![( + let execution_results = vec![ExecutionArtifact::new( *deploy.hash(), deploy.take_header(), ExecutionResult::from(ExecutionResultV2::random(rng)), + Vec::new(), )]; let state = State::new_not_to_be_gossiped(); @@ -277,10 +274,11 @@ mod tests { .build(rng), ); let deploy = Deploy::random(rng); - let execution_results = vec![( + let execution_results = vec![ExecutionArtifact::new( *deploy.hash(), deploy.take_header(), ExecutionResult::from(ExecutionResultV2::random(rng)), + Vec::new(), )]; let state = State::new(); @@ -309,16 +307,18 @@ mod tests { let block = Arc::new(TestBlockBuilder::new().build(rng)); let deploy1 = Deploy::random(rng); - let execution_results1 = vec![( + let execution_results1 = vec![ExecutionArtifact::new( *deploy1.hash(), deploy1.take_header(), ExecutionResult::from(ExecutionResultV2::random(rng)), + Vec::new(), )]; let deploy2 = Deploy::random(rng); - let execution_results2 = vec![( + let execution_results2 = vec![ExecutionArtifact::new( *deploy2.hash(), deploy2.take_header(), ExecutionResult::from(ExecutionResultV2::random(rng)), + Vec::new(), )]; let state = State::new(); diff --git a/node/src/utils/chain_specification.rs b/node/src/utils/chain_specification.rs index cc8ac0e797..e3d25c1d1e 100644 --- a/node/src/utils/chain_specification.rs +++ b/node/src/utils/chain_specification.rs @@ -148,8 +148,8 @@ mod tests { use casper_types::{ bytesrepr::FromBytes, ActivationPoint, BrTableCost, ChainspecRawBytes, ControlFlowCosts, CoreConfig, EraId, GlobalStateUpdate, HighwayConfig, HostFunction, HostFunctionCosts, - Motes, OpcodeCosts, ProtocolConfig, ProtocolVersion, StorageCosts, StoredValue, - TestBlockBuilder, TimeDiff, Timestamp, TransactionConfig, WasmConfig, U512, + MessageLimits, Motes, OpcodeCosts, ProtocolConfig, ProtocolVersion, StorageCosts, + StoredValue, TestBlockBuilder, TimeDiff, Timestamp, TransactionConfig, WasmConfig, U512, }; use super::*; @@ -242,6 +242,9 @@ mod tests { enable_contract_version: HostFunction::new(142, [0, 1, 2, 3]), // TODO: Update this cost. add_session_version: HostFunction::default(), + manage_message_topic: HostFunction::new(100, [0, 1, 2, 4]), + emit_message: HostFunction::new(100, [0, 1, 2, 3]), + cost_increase_per_message: 50, }); static EXPECTED_GENESIS_WASM_COSTS: Lazy = Lazy::new(|| { WasmConfig::new( @@ -250,6 +253,7 @@ mod tests { EXPECTED_GENESIS_COSTS, EXPECTED_GENESIS_STORAGE_COSTS, *EXPECTED_GENESIS_HOST_FUNCTION_COSTS, + MessageLimits::default(), ) }); diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index 51d423bc85..160eb1ecda 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -258,6 +258,14 @@ update_associated_key = { cost = 4_200, arguments = [0, 0, 0] } write = { cost = 14_000, arguments = [0, 0, 0, 980] } write_local = { cost = 9_500, arguments = [0, 1_800, 0, 520] } add_session_version = { cost = 200, arguments = [0, 0] } +manage_message_topic = { cost = 200, arguments = [0, 0, 0, 0] } +emit_message = { cost = 200, arguments = [0, 0, 0, 0] } +cost_increase_per_message = 50 + +[wasm.messages_limits] +max_topic_name_size = 256 +max_topics_per_contract = 128 +max_message_size = 1_024 [system_costs] wasmless_transfer_cost = 100_000_000 diff --git a/resources/production/chainspec.toml b/resources/production/chainspec.toml index f30228c656..af4e8ee358 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -322,6 +322,14 @@ write = { cost = 14_000, arguments = [0, 0, 0, 980] } write_local = { cost = 9_500, arguments = [0, 1_800, 0, 520] } enable_contract_version = { cost = 200, arguments = [0, 0, 0, 0] } add_session_version = { cost = 200, arguments = [0, 0] } +manage_message_topic = { cost = 200, arguments = [0, 0, 0, 0] } +emit_message = { cost = 200, arguments = [0, 0, 0, 0] } +cost_increase_per_message = 50 + +[wasm.messages_limits] +max_topic_name_size = 256 +max_topics_per_contract = 128 +max_message_size = 1_024 [system_costs] wasmless_transfer_cost = 100_000_000 diff --git a/resources/test/rpc_schema.json b/resources/test/rpc_schema.json index c631c18014..9eec7e96cd 100644 --- a/resources/test/rpc_schema.json +++ b/resources/test/rpc_schema.json @@ -5229,6 +5229,32 @@ } }, "additionalProperties": false + }, + { + "description": "Variant that stores a message topic.", + "type": "object", + "required": [ + "MessageTopic" + ], + "properties": { + "MessageTopic": { + "$ref": "#/components/schemas/MessageTopicSummary" + } + }, + "additionalProperties": false + }, + { + "description": "Variant that stores a message digest.", + "type": "object", + "required": [ + "Message" + ], + "properties": { + "Message": { + "$ref": "#/components/schemas/MessageChecksum" + } + }, + "additionalProperties": false } ] }, @@ -5552,6 +5578,7 @@ "byte_code_hash", "entry_points", "main_purse", + "message_topics", "named_keys", "package_hash", "protocol_version" @@ -5580,6 +5607,9 @@ }, "action_thresholds": { "$ref": "#/components/schemas/ActionThresholds" + }, + "message_topics": { + "$ref": "#/components/schemas/Array_of_MessageTopic" } } }, @@ -5587,6 +5617,35 @@ "description": "The hash address of the contract wasm", "type": "string" }, + "Array_of_MessageTopic": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageTopic" + } + }, + "MessageTopic": { + "type": "object", + "required": [ + "topic_name", + "topic_name_hash" + ], + "properties": { + "topic_name": { + "type": "string" + }, + "topic_name_hash": { + "allOf": [ + { + "$ref": "#/components/schemas/TopicNameHash" + } + ] + } + } + }, + "TopicNameHash": { + "description": "The hash of the name of the message topic.", + "type": "string" + }, "Package": { "description": "Entity definition, metadata, and security container.", "type": "object", @@ -5825,6 +5884,40 @@ } ] }, + "MessageTopicSummary": { + "description": "Summary of a message topic that will be stored in global state.", + "type": "object", + "required": [ + "blocktime", + "message_count" + ], + "properties": { + "message_count": { + "description": "Number of messages in this topic.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "blocktime": { + "description": "Block timestamp in which these messages were emitted.", + "allOf": [ + { + "$ref": "#/components/schemas/BlockTime" + } + ] + } + } + }, + "BlockTime": { + "description": "A newtype wrapping a [`u64`] which represents the block time.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "MessageChecksum": { + "description": "Message checksum as a formatted string.", + "type": "string" + }, "GlobalStateIdentifier": { "description": "Identifier for possible ways to query Global State", "oneOf": [ diff --git a/resources/test/sse_data_schema.json b/resources/test/sse_data_schema.json index 9deba518a0..f9659765d1 100644 --- a/resources/test/sse_data_schema.json +++ b/resources/test/sse_data_schema.json @@ -80,6 +80,7 @@ "account", "block_hash", "execution_result", + "messages", "timestamp", "transaction_hash", "ttl" @@ -102,6 +103,12 @@ }, "execution_result": { "$ref": "#/definitions/ExecutionResult" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/Message" + } } } } @@ -3469,6 +3476,71 @@ "$ref": "#/definitions/Transform" } }, + "Message": { + "description": "Message that was emitted by an addressable entity during execution.", + "type": "object", + "required": [ + "entity_addr", + "index", + "message", + "topic_name", + "topic_name_hash" + ], + "properties": { + "entity_addr": { + "description": "The identity of the entity that produced the message.", + "type": "string" + }, + "message": { + "description": "The payload of the message.", + "allOf": [ + { + "$ref": "#/definitions/MessagePayload" + } + ] + }, + "topic_name": { + "description": "The name of the topic on which the message was emitted on.", + "type": "string" + }, + "topic_name_hash": { + "description": "The hash of the name of the topic.", + "allOf": [ + { + "$ref": "#/definitions/TopicNameHash" + } + ] + }, + "index": { + "description": "Message index in the topic.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "MessagePayload": { + "description": "The payload of the message emitted by an addressable entity during execution.", + "oneOf": [ + { + "description": "Human readable string message.", + "type": "object", + "required": [ + "String" + ], + "properties": { + "String": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "TopicNameHash": { + "description": "The hash of the name of the message topic.", + "type": "string" + }, "FinalitySignature": { "description": "A validator's signature of a block, confirming it is finalized.", "type": "object", diff --git a/smart_contracts/contract/src/contract_api/runtime.rs b/smart_contracts/contract/src/contract_api/runtime.rs index 91a2ecc26e..d8decfe847 100644 --- a/smart_contracts/contract/src/contract_api/runtime.rs +++ b/smart_contracts/contract/src/contract_api/runtime.rs @@ -8,6 +8,7 @@ use casper_types::{ addressable_entity::NamedKeys, api_error, bytesrepr::{self, FromBytes}, + contract_messages::{MessagePayload, MessageTopicOperation}, package::EntityVersion, system::CallStackElement, AddressableEntityHash, ApiError, BlockTime, CLTyped, CLValue, Key, PackageHash, Phase, @@ -404,6 +405,47 @@ pub fn get_call_stack() -> Vec { bytesrepr::deserialize(bytes).unwrap_or_revert() } +/// Manages a message topic. +pub fn manage_message_topic( + topic_name: &str, + operation: MessageTopicOperation, +) -> Result<(), ApiError> { + if topic_name.is_empty() { + return Err(ApiError::InvalidArgument); + } + + let (operation_ptr, operation_size, _bytes) = contract_api::to_ptr(operation); + let result = unsafe { + ext_ffi::casper_manage_message_topic( + topic_name.as_ptr(), + topic_name.len(), + operation_ptr, + operation_size, + ) + }; + api_error::result_from(result) +} + +/// Emits a message on a topic. +pub fn emit_message(topic_name: &str, message: &MessagePayload) -> Result<(), ApiError> { + if topic_name.is_empty() { + return Err(ApiError::InvalidArgument); + } + + let (message_ptr, message_size, _bytes) = contract_api::to_ptr(message); + + let result = unsafe { + ext_ffi::casper_emit_message( + topic_name.as_ptr(), + topic_name.len(), + message_ptr, + message_size, + ) + }; + + api_error::result_from(result) +} + #[cfg(feature = "test-support")] /// Prints a debug message pub fn print(text: &str) { diff --git a/smart_contracts/contract/src/ext_ffi.rs b/smart_contracts/contract/src/ext_ffi.rs index 36aec2f1d6..4997d99f51 100644 --- a/smart_contracts/contract/src/ext_ffi.rs +++ b/smart_contracts/contract/src/ext_ffi.rs @@ -808,4 +808,34 @@ extern "C" { contract_hash_ptr: *const u8, contract_hash_size: usize, ) -> i32; + /// Manages a message topic. + /// + /// # Arguments + /// + /// * `topic_name_ptr` - pointer to the topic name UTF-8 string. + /// * `topic_name_size` - size of the serialized name string. + /// * `operation_ptr` - pointer to the management operation to be performed for the specified + /// topic. + /// * `operation_ptr_size` - size of the operation. + pub fn casper_manage_message_topic( + topic_name_ptr: *const u8, + topic_name_size: usize, + operation_ptr: *const u8, + operation_size: usize, + ) -> i32; + /// Emits a new message on the specified topic. + /// + /// # Arguments + /// + /// * `topic_name_ptr` - pointer to the topic name UTF-8 string where the message will be + /// emitted. + /// * `topic_name_size` - size of the serialized name string. + /// * `message_ptr` - pointer to the serialized message payload to be emitted. + /// * `message_size` - size of the serialized message payload. + pub fn casper_emit_message( + topic_name_ptr: *const u8, + topic_name_size: usize, + message_ptr: *const u8, + message_size: usize, + ) -> i32; } diff --git a/smart_contracts/contracts/explorer/faucet/README.md b/smart_contracts/contracts/explorer/faucet/README.md index c3a226681c..65eb82038a 100644 --- a/smart_contracts/contracts/explorer/faucet/README.md +++ b/smart_contracts/contracts/explorer/faucet/README.md @@ -30,3 +30,12 @@ After an interval passes after then last user was funded, the available amount w `distributions_per_interval`, `available_amount`, `time_interval` and `max_distributions_per_interval` must be set and must be a number greater than `0` for the contract to run properly. If you try to invoke the contract before these variables are set, then you'll get an error. + +### Costs by Entry Point + +| feature | cost | +|--------------------------|------------------| +| faucet install | `87_073_018_080` | +| faucet set variables | `650_909_620` | +| faucet call by installer | `3_250_976_180` | +| faucet call by user | `3_368_796_020` | diff --git a/smart_contracts/contracts/test/contract-messages-emitter/Cargo.toml b/smart_contracts/contracts/test/contract-messages-emitter/Cargo.toml new file mode 100644 index 0000000000..4594ca4c88 --- /dev/null +++ b/smart_contracts/contracts/test/contract-messages-emitter/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "contract-messages-emitter" +version = "0.1.0" +authors = ["Alexandru Sardan "] +edition = "2018" + +[[bin]] +name = "contract_messages_emitter" +path = "src/main.rs" +bench = false +doctest = false +test = false + +[dependencies] +casper-contract = { path = "../../../contract" } +casper-types = { path = "../../../../types" } diff --git a/smart_contracts/contracts/test/contract-messages-emitter/src/main.rs b/smart_contracts/contracts/test/contract-messages-emitter/src/main.rs new file mode 100644 index 0000000000..3a93d49b64 --- /dev/null +++ b/smart_contracts/contracts/test/contract-messages-emitter/src/main.rs @@ -0,0 +1,123 @@ +#![no_std] +#![no_main] + +#[macro_use] +extern crate alloc; +use alloc::{string::String, vec::Vec}; + +use casper_contract::{ + contract_api::{runtime, storage}, + unwrap_or_revert::UnwrapOrRevert, +}; + +use casper_types::{ + addressable_entity::{EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, NamedKeys}, + api_error::ApiError, + contract_messages::MessageTopicOperation, + CLType, CLTyped, Parameter, RuntimeArgs, +}; + +const ENTRY_POINT_INIT: &str = "init"; +const ENTRY_POINT_EMIT_MESSAGE: &str = "emit_message"; +const ENTRY_POINT_EMIT_MULTIPLE_MESSAGES: &str = "emit_multiple_messages"; +const ENTRY_POINT_ADD_TOPIC: &str = "add_topic"; +const MESSAGE_EMITTER_INITIALIZED: &str = "message_emitter_initialized"; +const ARG_MESSAGE_SUFFIX_NAME: &str = "message_suffix"; +const ARG_NUM_MESSAGES_TO_EMIT: &str = "num_messages_to_emit"; +const ARG_TOPIC_NAME: &str = "topic_name"; +const PACKAGE_HASH_KEY_NAME: &str = "messages_emitter_package_hash"; +const ACCESS_KEY_NAME: &str = "messages_emitter_access"; + +pub const MESSAGE_EMITTER_GENERIC_TOPIC: &str = "generic_messages"; +pub const MESSAGE_PREFIX: &str = "generic message: "; + +#[no_mangle] +pub extern "C" fn emit_message() { + let suffix: String = runtime::get_named_arg(ARG_MESSAGE_SUFFIX_NAME); + + runtime::emit_message( + MESSAGE_EMITTER_GENERIC_TOPIC, + &format!("{}{}", MESSAGE_PREFIX, suffix).into(), + ) + .unwrap_or_revert(); +} + +#[no_mangle] +pub extern "C" fn emit_multiple_messages() { + let num_messages: u32 = runtime::get_named_arg(ARG_NUM_MESSAGES_TO_EMIT); + + for i in 0..num_messages { + runtime::emit_message( + MESSAGE_EMITTER_GENERIC_TOPIC, + &format!("{}{}", MESSAGE_PREFIX, i).into(), + ) + .unwrap_or_revert(); + } +} + +#[no_mangle] +pub extern "C" fn add_topic() { + let topic_name: String = runtime::get_named_arg(ARG_TOPIC_NAME); + + runtime::manage_message_topic(topic_name.as_str(), MessageTopicOperation::Add) + .unwrap_or_revert(); +} + +#[no_mangle] +pub extern "C" fn init() { + if runtime::has_key(MESSAGE_EMITTER_INITIALIZED) { + runtime::revert(ApiError::User(0)); + } + + runtime::manage_message_topic(MESSAGE_EMITTER_GENERIC_TOPIC, MessageTopicOperation::Add) + .unwrap_or_revert(); + + runtime::put_key(MESSAGE_EMITTER_INITIALIZED, storage::new_uref(()).into()); +} + +#[no_mangle] +pub extern "C" fn call() { + let mut emitter_entry_points = EntryPoints::new(); + emitter_entry_points.add_entry_point(EntryPoint::new( + ENTRY_POINT_INIT, + Vec::new(), + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::AddressableEntity, + )); + emitter_entry_points.add_entry_point(EntryPoint::new( + ENTRY_POINT_EMIT_MESSAGE, + vec![Parameter::new(ARG_MESSAGE_SUFFIX_NAME, String::cl_type())], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::AddressableEntity, + )); + emitter_entry_points.add_entry_point(EntryPoint::new( + ENTRY_POINT_ADD_TOPIC, + vec![Parameter::new(ARG_TOPIC_NAME, String::cl_type())], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::AddressableEntity, + )); + emitter_entry_points.add_entry_point(EntryPoint::new( + ENTRY_POINT_EMIT_MULTIPLE_MESSAGES, + vec![Parameter::new(ARG_NUM_MESSAGES_TO_EMIT, u32::cl_type())], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::AddressableEntity, + )); + + let (stored_contract_hash, _contract_version) = storage::new_contract( + emitter_entry_points, + Some(NamedKeys::new()), + Some(PACKAGE_HASH_KEY_NAME.into()), + Some(ACCESS_KEY_NAME.into()), + ); + + // Call contract to initialize it + runtime::call_contract::<()>( + stored_contract_hash, + ENTRY_POINT_INIT, + RuntimeArgs::default(), + ); +} diff --git a/smart_contracts/contracts/test/contract-messages-from-account/Cargo.toml b/smart_contracts/contracts/test/contract-messages-from-account/Cargo.toml new file mode 100644 index 0000000000..a15fba294c --- /dev/null +++ b/smart_contracts/contracts/test/contract-messages-from-account/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "contract-messages-from-account" +version = "0.1.0" +authors = ["Alexandru Sardan "] +edition = "2018" + +[[bin]] +name = "contract_messages_from_account" +path = "src/main.rs" +bench = false +doctest = false +test = false + +[dependencies] +casper-contract = { path = "../../../contract" } +casper-types = { path = "../../../../types" } diff --git a/smart_contracts/contracts/test/contract-messages-from-account/src/main.rs b/smart_contracts/contracts/test/contract-messages-from-account/src/main.rs new file mode 100644 index 0000000000..c582143431 --- /dev/null +++ b/smart_contracts/contracts/test/contract-messages-from-account/src/main.rs @@ -0,0 +1,15 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +use casper_contract::{contract_api::runtime, unwrap_or_revert::UnwrapOrRevert}; + +use casper_types::contract_messages::MessageTopicOperation; + +const TOPIC_NAME: &str = "messages_topic"; + +#[no_mangle] +pub extern "C" fn call() { + runtime::manage_message_topic(TOPIC_NAME, MessageTopicOperation::Add).unwrap_or_revert(); +} diff --git a/smart_contracts/contracts/test/contract-messages-upgrader/Cargo.toml b/smart_contracts/contracts/test/contract-messages-upgrader/Cargo.toml new file mode 100644 index 0000000000..7db22af9b7 --- /dev/null +++ b/smart_contracts/contracts/test/contract-messages-upgrader/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "contract-messages-upgrader" +version = "0.1.0" +authors = ["Alexandru Sardan "] +edition = "2018" + +[[bin]] +name = "contract_messages_upgrader" +path = "src/main.rs" +bench = false +doctest = false +test = false + +[dependencies] +casper-contract = { path = "../../../contract" } +casper-types = { path = "../../../../types" } diff --git a/smart_contracts/contracts/test/contract-messages-upgrader/src/main.rs b/smart_contracts/contracts/test/contract-messages-upgrader/src/main.rs new file mode 100644 index 0000000000..389ba745b0 --- /dev/null +++ b/smart_contracts/contracts/test/contract-messages-upgrader/src/main.rs @@ -0,0 +1,136 @@ +#![no_std] +#![no_main] + +#[macro_use] +extern crate alloc; +use alloc::{string::String, vec::Vec}; + +use casper_contract::{ + contract_api::{runtime, storage}, + unwrap_or_revert::UnwrapOrRevert, +}; + +use casper_types::{ + addressable_entity::{EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, NamedKeys}, + api_error::ApiError, + contract_messages::MessageTopicOperation, + runtime_args, CLType, CLTyped, PackageHash, Parameter, RuntimeArgs, +}; + +const ENTRY_POINT_INIT: &str = "init"; +const FIRST_VERSION_ENTRY_POINT_EMIT_MESSAGE: &str = "emit_message"; +const ENTRY_POINT_EMIT_MESSAGE: &str = "upgraded_emit_message"; +const ENTRY_POINT_EMIT_MESSAGE_FROM_EACH_VERSION: &str = "emit_message_from_each_version"; +const UPGRADED_MESSAGE_EMITTER_INITIALIZED: &str = "upgraded_message_emitter_initialized"; +const ARG_MESSAGE_SUFFIX_NAME: &str = "message_suffix"; +const PACKAGE_HASH_KEY_NAME: &str = "messages_emitter_package_hash"; + +pub const MESSAGE_EMITTER_GENERIC_TOPIC: &str = "new_topic_after_upgrade"; +pub const MESSAGE_PREFIX: &str = "generic message: "; + +#[no_mangle] +pub extern "C" fn upgraded_emit_message() { + let suffix: String = runtime::get_named_arg(ARG_MESSAGE_SUFFIX_NAME); + + runtime::emit_message( + MESSAGE_EMITTER_GENERIC_TOPIC, + &format!("{}{}", MESSAGE_PREFIX, suffix).into(), + ) + .unwrap_or_revert(); +} + +#[no_mangle] +pub extern "C" fn emit_message_from_each_version() { + let suffix: String = runtime::get_named_arg(ARG_MESSAGE_SUFFIX_NAME); + + let contract_package_hash: PackageHash = runtime::get_key(PACKAGE_HASH_KEY_NAME) + .expect("should have contract package key") + .into_package_addr() + .unwrap_or_revert() + .into(); + + // Emit a message from this contract. + runtime::emit_message( + MESSAGE_EMITTER_GENERIC_TOPIC, + &"emitting multiple messages".into(), + ) + .unwrap_or_revert(); + + // Call previous contract version which will emit a message. + runtime::call_versioned_contract::<()>( + contract_package_hash, + Some(1), + FIRST_VERSION_ENTRY_POINT_EMIT_MESSAGE, + runtime_args! { + ARG_MESSAGE_SUFFIX_NAME => suffix.clone(), + }, + ); + + // Emit another message from this version. + runtime::emit_message( + MESSAGE_EMITTER_GENERIC_TOPIC, + &format!("{}{}", MESSAGE_PREFIX, suffix).into(), + ) + .unwrap_or_revert(); +} + +#[no_mangle] +pub extern "C" fn init() { + if runtime::has_key(UPGRADED_MESSAGE_EMITTER_INITIALIZED) { + runtime::revert(ApiError::User(0)); + } + + runtime::manage_message_topic(MESSAGE_EMITTER_GENERIC_TOPIC, MessageTopicOperation::Add) + .unwrap_or_revert(); + + runtime::put_key( + UPGRADED_MESSAGE_EMITTER_INITIALIZED, + storage::new_uref(()).into(), + ); +} + +#[no_mangle] +pub extern "C" fn call() { + let mut emitter_entry_points = EntryPoints::new(); + emitter_entry_points.add_entry_point(EntryPoint::new( + ENTRY_POINT_INIT, + Vec::new(), + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::AddressableEntity, + )); + emitter_entry_points.add_entry_point(EntryPoint::new( + ENTRY_POINT_EMIT_MESSAGE, + vec![Parameter::new(ARG_MESSAGE_SUFFIX_NAME, String::cl_type())], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::AddressableEntity, + )); + emitter_entry_points.add_entry_point(EntryPoint::new( + ENTRY_POINT_EMIT_MESSAGE_FROM_EACH_VERSION, + vec![Parameter::new(ARG_MESSAGE_SUFFIX_NAME, String::cl_type())], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::AddressableEntity, + )); + + let message_emitter_package_hash: PackageHash = runtime::get_key(PACKAGE_HASH_KEY_NAME) + .unwrap_or_revert() + .into_package_addr() + .unwrap() + .into(); + + let mut named_keys = NamedKeys::new(); + named_keys.insert( + PACKAGE_HASH_KEY_NAME.into(), + message_emitter_package_hash.into(), + ); + let (contract_hash, _contract_version) = storage::add_contract_version( + message_emitter_package_hash, + emitter_entry_points, + named_keys, + ); + + // Call contract to initialize it + runtime::call_contract::<()>(contract_hash, ENTRY_POINT_INIT, RuntimeArgs::default()); +} diff --git a/types/Cargo.toml b/types/Cargo.toml index 8891c0e5c6..de567fbe5e 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -47,6 +47,7 @@ tracing = { version = "0.1.37", default-features = false } uint = { version = "0.9.0", default-features = false } untrusted = { version = "0.7.1", optional = true } version-sync = { version = "0.9", optional = true } +derive_more = "0.99.17" [dev-dependencies] base16 = { version = "0.2.1", features = ["std"] } diff --git a/types/benches/bytesrepr_bench.rs b/types/benches/bytesrepr_bench.rs index dff857cefd..faec8cee4a 100644 --- a/types/benches/bytesrepr_bench.rs +++ b/types/benches/bytesrepr_bench.rs @@ -7,7 +7,9 @@ use std::{ use casper_types::{ account::AccountHash, - addressable_entity::{ActionThresholds, AddressableEntity, AssociatedKeys, NamedKeys}, + addressable_entity::{ + ActionThresholds, AddressableEntity, AssociatedKeys, MessageTopics, NamedKeys, + }, bytesrepr::{self, Bytes, FromBytes, ToBytes}, package::{PackageKind, PackageStatus}, system::auction::{Bid, Delegator, EraInfo, SeigniorageAllocation}, @@ -502,6 +504,7 @@ fn sample_contract(named_keys_len: u8, entry_points_len: u8) -> AddressableEntit URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), ) } diff --git a/types/src/addressable_entity.rs b/types/src/addressable_entity.rs index 15fdc839ca..9ea6d671fc 100644 --- a/types/src/addressable_entity.rs +++ b/types/src/addressable_entity.rs @@ -10,7 +10,7 @@ mod named_keys; mod weight; use alloc::{ - collections::{BTreeMap, BTreeSet}, + collections::{btree_map::Entry, BTreeMap, BTreeSet}, format, string::{String, ToString}, vec::Vec, @@ -26,6 +26,10 @@ use num_traits::FromPrimitive; #[cfg(feature = "datasize")] use datasize::DataSize; +use rand::{ + distributions::{Distribution, Standard}, + Rng, +}; #[cfg(feature = "json-schema")] use schemars::JsonSchema; use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; @@ -50,6 +54,7 @@ use crate::{ byte_code::ByteCodeHash, bytesrepr::{self, FromBytes, ToBytes}, checksummed_hex, + contract_messages::TopicNameHash, contracts::{Contract, ContractHash}, key::ByteCodeAddr, uref::{self, URef}, @@ -385,6 +390,12 @@ impl TryFrom<&Vec> for AddressableEntityHash { } } +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> AddressableEntityHash { + AddressableEntityHash(rng.gen()) + } +} + /// Errors that can occur while adding a new [`AccountHash`] to an account's associated keys map. #[derive(PartialEq, Eq, Debug, Copy, Clone)] #[repr(i32)] @@ -647,6 +658,113 @@ impl KeyValueJsonSchema for EntryPointLabels { const JSON_SCHEMA_KV_NAME: Option<&'static str> = Some("NamedEntryPoint"); } +/// Collection of named message topics. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Default)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(transparent, deny_unknown_fields)] +pub struct MessageTopics( + #[serde(with = "BTreeMapToArray::")] + BTreeMap, +); + +impl ToBytes for MessageTopics { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + self.0.to_bytes() + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.0.write_bytes(writer) + } +} + +impl FromBytes for MessageTopics { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (message_topics_map, remainder) = BTreeMap::::from_bytes(bytes)?; + Ok((MessageTopics(message_topics_map), remainder)) + } +} + +impl MessageTopics { + /// Adds new message topic by topic name. + pub fn add_topic( + &mut self, + topic_name: &str, + topic_name_hash: TopicNameHash, + ) -> Result<(), MessageTopicError> { + if self.0.len() >= u32::MAX as usize { + return Err(MessageTopicError::MaxTopicsExceeded); + } + + match self.0.entry(topic_name.to_string()) { + Entry::Vacant(entry) => { + entry.insert(topic_name_hash); + Ok(()) + } + Entry::Occupied(_) => Err(MessageTopicError::DuplicateTopic), + } + } + + /// Checks if given topic name exists. + pub fn has_topic(&self, topic_name: &str) -> bool { + self.0.contains_key(topic_name) + } + + /// Gets the topic hash from the collection by its topic name. + pub fn get(&self, topic_name: &str) -> Option<&TopicNameHash> { + self.0.get(topic_name) + } + + /// Returns the length of the message topics. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns true if no message topics are registered. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns an iterator over the topic name and its hash. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + +struct MessageTopicLabels; + +impl KeyValueLabels for MessageTopicLabels { + const KEY: &'static str = "topic_name"; + const VALUE: &'static str = "topic_name_hash"; +} + +#[cfg(feature = "json-schema")] +impl KeyValueJsonSchema for MessageTopicLabels { + const JSON_SCHEMA_KV_NAME: Option<&'static str> = Some("MessageTopic"); +} + +impl From> for MessageTopics { + fn from(topics: BTreeMap) -> MessageTopics { + MessageTopics(topics) + } +} + +/// Errors that can occur while adding a new topic. +#[derive(PartialEq, Eq, Debug, Clone)] +#[non_exhaustive] +pub enum MessageTopicError { + /// Topic already exists. + DuplicateTopic, + /// Maximum number of topics exceeded. + MaxTopicsExceeded, + /// Topic name size exceeded. + TopicNameSizeExceeded, +} + /// Methods and type signatures supported by a contract. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] @@ -660,6 +778,7 @@ pub struct AddressableEntity { main_purse: URef, associated_keys: AssociatedKeys, action_thresholds: ActionThresholds, + message_topics: MessageTopics, } impl From @@ -700,6 +819,7 @@ impl AddressableEntity { main_purse: URef, associated_keys: AssociatedKeys, action_thresholds: ActionThresholds, + message_topics: MessageTopics, ) -> Self { AddressableEntity { package_hash, @@ -710,6 +830,7 @@ impl AddressableEntity { main_purse, action_thresholds, associated_keys, + message_topics, } } @@ -925,6 +1046,20 @@ impl AddressableEntity { &self.entry_points } + /// Returns a reference to the message topics + pub fn message_topics(&self) -> &MessageTopics { + &self.message_topics + } + + /// Adds a new message topic to the entity + pub fn add_message_topic( + &mut self, + topic_name: &str, + topic_name_hash: TopicNameHash, + ) -> Result<(), MessageTopicError> { + self.message_topics.add_topic(topic_name, topic_name_hash) + } + /// Takes `named_keys` pub fn take_named_keys(self) -> NamedKeys { self.named_keys @@ -980,6 +1115,7 @@ impl AddressableEntity { main_purse: self.main_purse, associated_keys: self.associated_keys, action_thresholds: self.action_thresholds, + message_topics: self.message_topics, } } } @@ -995,6 +1131,7 @@ impl ToBytes for AddressableEntity { self.main_purse().write_bytes(&mut result)?; self.associated_keys().write_bytes(&mut result)?; self.action_thresholds().write_bytes(&mut result)?; + self.message_topics().write_bytes(&mut result)?; Ok(result) } @@ -1007,6 +1144,7 @@ impl ToBytes for AddressableEntity { + ToBytes::serialized_length(&self.main_purse) + ToBytes::serialized_length(&self.associated_keys) + ToBytes::serialized_length(&self.action_thresholds) + + ToBytes::serialized_length(&self.message_topics) } fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { @@ -1018,6 +1156,7 @@ impl ToBytes for AddressableEntity { self.main_purse().write_bytes(writer)?; self.associated_keys().write_bytes(writer)?; self.action_thresholds().write_bytes(writer)?; + self.message_topics().write_bytes(writer)?; Ok(()) } } @@ -1032,6 +1171,7 @@ impl FromBytes for AddressableEntity { let (main_purse, bytes) = URef::from_bytes(bytes)?; let (associated_keys, bytes) = AssociatedKeys::from_bytes(bytes)?; let (action_thresholds, bytes) = ActionThresholds::from_bytes(bytes)?; + let (message_topics, bytes) = MessageTopics::from_bytes(bytes)?; Ok(( AddressableEntity { package_hash, @@ -1042,6 +1182,7 @@ impl FromBytes for AddressableEntity { main_purse, associated_keys, action_thresholds, + message_topics, }, bytes, )) @@ -1059,6 +1200,7 @@ impl Default for AddressableEntity { main_purse: URef::default(), action_thresholds: ActionThresholds::default(), associated_keys: AssociatedKeys::default(), + message_topics: MessageTopics::default(), } } } @@ -1074,6 +1216,7 @@ impl From for AddressableEntity { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), ) } } @@ -1089,6 +1232,7 @@ impl From for AddressableEntity { value.main_purse(), value.associated_keys().clone().into(), value.action_thresholds().clone().into(), + MessageTopics::default(), ) } } @@ -1540,6 +1684,7 @@ mod tests { associated_keys, ActionThresholds::new(Weight::new(1), Weight::new(1), Weight::new(1)) .expect("should create thresholds"), + MessageTopics::default(), ); let access_rights = contract.extract_access_rights(entity_hash); let expected_uref = URef::new([42; UREF_ADDR_LENGTH], AccessRights::READ_ADD_WRITE); diff --git a/types/src/api_error.rs b/types/src/api_error.rs index d7de342b99..9efe7716f3 100644 --- a/types/src/api_error.rs +++ b/types/src/api_error.rs @@ -7,8 +7,8 @@ use core::{ use crate::{ addressable_entity::{ - self, AddKeyFailure, RemoveKeyFailure, SetThresholdFailure, TryFromIntError, - TryFromSliceForAccountHashError, UpdateKeyFailure, + self, AddKeyFailure, MessageTopicError, RemoveKeyFailure, SetThresholdFailure, + TryFromIntError, TryFromSliceForAccountHashError, UpdateKeyFailure, }, bytesrepr, system::{auction, handle_payment, mint}, @@ -396,6 +396,42 @@ pub enum ApiError { /// } /// ``` User(u16), + /// The message topic is already registered. + /// ``` + /// # use casper_types::ApiError; + /// assert_eq!(ApiError::from(41), ApiError::MessageTopicAlreadyRegistered); + /// ``` + MessageTopicAlreadyRegistered, + /// The maximum number of allowed message topics was exceeded. + /// ``` + /// # use casper_types::ApiError; + /// assert_eq!(ApiError::from(42), ApiError::MaxTopicsNumberExceeded); + /// ``` + MaxTopicsNumberExceeded, + /// The maximum size for the topic name was exceeded. + /// ``` + /// # use casper_types::ApiError; + /// assert_eq!(ApiError::from(43), ApiError::MaxTopicNameSizeExceeded); + /// ``` + MaxTopicNameSizeExceeded, + /// The message topic is not registered. + /// ``` + /// # use casper_types::ApiError; + /// assert_eq!(ApiError::from(44), ApiError::MessageTopicNotRegistered); + /// ``` + MessageTopicNotRegistered, + /// The message topic is full and cannot accept new messages. + /// ``` + /// # use casper_types::ApiError; + /// assert_eq!(ApiError::from(45), ApiError::MessageTopicFull); + /// ``` + MessageTopicFull, + /// The message topic is full and cannot accept new messages. + /// ``` + /// # use casper_types::ApiError; + /// assert_eq!(ApiError::from(46), ApiError::MessageTooLarge); + /// ``` + MessageTooLarge, } impl From for ApiError { @@ -499,6 +535,16 @@ impl From for ApiError { } } +impl From for ApiError { + fn from(error: MessageTopicError) -> Self { + match error { + MessageTopicError::DuplicateTopic => ApiError::MessageTopicAlreadyRegistered, + MessageTopicError::MaxTopicsExceeded => ApiError::MaxTopicsNumberExceeded, + MessageTopicError::TopicNameSizeExceeded => ApiError::MaxTopicNameSizeExceeded, + } + } +} + impl From for u32 { fn from(error: ApiError) -> Self { match error { @@ -542,6 +588,12 @@ impl From for u32 { ApiError::MissingSystemContractHash => 38, ApiError::ExceededRecursionDepth => 39, ApiError::NonRepresentableSerialization => 40, + ApiError::MessageTopicAlreadyRegistered => 41, + ApiError::MaxTopicsNumberExceeded => 42, + ApiError::MaxTopicNameSizeExceeded => 43, + ApiError::MessageTopicNotRegistered => 44, + ApiError::MessageTopicFull => 45, + ApiError::MessageTooLarge => 46, ApiError::AuctionError(value) => AUCTION_ERROR_OFFSET + u32::from(value), ApiError::ContractHeader(value) => HEADER_ERROR_OFFSET + u32::from(value), ApiError::Mint(value) => MINT_ERROR_OFFSET + u32::from(value), @@ -594,6 +646,12 @@ impl From for ApiError { 38 => ApiError::MissingSystemContractHash, 39 => ApiError::ExceededRecursionDepth, 40 => ApiError::NonRepresentableSerialization, + 41 => ApiError::MessageTopicAlreadyRegistered, + 42 => ApiError::MaxTopicsNumberExceeded, + 43 => ApiError::MaxTopicNameSizeExceeded, + 44 => ApiError::MessageTopicNotRegistered, + 45 => ApiError::MessageTopicFull, + 46 => ApiError::MessageTooLarge, USER_ERROR_MIN..=USER_ERROR_MAX => ApiError::User(value as u16), HP_ERROR_MIN..=HP_ERROR_MAX => ApiError::HandlePayment(value as u8), MINT_ERROR_MIN..=MINT_ERROR_MAX => ApiError::Mint(value as u8), @@ -652,6 +710,16 @@ impl Debug for ApiError { ApiError::NonRepresentableSerialization => { write!(f, "ApiError::NonRepresentableSerialization")? } + ApiError::MessageTopicAlreadyRegistered => { + write!(f, "ApiError::MessageTopicAlreadyRegistered")? + } + ApiError::MaxTopicsNumberExceeded => write!(f, "ApiError::MaxTopicsNumberExceeded")?, + ApiError::MaxTopicNameSizeExceeded => write!(f, "ApiError::MaxTopicNameSizeExceeded")?, + ApiError::MessageTopicNotRegistered => { + write!(f, "ApiError::MessageTopicNotRegistered")? + } + ApiError::MessageTopicFull => write!(f, "ApiError::MessageTopicFull")?, + ApiError::MessageTooLarge => write!(f, "ApiError::MessageTooLarge")?, ApiError::ExceededRecursionDepth => write!(f, "ApiError::ExceededRecursionDepth")?, ApiError::AuctionError(value) => write!( f, @@ -871,5 +939,11 @@ mod tests { round_trip(Err(ApiError::User(u16::MAX))); round_trip(Err(ApiError::AuctionError(0))); round_trip(Err(ApiError::AuctionError(u8::MAX))); + round_trip(Err(ApiError::MessageTopicAlreadyRegistered)); + round_trip(Err(ApiError::MaxTopicsNumberExceeded)); + round_trip(Err(ApiError::MaxTopicNameSizeExceeded)); + round_trip(Err(ApiError::MessageTopicNotRegistered)); + round_trip(Err(ApiError::MessageTopicFull)); + round_trip(Err(ApiError::MessageTooLarge)); } } diff --git a/types/src/block_time.rs b/types/src/block_time.rs index 4122f7ca94..f278a36b87 100644 --- a/types/src/block_time.rs +++ b/types/src/block_time.rs @@ -2,11 +2,19 @@ use alloc::vec::Vec; use crate::bytesrepr::{Error, FromBytes, ToBytes, U64_SERIALIZED_LENGTH}; +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + /// The number of bytes in a serialized [`BlockTime`]. pub const BLOCKTIME_SERIALIZED_LENGTH: usize = U64_SERIALIZED_LENGTH; /// A newtype wrapping a [`u64`] which represents the block time. -#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] pub struct BlockTime(u64); impl BlockTime { diff --git a/types/src/chainspec.rs b/types/src/chainspec.rs index 4d802253c2..6e8727c8ff 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -47,7 +47,7 @@ pub use transaction_config::{DeployConfig, TransactionConfig, TransactionV1Confi pub use transaction_config::{DEFAULT_MAX_PAYMENT_MOTES, DEFAULT_MIN_TRANSFER_MOTES}; pub use vm_config::{ AuctionCosts, BrTableCost, ChainspecRegistry, ControlFlowCosts, HandlePaymentCosts, - HostFunction, HostFunctionCost, HostFunctionCosts, MintCosts, OpcodeCosts, + HostFunction, HostFunctionCost, HostFunctionCosts, MessageLimits, MintCosts, OpcodeCosts, StandardPaymentCosts, StorageCosts, SystemConfig, UpgradeConfig, WasmConfig, DEFAULT_HOST_FUNCTION_NEW_DICTIONARY, }; diff --git a/types/src/chainspec/vm_config.rs b/types/src/chainspec/vm_config.rs index ff934a33d5..34bb856e63 100644 --- a/types/src/chainspec/vm_config.rs +++ b/types/src/chainspec/vm_config.rs @@ -2,6 +2,7 @@ mod auction_costs; mod chainspec_registry; mod handle_payment_costs; mod host_function_costs; +mod message_limits; mod mint_costs; mod opcode_costs; mod standard_payment_costs; @@ -17,6 +18,7 @@ pub use host_function_costs::{ Cost as HostFunctionCost, HostFunction, HostFunctionCosts, DEFAULT_HOST_FUNCTION_NEW_DICTIONARY, DEFAULT_NEW_DICTIONARY_COST, }; +pub use message_limits::MessageLimits; pub use mint_costs::{MintCosts, DEFAULT_TRANSFER_COST}; pub use opcode_costs::{BrTableCost, ControlFlowCosts, OpcodeCosts}; #[cfg(any(feature = "testing", test))] diff --git a/types/src/chainspec/vm_config/host_function_costs.rs b/types/src/chainspec/vm_config/host_function_costs.rs index c08dc14e74..c536fa762e 100644 --- a/types/src/chainspec/vm_config/host_function_costs.rs +++ b/types/src/chainspec/vm_config/host_function_costs.rs @@ -1,6 +1,10 @@ //! Support for host function gas cost tables. +use core::ops::Add; + #[cfg(feature = "datasize")] use datasize::DataSize; +use derive_more::Add; +use num_traits::Zero; use rand::{distributions::Standard, prelude::Distribution, Rng}; use serde::{Deserialize, Serialize}; @@ -84,6 +88,10 @@ pub const DEFAULT_NEW_DICTIONARY_COST: u32 = DEFAULT_NEW_UREF_COST; pub const DEFAULT_HOST_FUNCTION_NEW_DICTIONARY: HostFunction<[Cost; 1]> = HostFunction::new(DEFAULT_NEW_DICTIONARY_COST, [NOT_USED]); +/// Default value that the cost of calling `casper_emit_message` increases by for every new message +/// emitted within an execution. +pub const DEFAULT_COST_INCREASE_PER_MESSAGE_EMITTED: u32 = 50; + /// Representation of a host function cost. /// /// The total gas cost is equal to `cost` + sum of each argument weight multiplied by the byte size @@ -153,6 +161,28 @@ where } } +impl Add for HostFunction<[Cost; COUNT]> { + type Output = HostFunction<[Cost; COUNT]>; + + fn add(self, rhs: Self) -> Self::Output { + let mut result = HostFunction::new(self.cost + rhs.cost, [0; COUNT]); + for i in 0..COUNT { + result.arguments[i] = self.arguments[i] + rhs.arguments[i]; + } + result + } +} + +impl Zero for HostFunction<[Cost; COUNT]> { + fn zero() -> Self { + HostFunction::new(0, [0; COUNT]) + } + + fn is_zero(&self) -> bool { + !self.arguments.iter().any(|cost| *cost != 0) && self.cost.is_zero() + } +} + impl Distribution> for Standard where Standard: Distribution, @@ -201,10 +231,12 @@ where } /// Definition of a host function cost table. -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +#[derive(Add, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[serde(deny_unknown_fields)] pub struct HostFunctionCosts { + /// Cost increase for successive calls to `casper_emit_message` within an execution. + pub cost_increase_per_message: u32, /// Cost of calling the `read_value` host function. pub read_value: HostFunction<[Cost; 3]>, /// Cost of calling the `dictionary_get` host function. @@ -297,6 +329,166 @@ pub struct HostFunctionCosts { pub enable_contract_version: HostFunction<[Cost; 4]>, /// Cost of calling the `add_session_version` host function. pub add_session_version: HostFunction<[Cost; 2]>, + /// Cost of calling the `casper_manage_message_topic` host function. + pub manage_message_topic: HostFunction<[Cost; 4]>, + /// Cost of calling the `casper_emit_message` host function. + pub emit_message: HostFunction<[Cost; 4]>, +} + +impl Zero for HostFunctionCosts { + fn zero() -> Self { + Self { + read_value: HostFunction::zero(), + dictionary_get: HostFunction::zero(), + write: HostFunction::zero(), + dictionary_put: HostFunction::zero(), + add: HostFunction::zero(), + new_uref: HostFunction::zero(), + load_named_keys: HostFunction::zero(), + ret: HostFunction::zero(), + get_key: HostFunction::zero(), + has_key: HostFunction::zero(), + put_key: HostFunction::zero(), + remove_key: HostFunction::zero(), + revert: HostFunction::zero(), + is_valid_uref: HostFunction::zero(), + add_associated_key: HostFunction::zero(), + remove_associated_key: HostFunction::zero(), + update_associated_key: HostFunction::zero(), + set_action_threshold: HostFunction::zero(), + get_caller: HostFunction::zero(), + get_blocktime: HostFunction::zero(), + create_purse: HostFunction::zero(), + transfer_to_account: HostFunction::zero(), + transfer_from_purse_to_account: HostFunction::zero(), + transfer_from_purse_to_purse: HostFunction::zero(), + get_balance: HostFunction::zero(), + get_phase: HostFunction::zero(), + get_system_contract: HostFunction::zero(), + get_main_purse: HostFunction::zero(), + read_host_buffer: HostFunction::zero(), + create_contract_package_at_hash: HostFunction::zero(), + create_contract_user_group: HostFunction::zero(), + add_contract_version: HostFunction::zero(), + disable_contract_version: HostFunction::zero(), + call_contract: HostFunction::zero(), + call_versioned_contract: HostFunction::zero(), + get_named_arg_size: HostFunction::zero(), + get_named_arg: HostFunction::zero(), + remove_contract_user_group: HostFunction::zero(), + provision_contract_user_group_uref: HostFunction::zero(), + remove_contract_user_group_urefs: HostFunction::zero(), + print: HostFunction::zero(), + blake2b: HostFunction::zero(), + random_bytes: HostFunction::zero(), + enable_contract_version: HostFunction::zero(), + add_session_version: HostFunction::zero(), + manage_message_topic: HostFunction::zero(), + emit_message: HostFunction::zero(), + cost_increase_per_message: Zero::zero(), + } + } + + fn is_zero(&self) -> bool { + let HostFunctionCosts { + cost_increase_per_message, + read_value, + dictionary_get, + write, + dictionary_put, + add, + new_uref, + load_named_keys, + ret, + get_key, + has_key, + put_key, + remove_key, + revert, + is_valid_uref, + add_associated_key, + remove_associated_key, + update_associated_key, + set_action_threshold, + get_caller, + get_blocktime, + create_purse, + transfer_to_account, + transfer_from_purse_to_account, + transfer_from_purse_to_purse, + get_balance, + get_phase, + get_system_contract, + get_main_purse, + read_host_buffer, + create_contract_package_at_hash, + create_contract_user_group, + add_contract_version, + disable_contract_version, + call_contract, + call_versioned_contract, + get_named_arg_size, + get_named_arg, + remove_contract_user_group, + provision_contract_user_group_uref, + remove_contract_user_group_urefs, + print, + blake2b, + random_bytes, + enable_contract_version, + add_session_version, + manage_message_topic, + emit_message, + } = self; + read_value.is_zero() + && dictionary_get.is_zero() + && write.is_zero() + && dictionary_put.is_zero() + && add.is_zero() + && new_uref.is_zero() + && load_named_keys.is_zero() + && ret.is_zero() + && get_key.is_zero() + && has_key.is_zero() + && put_key.is_zero() + && remove_key.is_zero() + && revert.is_zero() + && is_valid_uref.is_zero() + && add_associated_key.is_zero() + && remove_associated_key.is_zero() + && update_associated_key.is_zero() + && set_action_threshold.is_zero() + && get_caller.is_zero() + && get_blocktime.is_zero() + && create_purse.is_zero() + && transfer_to_account.is_zero() + && transfer_from_purse_to_account.is_zero() + && transfer_from_purse_to_purse.is_zero() + && get_balance.is_zero() + && get_phase.is_zero() + && get_system_contract.is_zero() + && get_main_purse.is_zero() + && read_host_buffer.is_zero() + && create_contract_package_at_hash.is_zero() + && create_contract_user_group.is_zero() + && add_contract_version.is_zero() + && disable_contract_version.is_zero() + && call_contract.is_zero() + && call_versioned_contract.is_zero() + && get_named_arg_size.is_zero() + && get_named_arg.is_zero() + && remove_contract_user_group.is_zero() + && provision_contract_user_group_uref.is_zero() + && remove_contract_user_group_urefs.is_zero() + && print.is_zero() + && blake2b.is_zero() + && random_bytes.is_zero() + && enable_contract_version.is_zero() + && add_session_version.is_zero() + && manage_message_topic.is_zero() + && emit_message.is_zero() + && cost_increase_per_message.is_zero() + } } impl Default for HostFunctionCosts { @@ -430,6 +622,9 @@ impl Default for HostFunctionCosts { random_bytes: HostFunction::default(), enable_contract_version: HostFunction::default(), add_session_version: HostFunction::default(), + manage_message_topic: HostFunction::default(), + emit_message: HostFunction::default(), + cost_increase_per_message: DEFAULT_COST_INCREASE_PER_MESSAGE_EMITTED, } } } @@ -482,6 +677,9 @@ impl ToBytes for HostFunctionCosts { ret.append(&mut self.random_bytes.to_bytes()?); ret.append(&mut self.enable_contract_version.to_bytes()?); ret.append(&mut self.add_session_version.to_bytes()?); + ret.append(&mut self.manage_message_topic.to_bytes()?); + ret.append(&mut self.emit_message.to_bytes()?); + ret.append(&mut self.cost_increase_per_message.to_bytes()?); Ok(ret) } @@ -531,6 +729,9 @@ impl ToBytes for HostFunctionCosts { + self.random_bytes.serialized_length() + self.enable_contract_version.serialized_length() + self.add_session_version.serialized_length() + + self.manage_message_topic.serialized_length() + + self.emit_message.serialized_length() + + self.cost_increase_per_message.serialized_length() } } @@ -581,6 +782,9 @@ impl FromBytes for HostFunctionCosts { let (random_bytes, rem) = FromBytes::from_bytes(rem)?; let (enable_contract_version, rem) = FromBytes::from_bytes(rem)?; let (add_session_version, rem) = FromBytes::from_bytes(rem)?; + let (manage_message_topic, rem) = FromBytes::from_bytes(rem)?; + let (emit_message, rem) = FromBytes::from_bytes(rem)?; + let (cost_increase_per_message, rem) = FromBytes::from_bytes(rem)?; Ok(( HostFunctionCosts { read_value, @@ -628,6 +832,9 @@ impl FromBytes for HostFunctionCosts { random_bytes, enable_contract_version, add_session_version, + manage_message_topic, + emit_message, + cost_increase_per_message, }, rem, )) @@ -682,6 +889,9 @@ impl Distribution for Standard { random_bytes: rng.gen(), enable_contract_version: rng.gen(), add_session_version: rng.gen(), + manage_message_topic: rng.gen(), + emit_message: rng.gen(), + cost_increase_per_message: rng.gen(), } } } @@ -689,7 +899,7 @@ impl Distribution for Standard { #[doc(hidden)] #[cfg(any(feature = "gens", test))] pub mod gens { - use proptest::prelude::*; + use proptest::{num, prelude::*}; use crate::{HostFunction, HostFunctionCost, HostFunctionCosts}; @@ -746,6 +956,9 @@ pub mod gens { random_bytes in host_function_cost_arb(), enable_contract_version in host_function_cost_arb(), add_session_version in host_function_cost_arb(), + manage_message_topic in host_function_cost_arb(), + emit_message in host_function_cost_arb(), + cost_increase_per_message in num::u32::ANY, ) -> HostFunctionCosts { HostFunctionCosts { read_value, @@ -792,7 +1005,10 @@ pub mod gens { blake2b, random_bytes, enable_contract_version, - add_session_version + add_session_version, + manage_message_topic, + emit_message, + cost_increase_per_message, } } } diff --git a/types/src/chainspec/vm_config/message_limits.rs b/types/src/chainspec/vm_config/message_limits.rs new file mode 100644 index 0000000000..9363515357 --- /dev/null +++ b/types/src/chainspec/vm_config/message_limits.rs @@ -0,0 +1,131 @@ +#[cfg(feature = "datasize")] +use datasize::DataSize; +use rand::{distributions::Standard, prelude::*, Rng}; +use serde::{Deserialize, Serialize}; + +use crate::bytesrepr::{self, FromBytes, ToBytes}; + +/// Configuration for messages limits. +#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[serde(deny_unknown_fields)] +pub struct MessageLimits { + /// Maximum size (in bytes) of a topic name string. + pub max_topic_name_size: u32, + /// Maximum message size in bytes. + pub max_message_size: u32, + /// Maximum number of topics that a contract can register. + pub max_topics_per_contract: u32, +} + +impl MessageLimits { + /// Returns the max number of topics a contract can register. + pub fn max_topics_per_contract(&self) -> u32 { + self.max_topics_per_contract + } + + /// Returns the maximum allowed size for the topic name string. + pub fn max_topic_name_size(&self) -> u32 { + self.max_topic_name_size + } + + /// Returns the maximum allowed size (in bytes) of the serialized message payload. + pub fn max_message_size(&self) -> u32 { + self.max_message_size + } +} + +impl Default for MessageLimits { + fn default() -> Self { + Self { + max_topic_name_size: 256, + max_message_size: 1024, + max_topics_per_contract: 128, + } + } +} + +impl ToBytes for MessageLimits { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut ret = bytesrepr::unchecked_allocate_buffer(self); + + ret.append(&mut self.max_topic_name_size.to_bytes()?); + ret.append(&mut self.max_message_size.to_bytes()?); + ret.append(&mut self.max_topics_per_contract.to_bytes()?); + + Ok(ret) + } + + fn serialized_length(&self) -> usize { + self.max_topic_name_size.serialized_length() + + self.max_message_size.serialized_length() + + self.max_topics_per_contract.serialized_length() + } +} + +impl FromBytes for MessageLimits { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (max_topic_name_size, rem) = FromBytes::from_bytes(bytes)?; + let (max_message_size, rem) = FromBytes::from_bytes(rem)?; + let (max_topics_per_contract, rem) = FromBytes::from_bytes(rem)?; + + Ok(( + MessageLimits { + max_topic_name_size, + max_message_size, + max_topics_per_contract, + }, + rem, + )) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> MessageLimits { + MessageLimits { + max_topic_name_size: rng.gen(), + max_message_size: rng.gen(), + max_topics_per_contract: rng.gen(), + } + } +} + +#[doc(hidden)] +#[cfg(any(feature = "gens", test))] +pub mod gens { + use proptest::{num, prop_compose}; + + use super::MessageLimits; + + prop_compose! { + pub fn message_limits_arb()( + max_topic_name_size in num::u32::ANY, + max_message_size in num::u32::ANY, + max_topics_per_contract in num::u32::ANY, + ) -> MessageLimits { + MessageLimits { + max_topic_name_size, + max_message_size, + max_topics_per_contract, + } + } + } +} + +#[cfg(test)] +mod tests { + use proptest::proptest; + + use crate::bytesrepr; + + use super::gens; + + proptest! { + #[test] + fn should_serialize_and_deserialize_with_arbitrary_values( + message_limits in gens::message_limits_arb() + ) { + bytesrepr::test_serialization_roundtrip(&message_limits); + } + } +} diff --git a/types/src/chainspec/vm_config/opcode_costs.rs b/types/src/chainspec/vm_config/opcode_costs.rs index e7bfa2cd12..19c00a07cf 100644 --- a/types/src/chainspec/vm_config/opcode_costs.rs +++ b/types/src/chainspec/vm_config/opcode_costs.rs @@ -1,6 +1,8 @@ //! Support for Wasm opcode costs. #[cfg(feature = "datasize")] use datasize::DataSize; +use derive_more::Add; +use num_traits::Zero; use rand::{distributions::Standard, prelude::*, Rng}; use serde::{Deserialize, Serialize}; @@ -74,7 +76,7 @@ pub const DEFAULT_CONTROL_FLOW_BR_TABLE_MULTIPLIER: u32 = 100; /// cost + (len(br_table.targets) * size_multiplier) /// ``` // This is done to encourage users to avoid writing code with very long `br_table`s. -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +#[derive(Add, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[serde(deny_unknown_fields)] pub struct BrTableCost { @@ -141,8 +143,25 @@ impl FromBytes for BrTableCost { } } +impl Zero for BrTableCost { + fn zero() -> Self { + BrTableCost { + cost: 0, + size_multiplier: 0, + } + } + + fn is_zero(&self) -> bool { + let BrTableCost { + cost, + size_multiplier, + } = self; + cost.is_zero() && size_multiplier.is_zero() + } +} + /// Definition of a cost table for a Wasm control flow opcodes. -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +#[derive(Add, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[serde(deny_unknown_fields)] pub struct ControlFlowCosts { @@ -321,10 +340,61 @@ impl Distribution for Standard { } } +impl Zero for ControlFlowCosts { + fn zero() -> Self { + ControlFlowCosts { + block: 0, + op_loop: 0, + op_if: 0, + op_else: 0, + end: 0, + br: 0, + br_if: 0, + op_return: 0, + call: 0, + call_indirect: 0, + drop: 0, + select: 0, + br_table: BrTableCost::zero(), + } + } + + fn is_zero(&self) -> bool { + let ControlFlowCosts { + block, + op_loop, + op_if, + op_else, + end, + br, + br_if, + op_return, + call, + call_indirect, + drop, + select, + br_table, + } = self; + block.is_zero() + && op_loop.is_zero() + && op_if.is_zero() + && op_else.is_zero() + && end.is_zero() + && br.is_zero() + && br_if.is_zero() + && op_return.is_zero() + && call.is_zero() + && call_indirect.is_zero() + && drop.is_zero() + && select.is_zero() + && br_table.is_zero() + } +} + /// Definition of a cost table for Wasm opcodes. /// /// This is taken (partially) from parity-ethereum. -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +#[derive(Add, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[serde(deny_unknown_fields)] pub struct OpcodeCosts { @@ -531,6 +601,66 @@ impl FromBytes for OpcodeCosts { } } +impl Zero for OpcodeCosts { + fn zero() -> Self { + Self { + bit: 0, + add: 0, + mul: 0, + div: 0, + load: 0, + store: 0, + op_const: 0, + local: 0, + global: 0, + integer_comparison: 0, + conversion: 0, + unreachable: 0, + nop: 0, + current_memory: 0, + grow_memory: 0, + control_flow: ControlFlowCosts::zero(), + } + } + + fn is_zero(&self) -> bool { + let OpcodeCosts { + bit, + add, + mul, + div, + load, + store, + op_const, + local, + global, + integer_comparison, + conversion, + unreachable, + nop, + current_memory, + grow_memory, + control_flow, + } = self; + bit.is_zero() + && add.is_zero() + && mul.is_zero() + && div.is_zero() + && load.is_zero() + && store.is_zero() + && op_const.is_zero() + && local.is_zero() + && global.is_zero() + && integer_comparison.is_zero() + && conversion.is_zero() + && unreachable.is_zero() + && nop.is_zero() + && current_memory.is_zero() + && grow_memory.is_zero() + && control_flow.is_zero() + } +} + #[doc(hidden)] #[cfg(any(feature = "gens", test))] pub mod gens { diff --git a/types/src/chainspec/vm_config/storage_costs.rs b/types/src/chainspec/vm_config/storage_costs.rs index 144cdba719..0ce4e9ce6d 100644 --- a/types/src/chainspec/vm_config/storage_costs.rs +++ b/types/src/chainspec/vm_config/storage_costs.rs @@ -1,6 +1,8 @@ //! Support for storage costs. #[cfg(feature = "datasize")] use datasize::DataSize; +use derive_more::Add; +use num_traits::Zero; use rand::{distributions::Standard, prelude::*, Rng}; use serde::{Deserialize, Serialize}; @@ -13,7 +15,7 @@ use crate::{ pub const DEFAULT_GAS_PER_BYTE_COST: u32 = 630_000; /// Represents a cost table for storage costs. -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +#[derive(Add, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[serde(deny_unknown_fields)] pub struct StorageCosts { @@ -77,6 +79,16 @@ impl FromBytes for StorageCosts { } } +impl Zero for StorageCosts { + fn zero() -> Self { + StorageCosts { gas_per_byte: 0 } + } + + fn is_zero(&self) -> bool { + self.gas_per_byte.is_zero() + } +} + #[cfg(test)] pub mod tests { use crate::U512; diff --git a/types/src/chainspec/vm_config/wasm_config.rs b/types/src/chainspec/vm_config/wasm_config.rs index bfc9452496..ab73b44b6c 100644 --- a/types/src/chainspec/vm_config/wasm_config.rs +++ b/types/src/chainspec/vm_config/wasm_config.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, - chainspec::vm_config::{HostFunctionCosts, OpcodeCosts, StorageCosts}, + chainspec::vm_config::{HostFunctionCosts, MessageLimits, OpcodeCosts, StorageCosts}, }; /// Default maximum number of pages of the Wasm memory. @@ -32,6 +32,8 @@ pub struct WasmConfig { storage_costs: StorageCosts, /// Host function costs table. host_function_costs: HostFunctionCosts, + /// Messages limits. + messages_limits: MessageLimits, } impl WasmConfig { @@ -42,6 +44,7 @@ impl WasmConfig { opcode_costs: OpcodeCosts, storage_costs: StorageCosts, host_function_costs: HostFunctionCosts, + messages_limits: MessageLimits, ) -> Self { Self { max_memory, @@ -49,6 +52,7 @@ impl WasmConfig { opcode_costs, storage_costs, host_function_costs, + messages_limits, } } @@ -66,6 +70,11 @@ impl WasmConfig { pub fn take_host_function_costs(self) -> HostFunctionCosts { self.host_function_costs } + + /// Returns the limits config for messages. + pub fn messages_limits(&self) -> MessageLimits { + self.messages_limits + } } impl Default for WasmConfig { @@ -76,6 +85,7 @@ impl Default for WasmConfig { opcode_costs: OpcodeCosts::default(), storage_costs: StorageCosts::default(), host_function_costs: HostFunctionCosts::default(), + messages_limits: MessageLimits::default(), } } } @@ -89,6 +99,7 @@ impl ToBytes for WasmConfig { ret.append(&mut self.opcode_costs.to_bytes()?); ret.append(&mut self.storage_costs.to_bytes()?); ret.append(&mut self.host_function_costs.to_bytes()?); + ret.append(&mut self.messages_limits.to_bytes()?); Ok(ret) } @@ -99,6 +110,7 @@ impl ToBytes for WasmConfig { + self.opcode_costs.serialized_length() + self.storage_costs.serialized_length() + self.host_function_costs.serialized_length() + + self.messages_limits.serialized_length() } } @@ -109,6 +121,7 @@ impl FromBytes for WasmConfig { let (opcode_costs, rem) = FromBytes::from_bytes(rem)?; let (storage_costs, rem) = FromBytes::from_bytes(rem)?; let (host_function_costs, rem) = FromBytes::from_bytes(rem)?; + let (messages_limits, rem) = FromBytes::from_bytes(rem)?; Ok(( WasmConfig { @@ -117,6 +130,7 @@ impl FromBytes for WasmConfig { opcode_costs, storage_costs, host_function_costs, + messages_limits, }, rem, )) @@ -131,6 +145,7 @@ impl Distribution for Standard { opcode_costs: rng.gen(), storage_costs: rng.gen(), host_function_costs: rng.gen(), + messages_limits: rng.gen(), } } } @@ -143,7 +158,8 @@ pub mod gens { use crate::{ chainspec::vm_config::{ host_function_costs::gens::host_function_costs_arb, - opcode_costs::gens::opcode_costs_arb, storage_costs::gens::storage_costs_arb, + message_limits::gens::message_limits_arb, opcode_costs::gens::opcode_costs_arb, + storage_costs::gens::storage_costs_arb, }, WasmConfig, }; @@ -155,6 +171,7 @@ pub mod gens { opcode_costs in opcode_costs_arb(), storage_costs in storage_costs_arb(), host_function_costs in host_function_costs_arb(), + messages_limits in message_limits_arb(), ) -> WasmConfig { WasmConfig { max_memory, @@ -162,6 +179,7 @@ pub mod gens { opcode_costs, storage_costs, host_function_costs, + messages_limits, } } } diff --git a/types/src/contract_messages.rs b/types/src/contract_messages.rs new file mode 100644 index 0000000000..7bf3ccc9ab --- /dev/null +++ b/types/src/contract_messages.rs @@ -0,0 +1,228 @@ +//! Data types for interacting with contract level messages. + +mod error; +mod messages; +mod topics; + +pub use error::FromStrError; +pub use messages::{Message, MessageChecksum, MessagePayload, Messages}; +pub use topics::{ + MessageTopicOperation, MessageTopicSummary, TopicNameHash, TOPIC_NAME_HASH_LENGTH, +}; + +use crate::{ + alloc::string::ToString, + bytesrepr::{self, FromBytes, ToBytes}, + checksummed_hex, AddressableEntityHash, KEY_HASH_LENGTH, +}; + +use core::convert::TryFrom; + +use alloc::{string::String, vec::Vec}; +use core::fmt::{Debug, Display, Formatter}; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +use rand::{ + distributions::{Distribution, Standard}, + Rng, +}; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +const TOPIC_FORMATTED_STRING_PREFIX: &str = "topic-"; +const MESSAGE_ADDR_PREFIX: &str = "message-"; + +/// MessageTopicAddr +#[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[cfg_attr(feature = "datasize", derive(DataSize))] +pub struct MessageAddr { + /// The entity addr. + entity_addr: AddressableEntityHash, + /// The hash of the name of the message topic. + topic_name_hash: TopicNameHash, + /// The message index. + message_index: Option, +} + +impl MessageAddr { + /// Constructs a new topic address based on the addressable entity addr and the hash of the + /// message topic name. + pub const fn new_topic_addr( + entity_addr: AddressableEntityHash, + topic_name_hash: TopicNameHash, + ) -> Self { + Self { + entity_addr, + topic_name_hash, + message_index: None, + } + } + + /// Constructs a new message address based on the addressable entity addr, the hash of the + /// message topic name and the message index in the topic. + pub const fn new_message_addr( + entity_addr: AddressableEntityHash, + topic_name_hash: TopicNameHash, + message_index: u32, + ) -> Self { + Self { + entity_addr, + topic_name_hash, + message_index: Some(message_index), + } + } + + /// Formats the [`MessageAddr`] as a prefixed, hex-encoded string. + pub fn to_formatted_string(self) -> String { + match self.message_index { + Some(index) => { + format!( + "{}{}-{}-{:x}", + MESSAGE_ADDR_PREFIX, + base16::encode_lower(&self.entity_addr), + self.topic_name_hash.to_formatted_string(), + index, + ) + } + None => { + format!( + "{}{}{}-{}", + MESSAGE_ADDR_PREFIX, + TOPIC_FORMATTED_STRING_PREFIX, + base16::encode_lower(&self.entity_addr), + self.topic_name_hash.to_formatted_string(), + ) + } + } + } + + /// Parses a formatted string into a [`MessageAddr`]. + pub fn from_formatted_str(input: &str) -> Result { + let remainder = input + .strip_prefix(MESSAGE_ADDR_PREFIX) + .ok_or(FromStrError::InvalidPrefix)?; + + let (remainder, message_index) = match remainder.strip_prefix(TOPIC_FORMATTED_STRING_PREFIX) + { + Some(topic_string) => (topic_string, None), + None => { + let (remainder, message_index_str) = remainder + .rsplit_once('-') + .ok_or(FromStrError::MissingMessageIndex)?; + (remainder, Some(u32::from_str_radix(message_index_str, 16)?)) + } + }; + + let (entity_addr_str, topic_name_hash_str) = remainder + .split_once('-') + .ok_or(FromStrError::MissingMessageIndex)?; + + let bytes = checksummed_hex::decode(entity_addr_str)?; + let entity_addr = ::try_from(bytes[0..KEY_HASH_LENGTH].as_ref()) + .map_err(|err| FromStrError::EntityHashParseError(err.to_string()))?; + + let topic_name_hash = TopicNameHash::from_formatted_str(topic_name_hash_str)?; + Ok(MessageAddr { + entity_addr, + topic_name_hash, + message_index, + }) + } + + /// Returns the entity addr of this message topic. + pub fn entity_addr(&self) -> AddressableEntityHash { + self.entity_addr + } +} + +impl Display for MessageAddr { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self.message_index { + Some(index) => { + write!( + f, + "{}-{}-{:x}", + base16::encode_lower(&self.entity_addr), + self.topic_name_hash, + index, + ) + } + None => { + write!( + f, + "{}-{}", + base16::encode_lower(&self.entity_addr), + self.topic_name_hash, + ) + } + } + } +} + +impl ToBytes for MessageAddr { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + buffer.append(&mut self.entity_addr.to_bytes()?); + buffer.append(&mut self.topic_name_hash.to_bytes()?); + buffer.append(&mut self.message_index.to_bytes()?); + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.entity_addr.serialized_length() + + self.topic_name_hash.serialized_length() + + self.message_index.serialized_length() + } +} + +impl FromBytes for MessageAddr { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (entity_addr, rem) = FromBytes::from_bytes(bytes)?; + let (topic_hash, rem) = FromBytes::from_bytes(rem)?; + let (message_index, rem) = FromBytes::from_bytes(rem)?; + Ok(( + MessageAddr { + entity_addr, + topic_name_hash: topic_hash, + message_index, + }, + rem, + )) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> MessageAddr { + MessageAddr { + entity_addr: rng.gen(), + topic_name_hash: rng.gen(), + message_index: rng.gen(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{bytesrepr, KEY_HASH_LENGTH}; + + use super::{topics::TOPIC_NAME_HASH_LENGTH, *}; + + #[test] + fn serialization_roundtrip() { + let topic_addr = MessageAddr::new_topic_addr( + [1; KEY_HASH_LENGTH].into(), + [2; TOPIC_NAME_HASH_LENGTH].into(), + ); + bytesrepr::test_serialization_roundtrip(&topic_addr); + + let message_addr = MessageAddr::new_message_addr( + [1; KEY_HASH_LENGTH].into(), + [2; TOPIC_NAME_HASH_LENGTH].into(), + 3, + ); + bytesrepr::test_serialization_roundtrip(&message_addr); + } +} diff --git a/types/src/contract_messages/error.rs b/types/src/contract_messages/error.rs new file mode 100644 index 0000000000..ba7f2cd383 --- /dev/null +++ b/types/src/contract_messages/error.rs @@ -0,0 +1,74 @@ +use core::array::TryFromSliceError; + +use alloc::string::String; +use core::{ + fmt::{self, Debug, Display, Formatter}, + num::ParseIntError, +}; + +/// Error while parsing message hashes from string. +#[derive(Debug)] +#[non_exhaustive] +pub enum FromStrError { + /// The prefix is invalid. + InvalidPrefix, + /// No message index at the end of the string. + MissingMessageIndex, + /// String not formatted correctly. + Formatting, + /// Cannot parse entity hash. + EntityHashParseError(String), + /// Cannot parse message topic hash. + MessageTopicParseError(String), + /// Failed to decode address portion of URef. + Hex(base16::DecodeError), + /// Failed to parse an int. + Int(ParseIntError), + /// The slice is the wrong length. + Length(TryFromSliceError), +} + +impl From for FromStrError { + fn from(error: base16::DecodeError) -> Self { + FromStrError::Hex(error) + } +} + +impl From for FromStrError { + fn from(error: ParseIntError) -> Self { + FromStrError::Int(error) + } +} + +impl From for FromStrError { + fn from(error: TryFromSliceError) -> Self { + FromStrError::Length(error) + } +} + +impl Display for FromStrError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + FromStrError::InvalidPrefix => { + write!(f, "prefix is invalid") + } + FromStrError::MissingMessageIndex => { + write!(f, "no message index found at the end of the string") + } + FromStrError::Formatting => { + write!(f, "string not properly formatted") + } + FromStrError::EntityHashParseError(err) => { + write!(f, "could not parse entity hash: {}", err) + } + FromStrError::MessageTopicParseError(err) => { + write!(f, "could not parse topic hash: {}", err) + } + FromStrError::Hex(error) => { + write!(f, "failed to decode address portion from hex: {}", error) + } + FromStrError::Int(error) => write!(f, "failed to parse an int: {}", error), + FromStrError::Length(error) => write!(f, "address portion is wrong length: {}", error), + } + } +} diff --git a/types/src/contract_messages/messages.rs b/types/src/contract_messages/messages.rs new file mode 100644 index 0000000000..50f4e51560 --- /dev/null +++ b/types/src/contract_messages/messages.rs @@ -0,0 +1,324 @@ +use crate::{ + bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, + checksummed_hex, AddressableEntityHash, Key, +}; + +use alloc::{string::String, vec::Vec}; +use core::{convert::TryFrom, fmt::Debug}; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(any(feature = "testing", test))] +use rand::{ + distributions::{Alphanumeric, DistString, Distribution, Standard}, + Rng, +}; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; + +use super::{FromStrError, TopicNameHash}; + +/// Collection of multiple messages. +pub type Messages = Vec; + +/// The length of a message digest +pub const MESSAGE_CHECKSUM_LENGTH: usize = 32; + +const MESSAGE_CHECKSUM_STRING_PREFIX: &str = "message-checksum-"; + +/// A newtype wrapping an array which contains the raw bytes of +/// the hash of the message emitted. +#[derive(Default, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr( + feature = "json-schema", + derive(JsonSchema), + schemars(description = "Message checksum as a formatted string.") +)] +pub struct MessageChecksum( + #[cfg_attr(feature = "json-schema", schemars(skip, with = "String"))] + pub [u8; MESSAGE_CHECKSUM_LENGTH], +); + +impl MessageChecksum { + /// Returns inner value of the message checksum. + pub fn value(&self) -> [u8; MESSAGE_CHECKSUM_LENGTH] { + self.0 + } + + /// Formats the `MessageChecksum` as a human readable string. + pub fn to_formatted_string(self) -> String { + format!( + "{}{}", + MESSAGE_CHECKSUM_STRING_PREFIX, + base16::encode_lower(&self.0), + ) + } + + /// Parses a string formatted as per `Self::to_formatted_string()` into a + /// `MessageChecksum`. + pub fn from_formatted_str(input: &str) -> Result { + let hex_addr = input + .strip_prefix(MESSAGE_CHECKSUM_STRING_PREFIX) + .ok_or(FromStrError::InvalidPrefix)?; + + let bytes = + <[u8; MESSAGE_CHECKSUM_LENGTH]>::try_from(checksummed_hex::decode(hex_addr)?.as_ref())?; + Ok(MessageChecksum(bytes)) + } +} + +impl ToBytes for MessageChecksum { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + buffer.append(&mut self.0.to_bytes()?); + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } +} + +impl FromBytes for MessageChecksum { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (checksum, rem) = FromBytes::from_bytes(bytes)?; + Ok((MessageChecksum(checksum), rem)) + } +} + +impl Serialize for MessageChecksum { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + self.to_formatted_string().serialize(serializer) + } else { + self.0.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for MessageChecksum { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let formatted_string = String::deserialize(deserializer)?; + MessageChecksum::from_formatted_str(&formatted_string).map_err(SerdeError::custom) + } else { + let bytes = <[u8; MESSAGE_CHECKSUM_LENGTH]>::deserialize(deserializer)?; + Ok(MessageChecksum(bytes)) + } + } +} + +const MESSAGE_PAYLOAD_TAG_LENGTH: usize = U8_SERIALIZED_LENGTH; + +/// Tag for a message payload that contains a human readable string. +pub const MESSAGE_PAYLOAD_STRING_TAG: u8 = 0; + +/// The payload of the message emitted by an addressable entity during execution. +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum MessagePayload { + /// Human readable string message. + String(String), +} + +impl From for MessagePayload +where + T: Into, +{ + fn from(value: T) -> Self { + Self::String(value.into()) + } +} + +impl ToBytes for MessagePayload { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + match self { + MessagePayload::String(message_string) => { + buffer.insert(0, MESSAGE_PAYLOAD_STRING_TAG); + buffer.extend(message_string.to_bytes()?); + } + } + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + MESSAGE_PAYLOAD_TAG_LENGTH + + match self { + MessagePayload::String(message_string) => message_string.serialized_length(), + } + } +} + +impl FromBytes for MessagePayload { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + match tag { + MESSAGE_PAYLOAD_STRING_TAG => { + let (message, remainder): (String, _) = FromBytes::from_bytes(remainder)?; + Ok((Self::String(message), remainder)) + } + _ => Err(bytesrepr::Error::Formatting), + } + } +} + +/// Message that was emitted by an addressable entity during execution. +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct Message { + /// The identity of the entity that produced the message. + #[cfg_attr(feature = "json-schema", schemars(with = "String"))] + entity_addr: AddressableEntityHash, + /// The payload of the message. + message: MessagePayload, + /// The name of the topic on which the message was emitted on. + topic_name: String, + /// The hash of the name of the topic. + topic_name_hash: TopicNameHash, + /// Message index in the topic. + index: u32, +} + +impl Message { + /// Creates new instance of [`Message`] with the specified source and message payload. + pub fn new( + source: AddressableEntityHash, + message: MessagePayload, + topic_name: String, + topic_name_hash: TopicNameHash, + index: u32, + ) -> Self { + Self { + entity_addr: source, + message, + topic_name, + topic_name_hash, + index, + } + } + + /// Returns a reference to the identity of the entity that produced the message. + pub fn entity_addr(&self) -> &AddressableEntityHash { + &self.entity_addr + } + + /// Returns a reference to the payload of the message. + pub fn payload(&self) -> &MessagePayload { + &self.message + } + + /// Returns a reference to the name of the topic on which the message was emitted on. + pub fn topic_name(&self) -> &String { + &self.topic_name + } + + /// Returns a reference to the hash of the name of the topic. + pub fn topic_name_hash(&self) -> &TopicNameHash { + &self.topic_name_hash + } + + /// Returns the index of the message in the topic. + pub fn index(&self) -> u32 { + self.index + } + + /// Returns a new [`Key::Message`] based on the information in the message. + /// This key can be used to query the checksum record for the message in global state. + pub fn message_key(&self) -> Key { + Key::message(self.entity_addr, self.topic_name_hash, self.index) + } + + /// Returns a new [`Key::Message`] based on the information in the message. + /// This key can be used to query the control record for the topic of this message in global + /// state. + pub fn topic_key(&self) -> Key { + Key::message_topic(self.entity_addr, self.topic_name_hash) + } +} + +impl ToBytes for Message { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + buffer.append(&mut self.entity_addr.to_bytes()?); + buffer.append(&mut self.message.to_bytes()?); + buffer.append(&mut self.topic_name.to_bytes()?); + buffer.append(&mut self.topic_name_hash.to_bytes()?); + buffer.append(&mut self.index.to_bytes()?); + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.entity_addr.serialized_length() + + self.message.serialized_length() + + self.topic_name.serialized_length() + + self.topic_name_hash.serialized_length() + + self.index.serialized_length() + } +} + +impl FromBytes for Message { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (entity_addr, rem) = FromBytes::from_bytes(bytes)?; + let (message, rem) = FromBytes::from_bytes(rem)?; + let (topic_name, rem) = FromBytes::from_bytes(rem)?; + let (topic_name_hash, rem) = FromBytes::from_bytes(rem)?; + let (index, rem) = FromBytes::from_bytes(rem)?; + Ok(( + Message { + entity_addr, + message, + topic_name, + topic_name_hash, + index, + }, + rem, + )) + } +} + +#[cfg(any(feature = "testing", test))] +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Message { + let topic_name = Alphanumeric.sample_string(rng, 32); + let topic_name_hash = crate::crypto::blake2b(&topic_name).into(); + let message = Alphanumeric.sample_string(rng, 64).into(); + + Message { + entity_addr: rng.gen(), + message, + topic_name, + topic_name_hash, + index: rng.gen(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{bytesrepr, contract_messages::topics::TOPIC_NAME_HASH_LENGTH, KEY_HASH_LENGTH}; + + use super::*; + + #[test] + fn serialization_roundtrip() { + let message_checksum = MessageChecksum([1; MESSAGE_CHECKSUM_LENGTH]); + bytesrepr::test_serialization_roundtrip(&message_checksum); + + let message_payload = "message payload".into(); + bytesrepr::test_serialization_roundtrip(&message_payload); + + let message = Message::new( + [1; KEY_HASH_LENGTH].into(), + message_payload, + "test_topic".to_string(), + TopicNameHash::new([0x4du8; TOPIC_NAME_HASH_LENGTH]), + 10, + ); + bytesrepr::test_serialization_roundtrip(&message); + } +} diff --git a/types/src/contract_messages/topics.rs b/types/src/contract_messages/topics.rs new file mode 100644 index 0000000000..9a41d3e39a --- /dev/null +++ b/types/src/contract_messages/topics.rs @@ -0,0 +1,254 @@ +use crate::{ + bytesrepr::{self, FromBytes, ToBytes}, + checksummed_hex, BlockTime, +}; + +use core::convert::TryFrom; + +use alloc::{string::String, vec::Vec}; +use core::fmt::{Debug, Display, Formatter}; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +use rand::{ + distributions::{Distribution, Standard}, + Rng, +}; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; + +use super::error::FromStrError; + +/// The length in bytes of a topic name hash. +pub const TOPIC_NAME_HASH_LENGTH: usize = 32; +const MESSAGE_TOPIC_NAME_HASH: &str = "topic-name-"; + +/// The hash of the name of the message topic. +#[derive(Default, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr( + feature = "json-schema", + derive(JsonSchema), + schemars(description = "The hash of the name of the message topic.") +)] +pub struct TopicNameHash( + #[cfg_attr(feature = "json-schema", schemars(skip, with = "String"))] + pub [u8; TOPIC_NAME_HASH_LENGTH], +); + +impl TopicNameHash { + /// Returns a new [`TopicNameHash`] based on the specified value. + pub const fn new(topic_name_hash: [u8; TOPIC_NAME_HASH_LENGTH]) -> TopicNameHash { + TopicNameHash(topic_name_hash) + } + + /// Returns inner value of the topic hash. + pub fn value(&self) -> [u8; TOPIC_NAME_HASH_LENGTH] { + self.0 + } + + /// Formats the [`TopicNameHash`] as a prefixed, hex-encoded string. + pub fn to_formatted_string(self) -> String { + format!( + "{}{}", + MESSAGE_TOPIC_NAME_HASH, + base16::encode_lower(&self.0), + ) + } + + /// Parses a string formatted as per `Self::to_formatted_string()` into a [`TopicNameHash`]. + pub fn from_formatted_str(input: &str) -> Result { + let remainder = input + .strip_prefix(MESSAGE_TOPIC_NAME_HASH) + .ok_or(FromStrError::InvalidPrefix)?; + let bytes = + <[u8; TOPIC_NAME_HASH_LENGTH]>::try_from(checksummed_hex::decode(remainder)?.as_ref())?; + Ok(TopicNameHash(bytes)) + } +} + +impl ToBytes for TopicNameHash { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + buffer.append(&mut self.0.to_bytes()?); + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } +} + +impl FromBytes for TopicNameHash { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (hash, rem) = FromBytes::from_bytes(bytes)?; + Ok((TopicNameHash(hash), rem)) + } +} + +impl Serialize for TopicNameHash { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + self.to_formatted_string().serialize(serializer) + } else { + self.0.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for TopicNameHash { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let formatted_string = String::deserialize(deserializer)?; + TopicNameHash::from_formatted_str(&formatted_string).map_err(SerdeError::custom) + } else { + let bytes = <[u8; TOPIC_NAME_HASH_LENGTH]>::deserialize(deserializer)?; + Ok(TopicNameHash(bytes)) + } + } +} + +impl Display for TopicNameHash { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", base16::encode_lower(&self.0)) + } +} + +impl Debug for TopicNameHash { + fn fmt(&self, f: &mut Formatter) -> core::fmt::Result { + write!(f, "MessageTopicHash({})", base16::encode_lower(&self.0)) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> TopicNameHash { + TopicNameHash(rng.gen()) + } +} + +impl From<[u8; TOPIC_NAME_HASH_LENGTH]> for TopicNameHash { + fn from(value: [u8; TOPIC_NAME_HASH_LENGTH]) -> Self { + TopicNameHash(value) + } +} + +/// Summary of a message topic that will be stored in global state. +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct MessageTopicSummary { + /// Number of messages in this topic. + pub(crate) message_count: u32, + /// Block timestamp in which these messages were emitted. + pub(crate) blocktime: BlockTime, +} + +impl MessageTopicSummary { + /// Creates a new topic summary. + pub fn new(message_count: u32, blocktime: BlockTime) -> Self { + Self { + message_count, + blocktime, + } + } + + /// Returns the number of messages that were sent on this topic. + pub fn message_count(&self) -> u32 { + self.message_count + } + + /// Returns the block time. + pub fn blocktime(&self) -> BlockTime { + self.blocktime + } +} + +impl ToBytes for MessageTopicSummary { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + buffer.append(&mut self.message_count.to_bytes()?); + buffer.append(&mut self.blocktime.to_bytes()?); + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.message_count.serialized_length() + self.blocktime.serialized_length() + } +} + +impl FromBytes for MessageTopicSummary { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (message_count, rem) = FromBytes::from_bytes(bytes)?; + let (blocktime, rem) = FromBytes::from_bytes(rem)?; + Ok(( + MessageTopicSummary { + message_count, + blocktime, + }, + rem, + )) + } +} + +const TOPIC_OPERATION_ADD_TAG: u8 = 0; +const OPERATION_MAX_SERIALIZED_LEN: usize = 1; + +/// Operations that can be performed on message topics. +#[derive(Debug, PartialEq)] +pub enum MessageTopicOperation { + /// Add a new message topic. + Add, +} + +impl MessageTopicOperation { + /// Maximum serialized length of a message topic operation. + pub const fn max_serialized_len() -> usize { + OPERATION_MAX_SERIALIZED_LEN + } +} + +impl ToBytes for MessageTopicOperation { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + match self { + MessageTopicOperation::Add => buffer.push(TOPIC_OPERATION_ADD_TAG), + } + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + match self { + MessageTopicOperation::Add => 1, + } + } +} + +impl FromBytes for MessageTopicOperation { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder): (u8, &[u8]) = FromBytes::from_bytes(bytes)?; + match tag { + TOPIC_OPERATION_ADD_TAG => Ok((MessageTopicOperation::Add, remainder)), + _ => Err(bytesrepr::Error::Formatting), + } + } +} + +#[cfg(test)] +mod tests { + use crate::bytesrepr; + + use super::*; + + #[test] + fn serialization_roundtrip() { + let topic_name_hash = TopicNameHash::new([0x4du8; TOPIC_NAME_HASH_LENGTH]); + bytesrepr::test_serialization_roundtrip(&topic_name_hash); + + let topic_summary = MessageTopicSummary::new(10, BlockTime::new(100)); + bytesrepr::test_serialization_roundtrip(&topic_summary); + + let topic_operation = MessageTopicOperation::Add; + bytesrepr::test_serialization_roundtrip(&topic_operation); + } +} diff --git a/types/src/execution/execution_result_v2.rs b/types/src/execution/execution_result_v2.rs index a3065c95ab..2b3be0c6a4 100644 --- a/types/src/execution/execution_result_v2.rs +++ b/types/src/execution/execution_result_v2.rs @@ -23,12 +23,14 @@ use super::Effects; use super::{Transform, TransformKind}; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; +#[cfg(any(feature = "testing", feature = "json-schema", test))] +use crate::Key; +#[cfg(feature = "json-schema")] +use crate::KEY_HASH_LENGTH; use crate::{ bytesrepr::{self, FromBytes, ToBytes, RESULT_ERR_TAG, RESULT_OK_TAG, U8_SERIALIZED_LENGTH}, TransferAddr, U512, }; -#[cfg(feature = "json-schema")] -use crate::{Key, KEY_HASH_LENGTH}; #[cfg(feature = "json-schema")] static EXECUTION_RESULT: Lazy = Lazy::new(|| { @@ -93,16 +95,18 @@ impl Distribution for Standard { transfers.push(TransferAddr::new(rng.gen())) } + let effects = Effects::random(rng); + if rng.gen() { ExecutionResultV2::Failure { - effects: Effects::random(rng), + effects, transfers, cost: rng.gen::().into(), error_message: format!("Error message {}", rng.gen::()), } } else { ExecutionResultV2::Success { - effects: Effects::random(rng), + effects, transfers, cost: rng.gen::().into(), } diff --git a/types/src/execution/transform_kind.rs b/types/src/execution/transform_kind.rs index 421131e9e9..0c0f6ee429 100644 --- a/types/src/execution/transform_kind.rs +++ b/types/src/execution/transform_kind.rs @@ -172,6 +172,16 @@ impl TransformKind { let found = "ContractPackage".to_string(); Err(StoredValueTypeMismatch::new(expected, found).into()) } + StoredValue::MessageTopic(_) => { + let expected = "Contract or Account".to_string(); + let found = "MessageTopic".to_string(); + Err(StoredValueTypeMismatch::new(expected, found).into()) + } + StoredValue::Message(_) => { + let expected = "Contract or Account".to_string(); + let found = "Message".to_string(); + Err(StoredValueTypeMismatch::new(expected, found).into()) + } }, TransformKind::Failure(error) => Err(error), } diff --git a/types/src/gens.rs b/types/src/gens.rs index ffe7e62476..ac09ad123a 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -2,7 +2,12 @@ //! [`Proptest`](https://crates.io/crates/proptest). #![allow(missing_docs)] -use alloc::{boxed::Box, collections::BTreeSet, string::String, vec}; +use alloc::{ + boxed::Box, + collections::{BTreeMap, BTreeSet}, + string::String, + vec, +}; use proptest::{ array, bits, bool, @@ -14,17 +19,18 @@ use proptest::{ use crate::{ account::{self, action_thresholds::gens::account_action_thresholds_arb, AccountHash}, - addressable_entity::{NamedKeys, Parameters, Weight}, - crypto::gens::public_key_arb_no_system, + addressable_entity::{MessageTopics, NamedKeys, Parameters, Weight}, + contract_messages::{MessageChecksum, MessageTopicSummary, TopicNameHash}, + crypto::{self, gens::public_key_arb_no_system}, package::{EntityVersionKey, EntityVersions, Groups, PackageStatus}, system::auction::{ gens::era_info_arb, DelegationRate, Delegator, UnbondingPurse, WithdrawPurse, DELEGATION_RATE_DENOMINATOR, }, transfer::TransferAddr, - AccessRights, AddressableEntity, AddressableEntityHash, ByteCode, CLType, CLValue, EntryPoint, - EntryPointAccess, EntryPointType, EntryPoints, EraId, Group, Key, NamedArg, Package, Parameter, - Phase, ProtocolVersion, SemVer, StoredValue, URef, U128, U256, U512, + AccessRights, AddressableEntity, AddressableEntityHash, BlockTime, ByteCode, CLType, CLValue, + EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, EraId, Group, Key, NamedArg, + Package, Parameter, Phase, ProtocolVersion, SemVer, StoredValue, URef, U128, U256, U512, }; use crate::{ @@ -343,6 +349,20 @@ pub fn entry_points_arb() -> impl Strategy { collection::vec(entry_point_arb(), 1..10).prop_map(EntryPoints::from) } +pub fn message_topics_arb() -> impl Strategy { + collection::vec(any::(), 1..100).prop_map(|topic_names| { + MessageTopics::from( + topic_names + .into_iter() + .map(|name| { + let name_hash = crypto::blake2b(&name).into(); + (name, name_hash) + }) + .collect::>(), + ) + }) +} + pub fn account_arb() -> impl Strategy { ( account_hash_arb(), @@ -419,6 +439,7 @@ pub fn addressable_entity_arb() -> impl Strategy { uref_arb(), associated_keys_arb(), action_thresholds_arb(), + message_topics_arb(), ) .prop_map( |( @@ -430,6 +451,7 @@ pub fn addressable_entity_arb() -> impl Strategy { main_purse, associated_keys, action_thresholds, + message_topics, )| { AddressableEntity::new( contract_package_hash_arb.into(), @@ -440,6 +462,7 @@ pub fn addressable_entity_arb() -> impl Strategy { main_purse, associated_keys, action_thresholds, + message_topics, ) }, ) @@ -660,6 +683,17 @@ fn unbondings_arb(size: impl Into) -> impl Strategy impl Strategy { + (any::(), any::()).prop_map(|(message_count, blocktime)| MessageTopicSummary { + message_count, + blocktime: BlockTime::new(blocktime), + }) +} + +fn message_summary_arb() -> impl Strategy { + u8_slice_32().prop_map(MessageChecksum) +} + pub fn stored_value_arb() -> impl Strategy { prop_oneof![ cl_value_arb().prop_map(StoredValue::CLValue), @@ -675,7 +709,9 @@ pub fn stored_value_arb() -> impl Strategy { validator_bid_arb().prop_map(StoredValue::BidKind), delegator_bid_arb().prop_map(StoredValue::BidKind), withdraws_arb(1..50).prop_map(StoredValue::Withdraw), - unbondings_arb(1..50).prop_map(StoredValue::Unbonding) + unbondings_arb(1..50).prop_map(StoredValue::Unbonding), + message_topic_summary_arb().prop_map(StoredValue::MessageTopic), + message_summary_arb().prop_map(StoredValue::Message), ] .prop_map(|stored_value| // The following match statement is here only to make sure @@ -696,5 +732,7 @@ pub fn stored_value_arb() -> impl Strategy { StoredValue::BidKind(_) => stored_value, StoredValue::Package(_) => stored_value, StoredValue::ByteCode(_) => stored_value, + StoredValue::MessageTopic(_) => stored_value, + StoredValue::Message(_) => stored_value, }) } diff --git a/types/src/key.rs b/types/src/key.rs index bb43e9e26b..028ccaa9ca 100644 --- a/types/src/key.rs +++ b/types/src/key.rs @@ -34,8 +34,12 @@ use crate::{ addressable_entity, addressable_entity::AddressableEntityHash, byte_code::ByteCodeKind, - bytesrepr::{self, Error, FromBytes, ToBytes, U64_SERIALIZED_LENGTH, U8_SERIALIZED_LENGTH}, + bytesrepr::{ + self, Error, FromBytes, ToBytes, U32_SERIALIZED_LENGTH, U64_SERIALIZED_LENGTH, + U8_SERIALIZED_LENGTH, + }, checksummed_hex, + contract_messages::{self, MessageAddr, TopicNameHash, TOPIC_NAME_HASH_LENGTH}, contract_wasm::ContractWasmHash, contracts::{ContractHash, ContractPackageHash}, package::{PackageHash, PackageKindTag}, @@ -83,7 +87,6 @@ pub const DICTIONARY_ITEM_KEY_MAX_LENGTH: usize = 128; pub const ADDR_LENGTH: usize = 32; const PADDING_BYTES: [u8; 32] = [0u8; 32]; const KEY_ID_SERIALIZED_LENGTH: usize = 1; -const BID_TAG_SERIALIZED_LENGTH: usize = 1; // u8 used to determine the ID const KEY_HASH_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_HASH_LENGTH; const KEY_UREF_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + UREF_SERIALIZED_LENGTH; @@ -92,10 +95,6 @@ const KEY_DEPLOY_INFO_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_ const KEY_ERA_INFO_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + U64_SERIALIZED_LENGTH; const KEY_BALANCE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + UREF_ADDR_LENGTH; const KEY_BID_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_HASH_LENGTH; -const KEY_VALIDATOR_BID_SERIALIZED_LENGTH: usize = - KEY_ID_SERIALIZED_LENGTH + BID_TAG_SERIALIZED_LENGTH + KEY_HASH_LENGTH; -const KEY_DELEGATOR_BID_SERIALIZED_LENGTH: usize = - KEY_VALIDATOR_BID_SERIALIZED_LENGTH + KEY_HASH_LENGTH; const KEY_WITHDRAW_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_HASH_LENGTH; const KEY_UNBOND_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_HASH_LENGTH; const KEY_DICTIONARY_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_DICTIONARY_LENGTH; @@ -107,8 +106,13 @@ const KEY_CHAINSPEC_REGISTRY_SERIALIZED_LENGTH: usize = const KEY_CHECKSUM_REGISTRY_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + PADDING_BYTES.len(); const KEY_PACKAGE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + 32; +const KEY_MESSAGE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + + KEY_HASH_LENGTH + + TOPIC_NAME_HASH_LENGTH + + U8_SERIALIZED_LENGTH + + U32_SERIALIZED_LENGTH; -const MAX_SERIALIZED_LENGTH: usize = KEY_DELEGATOR_BID_SERIALIZED_LENGTH; +const MAX_SERIALIZED_LENGTH: usize = KEY_MESSAGE_SERIALIZED_LENGTH; /// An alias for [`Key`]s hash variant. pub type HashAddr = [u8; KEY_HASH_LENGTH]; @@ -148,6 +152,7 @@ pub enum KeyTag { Package = 16, AddressableEntity = 17, ByteCode = 18, + Message = 19, } impl Display for KeyTag { @@ -172,6 +177,7 @@ impl Display for KeyTag { KeyTag::Package => write!(f, "Package"), KeyTag::AddressableEntity => write!(f, "AddressableEntity"), KeyTag::ByteCode => write!(f, "ByteCode"), + KeyTag::Message => write!(f, "Message"), } } } @@ -222,6 +228,8 @@ pub enum Key { AddressableEntity(PackageKindTag, EntityAddr), /// A `Key` under which a byte code record is stored. ByteCode(ByteCodeKind, ByteCodeAddr), + /// A `Key` under which a message is stored. + Message(MessageAddr), } #[cfg(feature = "json-schema")] @@ -284,6 +292,8 @@ pub enum FromStrError { AddressableEntity(String), /// Byte code parse error. ByteCode(String), + /// Message parse error. + Message(contract_messages::FromStrError), /// Unknown prefix. UnknownPrefix, } @@ -306,6 +316,12 @@ impl From for FromStrError { } } +impl From for FromStrError { + fn from(error: contract_messages::FromStrError) -> Self { + FromStrError::Message(error) + } +} + impl Display for FromStrError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { @@ -350,6 +366,9 @@ impl Display for FromStrError { FromStrError::ByteCode(error) => { write!(f, "byte-code-key from string error: {}", error) } + FromStrError::Message(error) => { + write!(f, "message-key from string error: {}", error) + } FromStrError::UnknownPrefix => write!(f, "unknown prefix for key"), } } @@ -379,6 +398,7 @@ impl Key { Key::Package(_) => String::from("Key::Package"), Key::AddressableEntity(..) => String::from("Key::AddressableEntity"), Key::ByteCode(..) => String::from("Key::ByteCode"), + Key::Message(_) => String::from("Key::Message"), } } @@ -465,6 +485,7 @@ impl Key { Key::BidAddr(bid_addr) => { format!("{}{}", BID_ADDR_PREFIX, bid_addr) } + Key::Message(message_addr) => message_addr.to_formatted_string(), Key::Package(package_addr) => { format!("{}{}", PACKAGE_PREFIX, base16::encode_lower(&package_addr)) } @@ -715,6 +736,12 @@ impl Key { return Ok(Key::ByteCode(tag, byte_code_addr)); } + match MessageAddr::from_formatted_str(input) { + Ok(message_addr) => return Ok(Key::Message(message_addr)), + Err(contract_messages::FromStrError::InvalidPrefix) => {} + Err(error) => return Err(error.into()), + } + Err(FromStrError::UnknownPrefix) } @@ -860,6 +887,29 @@ impl Key { Key::ByteCode(byte_code_kind, byte_code_addr) } + /// Creates a new [`Key::Message`] variant that identifies an indexed message based on an + /// `entity_addr`, `topic_name_hash` and message `index`. + pub fn message( + entity_addr: AddressableEntityHash, + topic_name_hash: TopicNameHash, + index: u32, + ) -> Key { + Key::Message(MessageAddr::new_message_addr( + entity_addr, + topic_name_hash, + index, + )) + } + + /// Creates a new [`Key::Message`] variant that identifies a message topic based on an + /// `entity_addr` and a hash of the topic name. + pub fn message_topic( + entity_addr: AddressableEntityHash, + topic_name_hash: TopicNameHash, + ) -> Key { + Key::Message(MessageAddr::new_topic_addr(entity_addr, topic_name_hash)) + } + /// Returns true if the key is of type [`Key::Dictionary`]. pub fn is_dictionary_key(&self) -> bool { if let Key::Dictionary(_) = self { @@ -902,6 +952,15 @@ impl Key { false } + + /// Return true if the inner Key is of the smart contract type. + pub fn is_smart_contract_key(&self) -> bool { + if let Self::AddressableEntity(PackageKindTag::SmartContract, _) = self { + return true; + } + + false + } } impl Display for Key { @@ -949,6 +1008,9 @@ impl Display for Key { ) } Key::BidAddr(bid_addr) => write!(f, "Key::BidAddr({})", bid_addr), + Key::Message(message_addr) => { + write!(f, "Key::Message({})", message_addr) + } Key::Package(package_addr) => { write!(f, "Key::Package({})", base16::encode_lower(package_addr)) } @@ -998,6 +1060,7 @@ impl Tagged for Key { Key::Package(_) => KeyTag::Package, Key::AddressableEntity(..) => KeyTag::AddressableEntity, Key::ByteCode(..) => KeyTag::ByteCode, + Key::Message(_) => KeyTag::Message, } } } @@ -1088,6 +1151,9 @@ impl ToBytes for Key { U8_SERIALIZED_LENGTH + KEY_ID_SERIALIZED_LENGTH + ADDR_LENGTH } Key::ByteCode(..) => U8_SERIALIZED_LENGTH + KEY_ID_SERIALIZED_LENGTH + ADDR_LENGTH, + Key::Message(message_addr) => { + KEY_ID_SERIALIZED_LENGTH + message_addr.serialized_length() + } } } @@ -1126,6 +1192,7 @@ impl ToBytes for Key { byte_code_kind.write_bytes(writer)?; byte_code_addr.write_bytes(writer) } + Key::Message(message_addr) => message_addr.write_bytes(writer), } } } @@ -1212,6 +1279,10 @@ impl FromBytes for Key { let (byte_code_addr, rem) = ByteCodeAddr::from_bytes(rem)?; Ok((Key::ByteCode(byte_code_kind, byte_code_addr), rem)) } + tag if tag == KeyTag::Message as u8 => { + let (message_addr, rem) = MessageAddr::from_bytes(remainder)?; + Ok((Key::Message(message_addr), rem)) + } _ => Err(Error::Formatting), } } @@ -1241,6 +1312,7 @@ fn please_add_to_distribution_impl(key: Key) { Key::Package(_) => unimplemented!(), Key::AddressableEntity(..) => unimplemented!(), Key::ByteCode(..) => unimplemented!(), + Key::Message(_) => unimplemented!(), } } @@ -1267,6 +1339,7 @@ impl Distribution for Standard { 16 => Key::Package(rng.gen()), 17 => Key::AddressableEntity(rng.gen(), rng.gen()), 18 => Key::ByteCode(rng.gen(), rng.gen()), + 19 => Key::Message(rng.gen()), _ => unreachable!(), } } @@ -1297,6 +1370,7 @@ mod serde_helpers { Package(&'a PackageAddr), AddressableEntity(&'a PackageKindTag, &'a EntityAddr), ByteCode(&'a ByteCodeKind, &'a ByteCodeAddr), + Message(&'a MessageAddr), } #[derive(Deserialize)] @@ -1321,6 +1395,7 @@ mod serde_helpers { Package(PackageAddr), AddressableEntity(PackageKindTag, EntityAddr), ByteCode(ByteCodeKind, ByteCodeAddr), + Message(MessageAddr), } impl<'a> From<&'a Key> for BinarySerHelper<'a> { @@ -1342,6 +1417,7 @@ mod serde_helpers { Key::ChainspecRegistry => BinarySerHelper::ChainspecRegistry, Key::ChecksumRegistry => BinarySerHelper::ChecksumRegistry, Key::BidAddr(bid_addr) => BinarySerHelper::BidAddr(bid_addr), + Key::Message(message_addr) => BinarySerHelper::Message(message_addr), Key::Package(package_addr) => BinarySerHelper::Package(package_addr), Key::AddressableEntity(package_kind, entity_addr) => { BinarySerHelper::AddressableEntity(package_kind, entity_addr) @@ -1372,6 +1448,7 @@ mod serde_helpers { BinaryDeserHelper::ChainspecRegistry => Key::ChainspecRegistry, BinaryDeserHelper::ChecksumRegistry => Key::ChecksumRegistry, BinaryDeserHelper::BidAddr(bid_addr) => Key::BidAddr(bid_addr), + BinaryDeserHelper::Message(message_addr) => Key::Message(message_addr), BinaryDeserHelper::Package(package_addr) => Key::Package(package_addr), BinaryDeserHelper::AddressableEntity(package_kind, entity_addr) => { Key::AddressableEntity(package_kind, entity_addr) @@ -1446,6 +1523,15 @@ mod tests { Key::AddressableEntity(PackageKindTag::SmartContract, [42; 32]); const BYTE_CODE_EMPTY_KEY: Key = Key::ByteCode(ByteCodeKind::Empty, [42; 32]); const BYTE_CODE_V1_WASM_KEY: Key = Key::ByteCode(ByteCodeKind::V1CasperWasm, [42; 32]); + const MESSAGE_TOPIC_KEY: Key = Key::Message(MessageAddr::new_topic_addr( + AddressableEntityHash::new([42u8; 32]), + TopicNameHash::new([42; 32]), + )); + const MESSAGE_KEY: Key = Key::Message(MessageAddr::new_message_addr( + AddressableEntityHash::new([42u8; 32]), + TopicNameHash::new([2; 32]), + 15, + )); const KEYS: &[Key] = &[ ACCOUNT_KEY, HASH_KEY, @@ -1471,8 +1557,13 @@ mod tests { ADDRESSABLE_ENTITY_SMART_CONTRACT_KEY, BYTE_CODE_EMPTY_KEY, BYTE_CODE_V1_WASM_KEY, + MESSAGE_TOPIC_KEY, + MESSAGE_KEY, ]; const HEX_STRING: &str = "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"; + const TOPIC_NAME_HEX_STRING: &str = + "0202020202020202020202020202020202020202020202020202020202020202"; + const MESSAGE_INDEX_HEX_STRING: &str = "f"; const UNIFIED_HEX_STRING: &str = "002a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"; const VALIDATOR_HEX_STRING: &str = @@ -1629,6 +1720,18 @@ mod tests { format!("{}", BYTE_CODE_V1_WASM_KEY), format!("Key::ByteCode(v1-casper-wasm-{})", HEX_STRING) ); + assert_eq!( + format!("{}", MESSAGE_TOPIC_KEY), + format!("Key::Message({}-{})", HEX_STRING, HEX_STRING) + ); + + assert_eq!( + format!("{}", MESSAGE_KEY), + format!( + "Key::Message({}-{}-{})", + HEX_STRING, TOPIC_NAME_HEX_STRING, MESSAGE_INDEX_HEX_STRING + ) + ) } #[test] @@ -1970,5 +2073,14 @@ mod tests { )); round_trip(&Key::ByteCode(ByteCodeKind::Empty, zeros)); round_trip(&Key::ByteCode(ByteCodeKind::V1CasperWasm, zeros)); + round_trip(&Key::Message(MessageAddr::new_topic_addr( + zeros.into(), + nines.into(), + ))); + round_trip(&Key::Message(MessageAddr::new_message_addr( + zeros.into(), + nines.into(), + 1, + ))); } } diff --git a/types/src/lib.rs b/types/src/lib.rs index ca8623bec4..1e2fb287a6 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -35,6 +35,7 @@ mod chainspec; pub mod checksummed_hex; mod cl_type; mod cl_value; +pub mod contract_messages; mod contract_wasm; pub mod contracts; pub mod crypto; @@ -105,9 +106,10 @@ pub use chainspec::{ ControlFlowCosts, CoreConfig, DelegatorConfig, DeployConfig, FeeHandling, GenesisAccount, GenesisValidator, GlobalStateUpdate, GlobalStateUpdateConfig, GlobalStateUpdateError, HandlePaymentCosts, HighwayConfig, HostFunction, HostFunctionCost, HostFunctionCosts, - LegacyRequiredFinality, MintCosts, NetworkConfig, OpcodeCosts, ProtocolConfig, RefundHandling, - StandardPaymentCosts, StorageCosts, SystemConfig, TransactionConfig, TransactionV1Config, - UpgradeConfig, ValidatorConfig, WasmConfig, DEFAULT_HOST_FUNCTION_NEW_DICTIONARY, + LegacyRequiredFinality, MessageLimits, MintCosts, NetworkConfig, OpcodeCosts, ProtocolConfig, + RefundHandling, StandardPaymentCosts, StorageCosts, SystemConfig, TransactionConfig, + TransactionV1Config, UpgradeConfig, ValidatorConfig, WasmConfig, + DEFAULT_HOST_FUNCTION_NEW_DICTIONARY, }; #[cfg(any(all(feature = "std", feature = "testing"), test))] pub use chainspec::{ diff --git a/types/src/stored_value.rs b/types/src/stored_value.rs index 1bc6b3e7c8..c79b9554c8 100644 --- a/types/src/stored_value.rs +++ b/types/src/stored_value.rs @@ -17,6 +17,7 @@ use serde_bytes::ByteBuf; use crate::{ account::Account, bytesrepr::{self, Error, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, + contract_messages::{MessageChecksum, MessageTopicSummary}, contract_wasm::ContractWasm, contracts::{Contract, ContractPackage}, package::Package, @@ -43,6 +44,8 @@ enum Tag { BidKind = 12, Package = 13, ByteCode = 14, + MessageTopic = 15, + Message = 16, } /// A value stored in Global State. @@ -85,6 +88,10 @@ pub enum StoredValue { Package(Package), /// A record of byte code. ByteCode(ByteCode), + /// Variant that stores a message topic. + MessageTopic(MessageTopicSummary), + /// Variant that stores a message digest. + Message(MessageChecksum), } impl StoredValue { @@ -186,6 +193,24 @@ impl StoredValue { } } + /// Returns a reference to the wrapped `MessageTopicSummary` if this is a `MessageTopic` + /// variant. + pub fn as_message_topic_summary(&self) -> Option<&MessageTopicSummary> { + match self { + StoredValue::MessageTopic(summary) => Some(summary), + _ => None, + } + } + + /// Returns a reference to the wrapped `MessageChecksum` if this is a `Message` + /// variant. + pub fn as_message_checksum(&self) -> Option<&MessageChecksum> { + match self { + StoredValue::Message(checksum) => Some(checksum), + _ => None, + } + } + /// Returns a reference to the wrapped `BidKind` if this is a `BidKind` variant. pub fn as_bid_kind(&self) -> Option<&BidKind> { match self { @@ -318,6 +343,8 @@ impl StoredValue { StoredValue::BidKind(_) => "BidKind".to_string(), StoredValue::ByteCode(_) => "ByteCode".to_string(), StoredValue::Package(_) => "Package".to_string(), + StoredValue::MessageTopic(_) => "MessageTopic".to_string(), + StoredValue::Message(_) => "Message".to_string(), } } @@ -338,6 +365,8 @@ impl StoredValue { StoredValue::BidKind(_) => Tag::BidKind, StoredValue::Package(_) => Tag::Package, StoredValue::ByteCode(_) => Tag::ByteCode, + StoredValue::MessageTopic(_) => Tag::MessageTopic, + StoredValue::Message(_) => Tag::Message, } } } @@ -597,6 +626,10 @@ impl ToBytes for StoredValue { StoredValue::BidKind(bid_kind) => bid_kind.serialized_length(), StoredValue::Package(package) => package.serialized_length(), StoredValue::ByteCode(byte_code) => byte_code.serialized_length(), + StoredValue::MessageTopic(message_topic_summary) => { + message_topic_summary.serialized_length() + } + StoredValue::Message(message_digest) => message_digest.serialized_length(), } } @@ -620,6 +653,10 @@ impl ToBytes for StoredValue { StoredValue::BidKind(bid_kind) => bid_kind.write_bytes(writer)?, StoredValue::Package(package) => package.write_bytes(writer)?, StoredValue::ByteCode(byte_code) => byte_code.write_bytes(writer)?, + StoredValue::MessageTopic(message_topic_summary) => { + message_topic_summary.write_bytes(writer)? + } + StoredValue::Message(message_digest) => message_digest.write_bytes(writer)?, }; Ok(()) } @@ -671,6 +708,12 @@ impl FromBytes for StoredValue { .map(|(package, remainder)| (StoredValue::Package(package), remainder)), tag if tag == Tag::ByteCode as u8 => ByteCode::from_bytes(remainder) .map(|(byte_code, remainder)| (StoredValue::ByteCode(byte_code), remainder)), + tag if tag == Tag::MessageTopic as u8 => MessageTopicSummary::from_bytes(remainder) + .map(|(message_summary, remainder)| { + (StoredValue::MessageTopic(message_summary), remainder) + }), + tag if tag == Tag::Message as u8 => MessageChecksum::from_bytes(remainder) + .map(|(checksum, remainder)| (StoredValue::Message(checksum), remainder)), _ => Err(Error::Formatting), } } @@ -710,6 +753,10 @@ mod serde_helpers { Package(&'a Package), /// A record of byte code. ByteCode(&'a ByteCode), + /// Variant that stores [`MessageTopicSummary`]. + MessageTopic(&'a MessageTopicSummary), + /// Variant that stores a [`MessageChecksum`]. + Message(&'a MessageChecksum), } #[derive(Deserialize)] @@ -744,6 +791,10 @@ mod serde_helpers { Package(Package), /// A record of byte code. ByteCode(ByteCode), + /// Variant that stores [`MessageTopicSummary`]. + MessageTopic(MessageTopicSummary), + /// Variant that stores [`MessageChecksum`]. + Message(MessageChecksum), } impl<'a> From<&'a StoredValue> for BinarySerHelper<'a> { @@ -766,6 +817,10 @@ mod serde_helpers { StoredValue::BidKind(payload) => BinarySerHelper::BidKind(payload), StoredValue::Package(payload) => BinarySerHelper::Package(payload), StoredValue::ByteCode(payload) => BinarySerHelper::ByteCode(payload), + StoredValue::MessageTopic(message_topic_summary) => { + BinarySerHelper::MessageTopic(message_topic_summary) + } + StoredValue::Message(message_digest) => BinarySerHelper::Message(message_digest), } } } @@ -792,6 +847,10 @@ mod serde_helpers { BinaryDeserHelper::BidKind(payload) => StoredValue::BidKind(payload), BinaryDeserHelper::ByteCode(payload) => StoredValue::ByteCode(payload), BinaryDeserHelper::Package(payload) => StoredValue::Package(payload), + BinaryDeserHelper::MessageTopic(message_topic_summary) => { + StoredValue::MessageTopic(message_topic_summary) + } + BinaryDeserHelper::Message(message_digest) => StoredValue::Message(message_digest), } } } diff --git a/utils/global-state-update-gen/src/generic/state_tracker.rs b/utils/global-state-update-gen/src/generic/state_tracker.rs index 412fcefaaa..f906ec4d9e 100644 --- a/utils/global-state-update-gen/src/generic/state_tracker.rs +++ b/utils/global-state-update-gen/src/generic/state_tracker.rs @@ -8,7 +8,7 @@ use rand::Rng; use casper_types::{ account::AccountHash, - addressable_entity::{ActionThresholds, AssociatedKeys, NamedKeys, Weight}, + addressable_entity::{ActionThresholds, AssociatedKeys, MessageTopics, NamedKeys, Weight}, package::{EntityVersions, Groups, PackageKind, PackageKindTag, PackageStatus}, system::auction::{BidAddr, BidKind, BidsExt, SeigniorageRecipientsSnapshot, UnbondingPurse}, AccessRights, AddressableEntity, AddressableEntityHash, ByteCodeHash, CLValue, EntryPoints, @@ -176,6 +176,7 @@ impl StateTracker { main_purse, associated_keys, ActionThresholds::default(), + MessageTopics::default(), ); let mut contract_package = Package::new( diff --git a/utils/global-state-update-gen/src/generic/testing.rs b/utils/global-state-update-gen/src/generic/testing.rs index 6657b59ca2..edd9c4c1cc 100644 --- a/utils/global-state-update-gen/src/generic/testing.rs +++ b/utils/global-state-update-gen/src/generic/testing.rs @@ -5,7 +5,7 @@ use rand::Rng; use casper_types::{ account::AccountHash, - addressable_entity::{ActionThresholds, AssociatedKeys, NamedKeys, Weight}, + addressable_entity::{ActionThresholds, AssociatedKeys, MessageTopics, NamedKeys, Weight}, system::auction::{ BidKind, BidsExt, Delegator, SeigniorageRecipient, SeigniorageRecipients, SeigniorageRecipientsSnapshot, UnbondingPurse, UnbondingPurses, ValidatorBid, @@ -68,6 +68,7 @@ impl MockStateReader { main_purse, AssociatedKeys::new(account_hash, Weight::new(1)), ActionThresholds::default(), + MessageTopics::default(), ); self.purses.insert(main_purse.addr(), balance); diff --git a/utils/validation/src/generators.rs b/utils/validation/src/generators.rs index 37da42d868..3fec79f0d8 100644 --- a/utils/validation/src/generators.rs +++ b/utils/validation/src/generators.rs @@ -8,7 +8,9 @@ use casper_types::{ Account, AccountHash, ActionThresholds as AccountActionThresholds, AssociatedKeys as AccountAssociatedKeys, Weight as AccountWeight, }, - addressable_entity::{ActionThresholds, AddressableEntity, AssociatedKeys, NamedKeys}, + addressable_entity::{ + ActionThresholds, AddressableEntity, AssociatedKeys, MessageTopics, NamedKeys, + }, package::{EntityVersions, Groups, Package, PackageKind, PackageStatus}, system::auction::{ Bid, BidAddr, BidKind, Delegator, EraInfo, SeigniorageAllocation, UnbondingPurse, @@ -376,6 +378,7 @@ pub fn make_abi_test_fixtures() -> Result { URef::default(), AssociatedKeys::default(), ActionThresholds::default(), + MessageTopics::default(), ); stored_value.insert( "AddressableEntity".to_string(), diff --git a/utils/validation/tests/fixtures/ABI/stored_value.json b/utils/validation/tests/fixtures/ABI/stored_value.json index 54df5892c8..72859c212f 100644 --- a/utils/validation/tests/fixtures/ABI/stored_value.json +++ b/utils/validation/tests/fixtures/ABI/stored_value.json @@ -79,12 +79,13 @@ "deployment": 1, "upgrade_management": 1, "key_management": 1 - } + }, + "message_topics": [] } } } ], - "output": "0b64646464646464646464646464646464646464646464646464646464646464646565656565656565656565656565656565656565656565656565656565656565020000000400000068617368012b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b04000000757265660211111111111111111111111111111111111111111111111111111111111111110701000000170000007075626c69635f656e7472795f706f696e745f66756e63170000007075626c69635f656e7472795f706f696e745f66756e630200000006000000706172616d310806000000706172616d320a09010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101" + "output": "0b64646464646464646464646464646464646464646464646464646464646464646565656565656565656565656565656565656565656565656565656565656565020000000400000068617368012b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b04000000757265660211111111111111111111111111111111111111111111111111111111111111110701000000170000007075626c69635f656e7472795f706f696e745f66756e63170000007075626c69635f656e7472795f706f696e745f66756e630200000006000000706172616d310806000000706172616d320a0901010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010100000000" }, "Bid": { "input": [