diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/mod.rs index 1196d4ebe92..9ef1f1ce0e6 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::error::execution::ExecutionError; use crate::error::Error; @@ -17,7 +18,8 @@ where /// /// This function updates both the masternode list and the quorum information in the platform /// state. It calls the update_masternode_list and update_quorum_info functions to perform - /// the respective updates. + /// the respective updates. From version 1 it first records in Drive, for later steps to + /// read, the Core forks it sees active for the first time. /// /// # Arguments /// @@ -32,7 +34,8 @@ where /// # Returns /// /// * Result<(), Error> - Returns Ok(()) if the update is successful. Returns an error if - /// there is a problem updating the masternode list, quorum information, or the state. + /// there is a problem updating the masternode list, quorum information, or the state, and + /// from version 1 if Core cannot tell whether a fork is active. #[allow(clippy::too_many_arguments)] pub(crate) fn update_core_info( &self, @@ -59,9 +62,18 @@ where transaction, platform_version, ), + 1 => self.update_core_info_v1( + platform_state, + block_platform_state, + core_block_height, + is_init_chain, + block_info, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "update_core_info".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs index 91564d2db1c..ee446fe6a69 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs @@ -82,8 +82,9 @@ mod tests { /// fire RPC calls we haven't set up in the mock. #[test] fn v0_returns_ok_when_same_core_height_and_not_init() { - let platform_version = PlatformVersion::latest(); + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -125,8 +126,9 @@ mod tests { fn v0_short_circuit_is_idempotent_when_not_init_chain() { // Re-confirm that both conditions together are required by running // the short-circuit case twice; idempotency guards against flakiness. - let platform_version = PlatformVersion::latest(); + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v1/mod.rs new file mode 100644 index 00000000000..3900d715a64 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v1/mod.rs @@ -0,0 +1,594 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::platform_types::platform_state::PlatformState; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::rpc::core::{CoreRPCLike, V24_COINBASE_PAYLOAD_VERSION}; +use dpp::block::block_info::BlockInfo; +use dpp::version::PlatformVersion; +use drive::drive::system::core_fork_activations::CoreFork; +use drive::grovedb::Transaction; + +impl Platform +where + C: CoreRPCLike, +{ + /// Updates the core information in the platform state based on the given core block height. + /// + /// Version 0's steps, after first recording the Core forks seen active for the first time + /// at `core_block_height` (`record_core_fork_activations_v1`). Like the other steps, the + /// recording runs only at init chain and in a block that moves the chain locked height. + /// + /// # Arguments + /// + /// * platform_state - A reference to the platform state before execution of current block. + /// * block_platform_state - A mutable reference to the current platform state in the block + /// execution context to be updated. + /// * core_block_height - The current block height in the Dash Core. + /// * is_init_chain - A boolean indicating if the chain is being initialized. + /// * block_info - A reference to the block information. + /// * transaction - The current groveDB transaction. + /// + /// # Returns + /// + /// * Result<(), Error> - Returns Ok(()) if the update is successful. Returns an error if + /// Core cannot tell whether a fork is active, or if there is a problem updating the + /// masternode list, quorum information, or the state. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn update_core_info_v1( + &self, + platform_state: Option<&PlatformState>, + block_platform_state: &mut PlatformState, + core_block_height: u32, + is_init_chain: bool, + block_info: &BlockInfo, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + // the core height of the block platform state is the last committed + if !is_init_chain && block_platform_state.last_committed_core_height() == core_block_height + { + // if we get the same height that we know we do not need to update core info + return Ok(()); + } + + self.record_core_fork_activations_v1(core_block_height, transaction, platform_version)?; + + self.update_masternode_list( + platform_state, + block_platform_state, + core_block_height, + is_init_chain, + block_info, + transaction, + platform_version, + )?; + + self.update_quorum_info( + platform_state, + block_platform_state, + core_block_height, + false, + platform_version, + ) + } + + /// Records `core_block_height` as the height at which Core's V24 is active, if Core's V24 + /// is active there and nothing is recorded yet. This generation tracks V24 only; tracking + /// another fork needs a new generation with that fork's signal. + /// + /// V24's signal is the payload version of the coinbase of the Core block at + /// `core_block_height`: Core requires version 4 exactly while V24 is active, so every node + /// gets the same answer at a chain locked height. Forks never switch off, so once V24 is + /// recorded Core is not asked again. A Core error or a coinbase without a payload fails the + /// block rather than read as "not active". + /// + /// The record is read and written only through the block's transaction, so a round that is + /// thrown away takes its write with it. + fn record_core_fork_activations_v1( + &self, + core_block_height: u32, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + if self + .drive + .fetch_core_fork_activation_height(CoreFork::V24, Some(transaction), platform_version)? + .is_some() + { + return Ok(()); + } + // Every block at a Core height Platform follows carries a coinbase payload. + let coinbase_payload_version = self + .core_rpc + .get_coinbase_payload_version(core_block_height) + .inspect_err(|error| { + tracing::error!( + core_fork = ?CoreFork::V24, + core_block_height, + ?error, + "could not read the coinbase that tells whether a core fork is active" + ) + })? + .ok_or_else(|| { + ExecutionError::DashCoreBadResponseError(format!( + "the coinbase of the block at core height {core_block_height} has no payload" + )) + })?; + if coinbase_payload_version < V24_COINBASE_PAYLOAD_VERSION { + tracing::debug!( + core_fork = ?CoreFork::V24, + core_block_height, + coinbase_payload_version, + "core fork not active at this chain locked core height" + ); + return Ok(()); + } + self.drive.store_core_fork_activation_height( + CoreFork::V24, + core_block_height, + Some(transaction), + platform_version, + )?; + tracing::info!( + core_fork = ?CoreFork::V24, + core_block_height, + coinbase_payload_version, + "core fork active at this chain locked core height, recorded in the block being executed" + ); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::error::execution::ExecutionError; + use crate::error::Error; + use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::block::block_info::BlockInfo; + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0; + use dpp::dashcore_rpc::dashcore_rpc_json::{ExtendedQuorumListResult, MasternodeListDiff}; + use dpp::dashcore_rpc::Error as CoreRpcError; + use dpp::version::PlatformVersion; + use drive::drive::system::core_fork_activations::CoreFork; + use drive::grovedb::Transaction; + use std::collections::HashMap; + use std::sync::{Arc, Mutex}; + + /// The Core heights Platform asked the coinbase payload version of, in order + type CoinbaseReads = Arc>>; + + /// A platform at `platform_version` whose Core reports no masternode list changes and no + /// quorums, and `coinbase` for the coinbase payload version at each height. Returns the + /// heights Platform reads the coinbase at. + fn platform_with_core( + platform_version: &PlatformVersion, + coinbase: impl Fn(u32) -> Result, CoreRpcError> + Send + 'static, + ) -> (TempPlatform, CoinbaseReads) { + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + // Without the default answers, which would shadow the ones set here. + platform.core_rpc.checkpoint(); + platform + .core_rpc + .expect_get_protx_diff_with_masternodes() + .returning(|base_height, block_height| { + Ok(MasternodeListDiff { + base_height: base_height.unwrap_or_default(), + block_height, + added_mns: vec![], + removed_mns: vec![], + updated_mns: vec![], + }) + }); + let quorum_types = [ + platform.config.validator_set.quorum_type, + platform.config.chain_lock.quorum_type, + platform.config.instant_lock.quorum_type, + ]; + platform + .core_rpc + .expect_get_quorum_listextended() + .returning(move |_| { + Ok(ExtendedQuorumListResult { + quorums_by_type: quorum_types + .iter() + .map(|quorum_type| (*quorum_type, Default::default())) + .collect::>(), + }) + }); + let reads = CoinbaseReads::default(); + let recorded_reads = Arc::clone(&reads); + platform + .core_rpc + .expect_get_coinbase_payload_version() + .returning(move |core_height| { + recorded_reads + .lock() + .expect("expected the coinbase reads") + .push(core_height); + coinbase(core_height) + }); + (platform, reads) + } + + /// A Core whose coinbase has payload version 4 from `activation_height` on, as Core requires + /// while V24 is active, and 3 before. + fn v24_from(activation_height: u32) -> impl Fn(u32) -> Result, CoreRpcError> { + move |core_height| { + Ok(Some(if core_height >= activation_height { + 4 + } else { + 3 + })) + } + } + + /// Updates the core info of a block at chain locked height `core_height` whose previous + /// block was committed at `previous_core_height`, as `run_block_proposal` does. + fn run_block( + platform: &TempPlatform, + block_state: &mut PlatformState, + previous_core_height: u32, + core_height: u32, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let height = block_state.last_committed_block_height() + 1; + block_state.set_last_committed_block_info(Some( + ExtendedBlockInfoV0 { + basic_info: BlockInfo { + height: height - 1, + core_height: previous_core_height, + ..Default::default() + }, + app_hash: [0; 32], + quorum_hash: [0; 32], + block_id_hash: [0; 32], + proposer_pro_tx_hash: [0; 32], + signature: [0; 96], + round: 0, + } + .into(), + )); + platform.update_core_info( + None, + block_state, + core_height, + false, + &BlockInfo { + height, + core_height, + ..Default::default() + }, + transaction, + platform_version, + ) + } + + fn recorded_v24( + platform: &TempPlatform, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Option { + platform + .drive + .fetch_core_fork_activation_height(CoreFork::V24, Some(transaction), platform_version) + .expect("expected to read the V24 record") + } + + /// Core requires coinbase payload version 4 exactly while V24 is active, and refuses it + /// otherwise; a later version keeps V24's fields, so it means V24 too. + #[test] + fn should_record_v24_where_the_coinbase_has_payload_version_4_or_more() { + let platform_version = PlatformVersion::latest(); + for (payload_version, expected) in [(3, None), (4, Some(101)), (5, Some(101))] { + let (platform, reads) = + platform_with_core(platform_version, move |_| Ok(Some(payload_version))); + let transaction = platform.drive.grove.start_transaction(); + let mut block_state = platform.state.load().as_ref().clone(); + + run_block( + &platform, + &mut block_state, + 100, + 101, + &transaction, + platform_version, + ) + .expect("expected to update the core info"); + + assert_eq!( + recorded_v24(&platform, &transaction, platform_version), + expected, + "coinbase payload version {payload_version}" + ); + assert_eq!(*reads.lock().unwrap(), vec![101]); + } + } + + /// Forks never switch off, so once V24 is recorded no block depends on Core about it. + #[test] + fn should_never_ask_core_about_v24_once_it_is_recorded() { + let platform_version = PlatformVersion::latest(); + let (platform, reads) = platform_with_core(platform_version, |core_height| { + panic!("Core asked about the coinbase at {core_height} after V24 was recorded") + }); + let transaction = platform.drive.grove.start_transaction(); + platform + .drive + .store_core_fork_activation_height( + CoreFork::V24, + 90, + Some(&transaction), + platform_version, + ) + .expect("expected to record V24"); + let mut block_state = platform.state.load().as_ref().clone(); + + for (previous_core_height, core_height) in [(100, 101), (101, 150)] { + run_block( + &platform, + &mut block_state, + previous_core_height, + core_height, + &transaction, + platform_version, + ) + .expect("expected to update the core info"); + } + + assert!(reads.lock().unwrap().is_empty()); + assert_eq!( + recorded_v24(&platform, &transaction, platform_version), + Some(90) + ); + } + + /// A Core error or a coinbase without a payload is never read as "not active": the block + /// fails and nothing is recorded. + #[test] + fn should_fail_without_recording_when_core_cannot_tell() { + let platform_version = PlatformVersion::latest(); + + let (platform, _) = platform_with_core(platform_version, |_| Ok(None)); + let transaction = platform.drive.grove.start_transaction(); + let mut block_state = platform.state.load().as_ref().clone(); + let result = run_block( + &platform, + &mut block_state, + 100, + 101, + &transaction, + platform_version, + ); + assert!( + matches!( + result, + Err(Error::Execution(ExecutionError::DashCoreBadResponseError( + _ + ))) + ), + "a coinbase without a payload must fail the block, got {result:?}" + ); + assert_eq!( + recorded_v24(&platform, &transaction, platform_version), + None + ); + + let (platform, _) = platform_with_core(platform_version, |_| { + Err(CoreRpcError::UnexpectedStructure( + "Core is unreachable".to_string(), + )) + }); + let transaction = platform.drive.grove.start_transaction(); + let mut block_state = platform.state.load().as_ref().clone(); + let result = run_block( + &platform, + &mut block_state, + 100, + 101, + &transaction, + platform_version, + ); + assert!( + matches!(result, Err(Error::CoreRpc(_))), + "a Core error must fail the block, got {result:?}" + ); + assert_eq!( + recorded_v24(&platform, &transaction, platform_version), + None + ); + } + + /// A block that keeps the chain locked height makes no Core call, so a Core outage there + /// does not stop Platform. + #[test] + fn should_not_ask_core_about_forks_at_an_unchanged_chain_locked_height() { + let platform_version = PlatformVersion::latest(); + let (platform, reads) = platform_with_core(platform_version, v24_from(0)); + let transaction = platform.drive.grove.start_transaction(); + let mut block_state = platform.state.load().as_ref().clone(); + + run_block( + &platform, + &mut block_state, + 100, + 100, + &transaction, + platform_version, + ) + .expect("expected to skip the core info update"); + + assert!(reads.lock().unwrap().is_empty()); + assert_eq!( + recorded_v24(&platform, &transaction, platform_version), + None + ); + } + + /// At init chain the core info is updated from scratch at the genesis core height, although + /// the state already holds that height as its last committed one, so a chain born at this + /// protocol version records a V24 active there and asks Core about one that is not. + #[test] + fn should_ask_core_about_v24_at_init_chain() { + const GENESIS_CORE_HEIGHT: u32 = 1_000; + let platform_version = PlatformVersion::latest(); + for (activation_height, expected) in [ + (GENESIS_CORE_HEIGHT, Some(GENESIS_CORE_HEIGHT)), + (GENESIS_CORE_HEIGHT + 1, None), + ] { + let (platform, reads) = + platform_with_core(platform_version, v24_from(activation_height)); + let transaction = platform.drive.grove.start_transaction(); + let mut block_state = platform.state.load().as_ref().clone(); + block_state.set_genesis_block_info(Some(BlockInfo { + core_height: GENESIS_CORE_HEIGHT, + ..Default::default() + })); + assert_eq!( + block_state.last_committed_core_height(), + GENESIS_CORE_HEIGHT + ); + + platform + .update_core_info( + None, + &mut block_state, + GENESIS_CORE_HEIGHT, + true, + &BlockInfo { + core_height: GENESIS_CORE_HEIGHT, + ..Default::default() + }, + &transaction, + platform_version, + ) + .expect("expected to update the core info"); + + assert_eq!(*reads.lock().unwrap(), vec![GENESIS_CORE_HEIGHT]); + assert_eq!( + recorded_v24(&platform, &transaction, platform_version), + expected, + "V24 active from {activation_height}" + ); + } + } + + /// The record is a cache of Core's answer, not a new rule: after every block that moves the + /// chain locked height, "recorded" equals Core's answer at that height, whatever the heights + /// and however far they jump past the activation, and Core is asked only until it is + /// recorded. + #[test] + fn should_agree_with_core_after_every_block_that_moves_the_chain_locked_height() { + let platform_version = PlatformVersion::latest(); + let cases: [(u32, &[u32], Option); 6] = [ + // Activation inside the run, one height at a time. + (103, &[101, 102, 103, 104, 105], Some(103)), + // A jump over the activation height records the height jumped to. + (103, &[101, 101, 102, 110, 110, 111], Some(110)), + // Unchanged heights before and after the activation. + (102, &[100, 100, 102, 102, 102, 103], Some(102)), + // Active before the run; the first block keeps the committed height, 100. + (50, &[100, 101, 101, 140], Some(101)), + // Never active in the run. + (1_000, &[101, 120, 120, 999], None), + // Activation exactly at a height reached by a jump. + (120, &[101, 120, 121], Some(120)), + ]; + for (activation_height, core_heights, expected) in cases { + let core = v24_from(activation_height); + let core_says_active = |core_height| core(core_height).unwrap() == Some(4); + let (platform, reads) = + platform_with_core(platform_version, v24_from(activation_height)); + let transaction = platform.drive.grove.start_transaction(); + let mut block_state = platform.state.load().as_ref().clone(); + let mut previous_core_height = 100; + let mut expected_reads = vec![]; + let mut moved = false; + for &core_height in core_heights { + let recorded_before = recorded_v24(&platform, &transaction, platform_version); + run_block( + &platform, + &mut block_state, + previous_core_height, + core_height, + &transaction, + platform_version, + ) + .expect("expected to update the core info"); + if core_height != previous_core_height { + moved = true; + if recorded_before.is_none() { + expected_reads.push(core_height); + } + } + if moved { + assert_eq!( + recorded_v24(&platform, &transaction, platform_version).is_some(), + core_says_active(core_height), + "activation {activation_height}, heights {core_heights:?}, at {core_height}" + ); + } + previous_core_height = core_height; + } + assert_eq!( + recorded_v24(&platform, &transaction, platform_version), + expected, + "activation {activation_height}, heights {core_heights:?}" + ); + assert_eq!( + *reads.lock().unwrap(), + expected_reads, + "Core must be asked at each height the chain locked height moves to, until V24 \ + is recorded, and nowhere else" + ); + } + } + + /// Protocol version 14 shipped without the record: its blocks neither ask Core about the + /// coinbase nor write anything for a Core fork. + #[test] + fn should_neither_ask_core_nor_record_at_protocol_version_14() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let (platform, reads) = platform_with_core(platform_version, v24_from(0)); + let transaction = platform.drive.grove.start_transaction(); + let mut block_state = platform.state.load().as_ref().clone(); + let root_hash = |transaction: &Transaction| { + platform + .drive + .grove + .root_hash(Some(transaction), &platform_version.drive.grove_version) + .unwrap() + .expect("expected a root hash") + }; + let root_before = root_hash(&transaction); + + run_block( + &platform, + &mut block_state, + 100, + 101, + &transaction, + platform_version, + ) + .expect("expected to update the core info"); + + assert!(reads.lock().unwrap().is_empty()); + assert_eq!(root_hash(&transaction), root_before); + assert!(platform + .drive + .fetch_core_fork_activation_height( + CoreFork::V24, + Some(&transaction), + PlatformVersion::latest() + ) + .expect("expected to read the V24 record") + .is_none()); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/operator_key_encoding_tests.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/operator_key_encoding_tests.rs index 09295aac711..1fd9324bd58 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/operator_key_encoding_tests.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/operator_key_encoding_tests.rs @@ -131,6 +131,8 @@ fn platform_with_core( .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); + // Without the default answers, which would shadow the ones set here. + platform.core_rpc.checkpoint(); platform .core_rpc .expect_get_protx_diff_with_masternodes() diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/mod.rs index ba98310d003..4024e7420de 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/mod.rs @@ -7,7 +7,7 @@ mod v1; use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; -use crate::rpc::core::CoreRPCLike; +use crate::rpc::core::{CoreRPCLike, V24_COINBASE_PAYLOAD_VERSION}; use dpp::bls::PublicKey as BlsPublicKey; use dpp::dashcore::ProTxHash; use dpp::dashcore_rpc::dashcore_rpc_json::{DMNStateDiff, MasternodeListDiff, MasternodeListItem}; @@ -26,11 +26,6 @@ fn without_operator_payout_changes(mut masternode_diff: MasternodeListDiff) -> M masternode_diff } -/// The coinbase payload version Dash Core requires while DEPLOYMENT_V24 is active -/// (`MERKLE_ROOT_ASSETUNLOCKS`). Core requires it exactly at the blocks where V24 and V20 are -/// active and refuses it at every other block; V20 is active on every chain Platform runs on. -const V24_COINBASE_PAYLOAD_VERSION: u16 = 4; - /// The first byte of the point at infinity, which has the same 48 bytes in both schemes. const BLS_INFINITY_FIRST_BYTE: u8 = 0xc0; /// The legacy scheme's only flag: bit 7 of the first byte is the sign of y. Bits 5 and 6 are @@ -129,10 +124,7 @@ where #[cfg(test)] mod tests { - use super::{ - is_basic_re_encoding_of_legacy_key, without_operator_payout_changes, - V24_COINBASE_PAYLOAD_VERSION, - }; + use super::{is_basic_re_encoding_of_legacy_key, without_operator_payout_changes}; use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::platform_events::core_based_updates::update_masternode_list::operator_key_encoding_tests::{ @@ -142,6 +134,7 @@ mod tests { }; use crate::config::PlatformConfig; use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; + use crate::rpc::core::V24_COINBASE_PAYLOAD_VERSION; use crate::test::helpers::setup::TestPlatformBuilder; use dpp::dashcore::{Network, ProTxHash}; use dpp::dashcore_rpc::dashcore_rpc_json::{DMNStateDiff, MasternodeListDiff}; @@ -282,6 +275,8 @@ mod tests { (V24_COINBASE_PAYLOAD_VERSION, true), ] { let mut platform = TestPlatformBuilder::new().build_with_mock_rpc(); + // Without the default answers, which would shadow the ones set here. + platform.core_rpc.checkpoint(); platform .core_rpc .expect_get_coinbase_payload_version() @@ -314,6 +309,8 @@ mod tests { Ok(None), ] { let mut platform = TestPlatformBuilder::new().build_with_mock_rpc(); + // Without the default answers, which would shadow the ones set here. + platform.core_rpc.checkpoint(); let expect_none = answer.is_ok(); platform .core_rpc @@ -341,7 +338,9 @@ mod tests { #[test] fn should_keep_other_entries_without_asking_core() { let masternodes = recorded_masternodes(); - let platform = TestPlatformBuilder::new().build_with_mock_rpc(); + let mut platform = TestPlatformBuilder::new().build_with_mock_rpc(); + // Without the default answers, so asking Core fails the test. + platform.core_rpc.checkpoint(); let unlisted = ProTxHash::from_str(&"11".repeat(32)).expect("expected a pro tx hash"); let mut entries = vec![ ( @@ -391,6 +390,8 @@ mod tests { .collect(), }; let mut platform = TestPlatformBuilder::new().build_with_mock_rpc(); + // Without the default answers, which would shadow the ones set here. + platform.core_rpc.checkpoint(); platform .core_rpc .expect_get_coinbase_payload_version() @@ -438,6 +439,8 @@ mod tests { (1, PlatformVersion::latest()), ] { let mut platform = TestPlatformBuilder::new().build_with_mock_rpc(); + // Without the default answers, which would shadow the ones set here. + platform.core_rpc.checkpoint(); let reported = reported.clone(); platform .core_rpc diff --git a/packages/rs-drive-abci/src/platform_types/platform/mock.rs b/packages/rs-drive-abci/src/platform_types/platform/mock.rs index 8b5fb08caaf..539d84a480b 100644 --- a/packages/rs-drive-abci/src/platform_types/platform/mock.rs +++ b/packages/rs-drive-abci/src/platform_types/platform/mock.rs @@ -44,6 +44,13 @@ impl Platform { .expect_get_credit_pool_balance() .returning(|_| Ok(1_000_000_000_000_000)); + // A coinbase of payload version 3 at every height, a Core where V24 is not active, so + // no block records V24's activation. The same rule as above applies to an answer a test + // needs instead. + core_rpc_mock + .expect_get_coinbase_payload_version() + .returning(|_| Ok(Some(3))); + Self::open_with_client(path, config, core_rpc_mock, initial_protocol_version) } diff --git a/packages/rs-drive-abci/src/rpc/core.rs b/packages/rs-drive-abci/src/rpc/core.rs index e16e4e79863..2208254390d 100644 --- a/packages/rs-drive-abci/src/rpc/core.rs +++ b/packages/rs-drive-abci/src/rpc/core.rs @@ -58,6 +58,13 @@ pub(crate) fn credit_pool_balance_from_coinbase(coinbase: &[u8]) -> Result>, + } + + impl Core { + fn new(v24_activation_height: u32) -> Arc { + Arc::new(Self { + chain_locked_height: AtomicU32::new(1), + v24_activation_height, + coinbase_down: AtomicBool::new(false), + coinbase_reads: Mutex::new(vec![]), + }) + } + + fn chain_lock_at(&self, core_height: u32) { + self.chain_locked_height + .store(core_height, Ordering::SeqCst); + } + + fn coinbase_reads(&self) -> Vec { + self.coinbase_reads.lock().unwrap().clone() + } + } + + fn strategy(upgrading_info: Option) -> NetworkStrategy { + NetworkStrategy { + total_hpmns: 50, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info, + // Mock Core block hashes for every height these tests chain lock; `Core` reports + // which height is chain locked. + core_height_increase: CoreHeightIncrease::KnownCoreHeightIncreases( + (1..=MAX_CORE_HEIGHT).collect(), + ), + ..Default::default() + } + } + + fn config() -> PlatformConfig { + PlatformConfig { + validator_set: ValidatorSetConfig { + quorum_size: 30, + ..Default::default() + }, + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + epoch_time_length_s: BLOCKS_PER_EPOCH, + ..Default::default() + }, + block_spacing_ms: BLOCK_SPACING_MS, + testing_configs: PlatformTestConfig { + store_platform_state: true, + ..PlatformTestConfig::default_minimal_verifications() + }, + ..Default::default() + } + } + + /// A platform at `protocol_version` whose Core is `core`. Core's credit pool is large + /// enough that the Core-anchored withdrawal limit never binds. + fn platform_with_core( + protocol_version: ProtocolVersion, + core: &Arc, + ) -> TempPlatform { + let mut platform = TestPlatformBuilder::new() + .with_config(config()) + .with_initial_protocol_version(protocol_version) + .build_with_mock_rpc(); + + // The default answers would shadow the ones below. + platform.core_rpc.checkpoint(); + platform + .core_rpc + .expect_get_block_hash() + .returning(|_| Ok(BlockHash::all_zeros())); + platform + .core_rpc + .expect_get_block_json() + .returning(|_| Ok(serde_json::json!({ "tx": [] }))); + // 10 million Dash, in duffs. + platform + .core_rpc + .expect_get_credit_pool_balance() + .returning(|_| Ok(1_000_000_000_000_000)); + + let state = Arc::clone(core); + platform + .core_rpc + .expect_get_coinbase_payload_version() + .returning(move |core_height| { + state.coinbase_reads.lock().unwrap().push(core_height); + if state.coinbase_down.load(Ordering::SeqCst) { + return Err(CoreRpcError::UnexpectedStructure( + "Core is down".to_string(), + )); + } + let v24_active = core_height >= state.v24_activation_height; + Ok(Some(if v24_active { 4 } else { 3 })) + }); + + let state = Arc::clone(core); + platform + .core_rpc + .expect_get_best_chain_lock() + .returning(move || { + Ok(ChainLock { + block_height: state.chain_locked_height.load(Ordering::SeqCst), + block_hash: BlockHash::from_byte_array([1; 32]), + signature: [2; 96].into(), + }) + }); + + platform + } + + /// The node restarted from its data directory, with the same Core + fn restarted(platform: TempPlatform) -> TempPlatform { + let TempPlatform { + platform: mut stopped, + tempdir, + } = platform; + let core_rpc = std::mem::take(&mut stopped.core_rpc); + drop(stopped); + let mut platform = TempPlatform::open_with_tempdir(tempdir, config()); + platform.platform.core_rpc = core_rpc; + platform + } + + fn continuation(outcome: &ChainExecutionOutcome, block_count: u64) -> ChainExecutionParameters { + ChainExecutionParameters { + block_start: outcome + .abci_app + .platform + .state + .load() + .last_committed_block_height() + + 1, + core_height_start: 1, + block_count, + proposers: outcome.proposers.clone(), + validator_quorums: outcome.validator_quorums.clone(), + current_validator_quorum_hash: outcome.current_validator_quorum_hash, + current_proposer_versions: Some(outcome.current_proposer_versions.clone()), + current_identity_nonce_counter: outcome.identity_nonce_counter.clone(), + current_identity_contract_nonce_counter: outcome + .identity_contract_nonce_counter + .clone(), + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: outcome.end_time_ms, + instant_lock_quorums: outcome.instant_lock_quorums.clone(), + current_identities: Vec::new(), + current_addresses_with_balance: AddressesWithBalance::default(), + } + } + + /// Commits `block_count` blocks after `outcome` at the height `core` chain locks + async fn continue_chain<'a>( + platform: &'a Platform, + outcome: &ChainExecutionOutcome<'_>, + block_count: u64, + upgrading_info: Option, + seed: u64, + ) -> ChainExecutionOutcome<'a> { + continue_chain_for_strategy( + FullAbciApplication::new(platform), + continuation(outcome, block_count), + strategy(upgrading_info), + config(), + StrategyRandomness::SeedEntropy(seed), + ) + .await + } + + /// The committed V24 record + fn committed_v24(platform: &Platform) -> Option { + platform + .drive + .fetch_core_fork_activation_height(CoreFork::V24, None, PlatformVersion::latest()) + .expect("expected to read the V24 record") + } + + /// Once V24 is recorded Platform never asks Core about it again, also after a restart from + /// disk, so nothing node-local stands in for the record: a node whose Core then refuses to + /// tell the coinbase payload version commits the same chain as a node whose Core answers. + /// Only the V24 question fails here; the withdrawal scan still reads Core's credit pool in + /// the blocks that move the chain locked height, as it does at protocol version 14. + #[stack_size(4 * 1024 * 1024)] + #[test] + async fn should_never_ask_core_about_v24_again_once_it_is_recorded_even_after_a_restart() { + let mut app_hashes = vec![]; + for core_down_after_a_restart in [false, true] { + let core = Core::new(3); + let mut platform = + platform_with_core(PlatformVersion::latest().protocol_version, &core); + + let outcome = run_chain_for_strategy( + &mut platform, + 3, + strategy(None), + config(), + 40, + &mut None, + &mut None, + ) + .await; + assert_eq!( + core.coinbase_reads(), + vec![1], + "only init chain asks, at the genesis core height" + ); + assert_eq!(committed_v24(outcome.abci_app.platform), None); + + core.chain_lock_at(3); + let recorded = continue_chain(outcome.abci_app.platform, &outcome, 2, None, 41).await; + drop(outcome); + assert_eq!(committed_v24(recorded.abci_app.platform), Some(3)); + assert_eq!(core.coinbase_reads(), vec![1, 3]); + let parameters = continuation(&recorded, 2); + drop(recorded); + + if core_down_after_a_restart { + platform = restarted(platform); + core.coinbase_down.store(true, Ordering::SeqCst); + } + + let mut parameters = parameters; + for (core_height, seed) in [(4, 42), (6, 43)] { + core.chain_lock_at(core_height); + let outcome = continue_chain_for_strategy( + FullAbciApplication::new(&platform), + parameters, + strategy(None), + config(), + StrategyRandomness::SeedEntropy(seed), + ) + .await; + assert_eq!( + outcome + .abci_app + .platform + .state + .load() + .last_committed_core_height(), + core_height + ); + parameters = continuation(&outcome, 2); + } + + assert_eq!(committed_v24(&platform), Some(3)); + assert_eq!( + core.coinbase_reads(), + vec![1, 3], + "Core must not be asked about V24 once it is recorded" + ); + let state = platform.state.load(); + assert_eq!(state.last_committed_block_height(), 9); + app_hashes.push(state.last_committed_block_app_hash()); + } + assert_eq!( + app_hashes[0], app_hashes[1], + "the node whose Core went down after a restart must commit the same chain" + ); + } + + /// The upgrade block from protocol version 14 asks Core nothing when it keeps the chain + /// locked height, so V24 is recorded at the first protocol version 15 block that moves it. + /// A node reopened from disk right after the upgrade block keeps its last committed core + /// height, 2 rather than the genesis core height, asks nothing in the next block either, and + /// commits the same chain as a warm node. When the upgrade block itself moves the height, it + /// records V24 there. + #[stack_size(4 * 1024 * 1024)] + #[test] + async fn should_record_v24_at_the_first_protocol_version_15_block_that_moves_the_chain_locked_height( + ) { + let previous_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let latest_version = PlatformVersion::latest(); + let upgrading_info = Some(UpgradingInfo { + current_protocol_version: previous_version.protocol_version, + proposed_protocol_versions_with_weight: vec![(latest_version.protocol_version, 1)], + upgrade_three_quarters_life: 0.0, + }); + let mut app_hashes = vec![]; + for (activation_block_moves_core_height, reopen_after_activation) in + [(false, false), (false, true), (true, false)] + { + // V24 is active at every Core height these blocks chain lock. + let core = Core::new(1); + let mut platform = platform_with_core(previous_version.protocol_version, &core); + + let epoch_1 = run_chain_for_strategy( + &mut platform, + 2 * BLOCKS_PER_EPOCH - 1, + strategy(upgrading_info.clone()), + config(), + 13, + &mut None, + &mut None, + ) + .await; + // The last protocol version 14 block moves the chain locked height. + core.chain_lock_at(2); + let before_activation = continue_chain( + epoch_1.abci_app.platform, + &epoch_1, + 1, + upgrading_info.clone(), + 17, + ) + .await; + drop(epoch_1); + { + let state = before_activation.abci_app.platform.state.load(); + assert_eq!(state.current_protocol_version_in_consensus(), 14); + assert_eq!( + state.next_epoch_protocol_version(), + latest_version.protocol_version + ); + assert_eq!(state.last_committed_core_height(), 2); + } + assert!( + core.coinbase_reads().is_empty(), + "protocol version 14 never asks Core about V24" + ); + assert_eq!(committed_v24(before_activation.abci_app.platform), None); + + if activation_block_moves_core_height { + core.chain_lock_at(3); + } + let activation = continue_chain( + before_activation.abci_app.platform, + &before_activation, + 1, + upgrading_info.clone(), + 18, + ) + .await; + drop(before_activation); + assert_eq!( + activation + .abci_app + .platform + .state + .load() + .current_protocol_version_in_consensus(), + latest_version.protocol_version + ); + + if activation_block_moves_core_height { + assert_eq!(committed_v24(activation.abci_app.platform), Some(3)); + assert_eq!(core.coinbase_reads(), vec![3]); + continue; + } + + // The window: protocol version 15, V24 active in Core, not recorded yet. + assert_eq!(committed_v24(activation.abci_app.platform), None); + assert!(core.coinbase_reads().is_empty()); + let parameters = continuation(&activation, 1); + drop(activation); + + if reopen_after_activation { + platform = restarted(platform); + assert_eq!(platform.state.load().last_committed_core_height(), 2); + } + + let window = continue_chain_for_strategy( + FullAbciApplication::new(&platform), + parameters, + strategy(upgrading_info.clone()), + config(), + StrategyRandomness::SeedEntropy(19), + ) + .await; + assert_eq!(committed_v24(window.abci_app.platform), None); + assert!(core.coinbase_reads().is_empty()); + + core.chain_lock_at(3); + let recorded = continue_chain( + window.abci_app.platform, + &window, + 1, + upgrading_info.clone(), + 20, + ) + .await; + drop(window); + assert_eq!(committed_v24(recorded.abci_app.platform), Some(3)); + assert_eq!(core.coinbase_reads(), vec![3]); + app_hashes.push( + recorded + .abci_app + .platform + .state + .load() + .last_committed_block_app_hash(), + ); + } + assert_eq!( + app_hashes[0], app_hashes[1], + "a node reopened inside the window must commit the same chain as a warm node" + ); + } + + /// The proposal of `round` for the next height, at chain locked `core_chain_locked_height` + fn proposal( + outcome: &ChainExecutionOutcome, + round: u32, + core_chain_locked_height: u32, + ) -> RequestProcessProposal { + let platform_state = outcome.abci_app.platform.state.load(); + let height = platform_state.last_committed_block_height() + 1; + let time_ms = outcome.end_time_ms + BLOCK_SPACING_MS + round as u64 * 1000; + let proposer = outcome.proposers[round as usize].pro_tx_hash(); + + RequestProcessProposal { + txs: vec![], + proposed_last_commit: None, + misbehavior: vec![], + hash: [0xA0 + round as u8; 32].to_vec(), + height: height as i64, + time: Some(Timestamp::from_millis(time_ms).expect("expected a block time")), + next_validators_hash: [0u8; 32].to_vec(), + round: round as i32, + core_chain_locked_height, + core_chain_lock_update: None, + proposer_pro_tx_hash: proposer.to_byte_array().to_vec(), + proposed_app_version: PlatformVersion::latest().protocol_version as u64, + version: Some(Consensus { + block: 0, + app: PlatformVersion::latest().protocol_version as u64, + }), + quorum_hash: outcome + .current_quorum() + .quorum_hash + .to_byte_array() + .to_vec(), + } + } + + /// The request Tenderdash finalizes `proposal` with once it is committed in its own round, + /// with `app_hash` in the block header + fn finalize_request( + proposal: &RequestProcessProposal, + app_hash: Vec, + ) -> RequestFinalizeBlock { + RequestFinalizeBlock { + commit: Some(CommitInfo { + round: proposal.round, + quorum_hash: proposal.quorum_hash.clone(), + block_signature: vec![0u8; 96], + threshold_vote_extensions: vec![], + }), + misbehavior: vec![], + hash: proposal.hash.clone(), + height: proposal.height, + round: proposal.round, + block: Some(Block { + header: Some(Header { + version: proposal.version, + chain_id: CHAIN_ID.to_string(), + height: proposal.height, + time: proposal.time, + last_block_id: None, + last_commit_hash: vec![], + data_hash: vec![0u8; 32], + validators_hash: proposal.quorum_hash.clone(), + next_validators_hash: proposal.quorum_hash.clone(), + consensus_hash: vec![0u8; 32], + next_consensus_hash: vec![0u8; 32], + app_hash, + results_hash: vec![0u8; 32], + evidence_hash: vec![], + proposed_app_version: proposal.proposed_app_version, + proposer_pro_tx_hash: proposal.proposer_pro_tx_hash.clone(), + core_chain_locked_height: proposal.core_chain_locked_height, + }), + data: Some(Data { + txs: proposal.txs.clone(), + }), + evidence: Some(EvidenceList { evidence: vec![] }), + last_commit: None, + core_chain_lock: proposal.core_chain_lock_update.clone(), + }), + block_id: Some(BlockId { + hash: proposal.hash.clone(), + part_set_header: Some(PartSetHeader { + total: 0, + hash: vec![0u8; 32], + }), + state_id: vec![0u8; 32], + }), + } + } + + /// Processes `proposal`, which this node must accept, and returns its app hash + fn accept(outcome: &ChainExecutionOutcome, proposal: &RequestProcessProposal) -> Vec { + let response = outcome + .abci_app + .process_proposal(proposal.clone()) + .expect("expected to process the proposal"); + assert_eq!(response.status, ProposalStatus::Accept as i32); + response.app_hash + } + + /// The V24 record in the transaction of the block being executed + fn uncommitted_v24(outcome: &ChainExecutionOutcome) -> Option { + let transaction = outcome + .abci_app + .transaction + .read() + .expect("expected the block transaction"); + outcome + .abci_app + .platform + .drive + .fetch_core_fork_activation_height( + CoreFork::V24, + transaction.as_ref(), + PlatformVersion::latest(), + ) + .expect("expected to read the V24 record") + } + + /// A round of a height runs in the transaction left by the last commit, or by init chain + /// for height 1, and a round that is thrown away takes its record with it: the committed + /// record is the committed round's. Round 0 carries a new chain locked height, 3, where V24 + /// is active, and records it, or fails on a Core error. Round 1 is committed, at the last + /// committed height (no question to Core), at 3 again, or at 4. For height 1, a record + /// written at init chain survives every round. + #[stack_size(4 * 1024 * 1024)] + #[test] + async fn should_commit_only_the_v24_record_of_the_round_that_is_committed() { + struct Case { + blocks_before: u64, + v24_activation_height: u32, + round_0_core_down: bool, + round_1_core_height: u32, + after_round_0: Option, + committed: Option, + coinbase_reads: Vec, + } + let cases = [ + // Recorded at init chain, at the genesis core height. + Case { + blocks_before: 0, + v24_activation_height: 1, + round_0_core_down: false, + round_1_core_height: 1, + after_round_0: Some(1), + committed: Some(1), + coinbase_reads: vec![1], + }, + Case { + blocks_before: 0, + v24_activation_height: 3, + round_0_core_down: false, + round_1_core_height: 1, + after_round_0: Some(3), + committed: None, + coinbase_reads: vec![1, 3], + }, + Case { + blocks_before: 0, + v24_activation_height: 3, + round_0_core_down: false, + round_1_core_height: 4, + after_round_0: Some(3), + committed: Some(4), + coinbase_reads: vec![1, 3, 4], + }, + Case { + blocks_before: 2, + v24_activation_height: 3, + round_0_core_down: false, + round_1_core_height: 1, + after_round_0: Some(3), + committed: None, + coinbase_reads: vec![1, 3], + }, + Case { + blocks_before: 2, + v24_activation_height: 3, + round_0_core_down: false, + round_1_core_height: 4, + after_round_0: Some(3), + committed: Some(4), + coinbase_reads: vec![1, 3, 4], + }, + // Round 1 asks Core again rather than reuse round 0's answer. + Case { + blocks_before: 2, + v24_activation_height: 3, + round_0_core_down: false, + round_1_core_height: 3, + after_round_0: Some(3), + committed: Some(3), + coinbase_reads: vec![1, 3, 3], + }, + // Round 0 fails on the Core error and writes nothing. + Case { + blocks_before: 2, + v24_activation_height: 3, + round_0_core_down: true, + round_1_core_height: 3, + after_round_0: None, + committed: Some(3), + coinbase_reads: vec![1, 3, 3], + }, + ]; + for case in cases { + let core = Core::new(case.v24_activation_height); + let mut platform = + platform_with_core(PlatformVersion::latest().protocol_version, &core); + let outcome = run_chain_for_strategy( + &mut platform, + case.blocks_before, + NetworkStrategy { + // Without a block, the runner stops after init chain, whose transaction is + // not committed yet, instead of reading the committed state. + failure_testing: (case.blocks_before == 0).then(|| FailureStrategy { + dont_finalize_block: true, + ..Default::default() + }), + ..strategy(None) + }, + config(), + 15, + &mut None, + &mut None, + ) + .await; + let state = outcome.abci_app.platform.state.load(); + assert_eq!( + state.last_committed_block_height(), + case.blocks_before, + "the committed blocks" + ); + assert_eq!(state.last_committed_core_height(), 1); + drop(state); + let label = format!( + "after {} blocks, round 0 Core down {}, round 1 at core height {}", + case.blocks_before, case.round_0_core_down, case.round_1_core_height + ); + + let round_0 = proposal(&outcome, 0, 3); + core.coinbase_down + .store(case.round_0_core_down, Ordering::SeqCst); + let round_0_result = outcome.abci_app.process_proposal(round_0); + core.coinbase_down.store(false, Ordering::SeqCst); + if case.round_0_core_down { + assert!(round_0_result.is_err(), "{label}: round 0 must fail"); + } else { + let response = round_0_result.expect("expected to process round 0"); + assert_eq!(response.status, ProposalStatus::Accept as i32, "{label}"); + } + assert_eq!(uncommitted_v24(&outcome), case.after_round_0, "{label}"); + + let round_1 = proposal(&outcome, 1, case.round_1_core_height); + let app_hash = accept(&outcome, &round_1); + outcome + .abci_app + .finalize_block(finalize_request(&round_1, app_hash)) + .expect("expected to finalize round 1"); + assert_eq!( + committed_v24(outcome.abci_app.platform), + case.committed, + "{label}" + ); + assert_eq!(core.coinbase_reads(), case.coinbase_reads, "{label}"); + } + } +} diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs index 4190abd35c4..032d644d686 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs @@ -2,6 +2,7 @@ mod address_tests; mod basic_tests; mod chain_lock_update_tests; mod comprehensive_tests; +mod core_fork_activation_tests; mod core_height_increase; mod core_update_tests; mod credit_pool_backfill_tests; diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/withdrawal_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/withdrawal_tests.rs index 85c8007e9dc..fda1c95ffdb 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/withdrawal_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/withdrawal_tests.rs @@ -3129,6 +3129,11 @@ mod tests { .core_rpc .expect_get_credit_pool_balance() .returning(|_| Ok(dash_to_duffs!(120))); + // V24 is not active. + platform + .core_rpc + .expect_get_coinbase_payload_version() + .returning(|_| Ok(Some(3))); platform .core_rpc .expect_send_raw_transaction() diff --git a/packages/rs-drive/grovedb-structure.json b/packages/rs-drive/grovedb-structure.json index 297822e727c..e2ecc5d7bf8 100644 --- a/packages/rs-drive/grovedb-structure.json +++ b/packages/rs-drive/grovedb-structure.json @@ -4535,7 +4535,7 @@ "since": 1, "presence": "always", "source": "packages/rs-drive/src/drive/mod.rs", - "description": "Chain wide values: total credits, total token supplies, the genesis core height, and the documents waiting to expire.", + "description": "Chain wide values: total credits, total token supplies, the genesis core height, the Core forks Platform has seen active, and the documents waiting to expire.", "children": [ { "id": "misc.genesis_core_height", @@ -4639,6 +4639,24 @@ } ] }, + { + "id": "misc.core_fork_v24", + "key": { + "type": "fixed", + "hex": "4600", + "label": "CoreForkV24Activation", + "constant": "CORE_FORK_V24_ACTIVATION_KEY" + }, + "kinds": [ + "Item" + ], + "value": "u32 big endian", + "since": 15, + "presence": "lazy", + "source": "packages/rs-drive/src/drive/system/core_fork_activations/mod.rs", + "description": "The chain locked Core height of the first block that saw Dash Core's V24 active: the coinbase of that Core block has payload version 4 or higher. Written once, by a block that moves the chain locked height, or at init chain; never changed. The key is `F` and the fork's id.", + "children": [] + }, { "id": "misc.total_token_supplies", "key": { diff --git a/packages/rs-drive/src/drive/system/core_fork_activations/fetch_core_fork_activation_height/mod.rs b/packages/rs-drive/src/drive/system/core_fork_activations/fetch_core_fork_activation_height/mod.rs new file mode 100644 index 00000000000..02d920e84a0 --- /dev/null +++ b/packages/rs-drive/src/drive/system/core_fork_activations/fetch_core_fork_activation_height/mod.rs @@ -0,0 +1,51 @@ +mod v0; + +use crate::drive::system::core_fork_activations::CoreFork; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::prelude::CoreBlockHeight; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Fetches the chain locked Core height of the first Platform block that saw `fork` active. + /// + /// Read in a block after its Core info is updated, `Some` means the fork is active at the + /// block's chain locked height. `None` means Platform has not seen it active, not that it + /// is inactive: a block that keeps the chain locked height does not look. + /// + /// # Parameters + /// - `fork`: the Core fork. + /// - `transaction`: the transaction to read in; in a block, the block's transaction. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The recorded height, or `None` when nothing is recorded. + pub fn fetch_core_fork_activation_height( + &self, + fork: CoreFork, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .platform_system + .fetch_core_fork_activation_height + { + Some(0) => { + self.fetch_core_fork_activation_height_v0(fork, transaction, platform_version) + } + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "fetch_core_fork_activation_height".to_string(), + known_versions: vec![0], + received: version, + })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "fetch_core_fork_activation_height".to_string(), + known_versions: vec![0], + })), + } + } +} diff --git a/packages/rs-drive/src/drive/system/core_fork_activations/fetch_core_fork_activation_height/v0/mod.rs b/packages/rs-drive/src/drive/system/core_fork_activations/fetch_core_fork_activation_height/v0/mod.rs new file mode 100644 index 00000000000..7969dc7c97a --- /dev/null +++ b/packages/rs-drive/src/drive/system/core_fork_activations/fetch_core_fork_activation_height/v0/mod.rs @@ -0,0 +1,38 @@ +use crate::drive::system::core_fork_activations::CoreFork; +use crate::drive::system::misc_path; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::util::grove_operations::DirectQueryType; +use dpp::prelude::CoreBlockHeight; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + #[inline(always)] + pub(super) fn fetch_core_fork_activation_height_v0( + &self, + fork: CoreFork, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let Some(element) = self.grove_get_raw_optional( + (&misc_path()).into(), + fork.activation_key(), + DirectQueryType::StatefulDirectQuery, + transaction, + &mut vec![], + &platform_version.drive, + )? + else { + return Ok(None); + }; + let bytes = element.into_item_bytes()?; + let core_height = u32::from_be_bytes(bytes.try_into().map_err(|_| { + Error::Drive(DriveError::CorruptedSerialization( + "a Core fork activation height must be a u32".to_string(), + )) + })?); + Ok(Some(core_height)) + } +} diff --git a/packages/rs-drive/src/drive/system/core_fork_activations/mod.rs b/packages/rs-drive/src/drive/system/core_fork_activations/mod.rs new file mode 100644 index 00000000000..62194172b95 --- /dev/null +++ b/packages/rs-drive/src/drive/system/core_fork_activations/mod.rs @@ -0,0 +1,195 @@ +//! The Dash Core forks Platform has seen active, each with the chain locked Core height of the +//! first Platform block that saw it. + +mod fetch_core_fork_activation_height; +mod store_core_fork_activation_height; + +/// The first byte of every Core fork activation key in `Misc`; the fork's id follows it. +pub const CORE_FORK_ACTIVATION_KEY_PREFIX: u8 = b'F'; + +/// The `Misc` key holding the Core height at which Platform first saw Core's V24 active +pub const CORE_FORK_V24_ACTIVATION_KEY: &[u8; 2] = + &[CORE_FORK_ACTIVATION_KEY_PREFIX, CoreFork::V24 as u8]; + +/// A Dash Core fork whose activation Platform records. +/// +/// The discriminant is the fork's id in its `Misc` key, so it never changes. +// @append_only +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CoreFork { + /// Dash Core's `DEPLOYMENT_V24` + V24 = 0, +} + +impl CoreFork { + /// The `Misc` key holding the fork's activation height + pub const fn activation_key(self) -> &'static [u8; 2] { + match self { + CoreFork::V24 => CORE_FORK_V24_ACTIVATION_KEY, + } + } +} + +#[cfg(feature = "server")] +#[cfg(test)] +mod tests { + use super::{CoreFork, CORE_FORK_V24_ACTIVATION_KEY}; + use crate::drive::system::misc_path; + use crate::error::drive::DriveError; + use crate::error::Error; + use crate::util::grove_operations::DirectQueryType; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::version::PlatformVersion; + use grovedb::Element; + + #[test] + fn should_return_none_for_a_core_fork_never_recorded() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let transaction = drive.grove.start_transaction(); + + assert_eq!( + drive + .fetch_core_fork_activation_height( + CoreFork::V24, + Some(&transaction), + platform_version + ) + .expect("expected to read an absent record"), + None + ); + } + + /// The record is a big endian `u32` item without flags at `[b'F', 0]` in `Misc`, which + /// clients can prove through `getPathElements`. It is visible in the transaction that + /// wrote it before that transaction commits, and nowhere else. + #[test] + fn should_read_back_the_observed_height() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let transaction = drive.grove.start_transaction(); + + drive + .store_core_fork_activation_height( + CoreFork::V24, + 1_234_567, + Some(&transaction), + platform_version, + ) + .expect("expected to record V24"); + + assert_eq!( + drive + .fetch_core_fork_activation_height( + CoreFork::V24, + Some(&transaction), + platform_version + ) + .expect("expected to read the record"), + Some(1_234_567) + ); + assert_eq!(CORE_FORK_V24_ACTIVATION_KEY, &[b'F', 0]); + assert_eq!( + drive + .grove_get_raw_optional( + (&misc_path()).into(), + CORE_FORK_V24_ACTIVATION_KEY, + DirectQueryType::StatefulDirectQuery, + Some(&transaction), + &mut vec![], + &platform_version.drive, + ) + .expect("expected to read the element"), + Some(Element::Item(1_234_567u32.to_be_bytes().to_vec(), None)) + ); + assert_eq!( + drive + .fetch_core_fork_activation_height(CoreFork::V24, None, platform_version) + .expect("expected to read outside the transaction"), + None, + "an uncommitted record must not be visible outside its transaction" + ); + } + + /// A recorded height is what every later block compares with, so recording the fork again + /// is a bug and leaves the first height in place. + #[test] + fn should_refuse_to_record_a_core_fork_twice() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let transaction = drive.grove.start_transaction(); + + drive + .store_core_fork_activation_height( + CoreFork::V24, + 1_000, + Some(&transaction), + platform_version, + ) + .expect("expected to record V24"); + + for core_height in [1_000, 2_000] { + let result = drive.store_core_fork_activation_height( + CoreFork::V24, + core_height, + Some(&transaction), + platform_version, + ); + assert!( + matches!( + result, + Err(Error::Drive(DriveError::CorruptedCodeExecution(_))) + ), + "recording V24 again at {core_height} must fail, got {result:?}" + ); + } + assert_eq!( + drive + .fetch_core_fork_activation_height( + CoreFork::V24, + Some(&transaction), + platform_version + ) + .expect("expected to read the record"), + Some(1_000) + ); + } + + #[test] + fn should_refuse_a_stored_height_that_is_not_four_bytes() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + drive + .grove_insert( + (&misc_path()).into(), + CORE_FORK_V24_ACTIVATION_KEY, + Element::Item(vec![0, 0, 1], None), + None, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to write a malformed record"); + + assert!(matches!( + drive.fetch_core_fork_activation_height(CoreFork::V24, None, platform_version), + Err(Error::Drive(DriveError::CorruptedSerialization(_))) + )); + } + + #[test] + fn should_neither_read_nor_record_before_protocol_version_15() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + + assert!(matches!( + drive.fetch_core_fork_activation_height(CoreFork::V24, None, platform_version), + Err(Error::Drive(DriveError::VersionNotActive { .. })) + )); + assert!(matches!( + drive.store_core_fork_activation_height(CoreFork::V24, 1_000, None, platform_version), + Err(Error::Drive(DriveError::VersionNotActive { .. })) + )); + } +} diff --git a/packages/rs-drive/src/drive/system/core_fork_activations/store_core_fork_activation_height/mod.rs b/packages/rs-drive/src/drive/system/core_fork_activations/store_core_fork_activation_height/mod.rs new file mode 100644 index 00000000000..3f89a92401d --- /dev/null +++ b/packages/rs-drive/src/drive/system/core_fork_activations/store_core_fork_activation_height/mod.rs @@ -0,0 +1,55 @@ +mod v0; + +use crate::drive::system::core_fork_activations::CoreFork; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::prelude::CoreBlockHeight; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Records `core_height` as the chain locked Core height of the first Platform block that + /// saw `fork` active. The record is written once and never changed: recording a fork again + /// is an error. The write is applied at once, so later reads in the transaction see it. + /// + /// # Parameters + /// - `fork`: the Core fork. + /// - `core_height`: the chain locked Core height of the block that saw it active. + /// - `transaction`: the transaction to write in; in a block, the block's transaction. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the height is written, `CorruptedCodeExecution` when the fork is already + /// recorded. + pub fn store_core_fork_activation_height( + &self, + fork: CoreFork, + core_height: CoreBlockHeight, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .platform_system + .store_core_fork_activation_height + { + Some(0) => self.store_core_fork_activation_height_v0( + fork, + core_height, + transaction, + platform_version, + ), + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "store_core_fork_activation_height".to_string(), + known_versions: vec![0], + received: version, + })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "store_core_fork_activation_height".to_string(), + known_versions: vec![0], + })), + } + } +} diff --git a/packages/rs-drive/src/drive/system/core_fork_activations/store_core_fork_activation_height/v0/mod.rs b/packages/rs-drive/src/drive/system/core_fork_activations/store_core_fork_activation_height/v0/mod.rs new file mode 100644 index 00000000000..45393b34668 --- /dev/null +++ b/packages/rs-drive/src/drive/system/core_fork_activations/store_core_fork_activation_height/v0/mod.rs @@ -0,0 +1,34 @@ +use crate::drive::system::core_fork_activations::CoreFork; +use crate::drive::system::misc_path; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::prelude::CoreBlockHeight; +use dpp::version::PlatformVersion; +use grovedb::{Element, TransactionArg}; + +impl Drive { + #[inline(always)] + pub(super) fn store_core_fork_activation_height_v0( + &self, + fork: CoreFork, + core_height: CoreBlockHeight, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let inserted = self.grove_insert_if_not_exists( + (&misc_path()).into(), + fork.activation_key(), + Element::Item(core_height.to_be_bytes().to_vec(), None), + transaction, + None, + &platform_version.drive, + )?; + if !inserted { + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "a Core fork activation is recorded only once", + ))); + } + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/system/mod.rs b/packages/rs-drive/src/drive/system/mod.rs index 53e62a973ef..0ed2f208897 100644 --- a/packages/rs-drive/src/drive/system/mod.rs +++ b/packages/rs-drive/src/drive/system/mod.rs @@ -1,3 +1,5 @@ +/// Core fork activations module +pub mod core_fork_activations; mod estimation_costs; mod fetch_elements; /// Genesis time module diff --git a/packages/rs-drive/src/drive/system/structure.rs b/packages/rs-drive/src/drive/system/structure.rs index 1defaccef89..825e21a9a8b 100644 --- a/packages/rs-drive/src/drive/system/structure.rs +++ b/packages/rs-drive/src/drive/system/structure.rs @@ -1,6 +1,7 @@ use crate::drive::balances::{TOTAL_SYSTEM_CREDITS_STORAGE_KEY, TOTAL_TOKEN_SUPPLIES_STORAGE_KEY}; use crate::drive::document::expiration::paths::DOCUMENTS_EXPIRATIONS_KEY; use crate::drive::initialization::genesis_core_height::GENESIS_CORE_HEIGHT_KEY; +use crate::drive::system::core_fork_activations::CORE_FORK_V24_ACTIVATION_KEY; use crate::drive::RootTree; use crate::structure::{ElementKind, KeyEncoding, KeyMatcher, StructureNode}; @@ -11,8 +12,9 @@ pub(crate) fn structure() -> StructureNode { .source("packages/rs-drive/src/drive/mod.rs") .describe( "Chain wide values: total credits, total token \ - supplies, the genesis core height, and the documents \ - waiting to expire.", + supplies, the genesis core height, the Core forks \ + Platform has seen active, and the documents waiting \ + to expire.", ) .children(vec![ StructureNode::fixed( @@ -31,6 +33,25 @@ pub(crate) fn structure() -> StructureNode { Written when the chain is initialized, after the \ structure.", ), + StructureNode::fixed( + "core_fork_v24", + CORE_FORK_V24_ACTIVATION_KEY, + "CoreForkV24Activation", + "CORE_FORK_V24_ACTIVATION_KEY", + ) + .kind(ElementKind::Item) + .lazy() + .since(15) + .source("packages/rs-drive/src/drive/system/core_fork_activations/mod.rs") + .value("u32 big endian") + .describe( + "The chain locked Core height of the first block that \ + saw Dash Core's V24 active: the coinbase of that Core \ + block has payload version 4 or higher. Written once, \ + by a block that moves the chain locked height, or at \ + init chain; never changed. The key is `F` and the \ + fork's id.", + ), StructureNode::fixed( "documents_expirations", DOCUMENTS_EXPIRATIONS_KEY, diff --git a/packages/rs-drive/src/structure/tests.rs b/packages/rs-drive/src/structure/tests.rs index fb21f3d998c..bb645a2fe7c 100644 --- a/packages/rs-drive/src/structure/tests.rs +++ b/packages/rs-drive/src/structure/tests.rs @@ -279,6 +279,7 @@ mod fixtures { use super::*; use crate::drive::credit_pools::epochs::operations_factory::EpochOperations; use crate::drive::credit_pools::operations::update_lifetime_storage_fee_pool_operation; + use crate::drive::system::core_fork_activations::CoreFork; use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; use crate::drive::Drive; use crate::fees::op::LowLevelDriveOperation; @@ -1241,6 +1242,16 @@ mod fixtures { conformance_of(&drive, "core_anchored_withdrawal_accounting", run); } + /// A Core fork Platform has seen active. + fn core_fork_activations(run: &mut FixtureRun) { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + drive + .store_core_fork_activation_height(CoreFork::V24, 1_000, None, platform_version) + .expect("expected to record V24"); + conformance_of(&drive, "core_fork_activations", run); + } + /// An epoch while it runs, then after it was paid out: payout deletes the /// proposers and both fee items and keeps the epoch tree. The finished /// epoch info is written at payout, so no epoch ever holds all nine keys. @@ -1914,6 +1925,7 @@ mod fixtures { tokens_and_group_actions(&mut run); address_balances(&mut run); core_anchored_withdrawal_accounting(&mut run); + core_fork_activations(&mut run); current_then_paid_epoch(&mut run); contested_documents(&mut run); yes_no_vote_polls(&mut run); diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs index 4d5bb4133d1..064fa805aeb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs @@ -1,7 +1,8 @@ use super::v10::DRIVE_ABCI_METHOD_VERSIONS_V10; use super::{ - DkgMiningSchedule, DriveAbciCoreChainLockMethodVersionsAndConstants, - DriveAbciEngineMethodVersions, DriveAbciMethodVersions, DriveAbciVotingMethodVersions, + DkgMiningSchedule, DriveAbciCoreBasedUpdatesMethodVersions, + DriveAbciCoreChainLockMethodVersionsAndConstants, DriveAbciEngineMethodVersions, + DriveAbciMethodVersions, DriveAbciVotingMethodVersions, }; /// Dash Core's DKG schedule (`llmq/params.h`) of the non-rotating quorum types, the ones chain @@ -90,9 +91,12 @@ const DKG_MINING_SCHEDULES_V1: &[DkgMiningSchedule] = &[ /// Drive ABCI method versions 11, introduced in protocol v15. Differs from v10 in the yes/no /// poll kind's voting methods (`voting.tally_votes_for_yes_no_vote_poll`, /// `keep_record_of_finished_yes_no_vote_poll`, `clean_up_after_yes_no_vote_polls_end` and -/// `on_yes_no_vote_poll_finished` are `Some(0)`, `None` in every earlier table), in the -/// consensus evidence policy, and in the chain lock verification. The chain lock changes only -/// work as a whole: +/// `on_yes_no_vote_poll_finished` are `Some(0)`, `None` in every earlier table), in +/// `core_based_updates.update_core_info`, which is 1 rather than 0: init chain and a block that +/// move the chain locked Core height record, the first time they see it, that Core's V24 is +/// active there (the coinbase of that Core block has payload version 4 or higher), and Core is +/// not asked again once it is recorded; in the consensus evidence policy; and in the chain lock +/// verification. The chain lock changes only work as a whole: /// - `core_chain_lock.verify_chain_lock_locally` is 1 rather than 0: a chain lock is rejected /// locally only when Platform holds exactly the quorums active at its signing height, using /// Dash Core's DKG mining schedule in `core_chain_lock.dkg_mining_schedules`. When a quorum may @@ -115,6 +119,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMet dkg_mining_schedules: Some(DKG_MINING_SCHEDULES_V1), ..DRIVE_ABCI_METHOD_VERSIONS_V10.core_chain_lock }, + core_based_updates: DriveAbciCoreBasedUpdatesMethodVersions { + update_core_info: 1, // changed in v15: records the chain locked Core height at which V24 is first seen active + ..DRIVE_ABCI_METHOD_VERSIONS_V10.core_based_updates + }, voting: DriveAbciVotingMethodVersions { tally_votes_for_yes_no_vote_poll: Some(0), keep_record_of_finished_yes_no_vote_poll: Some(0), diff --git a/packages/rs-platform-version/src/version/drive_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/mod.rs index cd70ea2f37b..0bfc945eafc 100644 --- a/packages/rs-platform-version/src/version/drive_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/mod.rs @@ -15,6 +15,7 @@ use drive_token_method_versions::DriveTokenMethodVersions; use drive_verify_method_versions::DriveVerifyMethodVersions; use drive_vote_method_versions::DriveVoteMethodVersions; use grovedb_version::version::GroveVersion; +use versioned_feature_core::OptionalFeatureVersion; pub mod drive_address_funds_method_versions; pub mod drive_contract_group_method_versions; @@ -166,6 +167,12 @@ pub struct DriveFeesMethodVersions { #[derive(Clone, Debug, Default)] pub struct DrivePlatformSystemMethodVersions { pub estimation_costs: DriveSystemEstimationCostsMethodVersions, + /// Reads the chain locked Core height at which Platform first saw a Core fork active. + /// `None` (not active) before protocol version 15. + pub fetch_core_fork_activation_height: OptionalFeatureVersion, + /// Records the chain locked Core height at which Platform first saw a Core fork active. + /// `None` (not active) before protocol version 15. + pub store_core_fork_activation_height: OptionalFeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/v1.rs index ed81188a1fb..66b1056aa23 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v1.rs @@ -73,6 +73,8 @@ pub const DRIVE_VERSION_V1: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index 3aface1463c..281f858302f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -72,6 +72,9 @@ use crate::version::drive_versions::{ /// `stateless_delete_of_non_tree_for_costs` to 1, and /// `platform_system.estimation_costs.for_total_system_credits_update` is 1, so a delete and a /// system credits update describe their layers as they are. Applying a batch is unchanged. +/// * **Core fork activations**: `platform_system.fetch_core_fork_activation_height` and +/// `store_core_fork_activation_height` are `Some(0)` (`None` before): Drive keeps, in `Misc`, +/// the chain locked Core height at which Platform first saw each tracked Core fork active. /// /// Everything else matches `DRIVE_VERSION_V9`. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { @@ -111,6 +114,8 @@ pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 1, // changed in v10: `Misc` described with its subtrees }, + fetch_core_fork_activation_height: Some(0), // changed in v10: the Core fork activations + store_core_fork_activation_height: Some(0), // changed in v10: the Core fork activations }, ..DRIVE_VERSION_V9.methods }, diff --git a/packages/rs-platform-version/src/version/drive_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/v2.rs index 3281d9e4bff..a4097a1cde3 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v2.rs @@ -73,6 +73,8 @@ pub const DRIVE_VERSION_V2: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/v3.rs index 63b71e91a8d..f685007a316 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v3.rs @@ -73,6 +73,8 @@ pub const DRIVE_VERSION_V3: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/v4.rs index 8a9b94fc500..d8e2731c2f5 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v4.rs @@ -73,6 +73,8 @@ pub const DRIVE_VERSION_V4: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v5.rs b/packages/rs-platform-version/src/version/drive_versions/v5.rs index c36faf06791..f494c9d4c5e 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v5.rs @@ -75,6 +75,8 @@ pub const DRIVE_VERSION_V5: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v6.rs b/packages/rs-platform-version/src/version/drive_versions/v6.rs index 2af2c251d77..d07683bf664 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v6.rs @@ -77,6 +77,8 @@ pub const DRIVE_VERSION_V6: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v7.rs b/packages/rs-platform-version/src/version/drive_versions/v7.rs index 579be7e1c49..8ffa072432b 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v7.rs @@ -75,6 +75,8 @@ pub const DRIVE_VERSION_V7: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v8.rs b/packages/rs-platform-version/src/version/drive_versions/v8.rs index 6b2344ebe1b..23a5097ef4c 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v8.rs @@ -75,6 +75,8 @@ pub const DRIVE_VERSION_V8: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index 6e6c1131852..22ee74cbeca 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -124,6 +124,8 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index a92f11f3c4c..b511fdc35c5 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -113,6 +113,8 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { estimation_costs: DriveSystemEstimationCostsMethodVersions { for_total_system_credits_update: 0, }, + fetch_core_fork_activation_height: None, + store_core_fork_activation_height: None, }, operations: DriveOperationsMethodVersion { rollback_transaction: 0, diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index a7919273a43..5e959fd86cf 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -392,12 +392,27 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// stored without storage flags by `transition_to_version_15`. Users cannot write them: /// `creationRestrictionMode` 2 refuses a create, and the data trigger `bindings` 3 /// (`DRIVE_ABCI_VALIDATION_VERSIONS_V11`) refuse a replace or a delete. +/// +/// 18. **Core fork activations recorded in state**: `update_core_info` 1 +/// (`DRIVE_ABCI_METHOD_VERSIONS_V11`) asks Core, in a block that moves the chain locked +/// Core height, whether Dash Core's V24 is active at that height: the coinbase of that +/// Core block has payload version 4 or higher, which Core requires exactly while V24 is +/// active. The first block that sees it active stores the height in `Misc` +/// (`CORE_FORK_V24_ACTIVATION_KEY`, a big endian `u32` written once, through +/// `DRIVE_VERSION_V10`'s `platform_system` methods), where the app hash covers it, and no +/// later block asks Core about V24. A chain born at this version asks at init chain, at +/// the genesis core height. A block that keeps the chain locked height asks nothing, so +/// a chain upgraded to this version records V24 at its first block that moves the height +/// to one where V24 is active, which can be a few blocks after Core activated it. A Core +/// error or a coinbase without a payload fails the block, as other Core errors in +/// `update_core_info` do. Protocol v14 and earlier keep `update_core_info` 0, which +/// neither asks nor records. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10) and verify_private_document_entries Some(0) a page of a private type's store (note 13); state transition method versions v5: document_create_transition 1 deletes a consumed document from what its lookup read (note 11); document method versions v5: insert_empty_private_document_store_operation Some(0) creates a private document type's store, and fetch_private_document_entries and prove_private_document_entries Some(0) read and prove a page of it (note 13); apply_drive_operations 2 resolves a batch's estimate, remove_reference_for_index_level_for_contract_operations 2, stateless_delete_of_non_tree_for_costs 1 and for_total_system_credits_update 1 describe their layers as they are (note 14); create_initial_state_structure 5 adds the token marketplace escrow trees; calculate_total_tokens_balance 2 counts escrowed tokens; the escrow primitives; the identity balance, token balance, prefunded specialized balance and contract fee pot estimations (for_balances 1, for_token_balances 1, estimated_cost_for_prefunded_specialized_balance_update 2, add_estimation_costs_for_contract_fee_pot_update 1) keep a deeper layer description (note 17) + drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10) and verify_private_document_entries Some(0) a page of a private type's store (note 13); state transition method versions v5: document_create_transition 1 deletes a consumed document from what its lookup read (note 11); document method versions v5: insert_empty_private_document_store_operation Some(0) creates a private document type's store, and fetch_private_document_entries and prove_private_document_entries Some(0) read and prove a page of it (note 13); apply_drive_operations 2 resolves a batch's estimate, remove_reference_for_index_level_for_contract_operations 2, stateless_delete_of_non_tree_for_costs 1 and for_total_system_credits_update 1 describe their layers as they are (note 14); create_initial_state_structure 5 adds the token marketplace escrow trees; calculate_total_tokens_balance 2 counts escrowed tokens; the escrow primitives; the identity balance, token balance, prefunded specialized balance and contract fee pot estimations (for_balances 1, for_token_balances 1, estimated_cost_for_prefunded_specialized_balance_update 2, add_estimation_costs_for_contract_fee_pot_update 1) keep a deeper layer description (note 17); fetch_core_fork_activation_height and store_core_fork_activation_height Some(0) keep the Core fork activations (note 18) drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, - methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the yes/no poll's tally, record, clean-up and finish hook are Some(0) (note 10); verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock; consensus_params_update 2 installs the full evidence policy (note 16) + methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the yes/no poll's tally, record, clean-up and finish hook are Some(0) (note 10); verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock; consensus_params_update 2 installs the full evidence policy (note 16); update_core_info 1 records the chain locked height at which Core's V24 is first seen active (note 18) validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: identity update advanced_structure 1 refuses unpaid an identity update whose added key's proof of possession does not verify (note 2); create_domain_data_trigger 2 (note 3); contract update transform_into_action 1 holds an update to the compiled size cap in check_tx (note 8); batch advanced_structure 2 refuses a token config update on a read-only contract (note 9); masternode vote transform_into_action 1 looks up a masternode's previous answer on a yes/no poll (note 10); document_reference_validation 1 keeps what a consumed commitment's lookup read (note 11); document create structure_validation 2 holds a decision proposal to its fund, rule and end date (note 12); data trigger bindings 3 refuse a replace or a delete of a token market order (note 17) withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, query: DRIVE_ABCI_QUERY_VERSIONS_V2,