diff --git a/Cargo.lock b/Cargo.lock index 308905ff15..6c9ac1064f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1081,6 +1081,7 @@ dependencies = [ "assert_matches", "base16", "bincode", + "blake2 0.10.6", "casper-types", "criterion", "datasize", diff --git a/execution_engine/src/runtime/mod.rs b/execution_engine/src/runtime/mod.rs index a07fe8cf27..4a097b6a03 100644 --- a/execution_engine/src/runtime/mod.rs +++ b/execution_engine/src/runtime/mod.rs @@ -29,7 +29,7 @@ use num_rational::Ratio; use casper_storage::{ global_state::{error::Error as GlobalStateError, state::StateReader}, system::{auction::Auction, handle_payment::HandlePayment, mint::Mint}, - tracking_copy::TrackingCopyExt, + tracking_copy::{MessageEmissionError, TrackingCopyExt}, }; use casper_types::{ account::{ @@ -2790,6 +2790,35 @@ where self.context .metered_write_gs_unsafe(Key::Hash(contract_package_hash.value()), contract_package)?; + let current_blocktime = self.context.get_block_info().block_time(); + match self.context.emit_messages_for_new_installed_version( + current_blocktime, + Key::Hash(contract_package_hash.value()), + Key::Hash(contract_hash_addr), + Key::Hash(contract_wasm_hash), + insert_contract_result.protocol_version_major(), + insert_contract_result.contract_version(), + ) { + Ok(_) => (), + Err(MessageEmissionError::CLValue(clvalue_error)) => { + return Err(ExecError::CLValue(clvalue_error)) + } + Err(MessageEmissionError::TrackingCopy(error)) => { + return Err(ExecError::TrackingCopy(error)) + } + Err(MessageEmissionError::TypeMismatch(type_mismatch)) => { + return Err(ExecError::TypeMismatch(type_mismatch)) + } + Err(MessageEmissionError::BytesRepr(error)) => return Err(ExecError::BytesRepr(error)), + Err(MessageEmissionError::TopicNotRegistered(_)) => { + return Ok(Err(ApiError::MessageTopicNotRegistered)) + } + Err(MessageEmissionError::TopicFull(_)) => return Ok(Err(ApiError::MessageTopicFull)), + Err(MessageEmissionError::MaxMessagesPerBlockExceeded) => { + return Ok(Err(ApiError::MaxMessagesPerBlockExceeded)) + } + } + // set return values to buffer { let hash_bytes = match contract_hash_addr.to_bytes() { @@ -2916,6 +2945,35 @@ where self.context .metered_write_gs_unsafe(package_hash, package)?; + let current_blocktime = self.context.get_block_info().block_time(); + match self.context.emit_messages_for_new_installed_version( + current_blocktime, + Key::Hash(package_hash.value()), + entity_key, + Key::ByteCode(ByteCodeAddr::new_wasm_addr(byte_code_hash)), + insert_entity_version_result.protocol_version_major(), + insert_entity_version_result.entity_version(), + ) { + Ok(_) => (), + Err(MessageEmissionError::CLValue(clvalue_error)) => { + return Err(ExecError::CLValue(clvalue_error)) + } + Err(MessageEmissionError::TrackingCopy(error)) => { + return Err(ExecError::TrackingCopy(error)) + } + Err(MessageEmissionError::TypeMismatch(type_mismatch)) => { + return Err(ExecError::TypeMismatch(type_mismatch)) + } + Err(MessageEmissionError::BytesRepr(error)) => return Err(ExecError::BytesRepr(error)), + Err(MessageEmissionError::TopicNotRegistered(_)) => { + return Ok(Err(ApiError::MessageTopicNotRegistered)) + } + Err(MessageEmissionError::TopicFull(_)) => return Ok(Err(ApiError::MessageTopicFull)), + Err(MessageEmissionError::MaxMessagesPerBlockExceeded) => { + return Ok(Err(ApiError::MaxMessagesPerBlockExceeded)) + } + } + // set return values to buffer { let hash_bytes = match hash_addr.to_bytes() { diff --git a/execution_engine/src/runtime_context/mod.rs b/execution_engine/src/runtime_context/mod.rs index 301432debf..4ff3f5cf6f 100644 --- a/execution_engine/src/runtime_context/mod.rs +++ b/execution_engine/src/runtime_context/mod.rs @@ -16,8 +16,8 @@ use tracing::error; use casper_storage::{ global_state::{error::Error as GlobalStateError, state::StateReader}, tracking_copy::{ - AddResult, TrackingCopy, TrackingCopyCache, TrackingCopyEntityExt, TrackingCopyError, - TrackingCopyExt, + AddResult, MessageEmissionError, NewContractVersionInfo, TrackingCopy, TrackingCopyCache, + TrackingCopyEntityExt, TrackingCopyError, TrackingCopyExt, }, AddressGenerator, }; @@ -1647,4 +1647,28 @@ where Ok(Ok(())) } + + pub(crate) fn emit_messages_for_new_installed_version( + &self, + current_blocktime: BlockTime, + contract_package_key: Key, + contract_key: Key, + contract_wasm_key: Key, + version_major: u32, + version_minor: u32, + ) -> Result<(), MessageEmissionError> { + let message_limits = self.engine_config.wasm_config().messages_limits(); + let mut tracking_copy = self.tracking_copy.borrow_mut(); + tracking_copy.emit_messages_for_new_installed_version( + NewContractVersionInfo { + key_of_package: contract_package_key, + key_of_contract: contract_key, + key_of_wasm: contract_wasm_key, + version_major, + version_minor, + }, + current_blocktime, + message_limits, + ) + } } diff --git a/execution_engine_testing/tests/src/test/contract_messages.rs b/execution_engine_testing/tests/src/test/contract_messages.rs index d37ab1c835..d2b0804682 100644 --- a/execution_engine_testing/tests/src/test/contract_messages.rs +++ b/execution_engine_testing/tests/src/test/contract_messages.rs @@ -1,11 +1,11 @@ use num_traits::Zero; use std::cell::RefCell; -use casper_execution_engine::runtime::cryptography; +use casper_execution_engine::runtime::cryptography::{self}; use casper_engine_test_support::{ - ChainspecConfig, ExecuteRequestBuilder, LmdbWasmTestBuilder, DEFAULT_ACCOUNT_ADDR, - DEFAULT_BLOCK_TIME, LOCAL_GENESIS_REQUEST, + ChainspecConfig, ExecuteRequestBuilder, LmdbWasmTestBuilder, UpgradeRequestBuilder, + DEFAULT_ACCOUNT_ADDR, DEFAULT_BLOCK_TIME, DEFAULT_PROTOCOL_VERSION, LOCAL_GENESIS_REQUEST, }; use casper_types::{ @@ -13,11 +13,14 @@ use casper_types::{ bytesrepr::ToBytes, contract_messages::{MessageChecksum, MessagePayload, MessageTopicSummary, TopicNameHash}, runtime_args, AddressableEntityHash, BlockGlobalAddr, BlockTime, CLValue, CoreConfig, Digest, - EntityAddr, HostFunction, HostFunctionCostsV1, HostFunctionCostsV2, Key, MessageLimits, - OpcodeCosts, RuntimeArgs, StorageCosts, StoredValue, SystemConfig, WasmConfig, WasmV1Config, - WasmV2Config, DEFAULT_MAX_STACK_HEIGHT, DEFAULT_WASM_MAX_MEMORY, U512, + EntityAddr, EraId, HashAddr, HoldBalanceHandling, HostFunction, HostFunctionCostsV1, + HostFunctionCostsV2, Key, MessageLimits, OpcodeCosts, ProtocolVersion, PublicKey, RuntimeArgs, + StorageCosts, StoredValue, SystemConfig, WasmConfig, WasmV1Config, WasmV2Config, + DEFAULT_MAX_STACK_HEIGHT, DEFAULT_WASM_MAX_MEMORY, U512, }; +use crate::lmdb_fixture; + 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"; @@ -38,10 +41,10 @@ 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( +fn install_messages_emitter_contract_with_metadata( builder: &RefCell, use_initializer: bool, -) -> AddressableEntityHash { +) -> (AddressableEntityHash, HashAddr, HashAddr) { // Request to install the contract that will be emitting messages. let install_request = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, @@ -69,6 +72,24 @@ fn install_messages_emitter_contract( &[MESSAGE_EMITTER_PACKAGE_HASH_KEY_NAME.into()], ) .expect("should query"); + let account_query_result = builder + .borrow_mut() + .query(None, Key::from(*DEFAULT_ACCOUNT_ADDR), &[]) + .expect("should query"); + + let contract_package_hash = if let StoredValue::Account(acc) = account_query_result { + let key = acc + .named_keys() + .get(MESSAGE_EMITTER_PACKAGE_HASH_KEY_NAME) + .expect("Expected account to have named key"); + if let Key::Hash(package_addr_hash) = key { + *package_addr_hash + } else { + panic!("Not expected key variant: {key}"); + } + } else { + panic!("Stored value is not an account {:?}", account_query_result); + }; let message_emitter_package = if let StoredValue::ContractPackage(package) = query_result { package @@ -77,12 +98,29 @@ fn install_messages_emitter_contract( }; // Get the contract hash of the messages_emitter contract. - message_emitter_package + let contract_hash = message_emitter_package .versions() .values() .last() .map(|contract_hash| AddressableEntityHash::new(contract_hash.value())) - .expect("Should have contract hash") + .expect("Should have contract hash"); + let val = builder + .borrow_mut() + .query(None, Key::Hash(contract_hash.value()), &[]) + .unwrap(); + let wasm_addr_hash = if let StoredValue::Contract(contract) = val { + contract.contract_wasm_hash().value() + } else { + panic!("No contract found!") + }; + (contract_hash, contract_package_hash, wasm_addr_hash) +} + +fn install_messages_emitter_contract( + builder: &RefCell, + use_initializer: bool, +) -> AddressableEntityHash { + install_messages_emitter_contract_with_metadata(builder, use_initializer).0 } fn upgrade_messages_emitter_contract( @@ -198,17 +236,21 @@ impl<'a> ContractQueryView<'a> { } fn message_topic(&self, topic_name_hash: TopicNameHash) -> MessageTopicSummary { + self.message_topic_for_entity( + topic_name_hash, + EntityAddr::SmartContract(self.contract_hash.value()), + ) + } + + fn message_topic_for_entity( + &self, + topic_name_hash: TopicNameHash, + entity_addr: EntityAddr, + ) -> MessageTopicSummary { let query_result = self .builder .borrow_mut() - .query( - None, - Key::message_topic( - EntityAddr::SmartContract(self.contract_hash.value()), - topic_name_hash, - ), - &[], - ) + .query(None, Key::message_topic(entity_addr, topic_name_hash), &[]) .expect("should query"); match query_result { @@ -222,19 +264,16 @@ impl<'a> ContractQueryView<'a> { } } - fn message_summary( + fn message_summary_for_entity( &self, topic_name_hash: TopicNameHash, message_index: u32, state_hash: Option, + entity_addr: EntityAddr, ) -> Result { let query_result = self.builder.borrow_mut().query( state_hash, - Key::message( - EntityAddr::SmartContract(self.contract_hash.value()), - topic_name_hash, - message_index, - ), + Key::message(entity_addr, topic_name_hash, message_index), &[], )?; @@ -243,6 +282,20 @@ impl<'a> ContractQueryView<'a> { _ => panic!("Stored value is not a message summary: {:?}", query_result), } } + + fn message_summary( + &self, + topic_name_hash: TopicNameHash, + message_index: u32, + state_hash: Option, + ) -> Result { + self.message_summary_for_entity( + topic_name_hash, + message_index, + state_hash, + EntityAddr::SmartContract(self.contract_hash.value()), + ) + } } #[ignore] @@ -257,7 +310,6 @@ fn should_emit_messages() { let query_view = ContractQueryView::new(&builder, contract_hash); let message_topics = query_view.message_topics(); - let (topic_name, message_topic_hash) = message_topics .iter() .next() @@ -277,7 +329,7 @@ fn should_emit_messages() { let expected_message = MessagePayload::from(format!("{}{}", EMITTER_MESSAGE_PREFIX, "test")); let expected_message_hash = cryptography::blake2b( [ - 0u64.to_bytes().unwrap(), + 4u64.to_bytes().unwrap(), // there are system messages emitted before the custom one expected_message.to_bytes().unwrap(), ] .concat(), @@ -298,7 +350,7 @@ fn should_emit_messages() { emit_message_with_suffix(&builder, "test", &contract_hash, DEFAULT_BLOCK_TIME); let expected_message_hash = cryptography::blake2b( [ - 1u64.to_bytes().unwrap(), + 5u64.to_bytes().unwrap(), expected_message.to_bytes().unwrap(), ] .concat(), @@ -1043,16 +1095,16 @@ fn should_produce_per_block_message_ordering() { &emitter_contract_hash, DEFAULT_BLOCK_TIME, ); - assert_last_message_block_index(0); + assert_last_message_block_index(4); //there are 4 system messages on contract install assert_eq!( query_message_count(), - Some((BlockTime::new(DEFAULT_BLOCK_TIME), 1)) + Some((BlockTime::new(DEFAULT_BLOCK_TIME), 5)) ); let expected_message = MessagePayload::from(format!("{}{}", EMITTER_MESSAGE_PREFIX, "test 0")); let expected_message_hash = cryptography::blake2b( [ - 0u64.to_bytes().unwrap(), + 4u64.to_bytes().unwrap(), expected_message.to_bytes().unwrap(), ] .concat(), @@ -1070,16 +1122,16 @@ fn should_produce_per_block_message_ordering() { &emitter_contract_hash, DEFAULT_BLOCK_TIME, ); - assert_last_message_block_index(1); + assert_last_message_block_index(5); assert_eq!( query_message_count(), - Some((BlockTime::new(DEFAULT_BLOCK_TIME), 2)) + Some((BlockTime::new(DEFAULT_BLOCK_TIME), 6)) ); let expected_message = MessagePayload::from(format!("{}{}", EMITTER_MESSAGE_PREFIX, "test 1")); let expected_message_hash = cryptography::blake2b( [ - 1u64.to_bytes().unwrap(), + 5u64.to_bytes().unwrap(), expected_message.to_bytes().unwrap(), ] .concat(), @@ -1116,16 +1168,16 @@ fn should_produce_per_block_message_ordering() { .exec(emit_message_request) .expect_success() .commit(); - assert_last_message_block_index(2); + assert_last_message_block_index(10); assert_eq!( query_message_count(), - Some((BlockTime::new(DEFAULT_BLOCK_TIME), 3)) + Some((BlockTime::new(DEFAULT_BLOCK_TIME), 11)) ); let expected_message = MessagePayload::from(format!("{}{}", EMITTER_MESSAGE_PREFIX, "test 2")); let expected_message_hash = cryptography::blake2b( [ - 2u64.to_bytes().unwrap(), + 10u64.to_bytes().unwrap(), expected_message.to_bytes().unwrap(), ] .concat(), @@ -1222,3 +1274,368 @@ fn emit_message_should_consume_variable_gas_based_on_topic_and_message_size() { + COST_PER_MESSAGE_LENGTH * payload.serialized_length() as u32; assert_eq!(emit_message_gas_consume, expected_consume.into()); } + +#[ignore] +#[test] +fn on_install_should_emit_system_messages() { + let system_account_entity = EntityAddr::Account(PublicKey::System.to_account_hash().value()); + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(LOCAL_GENESIS_REQUEST.clone()); + + let (contract_hash, contract_package_addr, wasm_addr) = + install_messages_emitter_contract_with_metadata(&builder, true); + + let query_view = ContractQueryView::new(&builder, contract_hash); + + // Verify the block-global message counter reflects the 4 system messages. + let block_message_count: (BlockTime, u64) = builder + .borrow_mut() + .query(None, Key::BlockGlobal(BlockGlobalAddr::MessageCount), &[]) + .expect("should have block message count") + .as_cl_value() + .expect("should be CLValue") + .clone() + .into_t() + .expect("should deserialize"); + assert_eq!( + block_message_count.1, 4, + "block message count should be 4 after install" + ); + + expect_message_on_topic_and_index( + &query_view, + &format!("hash-{}", base16::encode_lower(&contract_package_addr)), + "package_key", + system_account_entity, + 0, + 0, + ); + expect_message_on_topic_and_index( + &query_view, + &format!("hash-{}", base16::encode_lower(&contract_hash.value())), + "contract_key", + system_account_entity, + 1, + 0, + ); + expect_message_on_topic_and_index( + &query_view, + &format!("hash-{}", base16::encode_lower(&wasm_addr)), + "bytecode_key", + system_account_entity, + 2, + 0, + ); + expect_message_on_topic_and_index( + &query_view, + "2.1", + "contract_version", + system_account_entity, + 3, + 0, + ); +} + +fn expect_message_on_topic_and_index<'a>( + query_view: &'a ContractQueryView<'a>, + message: &str, + topic_name: &str, + entity_addr: EntityAddr, + index_in_block: u64, + index_in_topic: u32, +) { + let expected_message = MessagePayload::from(message); + let expected_message_hash = cryptography::blake2b( + [ + index_in_block.to_bytes().unwrap(), + expected_message.to_bytes().unwrap(), + ] + .concat(), + ); + let topic_name_hash = cryptography::blake2b(topic_name); + let queried_message_summary = query_view + .message_summary_for_entity(topic_name_hash.into(), index_in_topic, None, entity_addr) + .expect("should have value") + .value(); + assert_eq!(expected_message_hash, queried_message_summary); + assert_eq!( + query_view + .message_topic_for_entity(topic_name_hash.into(), entity_addr) + .message_count(), + 1 + ); +} + +#[ignore] +#[test] +fn after_upgrade_should_emit_system_messages() { + // release_1_5_8 should not have Key::MessageTopic entries under the system account. + let (initial_builder, lmdb_fixture_state, _temp_dir) = + lmdb_fixture::builder_from_global_state_fixture(lmdb_fixture::RELEASE_1_5_8); + let builder = RefCell::new(initial_builder); + + let current_protocol_version = lmdb_fixture_state.genesis_protocol_version(); + // Patch bump keeps protocol major the same, so installed contract versions are "1.x". + let new_protocol_version = ProtocolVersion::from_parts( + current_protocol_version.value().major, + current_protocol_version.value().minor, + current_protocol_version.value().patch + 1, + ); + + assert!( + query_system_message_topic_summary(&builder, "package_key").is_none(), + "package_key topic must not exist in pre-feature state" + ); + assert!( + query_system_message_topic_summary(&builder, "contract_key").is_none(), + "contract_key topic must not exist in pre-feature state" + ); + assert!( + query_system_message_topic_summary(&builder, "bytecode_key").is_none(), + "bytecode_key topic must not exist in pre-feature state" + ); + assert!( + query_system_message_topic_summary(&builder, "contract_version").is_none(), + "contract_version topic must not exist in pre-feature state" + ); + + let mut upgrade_request = UpgradeRequestBuilder::new() + .with_current_protocol_version(current_protocol_version) + .with_new_protocol_version(new_protocol_version) + .with_activation_point(EraId::new(1)) + .with_new_gas_hold_handling(HoldBalanceHandling::Accrued) + .with_new_gas_hold_interval(24 * 60 * 60 * 1000) + .build(); + builder + .borrow_mut() + .with_block_time(BlockTime::new(DEFAULT_BLOCK_TIME)) + .upgrade_using_scratch(&mut upgrade_request) + .expect_upgrade_success(); + + // After the protocol upgrade: all four system messaging topics must have been created. + assert!( + query_system_message_topic_summary(&builder, "package_key").is_some(), + "package_key topic must be created by protocol upgrade" + ); + assert!( + query_system_message_topic_summary(&builder, "contract_key").is_some(), + "contract_key topic must be created by protocol upgrade" + ); + assert!( + query_system_message_topic_summary(&builder, "bytecode_key").is_some(), + "bytecode_key topic must be created by protocol upgrade" + ); + assert!( + query_system_message_topic_summary(&builder, "contract_version").is_some(), + "contract_version topic must be created by protocol upgrade" + ); + + // Install a contract. The install should emit 4 system messages. + let (install_contract_hash, contract_package_addr, install_wasm_addr) = + install_messages_emitter_contract_with_metadata(&builder, false); + + let system_account_entity = EntityAddr::Account(PublicKey::System.to_account_hash().value()); + let query_view = ContractQueryView::new(&builder, install_contract_hash); + + expect_message_on_topic_and_index( + &query_view, + &format!("hash-{}", base16::encode_lower(&contract_package_addr)), + "package_key", + system_account_entity, + 0, + 0, + ); + expect_message_on_topic_and_index( + &query_view, + &format!( + "hash-{}", + base16::encode_lower(&install_contract_hash.value()) + ), + "contract_key", + system_account_entity, + 1, + 0, + ); + expect_message_on_topic_and_index( + &query_view, + &format!("hash-{}", base16::encode_lower(&install_wasm_addr)), + "bytecode_key", + system_account_entity, + 2, + 0, + ); + expect_message_on_topic_and_index( + &query_view, + "2.1", + "contract_version", + system_account_entity, + 3, + 0, + ); +} + +#[ignore] +#[test] +fn multiple_installs_in_same_block_have_sequential_block_indices() { + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(LOCAL_GENESIS_REQUEST.clone()); + + // First install at block time T — system messages use block indices 0-3. + let install_a_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MESSAGE_EMITTER_INSTALLER_WASM, + runtime_args! { ARG_REGISTER_DEFAULT_TOPIC_WITH_INIT => false }, + ) + .with_block_time(DEFAULT_BLOCK_TIME) + .build(); + builder + .borrow_mut() + .exec(install_a_request) + .expect_success() + .commit(); + + let block_count_after_a: (BlockTime, u64) = builder + .borrow_mut() + .query(None, Key::BlockGlobal(BlockGlobalAddr::MessageCount), &[]) + .expect("should have count") + .as_cl_value() + .expect("should be CLValue") + .clone() + .into_t() + .expect("should deserialize"); + assert_eq!(block_count_after_a.1, 4, "4 messages after first install"); + + // Second install at the same block time T — system messages continue from index 4. + let install_b_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MESSAGE_EMITTER_INSTALLER_WASM, + runtime_args! { ARG_REGISTER_DEFAULT_TOPIC_WITH_INIT => false }, + ) + .with_block_time(DEFAULT_BLOCK_TIME) + .build(); + builder + .borrow_mut() + .exec(install_b_request) + .expect_success() + .commit(); + + let block_count_after_b: (BlockTime, u64) = builder + .borrow_mut() + .query(None, Key::BlockGlobal(BlockGlobalAddr::MessageCount), &[]) + .expect("should have count") + .as_cl_value() + .expect("should be CLValue") + .clone() + .into_t() + .expect("should deserialize"); + assert_eq!( + block_count_after_b.1, 8, + "8 total messages after two installs in the same block" + ); +} + +fn query_system_message_topic_summary( + builder: &RefCell, + topic_name: &str, +) -> Option { + let system_account_entity = EntityAddr::Account(PublicKey::System.to_account_hash().value()); + let topic_name_hash = cryptography::blake2b(topic_name); + let topic_key = Key::message_topic(system_account_entity, topic_name_hash.into()); + match builder.borrow_mut().query(None, topic_key, &[]) { + Ok(StoredValue::MessageTopic(summary)) => Some(summary), + Ok(_) | Err(_) => None, + } +} + +#[ignore] +#[test] +fn protocol_upgrade_creates_messaging_topics_and_is_idempotent() { + let builder = RefCell::new(LmdbWasmTestBuilder::default()); + builder + .borrow_mut() + .run_genesis(LOCAL_GENESIS_REQUEST.clone()); + + // After genesis, the 4 system messaging topics must already exist. + assert!( + query_system_message_topic_summary(&builder, "package_key").is_some(), + "package_key topic must exist after genesis" + ); + assert!( + query_system_message_topic_summary(&builder, "contract_key").is_some(), + "contract_key topic must exist after genesis" + ); + assert!( + query_system_message_topic_summary(&builder, "bytecode_key").is_some(), + "bytecode_key topic must exist after genesis" + ); + assert!( + query_system_message_topic_summary(&builder, "contract_version").is_some(), + "contract_version topic must exist after genesis" + ); + + // Capture the blocktime stored in the topics before the first upgrade. + let blocktime_before = query_system_message_topic_summary(&builder, "package_key") + .unwrap() + .blocktime(); + + // First protocol upgrade. + let new_protocol_version = ProtocolVersion::from_parts( + DEFAULT_PROTOCOL_VERSION.value().major, + DEFAULT_PROTOCOL_VERSION.value().minor + 1, + 0, + ); + let mut upgrade_request = UpgradeRequestBuilder::new() + .with_current_protocol_version(DEFAULT_PROTOCOL_VERSION) + .with_new_protocol_version(new_protocol_version) + .with_activation_point(EraId::new(0)) + .build(); + builder + .borrow_mut() + .upgrade(&mut upgrade_request) + .expect_upgrade_success(); + + // Topics must still exist after upgrade. + assert!( + query_system_message_topic_summary(&builder, "package_key").is_some(), + "package_key topic must survive protocol upgrade" + ); + + // the topic was already created, so its blocktime is NOT updated by the + // upgrade (add_topic_to_system_account returns early if topic already exists). + let blocktime_after_first_upgrade = query_system_message_topic_summary(&builder, "package_key") + .unwrap() + .blocktime(); + assert_eq!( + blocktime_before, blocktime_after_first_upgrade, + "topic blocktime must not change on protocol upgrade (idempotency)" + ); + + // Second protocol upgrade — topics must remain unchanged. + let newer_protocol_version = ProtocolVersion::from_parts( + new_protocol_version.value().major, + new_protocol_version.value().minor + 1, + 0, + ); + let mut upgrade_request_2 = UpgradeRequestBuilder::new() + .with_current_protocol_version(new_protocol_version) + .with_new_protocol_version(newer_protocol_version) + .with_activation_point(EraId::new(0)) + .build(); + builder + .borrow_mut() + .upgrade(&mut upgrade_request_2) + .expect_upgrade_success(); + + let blocktime_after_second_upgrade = + query_system_message_topic_summary(&builder, "package_key") + .unwrap() + .blocktime(); + assert_eq!( + blocktime_before, blocktime_after_second_upgrade, + "topic blocktime must remain unchanged after second protocol upgrade" + ); +} diff --git a/executor/wasm_host/src/host.rs b/executor/wasm_host/src/host.rs index d0d519f01c..06ef06857f 100644 --- a/executor/wasm_host/src/host.rs +++ b/executor/wasm_host/src/host.rs @@ -23,7 +23,9 @@ use casper_executor_wasm_interface::{ }; use casper_storage::{ global_state::GlobalStateReader, - tracking_copy::{TrackingCopyEntityExt, TrackingCopyError, TrackingCopyExt}, + tracking_copy::{ + NewContractVersionInfo, TrackingCopyEntityExt, TrackingCopyError, TrackingCopyExt, + }, }; use casper_types::{ account::AccountHash, @@ -677,7 +679,7 @@ pub fn casper_create( seed, ); - smart_contract_package.insert_entity_version( + let inserted_version = smart_contract_package.insert_entity_version( protocol_version_major, EntityAddr::SmartContract(smart_contract_addr), ); @@ -744,6 +746,11 @@ pub fn casper_create( StoredValue::AddressableEntity(addressable_entity), )?; + let block_time = caller.context().block_time; + let package_key = Key::SmartContract(smart_contract_addr); + let version_major = inserted_version.protocol_version_major(); + let version_minor = inserted_version.entity_version(); + let _initial_state = match constructor_entry_point { Some(entry_point_name) => { // Take the gas spent so far and use it as a limit for the new VM. @@ -789,10 +796,11 @@ pub fn casper_create( cache, messages, }) => { - // output caller.consume_gas(gas_usage.gas_spent())?; if let Some(host_error) = host_error { + // Constructor failed — do not emit system messages; the install is + // considered unsuccessful. return Ok(host_error.into_u32()); } @@ -801,6 +809,28 @@ pub fn casper_create( .tracking_copy .apply_changes(effects, cache, messages); + // Constructor succeeded — now that all resources are committed, announce + // the new contract. + let message_limits = caller.context().message_limits; + if let Err(e) = caller + .context_mut() + .tracking_copy + .emit_messages_for_new_installed_version( + NewContractVersionInfo { + key_of_package: package_key, + key_of_contract: addressable_entity_key, + key_of_wasm: Key::ByteCode(bytecode_addr), + version_major, + version_minor, + }, + block_time, + message_limits, + ) + { + error!(?e, "Failed to emit system messages for contract install"); + return Err(VMError::Internal(InternalHostError::TrackingCopy)); + } + output } Err(execute_error) => { @@ -811,7 +841,28 @@ pub fn casper_create( } } } - None => None, + None => { + let message_limits = caller.context().message_limits; + if let Err(e) = caller + .context_mut() + .tracking_copy + .emit_messages_for_new_installed_version( + NewContractVersionInfo { + key_of_package: package_key, + key_of_contract: addressable_entity_key, + key_of_wasm: Key::ByteCode(bytecode_addr), + version_major, + version_minor, + }, + block_time, + message_limits, + ) + { + error!(?e, "Failed to emit system messages for contract install"); + return Err(VMError::Internal(InternalHostError::TrackingCopy)); + } + None + } }; let create_result = CreateResult { @@ -1375,22 +1426,58 @@ pub fn casper_upgrade( // TODO: Is validating new code worth it if the user pays for the storage anyway? Should we // protect users against invalid code? - // 2. Update the code therefore making hash(new_code) != addressable_entity.bytecode_addr (aka - // hash(old_code)) - let bytecode_key = Key::ByteCode(ByteCodeAddr::V2CasperWasm( - callee_addressable_entity.byte_code_addr(), - )); + // 2. Store the new code under a content-addressed key derived from its hash. This preserves old + // bytecode and lets the entity point at the new version unambiguously. + let new_bytecode_hash = chain_utils::compute_wasm_bytecode_hash(&code); + let new_bytecode_key = Key::ByteCode(ByteCodeAddr::V2CasperWasm(new_bytecode_hash)); metered_write( &mut caller, - bytecode_key, + new_bytecode_key, StoredValue::ByteCode(ByteCode::new( ByteCodeKind::V2CasperWasm, code.clone().into(), )), )?; - // 3. Execute upgrade routine (if specified) - // this code should handle reading old state, and saving new state + // Update the entity to reference the new bytecode hash. + let entity_addr = EntityAddr::SmartContract(smart_contract_addr); + let updated_entity = AddressableEntity::new( + callee_addressable_entity.package_hash(), + ByteCodeHash::new(new_bytecode_hash), + callee_addressable_entity.protocol_version(), + callee_addressable_entity.main_purse(), + callee_addressable_entity.associated_keys().clone(), + callee_addressable_entity.action_thresholds().clone(), + callee_addressable_entity.entity_kind(), + ); + metered_write( + &mut caller, + callee_addressable_entity_key, + StoredValue::AddressableEntity(updated_entity), + )?; + + // Insert a new version entry in the package and write the package back. + let block_time = caller.context().block_time; + let package_key = Key::SmartContract(smart_contract_addr); + + // Read the package, insert the new version entry, and write the updated package back. + // The read and write are split to avoid a double-borrow of caller. + let pkg_result = caller.context_mut().tracking_copy.read(&package_key); + let (version_major, version_minor, maybe_updated_package) = match pkg_result { + Ok(Some(StoredValue::SmartContract(mut package))) => { + let protocol_major = callee_addressable_entity.protocol_version().value().major; + let new_version_key = package.insert_entity_version(protocol_major, entity_addr); + ( + new_version_key.protocol_version_major(), + new_version_key.entity_version(), + Some(package), + ) + } + _ => (0, 0, None), + }; + if let Some(pkg) = maybe_updated_package { + metered_write(&mut caller, package_key, StoredValue::SmartContract(pkg))?; + } if let Some(entry_point_name) = entry_point { // Take the gas spent so far and use it as a limit for the new VM. @@ -1438,10 +1525,11 @@ pub fn casper_upgrade( cache, messages, }) => { - // output caller.consume_gas(gas_usage.gas_spent())?; if let Some(host_error) = host_error { + // Upgrade entry point failed — do not emit system messages; the upgrade is + // considered unsuccessful. return Ok(host_error.into_u32()); } @@ -1450,6 +1538,28 @@ pub fn casper_upgrade( .tracking_copy .apply_changes(effects, cache, messages); + // Upgrade entry point succeeded — now that all resources are committed, announce + // the new version. + let message_limits = caller.context().message_limits; + if let Err(e) = caller + .context_mut() + .tracking_copy + .emit_messages_for_new_installed_version( + NewContractVersionInfo { + key_of_package: package_key, + key_of_contract: callee_addressable_entity_key, + key_of_wasm: new_bytecode_key, + version_major, + version_minor, + }, + block_time, + message_limits, + ) + { + error!(?e, "Failed to emit system messages for contract upgrade"); + return Err(VMError::Internal(InternalHostError::TrackingCopy)); + } + if let Some(output) = output { info!( ?entry_point_name, @@ -1471,6 +1581,26 @@ pub fn casper_upgrade( return Err(VMError::Execute(execute_error)); } } + } else { + let message_limits = caller.context().message_limits; + if let Err(e) = caller + .context_mut() + .tracking_copy + .emit_messages_for_new_installed_version( + NewContractVersionInfo { + key_of_package: package_key, + key_of_contract: callee_addressable_entity_key, + key_of_wasm: new_bytecode_key, + version_major, + version_minor, + }, + block_time, + message_limits, + ) + { + error!(?e, "Failed to emit system messages for contract upgrade"); + return Err(VMError::Internal(InternalHostError::TrackingCopy)); + } } Ok(CALLEE_SUCCEEDED) diff --git a/storage/CHANGELOG.md b/storage/CHANGELOG.md index 1ea8760e07..9933643cf7 100644 --- a/storage/CHANGELOG.md +++ b/storage/CHANGELOG.md @@ -13,6 +13,14 @@ All notable changes to this project will be documented in this file. The format ### Added +* Added system-level messaging: when a new contract version is installed or upgraded, four + system messages are emitted on behalf of the system account advertising the package key, + entity key, bytecode key, and version of the new deployment. + **Note on system contracts:** The core system contracts (Mint, Auction, HandlePayment, + StandardPayment) do not emit these system messages. They are not deployed via the standard + WASM install/upgrade path and do not live in WASM space, so tracking them via the messaging + system is intentionally out of scope. + * Added a field `rewards` handling to Config in the `runtime_native` module_ ### Changed diff --git a/storage/Cargo.toml b/storage/Cargo.toml index 39b141f0ea..8d08ffac8a 100644 --- a/storage/Cargo.toml +++ b/storage/Cargo.toml @@ -12,6 +12,7 @@ license = "Apache-2.0" [dependencies] bincode = "1.3.1" +blake2 = { version = "0.10.6", default-features = false } casper-types = { version = "7.0.0", path = "../types", features = ["datasize", "json-schema", "std"] } datasize = "0.2.4" either = "1.8.1" diff --git a/storage/src/lib.rs b/storage/src/lib.rs index fc242f4336..a2ca746a69 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -31,3 +31,8 @@ pub use block_store::{ lmdb::{DbTableId, UnknownDbTableId}, DbRawBytesSpec, }; + +pub(crate) const MESSAGING_PACKAGE_ADDR_TOPIC: &str = "package_key"; +pub(crate) const MESSAGING_CONTRACT_ADDR_TOPIC: &str = "contract_key"; +pub(crate) const MESSAGING_CONTRACT_BYTECODE_ADDR_TOPIC: &str = "bytecode_key"; +pub(crate) const MESSAGING_CONTRACT_VERSION_TOPIC: &str = "contract_version"; diff --git a/storage/src/system/genesis/account_contract_installer.rs b/storage/src/system/genesis/account_contract_installer.rs index 7472b62300..08aa913638 100644 --- a/storage/src/system/genesis/account_contract_installer.rs +++ b/storage/src/system/genesis/account_contract_installer.rs @@ -10,10 +10,7 @@ use std::{ use crate::{ global_state::state::StateProvider, - system::{ - genesis::{GenesisError, DEFAULT_ADDRESS, NO_WASM}, - protocol_upgrade::ProtocolUpgradeError, - }, + system::genesis::{GenesisError, DEFAULT_ADDRESS, NO_WASM}, tracking_copy::AddResult, AddressGenerator, TrackingCopy, }; @@ -49,7 +46,7 @@ use casper_types::{ standard_payment, SystemEntityType, AUCTION, HANDLE_PAYMENT, MINT, STANDARD_PAYMENT, }, AccessRights, Account, AddressableEntity, AddressableEntityHash, AdministratorAccount, - BlockGlobalAddr, ByteCode, ByteCodeAddr, ByteCodeHash, ByteCodeKind, CLValue, + BlockGlobalAddr, BlockTime, ByteCode, ByteCodeAddr, ByteCodeHash, ByteCodeKind, CLValue, ChainspecRegistry, Contract, ContractWasm, ContractWasmHash, Digest, EntityAddr, EntityKind, EntityVersions, EntryPointAddr, EntryPointValue, EntryPoints, EraId, GenesisAccount, GenesisConfig, Groups, HashAddr, Key, Motes, Package, PackageHash, PackageStatus, Phase, @@ -855,6 +852,11 @@ where // Write block time to global state self.store_block_time()?; + + self.tracking_copy + .borrow_mut() + .add_system_message_topics(BlockTime::new(self.config.genesis_timestamp_millis())) + .map_err(|e| Box::new(GenesisError::TrackingCopy(e)))?; Ok(()) } } diff --git a/storage/src/system/genesis/entity_installer.rs b/storage/src/system/genesis/entity_installer.rs index ff57767789..f108452cad 100644 --- a/storage/src/system/genesis/entity_installer.rs +++ b/storage/src/system/genesis/entity_installer.rs @@ -41,9 +41,9 @@ use casper_types::{ SystemEntityType, AUCTION, HANDLE_PAYMENT, MINT, }, AccessRights, AddressableEntity, AddressableEntityHash, AdministratorAccount, BlockGlobalAddr, - ByteCode, ByteCodeAddr, ByteCodeHash, ByteCodeKind, CLValue, ChainspecRegistry, Digest, - EntityAddr, EntityKind, EntityVersions, EntryPointAddr, EntryPointValue, EntryPoints, EraId, - GenesisAccount, GenesisConfig, Groups, HashAddr, Key, Motes, Package, PackageHash, + BlockTime, ByteCode, ByteCodeAddr, ByteCodeHash, ByteCodeKind, CLValue, ChainspecRegistry, + Digest, EntityAddr, EntityKind, EntityVersions, EntryPointAddr, EntryPointValue, EntryPoints, + EraId, GenesisAccount, GenesisConfig, Groups, HashAddr, Key, Motes, Package, PackageHash, PackageStatus, Phase, ProtocolVersion, PublicKey, StoredValue, SystemHashRegistry, Tagged, URef, U512, }; @@ -890,6 +890,11 @@ where // Write block time to global state self.store_block_time()?; + self.tracking_copy + .borrow_mut() + .add_system_message_topics(BlockTime::new(self.config.genesis_timestamp_millis())) + .map_err(|e| Box::new(GenesisError::TrackingCopy(e)))?; + Ok(()) } } diff --git a/storage/src/system/protocol_upgrade.rs b/storage/src/system/protocol_upgrade.rs index dd7e696719..53fd45839a 100644 --- a/storage/src/system/protocol_upgrade.rs +++ b/storage/src/system/protocol_upgrade.rs @@ -1,4 +1,8 @@ //! Support for applying upgrades on the execution engine. +use blake2::{ + digest::{Update, VariableOutput}, + Blake2bVar, +}; use num_rational::Ratio; use std::{ cell::RefCell, @@ -317,6 +321,11 @@ where let system_hash_addresses = SystemHashAddresses::new(mint, auction, handle_payment); + let block_time = self.tracking_copy.get_block_time()?.unwrap_or_default(); + self.tracking_copy + .add_system_message_topics(block_time) + .map_err(ProtocolUpgradeError::TrackingCopy)?; + Ok(system_hash_addresses) } @@ -1880,3 +1889,14 @@ enum AccountRepr { Account(Account), Entity(AddressableEntity), } + +const DIGEST_LENGTH: usize = 32; + +/// The 32-byte digest blake2b hash function +pub fn blake2b>(data: T) -> [u8; DIGEST_LENGTH] { + let mut result = [0; DIGEST_LENGTH]; + let mut hasher = Blake2bVar::new(DIGEST_LENGTH).expect("should create hasher"); + hasher.update(data.as_ref()); + hasher.finalize_variable(&mut result).ok(); + result +} diff --git a/storage/src/tracking_copy/messages.rs b/storage/src/tracking_copy/messages.rs new file mode 100644 index 0000000000..98cbe11e17 --- /dev/null +++ b/storage/src/tracking_copy/messages.rs @@ -0,0 +1,242 @@ +use std::convert::TryFrom; + +use crate::{ + global_state::{error::Error, state::StateReader}, + system::protocol_upgrade::blake2b, + tracking_copy::TrackingCopyError, + TrackingCopy, MESSAGING_CONTRACT_ADDR_TOPIC, MESSAGING_CONTRACT_BYTECODE_ADDR_TOPIC, + MESSAGING_CONTRACT_VERSION_TOPIC, MESSAGING_PACKAGE_ADDR_TOPIC, +}; + +use casper_types::{ + bytesrepr, + bytesrepr::ToBytes, + contract_messages::{Message, MessageAddr, MessagePayload, MessageTopicSummary}, + BlockGlobalAddr, BlockTime, CLValue, CLValueError, EntityAddr, Key, MessageLimits, PublicKey, + StoredValue, StoredValueTypeMismatch, +}; +use thiserror::Error; + +/// Errors that can be returned by the code emitting messages for a new contract version +#[derive(Debug, Error)] +pub enum MessageEmissionError { + /// Error occurred when trying to type downcast CLValue + #[error("Error occurred when trying to type downcast CLValue: {0}")] + CLValue(CLValueError), + /// Error when fetching data from the tracking copy + #[error("Error when fetching data from the tracking copy: {0}")] + TrackingCopy(TrackingCopyError), + /// Error when casting types + #[error("Error when casting types: {0}")] + TypeMismatch(StoredValueTypeMismatch), + /// Error when formatting a data structure in binary format + #[error("Error when formatting a data structure in binary format: {0}")] + BytesRepr(bytesrepr::Error), + /// Given topic was requested but does not exist + #[error("Given topic was requested but does not exist: {0}")] + TopicNotRegistered(Key), + /// Given topic is full + #[error("Given topic is full: {0}")] + TopicFull(Key), + /// No more messages in block allowed + #[error("No more messages in block allowed")] + MaxMessagesPerBlockExceeded, +} + +impl From for MessageEmissionError { + fn from(err: TrackingCopyError) -> Self { + MessageEmissionError::TrackingCopy(err) + } +} + +impl From for MessageEmissionError { + fn from(err: CLValueError) -> Self { + MessageEmissionError::CLValue(err) + } +} + +struct MessageEmitter; +impl MessageEmitter { + fn emit_message_for_entity( + tracking_copy: &mut TrackingCopy, + entity_addr: EntityAddr, + topic_name: &str, + message_payload: MessagePayload, + current_blocktime: BlockTime, + ) -> Result<(), MessageEmissionError> + where + T: StateReader, + { + let topic_name_hash = blake2b(topic_name).into(); + let topic_key = Key::Message(MessageAddr::new_topic_addr(entity_addr, topic_name_hash)); + let Some(StoredValue::MessageTopic(prev_topic_summary)) = tracking_copy + .read(&topic_key) + .map_err(MessageEmissionError::TrackingCopy)? + else { + return Err(MessageEmissionError::TopicNotRegistered(topic_key)); + }; + + let topic_message_index = if prev_topic_summary.blocktime() != current_blocktime { + for index in 1..prev_topic_summary.message_count() { + tracking_copy.prune(Key::message(entity_addr, topic_name_hash, index)); + } + 0 + } else { + prev_topic_summary.message_count() + }; + + let block_message_index: u64 = match tracking_copy + .read(&Key::BlockGlobal(BlockGlobalAddr::MessageCount))? + { + Some(stored_value) => { + let (prev_block_time, prev_count): (BlockTime, u64) = CLValue::into_t( + CLValue::try_from(stored_value).map_err(MessageEmissionError::TypeMismatch)?, + ) + .map_err(MessageEmissionError::CLValue)?; + if prev_block_time == current_blocktime { + prev_count + } else { + 0 + } + } + None => 0, + }; + + let Some(topic_message_count) = topic_message_index.checked_add(1) else { + return Err(MessageEmissionError::TopicFull(topic_key)); + }; + + let Some(block_message_count) = block_message_index.checked_add(1) else { + return Err(MessageEmissionError::MaxMessagesPerBlockExceeded); + }; + let message = Message::new( + entity_addr, + message_payload, + topic_name.to_string(), + topic_name_hash, + topic_message_index, + block_message_index, + ); + let topic_value = StoredValue::MessageTopic(MessageTopicSummary::new( + topic_message_count, + current_blocktime, + message.topic_name().to_owned(), + )); + let message_key = message.message_key(); + let message_value = StoredValue::Message( + message + .checksum() + .map_err(MessageEmissionError::BytesRepr)?, + ); + let cl_value = CLValue::from_t((current_blocktime, block_message_count))?; + let block_message_count_value = StoredValue::CLValue(cl_value); + + tracking_copy.emit_message( + topic_key, + topic_value, + message_key, + message_value, + block_message_count_value, + message, + ); + Ok(()) + } +} + +/// Identity information for a newly installed or upgraded contract version. +pub struct NewContractVersionInfo { + /// Key of the contract package. + pub key_of_package: Key, + /// Key of the contract/entity. + pub key_of_contract: Key, + /// Key of the wasm/bytecode. + pub key_of_wasm: Key, + /// Major protocol version. + pub version_major: u32, + /// Minor/entity version. + pub version_minor: u32, +} + +pub(crate) struct NewContractMessagesEmitter { + key_of_package: Key, + key_of_contract: Key, + key_of_wasm: Key, + version_major: u32, + version_minor: u32, +} + +impl NewContractMessagesEmitter { + pub(crate) fn new(info: NewContractVersionInfo) -> Self { + Self { + key_of_package: info.key_of_package, + key_of_contract: info.key_of_contract, + key_of_wasm: info.key_of_wasm, + version_major: info.version_major, + version_minor: info.version_minor, + } + } + + pub(crate) fn emit_contract_creation_messages( + &self, + tracking_copy: &mut TrackingCopy, + current_blocktime: BlockTime, + message_limits: MessageLimits, + ) -> Result<(), MessageEmissionError> + where + T: StateReader, + { + let system_account_hash = PublicKey::System.to_account_hash().value(); + let entity_addr = EntityAddr::Account(system_account_hash); + + let pkg_payload = MessagePayload::String(self.key_of_package.to_formatted_string()); + let contract_payload = MessagePayload::String(self.key_of_contract.to_formatted_string()); + let wasm_payload = MessagePayload::String(self.key_of_wasm.to_formatted_string()); + let version_payload = + MessagePayload::String(format!("{}.{}", self.version_major, self.version_minor)); + + // If any payload would exceed the configured message size limit, silently skip all system + // messages. System messages are best-effort observability signals; exceeding limits only + // occurs with unusually small chainspec values and should not abort contract installation. + let max = message_limits.max_message_size() as usize; + if pkg_payload.serialized_length() > max + || contract_payload.serialized_length() > max + || wasm_payload.serialized_length() > max + || version_payload.serialized_length() > max + { + return Ok(()); + } + + MessageEmitter::emit_message_for_entity( + tracking_copy, + entity_addr, + MESSAGING_PACKAGE_ADDR_TOPIC, + pkg_payload, + current_blocktime, + )?; + + MessageEmitter::emit_message_for_entity( + tracking_copy, + entity_addr, + MESSAGING_CONTRACT_ADDR_TOPIC, + contract_payload, + current_blocktime, + )?; + + MessageEmitter::emit_message_for_entity( + tracking_copy, + entity_addr, + MESSAGING_CONTRACT_BYTECODE_ADDR_TOPIC, + wasm_payload, + current_blocktime, + )?; + + MessageEmitter::emit_message_for_entity( + tracking_copy, + entity_addr, + MESSAGING_CONTRACT_VERSION_TOPIC, + version_payload, + current_blocktime, + )?; + Ok(()) + } +} diff --git a/storage/src/tracking_copy/mod.rs b/storage/src/tracking_copy/mod.rs index c7be3cfe9e..c56a6d6ec0 100644 --- a/storage/src/tracking_copy/mod.rs +++ b/storage/src/tracking_copy/mod.rs @@ -5,6 +5,7 @@ mod byte_size; mod error; mod ext; mod ext_entity; +mod messages; mod meter; #[cfg(test)] mod tests; @@ -26,19 +27,23 @@ use crate::{ error::Error as GlobalStateError, state::StateReader, trie_store::operations::compute_state_hash, DEFAULT_MAX_QUERY_DEPTH, }, - KeyPrefix, + system::protocol_upgrade::blake2b, + tracking_copy::messages::NewContractMessagesEmitter, + KeyPrefix, MESSAGING_CONTRACT_ADDR_TOPIC, MESSAGING_CONTRACT_BYTECODE_ADDR_TOPIC, + MESSAGING_CONTRACT_VERSION_TOPIC, MESSAGING_PACKAGE_ADDR_TOPIC, }; use casper_types::{ addressable_entity::NamedKeyAddr, bytesrepr::{self, ToBytes}, - contract_messages::{Message, Messages}, + contract_messages::{Message, MessageTopicSummary, Messages}, contracts::NamedKeys, execution::{ Effects, RetValue, TransformError, TransformInstruction, TransformKindV2, TransformV2, }, global_state::TrieMerkleProof, - handle_stored_dictionary_value, BlockGlobalAddr, CLType, CLValue, CLValueError, Digest, - HashAddr, Key, KeyTag, StoredValue, StoredValueTypeMismatch, U512, + handle_stored_dictionary_value, BlockGlobalAddr, BlockTime, CLType, CLValue, CLValueError, + Digest, EntityAddr, HashAddr, Key, KeyTag, MessageLimits, PublicKey, StoredValue, + StoredValueTypeMismatch, U512, }; use self::meter::{heap_meter::HeapSize, Meter}; @@ -46,6 +51,7 @@ pub use self::{ error::Error as TrackingCopyError, ext::TrackingCopyExt, ext_entity::{FeesPurseHandling, TrackingCopyEntityExt}, + messages::{MessageEmissionError, NewContractVersionInfo}, }; /// Result of a query on a `TrackingCopy`. @@ -451,7 +457,7 @@ where cache: self.cache.clone(), effects: self.effects.clone(), max_query_depth: self.max_query_depth, - messages: self.messages.clone(), + messages: Vec::new(), enable_addressable_entity: self.enable_addressable_entity, } } @@ -470,7 +476,9 @@ where ) { self.effects = effects; self.cache = cache; - self.messages = messages; + // Extend rather than replace: forks start with empty messages (see fork2), so the caller's + // pre-fork messages remain in self while the fork's new messages are appended here. + self.messages.extend(messages); } /// Returns a copy of the execution effects cached by this instance. @@ -715,6 +723,52 @@ where self.messages.clone() } + /// Creates the four system messaging topics under the system account, if they don't already + /// exist. Idempotent: a topic already present is left unchanged. + pub fn add_system_message_topics( + &mut self, + block_time: BlockTime, + ) -> Result<(), TrackingCopyError> { + self.add_system_message_topic(block_time, MESSAGING_PACKAGE_ADDR_TOPIC)?; + self.add_system_message_topic(block_time, MESSAGING_CONTRACT_ADDR_TOPIC)?; + self.add_system_message_topic(block_time, MESSAGING_CONTRACT_BYTECODE_ADDR_TOPIC)?; + self.add_system_message_topic(block_time, MESSAGING_CONTRACT_VERSION_TOPIC)?; + Ok(()) + } + + fn add_system_message_topic( + &mut self, + block_time: BlockTime, + topic_name: &str, + ) -> Result<(), TrackingCopyError> { + let entity_addr = EntityAddr::new_account(PublicKey::System.to_account_hash().value()); + let topic_name_hash = blake2b(topic_name.as_bytes()).into(); + let topic_key = Key::message_topic(entity_addr, topic_name_hash); + if self.get(&topic_key)?.is_some() { + return Ok(()); + } + self.write( + topic_key, + StoredValue::MessageTopic(MessageTopicSummary::new( + 0, + block_time, + topic_name.to_owned(), + )), + ); + Ok(()) + } + + /// Emits system messages for a newly installed or upgraded contract version. + pub fn emit_messages_for_new_installed_version( + &mut self, + info: NewContractVersionInfo, + current_blocktime: BlockTime, + message_limits: MessageLimits, + ) -> Result<(), MessageEmissionError> { + let emitter = NewContractMessagesEmitter::new(info); + emitter.emit_contract_creation_messages(self, current_blocktime, message_limits) + } + /// 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`. diff --git a/storage/src/tracking_copy/tests.rs b/storage/src/tracking_copy/tests.rs index 0eac9da17b..b211f9c0a0 100644 --- a/storage/src/tracking_copy/tests.rs +++ b/storage/src/tracking_copy/tests.rs @@ -13,14 +13,14 @@ use casper_types::{ ActionThresholds, AddressableEntityHash, AssociatedKeys, NamedKeyAddr, NamedKeyValue, Weight, }, - contract_messages::Messages, + contract_messages::{Message, MessagePayload, Messages, TopicNameHash}, contracts::{EntryPoints as ContractEntryPoints, NamedKeys}, execution::{Effects, TransformKindV2, TransformV2}, gens::*, global_state::TrieMerkleProof, - handle_stored_dictionary_value, AccessRights, AddressableEntity, ByteCodeHash, CLValue, - CLValueDictionary, CLValueError, ContractRuntimeTag, EntityAddr, EntityKind, HashAddr, Key, - KeyTag, PackageHash, ProtocolVersion, StoredValue, URef, U256, U512, UREF_ADDR_LENGTH, + handle_stored_dictionary_value, AccessRights, AddressableEntity, BlockTime, ByteCodeHash, + CLValue, CLValueDictionary, CLValueError, ContractRuntimeTag, EntityAddr, EntityKind, HashAddr, + Key, KeyTag, PackageHash, ProtocolVersion, StoredValue, URef, U256, U512, UREF_ADDR_LENGTH, }; use super::{ @@ -1257,3 +1257,90 @@ fn tracking_copy_keys_with_prefix_should_include_pruning_of_uncommited_keys() { BTreeSet::from_iter(vec![key]) ); } + +fn make_mock_message(index: u64) -> Message { + let entity_addr = EntityAddr::new_account([0u8; 32]); + let topic_name_hash = TopicNameHash::new([1u8; 32]); + Message::new( + entity_addr, + MessagePayload::from(format!("msg-{index}")), + "topic".to_string(), + topic_name_hash, + 0, + index, + ) +} + +fn emit_mock_message(tc: &mut TrackingCopy, index: u64) { + tc.emit_message( + Key::Hash([0u8; 32]), + StoredValue::CLValue(CLValue::from_t(0i32).unwrap()), + Key::Hash([(index as u8) + 1; 32]), + StoredValue::CLValue(CLValue::from_t(0i32).unwrap()), + StoredValue::CLValue(CLValue::from_t((BlockTime::new(1), index + 1)).unwrap()), + make_mock_message(index), + ); +} + +#[test] +fn fork2_starts_with_empty_messages_and_parent_retains_pre_fork_messages() { + let counter = Arc::new(RwLock::new(0)); + let db = CountingDb::new(counter); + let mut tc = TrackingCopy::new(db, DEFAULT_MAX_QUERY_DEPTH, DEFAULT_ENABLE_ENTITY); + + // Emit system messages to the MAIN tracking copy (as the fix does before forking). + emit_mock_message(&mut tc, 0); + emit_mock_message(&mut tc, 1); + assert_eq!(tc.messages().len(), 2, "main TC should have 2 messages"); + + // Fork (simulating constructor/upgrade-entry-point execution). + let fork = tc.fork2(); + assert_eq!( + fork.messages().len(), + 0, + "fork must start with empty messages to avoid double-counting on apply_changes" + ); + + // Discard the fork without calling apply_changes (simulating a host error / failed + // constructor). + drop(fork); + + // System messages must still be present in the main tracking copy. + assert_eq!( + tc.messages().len(), + 2, + "system messages emitted before fork must survive even when fork is discarded" + ); +} + +#[test] +fn apply_changes_merges_fork_messages_without_duplicating_pre_fork_messages() { + let counter = Arc::new(RwLock::new(0)); + let db = CountingDb::new(counter); + let mut tc = TrackingCopy::new(db, DEFAULT_MAX_QUERY_DEPTH, DEFAULT_ENABLE_ENTITY); + + // Emit system messages to main before fork (the fixed behavior). + emit_mock_message(&mut tc, 0); + + // Fork and emit a constructor message into the fork. + let mut fork = tc.fork2(); + emit_mock_message(&mut fork, 1); + assert_eq!( + fork.messages().len(), + 1, + "fork has only the constructor message" + ); + + // Merge fork into main (success path). + let fork_effects = fork.effects(); + let fork_cache = fork.cache(); + let fork_messages = fork.messages(); + tc.apply_changes(fork_effects, fork_cache, fork_messages); + + // Main TC should have both: the pre-fork system message AND the constructor message. + assert_eq!( + tc.messages().len(), + 2, + "apply_changes must merge fork messages without duplicating pre-fork ones" + ); +}