From eca4e15f12b17c781f4847b9ed04bd274fa00b87 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 30 Sep 2026 23:53:52 +0700 Subject: [PATCH 01/11] fix(dashmate): keep Core's service field for Platform with Core v24 Core v24 leaves `service`, `platformP2PPort` and `platformHTTPPort` out of its masternode list RPC output unless started with `-deprecatedrpc=service`, and Drive and DAPI read those fields. Render `deprecatedrpc=service` unconditionally in the default section of dash.conf so it applies on every network. Core v23 accepts the option and ignores it. Rollout follows earlier template-only changes to dash.conf (adding and removing `deprecatedrpc=hpmn`): no migration. Every command re-renders the service configs when loading migrates the config format version, so an upgrade from 4.1.x or earlier picks the line up on the first dashmate command. Every 4.2.0 prerelease stamps the format version as 4.2.0 (the newest migration key), so an upgrade from a 4.2.0 prerelease to a later prerelease or to 4.2.0 itself runs no migration and does not re-render. Those nodes need `dashmate config render --config=` and a Core restart before they run Core v24. The new test renders dash.conf for mainnet, testnet, devnet and local and checks the option sits before the first network section. Red to green: 4 failing before the template change, 4 passing after. Co-Authored-By: Claude Opus 5.5 --- .../dashmate/templates/core/dash.conf.dot | 3 ++ .../test/unit/templates/coreTemplate.spec.js | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/dashmate/test/unit/templates/coreTemplate.spec.js diff --git a/packages/dashmate/templates/core/dash.conf.dot b/packages/dashmate/templates/core/dash.conf.dot index 7e001947960..957a589bd93 100644 --- a/packages/dashmate/templates/core/dash.conf.dot +++ b/packages/dashmate/templates/core/dash.conf.dot @@ -31,6 +31,9 @@ debuglogfile=/var/log/dash/debug.log # JSON RPC server=1 rpcwallet=main +# Core leaves a masternode's `service`, `platformP2PPort` and `platformHTTPPort` +# out of RPC output unless they are requested here. Platform reads them. +deprecatedrpc=service rpcworkqueue=64 rpcthreads=16 rpcwhitelistdefault=0 diff --git a/packages/dashmate/test/unit/templates/coreTemplate.spec.js b/packages/dashmate/test/unit/templates/coreTemplate.spec.js new file mode 100644 index 00000000000..559505ccbc0 --- /dev/null +++ b/packages/dashmate/test/unit/templates/coreTemplate.spec.js @@ -0,0 +1,33 @@ +import { expect } from 'chai'; +import HomeDir from '../../../src/config/HomeDir.js'; +import getBaseConfigFactory from '../../../configs/defaults/getBaseConfigFactory.js'; +import renderTemplateFactory from '../../../src/templates/renderTemplateFactory.js'; +import renderServiceTemplatesFactory from '../../../src/templates/renderServiceTemplatesFactory.js'; + +describe('Core template', () => { + let config; + let renderServiceTemplates; + + beforeEach(() => { + config = getBaseConfigFactory(HomeDir.createTemp())(); + renderServiceTemplates = renderServiceTemplatesFactory(renderTemplateFactory()); + }); + + // Drive and DAPI read `service`, `platformP2PPort` and `platformHTTPPort` from + // Core's masternode list RPCs. Core v24 leaves them out unless the deprecated + // fields are requested, and Platform cannot process a masternode list without + // them. + ['mainnet', 'testnet', 'devnet', 'local'].forEach((network) => { + it(`should keep Core's deprecated masternode service fields in RPC output on ${network}`, () => { + config.set('network', network); + + const dashConf = renderServiceTemplates(config)['core/dash.conf']; + + // Options under a [section] header apply to that chain only, so the + // option has to be in the default section to cover every network. + const [defaultSection] = dashConf.split(/^\[/m); + + expect(defaultSection).to.match(/^deprecatedrpc=service$/m); + }); + }); +}); From a4d7a38511aae8d96045049e7435c2407b5d5487 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 1 Oct 2026 00:31:01 +0700 Subject: [PATCH 02/11] build: bump rust-dashcore to parse Core v24 masternode lists Pin rust-dashcore to 538522bc (dashpay/rust-dashcore#1093), whose rpc-json parses the masternode lists Dash Core v24 prints, and adapt drive-abci to it. Only rpc-json differs from the previous pin e4208c90. The re-exported `dpp::dashcore_rpc` (and `dash_sdk::dashcore_rpc`) masternode types change in a breaking way for Rust consumers; nothing nodes agree on changes. DMNState and DMNStateDiff rename the flat platform ports to the deprecated `legacy_platform_p2p_port` / `legacy_platform_http_port`, add the nested `addresses` and the `payouts` list, and make `owner_address`, `payout_address` and `collateral_address` optional, since a shared masternode has none of them. A Tor or I2P `service` reads as `[::]:0` and a `-1` platform port as absent, instead of failing the whole list. Adapt every reader and struct literal in drive-abci, behaviour-identical for every masternode list Core printed before v24 (all three addresses always present): - platform ports are read from the legacy fields exactly as before, with `#[allow(deprecated)]` on the single statement, pattern or field that touches one; `addresses` is not read; - create_owner_identity v0/v1 and transition_to_version_4 need a value from the now optional owner/payout address and fail with DashCoreBadResponseError when it is absent, instead of inventing one; what an added masternode without them gets is decided separately; - the stored MasternodeV0/MasternodeStateV0 format is unchanged: the addresses are stored as before (an absent one as zero bytes, which only a list the node could not process until now can produce) and read back as present; `payouts` and `addresses` are not stored and read back as `None`; - test fixtures wrap the same values in `Some`, drawing the same random numbers in the same order, so seeded strategy tests are unchanged. No behaviour change for pre-v24 lists, so no new test; the drive-abci tests, including the mainnet listdiff fixture test, pass unchanged. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 24 +-- Cargo.toml | 16 +- .../block_end/update_state_cache/v0/mod.rs | 14 +- .../create_operator_identity/v0/mod.rs | 14 +- .../create_owner_identity/v0/mod.rs | 23 ++- .../create_owner_identity/v1/mod.rs | 25 ++-- .../create_voter_identity/v0/mod.rs | 14 +- .../get_operator_identifier/v0/mod.rs | 14 +- .../update_operator_identity/v0/mod.rs | 140 +++++++++++------- .../update_state_masternode_list/v0/mod.rs | 32 ++-- .../v0/mod.rs | 9 +- .../storage/store_platform_state/v1/mod.rs | 22 ++- .../state_transition/state_transitions/mod.rs | 28 ++-- .../src/platform_types/masternode/v0/mod.rs | 27 ++-- .../platform_state/accessors.rs | 22 ++- .../src/platform_types/validator/v0/mod.rs | 5 +- .../masternode_list_item_helpers.rs | 36 +++-- .../tests/strategy_tests/masternodes.rs | 34 +++-- 18 files changed, 321 insertions(+), 178 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb10ccb208f..eb0cbc7553f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1654,7 +1654,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "cbindgen 0.29.4", "grovedb-bincode", @@ -1665,7 +1665,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "dash-network", ] @@ -1760,7 +1760,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "async-trait", "chrono", @@ -1789,7 +1789,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "anyhow", "base64-compat", @@ -1815,12 +1815,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "dashcore-rpc-json", "hex", @@ -1833,7 +1833,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "dashcore", "grovedb-bincode", @@ -1848,7 +1848,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "dashcore-private", "grovedb-bincode", @@ -2922,7 +2922,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" [[package]] name = "glob" @@ -4154,7 +4154,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "aes", "async-trait", @@ -4183,7 +4183,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4199,7 +4199,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" dependencies = [ "async-trait", "dashcore", diff --git a/Cargo.toml b/Cargo.toml index 08264c2c489..0d73bad6547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,14 +65,14 @@ grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "dce8252f8 grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "dce8252f8665bf06c7bd788e739efba354141690" } grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", rev = "dce8252f8665bf06c7bd788e739efba354141690" } grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "dce8252f8665bf06c7bd788e739efba354141690" } -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } tokio-metrics = "0.5" # Size-tuned profile for the iOS `rs-unified-sdk-ffi` staticlib, which diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs index dd5415f44bf..14f613d8302 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs @@ -282,7 +282,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array([0u8; 32]), collateral_index: 0, - collateral_address: [0u8; 20], + collateral_address: Some([0u8; 20]), operator_reward: 0.0, state: DMNState { service: "1.2.3.4:1234".parse().expect("socket address"), @@ -290,14 +290,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: [0u8; 20], + owner_address: Some([0u8; 20]), voting_address: [0u8; 20], - payout_address: [0u8; 20], + payout_address: Some([0u8; 20]), + payouts: None, pub_key_operator: vec![0u8; 48], operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; block_platform_state diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rs index 8e9be14377c..bab5291b1d4 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rs @@ -51,7 +51,7 @@ mod tests { pro_tx_hash: ProTxHash::from_byte_array(pro_tx), collateral_hash: Txid::from_byte_array([0u8; 32]), collateral_index: 0, - collateral_address: [0u8; 20], + collateral_address: Some([0u8; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -59,14 +59,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: [0u8; 20], + owner_address: Some([0u8; 20]), voting_address: [0u8; 20], - payout_address: [0u8; 20], + payout_address: Some([0u8; 20]), + payouts: None, pub_key_operator: vec![1u8; 48], operator_payout_address, platform_node_id, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs index 503fe5ac07c..7bdd4753d5c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -15,10 +16,16 @@ where masternode: &MasternodeListItem, platform_version: &PlatformVersion, ) -> Result { + let payout_address = masternode.state.payout_address.ok_or_else(|| { + Error::Execution(ExecutionError::DashCoreBadResponseError(format!( + "masternode {} has no payout address", + masternode.pro_tx_hash + ))) + })?; let owner_identifier = Self::get_owner_identifier(masternode)?; let mut identity = Identity::create_basic_identity(owner_identifier, platform_version)?; identity.add_public_keys([Self::get_owner_identity_withdrawal_key( - masternode.state.payout_address, + payout_address, 0, platform_version, )?]); @@ -52,7 +59,7 @@ mod tests { pro_tx_hash: ProTxHash::from_byte_array(pro_tx), collateral_hash: Txid::from_byte_array([0u8; 32]), collateral_index: 0, - collateral_address: [0u8; 20], + collateral_address: Some([0u8; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -60,14 +67,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: [0u8; 20], + owner_address: Some([0u8; 20]), voting_address: [0u8; 20], - payout_address, + payout_address: Some(payout_address), + payouts: None, pub_key_operator: vec![0u8; 48], operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs index ac593abf23d..597f4a287e3 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -14,19 +15,23 @@ where masternode: &MasternodeListItem, platform_version: &PlatformVersion, ) -> Result { + let payout_address = masternode.state.payout_address.ok_or_else(|| { + Error::Execution(ExecutionError::DashCoreBadResponseError(format!( + "masternode {} has no payout address", + masternode.pro_tx_hash + ))) + })?; + let owner_address = masternode.state.owner_address.ok_or_else(|| { + Error::Execution(ExecutionError::DashCoreBadResponseError(format!( + "masternode {} has no owner address", + masternode.pro_tx_hash + ))) + })?; let owner_identifier = Self::get_owner_identifier(masternode)?; let mut identity = Identity::create_basic_identity(owner_identifier, platform_version)?; identity.add_public_keys([ - Self::get_owner_identity_withdrawal_key( - masternode.state.payout_address, - 0, - platform_version, - )?, - Self::get_owner_identity_owner_key( - masternode.state.owner_address, - 1, - platform_version, - )?, + Self::get_owner_identity_withdrawal_key(payout_address, 0, platform_version)?, + Self::get_owner_identity_owner_key(owner_address, 1, platform_version)?, ]); Ok(identity) } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rs index b5a1b148cbd..0d667868338 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rs @@ -60,7 +60,7 @@ mod tests { pro_tx_hash: ProTxHash::from_byte_array(pro_tx), collateral_hash: Txid::from_byte_array([0u8; 32]), collateral_index: 0, - collateral_address: [0u8; 20], + collateral_address: Some([0u8; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -68,14 +68,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: [0u8; 20], + owner_address: Some([0u8; 20]), voting_address, - payout_address: [0u8; 20], + payout_address: Some([0u8; 20]), + payouts: None, pub_key_operator: vec![0u8; 48], operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rs index 8cb6c517ca9..960bc25b933 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rs @@ -37,7 +37,7 @@ mod tests { pro_tx_hash: ProTxHash::from_byte_array(pro_tx), collateral_hash: Txid::from_byte_array([0u8; 32]), collateral_index: 0, - collateral_address: [0u8; 20], + collateral_address: Some([0u8; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -45,14 +45,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: [0u8; 20], + owner_address: Some([0u8; 20]), voting_address: [0u8; 20], - payout_address: [0u8; 20], + payout_address: Some([0u8; 20]), + payouts: None, pub_key_operator, operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_operator_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_operator_identity/v0/mod.rs index e8ff92b902a..f059b250783 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_operator_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_operator_identity/v0/mod.rs @@ -528,7 +528,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -536,14 +536,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(node_id_bytes), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -605,7 +609,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -613,14 +617,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(node_id), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -665,7 +673,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -673,14 +681,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: pub_key_operator.clone(), operator_payout_address: Some(new_operator_payout_address), platform_node_id: Some(node_id), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -738,7 +750,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -746,14 +758,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(node_id_bytes), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -815,7 +831,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -823,14 +839,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(original_node_id), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -876,7 +896,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -884,14 +904,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(new_platform_node_id), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -964,7 +988,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -972,14 +996,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: original_pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(node_id_bytes), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -1094,7 +1122,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -1102,14 +1130,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: original_pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(node_id_bytes), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -1195,7 +1227,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -1203,14 +1235,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: original_pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(node_id_bytes), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -1256,7 +1292,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str("1.0.1.1:1234").unwrap(), @@ -1264,14 +1300,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator: new_pub_key_operator.clone(), operator_payout_address: Some(operator_payout_address), platform_node_id: Some(node_id_bytes), - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs index f3ada50f19b..1138c3aad35 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs @@ -72,11 +72,13 @@ where validator.node_ip = address.ip().to_string(); } - if let Some(p2p_port) = dmn_state_diff.platform_p2p_port { + #[allow(deprecated)] + if let Some(p2p_port) = dmn_state_diff.legacy_platform_p2p_port { validator.platform_p2p_port = p2p_port as u16; } - if let Some(http_port) = dmn_state_diff.platform_http_port { + #[allow(deprecated)] + if let Some(http_port) = dmn_state_diff.legacy_platform_http_port { validator.platform_http_port = http_port as u16; } } @@ -169,10 +171,12 @@ where } // these 3 fields are the only fields that are useful for validators. If they change we need to update // validator sets + #[allow(deprecated)] + let platform_p2p_port_changed = state_diff.legacy_platform_p2p_port.is_some(); if is_hpmn && (state_diff.pose_ban_height.is_some() || state_diff.service.is_some() - || state_diff.platform_p2p_port.is_some()) + || platform_p2p_port_changed) { // we updated the ban status the IP or the platform port, we need to update the validator in the validator list Self::update_masternode_in_validator_sets(pro_tx_hash, state_diff, state); @@ -222,7 +226,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array([0u8; 32]), collateral_index: 0, - collateral_address: [0u8; 20], + collateral_address: Some([0u8; 20]), operator_reward: 0.0, state: DMNState { service: "1.2.3.4:1234".parse().expect("socket address"), @@ -230,14 +234,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: [0u8; 20], + owner_address: Some([0u8; 20]), voting_address: [0u8; 20], - payout_address: [0u8; 20], + payout_address: Some([0u8; 20]), + payouts: None, pub_key_operator: vec![0u8; 48], operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, } } @@ -257,11 +265,15 @@ mod tests { owner_address: None, voting_address: None, payout_address: None, + payouts: None, pub_key_operator: None, operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 229787b8b78..9229b17454f 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -191,8 +191,15 @@ impl Platform { .max() .expect("there must be keys, we already checked"); + let owner_address = masternode.state.owner_address.ok_or_else(|| { + Error::Execution(ExecutionError::DashCoreBadResponseError(format!( + "masternode {} has no owner address", + masternode.pro_tx_hash + ))) + })?; + let new_owner_key = Self::get_owner_identity_owner_key( - masternode.state.owner_address, + owner_address, last_key_id + 1, platform_version, )?; diff --git a/packages/rs-drive-abci/src/execution/storage/store_platform_state/v1/mod.rs b/packages/rs-drive-abci/src/execution/storage/store_platform_state/v1/mod.rs index 762e21bbbd2..c4919e1fd19 100644 --- a/packages/rs-drive-abci/src/execution/storage/store_platform_state/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/storage/store_platform_state/v1/mod.rs @@ -231,7 +231,7 @@ mod tests { pro_tx_hash: pro_tx_hash(byte), collateral_hash: Txid::from_byte_array([0u8; 32]), collateral_index: 0, - collateral_address: [0u8; 20], + collateral_address: Some([0u8; 20]), operator_reward: 0.0, state: DMNState { service: "1.2.3.4:1234".parse().expect("socket address"), @@ -239,14 +239,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: [byte; 20], + owner_address: Some([byte; 20]), voting_address: [0u8; 20], - payout_address: [0u8; 20], + payout_address: Some([0u8; 20]), + payouts: None, pub_key_operator: vec![0u8; 48], operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, } } @@ -432,11 +436,15 @@ mod tests { owner_address: None, voting_address: None, payout_address: None, + payouts: None, pub_key_operator: None, operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, )); let removed_quorum = QuorumHash::from_byte_array([0xAA; 32]); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs index f0ea147d58c..5976e20fa48 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs @@ -751,7 +751,7 @@ pub(in crate::execution) mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen()), collateral_index: 0, - collateral_address: rng.gen(), + collateral_address: Some(rng.gen()), operator_reward: 0.0, state: DMNState { service: SocketAddr::new(IpAddr::V4(random_ip), 19999), @@ -759,14 +759,18 @@ pub(in crate::execution) mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address, + owner_address: Some(owner_address), voting_address: rng.gen(), - payout_address, + payout_address: Some(payout_address), + payouts: None, pub_key_operator: vec![], operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }, ); @@ -840,7 +844,7 @@ pub(in crate::execution) mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen()), collateral_index: 0, - collateral_address: rng.gen(), + collateral_address: Some(rng.gen()), operator_reward: 0.0, state: DMNState { service: SocketAddr::new(IpAddr::V4(random_ip), 19999), @@ -848,14 +852,18 @@ pub(in crate::execution) mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen(), + owner_address: Some(rng.gen()), voting_address, - payout_address: rng.gen(), + payout_address: Some(rng.gen()), + payouts: None, pub_key_operator: vec![], operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }, ); diff --git a/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs index 43e9dba0359..b18b9833026 100644 --- a/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs @@ -62,7 +62,7 @@ impl From for MasternodeV0 { pro_tx_hash, collateral_hash, collateral_index, - collateral_address, + collateral_address: collateral_address.unwrap_or_default(), operator_reward, state: state.into(), } @@ -86,7 +86,7 @@ impl From for MasternodeListItem { pro_tx_hash, collateral_hash, collateral_index, - collateral_address, + collateral_address: Some(collateral_address), operator_reward, state: state.into(), } @@ -139,6 +139,8 @@ pub struct MasternodeStateV0 { impl From for MasternodeStateV0 { fn from(value: DMNState) -> Self { + // The payout list and the nested addresses are not stored. + #[allow(deprecated)] let DMNState { service, registered_height, @@ -148,11 +150,13 @@ impl From for MasternodeStateV0 { owner_address, voting_address, payout_address, + payouts: _, pub_key_operator, operator_payout_address, platform_node_id, - platform_p2p_port, - platform_http_port, + legacy_platform_p2p_port: platform_p2p_port, + legacy_platform_http_port: platform_http_port, + addresses: _, } = value; Self { @@ -161,9 +165,9 @@ impl From for MasternodeStateV0 { pose_revived_height, pose_ban_height, revocation_reason, - owner_address, + owner_address: owner_address.unwrap_or_default(), voting_address, - payout_address, + payout_address: payout_address.unwrap_or_default(), pub_key_operator, operator_payout_address, platform_node_id, @@ -191,20 +195,23 @@ impl From for DMNState { platform_http_port, } = value; + #[allow(deprecated)] Self { service, registered_height, pose_revived_height, pose_ban_height, revocation_reason, - owner_address, + owner_address: Some(owner_address), voting_address, - payout_address, + payout_address: Some(payout_address), + payouts: None, pub_key_operator, operator_payout_address, platform_node_id, - platform_p2p_port, - platform_http_port, + legacy_platform_p2p_port: platform_p2p_port, + legacy_platform_http_port: platform_http_port, + addresses: None, } } } diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs b/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs index c3b16df2223..73a1990a4ea 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs @@ -895,7 +895,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array([0u8; 32]), collateral_index: 0, - collateral_address: [0u8; 20], + collateral_address: Some([0u8; 20]), operator_reward: 0.0, state: DMNState { service: "1.2.3.4:1234".parse().expect("socket address"), @@ -903,14 +903,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: [0u8; 20], + owner_address: Some([0u8; 20]), voting_address: [0u8; 20], - payout_address: [0u8; 20], + payout_address: Some([0u8; 20]), + payouts: None, pub_key_operator: vec![0u8; 48], operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, } } @@ -928,11 +932,15 @@ mod tests { owner_address: None, voting_address: None, payout_address: None, + payouts: None, pub_key_operator: None, operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, } } diff --git a/packages/rs-drive-abci/src/platform_types/validator/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/validator/v0/mod.rs index f52b335b54e..2fbab063bd1 100644 --- a/packages/rs-drive-abci/src/platform_types/validator/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/validator/v0/mod.rs @@ -22,12 +22,13 @@ impl NewValidatorIfMasternodeInState for ValidatorV0 { ) -> Option { let MasternodeListItem { state, .. } = state.hpmn_masternode_list().get(&pro_tx_hash)?; + #[allow(deprecated)] let DMNState { service, platform_node_id, pose_ban_height, - platform_p2p_port, - platform_http_port, + legacy_platform_p2p_port: platform_p2p_port, + legacy_platform_http_port: platform_http_port, .. } = state; let Some(platform_http_port) = platform_http_port else { diff --git a/packages/rs-drive-abci/tests/strategy_tests/masternode_list_item_helpers.rs b/packages/rs-drive-abci/tests/strategy_tests/masternode_list_item_helpers.rs index 82f61aaf0ba..3ada8f30353 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/masternode_list_item_helpers.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/masternode_list_item_helpers.rs @@ -15,8 +15,10 @@ impl UpdateMasternodeListItem for MasternodeListItem { .filter(|&field_idx| match field_idx { 4 => self.state.operator_payout_address.is_some(), 5 => self.state.platform_node_id.is_some(), - 6 => self.state.platform_p2p_port.is_some(), - 7 => self.state.platform_http_port.is_some(), + #[allow(deprecated)] + 6 => self.state.legacy_platform_p2p_port.is_some(), + #[allow(deprecated)] + 7 => self.state.legacy_platform_http_port.is_some(), _ => true, }) .collect(); @@ -29,9 +31,9 @@ impl UpdateMasternodeListItem for MasternodeListItem { available_fields.retain(|&idx| idx != field_idx); match field_idx { - 0 => self.state.owner_address = rng.gen::<[u8; 20]>(), + 0 => self.state.owner_address = Some(rng.gen::<[u8; 20]>()), 1 => self.state.voting_address = rng.gen::<[u8; 20]>(), - 2 => self.state.payout_address = rng.gen::<[u8; 20]>(), + 2 => self.state.payout_address = Some(rng.gen::<[u8; 20]>()), 3 => { let private_key_operator_bytes = bls_signatures::PrivateKey::generate_dash(rng) .expect("expected to generate a private key") @@ -55,13 +57,17 @@ impl UpdateMasternodeListItem for MasternodeListItem { *address = rng.gen::<[u8; 20]>(); } } - 6 => { - if let Some(ref mut port) = self.state.platform_p2p_port { + 6 => + { + #[allow(deprecated)] + if let Some(ref mut port) = self.state.legacy_platform_p2p_port { *port = rng.gen_range(1024..=65535); } } - 7 => { - if let Some(ref mut port) = self.state.platform_http_port { + 7 => + { + #[allow(deprecated)] + if let Some(ref mut port) = self.state.legacy_platform_http_port { *port = rng.gen_range(1024..=65535); } } @@ -101,7 +107,7 @@ mod tests { pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str(format!("1.0.{}.{}:1234", i / 256, i % 256).as_str()) @@ -110,14 +116,18 @@ mod tests { pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: rng.gen::<[u8; 20]>(), - payout_address: rng.gen::<[u8; 20]>(), + payout_address: Some(rng.gen::<[u8; 20]>()), + payouts: None, pub_key_operator, operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; diff --git a/packages/rs-drive-abci/tests/strategy_tests/masternodes.rs b/packages/rs-drive-abci/tests/strategy_tests/masternodes.rs index 55c2794bf59..60d53f8fabc 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/masternodes.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/masternodes.rs @@ -244,7 +244,7 @@ pub fn generate_test_masternodes( pro_tx_hash, collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str(format!("1.0.{}.{}:1234", i / 256, i % 256).as_str()) @@ -253,14 +253,18 @@ pub fn generate_test_masternodes( pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: generate_voting_address(rng, add_voting_keys_to_signer), - payout_address: generate_payout_address(rng, add_payout_keys_to_signer), + payout_address: Some(generate_payout_address(rng, add_payout_keys_to_signer)), + payouts: None, pub_key_operator, operator_payout_address: None, platform_node_id: None, - platform_p2p_port: None, - platform_http_port: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, }, }; @@ -381,7 +385,7 @@ pub fn generate_test_masternodes( pro_tx_hash: ProTxHash::from_byte_array(rng.gen::<[u8; 32]>()), collateral_hash: Txid::from_byte_array(rng.gen::<[u8; 32]>()), collateral_index: 0, - collateral_address: [0; 20], + collateral_address: Some([0; 20]), operator_reward: 0.0, state: DMNState { service: SocketAddr::from_str(format!("1.1.{}.{}:1234", i / 256, i % 256).as_str()) @@ -390,14 +394,18 @@ pub fn generate_test_masternodes( pose_revived_height: None, pose_ban_height: None, revocation_reason: 0, - owner_address: rng.gen::<[u8; 20]>(), + owner_address: Some(rng.gen::<[u8; 20]>()), voting_address: generate_voting_address(rng, add_voting_keys_to_signer), - payout_address: generate_payout_address(rng, add_payout_keys_to_signer), + payout_address: Some(generate_payout_address(rng, add_payout_keys_to_signer)), + payouts: None, pub_key_operator, operator_payout_address: None, platform_node_id: Some(rng.gen::<[u8; 20]>()), - platform_p2p_port: Some(3010), - platform_http_port: Some(8080), + #[allow(deprecated)] + legacy_platform_p2p_port: Some(3010), + #[allow(deprecated)] + legacy_platform_http_port: Some(8080), + addresses: None, }, }; @@ -530,12 +538,14 @@ pub fn generate_test_masternodes( SocketAddr::new(IpAddr::V4(random_ip), old_port); } if update.p2p_port { - if let Some(port) = hpmn_list_item_b.state.platform_p2p_port.as_mut() { + #[allow(deprecated)] + if let Some(port) = hpmn_list_item_b.state.legacy_platform_p2p_port.as_mut() { *port += 1 } } if update.http_port { - if let Some(port) = hpmn_list_item_b.state.platform_http_port.as_mut() { + #[allow(deprecated)] + if let Some(port) = hpmn_list_item_b.state.legacy_platform_http_port.as_mut() { *port += 1 } } From 6a2ed41945ed8403b1084adc9ef10e337008a7bf Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 1 Oct 2026 00:56:44 +0700 Subject: [PATCH 03/11] fix(drive-abci): process Core v24 shared and multi-payout masternodes Dash Core v24 lists masternodes every shipped Drive fails to parse, which halts the chain: a shared masternode has no owner, payout or collateral address, and an extended-address masternode has a `payouts` list instead of `payoutAddress`. Now that rust-dashcore parses them, decide what an added one gets. Its single payout address is the payout address, else the only entry of a one-entry payout list (Core turns a payout address into such a list when a masternode moves to extended addresses), else none. - No owner address (shared): no owner identity. Voter and operator identities as for any masternode. - Owner address and a single payout address: unchanged, TRANSFER key 0 and OWNER key 1, byte-identical to a legacy payout address. - Owner address and several payouts: an owner identity with only the OWNER key 1; key id 0 stays free. - create_owner_identity v0 (protocol versions 1-3), whose identity holds only the TRANSFER key: no owner identity without a single payout address. - transition_to_version_4 adds no OWNER key for a masternode without an owner address, after the existing skip of masternodes without owner identity keys. - Diffs are unchanged: only `payout_address` rotates the TRANSFER key; a `payouts` change is parsed but not acted on. - Stored masternodes keep their format: an absent owner, payout or collateral address is stored as zero bytes and now reads back as absent; `payouts` is not stored. - Drive logs at debug level, with the ProTx hash and the reason, each added masternode that gets no owner identity or one with only the OWNER key, and each masternode transition_to_version_4 gives no OWNER key. In-place change to shipped generations: create_owner_identity v0 (protocol versions 1-3) and v1 (4-14), update_masternode_identities v0 and perform_events_on_first_block_of_protocol_change v0 (transition_to_version_4, every version). Every input that takes a new branch is an added masternode without an owner address or without a single payout address, which only a Core v24 masternode list contains and every shipped binary rejects at deserialization, so no committed block was produced from one. Pre-v24 inputs are unchanged: Core prints an owner and payout address for every earlier masternode and refuses a null owner key, and a single payout gives the legacy identity byte for byte. The stored-state conversion is not consensus state; nothing that decides a block reads a stored payout or collateral address or the payout list. Tests, red to green: - should_update_masternode_identities_from_a_core_v24_masternode_list runs Core v24 `protx listdiff` JSON (shared, one payout, Tor primary address with two payouts, Evo with `-1` ports, IPv6 Evo with two payouts, then a legacy-to-extended-address diff and payout-list-only diffs) through update_masternode_list with the mock Core RPC. Against rust-dashcore e4208c90 it fails parsing: CoreRpc(Json(Error("missing field `payoutAddress`", line: 74, column: 7))) on the parent commit it fails with DashCoreBadResponseError("masternode 27978dd8...0bef has no payout address") and it passes here. - create_owner_identity: shared (protocol versions 1 and latest), single payout equals payout address byte for byte (1 and latest), several payouts give OWNER key 1 only, several payouts on version 1 give no owner identity. All four fail on the parent commit with DashCoreBadResponseError and pass here. - should_reload_absent_addresses_as_absent fails on the parent commit (Some([0; 20]) read back) and passes here. - should_add_no_owner_key_for_a_masternode_without_owner_address_on_transition_to_version_4 fails on the parent commit with DashCoreBadResponseError and passes here. - should_not_change_owner_identity_keys_on_a_payout_list_change and should_reload_a_masternode_with_every_address_unchanged pin unchanged behaviour and pass on both. Co-Authored-By: Claude Opus 5.5 --- .../create_owner_identity/mod.rs | 288 +++++++++++++++++- .../create_owner_identity/v0/mod.rs | 39 ++- .../create_owner_identity/v1/mod.rs | 55 ++-- .../update_masternode_identities/v0/mod.rs | 206 ++++++++++++- .../update_masternode_list/mod.rs | 271 ++++++++++++++++ .../v0/mod.rs | 150 ++++++++- .../src/platform_types/masternode/v0/mod.rs | 144 ++++++++- .../core_v24_protx_list_diffs/1-1250.json | 279 +++++++++++++++++ .../core_v24_protx_list_diffs/1250-1260.json | 61 ++++ 9 files changed, 1436 insertions(+), 57 deletions(-) create mode 100644 packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/1-1250.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/1250-1260.json diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs index f341cee2f1b..6b715c9e469 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs @@ -5,7 +5,7 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; -use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeListItem; +use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem}; use dpp::identity::Identity; use dpp::version::PlatformVersion; @@ -25,12 +25,13 @@ where /// /// # Returns /// - /// * Result - Returns the constructed identity for the owner if successful. - /// Otherwise, returns an error. + /// * Result, Error> - The owner identity, or `None` when the masternode + /// has no key an owner identity of this version holds: a shared masternode has no owner + /// and no payout address. Otherwise, returns an error. pub(crate) fn create_owner_identity( masternode: &MasternodeListItem, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result, Error> { match platform_version .drive_abci .methods @@ -48,3 +49,282 @@ where } } } + +/// The one address the owner reward of a masternode is paid to, which its owner identity's +/// TRANSFER key holds: the payout address, or the only entry of a payout list with one entry +/// (Core turns a payout address into such a list when a masternode moves to extended +/// addresses). `None` for a shared masternode, which has neither, and for a reward split +/// between several payouts. +/// +/// The stored masternode list does not keep payout lists, so this is only meaningful for a +/// masternode as Core reported it, never for one read from the stored list. +/// +/// Shared by the shipped `create_owner_identity` v0 and v1: changing it changes both. +fn effective_payout_address(state: &DMNState) -> Option<[u8; 20]> { + match (state.payout_address, state.payouts.as_deref()) { + (Some(payout_address), _) => Some(payout_address), + (None, Some([payout])) => Some(payout.address), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::block::block_info::BlockInfo; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{ProTxHash, PubkeyHash, ScriptBuf, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{ + DMNPayout, DMNState, MasternodeListDiff, MasternodeListItem, MasternodeType, + }; + use dpp::identifier::MasternodeIdentifiers; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{Identity, KeyID, Purpose}; + use dpp::prelude::Identifier; + use dpp::serialization::PlatformSerializable; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + const PRO_TX_HASH: [u8; 32] = [0x21; 32]; + const OWNER_ADDRESS: [u8; 20] = [0x22; 20]; + const VOTING_ADDRESS: [u8; 20] = [0x23; 20]; + const PAYOUT_ADDRESS: [u8; 20] = [0x24; 20]; + const SECOND_PAYOUT_ADDRESS: [u8; 20] = [0x25; 20]; + const PUB_KEY_OPERATOR: [u8; 48] = [0x26; 48]; + + /// A Regular masternode as Core lists it: a shared one has neither owner, payout nor + /// collateral address; an extended-address one has `payouts` instead of `payout_address`. + fn masternode( + owner_address: Option<[u8; 20]>, + payout_address: Option<[u8; 20]>, + payouts: Option>, + ) -> MasternodeListItem { + MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array(PRO_TX_HASH), + collateral_hash: Txid::from_byte_array([0x27; 32]), + collateral_index: 0, + collateral_address: owner_address.map(|_| [0x28; 20]), + operator_reward: 0.0, + state: DMNState { + service: "1.2.3.4:9999".parse().expect("socket address"), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address, + voting_address: VOTING_ADDRESS, + payout_address, + payouts, + pub_key_operator: PUB_KEY_OPERATOR.to_vec(), + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + }, + } + } + + fn p2pkh_payout(key_hash: [u8; 20], reward: u16) -> DMNPayout { + DMNPayout { + address: key_hash, + script: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(key_hash)), + reward, + } + } + + fn shared_masternode() -> MasternodeListItem { + masternode(None, None, None) + } + + fn legacy_masternode() -> MasternodeListItem { + masternode(Some(OWNER_ADDRESS), Some(PAYOUT_ADDRESS), None) + } + + fn single_payout_masternode() -> MasternodeListItem { + masternode( + Some(OWNER_ADDRESS), + None, + Some(vec![p2pkh_payout(PAYOUT_ADDRESS, 10000)]), + ) + } + + fn multi_payout_masternode() -> MasternodeListItem { + masternode( + Some(OWNER_ADDRESS), + None, + Some(vec![ + p2pkh_payout(PAYOUT_ADDRESS, 7000), + p2pkh_payout(SECOND_PAYOUT_ADDRESS, 3000), + ]), + ) + } + + /// A platform on `platform_version` whose masternode identities were created for + /// `masternode`, added on the first block. + fn platform_with_added_masternode( + masternode: MasternodeListItem, + platform_version: &PlatformVersion, + ) -> TempPlatform { + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + platform + .update_masternode_identities( + MasternodeListDiff { + base_height: 0, + block_height: 1, + added_mns: vec![masternode], + removed_mns: vec![], + updated_mns: vec![], + }, + &BTreeMap::new(), + &BlockInfo::default(), + None, + &transaction, + platform_version, + ) + .expect("expected to create the masternode identities"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + platform + } + + fn fetch_identity( + platform: &TempPlatform, + identity_id: Identifier, + platform_version: &PlatformVersion, + ) -> Option { + platform + .drive + .fetch_full_identity(identity_id.to_buffer(), None, platform_version) + .expect("expected to fetch an identity") + } + + fn owner_identity( + platform: &TempPlatform, + platform_version: &PlatformVersion, + ) -> Option { + fetch_identity(platform, PRO_TX_HASH.into(), platform_version) + } + + fn assert_voter_and_operator_identities_exist( + platform: &TempPlatform, + platform_version: &PlatformVersion, + ) { + let voter_identifier = Identifier::create_voter_identifier(&PRO_TX_HASH, &VOTING_ADDRESS); + let operator_identifier = + Identifier::create_operator_identifier(&PRO_TX_HASH, &PUB_KEY_OPERATOR); + assert!( + fetch_identity(platform, voter_identifier, platform_version).is_some(), + "the voter identity must be created" + ); + assert!( + fetch_identity(platform, operator_identifier, platform_version).is_some(), + "the operator identity must be created" + ); + } + + /// Both generations: the owner identity carries the owner's keys, and a shared + /// masternode has no owner key (its owners are its share holders), so it gets no owner + /// identity. Its voter and operator identities are created as for any masternode. + #[test] + fn should_create_no_owner_identity_for_a_shared_masternode() { + for platform_version in [PlatformVersion::first(), PlatformVersion::latest()] { + let platform = platform_with_added_masternode(shared_masternode(), platform_version); + + assert_eq!( + owner_identity(&platform, platform_version), + None, + "protocol version {}", + platform_version.protocol_version + ); + assert_voter_and_operator_identities_exist(&platform, platform_version); + } + } + + /// Both generations: Core turns a payout address into a payout list with one entry + /// when a masternode moves to extended addresses, so a single payout must give the + /// owner identity a payout address gives, byte for byte. + #[test] + fn should_create_the_same_owner_identity_for_a_single_payout_as_for_a_payout_address() { + for platform_version in [PlatformVersion::first(), PlatformVersion::latest()] { + let legacy_owner_identity = owner_identity( + &platform_with_added_masternode(legacy_masternode(), platform_version), + platform_version, + ) + .expect("expected an owner identity for a payout address"); + let single_payout_owner_identity = owner_identity( + &platform_with_added_masternode(single_payout_masternode(), platform_version), + platform_version, + ) + .expect("expected an owner identity for a single payout"); + + assert_eq!( + single_payout_owner_identity + .serialize_to_bytes() + .expect("expected to serialize"), + legacy_owner_identity + .serialize_to_bytes() + .expect("expected to serialize"), + "protocol version {}", + platform_version.protocol_version + ); + } + } + + /// Several payouts have no single address for the TRANSFER key id 0, so the owner + /// identity gets only the OWNER key id 1 a payout address's owner identity has, and key + /// id 0 stays free. + #[test] + fn should_create_an_owner_identity_with_only_the_owner_key_for_several_payouts() { + let platform_version = PlatformVersion::latest(); + let multi_payout_owner_identity = owner_identity( + &platform_with_added_masternode(multi_payout_masternode(), platform_version), + platform_version, + ) + .expect("expected an owner identity for several payouts"); + let legacy_owner_identity = owner_identity( + &platform_with_added_masternode(legacy_masternode(), platform_version), + platform_version, + ) + .expect("expected an owner identity for a payout address"); + + let owner_key = legacy_owner_identity + .public_keys() + .get(&1) + .expect("expected the OWNER key id 1") + .clone(); + assert_eq!(owner_key.purpose(), Purpose::OWNER); + assert_eq!(owner_key.data().as_slice(), OWNER_ADDRESS.as_slice()); + assert_eq!( + multi_payout_owner_identity.public_keys(), + &BTreeMap::from([(1 as KeyID, owner_key)]) + ); + } + + /// The first generation's owner identity holds only the TRANSFER key of the payout + /// address, so several payouts leave it with no key: no owner identity is created, as + /// for a shared masternode. + #[test] + fn should_create_no_owner_identity_for_several_payouts_in_protocol_versions_without_owner_keys() + { + let platform_version = PlatformVersion::first(); + let platform = platform_with_added_masternode(multi_payout_masternode(), platform_version); + + assert_eq!(owner_identity(&platform, platform_version), None); + assert_voter_and_operator_identities_exist(&platform, platform_version); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs index 7bdd4753d5c..dcb7634b183 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs @@ -1,4 +1,4 @@ -use crate::error::execution::ExecutionError; +use super::effective_payout_address; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -15,13 +15,21 @@ where pub(super) fn create_owner_identity_v0( masternode: &MasternodeListItem, platform_version: &PlatformVersion, - ) -> Result { - let payout_address = masternode.state.payout_address.ok_or_else(|| { - Error::Execution(ExecutionError::DashCoreBadResponseError(format!( - "masternode {} has no payout address", - masternode.pro_tx_hash - ))) - })?; + ) -> Result, Error> { + // The identity holds only the payout address's TRANSFER key, so a masternode without a + // single payout address (shared, or paid to several payouts) gets none. Only a Core v24 + // masternode list has such a masternode, and no earlier binary can parse one, so no + // committed block reached this branch. A chain on Core v24 with V24 active must start at + // protocol version 4 or later: an Evo paid to several payouts gets no owner identity + // here, so paying it as a block proposer would fail. + let Some(payout_address) = effective_payout_address(&masternode.state) else { + tracing::debug!( + pro_tx_hash = %masternode.pro_tx_hash, + method = "create_owner_identity_v0", + "no owner identity: the masternode has no single payout address" + ); + return Ok(None); + }; let owner_identifier = Self::get_owner_identifier(masternode)?; let mut identity = Identity::create_basic_identity(owner_identifier, platform_version)?; identity.add_public_keys([Self::get_owner_identity_withdrawal_key( @@ -29,7 +37,7 @@ where 0, platform_version, )?]); - Ok(identity) + Ok(Some(identity)) } pub(crate) fn get_owner_identifier( @@ -105,7 +113,8 @@ mod tests { let mn = make_masternode(pro_tx, [0x12u8; 20]); let identity = Platform::::create_owner_identity_v0(&mn, platform_version) - .expect("create_owner_identity_v0 must succeed"); + .expect("create_owner_identity_v0 must succeed") + .expect("a masternode with a payout address has an owner identity"); assert_eq!(identity.id(), Identifier::from(pro_tx)); assert_eq!( @@ -122,10 +131,12 @@ mod tests { let a = make_masternode([0x01u8; 32], [0u8; 20]); let b = make_masternode([0x02u8; 32], [0u8; 20]); - let ia = - Platform::::create_owner_identity_v0(&a, platform_version).expect("a"); - let ib = - Platform::::create_owner_identity_v0(&b, platform_version).expect("b"); + let ia = Platform::::create_owner_identity_v0(&a, platform_version) + .expect("a") + .expect("a has an owner identity"); + let ib = Platform::::create_owner_identity_v0(&b, platform_version) + .expect("b") + .expect("b has an owner identity"); assert_ne!(ia.id(), ib.id()); } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs index 597f4a287e3..0eb2ddadece 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs @@ -1,4 +1,4 @@ -use crate::error::execution::ExecutionError; +use super::effective_payout_address; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -14,25 +14,42 @@ where pub(super) fn create_owner_identity_v1( masternode: &MasternodeListItem, platform_version: &PlatformVersion, - ) -> Result { - let payout_address = masternode.state.payout_address.ok_or_else(|| { - Error::Execution(ExecutionError::DashCoreBadResponseError(format!( - "masternode {} has no payout address", - masternode.pro_tx_hash - ))) - })?; - let owner_address = masternode.state.owner_address.ok_or_else(|| { - Error::Execution(ExecutionError::DashCoreBadResponseError(format!( - "masternode {} has no owner address", - masternode.pro_tx_hash - ))) - })?; + ) -> Result, Error> { + // Only a Core v24 masternode list has a masternode without an owner address or without + // a single payout address, and no earlier binary can parse one, so no committed block + // reached the branches below that skip a key. + // + // A shared masternode has no owner key: its owners are its share holders. + let Some(owner_address) = masternode.state.owner_address else { + tracing::debug!( + pro_tx_hash = %masternode.pro_tx_hash, + method = "create_owner_identity_v1", + "no owner identity: the masternode has no owner address" + ); + return Ok(None); + }; let owner_identifier = Self::get_owner_identifier(masternode)?; let mut identity = Identity::create_basic_identity(owner_identifier, platform_version)?; - identity.add_public_keys([ - Self::get_owner_identity_withdrawal_key(payout_address, 0, platform_version)?, - Self::get_owner_identity_owner_key(owner_address, 1, platform_version)?, - ]); - Ok(identity) + // Several payouts have no single address for the TRANSFER key, so the identity holds + // only the OWNER key, under the same id as next to a TRANSFER key. + if let Some(payout_address) = effective_payout_address(&masternode.state) { + identity.add_public_keys([Self::get_owner_identity_withdrawal_key( + payout_address, + 0, + platform_version, + )?]); + } else { + tracing::debug!( + pro_tx_hash = %masternode.pro_tx_hash, + method = "create_owner_identity_v1", + "owner identity with only the OWNER key: no single payout address" + ); + } + identity.add_public_keys([Self::get_owner_identity_owner_key( + owner_address, + 1, + platform_version, + )?]); + Ok(Some(identity)) } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs index 6cdff83e2f1..06458c123a9 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs @@ -81,10 +81,17 @@ where "create operator identity" ); - drive_operations.push(IdentityOperation(AddNewIdentity { - identity: owner_identity, - is_masternode_identity: true, - })); + // A masternode with none of the keys an owner identity holds, such as a shared + // masternode, has no owner identity. Only an added masternode without an owner + // address, or with `payouts` instead of `payoutAddress`, gets none, and no earlier + // binary can deserialize a masternode list holding one, so no committed block + // skipped an owner identity here. + if let Some(owner_identity) = owner_identity { + drive_operations.push(IdentityOperation(AddNewIdentity { + identity: owner_identity, + is_masternode_identity: true, + })); + } drive_operations.push(IdentityOperation(AddNewIdentity { identity: voter_identity, @@ -166,3 +173,194 @@ where Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::config::PlatformConfig; + use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{Network, ProTxHash, PubkeyHash, ScriptBuf, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{ + DMNPayout, DMNState, DMNStateDiff, MasternodeListDiff, MasternodeListItem, MasternodeType, + }; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyID, Purpose}; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + /// A payout change of an extended-address masternode reaches Drive only as `payouts`, + /// never as `payout_address`. Only a `payout_address` change rotates the owner identity's + /// TRANSFER key; acting on `payouts` as well would change what a block does to owner + /// identities, which needs a new method version. + #[test] + fn should_not_change_owner_identity_keys_on_a_payout_list_change() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let pro_tx_hash = ProTxHash::from_byte_array([0x51; 32]); + let owner_address = [0x52; 20]; + let payout_address = [0x53; 20]; + let masternode = MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash, + collateral_hash: Txid::from_byte_array([0x54; 32]), + collateral_index: 0, + collateral_address: Some([0x55; 20]), + operator_reward: 0.0, + state: DMNState { + service: "1.2.3.4:9999".parse().expect("socket address"), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address: Some(owner_address), + voting_address: [0x56; 20], + payout_address: Some(payout_address), + payouts: None, + pub_key_operator: vec![0x57; 48], + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + }, + }; + + let transaction = platform.drive.grove.start_transaction(); + platform + .update_masternode_identities( + MasternodeListDiff { + base_height: 0, + block_height: 1, + added_mns: vec![masternode.clone()], + removed_mns: vec![], + updated_mns: vec![], + }, + &BTreeMap::new(), + &BlockInfo::default(), + None, + &transaction, + platform_version, + ) + .expect("expected to create the masternode identities"); + + let mut platform_state = PlatformState::default_with_protocol_versions( + platform_version.protocol_version, + platform_version.protocol_version, + &PlatformConfig::default_for_network(Network::Testnet), + ) + .expect("platform state"); + platform_state.insert_masternode(masternode); + + let apply_update = |state_diff: DMNStateDiff| { + platform + .update_masternode_identities( + MasternodeListDiff { + base_height: 1, + block_height: 2, + added_mns: vec![], + removed_mns: vec![], + updated_mns: vec![(pro_tx_hash, state_diff)], + }, + &BTreeMap::new(), + &BlockInfo::default(), + Some(&platform_state), + &transaction, + platform_version, + ) + .expect("expected to process the masternode update"); + }; + let owner_identity_keys = || -> BTreeMap, bool)> { + platform + .drive + .fetch_full_identity( + pro_tx_hash.to_byte_array(), + Some(&transaction), + platform_version, + ) + .expect("expected to fetch the owner identity") + .expect("expected the owner identity to exist") + .public_keys() + .iter() + .map(|(key_id, key)| { + ( + *key_id, + (key.purpose(), key.data().to_vec(), key.is_disabled()), + ) + }) + .collect() + }; + let keys_before = BTreeMap::from([ + (0, (Purpose::TRANSFER, payout_address.to_vec(), false)), + (1, (Purpose::OWNER, owner_address.to_vec(), false)), + ]); + assert_eq!(owner_identity_keys(), keys_before); + + let payout = |address: [u8; 20], reward: u16| DMNPayout { + address, + script: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(address)), + reward, + }; + for payouts in [ + // The payout address moved into a list, as Core does when a masternode moves to + // extended addresses. + vec![payout(payout_address, 10000)], + // Another single payout address. + vec![payout([0x58; 20], 10000)], + // Several payouts. + vec![payout([0x58; 20], 2500), payout([0x59; 20], 7500)], + ] { + apply_update(DMNStateDiff { + payouts: Some(payouts.clone()), + ..empty_state_diff() + }); + assert_eq!(owner_identity_keys(), keys_before, "payouts {payouts:?}"); + } + + // The same processing does rotate the TRANSFER key on a payout address change. + apply_update(DMNStateDiff { + payout_address: Some([0x5a; 20]), + ..empty_state_diff() + }); + assert_eq!( + owner_identity_keys(), + BTreeMap::from([ + (0, (Purpose::TRANSFER, payout_address.to_vec(), true)), + (1, (Purpose::OWNER, owner_address.to_vec(), false)), + (2, (Purpose::TRANSFER, vec![0x5a; 20], false)), + ]) + ); + } + + fn empty_state_diff() -> DMNStateDiff { + DMNStateDiff { + service: None, + registered_height: None, + last_paid_height: None, + consecutive_payments: None, + pose_penalty: None, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: None, + owner_address: None, + voting_address: None, + payout_address: None, + payouts: None, + pub_key_operator: None, + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs index a656a484c91..ef114d68582 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs @@ -73,12 +73,26 @@ where mod test { use super::*; use crate::config::PlatformConfig; + use crate::platform_types::platform_state::platform_state_for_saving::v2::{ + deserialize_masternode_entry, serialize_masternode_entry, + }; + use crate::platform_types::platform_state::PlatformStateV0Methods; + use crate::platform_types::validator::v0::{NewValidatorIfMasternodeInState, ValidatorV0}; use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::ProTxHash; use dpp::dashcore_rpc::json::MasternodeListDiff; + use dpp::identifier::MasternodeIdentifiers; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyID, Purpose}; + use dpp::prelude::Identifier; + use std::collections::BTreeMap; use std::env; use std::fs::File; use std::io::BufReader; use std::path::PathBuf; + use std::str::FromStr; #[test] fn test_update_masternode_list() { @@ -198,4 +212,261 @@ mod test { .unwrap() .expect("expected to commit"); } + + /// Dash Core v24 prints masternodes that differ from every earlier list: a shared + /// masternode has no owner, payout or collateral address; an extended-address masternode + /// has a `payouts` list instead of `payoutAddress`; a Tor primary address is not an + /// `ip:port` `service`; an Evo without addresses has `-1` platform ports. The list must be + /// processed rather than fail the block, and the masternode identities must be: + /// - shared: voter and operator identities, no owner identity (it has no owner key); + /// - one payout: the same owner identity as a legacy payout address (TRANSFER key 0, + /// OWNER key 1); + /// - several payouts: an owner identity with only the OWNER key 1; + /// - a later payout change through `payouts`: no key change, since only a `payoutAddress` + /// change rotates the TRANSFER key. + #[test] + fn should_update_masternode_identities_from_a_core_v24_masternode_list() { + let platform_version = PlatformVersion::latest(); + + let mut platform = TestPlatformBuilder::new() + .with_config(PlatformConfig::default()) + .build_with_mock_rpc() + .set_genesis_state(); + + let genesis_core_block_height = 1250; + let first_block_core_block_height = 1260; + + platform + .core_rpc + .expect_get_protx_diff_with_masternodes() + .returning(move |_base_block, block| { + let file_name = if block == genesis_core_block_height { + "1-1250.json" + } else { + "1250-1260.json" + }; + let json = std::fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/supporting_files/core_v24_protx_list_diffs") + .join(file_name), + ) + .expect("expected to read the masternode list diff"); + // Parse it as the Core RPC client parses the `protx listdiff` response, so a + // response the client rejects fails the update with the same error. + Ok(serde_json::from_str(&json)?) + }); + + let transaction = platform.drive.grove.start_transaction(); + + let mut init_chain_platform_state = platform.state.load().as_ref().clone(); + + platform + .update_masternode_list( + None, + &mut init_chain_platform_state, + genesis_core_block_height, + true, + &BlockInfo { + height: 1, + core_height: genesis_core_block_height, + time_ms: 1, + ..Default::default() + }, + &transaction, + platform_version, + ) + .expect("expected to process the Core v24 masternode list"); + + let platform_state = init_chain_platform_state.clone(); + let mut block_platform_state = platform_state.clone(); + + platform + .update_masternode_list( + Some(&platform_state), + &mut block_platform_state, + first_block_core_block_height, + false, + &BlockInfo { + height: 1, + core_height: first_block_core_block_height, + time_ms: 2, + ..Default::default() + }, + &transaction, + platform_version, + ) + .expect("expected to process the Core v24 masternode list diff"); + + assert_eq!(block_platform_state.full_masternode_list().len(), 7); + assert_eq!(block_platform_state.hpmn_masternode_list().len(), 3); + + // An Evo without addresses prints `-1` platform ports, which read as no port, so it + // cannot be a validator, like any Evo without ports. The legacy Evo is one, with the + // ports and address the diff gave it. + let validator = |pro_tx_hash: &str| { + ValidatorV0::new_validator_if_masternode_in_state( + ProTxHash::from_str(pro_tx_hash).expect("expected a pro tx hash"), + None, + &block_platform_state, + ) + }; + assert!( + validator("6f1757595185032c808321af3e2e8468fae10b8f91e2a657d7a4c7122f4b2706").is_none() + ); + let legacy_evo_validator = + validator("ca3d32c4f2ef62eaf736bf41bb3235a832ec1eb6d8dfaccd4d3555e70f6757b0") + .expect("expected the legacy Evo to be a validator"); + assert_eq!(legacy_evo_validator.node_ip, "192.0.2.40"); + assert_eq!(legacy_evo_validator.platform_p2p_port, 36656); + assert_eq!(legacy_evo_validator.platform_http_port, 1443); + + // Every parsed masternode survives a store and reload, except for the payout list and + // nested addresses, which are not stored. + for masternode in block_platform_state.full_masternode_list().values() { + let bytes = serialize_masternode_entry(masternode, platform_version) + .expect("expected to serialize the masternode entry"); + let mut expected = masternode.clone(); + expected.state.payouts = None; + expected.state.addresses = None; + assert_eq!( + deserialize_masternode_entry(&bytes) + .expect("expected to deserialize the masternode entry"), + expected + ); + } + + let hash160 = |hex: &str| -> Vec { hex::decode(hex).expect("expected hex") }; + + // The keys of an identity, none of them disabled; `None` if there is no identity. + let identity_keys = + |identity_id: Identifier| -> Option)>> { + platform + .drive + .fetch_full_identity( + identity_id.to_buffer(), + Some(&transaction), + platform_version, + ) + .expect("expected to fetch an identity") + .map(|identity| { + identity + .public_keys() + .iter() + .map(|(key_id, key)| { + assert!(key.disabled_at().is_none(), "key {key_id} is disabled"); + (*key_id, (key.purpose(), key.data().to_vec())) + }) + .collect() + }) + }; + + let owner_key = |hex: &str| (Purpose::OWNER, hash160(hex)); + let transfer_key = |hex: &str| (Purpose::TRANSFER, hash160(hex)); + + let voting_421c = "421c03add2c804421451c4e022258778175e60d8"; + let voting_064c = "064cd21ff210c1a92adaea49a4768a41c2aef1bc"; + + // (pro_tx_hash, voting key hash, operator public key, owner identity keys) + let expected = [ + ( + // Legacy Evo whose payout address the diff moves into `payouts` + "ca3d32c4f2ef62eaf736bf41bb3235a832ec1eb6d8dfaccd4d3555e70f6757b0", + voting_421c, + "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563".to_string(), + Some(BTreeMap::from([ + (0, transfer_key("75d57974b6e29a4a70df57a3b11195ce0a0dc817")), + (1, owner_key("1f67d90f35e3c5070c368ae6f3635aac357e47df")), + ])), + ), + ( + // One payout, which the diff replaces with two + "27978dd892b7c876c238be1a6141461c2824f3497dd0058e160979bc8f0a0bef", + voting_421c, + "a792ce1af5f7bb9281053b3934cb8b08d00d075a56498e1a525388ce467f188e8a80911fd96a20982baa9b9678452534".to_string(), + Some(BTreeMap::from([ + (0, transfer_key("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")), + (1, owner_key("b740a2ab3f631e4dfe15debf746364f1d8352c50")), + ])), + ), + ( + // Shared + "a4d26868017c0ccffe2efe50944ef4211834660cca834c6e9f86dec6a88246fa", + voting_421c, + "b1".repeat(48), + None, + ), + ( + // One payout, which the diff replaces with another single payout + "813a7c3f28817988a8e6ce66e07e43e261e78398373bfbaae94c898645111d6b", + voting_421c, + "b2".repeat(48), + Some(BTreeMap::from([ + (0, transfer_key("5555555555555555555555555555555555555555")), + (1, owner_key("0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a")), + ])), + ), + ( + // Tor primary address, two payouts; the owner and voting keys are the same + "3023ffd989974768b0dfc347410ad923fa6d3f1eee90180bd0c435e81cb1a82f", + voting_064c, + "b3".repeat(48), + Some(BTreeMap::from([(1, owner_key(voting_064c))])), + ), + ( + // Evo without addresses, one payout + "6f1757595185032c808321af3e2e8468fae10b8f91e2a657d7a4c7122f4b2706", + voting_421c, + "b4".repeat(48), + Some(BTreeMap::from([ + (0, transfer_key("9999999999999999999999999999999999999999")), + (1, owner_key("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")), + ])), + ), + ( + // Evo with an IPv6 primary address, two payouts + "aecd2830b843e6a84283ba290492a213e355cea7c5026e6118a21e1bfbc36783", + voting_421c, + "b5".repeat(48), + Some(BTreeMap::from([( + 1, + owner_key("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"), + )])), + ), + ]; + + for (pro_tx_hash, voting_key_hash, operator_public_key, owner_identity_keys) in expected { + let pro_tx_hash = ProTxHash::from_str(pro_tx_hash) + .expect("expected a pro tx hash") + .to_byte_array(); + let voting_key_hash: [u8; 20] = hash160(voting_key_hash) + .try_into() + .expect("expected 20 bytes"); + let operator_public_key = hex::decode(operator_public_key).expect("expected hex"); + + assert_eq!( + identity_keys(pro_tx_hash.into()), + owner_identity_keys, + "owner identity of {}", + hex::encode(pro_tx_hash) + ); + assert!( + identity_keys(Identifier::create_voter_identifier( + &pro_tx_hash, + &voting_key_hash + )) + .is_some(), + "voter identity of {}", + hex::encode(pro_tx_hash) + ); + assert!( + identity_keys(Identifier::create_operator_identifier( + &pro_tx_hash, + &operator_public_key + )) + .is_some(), + "operator identity of {}", + hex::encode(pro_tx_hash) + ); + } + } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 9229b17454f..13e7b5a13bf 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -186,18 +186,22 @@ impl Platform { continue; } + // A shared masternode has no owner key to add. Only a Core v24 masternode list has + // one, and no earlier binary can parse it, so no committed block reached this branch. + let Some(owner_address) = masternode.state.owner_address else { + tracing::debug!( + pro_tx_hash = %masternode.pro_tx_hash, + method = "transition_to_version_4", + "no OWNER key added: the masternode has no owner address" + ); + continue; + }; + let last_key_id = *old_owner_identity_keys .keys() .max() .expect("there must be keys, we already checked"); - let owner_address = masternode.state.owner_address.ok_or_else(|| { - Error::Execution(ExecutionError::DashCoreBadResponseError(format!( - "masternode {} has no owner address", - masternode.pro_tx_hash - ))) - })?; - let new_owner_key = Self::get_owner_identity_owner_key( owner_address, last_key_id + 1, @@ -1043,6 +1047,138 @@ mod tests { assert!(result.is_ok()); } + /// Version 4 gives every owner identity the OWNER key of its masternode's owner address. + /// A shared masternode has no owner address, so its identity, if one exists under its + /// ProTx hash, gets no key; the owner identities of other masternodes still get theirs. + #[test] + fn should_add_no_owner_key_for_a_masternode_without_owner_address_on_transition_to_version_4() { + use dpp::dashcore::{ProTxHash, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem, MasternodeType}; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{Identity, KeyID, Purpose}; + use std::collections::BTreeMap; + + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let masternode = + |pro_tx_hash: [u8; 32], owner_address: Option<[u8; 20]>| MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array(pro_tx_hash), + collateral_hash: Txid::from_byte_array([0x41; 32]), + collateral_index: 0, + collateral_address: owner_address.map(|_| [0x42; 20]), + operator_reward: 0.0, + state: DMNState { + service: "1.2.3.4:9999".parse().expect("socket address"), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address, + voting_address: [0x43; 20], + payout_address: owner_address.map(|_| [0x44; 20]), + payouts: None, + pub_key_operator: vec![0x45; 48], + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + }, + }; + + let owner_address = [0x46; 20]; + let with_owner = [0x47; 32]; + let shared_with_identity = [0x48; 32]; + let shared_without_identity = [0x49; 32]; + + let mut platform_state = platform.state.load().as_ref().clone(); + for item in [ + masternode(with_owner, Some(owner_address)), + masternode(shared_with_identity, None), + masternode(shared_without_identity, None), + ] { + platform_state.insert_masternode(item); + } + + // Owner identities as created before version 4: only the TRANSFER key id 0. + for pro_tx_hash in [with_owner, shared_with_identity] { + let mut identity = + Identity::create_basic_identity(pro_tx_hash.into(), platform_version) + .expect("expected an identity"); + identity.add_public_keys([ + Platform::::get_owner_identity_withdrawal_key( + [0x44; 20], + 0, + platform_version, + ) + .expect("expected a withdrawal key"), + ]); + platform + .drive + .add_new_identity( + identity, + true, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add the owner identity"); + } + + let transaction = platform.drive.grove.start_transaction(); + platform + .transition_to_version_4( + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + ) + .expect("expected the transition to version 4 to succeed"); + + let identity_keys = |pro_tx_hash: [u8; 32]| -> BTreeMap)> { + platform + .drive + .fetch_full_identity(pro_tx_hash, Some(&transaction), platform_version) + .expect("expected to fetch an identity") + .expect("expected the identity to exist") + .public_keys() + .iter() + .map(|(key_id, key)| (*key_id, (key.purpose(), key.data().to_vec()))) + .collect() + }; + + assert_eq!( + identity_keys(with_owner), + BTreeMap::from([ + (0, (Purpose::TRANSFER, vec![0x44; 20])), + (1, (Purpose::OWNER, owner_address.to_vec())), + ]) + ); + assert_eq!( + identity_keys(shared_with_identity), + BTreeMap::from([(0, (Purpose::TRANSFER, vec![0x44; 20]))]) + ); + assert_eq!( + platform + .drive + .fetch_full_identity( + shared_without_identity, + Some(&transaction), + platform_version + ) + .expect("expected to fetch an identity"), + None + ); + } + #[test] fn test_transition_to_version_6_inserts_wallet_utils_contract() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs index b18b9833026..2ecd3a87da5 100644 --- a/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs @@ -23,7 +23,8 @@ pub struct MasternodeV0 { pub collateral_hash: Txid, /// The index of the collateral transaction output. pub collateral_index: u32, - /// The address where the collateral is stored. + /// The address where the collateral is stored; zero bytes when it has none, as for a + /// shared masternode. pub collateral_address: [u8; 20], /// The amount of the operator's reward for running the masternode. pub operator_reward: f32, @@ -62,7 +63,7 @@ impl From for MasternodeV0 { pro_tx_hash, collateral_hash, collateral_index, - collateral_address: collateral_address.unwrap_or_default(), + collateral_address: stored_address(collateral_address), operator_reward, state: state.into(), } @@ -86,13 +87,27 @@ impl From for MasternodeListItem { pro_tx_hash, collateral_hash, collateral_index, - collateral_address: Some(collateral_address), + collateral_address: loaded_address(collateral_address), operator_reward, state: state.into(), } } } +/// The stored form of an address a masternode may not have. The stored format has no room for +/// an absent address, so it is stored as zero bytes, and zero bytes read back as absent. That +/// loses no owner address: Core refuses a null owner key for every masternode but a shared one, +/// which has none. A payout or collateral address of zero bytes also reads back as absent; +/// nothing reads those from the stored list. +fn stored_address(address: Option<[u8; 20]>) -> [u8; 20] { + address.unwrap_or_default() +} + +/// The address a [`stored_address`] holds. +fn loaded_address(address: [u8; 20]) -> Option<[u8; 20]> { + (address != [0u8; 20]).then_some(address) +} + /// A `MasternodeState` contains information about a masternode's state. #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub struct MasternodeStateV0 { @@ -112,13 +127,15 @@ pub struct MasternodeStateV0 { /// Reason for the masternode's revocation (encoded as an integer). pub revocation_reason: u32, - /// The masternode owner's public address. + /// The masternode owner's public address; zero bytes when it has none, as a shared + /// masternode. pub owner_address: [u8; 20], /// The masternode voting public address. pub voting_address: [u8; 20], - /// The masternode payout public address. + /// The masternode payout public address; zero bytes when it has none, as a shared + /// masternode or one with a payout list. pub payout_address: [u8; 20], /// The masternode operator's public key. @@ -165,9 +182,9 @@ impl From for MasternodeStateV0 { pose_revived_height, pose_ban_height, revocation_reason, - owner_address: owner_address.unwrap_or_default(), + owner_address: stored_address(owner_address), voting_address, - payout_address: payout_address.unwrap_or_default(), + payout_address: stored_address(payout_address), pub_key_operator, operator_payout_address, platform_node_id, @@ -202,9 +219,9 @@ impl From for DMNState { pose_revived_height, pose_ban_height, revocation_reason, - owner_address: Some(owner_address), + owner_address: loaded_address(owner_address), voting_address, - payout_address: Some(payout_address), + payout_address: loaded_address(payout_address), payouts: None, pub_key_operator, operator_payout_address, @@ -215,3 +232,112 @@ impl From for DMNState { } } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform_state::platform_state_for_saving::v2::{ + deserialize_masternode_entry, serialize_masternode_entry, + }; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{ProTxHash, PubkeyHash, ScriptBuf, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{ + DMNPayout, DMNState, MasternodeAddresses, MasternodeListItem, MasternodeType, + }; + use dpp::version::PlatformVersion; + + fn masternode( + node_type: MasternodeType, + collateral_address: Option<[u8; 20]>, + owner_address: Option<[u8; 20]>, + payout_address: Option<[u8; 20]>, + payouts: Option>, + ) -> MasternodeListItem { + let is_evo = node_type == MasternodeType::Evo; + MasternodeListItem { + node_type, + pro_tx_hash: ProTxHash::from_byte_array([0x31; 32]), + collateral_hash: Txid::from_byte_array([0x32; 32]), + collateral_index: 1, + collateral_address, + operator_reward: 0.0, + state: DMNState { + service: "1.2.3.4:9999".parse().expect("socket address"), + registered_height: 10, + pose_revived_height: None, + pose_ban_height: Some(20), + revocation_reason: 0, + owner_address, + voting_address: [0x33; 20], + payout_address, + payouts, + pub_key_operator: vec![0x34; 48], + operator_payout_address: Some([0x35; 20]), + platform_node_id: is_evo.then_some([0x36; 20]), + #[allow(deprecated)] + legacy_platform_p2p_port: is_evo.then_some(26656), + #[allow(deprecated)] + legacy_platform_http_port: is_evo.then_some(443), + addresses: None, + }, + } + } + + fn p2pkh_payout(key_hash: [u8; 20], reward: u16) -> DMNPayout { + DMNPayout { + address: key_hash, + script: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(key_hash)), + reward, + } + } + + fn stored_and_loaded(masternode: &MasternodeListItem) -> MasternodeListItem { + let bytes = serialize_masternode_entry(masternode, PlatformVersion::latest()) + .expect("expected to serialize the masternode entry"); + deserialize_masternode_entry(&bytes).expect("expected to deserialize the masternode entry") + } + + /// A masternode from a list Core printed before v24 has every address, and reloads as + /// it was stored. + #[test] + fn should_reload_a_masternode_with_every_address_unchanged() { + let masternode = masternode( + MasternodeType::Evo, + Some([0x37; 20]), + Some([0x38; 20]), + Some([0x39; 20]), + None, + ); + + assert_eq!(stored_and_loaded(&masternode), masternode); + } + + /// The stored format has no room for an absent address: a shared masternode's owner, + /// payout and collateral addresses are stored as zero bytes and must reload as absent, + /// not as an address of zeros. The payout list and nested addresses are not stored. + #[test] + fn should_reload_absent_addresses_as_absent() { + let mut shared = masternode(MasternodeType::Regular, None, None, None, None); + shared.state.addresses = Some(MasternodeAddresses { + core_p2p: vec!["1.2.3.4:9999".to_string()], + platform_p2p: vec![], + platform_https: vec![], + }); + let mut expected = shared.clone(); + expected.state.addresses = None; + assert_eq!(stored_and_loaded(&shared), expected); + + let multi_payout = masternode( + MasternodeType::Evo, + Some([0x37; 20]), + Some([0x38; 20]), + None, + Some(vec![ + p2pkh_payout([0x3a; 20], 5000), + p2pkh_payout([0x3b; 20], 5000), + ]), + ); + let mut expected = multi_payout.clone(); + expected.state.payouts = None; + assert_eq!(stored_and_loaded(&multi_payout), expected); + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/1-1250.json b/packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/1-1250.json new file mode 100644 index 00000000000..9c2826584a9 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/1-1250.json @@ -0,0 +1,279 @@ +{ + "baseHeight": 1, + "blockHeight": 1250, + "addedMNs": [ + { + "type": "Evo", + "proTxHash": "ca3d32c4f2ef62eaf736bf41bb3235a832ec1eb6d8dfaccd4d3555e70f6757b0", + "collateralHash": "5c1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8", + "collateralIndex": 0, + "collateralAddress": "yh6REqVqc88VTnJ5rCXPYnQNtrc72YmiXB", + "operatorReward": 0, + "state": { + "version": 2, + "service": "192.0.2.40:9999", + "addresses": { + "core_p2p": [ + "192.0.2.40:9999" + ], + "platform_https": [ + "192.0.2.40:443" + ], + "platform_p2p": [ + "192.0.2.40:26656" + ] + }, + "registeredHeight": 900, + "lastPaidHeight": 1190, + "consecutivePayments": 0, + "PoSePenalty": 0, + "PoSeRevivedHeight": -1, + "PoSeBanHeight": -1, + "revocationReason": 0, + "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", + "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", + "platformNodeID": "9e391c2c041a122a779bafe09d6c47ea600dfcbe", + "platformP2PPort": 26656, + "platformHTTPPort": 443, + "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", + "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563" + } + }, + { + "type": "Regular", + "proTxHash": "27978dd892b7c876c238be1a6141461c2824f3497dd0058e160979bc8f0a0bef", + "collateralHash": "6d2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809", + "collateralIndex": 1, + "collateralAddress": "yguoUwpMVtQCgq49SzH8r5QUuUmYJd89fR", + "operatorReward": 0, + "state": { + "version": 3, + "service": "192.0.2.41:9999", + "addresses": { + "core_p2p": [ + "192.0.2.41:9999" + ] + }, + "registeredHeight": 1100, + "lastPaidHeight": 1180, + "consecutivePayments": 0, + "PoSePenalty": 0, + "PoSeRevivedHeight": -1, + "PoSeBanHeight": -1, + "revocationReason": 0, + "ownerAddress": "yd2PwFoqtEJdnJVSEzBDMxVnFVgEvJyvyY", + "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", + "payouts": [ + { + "address": "yi6ouEk1CRe5j27k3rsnQftoqRiD29vExs", + "script": "76a914eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee88ac", + "reward": 10000 + } + ], + "pubKeyOperator": "a792ce1af5f7bb9281053b3934cb8b08d00d075a56498e1a525388ce467f188e8a80911fd96a20982baa9b9678452534" + } + }, + { + "type": "Regular", + "proTxHash": "a4d26868017c0ccffe2efe50944ef4211834660cca834c6e9f86dec6a88246fa", + "collateralHash": "a4d26868017c0ccffe2efe50944ef4211834660cca834c6e9f86dec6a88246fa", + "collateralIndex": 1, + "operatorReward": 0, + "state": { + "version": 3, + "service": "192.0.2.30:9999", + "addresses": { + "core_p2p": [ + "192.0.2.30:9999" + ] + }, + "registeredHeight": 1210, + "lastPaidHeight": 0, + "consecutivePayments": 0, + "PoSePenalty": 0, + "PoSeRevivedHeight": -1, + "PoSeBanHeight": -1, + "revocationReason": 0, + "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", + "shares": [ + { + "amount": 60000000000, + "refundAddress": "yMsgnH1xKGa85n4bq2imrZbG2KgrmGttAV", + "refundScript": "76a914111111111111111111111111111111111111111188ac", + "rewardAddress": "yPRviMJaEHGafsc5EKovpa5Nw2Jewo9mdj", + "rewardScript": "76a914222222222222222222222222222222222222222288ac", + "ownerAddress": "yQzAeRbC9Hy3Fy9Ydcu5naZVqivT95Ka2R" + }, + { + "amount": 40000000000, + "refundAddress": "ySYQaVsp4JfVr4h22uzEkb3ckRYFHJwGq9", + "refundScript": "76a914444444444444444444444444444444444444444488ac", + "rewardAddress": "ySYQaVsp4JfVr4h22uzEkb3ckRYFHJwGq9", + "rewardScript": "76a914444444444444444444444444444444444444444488ac", + "ownerAddress": "yVetSeT3tL4R2FmxqWAYgc1rZpmqbMcFhL" + } + ], + "earlyPeriodBlocks": 1000, + "earlyPenalty": 100000000, + "pubKeyOperator": "b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1" + } + }, + { + "type": "Regular", + "proTxHash": "813a7c3f28817988a8e6ce66e07e43e261e78398373bfbaae94c898645111d6b", + "collateralHash": "ff6226e6c97bfcf40b6d04e12e3f75678024988823bfba28cde2a9ac11b1a765", + "collateralIndex": 1, + "collateralAddress": "yNqYnF9sHURjwRmhZMLFGQ3WjC5DZNJMUi", + "operatorReward": 0, + "state": { + "version": 3, + "service": "192.0.2.31:9999", + "addresses": { + "core_p2p": [ + "192.0.2.31:9999" + ] + }, + "registeredHeight": 1220, + "lastPaidHeight": 0, + "consecutivePayments": 0, + "PoSePenalty": 0, + "PoSeRevivedHeight": -1, + "PoSeBanHeight": -1, + "revocationReason": 0, + "ownerAddress": "yMEXdf8kwSX7NSipRnrtQ66d4WFtBCi71e", + "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", + "payouts": [ + { + "address": "yU6eWaARyKMxSAEVSD5PibXjf8A3TH4gqJ", + "script": "76a914555555555555555555555555555555555555555588ac", + "reward": 10000 + } + ], + "pubKeyOperator": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2" + } + }, + { + "type": "Regular", + "proTxHash": "3023ffd989974768b0dfc347410ad923fa6d3f1eee90180bd0c435e81cb1a82f", + "collateralHash": "35215134107b5e423d327cab12d2b4c60a9b769301096e05a95916676d2f7867", + "collateralIndex": 0, + "collateralAddress": "yd2PwFoqtEJdnJVSEzBDMxVnFVgEvJyvyY", + "operatorReward": 0, + "state": { + "version": 3, + "service": "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:9999", + "addresses": { + "core_p2p": [ + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:9999", + "192.0.2.32:9999" + ] + }, + "registeredHeight": 1230, + "lastPaidHeight": 0, + "consecutivePayments": 0, + "PoSePenalty": 0, + "PoSeRevivedHeight": -1, + "PoSeBanHeight": -1, + "revocationReason": 0, + "ownerAddress": "yLtkvxSueGSufQZQq8L9GVHch9QRqJqGkZ", + "votingAddress": "yLtkvxSueGSufQZQq8L9GVHch9QRqJqGkZ", + "payouts": [ + { + "address": "yYmNJo2HiMTLCSrue6Lrccz6PE1S1ELq2T", + "script": "76a914888888888888888888888888888888888888888888ac", + "reward": 7000 + }, + { + "address": "8qK9EafotWgreuSxH2x8ySZnKVDVivy3Zf", + "script": "a914777777777777777777777777777777777777777787", + "reward": 3000 + } + ], + "pubKeyOperator": "b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3" + } + }, + { + "type": "Evo", + "proTxHash": "6f1757595185032c808321af3e2e8468fae10b8f91e2a657d7a4c7122f4b2706", + "collateralHash": "cbf3c744b1c18fe1866e79972818c98fb1a268736f1757595185032c808321af", + "collateralIndex": 0, + "collateralAddress": "ybhjexnMcGckdJCyUwFu3F25zPo4mqQg1k", + "operatorReward": 0, + "state": { + "version": 3, + "service": "[::]:0", + "addresses": {}, + "registeredHeight": 1240, + "lastPaidHeight": 0, + "consecutivePayments": 0, + "PoSePenalty": 0, + "PoSeRevivedHeight": -1, + "PoSeBanHeight": -1, + "revocationReason": 0, + "ownerAddress": "yMKqWbyVzZPFkvLndPz1kSba4CBAbA9oZg", + "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", + "platformNodeID": "cbf3c744b1c18fe1866e79972818c98fb1a26873", + "platformP2PPort": -1, + "platformHTTPPort": -1, + "payouts": [ + { + "address": "yaKcEsJudN9nnYQP3PS1adUDHvdE6sjNiQ", + "script": "76a914999999999999999999999999999999999999999988ac", + "reward": 10000 + } + ], + "pubKeyOperator": "b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4" + } + }, + { + "type": "Evo", + "proTxHash": "aecd2830b843e6a84283ba290492a213e355cea7c5026e6118a21e1bfbc36783", + "collateralHash": "59cd030a1a4cd43a53c3a600c20f64ed07399873aecd2830b843e6a84283ba29", + "collateralIndex": 0, + "collateralAddress": "yNqYnF9sHURjwRmhZMLFGQ3WjC5DZNJMUi", + "operatorReward": 0, + "state": { + "version": 3, + "service": "[2001:db8::4]:9999", + "addresses": { + "core_p2p": [ + "[2001:db8::4]:9999" + ], + "platform_p2p": [ + "[2001:db8::4]:26656" + ], + "platform_https": [ + "[2001:db8::4]:443" + ] + }, + "registeredHeight": 1250, + "lastPaidHeight": 0, + "consecutivePayments": 0, + "PoSePenalty": 0, + "PoSeRevivedHeight": -1, + "PoSeBanHeight": -1, + "revocationReason": 0, + "ownerAddress": "yMR9PYpF3gFQ9Pxkq1796o6X3t6SxnHXrq", + "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", + "platformNodeID": "4bcc85253e395ec272998a0722ac3ee1dd3965f7", + "platformP2PPort": 26656, + "platformHTTPPort": 443, + "payouts": [ + { + "address": "ybsrAwbXYNrFNdwrSgXAYdxLCdF2GdFbqY", + "script": "76a914aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa88ac", + "reward": 5000 + }, + { + "address": "ydS671t9TPYhxjVKqycKWeST7KrpUM1c4t", + "script": "76a914bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb88ac", + "reward": 5000 + } + ], + "pubKeyOperator": "b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5" + } + } + ], + "removedMNs": [], + "updatedMNs": [] +} diff --git a/packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/1250-1260.json b/packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/1250-1260.json new file mode 100644 index 00000000000..e935e1508df --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/1250-1260.json @@ -0,0 +1,61 @@ +{ + "baseHeight": 1250, + "blockHeight": 1260, + "addedMNs": [], + "removedMNs": [], + "updatedMNs": [ + { + "ca3d32c4f2ef62eaf736bf41bb3235a832ec1eb6d8dfaccd4d3555e70f6757b0": { + "version": 3, + "service": "192.0.2.40:9999", + "payouts": [ + { + "address": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", + "script": "76a91475d57974b6e29a4a70df57a3b11195ce0a0dc81788ac", + "reward": 10000 + } + ], + "platformP2PPort": 36656, + "platformHTTPPort": 1443, + "addresses": { + "core_p2p": [ + "192.0.2.40:9999" + ], + "platform_p2p": [ + "192.0.2.40:36656" + ], + "platform_https": [ + "192.0.2.40:1443" + ] + } + } + }, + { + "27978dd892b7c876c238be1a6141461c2824f3497dd0058e160979bc8f0a0bef": { + "payouts": [ + { + "address": "yezL36AmNQFAYq2oFGhUUeva22UccZnF8F", + "script": "76a914cccccccccccccccccccccccccccccccccccccccc88ac", + "reward": 2500 + }, + { + "address": "ygYZyATPHQwd8vaGeZndSfQgvj6Qro5WbT", + "script": "76a914dddddddddddddddddddddddddddddddddddddddd88ac", + "reward": 7500 + } + ] + } + }, + { + "813a7c3f28817988a8e6ce66e07e43e261e78398373bfbaae94c898645111d6b": { + "payouts": [ + { + "address": "yiNjY5GEMnEWtSyeegFATjPepVU48ATT7E", + "script": "76a914f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f188ac", + "reward": 10000 + } + ] + } + } + ] +} From 820bf2c0d25a61874f3d0266b71c0757949335c1 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 1 Oct 2026 22:56:09 +0700 Subject: [PATCH 04/11] build: bump rust-dashcore for bracketed IPv6 legacy platform hosts Moves the rust-dashcore pin to 40268cc0 (dashpay/rust-dashcore#1093), whose platform address accessors bracket an IPv6 node IP in the legacy port fallback, as nested entries already are. Drive takes only the port from these accessors, so nothing it decides changes. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 16 ++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb0cbc7553f..abe92a65ca5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1654,7 +1654,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "cbindgen 0.29.4", "grovedb-bincode", @@ -1665,7 +1665,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "dash-network", ] @@ -1760,7 +1760,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "async-trait", "chrono", @@ -1789,7 +1789,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "anyhow", "base64-compat", @@ -1815,12 +1815,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "dashcore-rpc-json", "hex", @@ -1833,7 +1833,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "dashcore", "grovedb-bincode", @@ -1848,7 +1848,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "dashcore-private", "grovedb-bincode", @@ -2922,7 +2922,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" [[package]] name = "glob" @@ -4154,7 +4154,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "aes", "async-trait", @@ -4183,7 +4183,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4199,7 +4199,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=538522bc6789defed276a358d7504bdd1ebb7656#538522bc6789defed276a358d7504bdd1ebb7656" +source = "git+https://github.com/dashpay/rust-dashcore?rev=40268cc0402a8933ec539f16b2d634c4e25876ad#40268cc0402a8933ec539f16b2d634c4e25876ad" dependencies = [ "async-trait", "dashcore", diff --git a/Cargo.toml b/Cargo.toml index 0d73bad6547..b3c14730fac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,14 +65,14 @@ grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "dce8252f8 grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "dce8252f8665bf06c7bd788e739efba354141690" } grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", rev = "dce8252f8665bf06c7bd788e739efba354141690" } grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "dce8252f8665bf06c7bd788e739efba354141690" } -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "538522bc6789defed276a358d7504bdd1ebb7656" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "40268cc0402a8933ec539f16b2d634c4e25876ad" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "40268cc0402a8933ec539f16b2d634c4e25876ad" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "40268cc0402a8933ec539f16b2d634c4e25876ad" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "40268cc0402a8933ec539f16b2d634c4e25876ad" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "40268cc0402a8933ec539f16b2d634c4e25876ad" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "40268cc0402a8933ec539f16b2d634c4e25876ad" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "40268cc0402a8933ec539f16b2d634c4e25876ad" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "40268cc0402a8933ec539f16b2d634c4e25876ad" } tokio-metrics = "0.5" # Size-tuned profile for the iOS `rs-unified-sdk-ffi` staticlib, which From 7c67e011ef88b38211137a7d5afec7fe5c780c7b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 1 Oct 2026 22:56:10 +0700 Subject: [PATCH 05/11] test(drive-abci): pin that a stored address of zero bytes reads back as absent The stored masternode list writes an absent owner, payout or collateral address as zero bytes and reads zero bytes back as absent, so a present address of zero bytes does not round-trip. Core never prints one (it refuses a null owner key for every masternode but a shared one, and nothing reads a stored payout or collateral address), so this is the intended collision; pin it with a test instead of leaving it to prose, and say on the collateral_address accessor that zero bytes mean absent. No behaviour change; the test passes before and after. Co-Authored-By: Claude Opus 5.5 --- .../platform_types/masternode/v0/accessors.rs | 3 ++- .../src/platform_types/masternode/v0/mod.rs | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/rs-drive-abci/src/platform_types/masternode/v0/accessors.rs b/packages/rs-drive-abci/src/platform_types/masternode/v0/accessors.rs index 6406323f28c..c48a8957993 100644 --- a/packages/rs-drive-abci/src/platform_types/masternode/v0/accessors.rs +++ b/packages/rs-drive-abci/src/platform_types/masternode/v0/accessors.rs @@ -11,7 +11,8 @@ pub trait MasternodeAccessorsV0 { fn collateral_hash(&self) -> Txid; /// The index of the collateral transaction output. fn collateral_index(&self) -> u32; - /// The address where the collateral is stored. + /// The address where the collateral is stored; zero bytes when it has none, as for a + /// shared masternode. fn collateral_address(&self) -> [u8; 20]; /// The amount of the operator's reward for running the masternode. fn operator_reward(&self) -> f32; diff --git a/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs index 2ecd3a87da5..c67b646feb8 100644 --- a/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs @@ -340,4 +340,22 @@ mod tests { expected.state.payouts = None; assert_eq!(stored_and_loaded(&multi_payout), expected); } + + /// Zero bytes stand for an absent address in the stored list, so an address of zero bytes + /// reads back as absent. Core never prints one: it refuses a null owner key for every + /// masternode but a shared one, and nothing reads a stored payout or collateral address. + #[test] + fn should_reload_an_address_of_zero_bytes_as_absent() { + let zero = masternode( + MasternodeType::Regular, + Some([0u8; 20]), + Some([0u8; 20]), + Some([0u8; 20]), + None, + ); + let reloaded = stored_and_loaded(&zero); + assert_eq!(reloaded.collateral_address, None); + assert_eq!(reloaded.state.owner_address, None); + assert_eq!(reloaded.state.payout_address, None); + } } From 8b1c9c80a7ddd6fbfa119ec2dc44aea6c88beb6c Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 3 Oct 2026 16:04:09 +0700 Subject: [PATCH 06/11] test(dashmate): drop the core template test The rendered-dash.conf test for `deprecatedrpc=service` is not needed; the template change stays. Co-Authored-By: Claude Opus 5.5 --- .../test/unit/templates/coreTemplate.spec.js | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 packages/dashmate/test/unit/templates/coreTemplate.spec.js diff --git a/packages/dashmate/test/unit/templates/coreTemplate.spec.js b/packages/dashmate/test/unit/templates/coreTemplate.spec.js deleted file mode 100644 index 559505ccbc0..00000000000 --- a/packages/dashmate/test/unit/templates/coreTemplate.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import { expect } from 'chai'; -import HomeDir from '../../../src/config/HomeDir.js'; -import getBaseConfigFactory from '../../../configs/defaults/getBaseConfigFactory.js'; -import renderTemplateFactory from '../../../src/templates/renderTemplateFactory.js'; -import renderServiceTemplatesFactory from '../../../src/templates/renderServiceTemplatesFactory.js'; - -describe('Core template', () => { - let config; - let renderServiceTemplates; - - beforeEach(() => { - config = getBaseConfigFactory(HomeDir.createTemp())(); - renderServiceTemplates = renderServiceTemplatesFactory(renderTemplateFactory()); - }); - - // Drive and DAPI read `service`, `platformP2PPort` and `platformHTTPPort` from - // Core's masternode list RPCs. Core v24 leaves them out unless the deprecated - // fields are requested, and Platform cannot process a masternode list without - // them. - ['mainnet', 'testnet', 'devnet', 'local'].forEach((network) => { - it(`should keep Core's deprecated masternode service fields in RPC output on ${network}`, () => { - config.set('network', network); - - const dashConf = renderServiceTemplates(config)['core/dash.conf']; - - // Options under a [section] header apply to that chain only, so the - // option has to be in the default section to cover every network. - const [defaultSection] = dashConf.split(/^\[/m); - - expect(defaultSection).to.match(/^deprecatedrpc=service$/m); - }); - }); -}); From d55d9e4e9d28fbe744b36822cc99027896101e9a Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 3 Oct 2026 16:09:56 +0700 Subject: [PATCH 07/11] refactor(drive-abci): keep shipped masternode identity generations unchanged Shipped generations may change in place only where the new code cannot run under the protocol versions that select them. A node at protocol version 13 running against Dash Core v24 does receive shared and extended-address masternodes, so the branches the previous commit added to them were reachable and would have created state where every released binary stops. Restore create_owner_identity v0 (protocol versions 1-3) and v1 (4-13), update_masternode_identities v0 and transition_to_version_4 to the mechanical adaptation of the rust-dashcore bump: a masternode without the payout or owner address they read fails with DashCoreBadResponseError, the same halt, without state, as a release that cannot parse the list. The three create_owner_identity and update_masternode_identities files are byte-identical to that commit again; transition_to_version_4 differs only in its test. The stored masternode conversion (absent address <-> zero bytes) stays: it is aux storage, not consensus state. Tests now pin the shipped behaviour: - create_owner_identity at protocol versions 3 and 13: shared, single payout list, several payouts (and, at 13, no owner address) fail with DashCoreBadResponseError; a legacy masternode gets its identity. - The Core v24 listdiff fixture at protocol version 13 fails with DashCoreBadResponseError ("... has no payout address"). - transition_to_version_4 gives a masternode with an owner address its OWNER key after its TRANSFER key, skips a masternode without an owner identity, and fails on one with an owner identity but no owner address. - The payout-list-diff test moves to the update_masternode_identities dispatcher and runs at protocol version 13 and the latest version. The tests for the new owner identities move to the new generations in the next commit. Red to green: the three new error tests fail on the parent commit ("expected DashCoreBadResponseError, got Ok(None)" / "got Ok(())") and pass here. Co-Authored-By: Claude Opus 5.5 --- .../create_owner_identity/mod.rs | 274 ++++------------- .../create_owner_identity/v0/mod.rs | 39 +-- .../create_owner_identity/v1/mod.rs | 55 ++-- .../update_masternode_identities/mod.rs | 213 +++++++++++++ .../update_masternode_identities/v0/mod.rs | 206 +------------ .../update_masternode_list/mod.rs | 282 +++--------------- .../v0/mod.rs | 107 ++++--- 7 files changed, 401 insertions(+), 775 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs index 6b715c9e469..d520d7f43f4 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs @@ -5,7 +5,7 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; -use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem}; +use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeListItem; use dpp::identity::Identity; use dpp::version::PlatformVersion; @@ -25,13 +25,12 @@ where /// /// # Returns /// - /// * Result, Error> - The owner identity, or `None` when the masternode - /// has no key an owner identity of this version holds: a shared masternode has no owner - /// and no payout address. Otherwise, returns an error. + /// * Result - Returns the constructed identity for the owner if successful. + /// Otherwise, returns an error. pub(crate) fn create_owner_identity( masternode: &MasternodeListItem, platform_version: &PlatformVersion, - ) -> Result, Error> { + ) -> Result { match platform_version .drive_abci .methods @@ -50,49 +49,22 @@ where } } -/// The one address the owner reward of a masternode is paid to, which its owner identity's -/// TRANSFER key holds: the payout address, or the only entry of a payout list with one entry -/// (Core turns a payout address into such a list when a masternode moves to extended -/// addresses). `None` for a shared masternode, which has neither, and for a reward split -/// between several payouts. -/// -/// The stored masternode list does not keep payout lists, so this is only meaningful for a -/// masternode as Core reported it, never for one read from the stored list. -/// -/// Shared by the shipped `create_owner_identity` v0 and v1: changing it changes both. -fn effective_payout_address(state: &DMNState) -> Option<[u8; 20]> { - match (state.payout_address, state.payouts.as_deref()) { - (Some(payout_address), _) => Some(payout_address), - (None, Some([payout])) => Some(payout.address), - _ => None, - } -} - #[cfg(test)] mod tests { + use crate::error::execution::ExecutionError; + use crate::error::Error; + use crate::platform_types::platform::Platform; use crate::rpc::core::MockCoreRPCLike; - use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; - use dpp::block::block_info::BlockInfo; use dpp::dashcore::hashes::Hash; use dpp::dashcore::{ProTxHash, PubkeyHash, ScriptBuf, Txid}; use dpp::dashcore_rpc::dashcore_rpc_json::{ - DMNPayout, DMNState, MasternodeListDiff, MasternodeListItem, MasternodeType, + DMNPayout, DMNState, MasternodeListItem, MasternodeType, }; - use dpp::identifier::MasternodeIdentifiers; - use dpp::identity::accessors::IdentityGettersV0; - use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; - use dpp::identity::{Identity, KeyID, Purpose}; - use dpp::prelude::Identifier; - use dpp::serialization::PlatformSerializable; use dpp::version::PlatformVersion; - use std::collections::BTreeMap; - const PRO_TX_HASH: [u8; 32] = [0x21; 32]; const OWNER_ADDRESS: [u8; 20] = [0x22; 20]; - const VOTING_ADDRESS: [u8; 20] = [0x23; 20]; const PAYOUT_ADDRESS: [u8; 20] = [0x24; 20]; const SECOND_PAYOUT_ADDRESS: [u8; 20] = [0x25; 20]; - const PUB_KEY_OPERATOR: [u8; 48] = [0x26; 48]; /// A Regular masternode as Core lists it: a shared one has neither owner, payout nor /// collateral address; an extended-address one has `payouts` instead of `payout_address`. @@ -103,7 +75,7 @@ mod tests { ) -> MasternodeListItem { MasternodeListItem { node_type: MasternodeType::Regular, - pro_tx_hash: ProTxHash::from_byte_array(PRO_TX_HASH), + pro_tx_hash: ProTxHash::from_byte_array([0x21; 32]), collateral_hash: Txid::from_byte_array([0x27; 32]), collateral_index: 0, collateral_address: owner_address.map(|_| [0x28; 20]), @@ -115,10 +87,10 @@ mod tests { pose_ban_height: None, revocation_reason: 0, owner_address, - voting_address: VOTING_ADDRESS, + voting_address: [0x23; 20], payout_address, payouts, - pub_key_operator: PUB_KEY_OPERATOR.to_vec(), + pub_key_operator: vec![0x26; 48], operator_payout_address: None, platform_node_id: None, #[allow(deprecated)] @@ -138,193 +110,63 @@ mod tests { } } - fn shared_masternode() -> MasternodeListItem { - masternode(None, None, None) - } - - fn legacy_masternode() -> MasternodeListItem { - masternode(Some(OWNER_ADDRESS), Some(PAYOUT_ADDRESS), None) - } - - fn single_payout_masternode() -> MasternodeListItem { - masternode( + /// Generation 0 (protocol versions 1 to 3) builds the owner identity from the payout + /// address, and generation 1 (4 to 13) from the payout and the owner address. A masternode + /// Core lists without one of them fails with `DashCoreBadResponseError`, so its block + /// fails instead of the masternode getting an owner identity these generations do not + /// define: a shared masternode has neither address, and an extended-address masternode + /// has a payout list instead of a payout address. + #[test] + fn should_fail_for_a_masternode_without_a_payout_or_owner_address_up_to_protocol_version_13() { + let legacy = masternode(Some(OWNER_ADDRESS), Some(PAYOUT_ADDRESS), None); + let shared = masternode(None, None, None); + let single_payout = masternode( Some(OWNER_ADDRESS), None, Some(vec![p2pkh_payout(PAYOUT_ADDRESS, 10000)]), - ) - } - - fn multi_payout_masternode() -> MasternodeListItem { - masternode( + ); + let several_payouts = masternode( Some(OWNER_ADDRESS), None, Some(vec![ p2pkh_payout(PAYOUT_ADDRESS, 7000), p2pkh_payout(SECOND_PAYOUT_ADDRESS, 3000), ]), - ) - } - - /// A platform on `platform_version` whose masternode identities were created for - /// `masternode`, added on the first block. - fn platform_with_added_masternode( - masternode: MasternodeListItem, - platform_version: &PlatformVersion, - ) -> TempPlatform { - let platform = TestPlatformBuilder::new() - .with_initial_protocol_version(platform_version.protocol_version) - .build_with_mock_rpc() - .set_genesis_state(); - - let transaction = platform.drive.grove.start_transaction(); - platform - .update_masternode_identities( - MasternodeListDiff { - base_height: 0, - block_height: 1, - added_mns: vec![masternode], - removed_mns: vec![], - updated_mns: vec![], - }, - &BTreeMap::new(), - &BlockInfo::default(), - None, - &transaction, - platform_version, - ) - .expect("expected to create the masternode identities"); - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit"); - platform - } - - fn fetch_identity( - platform: &TempPlatform, - identity_id: Identifier, - platform_version: &PlatformVersion, - ) -> Option { - platform - .drive - .fetch_full_identity(identity_id.to_buffer(), None, platform_version) - .expect("expected to fetch an identity") - } - - fn owner_identity( - platform: &TempPlatform, - platform_version: &PlatformVersion, - ) -> Option { - fetch_identity(platform, PRO_TX_HASH.into(), platform_version) - } - - fn assert_voter_and_operator_identities_exist( - platform: &TempPlatform, - platform_version: &PlatformVersion, - ) { - let voter_identifier = Identifier::create_voter_identifier(&PRO_TX_HASH, &VOTING_ADDRESS); - let operator_identifier = - Identifier::create_operator_identifier(&PRO_TX_HASH, &PUB_KEY_OPERATOR); - assert!( - fetch_identity(platform, voter_identifier, platform_version).is_some(), - "the voter identity must be created" - ); - assert!( - fetch_identity(platform, operator_identifier, platform_version).is_some(), - "the operator identity must be created" ); - } - - /// Both generations: the owner identity carries the owner's keys, and a shared - /// masternode has no owner key (its owners are its share holders), so it gets no owner - /// identity. Its voter and operator identities are created as for any masternode. - #[test] - fn should_create_no_owner_identity_for_a_shared_masternode() { - for platform_version in [PlatformVersion::first(), PlatformVersion::latest()] { - let platform = platform_with_added_masternode(shared_masternode(), platform_version); - - assert_eq!( - owner_identity(&platform, platform_version), - None, - "protocol version {}", - platform_version.protocol_version - ); - assert_voter_and_operator_identities_exist(&platform, platform_version); + let without_owner_address = masternode(None, Some(PAYOUT_ADDRESS), None); + + for protocol_version in [3, 13] { + let platform_version = + PlatformVersion::get(protocol_version).expect("expected a known protocol version"); + + Platform::::create_owner_identity(&legacy, platform_version) + .expect("a masternode with an owner and a payout address has an owner identity"); + + let mut cases = vec![ + (&shared, "has no payout address"), + (&single_payout, "has no payout address"), + (&several_payouts, "has no payout address"), + ]; + if protocol_version == 13 { + cases.push((&without_owner_address, "has no owner address")); + } + for (masternode, expected_message) in cases { + match Platform::::create_owner_identity( + masternode, + platform_version, + ) { + Err(Error::Execution(ExecutionError::DashCoreBadResponseError(message))) => { + assert!( + message.ends_with(expected_message), + "protocol version {protocol_version}: unexpected message {message}" + ) + } + other => panic!( + "protocol version {protocol_version}: expected DashCoreBadResponseError \ + for {masternode:?}, got {other:?}" + ), + } + } } } - - /// Both generations: Core turns a payout address into a payout list with one entry - /// when a masternode moves to extended addresses, so a single payout must give the - /// owner identity a payout address gives, byte for byte. - #[test] - fn should_create_the_same_owner_identity_for_a_single_payout_as_for_a_payout_address() { - for platform_version in [PlatformVersion::first(), PlatformVersion::latest()] { - let legacy_owner_identity = owner_identity( - &platform_with_added_masternode(legacy_masternode(), platform_version), - platform_version, - ) - .expect("expected an owner identity for a payout address"); - let single_payout_owner_identity = owner_identity( - &platform_with_added_masternode(single_payout_masternode(), platform_version), - platform_version, - ) - .expect("expected an owner identity for a single payout"); - - assert_eq!( - single_payout_owner_identity - .serialize_to_bytes() - .expect("expected to serialize"), - legacy_owner_identity - .serialize_to_bytes() - .expect("expected to serialize"), - "protocol version {}", - platform_version.protocol_version - ); - } - } - - /// Several payouts have no single address for the TRANSFER key id 0, so the owner - /// identity gets only the OWNER key id 1 a payout address's owner identity has, and key - /// id 0 stays free. - #[test] - fn should_create_an_owner_identity_with_only_the_owner_key_for_several_payouts() { - let platform_version = PlatformVersion::latest(); - let multi_payout_owner_identity = owner_identity( - &platform_with_added_masternode(multi_payout_masternode(), platform_version), - platform_version, - ) - .expect("expected an owner identity for several payouts"); - let legacy_owner_identity = owner_identity( - &platform_with_added_masternode(legacy_masternode(), platform_version), - platform_version, - ) - .expect("expected an owner identity for a payout address"); - - let owner_key = legacy_owner_identity - .public_keys() - .get(&1) - .expect("expected the OWNER key id 1") - .clone(); - assert_eq!(owner_key.purpose(), Purpose::OWNER); - assert_eq!(owner_key.data().as_slice(), OWNER_ADDRESS.as_slice()); - assert_eq!( - multi_payout_owner_identity.public_keys(), - &BTreeMap::from([(1 as KeyID, owner_key)]) - ); - } - - /// The first generation's owner identity holds only the TRANSFER key of the payout - /// address, so several payouts leave it with no key: no owner identity is created, as - /// for a shared masternode. - #[test] - fn should_create_no_owner_identity_for_several_payouts_in_protocol_versions_without_owner_keys() - { - let platform_version = PlatformVersion::first(); - let platform = platform_with_added_masternode(multi_payout_masternode(), platform_version); - - assert_eq!(owner_identity(&platform, platform_version), None); - assert_voter_and_operator_identities_exist(&platform, platform_version); - } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs index dcb7634b183..7bdd4753d5c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs @@ -1,4 +1,4 @@ -use super::effective_payout_address; +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -15,21 +15,13 @@ where pub(super) fn create_owner_identity_v0( masternode: &MasternodeListItem, platform_version: &PlatformVersion, - ) -> Result, Error> { - // The identity holds only the payout address's TRANSFER key, so a masternode without a - // single payout address (shared, or paid to several payouts) gets none. Only a Core v24 - // masternode list has such a masternode, and no earlier binary can parse one, so no - // committed block reached this branch. A chain on Core v24 with V24 active must start at - // protocol version 4 or later: an Evo paid to several payouts gets no owner identity - // here, so paying it as a block proposer would fail. - let Some(payout_address) = effective_payout_address(&masternode.state) else { - tracing::debug!( - pro_tx_hash = %masternode.pro_tx_hash, - method = "create_owner_identity_v0", - "no owner identity: the masternode has no single payout address" - ); - return Ok(None); - }; + ) -> Result { + let payout_address = masternode.state.payout_address.ok_or_else(|| { + Error::Execution(ExecutionError::DashCoreBadResponseError(format!( + "masternode {} has no payout address", + masternode.pro_tx_hash + ))) + })?; let owner_identifier = Self::get_owner_identifier(masternode)?; let mut identity = Identity::create_basic_identity(owner_identifier, platform_version)?; identity.add_public_keys([Self::get_owner_identity_withdrawal_key( @@ -37,7 +29,7 @@ where 0, platform_version, )?]); - Ok(Some(identity)) + Ok(identity) } pub(crate) fn get_owner_identifier( @@ -113,8 +105,7 @@ mod tests { let mn = make_masternode(pro_tx, [0x12u8; 20]); let identity = Platform::::create_owner_identity_v0(&mn, platform_version) - .expect("create_owner_identity_v0 must succeed") - .expect("a masternode with a payout address has an owner identity"); + .expect("create_owner_identity_v0 must succeed"); assert_eq!(identity.id(), Identifier::from(pro_tx)); assert_eq!( @@ -131,12 +122,10 @@ mod tests { let a = make_masternode([0x01u8; 32], [0u8; 20]); let b = make_masternode([0x02u8; 32], [0u8; 20]); - let ia = Platform::::create_owner_identity_v0(&a, platform_version) - .expect("a") - .expect("a has an owner identity"); - let ib = Platform::::create_owner_identity_v0(&b, platform_version) - .expect("b") - .expect("b has an owner identity"); + let ia = + Platform::::create_owner_identity_v0(&a, platform_version).expect("a"); + let ib = + Platform::::create_owner_identity_v0(&b, platform_version).expect("b"); assert_ne!(ia.id(), ib.id()); } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs index 0eb2ddadece..597f4a287e3 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v1/mod.rs @@ -1,4 +1,4 @@ -use super::effective_payout_address; +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -14,42 +14,25 @@ where pub(super) fn create_owner_identity_v1( masternode: &MasternodeListItem, platform_version: &PlatformVersion, - ) -> Result, Error> { - // Only a Core v24 masternode list has a masternode without an owner address or without - // a single payout address, and no earlier binary can parse one, so no committed block - // reached the branches below that skip a key. - // - // A shared masternode has no owner key: its owners are its share holders. - let Some(owner_address) = masternode.state.owner_address else { - tracing::debug!( - pro_tx_hash = %masternode.pro_tx_hash, - method = "create_owner_identity_v1", - "no owner identity: the masternode has no owner address" - ); - return Ok(None); - }; + ) -> Result { + let payout_address = masternode.state.payout_address.ok_or_else(|| { + Error::Execution(ExecutionError::DashCoreBadResponseError(format!( + "masternode {} has no payout address", + masternode.pro_tx_hash + ))) + })?; + let owner_address = masternode.state.owner_address.ok_or_else(|| { + Error::Execution(ExecutionError::DashCoreBadResponseError(format!( + "masternode {} has no owner address", + masternode.pro_tx_hash + ))) + })?; let owner_identifier = Self::get_owner_identifier(masternode)?; let mut identity = Identity::create_basic_identity(owner_identifier, platform_version)?; - // Several payouts have no single address for the TRANSFER key, so the identity holds - // only the OWNER key, under the same id as next to a TRANSFER key. - if let Some(payout_address) = effective_payout_address(&masternode.state) { - identity.add_public_keys([Self::get_owner_identity_withdrawal_key( - payout_address, - 0, - platform_version, - )?]); - } else { - tracing::debug!( - pro_tx_hash = %masternode.pro_tx_hash, - method = "create_owner_identity_v1", - "owner identity with only the OWNER key: no single payout address" - ); - } - identity.add_public_keys([Self::get_owner_identity_owner_key( - owner_address, - 1, - platform_version, - )?]); - Ok(Some(identity)) + identity.add_public_keys([ + Self::get_owner_identity_withdrawal_key(payout_address, 0, platform_version)?, + Self::get_owner_identity_owner_key(owner_address, 1, platform_version)?, + ]); + Ok(identity) } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs index 169f1f4cd06..1b635109277 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs @@ -49,3 +49,216 @@ where } } } + +#[cfg(test)] +mod tests { + use crate::config::PlatformConfig; + use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{Network, ProTxHash, PubkeyHash, ScriptBuf, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{ + DMNPayout, DMNState, DMNStateDiff, MasternodeListDiff, MasternodeListItem, MasternodeType, + }; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyID, Purpose}; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + /// A payout change of an extended-address masternode reaches Drive only as `payouts`, + /// never as `payout_address`. Every protocol version rotates the owner identity's TRANSFER + /// key only on a `payout_address` change; acting on `payouts` as well would change what a + /// block does to owner identities. Protocol version 13 is pinned because a node running it + /// against Dash Core v24 receives such diffs. + #[test] + fn should_not_change_owner_identity_keys_on_a_payout_list_change() { + for platform_version in [ + PlatformVersion::get(13).expect("expected protocol version 13"), + PlatformVersion::latest(), + ] { + assert_owner_identity_keys_unchanged_by_payout_list_changes(platform_version); + } + } + + fn assert_owner_identity_keys_unchanged_by_payout_list_changes( + platform_version: &PlatformVersion, + ) { + let protocol_version = platform_version.protocol_version; + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + + let pro_tx_hash = ProTxHash::from_byte_array([0x51; 32]); + let owner_address = [0x52; 20]; + let payout_address = [0x53; 20]; + let masternode = MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash, + collateral_hash: Txid::from_byte_array([0x54; 32]), + collateral_index: 0, + collateral_address: Some([0x55; 20]), + operator_reward: 0.0, + state: DMNState { + service: "1.2.3.4:9999".parse().expect("socket address"), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address: Some(owner_address), + voting_address: [0x56; 20], + payout_address: Some(payout_address), + payouts: None, + pub_key_operator: vec![0x57; 48], + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + }, + }; + + let transaction = platform.drive.grove.start_transaction(); + platform + .update_masternode_identities( + MasternodeListDiff { + base_height: 0, + block_height: 1, + added_mns: vec![masternode.clone()], + removed_mns: vec![], + updated_mns: vec![], + }, + &BTreeMap::new(), + &BlockInfo::default(), + None, + &transaction, + platform_version, + ) + .expect("expected to create the masternode identities"); + + let mut platform_state = PlatformState::default_with_protocol_versions( + protocol_version, + protocol_version, + &PlatformConfig::default_for_network(Network::Testnet), + ) + .expect("platform state"); + platform_state.insert_masternode(masternode); + + let apply_update = |state_diff: DMNStateDiff| { + platform + .update_masternode_identities( + MasternodeListDiff { + base_height: 1, + block_height: 2, + added_mns: vec![], + removed_mns: vec![], + updated_mns: vec![(pro_tx_hash, state_diff)], + }, + &BTreeMap::new(), + &BlockInfo::default(), + Some(&platform_state), + &transaction, + platform_version, + ) + .expect("expected to process the masternode update"); + }; + let owner_identity_keys = || -> BTreeMap, bool)> { + platform + .drive + .fetch_full_identity( + pro_tx_hash.to_byte_array(), + Some(&transaction), + platform_version, + ) + .expect("expected to fetch the owner identity") + .expect("expected the owner identity to exist") + .public_keys() + .iter() + .map(|(key_id, key)| { + ( + *key_id, + (key.purpose(), key.data().to_vec(), key.is_disabled()), + ) + }) + .collect() + }; + let keys_before = BTreeMap::from([ + (0, (Purpose::TRANSFER, payout_address.to_vec(), false)), + (1, (Purpose::OWNER, owner_address.to_vec(), false)), + ]); + assert_eq!( + owner_identity_keys(), + keys_before, + "protocol version {protocol_version}" + ); + + let payout = |address: [u8; 20], reward: u16| DMNPayout { + address, + script: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(address)), + reward, + }; + for payouts in [ + // The payout address moved into a list, as Core does when a masternode moves to + // extended addresses. + vec![payout(payout_address, 10000)], + // Another single payout address. + vec![payout([0x58; 20], 10000)], + // Several payouts. + vec![payout([0x58; 20], 2500), payout([0x59; 20], 7500)], + ] { + apply_update(DMNStateDiff { + payouts: Some(payouts.clone()), + ..empty_state_diff() + }); + assert_eq!( + owner_identity_keys(), + keys_before, + "protocol version {protocol_version}, payouts {payouts:?}" + ); + } + + // The same processing does rotate the TRANSFER key on a payout address change. + apply_update(DMNStateDiff { + payout_address: Some([0x5a; 20]), + ..empty_state_diff() + }); + assert_eq!( + owner_identity_keys(), + BTreeMap::from([ + (0, (Purpose::TRANSFER, payout_address.to_vec(), true)), + (1, (Purpose::OWNER, owner_address.to_vec(), false)), + (2, (Purpose::TRANSFER, vec![0x5a; 20], false)), + ]), + "protocol version {protocol_version}" + ); + } + + fn empty_state_diff() -> DMNStateDiff { + DMNStateDiff { + service: None, + registered_height: None, + last_paid_height: None, + consecutive_payments: None, + pose_penalty: None, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: None, + owner_address: None, + voting_address: None, + payout_address: None, + payouts: None, + pub_key_operator: None, + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs index 06458c123a9..6cdff83e2f1 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs @@ -81,17 +81,10 @@ where "create operator identity" ); - // A masternode with none of the keys an owner identity holds, such as a shared - // masternode, has no owner identity. Only an added masternode without an owner - // address, or with `payouts` instead of `payoutAddress`, gets none, and no earlier - // binary can deserialize a masternode list holding one, so no committed block - // skipped an owner identity here. - if let Some(owner_identity) = owner_identity { - drive_operations.push(IdentityOperation(AddNewIdentity { - identity: owner_identity, - is_masternode_identity: true, - })); - } + drive_operations.push(IdentityOperation(AddNewIdentity { + identity: owner_identity, + is_masternode_identity: true, + })); drive_operations.push(IdentityOperation(AddNewIdentity { identity: voter_identity, @@ -173,194 +166,3 @@ where Ok(()) } } - -#[cfg(test)] -mod tests { - use crate::config::PlatformConfig; - use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; - use crate::test::helpers::setup::TestPlatformBuilder; - use dpp::block::block_info::BlockInfo; - use dpp::dashcore::hashes::Hash; - use dpp::dashcore::{Network, ProTxHash, PubkeyHash, ScriptBuf, Txid}; - use dpp::dashcore_rpc::dashcore_rpc_json::{ - DMNPayout, DMNState, DMNStateDiff, MasternodeListDiff, MasternodeListItem, MasternodeType, - }; - use dpp::identity::accessors::IdentityGettersV0; - use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; - use dpp::identity::{KeyID, Purpose}; - use dpp::version::PlatformVersion; - use std::collections::BTreeMap; - - /// A payout change of an extended-address masternode reaches Drive only as `payouts`, - /// never as `payout_address`. Only a `payout_address` change rotates the owner identity's - /// TRANSFER key; acting on `payouts` as well would change what a block does to owner - /// identities, which needs a new method version. - #[test] - fn should_not_change_owner_identity_keys_on_a_payout_list_change() { - let platform_version = PlatformVersion::latest(); - let platform = TestPlatformBuilder::new() - .build_with_mock_rpc() - .set_genesis_state(); - - let pro_tx_hash = ProTxHash::from_byte_array([0x51; 32]); - let owner_address = [0x52; 20]; - let payout_address = [0x53; 20]; - let masternode = MasternodeListItem { - node_type: MasternodeType::Regular, - pro_tx_hash, - collateral_hash: Txid::from_byte_array([0x54; 32]), - collateral_index: 0, - collateral_address: Some([0x55; 20]), - operator_reward: 0.0, - state: DMNState { - service: "1.2.3.4:9999".parse().expect("socket address"), - registered_height: 0, - pose_revived_height: None, - pose_ban_height: None, - revocation_reason: 0, - owner_address: Some(owner_address), - voting_address: [0x56; 20], - payout_address: Some(payout_address), - payouts: None, - pub_key_operator: vec![0x57; 48], - operator_payout_address: None, - platform_node_id: None, - #[allow(deprecated)] - legacy_platform_p2p_port: None, - #[allow(deprecated)] - legacy_platform_http_port: None, - addresses: None, - }, - }; - - let transaction = platform.drive.grove.start_transaction(); - platform - .update_masternode_identities( - MasternodeListDiff { - base_height: 0, - block_height: 1, - added_mns: vec![masternode.clone()], - removed_mns: vec![], - updated_mns: vec![], - }, - &BTreeMap::new(), - &BlockInfo::default(), - None, - &transaction, - platform_version, - ) - .expect("expected to create the masternode identities"); - - let mut platform_state = PlatformState::default_with_protocol_versions( - platform_version.protocol_version, - platform_version.protocol_version, - &PlatformConfig::default_for_network(Network::Testnet), - ) - .expect("platform state"); - platform_state.insert_masternode(masternode); - - let apply_update = |state_diff: DMNStateDiff| { - platform - .update_masternode_identities( - MasternodeListDiff { - base_height: 1, - block_height: 2, - added_mns: vec![], - removed_mns: vec![], - updated_mns: vec![(pro_tx_hash, state_diff)], - }, - &BTreeMap::new(), - &BlockInfo::default(), - Some(&platform_state), - &transaction, - platform_version, - ) - .expect("expected to process the masternode update"); - }; - let owner_identity_keys = || -> BTreeMap, bool)> { - platform - .drive - .fetch_full_identity( - pro_tx_hash.to_byte_array(), - Some(&transaction), - platform_version, - ) - .expect("expected to fetch the owner identity") - .expect("expected the owner identity to exist") - .public_keys() - .iter() - .map(|(key_id, key)| { - ( - *key_id, - (key.purpose(), key.data().to_vec(), key.is_disabled()), - ) - }) - .collect() - }; - let keys_before = BTreeMap::from([ - (0, (Purpose::TRANSFER, payout_address.to_vec(), false)), - (1, (Purpose::OWNER, owner_address.to_vec(), false)), - ]); - assert_eq!(owner_identity_keys(), keys_before); - - let payout = |address: [u8; 20], reward: u16| DMNPayout { - address, - script: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(address)), - reward, - }; - for payouts in [ - // The payout address moved into a list, as Core does when a masternode moves to - // extended addresses. - vec![payout(payout_address, 10000)], - // Another single payout address. - vec![payout([0x58; 20], 10000)], - // Several payouts. - vec![payout([0x58; 20], 2500), payout([0x59; 20], 7500)], - ] { - apply_update(DMNStateDiff { - payouts: Some(payouts.clone()), - ..empty_state_diff() - }); - assert_eq!(owner_identity_keys(), keys_before, "payouts {payouts:?}"); - } - - // The same processing does rotate the TRANSFER key on a payout address change. - apply_update(DMNStateDiff { - payout_address: Some([0x5a; 20]), - ..empty_state_diff() - }); - assert_eq!( - owner_identity_keys(), - BTreeMap::from([ - (0, (Purpose::TRANSFER, payout_address.to_vec(), true)), - (1, (Purpose::OWNER, owner_address.to_vec(), false)), - (2, (Purpose::TRANSFER, vec![0x5a; 20], false)), - ]) - ); - } - - fn empty_state_diff() -> DMNStateDiff { - DMNStateDiff { - service: None, - registered_height: None, - last_paid_height: None, - consecutive_payments: None, - pose_penalty: None, - pose_revived_height: None, - pose_ban_height: None, - revocation_reason: None, - owner_address: None, - voting_address: None, - payout_address: None, - payouts: None, - pub_key_operator: None, - operator_payout_address: None, - platform_node_id: None, - #[allow(deprecated)] - legacy_platform_p2p_port: None, - #[allow(deprecated)] - legacy_platform_http_port: None, - addresses: None, - } - } -} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs index ef114d68582..8494a69df19 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs @@ -73,26 +73,13 @@ where mod test { use super::*; use crate::config::PlatformConfig; - use crate::platform_types::platform_state::platform_state_for_saving::v2::{ - deserialize_masternode_entry, serialize_masternode_entry, - }; - use crate::platform_types::platform_state::PlatformStateV0Methods; - use crate::platform_types::validator::v0::{NewValidatorIfMasternodeInState, ValidatorV0}; - use crate::test::helpers::setup::TestPlatformBuilder; - use dpp::dashcore::hashes::Hash; - use dpp::dashcore::ProTxHash; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; use dpp::dashcore_rpc::json::MasternodeListDiff; - use dpp::identifier::MasternodeIdentifiers; - use dpp::identity::accessors::IdentityGettersV0; - use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; - use dpp::identity::{KeyID, Purpose}; - use dpp::prelude::Identifier; - use std::collections::BTreeMap; use std::env; use std::fs::File; use std::io::BufReader; use std::path::PathBuf; - use std::str::FromStr; #[test] fn test_update_masternode_list() { @@ -213,34 +200,25 @@ mod test { .expect("expected to commit"); } - /// Dash Core v24 prints masternodes that differ from every earlier list: a shared - /// masternode has no owner, payout or collateral address; an extended-address masternode - /// has a `payouts` list instead of `payoutAddress`; a Tor primary address is not an - /// `ip:port` `service`; an Evo without addresses has `-1` platform ports. The list must be - /// processed rather than fail the block, and the masternode identities must be: - /// - shared: voter and operator identities, no owner identity (it has no owner key); - /// - one payout: the same owner identity as a legacy payout address (TRANSFER key 0, - /// OWNER key 1); - /// - several payouts: an owner identity with only the OWNER key 1; - /// - a later payout change through `payouts`: no key change, since only a `payoutAddress` - /// change rotates the TRANSFER key. - #[test] - fn should_update_masternode_identities_from_a_core_v24_masternode_list() { - let platform_version = PlatformVersion::latest(); - + /// A platform at `platform_version` whose mock Core returns Dash Core v24 `protx listdiff` + /// output: the diff from height 1 to 1250 adds a legacy Evo, a shared masternode, Regular + /// masternodes with one and with two payouts (one of them with a Tor primary address), an + /// Evo without addresses and an IPv6 Evo with two payouts; the diff to 1260 moves the + /// legacy Evo to extended addresses and changes two payout lists. + fn platform_with_core_v24_masternode_list( + platform_version: &PlatformVersion, + ) -> TempPlatform { let mut platform = TestPlatformBuilder::new() .with_config(PlatformConfig::default()) + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); - let genesis_core_block_height = 1250; - let first_block_core_block_height = 1260; - platform .core_rpc .expect_get_protx_diff_with_masternodes() .returning(move |_base_block, block| { - let file_name = if block == genesis_core_block_height { + let file_name = if block == 1250 { "1-1250.json" } else { "1250-1260.json" @@ -256,217 +234,41 @@ mod test { Ok(serde_json::from_str(&json)?) }); - let transaction = platform.drive.grove.start_transaction(); - - let mut init_chain_platform_state = platform.state.load().as_ref().clone(); - - platform - .update_masternode_list( - None, - &mut init_chain_platform_state, - genesis_core_block_height, - true, - &BlockInfo { - height: 1, - core_height: genesis_core_block_height, - time_ms: 1, - ..Default::default() - }, - &transaction, - platform_version, - ) - .expect("expected to process the Core v24 masternode list"); - - let platform_state = init_chain_platform_state.clone(); - let mut block_platform_state = platform_state.clone(); - platform - .update_masternode_list( - Some(&platform_state), - &mut block_platform_state, - first_block_core_block_height, - false, - &BlockInfo { - height: 1, - core_height: first_block_core_block_height, - time_ms: 2, - ..Default::default() - }, - &transaction, - platform_version, - ) - .expect("expected to process the Core v24 masternode list diff"); - - assert_eq!(block_platform_state.full_masternode_list().len(), 7); - assert_eq!(block_platform_state.hpmn_masternode_list().len(), 3); - - // An Evo without addresses prints `-1` platform ports, which read as no port, so it - // cannot be a validator, like any Evo without ports. The legacy Evo is one, with the - // ports and address the diff gave it. - let validator = |pro_tx_hash: &str| { - ValidatorV0::new_validator_if_masternode_in_state( - ProTxHash::from_str(pro_tx_hash).expect("expected a pro tx hash"), - None, - &block_platform_state, - ) - }; - assert!( - validator("6f1757595185032c808321af3e2e8468fae10b8f91e2a657d7a4c7122f4b2706").is_none() - ); - let legacy_evo_validator = - validator("ca3d32c4f2ef62eaf736bf41bb3235a832ec1eb6d8dfaccd4d3555e70f6757b0") - .expect("expected the legacy Evo to be a validator"); - assert_eq!(legacy_evo_validator.node_ip, "192.0.2.40"); - assert_eq!(legacy_evo_validator.platform_p2p_port, 36656); - assert_eq!(legacy_evo_validator.platform_http_port, 1443); - - // Every parsed masternode survives a store and reload, except for the payout list and - // nested addresses, which are not stored. - for masternode in block_platform_state.full_masternode_list().values() { - let bytes = serialize_masternode_entry(masternode, platform_version) - .expect("expected to serialize the masternode entry"); - let mut expected = masternode.clone(); - expected.state.payouts = None; - expected.state.addresses = None; - assert_eq!( - deserialize_masternode_entry(&bytes) - .expect("expected to deserialize the masternode entry"), - expected - ); - } - - let hash160 = |hex: &str| -> Vec { hex::decode(hex).expect("expected hex") }; - - // The keys of an identity, none of them disabled; `None` if there is no identity. - let identity_keys = - |identity_id: Identifier| -> Option)>> { - platform - .drive - .fetch_full_identity( - identity_id.to_buffer(), - Some(&transaction), - platform_version, - ) - .expect("expected to fetch an identity") - .map(|identity| { - identity - .public_keys() - .iter() - .map(|(key_id, key)| { - assert!(key.disabled_at().is_none(), "key {key_id} is disabled"); - (*key_id, (key.purpose(), key.data().to_vec())) - }) - .collect() - }) - }; + } - let owner_key = |hex: &str| (Purpose::OWNER, hash160(hex)); - let transfer_key = |hex: &str| (Purpose::TRANSFER, hash160(hex)); + /// Up to protocol version 13 an added masternode needs a payout and an owner address for + /// its owner identity. A Dash Core v24 list adds shared and extended-address masternodes + /// that have neither or no payout address, so the update fails with + /// `DashCoreBadResponseError` and the block with it, as on every release that cannot parse + /// such a list. + #[test] + fn should_fail_on_a_core_v24_masternode_list_up_to_protocol_version_13() { + let platform_version = PlatformVersion::get(13).expect("expected protocol version 13"); + let platform = platform_with_core_v24_masternode_list(platform_version); - let voting_421c = "421c03add2c804421451c4e022258778175e60d8"; - let voting_064c = "064cd21ff210c1a92adaea49a4768a41c2aef1bc"; + let transaction = platform.drive.grove.start_transaction(); + let mut init_chain_platform_state = platform.state.load().as_ref().clone(); - // (pro_tx_hash, voting key hash, operator public key, owner identity keys) - let expected = [ - ( - // Legacy Evo whose payout address the diff moves into `payouts` - "ca3d32c4f2ef62eaf736bf41bb3235a832ec1eb6d8dfaccd4d3555e70f6757b0", - voting_421c, - "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563".to_string(), - Some(BTreeMap::from([ - (0, transfer_key("75d57974b6e29a4a70df57a3b11195ce0a0dc817")), - (1, owner_key("1f67d90f35e3c5070c368ae6f3635aac357e47df")), - ])), - ), - ( - // One payout, which the diff replaces with two - "27978dd892b7c876c238be1a6141461c2824f3497dd0058e160979bc8f0a0bef", - voting_421c, - "a792ce1af5f7bb9281053b3934cb8b08d00d075a56498e1a525388ce467f188e8a80911fd96a20982baa9b9678452534".to_string(), - Some(BTreeMap::from([ - (0, transfer_key("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")), - (1, owner_key("b740a2ab3f631e4dfe15debf746364f1d8352c50")), - ])), - ), - ( - // Shared - "a4d26868017c0ccffe2efe50944ef4211834660cca834c6e9f86dec6a88246fa", - voting_421c, - "b1".repeat(48), - None, + match platform.update_masternode_list( + None, + &mut init_chain_platform_state, + 1250, + true, + &BlockInfo { + height: 1, + core_height: 1250, + time_ms: 1, + ..Default::default() + }, + &transaction, + platform_version, + ) { + Err(Error::Execution(ExecutionError::DashCoreBadResponseError(message))) => assert!( + message.ends_with("has no payout address"), + "unexpected message: {message}" ), - ( - // One payout, which the diff replaces with another single payout - "813a7c3f28817988a8e6ce66e07e43e261e78398373bfbaae94c898645111d6b", - voting_421c, - "b2".repeat(48), - Some(BTreeMap::from([ - (0, transfer_key("5555555555555555555555555555555555555555")), - (1, owner_key("0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a")), - ])), - ), - ( - // Tor primary address, two payouts; the owner and voting keys are the same - "3023ffd989974768b0dfc347410ad923fa6d3f1eee90180bd0c435e81cb1a82f", - voting_064c, - "b3".repeat(48), - Some(BTreeMap::from([(1, owner_key(voting_064c))])), - ), - ( - // Evo without addresses, one payout - "6f1757595185032c808321af3e2e8468fae10b8f91e2a657d7a4c7122f4b2706", - voting_421c, - "b4".repeat(48), - Some(BTreeMap::from([ - (0, transfer_key("9999999999999999999999999999999999999999")), - (1, owner_key("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")), - ])), - ), - ( - // Evo with an IPv6 primary address, two payouts - "aecd2830b843e6a84283ba290492a213e355cea7c5026e6118a21e1bfbc36783", - voting_421c, - "b5".repeat(48), - Some(BTreeMap::from([( - 1, - owner_key("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"), - )])), - ), - ]; - - for (pro_tx_hash, voting_key_hash, operator_public_key, owner_identity_keys) in expected { - let pro_tx_hash = ProTxHash::from_str(pro_tx_hash) - .expect("expected a pro tx hash") - .to_byte_array(); - let voting_key_hash: [u8; 20] = hash160(voting_key_hash) - .try_into() - .expect("expected 20 bytes"); - let operator_public_key = hex::decode(operator_public_key).expect("expected hex"); - - assert_eq!( - identity_keys(pro_tx_hash.into()), - owner_identity_keys, - "owner identity of {}", - hex::encode(pro_tx_hash) - ); - assert!( - identity_keys(Identifier::create_voter_identifier( - &pro_tx_hash, - &voting_key_hash - )) - .is_some(), - "voter identity of {}", - hex::encode(pro_tx_hash) - ); - assert!( - identity_keys(Identifier::create_operator_identifier( - &pro_tx_hash, - &operator_public_key - )) - .is_some(), - "operator identity of {}", - hex::encode(pro_tx_hash) - ); + other => panic!("expected DashCoreBadResponseError, got {other:?}"), } } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 13e7b5a13bf..76eacdf65a3 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -186,22 +186,18 @@ impl Platform { continue; } - // A shared masternode has no owner key to add. Only a Core v24 masternode list has - // one, and no earlier binary can parse it, so no committed block reached this branch. - let Some(owner_address) = masternode.state.owner_address else { - tracing::debug!( - pro_tx_hash = %masternode.pro_tx_hash, - method = "transition_to_version_4", - "no OWNER key added: the masternode has no owner address" - ); - continue; - }; - let last_key_id = *old_owner_identity_keys .keys() .max() .expect("there must be keys, we already checked"); + let owner_address = masternode.state.owner_address.ok_or_else(|| { + Error::Execution(ExecutionError::DashCoreBadResponseError(format!( + "masternode {} has no owner address", + masternode.pro_tx_hash + ))) + })?; + let new_owner_key = Self::get_owner_identity_owner_key( owner_address, last_key_id + 1, @@ -1047,11 +1043,14 @@ mod tests { assert!(result.is_ok()); } - /// Version 4 gives every owner identity the OWNER key of its masternode's owner address. - /// A shared masternode has no owner address, so its identity, if one exists under its - /// ProTx hash, gets no key; the owner identities of other masternodes still get theirs. + /// Version 4 gives every owner identity the OWNER key of its masternode's owner address, + /// after the keys it has. A masternode without an owner identity is skipped before its + /// owner address is read. One with an owner identity but no owner address (a shared + /// masternode) has no key to give, so the transition fails with `DashCoreBadResponseError` + /// instead of leaving the identity without an OWNER key. #[test] - fn should_add_no_owner_key_for_a_masternode_without_owner_address_on_transition_to_version_4() { + fn should_fail_transition_to_version_4_for_an_owner_identity_whose_masternode_has_no_owner_address( + ) { use dpp::dashcore::{ProTxHash, Txid}; use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem, MasternodeType}; use dpp::identity::accessors::IdentityGettersV0; @@ -1064,6 +1063,8 @@ mod tests { .build_with_mock_rpc() .set_genesis_state(); + // A masternode as Core lists it; a shared one has no owner, payout or collateral + // address. let masternode = |pro_tx_hash: [u8; 32], owner_address: Option<[u8; 20]>| MasternodeListItem { node_type: MasternodeType::Regular, @@ -1092,23 +1093,13 @@ mod tests { addresses: None, }, }; - let owner_address = [0x46; 20]; - let with_owner = [0x47; 32]; - let shared_with_identity = [0x48; 32]; - let shared_without_identity = [0x49; 32]; - - let mut platform_state = platform.state.load().as_ref().clone(); - for item in [ - masternode(with_owner, Some(owner_address)), - masternode(shared_with_identity, None), - masternode(shared_without_identity, None), - ] { - platform_state.insert_masternode(item); - } + let with_owner_address = [0x47; 32]; + let shared_without_owner_identity = [0x48; 32]; + let shared_with_owner_identity = [0x49; 32]; // Owner identities as created before version 4: only the TRANSFER key id 0. - for pro_tx_hash in [with_owner, shared_with_identity] { + for pro_tx_hash in [with_owner_address, shared_with_owner_identity] { let mut identity = Identity::create_basic_identity(pro_tx_hash.into(), platform_version) .expect("expected an identity"); @@ -1134,49 +1125,53 @@ mod tests { } let transaction = platform.drive.grove.start_transaction(); + let platform_state_with = |masternodes: Vec| { + let mut platform_state = platform.state.load().as_ref().clone(); + for masternode in masternodes { + platform_state.insert_masternode(masternode); + } + platform_state + }; + platform .transition_to_version_4( - &platform_state, + &platform_state_with(vec![ + masternode(with_owner_address, Some(owner_address)), + masternode(shared_without_owner_identity, None), + ]), &BlockInfo::default(), &transaction, platform_version, ) - .expect("expected the transition to version 4 to succeed"); - - let identity_keys = |pro_tx_hash: [u8; 32]| -> BTreeMap)> { + .expect("expected a masternode without an owner identity to be skipped"); + assert_eq!( platform .drive - .fetch_full_identity(pro_tx_hash, Some(&transaction), platform_version) - .expect("expected to fetch an identity") - .expect("expected the identity to exist") + .fetch_full_identity(with_owner_address, Some(&transaction), platform_version) + .expect("expected to fetch the owner identity") + .expect("expected the owner identity to exist") .public_keys() .iter() .map(|(key_id, key)| (*key_id, (key.purpose(), key.data().to_vec()))) - .collect() - }; - - assert_eq!( - identity_keys(with_owner), + .collect::)>>(), BTreeMap::from([ (0, (Purpose::TRANSFER, vec![0x44; 20])), (1, (Purpose::OWNER, owner_address.to_vec())), ]) ); - assert_eq!( - identity_keys(shared_with_identity), - BTreeMap::from([(0, (Purpose::TRANSFER, vec![0x44; 20]))]) - ); - assert_eq!( - platform - .drive - .fetch_full_identity( - shared_without_identity, - Some(&transaction), - platform_version - ) - .expect("expected to fetch an identity"), - None - ); + + match platform.transition_to_version_4( + &platform_state_with(vec![masternode(shared_with_owner_identity, None)]), + &BlockInfo::default(), + &transaction, + platform_version, + ) { + Err(Error::Execution(ExecutionError::DashCoreBadResponseError(message))) => assert!( + message.ends_with("has no owner address"), + "unexpected message: {message}" + ), + other => panic!("expected DashCoreBadResponseError, got {other:?}"), + } } #[test] From b442df4fffeac19ab1ff468ea3aabfa4802fc9a2 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 3 Oct 2026 16:15:54 +0700 Subject: [PATCH 08/11] feat(drive-abci)!: create owner identities for Core v24 shared and multi-payout masternodes at protocol version 14 From v24 on, Dash Core lists shared masternodes, which have no owner, payout or collateral address, and extended-address masternodes, which have a `payouts` list instead of `payoutAddress`. create_owner_identity 1 fails on both with DashCoreBadResponseError, which fails the block. New generations, selected only by protocol version 14 (DRIVE_ABCI_METHOD_VERSIONS_V10, amended in place; only PLATFORM_V14 references it): - create_owner_identity 2, a copy of 1: no owner address gives no owner identity; an owner address and a single payout address (payoutAddress, else the only entry of a one-entry payout list) give the version 1 identity byte for byte (TRANSFER key 0, OWNER key 1); several payouts give an owner identity with only the OWNER key 1, key id 0 free. The effective-payout helper lives in v2, its only user. - update_masternode_identities 1, a copy of 0 that adds no owner identity when create_owner_identity gives none. Updates are processed as in 0: only a payoutAddress change rotates the TRANSFER key, and a `payouts` change is not acted on. The create_owner_identity dispatcher now returns Option. Versions 0 and 1 are unchanged and wrapped in Some; the only change to update_masternode_identities 0 unwraps it and treats None as CorruptedCodeExecution, unreachable because every protocol version that selects it selects create_owner_identity 0 or 1. transition_to_version_4 is unchanged; it only runs when crossing protocol version 4. Item 66 of PLATFORM_V14's doc records the change: protocol version 14 has to be active on a network before its Dash Core activates V24, since earlier versions keep failing on these masternodes. Tests at the latest version, through the dispatchers: - create_owner_identity v2: shared gives None; a single payout gives the payout address's identity, which equals version 1's at protocol version 13, byte for byte; several payouts give OWNER key 1 only. - update_masternode_identities v1: a shared masternode gets voter and operator identities and no owner identity; a change of its voting address and operator key, and its removal, succeed without an owner identity and leave its voter and operator keys disabled. - update_masternode_list with the Core v24 listdiff fixture: processed, with the identities above and no key change on payout-list diffs (protocol version 13 keeps failing on it, pinned in the previous commit). - The shipped-generation test now also checks the key layout a legacy masternode gets at protocol versions 3 (TRANSFER 0) and 13 (TRANSFER 0, OWNER 1), and the payout-list-diff test describes both generations. Red to green: with create_owner_identity 1 and update_masternode_identities 0 left in DRIVE_ABCI_METHOD_VERSIONS_V10 the six new-generation tests fail with DashCoreBadResponseError ("... has no payout address"); with the table change they pass. Co-Authored-By: Claude Opus 5.5 --- .../create_owner_identity/mod.rs | 53 ++- .../create_owner_identity/v2/mod.rs | 244 ++++++++++ .../update_masternode_identities/mod.rs | 23 +- .../update_masternode_identities/v0/mod.rs | 9 +- .../update_masternode_identities/v1/mod.rs | 417 ++++++++++++++++++ .../update_masternode_list/mod.rs | 249 +++++++++++ .../drive_abci_method_versions/v10.rs | 8 +- .../rs-platform-version/src/version/v14.rs | 18 + 8 files changed, 1000 insertions(+), 21 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v2/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/mod.rs diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs index d520d7f43f4..df59d420863 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/mod.rs @@ -1,5 +1,6 @@ mod v0; mod v1; +mod v2; use crate::error::execution::ExecutionError; use crate::error::Error; @@ -18,6 +19,11 @@ where /// This function constructs an identity for an owner using details from the masternode. /// It delegates to a version-specific method depending on the platform version. /// + /// Versions 0 and 1 always create an identity and fail on a masternode without the payout + /// address (and, in version 1, the owner address) they read. Version 2 (from protocol + /// version 14) creates none for a masternode without an owner address, such as a shared + /// masternode, and only the OWNER key for one paid to several payouts. + /// /// # Arguments /// /// * masternode - A reference to the masternode list item. @@ -25,12 +31,12 @@ where /// /// # Returns /// - /// * Result - Returns the constructed identity for the owner if successful. - /// Otherwise, returns an error. + /// * Result, Error> - The owner identity, or `None` when the masternode + /// has none (only version 2 returns `None`). Otherwise, returns an error. pub(crate) fn create_owner_identity( masternode: &MasternodeListItem, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result, Error> { match platform_version .drive_abci .methods @@ -38,11 +44,12 @@ where .masternode_updates .create_owner_identity { - 0 => Self::create_owner_identity_v0(masternode, platform_version), - 1 => Self::create_owner_identity_v1(masternode, platform_version), + 0 => Self::create_owner_identity_v0(masternode, platform_version).map(Some), + 1 => Self::create_owner_identity_v1(masternode, platform_version).map(Some), + 2 => Self::create_owner_identity_v2(masternode, platform_version), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "create_owner_identity".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, })), } @@ -60,7 +67,11 @@ mod tests { use dpp::dashcore_rpc::dashcore_rpc_json::{ DMNPayout, DMNState, MasternodeListItem, MasternodeType, }; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyID, Purpose}; use dpp::version::PlatformVersion; + use std::collections::BTreeMap; const OWNER_ADDRESS: [u8; 20] = [0x22; 20]; const PAYOUT_ADDRESS: [u8; 20] = [0x24; 20]; @@ -111,11 +122,12 @@ mod tests { } /// Generation 0 (protocol versions 1 to 3) builds the owner identity from the payout - /// address, and generation 1 (4 to 13) from the payout and the owner address. A masternode - /// Core lists without one of them fails with `DashCoreBadResponseError`, so its block - /// fails instead of the masternode getting an owner identity these generations do not - /// define: a shared masternode has neither address, and an extended-address masternode - /// has a payout list instead of a payout address. + /// address (TRANSFER key 0), and generation 1 (4 to 13) from the payout and the owner + /// address (TRANSFER key 0, OWNER key 1). A masternode Core lists without one of them + /// fails with `DashCoreBadResponseError`, so its block fails instead of the masternode + /// getting an owner identity these generations do not define: a shared masternode has + /// neither address, and an extended-address masternode has a payout list instead of a + /// payout address. #[test] fn should_fail_for_a_masternode_without_a_payout_or_owner_address_up_to_protocol_version_13() { let legacy = masternode(Some(OWNER_ADDRESS), Some(PAYOUT_ADDRESS), None); @@ -139,8 +151,23 @@ mod tests { let platform_version = PlatformVersion::get(protocol_version).expect("expected a known protocol version"); - Platform::::create_owner_identity(&legacy, platform_version) - .expect("a masternode with an owner and a payout address has an owner identity"); + let legacy_owner_identity_keys: BTreeMap)> = + Platform::::create_owner_identity(&legacy, platform_version) + .expect("expected to create the owner identity") + .expect("a masternode with an owner and a payout address has an owner identity") + .public_keys() + .iter() + .map(|(key_id, key)| (*key_id, (key.purpose(), key.data().to_vec()))) + .collect(); + let mut expected_keys = + BTreeMap::from([(0, (Purpose::TRANSFER, PAYOUT_ADDRESS.to_vec()))]); + if protocol_version == 13 { + expected_keys.insert(1, (Purpose::OWNER, OWNER_ADDRESS.to_vec())); + } + assert_eq!( + legacy_owner_identity_keys, expected_keys, + "protocol version {protocol_version}" + ); let mut cases = vec![ (&shared, "has no payout address"), diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v2/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v2/mod.rs new file mode 100644 index 00000000000..f3df1718813 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v2/mod.rs @@ -0,0 +1,244 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem}; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::identity::Identity; +use dpp::version::PlatformVersion; + +impl Platform +where + C: CoreRPCLike, +{ + /// Creates the owner identity of a masternode like version 1, and also decides it for the + /// masternodes version 1 fails on. + /// + /// Version 1 reads a payout address and an owner address and fails with + /// `DashCoreBadResponseError` when either is absent. Dash Core lists such masternodes from + /// v24 on: a shared masternode has no owner, payout or collateral address (its owners are + /// its share holders), and an extended-address masternode has a payout list instead of a + /// payout address. This version gives: + /// + /// - no owner address: no owner identity (`None`), as there is no owner key to hold; the + /// voter and operator identities of the masternode do not depend on it; + /// - an owner address and a single payout address (see `effective_payout_address`): the + /// identity of version 1, TRANSFER key id 0 and OWNER key id 1, byte for byte; + /// - an owner address and several payouts: an identity with only the OWNER key id 1, as no + /// single address can hold the TRANSFER key; key id 0 stays free. + pub(super) fn create_owner_identity_v2( + masternode: &MasternodeListItem, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let Some(owner_address) = masternode.state.owner_address else { + tracing::debug!( + pro_tx_hash = %masternode.pro_tx_hash, + method = "create_owner_identity_v2", + "no owner identity: the masternode has no owner address" + ); + return Ok(None); + }; + let owner_identifier = Self::get_owner_identifier(masternode)?; + let mut identity = Identity::create_basic_identity(owner_identifier, platform_version)?; + match effective_payout_address(&masternode.state) { + Some(payout_address) => identity.add_public_keys([ + Self::get_owner_identity_withdrawal_key(payout_address, 0, platform_version)?, + Self::get_owner_identity_owner_key(owner_address, 1, platform_version)?, + ]), + None => { + tracing::debug!( + pro_tx_hash = %masternode.pro_tx_hash, + method = "create_owner_identity_v2", + "owner identity with only the OWNER key: no single payout address" + ); + identity.add_public_keys([Self::get_owner_identity_owner_key( + owner_address, + 1, + platform_version, + )?]); + } + } + Ok(Some(identity)) + } +} + +/// The one address the owner reward of a masternode is paid to, which its owner identity's +/// TRANSFER key holds: the payout address, or the only entry of a payout list with one entry +/// (Core turns a payout address into such a list when a masternode moves to extended +/// addresses). `None` for a shared masternode, which has neither, and for a reward split +/// between several payouts. +/// +/// The stored masternode list does not keep payout lists, so this is only meaningful for a +/// masternode as Core reported it, never for one read from the stored list. +fn effective_payout_address(state: &DMNState) -> Option<[u8; 20]> { + match (state.payout_address, state.payouts.as_deref()) { + (Some(payout_address), _) => Some(payout_address), + (None, Some([payout])) => Some(payout.address), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{ProTxHash, PubkeyHash, ScriptBuf, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{ + DMNPayout, DMNState, MasternodeListItem, MasternodeType, + }; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{Identity, KeyID, Purpose}; + use dpp::prelude::Identifier; + use dpp::serialization::PlatformSerializable; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + const OWNER_ADDRESS: [u8; 20] = [0x22; 20]; + const PAYOUT_ADDRESS: [u8; 20] = [0x24; 20]; + const SECOND_PAYOUT_ADDRESS: [u8; 20] = [0x25; 20]; + + /// A Regular masternode as Core lists it: a shared one has neither owner, payout nor + /// collateral address; an extended-address one has `payouts` instead of `payout_address`. + fn masternode( + owner_address: Option<[u8; 20]>, + payout_address: Option<[u8; 20]>, + payouts: Option>, + ) -> MasternodeListItem { + MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array([0x21; 32]), + collateral_hash: Txid::from_byte_array([0x27; 32]), + collateral_index: 0, + collateral_address: owner_address.map(|_| [0x28; 20]), + operator_reward: 0.0, + state: DMNState { + service: "1.2.3.4:9999".parse().expect("socket address"), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address, + voting_address: [0x23; 20], + payout_address, + payouts, + pub_key_operator: vec![0x26; 48], + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + }, + } + } + + fn p2pkh_payout(key_hash: [u8; 20], reward: u16) -> DMNPayout { + DMNPayout { + address: key_hash, + script: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(key_hash)), + reward, + } + } + + fn legacy_masternode() -> MasternodeListItem { + masternode(Some(OWNER_ADDRESS), Some(PAYOUT_ADDRESS), None) + } + + fn create_owner_identity( + masternode: &MasternodeListItem, + platform_version: &PlatformVersion, + ) -> Option { + Platform::::create_owner_identity(masternode, platform_version) + .expect("expected to decide the owner identity") + } + + fn serialized(identity: Option) -> Vec { + identity + .expect("expected an owner identity") + .serialize_to_bytes() + .expect("expected to serialize the owner identity") + } + + /// The owner identity holds the owner's keys, and a shared masternode has no owner key + /// (its owners are its share holders), so it gets no owner identity. + #[test] + fn should_create_no_owner_identity_for_a_shared_masternode() { + let shared = masternode(None, None, None); + + assert_eq!( + create_owner_identity(&shared, PlatformVersion::latest()), + None + ); + } + + /// Core turns a payout address into a payout list with one entry when a masternode moves + /// to extended addresses, so a single payout must give the owner identity a payout address + /// gives, which is the identity version 1 creates, byte for byte. + #[test] + fn should_create_the_same_owner_identity_for_a_single_payout_as_for_a_payout_address() { + let platform_version = PlatformVersion::latest(); + let single_payout = masternode( + Some(OWNER_ADDRESS), + None, + Some(vec![p2pkh_payout(PAYOUT_ADDRESS, 10000)]), + ); + + let legacy_owner_identity = serialized(create_owner_identity( + &legacy_masternode(), + platform_version, + )); + + assert_eq!( + serialized(create_owner_identity(&single_payout, platform_version)), + legacy_owner_identity + ); + assert_eq!( + serialized(create_owner_identity( + &legacy_masternode(), + PlatformVersion::get(13).expect("expected protocol version 13"), + )), + legacy_owner_identity, + "a payout address must give the identity version 1 gives" + ); + } + + /// Several payouts have no single address for the TRANSFER key id 0, so the owner identity + /// gets only the OWNER key id 1 a payout address's owner identity has, and key id 0 stays + /// free. + #[test] + fn should_create_an_owner_identity_with_only_the_owner_key_for_several_payouts() { + let platform_version = PlatformVersion::latest(); + let several_payouts = masternode( + Some(OWNER_ADDRESS), + None, + Some(vec![ + p2pkh_payout(PAYOUT_ADDRESS, 7000), + p2pkh_payout(SECOND_PAYOUT_ADDRESS, 3000), + ]), + ); + + let owner_key = create_owner_identity(&legacy_masternode(), platform_version) + .expect("expected an owner identity for a payout address") + .public_keys() + .get(&1) + .expect("expected the OWNER key id 1") + .clone(); + assert_eq!(owner_key.purpose(), Purpose::OWNER); + assert_eq!(owner_key.data().as_slice(), OWNER_ADDRESS.as_slice()); + + let several_payouts_owner_identity = + create_owner_identity(&several_payouts, platform_version) + .expect("expected an owner identity for several payouts"); + assert_eq!( + several_payouts_owner_identity.id(), + Identifier::from([0x21; 32]), + "the owner identity id is the ProTx hash" + ); + assert_eq!( + several_payouts_owner_identity.public_keys(), + &BTreeMap::from([(1 as KeyID, owner_key)]) + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs index 1b635109277..69ce72d94fa 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs @@ -11,12 +11,17 @@ use drive::grovedb::Transaction; use std::collections::BTreeMap; mod v0; +mod v1; impl Platform where C: CoreRPCLike, { /// Update of the masternode identities + /// + /// Version 0 adds an owner identity for every added masternode. Version 1 (from protocol + /// version 14) adds none for a masternode `create_owner_identity` gives none, such as a + /// shared masternode. pub(in crate::execution) fn update_masternode_identities( &self, masternode_diff: MasternodeListDiff, @@ -41,9 +46,17 @@ where transaction, platform_version, ), + 1 => self.update_masternode_identities_v1( + masternode_diff, + removed_masternodes, + block_info, + platform_state, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "update_masternode_identities".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } @@ -68,10 +81,10 @@ mod tests { use std::collections::BTreeMap; /// A payout change of an extended-address masternode reaches Drive only as `payouts`, - /// never as `payout_address`. Every protocol version rotates the owner identity's TRANSFER - /// key only on a `payout_address` change; acting on `payouts` as well would change what a - /// block does to owner identities. Protocol version 13 is pinned because a node running it - /// against Dash Core v24 receives such diffs. + /// never as `payout_address`. Both generations rotate the owner identity's TRANSFER key + /// only on a `payout_address` change and leave the owner identity's keys as they are on a + /// `payouts` change. Protocol version 13 is pinned because a node running it against Dash + /// Core v24 receives such diffs. #[test] fn should_not_change_owner_identity_keys_on_a_payout_list_change() { for platform_version in [ diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs index 6cdff83e2f1..a4182e82e64 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; @@ -54,7 +55,13 @@ where let mut drive_operations = vec![]; for masternode in added_mns { - let owner_identity = Self::create_owner_identity(&masternode, platform_version)?; + // Protocol versions 1 to 13, the ones that select this generation, select + // create_owner_identity 0 or 1, which create an identity or fail, and never return + // `None`. + let owner_identity = Self::create_owner_identity(&masternode, platform_version)? + .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "create_owner_identity 0 and 1 always create an owner identity", + )))?; tracing::trace!( identity = ?owner_identity, diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/mod.rs new file mode 100644 index 00000000000..72d61c97c0f --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/mod.rs @@ -0,0 +1,417 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::platform_types::platform_state::PlatformState; +use crate::rpc::core::CoreRPCLike; + +use dpp::block::block_info::BlockInfo; +use dpp::dashcore::ProTxHash; +use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeListDiff; +use dpp::dashcore_rpc::json::MasternodeListItem; + +use dpp::version::PlatformVersion; + +use drive::util::batch::DriveOperation::IdentityOperation; +use drive::util::batch::IdentityOperationType::AddNewIdentity; + +use crate::platform_types::platform_state::PlatformStateV0Methods; + +use dpp::dashcore::hashes::Hash; +use drive::grovedb::Transaction; +use std::collections::BTreeMap; +use tracing::Level; + +impl Platform +where + C: CoreRPCLike, +{ + /// Updates the masternode identities like version 0, except that an added masternode may + /// have no owner identity. + /// + /// Version 0 is paired with `create_owner_identity` 0 and 1, which create an owner identity + /// for every masternode or fail. `create_owner_identity` 2 creates none for a masternode + /// without an owner address, such as a shared masternode, and this version then adds only + /// its voter and operator identities. Updates and removals are processed exactly as in + /// version 0: only a `payout_address` change rotates the owner identity's TRANSFER key, and + /// a `payouts` change is not acted on. + pub(super) fn update_masternode_identities_v1( + &self, + masternode_diff: MasternodeListDiff, + removed_masternodes: &BTreeMap, + block_info: &BlockInfo, + platform_state: Option<&PlatformState>, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let MasternodeListDiff { + mut added_mns, + mut updated_mns, + .. + } = masternode_diff; + + let span = tracing::span!(Level::TRACE, "update_masternode_identities"); + let _enter = span.enter(); + + // We should don't trust the order of added mns or updated mns + + // Sort added_mns based on pro_tx_hash + added_mns.sort_by_key(|mn| mn.pro_tx_hash); + + // Sort updated_mns based on pro_tx_hash (the first element of the tuple) + updated_mns.sort_by_key(|mn| mn.0); + + let mut drive_operations = vec![]; + + for masternode in added_mns { + let owner_identity = Self::create_owner_identity(&masternode, platform_version)?; + + tracing::trace!( + identity = ?owner_identity, + method = "update_masternode_identities_v1", + "create owner identity" + ); + + let voter_identity = Self::create_voter_identity_from_masternode_list_item( + &masternode, + platform_version, + )?; + + tracing::trace!( + identity = ?voter_identity, + method = "update_masternode_identities_v1", + "create voter identity" + ); + + let operator_identity = Self::create_operator_identity(&masternode, platform_version)?; + + tracing::trace!( + identity = ?operator_identity, + method = "update_masternode_identities_v1", + "create operator identity" + ); + + if let Some(owner_identity) = owner_identity { + drive_operations.push(IdentityOperation(AddNewIdentity { + identity: owner_identity, + is_masternode_identity: true, + })); + } + + drive_operations.push(IdentityOperation(AddNewIdentity { + identity: voter_identity, + is_masternode_identity: true, + })); + + drive_operations.push(IdentityOperation(AddNewIdentity { + identity: operator_identity, + is_masternode_identity: true, + })); + } + + if let Some(platform_state) = platform_state { + // On initialization there is no platform state, but we also don't need to update + // masternode identities. + for update in updated_mns.iter() { + let (pro_tx_hash, state_diff) = update; + if let Some(new_withdrawal_address) = state_diff.payout_address { + let owner_identifier: [u8; 32] = pro_tx_hash.to_byte_array(); + self.update_owner_withdrawal_address( + owner_identifier, + new_withdrawal_address, + block_info, + transaction, + &mut drive_operations, + platform_version, + )?; + }; + + self.update_voter_identity( + update, + block_info, + platform_state, + transaction, + &mut drive_operations, + platform_version, + )?; + if state_diff.platform_node_id.is_some() + || state_diff.operator_payout_address.is_some() + || state_diff.pub_key_operator.is_some() + { + self.update_operator_identity( + pro_tx_hash, + state_diff.pub_key_operator.as_ref(), + state_diff.operator_payout_address, + state_diff.platform_node_id, + platform_state, + transaction, + &mut drive_operations, + platform_version, + )?; + } + } + + for masternode in removed_masternodes.values() { + self.disable_identity_keys( + masternode, + block_info, + transaction, + &mut drive_operations, + platform_version, + )?; + } + } + + let previous_fee_versions = platform_state.map(|state| state.previous_fee_versions()); + self.drive.apply_drive_operations( + drive_operations, + true, + block_info, + Some(transaction), + platform_version, + previous_fee_versions, + )?; + + let height = block_info.height; + tracing::debug!(height, "Updated masternode identities"); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::config::PlatformConfig; + use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{Network, ProTxHash, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{ + DMNState, DMNStateDiff, MasternodeListDiff, MasternodeListItem, MasternodeType, + }; + use dpp::identifier::MasternodeIdentifiers; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::Identity; + use dpp::prelude::Identifier; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + const PRO_TX_HASH: [u8; 32] = [0x61; 32]; + const VOTING_ADDRESS: [u8; 20] = [0x62; 20]; + const PUB_KEY_OPERATOR: [u8; 48] = [0x63; 48]; + + /// A shared masternode as Core lists it: no owner, payout or collateral address. + fn shared_masternode() -> MasternodeListItem { + MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array(PRO_TX_HASH), + collateral_hash: Txid::from_byte_array([0x64; 32]), + collateral_index: 0, + collateral_address: None, + operator_reward: 0.0, + state: DMNState { + service: "1.2.3.4:9999".parse().expect("socket address"), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address: None, + voting_address: VOTING_ADDRESS, + payout_address: None, + payouts: None, + pub_key_operator: PUB_KEY_OPERATOR.to_vec(), + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + }, + } + } + + fn masternode_list_diff( + added_mns: Vec, + removed_mns: Vec, + updated_mns: Vec<(ProTxHash, DMNStateDiff)>, + ) -> MasternodeListDiff { + MasternodeListDiff { + base_height: 0, + block_height: 1, + added_mns, + removed_mns, + updated_mns, + } + } + + /// A shared masternode has no owner address, so `create_owner_identity` gives it no owner + /// identity. Its voter and operator identities do not depend on the owner and are added as + /// for any masternode. + #[test] + fn should_add_the_voter_and_operator_identities_but_no_owner_identity_for_a_shared_masternode() + { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + platform + .update_masternode_identities( + masternode_list_diff(vec![shared_masternode()], vec![], vec![]), + &BTreeMap::new(), + &BlockInfo::default(), + None, + &transaction, + platform_version, + ) + .expect("expected to add the masternode identities"); + + let identity_exists = |identity_id: Identifier| { + platform + .drive + .fetch_full_identity( + identity_id.to_buffer(), + Some(&transaction), + platform_version, + ) + .expect("expected to fetch an identity") + .is_some() + }; + assert!( + !identity_exists(PRO_TX_HASH.into()), + "a shared masternode has no owner identity" + ); + assert!( + identity_exists(Identifier::create_voter_identifier( + &PRO_TX_HASH, + &VOTING_ADDRESS + )), + "the voter identity must be added" + ); + assert!( + identity_exists(Identifier::create_operator_identifier( + &PRO_TX_HASH, + &PUB_KEY_OPERATOR + )), + "the operator identity must be added" + ); + } + + /// Only a `payout_address` change reads the owner identity, and Core prints none for a + /// shared masternode. A change of its voting address and operator key, and its removal, + /// update its voter and operator identities as for any masternode and do not fail for want + /// of an owner identity. + #[test] + fn should_update_and_remove_a_shared_masternode_without_an_owner_identity() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + let masternode = shared_masternode(); + let pro_tx_hash = masternode.pro_tx_hash; + platform + .update_masternode_identities( + masternode_list_diff(vec![masternode.clone()], vec![], vec![]), + &BTreeMap::new(), + &BlockInfo::default(), + None, + &transaction, + platform_version, + ) + .expect("expected to add the masternode identities"); + + let mut platform_state = PlatformState::default_with_protocol_versions( + platform_version.protocol_version, + platform_version.protocol_version, + &PlatformConfig::default_for_network(Network::Testnet), + ) + .expect("platform state"); + platform_state.insert_masternode(masternode.clone()); + + let new_voting_address = [0x65; 20]; + let new_pub_key_operator = [0x66; 48]; + let state_diff = DMNStateDiff { + service: None, + registered_height: None, + last_paid_height: None, + consecutive_payments: None, + pose_penalty: None, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: None, + owner_address: None, + voting_address: Some(new_voting_address), + payout_address: None, + payouts: None, + pub_key_operator: Some(new_pub_key_operator.to_vec()), + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + }; + platform + .update_masternode_identities( + masternode_list_diff(vec![], vec![], vec![(pro_tx_hash, state_diff.clone())]), + &BTreeMap::new(), + &BlockInfo::default(), + Some(&platform_state), + &transaction, + platform_version, + ) + .expect("expected to update the identities of the shared masternode"); + + let mut updated_masternode = masternode; + updated_masternode.state.apply_diff(state_diff); + platform_state.insert_masternode(updated_masternode.clone()); + platform + .update_masternode_identities( + masternode_list_diff(vec![], vec![pro_tx_hash], vec![]), + &BTreeMap::from([(pro_tx_hash, updated_masternode)]), + &BlockInfo::default(), + Some(&platform_state), + &transaction, + platform_version, + ) + .expect("expected to remove the shared masternode"); + + let fetch_identity = |identity_id: Identifier| -> Option { + platform + .drive + .fetch_full_identity( + identity_id.to_buffer(), + Some(&transaction), + platform_version, + ) + .expect("expected to fetch an identity") + }; + let has_an_enabled_key = |identity_id: Identifier| { + fetch_identity(identity_id) + .expect("expected the identity to exist") + .public_keys() + .values() + .any(|key| !key.is_disabled()) + }; + assert_eq!(fetch_identity(PRO_TX_HASH.into()), None); + assert!( + !has_an_enabled_key(Identifier::create_voter_identifier( + &PRO_TX_HASH, + &new_voting_address + )), + "the removed masternode's voter identity keys must be disabled" + ); + assert!( + !has_an_enabled_key(Identifier::create_operator_identifier( + &PRO_TX_HASH, + &new_pub_key_operator + )), + "the removed masternode's operator identity keys must be disabled" + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs index 8494a69df19..1071fd1cf6c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs @@ -73,13 +73,27 @@ where mod test { use super::*; use crate::config::PlatformConfig; + use crate::platform_types::platform_state::platform_state_for_saving::v2::{ + deserialize_masternode_entry, serialize_masternode_entry, + }; + use crate::platform_types::platform_state::PlatformStateV0Methods; + use crate::platform_types::validator::v0::{NewValidatorIfMasternodeInState, ValidatorV0}; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::ProTxHash; use dpp::dashcore_rpc::json::MasternodeListDiff; + use dpp::identifier::MasternodeIdentifiers; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyID, Purpose}; + use dpp::prelude::Identifier; + use std::collections::BTreeMap; use std::env; use std::fs::File; use std::io::BufReader; use std::path::PathBuf; + use std::str::FromStr; #[test] fn test_update_masternode_list() { @@ -271,4 +285,239 @@ mod test { other => panic!("expected DashCoreBadResponseError, got {other:?}"), } } + + /// Dash Core v24 prints masternodes that differ from every earlier list: a shared + /// masternode has no owner, payout or collateral address; an extended-address masternode + /// has a `payouts` list instead of `payoutAddress`; a Tor primary address is not an + /// `ip:port` `service`; an Evo without addresses has `-1` platform ports. From protocol + /// version 14 the list is processed rather than failing the block, and the masternode + /// identities are: + /// - shared: voter and operator identities, no owner identity (it has no owner key); + /// - one payout: the same owner identity as a legacy payout address (TRANSFER key 0, + /// OWNER key 1); + /// - several payouts: an owner identity with only the OWNER key 1; + /// - a later payout change through `payouts`: no key change, since only a `payoutAddress` + /// change rotates the TRANSFER key. + #[test] + fn should_update_masternode_identities_from_a_core_v24_masternode_list() { + let platform_version = PlatformVersion::latest(); + + let platform = platform_with_core_v24_masternode_list(platform_version); + + let genesis_core_block_height = 1250; + let first_block_core_block_height = 1260; + + let transaction = platform.drive.grove.start_transaction(); + + let mut init_chain_platform_state = platform.state.load().as_ref().clone(); + + platform + .update_masternode_list( + None, + &mut init_chain_platform_state, + genesis_core_block_height, + true, + &BlockInfo { + height: 1, + core_height: genesis_core_block_height, + time_ms: 1, + ..Default::default() + }, + &transaction, + platform_version, + ) + .expect("expected to process the Core v24 masternode list"); + + let platform_state = init_chain_platform_state.clone(); + let mut block_platform_state = platform_state.clone(); + + platform + .update_masternode_list( + Some(&platform_state), + &mut block_platform_state, + first_block_core_block_height, + false, + &BlockInfo { + height: 1, + core_height: first_block_core_block_height, + time_ms: 2, + ..Default::default() + }, + &transaction, + platform_version, + ) + .expect("expected to process the Core v24 masternode list diff"); + + assert_eq!(block_platform_state.full_masternode_list().len(), 7); + assert_eq!(block_platform_state.hpmn_masternode_list().len(), 3); + + // An Evo without addresses prints `-1` platform ports, which read as no port, so it + // cannot be a validator, like any Evo without ports. The legacy Evo is one, with the + // ports and address the diff gave it. + let validator = |pro_tx_hash: &str| { + ValidatorV0::new_validator_if_masternode_in_state( + ProTxHash::from_str(pro_tx_hash).expect("expected a pro tx hash"), + None, + &block_platform_state, + ) + }; + assert!( + validator("6f1757595185032c808321af3e2e8468fae10b8f91e2a657d7a4c7122f4b2706").is_none() + ); + let legacy_evo_validator = + validator("ca3d32c4f2ef62eaf736bf41bb3235a832ec1eb6d8dfaccd4d3555e70f6757b0") + .expect("expected the legacy Evo to be a validator"); + assert_eq!(legacy_evo_validator.node_ip, "192.0.2.40"); + assert_eq!(legacy_evo_validator.platform_p2p_port, 36656); + assert_eq!(legacy_evo_validator.platform_http_port, 1443); + + // Every parsed masternode survives a store and reload, except for the payout list and + // nested addresses, which are not stored. + for masternode in block_platform_state.full_masternode_list().values() { + let bytes = serialize_masternode_entry(masternode, platform_version) + .expect("expected to serialize the masternode entry"); + let mut expected = masternode.clone(); + expected.state.payouts = None; + expected.state.addresses = None; + assert_eq!( + deserialize_masternode_entry(&bytes) + .expect("expected to deserialize the masternode entry"), + expected + ); + } + + let hash160 = |hex: &str| -> Vec { hex::decode(hex).expect("expected hex") }; + + // The keys of an identity, none of them disabled; `None` if there is no identity. + let identity_keys = + |identity_id: Identifier| -> Option)>> { + platform + .drive + .fetch_full_identity( + identity_id.to_buffer(), + Some(&transaction), + platform_version, + ) + .expect("expected to fetch an identity") + .map(|identity| { + identity + .public_keys() + .iter() + .map(|(key_id, key)| { + assert!(key.disabled_at().is_none(), "key {key_id} is disabled"); + (*key_id, (key.purpose(), key.data().to_vec())) + }) + .collect() + }) + }; + + let owner_key = |hex: &str| (Purpose::OWNER, hash160(hex)); + let transfer_key = |hex: &str| (Purpose::TRANSFER, hash160(hex)); + + let voting_421c = "421c03add2c804421451c4e022258778175e60d8"; + let voting_064c = "064cd21ff210c1a92adaea49a4768a41c2aef1bc"; + + // (pro_tx_hash, voting key hash, operator public key, owner identity keys) + let expected = [ + ( + // Legacy Evo whose payout address the diff moves into `payouts` + "ca3d32c4f2ef62eaf736bf41bb3235a832ec1eb6d8dfaccd4d3555e70f6757b0", + voting_421c, + "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563".to_string(), + Some(BTreeMap::from([ + (0, transfer_key("75d57974b6e29a4a70df57a3b11195ce0a0dc817")), + (1, owner_key("1f67d90f35e3c5070c368ae6f3635aac357e47df")), + ])), + ), + ( + // One payout, which the diff replaces with two + "27978dd892b7c876c238be1a6141461c2824f3497dd0058e160979bc8f0a0bef", + voting_421c, + "a792ce1af5f7bb9281053b3934cb8b08d00d075a56498e1a525388ce467f188e8a80911fd96a20982baa9b9678452534".to_string(), + Some(BTreeMap::from([ + (0, transfer_key("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")), + (1, owner_key("b740a2ab3f631e4dfe15debf746364f1d8352c50")), + ])), + ), + ( + // Shared + "a4d26868017c0ccffe2efe50944ef4211834660cca834c6e9f86dec6a88246fa", + voting_421c, + "b1".repeat(48), + None, + ), + ( + // One payout, which the diff replaces with another single payout + "813a7c3f28817988a8e6ce66e07e43e261e78398373bfbaae94c898645111d6b", + voting_421c, + "b2".repeat(48), + Some(BTreeMap::from([ + (0, transfer_key("5555555555555555555555555555555555555555")), + (1, owner_key("0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a")), + ])), + ), + ( + // Tor primary address, two payouts; the owner and voting keys are the same + "3023ffd989974768b0dfc347410ad923fa6d3f1eee90180bd0c435e81cb1a82f", + voting_064c, + "b3".repeat(48), + Some(BTreeMap::from([(1, owner_key(voting_064c))])), + ), + ( + // Evo without addresses, one payout + "6f1757595185032c808321af3e2e8468fae10b8f91e2a657d7a4c7122f4b2706", + voting_421c, + "b4".repeat(48), + Some(BTreeMap::from([ + (0, transfer_key("9999999999999999999999999999999999999999")), + (1, owner_key("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")), + ])), + ), + ( + // Evo with an IPv6 primary address, two payouts + "aecd2830b843e6a84283ba290492a213e355cea7c5026e6118a21e1bfbc36783", + voting_421c, + "b5".repeat(48), + Some(BTreeMap::from([( + 1, + owner_key("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"), + )])), + ), + ]; + + for (pro_tx_hash, voting_key_hash, operator_public_key, owner_identity_keys) in expected { + let pro_tx_hash = ProTxHash::from_str(pro_tx_hash) + .expect("expected a pro tx hash") + .to_byte_array(); + let voting_key_hash: [u8; 20] = hash160(voting_key_hash) + .try_into() + .expect("expected 20 bytes"); + let operator_public_key = hex::decode(operator_public_key).expect("expected hex"); + + assert_eq!( + identity_keys(pro_tx_hash.into()), + owner_identity_keys, + "owner identity of {}", + hex::encode(pro_tx_hash) + ); + assert!( + identity_keys(Identifier::create_voter_identifier( + &pro_tx_hash, + &voting_key_hash + )) + .is_some(), + "voter identity of {}", + hex::encode(pro_tx_hash) + ); + assert!( + identity_keys(Identifier::create_operator_identifier( + &pro_tx_hash, + &operator_public_key + )) + .is_some(), + "operator identity of {}", + hex::encode(pro_tx_hash) + ); + } + } } diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index 95493a875cf..9c84986f95a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -22,6 +22,10 @@ use crate::version::drive_abci_versions::drive_abci_method_versions::{ /// `decode_raw_state_transitions` 1 refuses bytes left over after a raw state transition. /// `add_distribute_storage_fee_to_epochs_operations` 1 claws each pending storage refund back /// from the epochs it was priced for. +/// `create_owner_identity` 2 and `update_masternode_identities` 1 decide the owner identity of +/// the masternodes Dash Core lists from v24 on, on which version 1 fails: none without an owner +/// address (a shared masternode), the version 1 identity for a payout list with one entry, and +/// only the OWNER key for several payouts. /// Everything else matches `DRIVE_ABCI_METHOD_VERSIONS_V9`. pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMethodVersions { engine: DriveAbciEngineMethodVersions { @@ -47,10 +51,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet get_voter_identifier_from_masternode_list_item: 0, get_operator_identifier_from_masternode_list_item: 0, create_operator_identity: 0, - create_owner_identity: 1, + create_owner_identity: 2, // changed in v14: no owner identity without an owner address; only the OWNER key for several payouts create_voter_identity: 0, disable_identity_keys: 0, - update_masternode_identities: 0, + update_masternode_identities: 1, // changed in v14: adds no owner identity where create_owner_identity gives none update_operator_identity: 0, update_owner_withdrawal_address: 1, update_voter_identity: 0, diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index adba75bd4c5..931b9c214af 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -1729,6 +1729,24 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// with `==`. Every step is inert without a derived index property, which only generation 3 /// declares. /// +/// 66. **Owner identities for shared and extended-address masternodes**: from +/// v24 on, Dash Core lists shared masternodes, which have no owner, payout +/// or collateral address, and extended-address masternodes, which have a +/// `payouts` list instead of a `payoutAddress`. `create_owner_identity` 1 +/// needs both addresses and fails on such a masternode with +/// `DashCoreBadResponseError`, which fails the block. With +/// `create_owner_identity` 2 and `update_masternode_identities` 1 +/// (`DRIVE_ABCI_METHOD_VERSIONS_V10`), a masternode without an owner +/// address gets no owner identity, only its voter and operator identities; +/// one with an owner address and a single payout address (the payout +/// address, else the only entry of a one-entry payout list) gets the +/// version 1 identity, TRANSFER key id 0 and OWNER key id 1, byte for byte; +/// and one paid to several payouts gets an owner identity with only the +/// OWNER key id 1. Updates are unchanged: only a `payoutAddress` change +/// rotates the TRANSFER key, and a `payouts` change is not acted on. This +/// version must be active on a network before its Dash Core activates V24, +/// since earlier versions keep failing on these masternodes. +/// /// The app-connect system contract (`SystemDataContract::AppConnect`, schema v1) /// carries only the wallet's `loginKeyResponse`: a flat indexOnly entry keyed by /// the app's ephemeral key hash and the responding identity, with the wallet's From 279687b1b3c383c7b0a3646f3836f9de028006d7 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 4 Oct 2026 14:01:23 +0700 Subject: [PATCH 09/11] test(drive-abci): verify owner-key transition at protocol version 4 Pin the historical transition regression and its initial storage to PV4 so the successful OWNER-key insertion checks the protocol that first shipped it. The targeted test passes. Test-only change; no production behavior delta. --- .../perform_events_on_first_block_of_protocol_change/v0/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 76eacdf65a3..26b3fbc38c2 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -1058,8 +1058,9 @@ mod tests { use dpp::identity::{Identity, KeyID, Purpose}; use std::collections::BTreeMap; - let platform_version = PlatformVersion::latest(); + let platform_version = PlatformVersion::get(4).expect("expected protocol version 4"); let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); From 0084f1af0a5ddc875f6411bef0a3e3ce44967a55 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 5 Oct 2026 01:40:22 +0700 Subject: [PATCH 10/11] fix(drive-abci): revoke obsolete Core payout withdrawal keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconcile Core payout-list changes in the unreleased PV14 updater. Retain only the supported sole P2PKH TRANSFER authority, preserving OWNER and credits. Match key purpose/type/data, reuse keys deterministically, check allocation overflow, and reject mixed payout representations. Apply the same supported-script rule to PV14 creation; keep shipped generations and the legacy rotation helper unchanged. Test would have caught this in CI: ✖ before fix, ✔ after. Correctly signed obsolete-key withdrawals executed successfully before the fix after both recipient replacement and a split. The same node-side regressions now reject the old key and protect subsequently added credits, without debiting withdrawal funds or creating documents. Validation: 163 masternode tests passed (1 existing ignored), 202 withdrawal tests passed (none skipped), formatting and all-target/all-feature Clippy passed. Independent security, architecture and test reviews found no remaining blockers. Default-destination history and pre-PV14 state repair remain explicitly separate limitations. --- .../create_owner_identity/v2/mod.rs | 46 +- .../update_masternode_identities/mod.rs | 13 +- .../update_masternode_identities/v1/mod.rs | 137 +++- .../v1/payout_tests.rs | 701 ++++++++++++++++++ .../update_masternode_list/mod.rs | 23 +- 5 files changed, 888 insertions(+), 32 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/payout_tests.rs diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v2/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v2/mod.rs index f3df1718813..aea2c9ec61f 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v2/mod.rs @@ -1,6 +1,8 @@ use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::{PubkeyHash, ScriptBuf}; use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem}; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::Identity; @@ -23,7 +25,7 @@ where /// voter and operator identities of the masternode do not depend on it; /// - an owner address and a single payout address (see `effective_payout_address`): the /// identity of version 1, TRANSFER key id 0 and OWNER key id 1, byte for byte; - /// - an owner address and several payouts: an identity with only the OWNER key id 1, as no + /// - an owner address and no sole supported P2PKH payout: only the OWNER key id 1, as no /// single address can hold the TRANSFER key; key id 0 stays free. pub(super) fn create_owner_identity_v2( masternode: &MasternodeListItem, @@ -63,16 +65,21 @@ where /// The one address the owner reward of a masternode is paid to, which its owner identity's /// TRANSFER key holds: the payout address, or the only entry of a payout list with one entry -/// (Core turns a payout address into such a list when a masternode moves to extended -/// addresses). `None` for a shared masternode, which has neither, and for a reward split -/// between several payouts. +/// with a matching P2PKH script (Core turns a payout address into such a list when a +/// masternode moves to extended addresses). Other scripts and reward splits have no single +/// supported TRANSFER authority. /// /// The stored masternode list does not keep payout lists, so this is only meaningful for a /// masternode as Core reported it, never for one read from the stored list. fn effective_payout_address(state: &DMNState) -> Option<[u8; 20]> { match (state.payout_address, state.payouts.as_deref()) { (Some(payout_address), _) => Some(payout_address), - (None, Some([payout])) => Some(payout.address), + (None, Some([payout])) + if payout.script + == ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(payout.address)) => + { + Some(payout.address) + } _ => None, } } @@ -82,7 +89,7 @@ mod tests { use crate::platform_types::platform::Platform; use crate::rpc::core::MockCoreRPCLike; use dpp::dashcore::hashes::Hash; - use dpp::dashcore::{ProTxHash, PubkeyHash, ScriptBuf, Txid}; + use dpp::dashcore::{ProTxHash, PubkeyHash, ScriptBuf, ScriptHash, Txid}; use dpp::dashcore_rpc::dashcore_rpc_json::{ DMNPayout, DMNState, MasternodeListItem, MasternodeType, }; @@ -146,6 +153,33 @@ mod tests { masternode(Some(OWNER_ADDRESS), Some(PAYOUT_ADDRESS), None) } + #[test] + fn should_not_grant_transfer_authority_to_unsupported_or_inconsistent_payout_scripts() { + for script in [ + ScriptBuf::new_p2sh(&ScriptHash::from_byte_array(PAYOUT_ADDRESS)), + ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(SECOND_PAYOUT_ADDRESS)), + ] { + let identity = create_owner_identity( + &masternode( + Some(OWNER_ADDRESS), + None, + Some(vec![DMNPayout { + address: PAYOUT_ADDRESS, + script, + reward: 10000, + }]), + ), + PlatformVersion::latest(), + ) + .expect("owner identity"); + assert_eq!(identity.public_keys().len(), 1); + assert_eq!( + identity.public_keys().get(&1).expect("owner key").purpose(), + Purpose::OWNER + ); + } + } + fn create_owner_identity( masternode: &MasternodeListItem, platform_version: &PlatformVersion, diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs index 69ce72d94fa..ecaf08fc676 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/mod.rs @@ -81,18 +81,13 @@ mod tests { use std::collections::BTreeMap; /// A payout change of an extended-address masternode reaches Drive only as `payouts`, - /// never as `payout_address`. Both generations rotate the owner identity's TRANSFER key - /// only on a `payout_address` change and leave the owner identity's keys as they are on a - /// `payouts` change. Protocol version 13 is pinned because a node running it against Dash - /// Core v24 receives such diffs. + /// never as `payout_address`. The historical updater leaves these keys unchanged. + /// Protocol version 13 is pinned to preserve replay of its payout-list policy. #[test] fn should_not_change_owner_identity_keys_on_a_payout_list_change() { - for platform_version in [ + assert_owner_identity_keys_unchanged_by_payout_list_changes( PlatformVersion::get(13).expect("expected protocol version 13"), - PlatformVersion::latest(), - ] { - assert_owner_identity_keys_unchanged_by_payout_list_changes(platform_version); - } + ); } fn assert_owner_identity_keys_unchanged_by_payout_list_changes( diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/mod.rs index 72d61c97c0f..58380396215 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/mod.rs @@ -1,17 +1,26 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; -use dpp::dashcore::ProTxHash; -use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeListDiff; +use dpp::dashcore::{ProTxHash, PubkeyHash, ScriptBuf}; +use dpp::dashcore_rpc::dashcore_rpc_json::{DMNPayout, MasternodeListDiff}; use dpp::dashcore_rpc::json::MasternodeListItem; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::{KeyType, Purpose}; use dpp::version::PlatformVersion; +use drive::drive::identity::key::fetch::{ + IdentityKeysRequest, KeyIDIdentityPublicKeyPairBTreeMap, KeyRequestType, +}; +use drive::util::batch::DriveOperation; use drive::util::batch::DriveOperation::IdentityOperation; -use drive::util::batch::IdentityOperationType::AddNewIdentity; +use drive::util::batch::IdentityOperationType::{ + AddNewIdentity, AddNewKeysToIdentity, DisableIdentityKeys, ReEnableIdentityKeys, +}; use crate::platform_types::platform_state::PlatformStateV0Methods; @@ -30,9 +39,9 @@ where /// Version 0 is paired with `create_owner_identity` 0 and 1, which create an owner identity /// for every masternode or fail. `create_owner_identity` 2 creates none for a masternode /// without an owner address, such as a shared masternode, and this version then adds only - /// its voter and operator identities. Updates and removals are processed exactly as in - /// version 0: only a `payout_address` change rotates the owner identity's TRANSFER key, and - /// a `payouts` change is not acted on. + /// its voter and operator identities. Legacy payout-address changes and removals follow + /// version 0. Payout-list changes reconcile TRANSFER keys: only a sole supported P2PKH + /// recipient retains withdrawal authority. pub(super) fn update_masternode_identities_v1( &self, masternode_diff: MasternodeListDiff, @@ -59,6 +68,16 @@ where // Sort updated_mns based on pro_tx_hash (the first element of the tuple) updated_mns.sort_by_key(|mn| mn.0); + if updated_mns + .iter() + .any(|(_, diff)| diff.payout_address.is_some() && diff.payouts.is_some()) + { + return Err(ExecutionError::DashCoreBadResponseError( + "masternode diff contains both a payout address and a payout list".to_string(), + ) + .into()); + } + let mut drive_operations = vec![]; for masternode in added_mns { @@ -122,7 +141,15 @@ where &mut drive_operations, platform_version, )?; - }; + } else if let Some(payouts) = state_diff.payouts.as_deref() { + self.reconcile_owner_payout_keys( + pro_tx_hash.to_byte_array(), + payouts, + transaction, + &mut drive_operations, + platform_version, + )?; + } self.update_voter_identity( update, @@ -175,8 +202,104 @@ where Ok(()) } + + /// A sole P2PKH recipient can spend the owner's balance. Other payout shapes must + /// retire that authority without changing the OWNER key or the accumulated credits. + fn reconcile_owner_payout_keys( + &self, + owner_identifier: [u8; 32], + payouts: &[DMNPayout], + transaction: &Transaction, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let authorized_address = match payouts { + [payout] + if payout.script + == ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(payout.address)) => + { + Some(payout.address) + } + _ => None, + }; + let keys = self + .drive + .fetch_identity_keys::( + IdentityKeysRequest { + identity_id: owner_identifier, + request_type: KeyRequestType::AllKeys, + limit: None, + offset: None, + }, + Some(transaction), + platform_version, + )?; + let Some((&last_key_id, _)) = keys.last_key_value() else { + return Err(ExecutionError::DriveMissingData( + "expected masternode owner identity to be in state".to_string(), + ) + .into()); + }; + // Prefer an enabled matching key, then a disabled one; numeric IDs break ties. + let reusable_key = authorized_address.and_then(|address| { + keys.iter() + .filter(|(_, key)| { + key.purpose() == Purpose::TRANSFER + && key.key_type() == KeyType::ECDSA_HASH160 + && key.data().as_slice() == address + }) + .min_by_key(|(key_id, key)| (key.is_disabled(), **key_id)) + }); + let new_key = match (authorized_address, reusable_key) { + (Some(address), None) => { + let key_id = last_key_id.checked_add(1).ok_or(ExecutionError::Overflow( + "masternode owner identity key id exhausted", + ))?; + Some(Self::get_owner_identity_withdrawal_key( + address, + key_id, + platform_version, + )?) + } + _ => None, + }; + let keys_ids = keys + .iter() + .filter_map(|(key_id, key)| { + (key.purpose() == Purpose::TRANSFER + && !key.is_disabled() + && reusable_key.map(|(reused_id, _)| reused_id) != Some(key_id)) + .then_some(*key_id) + }) + .collect::>(); + if !keys_ids.is_empty() { + drive_operations.push(IdentityOperation(DisableIdentityKeys { + identity_id: owner_identifier, + keys_ids, + })); + } + if let Some((key_id, key)) = reusable_key { + if key.is_disabled() { + drive_operations.push(IdentityOperation(ReEnableIdentityKeys { + identity_id: owner_identifier, + keys_ids: vec![*key_id], + })); + } + } + if let Some(key) = new_key { + drive_operations.push(IdentityOperation(AddNewKeysToIdentity { + identity_id: owner_identifier, + unique_keys_to_add: vec![], + non_unique_keys_to_add: vec![key], + })); + } + Ok(()) + } } +#[cfg(test)] +mod payout_tests; + #[cfg(test)] mod tests { use crate::config::PlatformConfig; diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/payout_tests.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/payout_tests.rs new file mode 100644 index 00000000000..0cccb5337a5 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/payout_tests.rs @@ -0,0 +1,701 @@ +use crate::config::{PlatformConfig, PlatformTestConfig}; +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; +use crate::rpc::core::MockCoreRPCLike; +use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; +use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; +use assert_matches::assert_matches; +use dpp::block::block_info::BlockInfo; +use dpp::consensus::signature::SignatureError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::ConsensusError; +use dpp::dash_to_credits; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::{ProTxHash, PubkeyHash, ScriptBuf, ScriptHash, Txid}; +use dpp::dashcore_rpc::dashcore_rpc_json::{ + DMNPayout, DMNState, DMNStateDiff, MasternodeListDiff, MasternodeListItem, MasternodeType, +}; +use dpp::document::DocumentV0Getters; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::identity::core_script::CoreScript; +use dpp::identity::hash::IdentityPublicKeyHashMethodsV0; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::{Identity, IdentityPublicKey, KeyID, Purpose}; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use dpp::serialization::PlatformSerializable; +use dpp::state_transition::identity_credit_withdrawal_transition::methods::{ + IdentityCreditWithdrawalTransitionMethodsV0, PreferredKeyPurposeForSigningWithdrawal, +}; +use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; +use dpp::version::PlatformVersion; +use dpp::withdrawal::Pooling; +use drive::drive::identity::key::fetch::{ + IdentityKeysRequest, KeyIDIdentityPublicKeyPairBTreeMap, KeyRequestType, +}; +use drive::grovedb::Transaction; +use drive::util::batch::{DriveOperation, IdentityOperationType}; +use rand::prelude::StdRng; +use rand::SeedableRng; +use simple_signer::signer::SimpleSigner; +use std::collections::BTreeMap; +use std::sync::Arc; + +const PRO_TX_HASH: [u8; 32] = [0x71; 32]; +const UPDATE_TIME: u64 = 1_200_002_000; + +fn payout(address: [u8; 20], reward: u16) -> DMNPayout { + DMNPayout { + address, + script: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(address)), + reward, + } +} + +fn empty_diff() -> DMNStateDiff { + serde_json::from_str("{}").expect("empty Core state diff") +} + +struct PayoutFixture { + platform: TempPlatform, + signer: SimpleSigner, + original_identity: Identity, + original_key: IdentityPublicKey, +} + +impl PayoutFixture { + fn new() -> Self { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .with_config(PlatformConfig { + testing_configs: PlatformTestConfig { + disable_instant_lock_signature_verification: true, + ..Default::default() + }, + ..Default::default() + }) + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + fast_forward_to_block(&platform, 1_200_000_000, 900, 42, 1, false); + + let mut rng = StdRng::seed_from_u64(713); + let (original_key, original_secret) = + IdentityPublicKey::random_masternode_transfer_key_with_rng( + 0, + &mut rng, + platform_version, + ) + .expect("payout key pair"); + let (owner_key, owner_secret) = + IdentityPublicKey::random_masternode_owner_key_with_rng(1, &mut rng, platform_version) + .expect("owner key pair"); + let mut signer = SimpleSigner::default(); + signer.add_identity_public_key(original_key.clone(), original_secret); + signer.add_identity_public_key(owner_key.clone(), owner_secret); + let masternode = MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array(PRO_TX_HASH), + collateral_hash: Txid::from_byte_array([0x72; 32]), + collateral_index: 0, + collateral_address: Some([0x73; 20]), + operator_reward: 0.0, + state: DMNState { + service: "1.2.3.4:9999".parse().expect("service address"), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address: Some(owner_key.public_key_hash().expect("owner hash")), + voting_address: [0x74; 20], + payout_address: None, + payouts: Some(vec![payout( + original_key.public_key_hash().expect("payout hash"), + 10000, + )]), + pub_key_operator: vec![0x75; 48], + operator_payout_address: None, + platform_node_id: None, + #[allow(deprecated)] + legacy_platform_p2p_port: None, + #[allow(deprecated)] + legacy_platform_http_port: None, + addresses: None, + }, + }; + let transaction = platform.drive.grove.start_transaction(); + platform + .update_masternode_identities( + MasternodeListDiff { + base_height: 0, + block_height: 1, + added_mns: vec![masternode.clone()], + removed_mns: vec![], + updated_mns: vec![], + }, + &BTreeMap::new(), + &BlockInfo::default_with_time(1_200_001_000), + None, + &transaction, + platform_version, + ) + .expect("register sole-payout masternode"); + let credits = dash_to_credits!(0.5); + platform + .drive + .add_to_system_credits(credits, Some(&transaction), platform_version) + .expect("system credits"); + platform + .drive + .add_to_identity_balance( + PRO_TX_HASH, + credits, + &BlockInfo::default(), + true, + Some(&transaction), + platform_version, + ) + .expect("fund payout identity"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit registration"); + let mut state = platform.state.load().as_ref().clone(); + state.insert_masternode(masternode); + platform.state.store(Arc::new(state)); + let original_identity = platform + .drive + .fetch_full_identity(PRO_TX_HASH, None, platform_version) + .expect("fetch registered identity") + .expect("registered owner identity"); + Self { + platform, + signer, + original_identity, + original_key, + } + } + + fn identity(&self, transaction: &Transaction) -> Identity { + self.platform + .drive + .fetch_full_identity(PRO_TX_HASH, Some(transaction), PlatformVersion::latest()) + .expect("fetch owner identity") + .expect("owner identity") + } + + fn update(&self, payouts: Vec, transaction: &Transaction) { + self.apply_diff( + DMNStateDiff { + payouts: Some(payouts), + ..empty_diff() + }, + transaction, + ) + .expect("apply payout list update"); + } + + fn apply_diff(&self, diff: DMNStateDiff, transaction: &Transaction) -> Result<(), Error> { + self.platform.update_masternode_identities( + MasternodeListDiff { + base_height: 1, + block_height: 2, + added_mns: vec![], + removed_mns: vec![], + updated_mns: vec![(ProTxHash::from_byte_array(PRO_TX_HASH), diff)], + }, + &BTreeMap::new(), + &BlockInfo::default_with_time(UPDATE_TIME), + Some(&self.platform.state.load()), + transaction, + PlatformVersion::latest(), + ) + } + + fn add_later_credits(&self, transaction: &Transaction) { + let credits = dash_to_credits!(0.2); + let version = PlatformVersion::latest(); + self.platform + .drive + .add_to_system_credits(credits, Some(transaction), version) + .expect("later system credits"); + self.platform + .drive + .add_to_identity_balance( + PRO_TX_HASH, + credits, + &BlockInfo::default_with_time(UPDATE_TIME), + true, + Some(transaction), + version, + ) + .expect("later payout credits"); + } + + async fn withdraw( + &self, + identity: &Identity, + key: &IdentityPublicKey, + nonce: u64, + explicit_destination: bool, + transaction: &Transaction<'_>, + ) -> Vec { + let transition = IdentityCreditWithdrawalTransition::try_from_identity( + identity, + explicit_destination.then(|| CoreScript::random_p2pkh(&mut StdRng::seed_from_u64(714))), + dash_to_credits!(0.1), + Pooling::Never, + 1, + 0, + self.signer.clone(), + Some(key), + PreferredKeyPurposeForSigningWithdrawal::Any, + nonce, + PlatformVersion::latest(), + None, + ) + .await + .expect("sign withdrawal with the supplied enabled-key snapshot"); + self.platform + .process_raw_state_transitions( + &vec![transition + .serialize_to_bytes() + .expect("serialize withdrawal")], + &self.platform.state.load(), + &BlockInfo::default_with_time(UPDATE_TIME + 1), + transaction, + PlatformVersion::latest(), + false, + None, + ) + .expect("process signed withdrawal") + .into_execution_results() + } +} + +#[tokio::test] +async fn should_allow_replacement_payout_and_preserve_owner_default_withdrawals() { + let mut fixture = PayoutFixture::new(); + let transaction = fixture.platform.drive.grove.start_transaction(); + let (key, secret) = IdentityPublicKey::random_masternode_transfer_key_with_rng( + 2, + &mut StdRng::seed_from_u64(715), + PlatformVersion::latest(), + ) + .expect("replacement key pair"); + let address = key.public_key_hash().expect("replacement hash"); + fixture.update(vec![payout(address, 10000)], &transaction); + fixture.add_later_credits(&transaction); + let identity = fixture.identity(&transaction); + let replacement = identity + .public_keys() + .get(&2) + .expect("replacement transfer key"); + fixture + .signer + .add_identity_public_key(replacement.clone(), secret); + assert_matches!( + fixture + .withdraw(&identity, replacement, 1, true, &transaction) + .await + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let owner = identity.public_keys().get(&1).expect("owner key"); + assert_matches!( + fixture + .withdraw(&identity, owner, 2, false, &transaction) + .await + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let documents = fixture + .platform + .drive + .fetch_oldest_withdrawal_documents(Some(&transaction), PlatformVersion::latest()) + .expect("withdrawal documents"); + assert!(documents.values().flatten().any(|document| document + .properties() + .get_bytes("outputScript") + .expect("output script") + == CoreScript::new_p2pkh(address).to_bytes())); + let before_repeat = fixture.identity(&transaction); + fixture.update(vec![payout(address, 10000)], &transaction); + assert_eq!(fixture.identity(&transaction), before_repeat); +} + +#[tokio::test] +async fn should_restore_single_payout_authority_without_duplicate_keys() { + let fixture = PayoutFixture::new(); + let transaction = fixture.platform.drive.grove.start_transaction(); + let original_address = fixture + .original_key + .public_key_hash() + .expect("original hash"); + fixture.update(vec![payout([0x76; 20], 10000)], &transaction); + let replacement_identity = fixture.identity(&transaction); + fixture.update(vec![], &transaction); + fixture.update(vec![payout(original_address, 10000)], &transaction); + let restored = fixture.identity(&transaction); + assert_eq!(restored.public_keys().len(), 3); + assert_eq!(restored.public_keys().get(&0), Some(&fixture.original_key)); + assert!(restored + .public_keys() + .get(&2) + .expect("replacement key") + .is_disabled()); + assert_matches!( + fixture + .withdraw(&restored, &fixture.original_key, 1, true, &transaction) + .await + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!( + restored.public_keys().get(&1), + replacement_identity.public_keys().get(&1) + ); +} + +#[tokio::test] +async fn should_revoke_p2pkh_authority_for_unsupported_or_inconsistent_payout_scripts() { + let fixture = PayoutFixture::new(); + let transaction = fixture.platform.drive.grove.start_transaction(); + let address = fixture.original_key.public_key_hash().expect("payout hash"); + for script in [ + ScriptBuf::new_p2sh(&ScriptHash::from_byte_array(address)), + ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array([0x76; 20])), + ] { + fixture.update( + vec![DMNPayout { + address, + script, + reward: 10000, + }], + &transaction, + ); + assert_disabled_key( + &fixture + .withdraw( + &fixture.original_identity, + &fixture.original_key, + 1, + true, + &transaction, + ) + .await, + 0, + ); + fixture.update(vec![payout(address, 10000)], &transaction); + assert_eq!( + fixture.identity(&transaction).public_keys(), + fixture.original_identity.public_keys() + ); + } +} + +#[tokio::test] +async fn should_reuse_old_payout_keys_after_more_than_six_rotations_with_an_explicit_destination() { + let fixture = PayoutFixture::new(); + let transaction = fixture.platform.drive.grove.start_transaction(); + for address in 0x80..0x88 { + fixture.update(vec![payout([address; 20], 10000)], &transaction); + } + fixture.update( + vec![payout( + fixture.original_key.public_key_hash().expect("old hash"), + 10000, + )], + &transaction, + ); + let restored = fixture.identity(&transaction); + assert_eq!(restored.public_keys().len(), 10); + assert_eq!( + restored + .public_keys() + .values() + .filter(|key| key.purpose() == Purpose::TRANSFER && !key.is_disabled()) + .count(), + 1 + ); + assert_matches!( + fixture + .withdraw(&restored, &fixture.original_key, 1, true, &transaction) + .await + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + // The inherited default-destination lookup is bounded; revocation does not widen it. + assert_matches!( + fixture + .withdraw( + &restored, + restored.public_keys().get(&1).expect("owner key"), + 2, + false, + &transaction + ) + .await + .as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::IdentityPublicKeyIsDisabledError(error)) + )] if error.public_key_index() == 9 + ); +} + +fn recent_keys( + fixture: &PayoutFixture, + transaction: &Transaction, +) -> KeyIDIdentityPublicKeyPairBTreeMap { + fixture + .platform + .drive + .fetch_identity_keys( + IdentityKeysRequest { + identity_id: PRO_TX_HASH, + request_type: KeyRequestType::RecentWithdrawalKeys, + limit: None, + offset: None, + }, + Some(transaction), + PlatformVersion::latest(), + ) + .expect("withdrawal index keys") +} + +#[test] +fn should_rollback_payout_authority_and_retry_without_index_divergence() { + let fixture = PayoutFixture::new(); + let candidate = fixture.platform.drive.grove.start_transaction(); + let initial_recent = recent_keys(&fixture, &candidate); + fixture.update(vec![payout([0x76; 20], 10000)], &candidate); + let accepted_identity = fixture.identity(&candidate); + let accepted_recent = recent_keys(&fixture, &candidate); + assert!(accepted_recent + .get(&0) + .expect("old indexed key") + .is_disabled()); + assert!(!accepted_recent + .get(&2) + .expect("new indexed key") + .is_disabled()); + assert_eq!( + accepted_identity.balance(), + fixture.original_identity.balance() + ); + fixture + .platform + .drive + .grove + .rollback_transaction(&candidate) + .expect("rollback candidate"); + assert_eq!(fixture.identity(&candidate), fixture.original_identity); + assert_eq!(recent_keys(&fixture, &candidate), initial_recent); + drop(candidate); + let retry = fixture.platform.drive.grove.start_transaction(); + fixture.update(vec![payout([0x76; 20], 10000)], &retry); + assert_eq!(fixture.identity(&retry), accepted_identity); + assert_eq!(recent_keys(&fixture, &retry), accepted_recent); +} + +#[test] +fn should_reject_mixed_payout_representations_without_changing_identity() { + let fixture = PayoutFixture::new(); + let transaction = fixture.platform.drive.grove.start_transaction(); + assert_matches!( + fixture.apply_diff( + DMNStateDiff { + payout_address: Some([0x76; 20]), + payouts: Some(vec![payout([0x77; 20], 10000)]), + ..empty_diff() + }, + &transaction + ), + Err(Error::Execution(ExecutionError::DashCoreBadResponseError( + _ + ))) + ); + assert_eq!(fixture.identity(&transaction), fixture.original_identity); +} + +#[test] +fn should_not_reuse_owner_keys_as_transfer_keys_even_if_the_hash_matches() { + let fixture = PayoutFixture::new(); + let transaction = fixture.platform.drive.grove.start_transaction(); + // Core rejects this owner/payout collision; purpose matching is defensive for malformed data. + let owner_hash = fixture + .original_identity + .public_keys() + .get(&1) + .expect("owner key") + .public_key_hash() + .expect("owner hash"); + fixture.update(vec![payout(owner_hash, 10000)], &transaction); + let identity = fixture.identity(&transaction); + assert_eq!( + identity.public_keys().get(&1), + fixture.original_identity.public_keys().get(&1) + ); + assert_eq!( + identity + .public_keys() + .get(&2) + .expect("new payout key") + .purpose(), + Purpose::TRANSFER + ); +} + +#[test] +fn should_fail_new_key_allocation_at_exhaustion_but_allow_reuse_and_revocation() { + let fixture = PayoutFixture::new(); + let transaction = fixture.platform.drive.grove.start_transaction(); + let (maximum_key, _) = IdentityPublicKey::random_masternode_transfer_key_with_rng( + KeyID::MAX, + &mut StdRng::seed_from_u64(716), + PlatformVersion::latest(), + ) + .expect("maximum-id key"); + fixture + .platform + .drive + .apply_drive_operations( + vec![DriveOperation::IdentityOperation( + IdentityOperationType::AddNewKeysToIdentity { + identity_id: PRO_TX_HASH, + unique_keys_to_add: vec![], + non_unique_keys_to_add: vec![maximum_key], + }, + )], + true, + &BlockInfo::default(), + Some(&transaction), + PlatformVersion::latest(), + None, + ) + .expect("add maximum key id"); + let before = fixture.identity(&transaction); + let mut operations = vec![]; + assert_matches!( + fixture.platform.reconcile_owner_payout_keys( + PRO_TX_HASH, + &[payout([0x76; 20], 10000)], + &transaction, + &mut operations, + PlatformVersion::latest() + ), + Err(Error::Execution(ExecutionError::Overflow(_))) + ); + assert!(operations.is_empty()); + assert_eq!(fixture.identity(&transaction), before); + fixture.update( + vec![payout( + fixture + .original_key + .public_key_hash() + .expect("original hash"), + 10000, + )], + &transaction, + ); + assert!(!fixture + .identity(&transaction) + .public_keys() + .get(&0) + .expect("original key") + .is_disabled()); + fixture.update(vec![], &transaction); + assert!(fixture + .identity(&transaction) + .public_keys() + .values() + .filter(|key| key.purpose() == Purpose::TRANSFER) + .all(|key| key.is_disabled())); +} + +fn assert_disabled_key(results: &[StateTransitionExecutionResult], expected_id: u32) { + assert_matches!(results, [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::SignatureError(SignatureError::PublicKeyIsDisabledError(error)) + )] if error.public_key_id() == expected_id); +} + +async fn rejects_obsolete_payout(split: bool) { + let fixture = PayoutFixture::new(); + let transaction = fixture.platform.drive.grove.start_transaction(); + assert_matches!( + fixture + .withdraw( + &fixture.original_identity, + &fixture.original_key, + 1, + true, + &transaction + ) + .await + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let old_address = fixture + .original_key + .public_key_hash() + .expect("old payout hash"); + let payouts = if split { + vec![payout(old_address, 5000), payout([0x76; 20], 5000)] + } else { + vec![payout([0x76; 20], 10000)] + }; + fixture.update(payouts, &transaction); + fixture.add_later_credits(&transaction); + let balance_before = fixture.identity(&transaction).balance(); + let documents_before = fixture + .platform + .drive + .fetch_oldest_withdrawal_documents(Some(&transaction), PlatformVersion::latest()) + .expect("withdrawal documents before rejection"); + // An attacker retains the old enabled key and can still sign. Only node-side validation + // of the updated stored identity can remove that key's authority over later rewards. + let results = fixture + .withdraw( + &fixture.original_identity, + &fixture.original_key, + 2, + true, + &transaction, + ) + .await; + assert_disabled_key(&results, 0); + if split { + assert!(fixture + .identity(&transaction) + .public_keys() + .values() + .filter(|key| key.purpose() == Purpose::TRANSFER) + .all(|key| key.is_disabled())); + } + assert_eq!(fixture.identity(&transaction).balance(), balance_before); + assert_eq!( + fixture + .platform + .drive + .fetch_oldest_withdrawal_documents(Some(&transaction), PlatformVersion::latest()) + .expect("withdrawal documents after rejection"), + documents_before + ); + assert_eq!( + fixture.identity(&transaction).public_keys().get(&1), + fixture.original_identity.public_keys().get(&1) + ); +} + +#[tokio::test] +async fn should_reject_obsolete_payout_key_withdrawals_after_recipient_replacement() { + rejects_obsolete_payout(false).await; +} + +#[tokio::test] +async fn should_reject_withdrawals_by_a_recipient_remaining_in_a_split() { + rejects_obsolete_payout(true).await; +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs index 1071fd1cf6c..5ba3ac81174 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs @@ -296,8 +296,8 @@ mod test { /// - one payout: the same owner identity as a legacy payout address (TRANSFER key 0, /// OWNER key 1); /// - several payouts: an owner identity with only the OWNER key 1; - /// - a later payout change through `payouts`: no key change, since only a `payoutAddress` - /// change rotates the TRANSFER key. + /// - a later payout-list change: revoke obsolete keys and authorize only a sole P2PKH + /// recipient, preserving the OWNER key. #[test] fn should_update_masternode_identities_from_a_core_v24_masternode_list() { let platform_version = PlatformVersion::latest(); @@ -388,9 +388,9 @@ mod test { let hash160 = |hex: &str| -> Vec { hex::decode(hex).expect("expected hex") }; - // The keys of an identity, none of them disabled; `None` if there is no identity. + // The keys and disabled state of an identity; `None` if there is no identity. let identity_keys = - |identity_id: Identifier| -> Option)>> { + |identity_id: Identifier| -> Option, bool)>> { platform .drive .fetch_full_identity( @@ -404,15 +404,17 @@ mod test { .public_keys() .iter() .map(|(key_id, key)| { - assert!(key.disabled_at().is_none(), "key {key_id} is disabled"); - (*key_id, (key.purpose(), key.data().to_vec())) + ( + *key_id, + (key.purpose(), key.data().to_vec(), key.is_disabled()), + ) }) .collect() }) }; - let owner_key = |hex: &str| (Purpose::OWNER, hash160(hex)); - let transfer_key = |hex: &str| (Purpose::TRANSFER, hash160(hex)); + let owner_key = |hex: &str| (Purpose::OWNER, hash160(hex), false); + let transfer_key = |hex: &str| (Purpose::TRANSFER, hash160(hex), false); let voting_421c = "421c03add2c804421451c4e022258778175e60d8"; let voting_064c = "064cd21ff210c1a92adaea49a4768a41c2aef1bc"; @@ -435,7 +437,7 @@ mod test { voting_421c, "a792ce1af5f7bb9281053b3934cb8b08d00d075a56498e1a525388ce467f188e8a80911fd96a20982baa9b9678452534".to_string(), Some(BTreeMap::from([ - (0, transfer_key("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")), + (0, (Purpose::TRANSFER, hash160("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), true)), (1, owner_key("b740a2ab3f631e4dfe15debf746364f1d8352c50")), ])), ), @@ -452,7 +454,8 @@ mod test { voting_421c, "b2".repeat(48), Some(BTreeMap::from([ - (0, transfer_key("5555555555555555555555555555555555555555")), + (0, (Purpose::TRANSFER, hash160("5555555555555555555555555555555555555555"), true)), + (2, transfer_key("f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1")), (1, owner_key("0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a")), ])), ), From bc2a201c6a52ba11c74fd1e4e60e993a4b71632d Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 5 Oct 2026 02:20:22 +0700 Subject: [PATCH 11/11] test(drive-abci): cover first payout key for owner-only identities Register a multi-payout masternode with only OWNER key 1, then apply a sole P2PKH payout through the dispatcher. Verify allocation of TRANSFER key 2, its withdrawal-index reference, unchanged OWNER and balance, and a genuinely signed withdrawal. Preserve the original sole-payout fixtures. Correct the permanent PV14 and V10 protocol references to describe supported scripts and payout-list reconciliation. No production behavior or table values change; the regression covers behavior already fixed in 0084f1af0a. Validation: 164 masternode tests passed (1 existing ignored), formatting and all-target/all-feature drive-abci Clippy passed. Independent test and protocol documentation reviews found no remaining issues. --- .../v1/payout_tests.rs | 77 ++++++++++++++++++- .../drive_abci_method_versions/v10.rs | 12 ++- .../rs-platform-version/src/version/v14.rs | 14 ++-- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/payout_tests.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/payout_tests.rs index 0cccb5337a5..846bcdb94ab 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/payout_tests.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_masternode_identities/v1/payout_tests.rs @@ -66,6 +66,10 @@ struct PayoutFixture { impl PayoutFixture { fn new() -> Self { + Self::with_split_payout(false) + } + + fn with_split_payout(initial_split: bool) -> Self { let platform_version = PlatformVersion::latest(); let platform = TestPlatformBuilder::new() .with_config(PlatformConfig { @@ -110,10 +114,17 @@ impl PayoutFixture { owner_address: Some(owner_key.public_key_hash().expect("owner hash")), voting_address: [0x74; 20], payout_address: None, - payouts: Some(vec![payout( - original_key.public_key_hash().expect("payout hash"), - 10000, - )]), + payouts: Some(if initial_split { + vec![ + payout(original_key.public_key_hash().expect("payout hash"), 5000), + payout([0x76; 20], 5000), + ] + } else { + vec![payout( + original_key.public_key_hash().expect("payout hash"), + 10000, + )] + }), pub_key_operator: vec![0x75; 48], operator_payout_address: None, platform_node_id: None, @@ -276,6 +287,64 @@ impl PayoutFixture { } } +#[tokio::test] +async fn should_create_first_transfer_key_and_withdraw_from_an_initially_owner_only_identity() { + let mut fixture = PayoutFixture::with_split_payout(true); + let transaction = fixture.platform.drive.grove.start_transaction(); + let initial = fixture.identity(&transaction); + assert_eq!( + initial.public_keys().keys().copied().collect::>(), + vec![1] + ); + assert!(recent_keys(&fixture, &transaction).is_empty()); + fixture.update( + vec![payout( + fixture + .original_key + .public_key_hash() + .expect("sole payout hash"), + 10000, + )], + &transaction, + ); + let identity = fixture.identity(&transaction); + assert_eq!( + identity.public_keys().keys().copied().collect::>(), + vec![1, 2] + ); + assert_eq!( + identity.public_keys().get(&1), + initial.public_keys().get(&1) + ); + assert_eq!(identity.balance(), initial.balance()); + let transfer = identity + .public_keys() + .get(&2) + .expect("first transfer key") + .clone(); + assert_eq!(transfer.purpose(), Purpose::TRANSFER); + assert!(!transfer.is_disabled()); + assert_eq!( + recent_keys(&fixture, &transaction), + BTreeMap::from([(2, transfer.clone())]) + ); + let secret = *fixture + .signer + .private_keys + .get(&fixture.original_key) + .expect("payout secret"); + fixture + .signer + .add_identity_public_key(transfer.clone(), secret); + assert_matches!( + fixture + .withdraw(&identity, &transfer, 1, true, &transaction) + .await + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); +} + #[tokio::test] async fn should_allow_replacement_payout_and_preserve_owner_default_withdrawals() { let mut fixture = PayoutFixture::new(); diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index 0b264acdbbf..06bc0616f06 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -24,8 +24,12 @@ use crate::version::drive_abci_versions::drive_abci_method_versions::{ /// from the epochs it was priced for. /// `create_owner_identity` 2 and `update_masternode_identities` 1 decide the owner identity of /// the masternodes Dash Core lists from v24 on, on which version 1 fails: none without an owner -/// address (a shared masternode), the version 1 identity for a payout list with one entry, and -/// only the OWNER key for several payouts. +/// address (a shared masternode), the version 1 identity for a legacy payout address or a sole +/// payout with a matching P2PKH script, and only the OWNER key for other payout lists. +/// Payout-list changes reconcile TRANSFER keys: retain, re-enable or add the supported sole +/// recipient and disable obsolete authority; split, empty or unsupported lists disable all +/// TRANSFER keys while preserving OWNER and balance. Historical updater behavior and legacy +/// payout-address rotation remain unchanged. /// Everything else matches `DRIVE_ABCI_METHOD_VERSIONS_V9`. pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMethodVersions { engine: DriveAbciEngineMethodVersions { @@ -51,10 +55,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet get_voter_identifier_from_masternode_list_item: 0, get_operator_identifier_from_masternode_list_item: 0, create_operator_identity: 0, - create_owner_identity: 2, // changed in v14: no owner identity without an owner address; only the OWNER key for several payouts + create_owner_identity: 2, // no owner identity without an owner; TRANSFER authority requires a sole supported payout create_voter_identity: 0, disable_identity_keys: 0, - update_masternode_identities: 1, // changed in v14: adds no owner identity where create_owner_identity gives none + update_masternode_identities: 1, // tolerates absent owner identities and reconciles payout-list TRANSFER authority update_operator_identity: 0, update_owner_withdrawal_address: 1, update_voter_identity: 0, diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 7501f835eac..112e9bf0b3a 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -1964,12 +1964,14 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// `create_owner_identity` 2 and `update_masternode_identities` 1 /// (`DRIVE_ABCI_METHOD_VERSIONS_V10`), a masternode without an owner /// address gets no owner identity, only its voter and operator identities; -/// one with an owner address and a single payout address (the payout -/// address, else the only entry of a one-entry payout list) gets the -/// version 1 identity, TRANSFER key id 0 and OWNER key id 1, byte for byte; -/// and one paid to several payouts gets an owner identity with only the -/// OWNER key id 1. Updates are unchanged: only a `payoutAddress` change -/// rotates the TRANSFER key, and a `payouts` change is not acted on. This +/// one with an owner address and either a legacy payout address or a sole +/// payout with a matching P2PKH script gets the version 1 identity, +/// TRANSFER key id 0 and OWNER key id 1, byte for byte; other payout shapes +/// get only OWNER key id 1. Legacy payout-address rotation is unchanged. +/// Payout-list changes retain, re-enable or add the sole supported P2PKH +/// TRANSFER key and disable obsolete TRANSFER keys. Split, empty or +/// unsupported lists disable all TRANSFER authority while preserving OWNER +/// and balance. Historical updaters keep their payout-list policy. This /// version must be active on a network before its Dash Core activates V24, /// since earlier versions keep failing on these masternodes. ///