From 8bc83b8f5c3c806cc923c97a425bc9bb1730f20e Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 2 Dec 2025 10:32:32 +0700 Subject: [PATCH 01/14] feat: platform addresses --- dash/src/address.rs | 310 +++++++++++++++++- dash/src/blockdata/constants.rs | 11 + key-wallet-ffi/include/key_wallet_ffi.h | 4 + key-wallet-ffi/src/address_pool.rs | 12 + key-wallet-ffi/src/managed_account.rs | 10 + key-wallet-ffi/src/transaction_checking.rs | 21 ++ key-wallet-ffi/src/types.rs | 14 + key-wallet/src/account/account_collection.rs | 56 ++++ key-wallet/src/account/account_type.rs | 45 +++ key-wallet/src/dip9.rs | 42 +++ .../src/managed_account/address_pool.rs | 35 ++ .../managed_account_collection.rs | 52 ++- .../managed_account/managed_account_type.rs | 54 +++ key-wallet/src/managed_account/mod.rs | 16 + .../transaction_checking/account_checker.rs | 31 ++ .../transaction_router/mod.rs | 9 + .../transaction_checking/wallet_checker.rs | 7 + key-wallet/src/wallet/helper.rs | 5 + 18 files changed, 724 insertions(+), 10 deletions(-) diff --git a/dash/src/address.rs b/dash/src/address.rs index 02e4ba5f0..bce7e2ade 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -48,8 +48,9 @@ use core::str::FromStr; use crate::base58; use crate::blockdata::constants::{ - MAX_SCRIPT_ELEMENT_SIZE, PUBKEY_ADDRESS_PREFIX_MAIN, PUBKEY_ADDRESS_PREFIX_TEST, - SCRIPT_ADDRESS_PREFIX_MAIN, SCRIPT_ADDRESS_PREFIX_TEST, + MAX_SCRIPT_ELEMENT_SIZE, PLATFORM_P2PKH_PREFIX_MAIN, PLATFORM_P2PKH_PREFIX_TEST, + PLATFORM_P2SH_PREFIX_MAIN, PLATFORM_P2SH_PREFIX_TEST, PUBKEY_ADDRESS_PREFIX_MAIN, + PUBKEY_ADDRESS_PREFIX_TEST, SCRIPT_ADDRESS_PREFIX_MAIN, SCRIPT_ADDRESS_PREFIX_TEST, }; use crate::blockdata::opcodes; use crate::blockdata::opcodes::all::*; @@ -197,6 +198,12 @@ pub enum AddressType { P2wsh, /// Pay to taproot. P2tr, + /// Platform payment P2PKH (DIP-18) - "D" prefix on mainnet, "d" on testnet. + /// These addresses are for Dash Platform and MUST NOT be used in Core chain transactions. + PlatformP2pkh, + /// Platform payment P2SH (DIP-18) - "P" prefix on mainnet, "p" on testnet. + /// These addresses are for Dash Platform multisig/scripts and MUST NOT be used in Core chain transactions. + PlatformP2sh, } impl fmt::Display for AddressType { @@ -207,6 +214,8 @@ impl fmt::Display for AddressType { AddressType::P2wpkh => "p2wpkh", AddressType::P2wsh => "p2wsh", AddressType::P2tr => "p2tr", + AddressType::PlatformP2pkh => "platform_p2pkh", + AddressType::PlatformP2sh => "platform_p2sh", }) } } @@ -220,6 +229,8 @@ impl FromStr for AddressType { "p2wpkh" => Ok(AddressType::P2wpkh), "p2wsh" => Ok(AddressType::P2wsh), "p2tr" => Ok(AddressType::P2tr), + "platform_p2pkh" => Ok(AddressType::PlatformP2pkh), + "platform_p2sh" => Ok(AddressType::PlatformP2sh), _ => Err(Error::UnknownAddressType(s.to_owned())), } } @@ -437,6 +448,12 @@ pub enum Payload { ScriptHash(ScriptHash), /// Segwit address. WitnessProgram(WitnessProgram), + /// Platform P2PKH address (DIP-18). + /// These addresses are for Dash Platform payments and MUST NOT be used in Core chain transactions. + PlatformPubkeyHash(PubkeyHash), + /// Platform P2SH address (DIP-18). + /// These addresses are for Dash Platform scripts and MUST NOT be used in Core chain transactions. + PlatformScriptHash(ScriptHash), } impl Payload { @@ -515,10 +532,17 @@ impl Payload { } /// Generates a script pubkey spending to this [Payload]. + /// + /// Note: For Platform addresses, this generates the equivalent Core chain script, + /// but Platform addresses should NOT be used in Core chain transactions. pub fn script_pubkey(&self) -> ScriptBuf { match *self { - Payload::PubkeyHash(ref hash) => ScriptBuf::new_p2pkh(hash), - Payload::ScriptHash(ref hash) => ScriptBuf::new_p2sh(hash), + Payload::PubkeyHash(ref hash) | Payload::PlatformPubkeyHash(ref hash) => { + ScriptBuf::new_p2pkh(hash) + } + Payload::ScriptHash(ref hash) | Payload::PlatformScriptHash(ref hash) => { + ScriptBuf::new_p2sh(hash) + } Payload::WitnessProgram(ref prog) => ScriptBuf::new_witness_program(prog), } } @@ -527,16 +551,24 @@ impl Payload { /// This function doesn't make any allocations. pub fn matches_script_pubkey(&self, script: &Script) -> bool { match *self { - Payload::PubkeyHash(ref hash) if script.is_p2pkh() => { + Payload::PubkeyHash(ref hash) | Payload::PlatformPubkeyHash(ref hash) + if script.is_p2pkh() => + { &script.as_bytes()[3..23] == >::as_ref(hash) } - Payload::ScriptHash(ref hash) if script.is_p2sh() => { + Payload::ScriptHash(ref hash) | Payload::PlatformScriptHash(ref hash) + if script.is_p2sh() => + { &script.as_bytes()[2..22] == >::as_ref(hash) } Payload::WitnessProgram(ref prog) if script.is_witness_program() => { &script.as_bytes()[2..] == prog.program.as_bytes() } - Payload::PubkeyHash(_) | Payload::ScriptHash(_) | Payload::WitnessProgram(_) => false, + Payload::PubkeyHash(_) + | Payload::ScriptHash(_) + | Payload::WitnessProgram(_) + | Payload::PlatformPubkeyHash(_) + | Payload::PlatformScriptHash(_) => false, } } @@ -612,11 +644,55 @@ impl Payload { /// is a script hash or pubkey hash, a reference to the hash is returned. fn inner_prog_as_bytes(&self) -> &[u8] { match self { - Payload::ScriptHash(hash) => hash.as_ref(), - Payload::PubkeyHash(hash) => hash.as_ref(), + Payload::ScriptHash(hash) | Payload::PlatformScriptHash(hash) => hash.as_ref(), + Payload::PubkeyHash(hash) | Payload::PlatformPubkeyHash(hash) => hash.as_ref(), Payload::WitnessProgram(prog) => prog.program().as_bytes(), } } + + /// Creates a Platform P2PKH payload from a public key (DIP-18). + /// + /// This creates a payload for Dash Platform payment addresses. + /// These addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn platform_p2pkh(pk: &PublicKey) -> Payload { + Payload::PlatformPubkeyHash(pk.pubkey_hash()) + } + + /// Creates a Platform P2PKH payload from a pubkey hash (DIP-18). + /// + /// This creates a payload for Dash Platform payment addresses. + /// These addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn platform_p2pkh_from_hash(hash: PubkeyHash) -> Payload { + Payload::PlatformPubkeyHash(hash) + } + + /// Creates a Platform P2SH payload from a script (DIP-18). + /// + /// This creates a payload for Dash Platform script addresses. + /// These addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn platform_p2sh(script: &Script) -> Result { + if script.len() > MAX_SCRIPT_ELEMENT_SIZE { + return Err(Error::ExcessiveScriptSize); + } + Ok(Payload::PlatformScriptHash(script.script_hash())) + } + + /// Creates a Platform P2SH payload from a script hash (DIP-18). + /// + /// This creates a payload for Dash Platform script addresses. + /// These addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn platform_p2sh_from_hash(hash: ScriptHash) -> Payload { + Payload::PlatformScriptHash(hash) + } + + /// Returns true if this is a Platform address payload (DIP-18). + pub fn is_platform(&self) -> bool { + matches!(self, Payload::PlatformPubkeyHash(_) | Payload::PlatformScriptHash(_)) + } } /// A utility struct to encode an address payload with the given parameters. @@ -630,6 +706,10 @@ pub struct AddressEncoding<'a> { pub p2sh_prefix: u8, /// hrp used in bech32 address (e.g. "bc" for "bc1..." addresses). pub bech32_hrp: &'a str, + /// base58 version byte for Platform P2PKH payloads (DIP-18). + pub platform_p2pkh_prefix: u8, + /// base58 version byte for Platform P2SH payloads (DIP-18). + pub platform_p2sh_prefix: u8, } /// Formats bech32 as upper case if alternate formatting is chosen (`{:#}`). @@ -662,6 +742,18 @@ impl<'a> fmt::Display for AddressEncoding<'a> { bech32::WriteBase32::write_u5(&mut bech32_writer, version.into())?; bech32::ToBase32::write_base32(&prog.as_bytes(), &mut bech32_writer) } + Payload::PlatformPubkeyHash(hash) => { + let mut prefixed = [0; 21]; + prefixed[0] = self.platform_p2pkh_prefix; + prefixed[1..].copy_from_slice(&hash[..]); + base58::encode_check_to_fmt(fmt, &prefixed[..]) + } + Payload::PlatformScriptHash(hash) => { + let mut prefixed = [0; 21]; + prefixed[0] = self.platform_p2sh_prefix; + prefixed[1..].copy_from_slice(&hash[..]); + base58::encode_check_to_fmt(fmt, &prefixed[..]) + } } } } @@ -910,6 +1002,8 @@ impl bincode::Decode for AddressType { 2 => Ok(AddressType::P2wpkh), 3 => Ok(AddressType::P2wsh), 4 => Ok(AddressType::P2tr), + 5 => Ok(AddressType::PlatformP2pkh), + 6 => Ok(AddressType::PlatformP2sh), _ => Err(bincode::error::DecodeError::OtherString("invalid address type".to_string())), } } @@ -927,6 +1021,8 @@ impl<'de> bincode::BorrowDecode<'de> for AddressType { 2 => Ok(AddressType::P2wpkh), 3 => Ok(AddressType::P2wsh), 4 => Ok(AddressType::P2tr), + 5 => Ok(AddressType::PlatformP2pkh), + 6 => Ok(AddressType::PlatformP2sh), _ => Err(bincode::error::DecodeError::OtherString("invalid address type".to_string())), } } @@ -972,6 +1068,8 @@ impl Address { match self.payload() { Payload::PubkeyHash(_) => Some(AddressType::P2pkh), Payload::ScriptHash(_) => Some(AddressType::P2sh), + Payload::PlatformPubkeyHash(_) => Some(AddressType::PlatformP2pkh), + Payload::PlatformScriptHash(_) => Some(AddressType::PlatformP2sh), Payload::WitnessProgram(prog) => { // BIP-141 p2wpkh or p2wsh addresses. match prog.version() { @@ -1007,11 +1105,24 @@ impl Address { Network::Regtest => "dsrt", other => unreachable!("Unknown network {other:?} – add explicit prefix"), }; + // DIP-18: Platform address prefixes + let platform_p2pkh_prefix = match self.network() { + Network::Dash => PLATFORM_P2PKH_PREFIX_MAIN, + Network::Testnet | Network::Devnet | Network::Regtest => PLATFORM_P2PKH_PREFIX_TEST, + other => unreachable!("Unknown network {other:?} – add explicit prefix"), + }; + let platform_p2sh_prefix = match self.network() { + Network::Dash => PLATFORM_P2SH_PREFIX_MAIN, + Network::Testnet | Network::Devnet | Network::Regtest => PLATFORM_P2SH_PREFIX_TEST, + other => unreachable!("Unknown network {other:?} – add explicit prefix"), + }; let encoding = AddressEncoding { payload: self.payload(), p2pkh_prefix, p2sh_prefix, bech32_hrp, + platform_p2pkh_prefix, + platform_p2sh_prefix, }; use fmt::Display; @@ -1101,6 +1212,50 @@ impl Address { Address::new(network, Payload::p2tr_tweaked(output_key)) } + /// Creates a Platform P2PKH address from a public key (DIP-18). + /// + /// This creates a Dash Platform payment address ("D" prefix on mainnet, "d" on testnet). + /// **Warning**: These addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn platform_p2pkh(pk: &PublicKey, network: Network) -> Address { + Address::new(network, Payload::platform_p2pkh(pk)) + } + + /// Creates a Platform P2PKH address from a pubkey hash (DIP-18). + /// + /// This creates a Dash Platform payment address ("D" prefix on mainnet, "d" on testnet). + /// **Warning**: These addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn platform_p2pkh_from_hash(hash: PubkeyHash, network: Network) -> Address { + Address::new(network, Payload::platform_p2pkh_from_hash(hash)) + } + + /// Creates a Platform P2SH address from a script (DIP-18). + /// + /// This creates a Dash Platform script address ("P" prefix on mainnet, "p" on testnet). + /// **Warning**: These addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn platform_p2sh(script: &Script, network: Network) -> Result { + Ok(Address::new(network, Payload::platform_p2sh(script)?)) + } + + /// Creates a Platform P2SH address from a script hash (DIP-18). + /// + /// This creates a Dash Platform script address ("P" prefix on mainnet, "p" on testnet). + /// **Warning**: These addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn platform_p2sh_from_hash(hash: ScriptHash, network: Network) -> Address { + Address::new(network, Payload::platform_p2sh_from_hash(hash)) + } + + /// Returns true if this is a Platform address (DIP-18). + /// + /// Platform addresses MUST NOT be used in Core chain transactions. + #[inline] + pub fn is_platform(&self) -> bool { + self.payload().is_platform() + } + /// Gets the address type of the address. /// /// # Returns @@ -1393,6 +1548,23 @@ impl FromStr for Address { SCRIPT_ADDRESS_PREFIX_TEST => { (Network::Testnet, Payload::ScriptHash(ScriptHash::from_slice(&data[1..]).unwrap())) } + // DIP-18: Platform address prefixes + PLATFORM_P2PKH_PREFIX_MAIN => ( + Network::Dash, + Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap()), + ), + PLATFORM_P2SH_PREFIX_MAIN => ( + Network::Dash, + Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..]).unwrap()), + ), + PLATFORM_P2PKH_PREFIX_TEST => ( + Network::Testnet, + Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap()), + ), + PLATFORM_P2SH_PREFIX_TEST => ( + Network::Testnet, + Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..]).unwrap()), + ), x => return Err(Error::Base58(base58::Error::InvalidAddressVersion(x))), }; @@ -2121,4 +2293,124 @@ mod tests { } } } + + // DIP-18 Platform Payment Address Tests + // Test vectors from DIP-0018: Dash Platform Payment Address Encodings + + #[test] + fn test_dip18_platform_p2pkh_mainnet() { + // DIP-18 test vector 1 - mainnet P2PKH + // HASH160: f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 + // Expected mainnet address: DTjceJiEqrNkCsSizK65fojEANTKoQMtsR + let hash160: PubkeyHash = "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525".parse().unwrap(); + let addr = Address::new(Dash, Payload::PlatformPubkeyHash(hash160)); + assert_eq!(addr.to_string(), "DTjceJiEqrNkCsSizK65fojEANTKoQMtsR"); + assert_eq!(addr.address_type(), Some(AddressType::PlatformP2pkh)); + assert!(addr.is_platform()); + + // Test round-trip parsing + let parsed = Address::from_str("DTjceJiEqrNkCsSizK65fojEANTKoQMtsR").unwrap().assume_checked(); + assert_eq!(parsed.payload(), addr.payload()); + assert!(parsed.is_platform()); + } + + #[test] + fn test_dip18_platform_p2pkh_testnet() { + // DIP-18 test vector 1 - testnet P2PKH + // Using same HASH160 as mainnet to verify proper version byte encoding + // HASH160: f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 + // Note: The DIP shows a different testnet address because it uses a different + // derivation path (m/9'/1'/17'... vs m/9'/5'/17'...) which produces a different pubkey. + // Here we verify the encoding is correct for the same payload with testnet version byte. + let hash160: PubkeyHash = "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525".parse().unwrap(); + let addr = Address::new(Testnet, Payload::PlatformPubkeyHash(hash160)); + // Base58Check of [0x5a || f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 || checksum] + assert_eq!(addr.to_string(), "dc1oipbXSgBKGsovTV5CmL4RvduvRWRbsr"); + assert_eq!(addr.address_type(), Some(AddressType::PlatformP2pkh)); + assert!(addr.is_platform()); + + // Test round-trip parsing + let parsed = Address::from_str("dc1oipbXSgBKGsovTV5CmL4RvduvRWRbsr").unwrap().assume_checked(); + assert_eq!(parsed.payload(), addr.payload()); + assert!(parsed.is_platform()); + } + + #[test] + fn test_dip18_platform_p2pkh_vector2() { + // DIP-18 test vector 2 + // HASH160: a5ff0046217fd1c7d238e3e146cc5bfd90832a7e + // Mainnet: DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm + let hash160: PubkeyHash = "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e".parse().unwrap(); + + let mainnet_addr = Address::new(Dash, Payload::PlatformPubkeyHash(hash160)); + assert_eq!(mainnet_addr.to_string(), "DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm"); + + // Testnet uses same HASH160 with version 0x5a + let testnet_addr = Address::new(Testnet, Payload::PlatformPubkeyHash(hash160)); + assert_eq!(testnet_addr.to_string(), "dUYzbDAwmo4BNb2dA41nLtSsiwoRzmDg3K"); + } + + #[test] + fn test_dip18_platform_p2pkh_vector3() { + // DIP-18 test vector 3 (non-default key_class) + // HASH160: 6d92674fd64472a3dfcfc3ebcfed7382bf699d7b + // Mainnet: DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo + let hash160: PubkeyHash = "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b".parse().unwrap(); + + let mainnet_addr = Address::new(Dash, Payload::PlatformPubkeyHash(hash160)); + assert_eq!(mainnet_addr.to_string(), "DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo"); + + // Testnet uses same HASH160 with version 0x5a + let testnet_addr = Address::new(Testnet, Payload::PlatformPubkeyHash(hash160)); + assert_eq!(testnet_addr.to_string(), "dPQeeyrQ9g9CLZD2Znvhuxp3PcWS2D5NCy"); + } + + #[test] + fn test_dip18_platform_p2sh() { + // DIP-18 P2SH test vector + // Script: 76a914000102030405060708090a0b0c0d0e0f101112131488ac + // HASH160(script): 43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636 + // Mainnet P2SH: Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k + // Testnet P2SH: pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh + let script_hash: ScriptHash = "43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636".parse().unwrap(); + + let mainnet_addr = Address::new(Dash, Payload::PlatformScriptHash(script_hash)); + assert_eq!(mainnet_addr.to_string(), "Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k"); + assert_eq!(mainnet_addr.address_type(), Some(AddressType::PlatformP2sh)); + assert!(mainnet_addr.is_platform()); + + let testnet_addr = Address::new(Testnet, Payload::PlatformScriptHash(script_hash)); + assert_eq!(testnet_addr.to_string(), "pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh"); + assert_eq!(testnet_addr.address_type(), Some(AddressType::PlatformP2sh)); + assert!(testnet_addr.is_platform()); + } + + #[test] + fn test_platform_address_parsing() { + // Test that Platform addresses can be parsed and distinguished from Core addresses + let platform_mainnet = Address::from_str("DTjceJiEqrNkCsSizK65fojEANTKoQMtsR").unwrap().assume_checked(); + assert!(platform_mainnet.is_platform()); + assert_eq!(platform_mainnet.address_type(), Some(AddressType::PlatformP2pkh)); + + let platform_testnet = Address::from_str("dSqV2orinasFpYAMGQTLy6uYpW9Dnge563").unwrap().assume_checked(); + assert!(platform_testnet.is_platform()); + assert_eq!(platform_testnet.address_type(), Some(AddressType::PlatformP2pkh)); + + // Core addresses should NOT be platform + let core_mainnet = Address::from_str("Xci5rLWMqdQDy5uaCDVWEcTi6sfnkmqdUz").unwrap().assume_checked(); + assert!(!core_mainnet.is_platform()); + assert_eq!(core_mainnet.address_type(), Some(AddressType::P2pkh)); + } + + #[test] + fn test_platform_p2sh_parsing() { + // Test Platform P2SH address parsing + let mainnet_p2sh = Address::from_str("Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k").unwrap().assume_checked(); + assert!(mainnet_p2sh.is_platform()); + assert_eq!(mainnet_p2sh.address_type(), Some(AddressType::PlatformP2sh)); + + let testnet_p2sh = Address::from_str("pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh").unwrap().assume_checked(); + assert!(testnet_p2sh.is_platform()); + assert_eq!(testnet_p2sh.address_type(), Some(AddressType::PlatformP2sh)); + } } diff --git a/dash/src/blockdata/constants.rs b/dash/src/blockdata/constants.rs index bf03e274c..1e6c72af8 100644 --- a/dash/src/blockdata/constants.rs +++ b/dash/src/blockdata/constants.rs @@ -55,6 +55,17 @@ pub const PUBKEY_ADDRESS_PREFIX_TEST: u8 = 140; /// Test (testnet, devnet, regtest) script address prefix. pub const SCRIPT_ADDRESS_PREFIX_TEST: u8 = 19; // 0x13 + +// DIP-18: Platform Payment Address Prefixes +/// Platform payment P2PKH address prefix (mainnet) - produces 'D' prefix. +pub const PLATFORM_P2PKH_PREFIX_MAIN: u8 = 0x1e; // 30 +/// Platform payment P2PKH address prefix (testnet/devnet/regtest) - produces 'd' prefix. +pub const PLATFORM_P2PKH_PREFIX_TEST: u8 = 0x5a; // 90 +/// Platform payment P2SH address prefix (mainnet) - produces 'P' prefix. +pub const PLATFORM_P2SH_PREFIX_MAIN: u8 = 0x38; // 56 +/// Platform payment P2SH address prefix (testnet/devnet/regtest) - produces 'p' prefix. +pub const PLATFORM_P2SH_PREFIX_TEST: u8 = 0x75; // 117 + /// The maximum allowed script size. pub const MAX_SCRIPT_ELEMENT_SIZE: usize = 520; /// How many blocks between halvings. diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 0e5dba7ef..a1cfd6b94 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -92,6 +92,10 @@ typedef enum { PROVIDER_PLATFORM_KEYS = 10, DASHPAY_RECEIVING_FUNDS = 11, DASHPAY_EXTERNAL_ACCOUNT = 12, + /* + Platform Payment address (DIP17/DIP18) - Path: m/9'/5'/17'/account'/key_class'/index + */ + PLATFORM_PAYMENT = 13, } FFIAccountType; /* diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index c1f99a23f..506c1b4b4 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -59,6 +59,12 @@ fn get_managed_account_by_type<'a>( // DashPay managed accounts are not currently persisted in ManagedAccountCollection None } + AccountType::PlatformPayment { + .. + } => { + // Platform Payment accounts are not currently persisted in ManagedAccountCollection + None + } } } @@ -102,6 +108,12 @@ fn get_managed_account_by_type_mut<'a>( // DashPay managed accounts are not currently persisted in ManagedAccountCollection None } + AccountType::PlatformPayment { + .. + } => { + // Platform Payment accounts are not currently persisted in ManagedAccountCollection + None + } } } diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index 0be10f68f..5fb71c263 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -178,6 +178,9 @@ pub unsafe extern "C" fn managed_wallet_get_account( AccountType::DashpayExternalAccount { .. } => None, + AccountType::PlatformPayment { + .. + } => None, }; match managed_account { @@ -527,6 +530,9 @@ pub unsafe extern "C" fn managed_account_get_account_type( AccountType::DashpayExternalAccount { .. } => FFIAccountType::DashpayExternalAccount, + AccountType::PlatformPayment { + .. + } => FFIAccountType::PlatformPayment, } } @@ -1010,6 +1016,10 @@ pub unsafe extern "C" fn managed_account_get_address_pool( addresses, .. } => addresses, + ManagedAccountType::PlatformPayment { + addresses, + .. + } => addresses, }; let ffi_pool = FFIAddressPool { diff --git a/key-wallet-ffi/src/transaction_checking.rs b/key-wallet-ffi/src/transaction_checking.rs index 6c05093e7..4074ae953 100644 --- a/key-wallet-ffi/src/transaction_checking.rs +++ b/key-wallet-ffi/src/transaction_checking.rs @@ -455,6 +455,27 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( ffi_accounts.push(ffi_match); continue; } + AccountTypeMatch::PlatformPayment { + account_index, + involved_addresses, + .. + } => { + // Note: Platform Payment addresses are NOT used in Core chain transactions + // per DIP17/DIP18. This branch should never be reached in practice. + let ffi_match = FFIAccountMatch { + account_type: 13, // PlatformPayment + account_index: *account_index, + registration_index: 0, + received: account_match.received, + sent: account_match.sent, + external_addresses_count: involved_addresses.len() as c_uint, + internal_addresses_count: 0, + has_external_addresses: !involved_addresses.is_empty(), + has_internal_addresses: false, + }; + ffi_accounts.push(ffi_match); + continue; + } } } diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index 3dcb0f48e..e7e030ef9 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -266,6 +266,8 @@ pub enum FFIAccountType { ProviderPlatformKeys = 10, DashpayReceivingFunds = 11, DashpayExternalAccount = 12, + /// Platform Payment address (DIP17/DIP18) - Path: m/9'/5'/17'/account'/key_class'/index + PlatformPayment = 13, } impl FFIAccountType { @@ -327,6 +329,14 @@ impl FFIAccountType { DashPay account creation must use a different API path." ); } + FFIAccountType::PlatformPayment => { + panic!( + "FFIAccountType::PlatformPayment cannot be converted to AccountType \ + without account and key_class indices. The FFI API does not yet \ + support passing these values. This is a programming error - \ + Platform Payment account creation must use a different API path." + ); + } } } @@ -411,6 +421,10 @@ impl FFIAccountType { &friend_identity_id[..8] ); } + key_wallet::AccountType::PlatformPayment { + account, + key_class, + } => (FFIAccountType::PlatformPayment, *account, Some(*key_class)), } } } diff --git a/key-wallet/src/account/account_collection.rs b/key-wallet/src/account/account_collection.rs index b84aea6cf..d141498b3 100644 --- a/key-wallet/src/account/account_collection.rs +++ b/key-wallet/src/account/account_collection.rs @@ -28,6 +28,17 @@ pub struct DashpayAccountKey { pub friend_identity_id: DashpayContactIdentityId, } +/// Key for Platform Payment accounts (DIP-17) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +pub struct PlatformPaymentAccountKey { + /// Account index (hardened) + pub account: u32, + /// Key class (hardened) + pub key_class: u32, +} + /// Collection of accounts organized by type #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -61,6 +72,8 @@ pub struct AccountCollection { pub dashpay_receival_accounts: BTreeMap, /// DashPay external (watch-only) accounts pub dashpay_external_accounts: BTreeMap, + /// Platform Payment accounts (DIP-17) + pub platform_payment_accounts: BTreeMap, } impl AccountCollection { @@ -82,6 +95,7 @@ impl AccountCollection { provider_platform_keys: None, dashpay_receival_accounts: BTreeMap::new(), dashpay_external_accounts: BTreeMap::new(), + platform_payment_accounts: BTreeMap::new(), } } @@ -157,6 +171,16 @@ impl AccountCollection { }; self.dashpay_external_accounts.insert(key, account); } + AccountType::PlatformPayment { + account: acc_index, + key_class, + } => { + let key = PlatformPaymentAccountKey { + account: *acc_index, + key_class: *key_class, + }; + self.platform_payment_accounts.insert(key, account); + } } Ok(()) } @@ -240,6 +264,16 @@ impl AccountCollection { }; self.dashpay_external_accounts.contains_key(&key) } + AccountType::PlatformPayment { + account, + key_class, + } => { + let key = PlatformPaymentAccountKey { + account: *account, + key_class: *key_class, + }; + self.platform_payment_accounts.contains_key(&key) + } } } @@ -293,6 +327,13 @@ impl AccountCollection { }; self.dashpay_external_accounts.get(&key) } + AccountType::PlatformPayment { + account, + key_class, + } => { + let key = PlatformPaymentAccountKey { account, key_class }; + self.platform_payment_accounts.get(&key) + } } } @@ -346,6 +387,13 @@ impl AccountCollection { }; self.dashpay_external_accounts.get_mut(&key) } + AccountType::PlatformPayment { + account, + key_class, + } => { + let key = PlatformPaymentAccountKey { account, key_class }; + self.platform_payment_accounts.get_mut(&key) + } } } @@ -382,6 +430,10 @@ impl AccountCollection { // Note: provider_operator_keys (BLS) and provider_platform_keys (EdDSA) are excluded // Use specific methods to access them + accounts.extend(self.dashpay_receival_accounts.values()); + accounts.extend(self.dashpay_external_accounts.values()); + accounts.extend(self.platform_payment_accounts.values()); + accounts } @@ -418,6 +470,10 @@ impl AccountCollection { // Note: provider_operator_keys (BLS) and provider_platform_keys (EdDSA) are excluded // Use specific methods to access them + accounts.extend(self.dashpay_receival_accounts.values_mut()); + accounts.extend(self.dashpay_external_accounts.values_mut()); + accounts.extend(self.platform_payment_accounts.values_mut()); + accounts } diff --git a/key-wallet/src/account/account_type.rs b/key-wallet/src/account/account_type.rs index 08ee92bc4..7983ba9a6 100644 --- a/key-wallet/src/account/account_type.rs +++ b/key-wallet/src/account/account_type.rs @@ -83,6 +83,16 @@ pub enum AccountType { /// Our contact's identity id (32 bytes) friend_identity_id: [u8; 32], }, + /// Platform Payment account (DIP-17) + /// Path: m/9'/coin_type'/17'/account'/key_class'/index + /// These addresses are encoded with DIP-18 format ("D"/"d" prefix for P2PKH). + /// **Warning**: Platform addresses MUST NOT be used in Core chain transactions. + PlatformPayment { + /// Account index (hardened) - default 0' + account: u32, + /// Key class (hardened) - default 0', 1' reserved for change-like segregation + key_class: u32, + }, } impl From for AccountTypeToCheck { @@ -116,6 +126,9 @@ impl From for AccountTypeToCheck { AccountType::DashpayExternalAccount { .. } => AccountTypeToCheck::DashpayExternalAccount, + AccountType::PlatformPayment { + .. + } => AccountTypeToCheck::PlatformPayment, } } } @@ -140,6 +153,10 @@ impl AccountType { index, .. } => Some(*index), + Self::PlatformPayment { + account, + .. + } => Some(*account), // Identity and provider types don't have account indices Self::IdentityRegistration | Self::IdentityTopUp { @@ -208,6 +225,9 @@ impl AccountType { Self::DashpayExternalAccount { .. } => DerivationPathReference::ContactBasedFundsExternal, + Self::PlatformPayment { + .. + } => DerivationPathReference::PlatformPayment, } } @@ -390,6 +410,31 @@ impl AccountType { }); Ok(path) } + Self::PlatformPayment { + account, + key_class, + } => { + // DIP-17: m/9'/coin_type'/17'/account'/key_class' + // The leaf index is non-hardened and appended during address generation + let mut path = match network { + Network::Dash => { + DerivationPath::from(crate::dip9::PLATFORM_PAYMENT_ROOT_PATH_MAINNET) + } + Network::Testnet => { + DerivationPath::from(crate::dip9::PLATFORM_PAYMENT_ROOT_PATH_TESTNET) + } + _ => return Err(crate::error::Error::InvalidNetwork), + }; + path.push( + ChildNumber::from_hardened_idx(*account) + .map_err(crate::error::Error::Bip32)?, + ); + path.push( + ChildNumber::from_hardened_idx(*key_class) + .map_err(crate::error::Error::Bip32)?, + ); + Ok(path) + } } } } diff --git a/key-wallet/src/dip9.rs b/key-wallet/src/dip9.rs index d1b279359..a69386c20 100644 --- a/key-wallet/src/dip9.rs +++ b/key-wallet/src/dip9.rs @@ -27,6 +27,8 @@ pub enum DerivationPathReference { BlockchainIdentityCreditInvitationFunding = 13, ProviderPlatformNodeKeys = 14, CoinJoin = 15, + /// DIP-17: Platform Payment Addresses + PlatformPayment = 16, Root = 255, } @@ -131,6 +133,8 @@ pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_REGISTRATION: u32 = 1; pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_TOPUP: u32 = 2; pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_INVITATIONS: u32 = 3; pub const FEATURE_PURPOSE_DASHPAY: u32 = 15; +/// DIP-17: Platform Payment Addresses feature index +pub const FEATURE_PURPOSE_PLATFORM_PAYMENT: u32 = 17; pub const DASH_BIP44_PATH_MAINNET: IndexConstPath<2> = IndexConstPath { indexes: [ ChildNumber::Hardened { @@ -376,3 +380,41 @@ pub const IDENTITY_AUTHENTICATION_PATH_TESTNET: IndexConstPath<4> = IndexConstPa reference: DerivationPathReference::BlockchainIdentities, path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION, }; + +// DIP-17: Platform Payment Address Paths +// Path: m/9'/coin_type'/17'/account'/key_class'/index +// Note: The full path includes account'/key_class'/index which is appended during derivation + +/// Platform Payment root path for mainnet: m/9'/5'/17' +pub const PLATFORM_PAYMENT_ROOT_PATH_MAINNET: IndexConstPath<3> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_PLATFORM_PAYMENT, + }, + ], + reference: DerivationPathReference::PlatformPayment, + path_type: DerivationPathType::CLEAR_FUNDS, +}; + +/// Platform Payment root path for testnet: m/9'/1'/17' +pub const PLATFORM_PAYMENT_ROOT_PATH_TESTNET: IndexConstPath<3> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_TESTNET_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_PLATFORM_PAYMENT, + }, + ], + reference: DerivationPathReference::PlatformPayment, + path_type: DerivationPathType::CLEAR_FUNDS, +}; diff --git a/key-wallet/src/managed_account/address_pool.rs b/key-wallet/src/managed_account/address_pool.rs index cfef783f9..a8d89e631 100644 --- a/key-wallet/src/managed_account/address_pool.rs +++ b/key-wallet/src/managed_account/address_pool.rs @@ -394,11 +394,39 @@ impl AddressPool { } } + /// Create a new Platform payment address pool (DIP-17/DIP-18) + /// + /// This creates an address pool that generates Platform payment addresses + /// with DIP-18 encoding ("D" prefix on mainnet, "d" on testnet). + /// **Warning**: Platform addresses MUST NOT be used in Core chain transactions. + pub fn new_platform( + base_path: DerivationPath, + pool_type: AddressPoolType, + gap_limit: u32, + network: Network, + key_source: &KeySource, + ) -> Result { + let mut pool = Self::new_without_generation(base_path, pool_type, gap_limit, network); + pool.address_type = AddressType::PlatformP2pkh; + + // Generate addresses up to the gap limit if we have a key source + if !matches!(key_source, KeySource::NoKeySource) { + pool.generate_addresses(gap_limit, key_source, true)?; + } + + Ok(pool) + } + /// Set the address type for new addresses pub fn set_address_type(&mut self, address_type: AddressType) { self.address_type = address_type; } + /// Returns true if this is a Platform address pool (DIP-18) + pub fn is_platform(&self) -> bool { + matches!(self.address_type, AddressType::PlatformP2pkh | AddressType::PlatformP2sh) + } + /// Check if this is an internal (change) address pool pub fn is_internal(&self) -> bool { self.pool_type == AddressPoolType::Internal @@ -482,6 +510,13 @@ impl AddressPool { // For now, default to P2PKH Address::p2pkh(&dash_pubkey, network) } + // DIP-18: Platform payment addresses + AddressType::PlatformP2pkh => Address::platform_p2pkh(&dash_pubkey, network), + AddressType::PlatformP2sh => { + // For Platform P2SH, we'd need script information + // For now, default to Platform P2PKH + Address::platform_p2pkh(&dash_pubkey, network) + } _ => { // For other address types, default to P2PKH Address::p2pkh(&dash_pubkey, network) diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 85e1614a4..a2156a7c9 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -3,7 +3,7 @@ //! This module provides a structure for managing multiple accounts //! across different networks in a hierarchical manner. -use crate::account::account_collection::DashpayAccountKey; +use crate::account::account_collection::{DashpayAccountKey, PlatformPaymentAccountKey}; use crate::account::account_type::AccountType; use crate::gap_limit::{ DEFAULT_COINJOIN_GAP_LIMIT, DEFAULT_EXTERNAL_GAP_LIMIT, DEFAULT_INTERNAL_GAP_LIMIT, @@ -49,6 +49,8 @@ pub struct ManagedAccountCollection { pub dashpay_receival_accounts: BTreeMap, /// DashPay external accounts keyed by (index, user_id, friend_id) pub dashpay_external_accounts: BTreeMap, + /// Platform Payment accounts (DIP-17) + pub platform_payment_accounts: BTreeMap, } impl ManagedAccountCollection { @@ -68,6 +70,7 @@ impl ManagedAccountCollection { provider_platform_keys: None, dashpay_receival_accounts: BTreeMap::new(), dashpay_external_accounts: BTreeMap::new(), + platform_payment_accounts: BTreeMap::new(), } } @@ -143,6 +146,17 @@ impl ManagedAccountCollection { }; self.dashpay_external_accounts.contains_key(&key) } + ManagedAccountType::PlatformPayment { + account, + key_class, + .. + } => { + let key = PlatformPaymentAccountKey { + account: *account, + key_class: *key_class, + }; + self.platform_payment_accounts.contains_key(&key) + } } } @@ -236,6 +250,17 @@ impl ManagedAccountCollection { }; self.dashpay_external_accounts.insert(key, account); } + ManagedAccountType::PlatformPayment { + account: acc_index, + key_class, + .. + } => { + let key = PlatformPaymentAccountKey { + account: *acc_index, + key_class: *key_class, + }; + self.platform_payment_accounts.insert(key, account); + } } } @@ -332,6 +357,13 @@ impl ManagedAccountCollection { } } + // Convert Platform Payment accounts + for (key, account) in &account_collection.platform_payment_accounts { + if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + managed_collection.platform_payment_accounts.insert(*key, managed_account); + } + } + managed_collection } @@ -572,6 +604,24 @@ impl ManagedAccountCollection { addresses, } } + AccountType::PlatformPayment { + account, + key_class, + } => { + // DIP-17/DIP-18: Platform Payment addresses + let addresses = AddressPool::new_platform( + base_path, + AddressPoolType::Absent, + DEFAULT_SPECIAL_GAP_LIMIT, + network, + key_source, + )?; + ManagedAccountType::PlatformPayment { + account, + key_class, + addresses, + } + } }; Ok(ManagedAccount::new(managed_type, network, is_watch_only)) diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index 8dbc2e805..d794b9563 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -104,6 +104,18 @@ pub enum ManagedAccountType { /// Address pool addresses: AddressPool, }, + /// Platform Payment account (DIP-17) + /// Path: m/9'/coin_type'/17'/account'/key_class'/index + /// These addresses are encoded with DIP-18 format ("D"/"d" prefix for P2PKH). + /// **Warning**: Platform addresses MUST NOT be used in Core chain transactions. + PlatformPayment { + /// Account index (hardened) + account: u32, + /// Key class (hardened) + key_class: u32, + /// Platform payment address pool (single pool, non-hardened leaf index) + addresses: AddressPool, + }, } impl ManagedAccountType { @@ -152,6 +164,10 @@ impl ManagedAccountType { index, .. } => Some(*index), + Self::PlatformPayment { + account, + .. + } => Some(*account), } } @@ -226,6 +242,10 @@ impl ManagedAccountType { | Self::DashpayExternalAccount { addresses, .. + } + | Self::PlatformPayment { + addresses, + .. } => vec![addresses], } } @@ -285,6 +305,10 @@ impl ManagedAccountType { | Self::DashpayExternalAccount { addresses, .. + } + | Self::PlatformPayment { + addresses, + .. } => vec![addresses], } } @@ -401,6 +425,14 @@ impl ManagedAccountType { user_identity_id: *user_identity_id, friend_identity_id: *friend_identity_id, }, + Self::PlatformPayment { + account, + key_class, + .. + } => AccountType::PlatformPayment { + account: *account, + key_class: *key_class, + }, } } @@ -644,6 +676,28 @@ impl ManagedAccountType { addresses: pool, }) } + AccountType::PlatformPayment { + account, + key_class, + } => { + // DIP-17: m/9'/coin_type'/17'/account'/key_class'/index + // The leaf index is non-hardened + let path = account_type + .derivation_path(network) + .unwrap_or_else(|_| DerivationPath::master()); + let pool = AddressPool::new_platform( + path, + crate::managed_account::address_pool::AddressPoolType::Absent, + DEFAULT_SPECIAL_GAP_LIMIT, // DIP-17 recommends gap limit of 20 + network, + key_source, + )?; + Ok(Self::PlatformPayment { + account, + key_class, + addresses: pool, + }) + } } } } diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index a55248a36..94fea9d6f 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -244,6 +244,10 @@ impl ManagedAccount { | ManagedAccountType::DashpayExternalAccount { addresses, .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. } => { addresses.unused_addresses().first().and_then(|addr| addresses.address_index(addr)) } @@ -492,6 +496,10 @@ impl ManagedAccount { | ManagedAccountType::DashpayExternalAccount { addresses, .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -577,6 +585,10 @@ impl ManagedAccount { | ManagedAccountType::DashpayExternalAccount { addresses, .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -802,6 +814,10 @@ impl ManagedAccount { | ManagedAccountType::DashpayExternalAccount { addresses, .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. } => Some(addresses.gap_limit), } } diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index 9048bdf0b..cf6a6e141 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -103,6 +103,13 @@ pub enum AccountTypeMatch { account_index: u32, involved_addresses: Vec, }, + /// Platform Payment account (DIP17/DIP18) + /// Note: Platform addresses are NOT used in Core chain transactions + PlatformPayment { + account_index: u32, + key_class: u32, + involved_addresses: Vec, + }, } impl AccountTypeMatch { @@ -159,6 +166,10 @@ impl AccountTypeMatch { | AccountTypeMatch::DashpayExternalAccount { involved_addresses, .. + } + | AccountTypeMatch::PlatformPayment { + involved_addresses, + .. } => involved_addresses.clone(), } } @@ -189,6 +200,10 @@ impl AccountTypeMatch { | AccountTypeMatch::DashpayExternalAccount { account_index, .. + } + | AccountTypeMatch::PlatformPayment { + account_index, + .. } => Some(*account_index), _ => None, } @@ -236,6 +251,9 @@ impl AccountTypeMatch { AccountTypeMatch::DashpayExternalAccount { .. } => AccountTypeToCheck::DashpayExternalAccount, + AccountTypeMatch::PlatformPayment { + .. + } => AccountTypeToCheck::PlatformPayment, } } } @@ -366,6 +384,12 @@ impl ManagedAccountCollection { } matches } + AccountTypeToCheck::PlatformPayment => { + // Platform Payment addresses (DIP17/DIP18) are NOT used in Core chain transactions. + // They are only for Platform-side payments. This account type should never match + // any Core chain transaction by design. + Vec::new() + } } } @@ -621,6 +645,13 @@ impl ManagedAccount { account_index: index.unwrap_or(0), involved_addresses: involved_other_addresses, }, + ManagedAccountType::PlatformPayment { + account, key_class, .. + } => AccountTypeMatch::PlatformPayment { + account_index: *account, + key_class: *key_class, + involved_addresses: involved_other_addresses, + }, }; Some(AccountMatch { diff --git a/key-wallet/src/transaction_checking/transaction_router/mod.rs b/key-wallet/src/transaction_checking/transaction_router/mod.rs index 19e9bd06c..36fde7583 100644 --- a/key-wallet/src/transaction_checking/transaction_router/mod.rs +++ b/key-wallet/src/transaction_checking/transaction_router/mod.rs @@ -178,6 +178,9 @@ pub enum AccountTypeToCheck { ProviderPlatformKeys, DashpayReceivingFunds, DashpayExternalAccount, + /// Platform Payment accounts (DIP-17). + /// Note: These are NOT checked for Core chain transactions as they operate on Dash Platform. + PlatformPayment, } impl From for AccountTypeToCheck { @@ -227,6 +230,9 @@ impl From for AccountTypeToCheck { ManagedAccountType::DashpayExternalAccount { .. } => AccountTypeToCheck::DashpayExternalAccount, + ManagedAccountType::PlatformPayment { + .. + } => AccountTypeToCheck::PlatformPayment, } } } @@ -278,6 +284,9 @@ impl From<&ManagedAccountType> for AccountTypeToCheck { ManagedAccountType::DashpayExternalAccount { .. } => AccountTypeToCheck::DashpayExternalAccount, + ManagedAccountType::PlatformPayment { + .. + } => AccountTypeToCheck::PlatformPayment, } } } diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 722f45deb..71fc3d6c1 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -141,6 +141,13 @@ impl WalletTransactionChecker for ManagedWalletInfo { // DashPay managed accounts are not persisted here yet None } + AccountTypeMatch::PlatformPayment { + .. + } => { + // Platform Payment addresses are NOT used in Core chain transactions. + // This branch should never be reached by design (per DIP17/DIP18). + None + } }; if let Some(account) = account { diff --git a/key-wallet/src/wallet/helper.rs b/key-wallet/src/wallet/helper.rs index a9ac07be3..8e4167d94 100644 --- a/key-wallet/src/wallet/helper.rs +++ b/key-wallet/src/wallet/helper.rs @@ -873,6 +873,11 @@ impl Wallet { // Currently not retrieved via this helper None } + crate::transaction_checking::transaction_router::AccountTypeToCheck::PlatformPayment => { + // Platform Payment addresses are not used in Core chain transactions + // and xpubs are not retrieved via this helper + None + } } }) } From 5dd06973d75c0189265d866230b6b75d676eae7e Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 2 Dec 2025 15:41:45 +0700 Subject: [PATCH 02/14] fix: test vector mismatch --- dash/src/address.rs | 21 +- key-wallet/tests/derivation_tests.rs | 277 +++++++++++++++++++++++++++ 2 files changed, 291 insertions(+), 7 deletions(-) diff --git a/dash/src/address.rs b/dash/src/address.rs index bce7e2ade..39228ffb9 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -2309,7 +2309,8 @@ mod tests { assert!(addr.is_platform()); // Test round-trip parsing - let parsed = Address::from_str("DTjceJiEqrNkCsSizK65fojEANTKoQMtsR").unwrap().assume_checked(); + let parsed = + Address::from_str("DTjceJiEqrNkCsSizK65fojEANTKoQMtsR").unwrap().assume_checked(); assert_eq!(parsed.payload(), addr.payload()); assert!(parsed.is_platform()); } @@ -2330,7 +2331,8 @@ mod tests { assert!(addr.is_platform()); // Test round-trip parsing - let parsed = Address::from_str("dc1oipbXSgBKGsovTV5CmL4RvduvRWRbsr").unwrap().assume_checked(); + let parsed = + Address::from_str("dc1oipbXSgBKGsovTV5CmL4RvduvRWRbsr").unwrap().assume_checked(); assert_eq!(parsed.payload(), addr.payload()); assert!(parsed.is_platform()); } @@ -2388,16 +2390,19 @@ mod tests { #[test] fn test_platform_address_parsing() { // Test that Platform addresses can be parsed and distinguished from Core addresses - let platform_mainnet = Address::from_str("DTjceJiEqrNkCsSizK65fojEANTKoQMtsR").unwrap().assume_checked(); + let platform_mainnet = + Address::from_str("DTjceJiEqrNkCsSizK65fojEANTKoQMtsR").unwrap().assume_checked(); assert!(platform_mainnet.is_platform()); assert_eq!(platform_mainnet.address_type(), Some(AddressType::PlatformP2pkh)); - let platform_testnet = Address::from_str("dSqV2orinasFpYAMGQTLy6uYpW9Dnge563").unwrap().assume_checked(); + let platform_testnet = + Address::from_str("dSqV2orinasFpYAMGQTLy6uYpW9Dnge563").unwrap().assume_checked(); assert!(platform_testnet.is_platform()); assert_eq!(platform_testnet.address_type(), Some(AddressType::PlatformP2pkh)); // Core addresses should NOT be platform - let core_mainnet = Address::from_str("Xci5rLWMqdQDy5uaCDVWEcTi6sfnkmqdUz").unwrap().assume_checked(); + let core_mainnet = + Address::from_str("Xci5rLWMqdQDy5uaCDVWEcTi6sfnkmqdUz").unwrap().assume_checked(); assert!(!core_mainnet.is_platform()); assert_eq!(core_mainnet.address_type(), Some(AddressType::P2pkh)); } @@ -2405,11 +2410,13 @@ mod tests { #[test] fn test_platform_p2sh_parsing() { // Test Platform P2SH address parsing - let mainnet_p2sh = Address::from_str("Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k").unwrap().assume_checked(); + let mainnet_p2sh = + Address::from_str("Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k").unwrap().assume_checked(); assert!(mainnet_p2sh.is_platform()); assert_eq!(mainnet_p2sh.address_type(), Some(AddressType::PlatformP2sh)); - let testnet_p2sh = Address::from_str("pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh").unwrap().assume_checked(); + let testnet_p2sh = + Address::from_str("pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh").unwrap().assume_checked(); assert!(testnet_p2sh.is_platform()); assert_eq!(testnet_p2sh.address_type(), Some(AddressType::PlatformP2sh)); } diff --git a/key-wallet/tests/derivation_tests.rs b/key-wallet/tests/derivation_tests.rs index 6be6c7aa2..17c148216 100644 --- a/key-wallet/tests/derivation_tests.rs +++ b/key-wallet/tests/derivation_tests.rs @@ -1,5 +1,6 @@ //! Derivation tests +use dashcore::hashes::Hash; use key_wallet::derivation::{AccountDerivation, HDWallet}; use key_wallet::mnemonic::{Language, Mnemonic}; use key_wallet::{DerivationPath, ExtendedPubKey, Network}; @@ -109,3 +110,279 @@ fn test_public_key_derivation() { assert_eq!(xpub.public_key, xpub_from_prv.public_key); } + +// ============================================================================= +// DIP-17/DIP-18 Platform Payment Address Test Vectors +// ============================================================================= +// +// These tests verify the complete derivation and encoding of Platform Payment +// addresses as specified in DIP-0017 (HD Derivation) and DIP-0018 (Address Encoding). +// +// Test mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" +// Passphrase: "" (empty) + +/// DIP-17 Test Vector 1: Platform Payment address derivation (mainnet) +/// Path: m/9'/5'/17'/0'/0'/0 +/// Expected private key: 6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe +/// Expected pubkey: 03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e +/// Expected HASH160: f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 +/// Expected mainnet address (DIP-18): DTjceJiEqrNkCsSizK65fojEANTKoQMtsR +#[test] +fn test_dip17_platform_payment_vector1_mainnet() { + use dashcore::address::{Address, Payload}; + use dashcore::crypto::key::PublicKey; + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English + ).unwrap(); + + let seed = mnemonic.to_seed(""); + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Derive Platform Payment key: m/9'/5'/17'/0'/0'/0 + let path = DerivationPath::from_str("m/9'/5'/17'/0'/0'/0").unwrap(); + let xprv = wallet.derive(&path).unwrap(); + + // Verify private key matches DIP-17 test vector + let privkey_hex = hex::encode(xprv.private_key.secret_bytes()); + assert_eq!( + privkey_hex, + "6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe", + "Private key mismatch for DIP-17 vector 1" + ); + + // Get compressed public key + let secp = Secp256k1::new(); + let xpub = ExtendedPubKey::from_priv(&secp, &xprv); + let pubkey = PublicKey::new(xpub.public_key); + let pubkey_hex = hex::encode(pubkey.to_bytes()); + assert_eq!( + pubkey_hex, + "03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e", + "Public key mismatch for DIP-17 vector 1" + ); + + // Verify HASH160 + let pubkey_hash = pubkey.pubkey_hash(); + let hash160_hex = hex::encode(pubkey_hash.to_byte_array()); + assert_eq!( + hash160_hex, + "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", + "HASH160 mismatch for DIP-17 vector 1" + ); + + // Create Platform P2PKH address and verify DIP-18 encoding + let addr = Address::new( + dashcore::Network::Dash, + Payload::PlatformPubkeyHash(pubkey_hash), + ); + assert_eq!( + addr.to_string(), + "DTjceJiEqrNkCsSizK65fojEANTKoQMtsR", + "DIP-18 mainnet address mismatch for vector 1" + ); +} + +/// DIP-17 Test Vector 1: Platform Payment address derivation (testnet) +/// Path: m/9'/1'/17'/0'/0'/0 (note: coin_type 1' for testnet) +/// Expected testnet address (DIP-18): dSqV2orinasFpYAMGQTLy6uYpW9Dnge563 +#[test] +fn test_dip17_platform_payment_vector1_testnet() { + use dashcore::address::{Address, Payload}; + use dashcore::crypto::key::PublicKey; + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English + ).unwrap(); + + let seed = mnemonic.to_seed(""); + let wallet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); + + // Derive Platform Payment key: m/9'/1'/17'/0'/0'/0 (testnet uses coin_type 1') + let path = DerivationPath::from_str("m/9'/1'/17'/0'/0'/0").unwrap(); + let xprv = wallet.derive(&path).unwrap(); + + // Get compressed public key and HASH160 + let secp = Secp256k1::new(); + let xpub = ExtendedPubKey::from_priv(&secp, &xprv); + let pubkey = PublicKey::new(xpub.public_key); + let pubkey_hash = pubkey.pubkey_hash(); + + // Create Platform P2PKH address and verify DIP-18 encoding + let addr = Address::new( + dashcore::Network::Testnet, + Payload::PlatformPubkeyHash(pubkey_hash), + ); + assert_eq!( + addr.to_string(), + "dSqV2orinasFpYAMGQTLy6uYpW9Dnge563", + "DIP-18 testnet address mismatch for vector 1" + ); +} + +/// DIP-17 Test Vector 2: Platform Payment address (index 1) +/// Path: m/9'/5'/17'/0'/0'/1 (mainnet) / m/9'/1'/17'/0'/0'/1 (testnet) +/// Expected private key: eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523 +/// Expected pubkey: 02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da +/// Expected HASH160: a5ff0046217fd1c7d238e3e146cc5bfd90832a7e +/// Expected mainnet address: DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm +/// Expected testnet address: dZoepEc46ivSfm3VYr8mJeA4hZXYytgkKZ +#[test] +fn test_dip17_platform_payment_vector2() { + use dashcore::address::{Address, Payload}; + use dashcore::crypto::key::PublicKey; + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English + ).unwrap(); + + // Test mainnet + let seed = mnemonic.to_seed(""); + let wallet_mainnet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + let path_mainnet = DerivationPath::from_str("m/9'/5'/17'/0'/0'/1").unwrap(); + let xprv_mainnet = wallet_mainnet.derive(&path_mainnet).unwrap(); + + // Verify private key + let privkey_hex = hex::encode(xprv_mainnet.private_key.secret_bytes()); + assert_eq!( + privkey_hex, + "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523", + "Private key mismatch for DIP-17 vector 2" + ); + + let secp = Secp256k1::new(); + let xpub_mainnet = ExtendedPubKey::from_priv(&secp, &xprv_mainnet); + let pubkey_mainnet = PublicKey::new(xpub_mainnet.public_key); + + // Verify public key + let pubkey_hex = hex::encode(pubkey_mainnet.to_bytes()); + assert_eq!( + pubkey_hex, + "02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da", + "Public key mismatch for DIP-17 vector 2" + ); + + // Verify HASH160 + let pubkey_hash_mainnet = pubkey_mainnet.pubkey_hash(); + let hash160_hex = hex::encode(pubkey_hash_mainnet.to_byte_array()); + assert_eq!( + hash160_hex, + "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", + "HASH160 mismatch for DIP-17 vector 2" + ); + + // Verify mainnet address + let addr_mainnet = Address::new( + dashcore::Network::Dash, + Payload::PlatformPubkeyHash(pubkey_hash_mainnet), + ); + assert_eq!( + addr_mainnet.to_string(), + "DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm", + "DIP-18 mainnet address mismatch for vector 2" + ); + + // Test testnet + let wallet_testnet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); + let path_testnet = DerivationPath::from_str("m/9'/1'/17'/0'/0'/1").unwrap(); + let xprv_testnet = wallet_testnet.derive(&path_testnet).unwrap(); + let xpub_testnet = ExtendedPubKey::from_priv(&secp, &xprv_testnet); + let pubkey_testnet = PublicKey::new(xpub_testnet.public_key); + let pubkey_hash_testnet = pubkey_testnet.pubkey_hash(); + + let addr_testnet = Address::new( + dashcore::Network::Testnet, + Payload::PlatformPubkeyHash(pubkey_hash_testnet), + ); + assert_eq!( + addr_testnet.to_string(), + "dZoepEc46ivSfm3VYr8mJeA4hZXYytgkKZ", + "DIP-18 testnet address mismatch for vector 2" + ); +} + +/// DIP-17 Test Vector 3: Platform Payment address with non-default key_class +/// Path: m/9'/5'/17'/0'/1'/0 (mainnet) / m/9'/1'/17'/0'/1'/0 (testnet) +/// Note: key_class' = 1' instead of default 0' +/// Expected private key: cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db +/// Expected pubkey: 0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3 +/// Expected HASH160: 6d92674fd64472a3dfcfc3ebcfed7382bf699d7b +/// Expected mainnet address: DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo +/// Expected testnet address: dFQSkGujaeDNwWTQDDVfbrurQ9ChXXYDov +#[test] +fn test_dip17_platform_payment_vector3_non_default_key_class() { + use dashcore::address::{Address, Payload}; + use dashcore::crypto::key::PublicKey; + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English + ).unwrap(); + + // Test mainnet with key_class' = 1' + let seed = mnemonic.to_seed(""); + let wallet_mainnet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + let path_mainnet = DerivationPath::from_str("m/9'/5'/17'/0'/1'/0").unwrap(); + let xprv_mainnet = wallet_mainnet.derive(&path_mainnet).unwrap(); + + // Verify private key + let privkey_hex = hex::encode(xprv_mainnet.private_key.secret_bytes()); + assert_eq!( + privkey_hex, + "cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db", + "Private key mismatch for DIP-17 vector 3" + ); + + let secp = Secp256k1::new(); + let xpub_mainnet = ExtendedPubKey::from_priv(&secp, &xprv_mainnet); + let pubkey_mainnet = PublicKey::new(xpub_mainnet.public_key); + + // Verify public key + let pubkey_hex = hex::encode(pubkey_mainnet.to_bytes()); + assert_eq!( + pubkey_hex, + "0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3", + "Public key mismatch for DIP-17 vector 3" + ); + + // Verify HASH160 + let pubkey_hash_mainnet = pubkey_mainnet.pubkey_hash(); + let hash160_hex = hex::encode(pubkey_hash_mainnet.to_byte_array()); + assert_eq!( + hash160_hex, + "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", + "HASH160 mismatch for DIP-17 vector 3" + ); + + // Verify mainnet address + let addr_mainnet = Address::new( + dashcore::Network::Dash, + Payload::PlatformPubkeyHash(pubkey_hash_mainnet), + ); + assert_eq!( + addr_mainnet.to_string(), + "DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo", + "DIP-18 mainnet address mismatch for vector 3" + ); + + // Test testnet with key_class' = 1' + let wallet_testnet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); + let path_testnet = DerivationPath::from_str("m/9'/1'/17'/0'/1'/0").unwrap(); + let xprv_testnet = wallet_testnet.derive(&path_testnet).unwrap(); + let xpub_testnet = ExtendedPubKey::from_priv(&secp, &xprv_testnet); + let pubkey_testnet = PublicKey::new(xpub_testnet.public_key); + let pubkey_hash_testnet = pubkey_testnet.pubkey_hash(); + + let addr_testnet = Address::new( + dashcore::Network::Testnet, + Payload::PlatformPubkeyHash(pubkey_hash_testnet), + ); + assert_eq!( + addr_testnet.to_string(), + "dFQSkGujaeDNwWTQDDVfbrurQ9ChXXYDov", + "DIP-18 testnet address mismatch for vector 3" + ); +} From 0374ff288be8923143b50aaa061aba92c1c897f4 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 2 Dec 2025 15:42:19 +0700 Subject: [PATCH 03/14] fix: platform address constant formatting --- dash/src/blockdata/constants.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dash/src/blockdata/constants.rs b/dash/src/blockdata/constants.rs index 1e6c72af8..2c931defb 100644 --- a/dash/src/blockdata/constants.rs +++ b/dash/src/blockdata/constants.rs @@ -58,13 +58,17 @@ pub const SCRIPT_ADDRESS_PREFIX_TEST: u8 = 19; // DIP-18: Platform Payment Address Prefixes /// Platform payment P2PKH address prefix (mainnet) - produces 'D' prefix. -pub const PLATFORM_P2PKH_PREFIX_MAIN: u8 = 0x1e; // 30 +pub const PLATFORM_P2PKH_PREFIX_MAIN: u8 = 30; +// 0x1e /// Platform payment P2PKH address prefix (testnet/devnet/regtest) - produces 'd' prefix. -pub const PLATFORM_P2PKH_PREFIX_TEST: u8 = 0x5a; // 90 +pub const PLATFORM_P2PKH_PREFIX_TEST: u8 = 90; +// 0x5a /// Platform payment P2SH address prefix (mainnet) - produces 'P' prefix. -pub const PLATFORM_P2SH_PREFIX_MAIN: u8 = 0x38; // 56 +pub const PLATFORM_P2SH_PREFIX_MAIN: u8 = 56; +// 0x38 /// Platform payment P2SH address prefix (testnet/devnet/regtest) - produces 'p' prefix. -pub const PLATFORM_P2SH_PREFIX_TEST: u8 = 0x75; // 117 +pub const PLATFORM_P2SH_PREFIX_TEST: u8 = 117; +// 0x75 /// The maximum allowed script size. pub const MAX_SCRIPT_ELEMENT_SIZE: usize = 520; From 9f8a29cd856337b46c4e7b51eadf3b629caa1cbb Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 2 Dec 2025 15:56:23 +0700 Subject: [PATCH 04/14] chore: cleanup docs --- key-wallet/src/dip9.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/key-wallet/src/dip9.rs b/key-wallet/src/dip9.rs index a69386c20..76f6ca0e8 100644 --- a/key-wallet/src/dip9.rs +++ b/key-wallet/src/dip9.rs @@ -27,7 +27,6 @@ pub enum DerivationPathReference { BlockchainIdentityCreditInvitationFunding = 13, ProviderPlatformNodeKeys = 14, CoinJoin = 15, - /// DIP-17: Platform Payment Addresses PlatformPayment = 16, Root = 255, } From 4f3bf506056f337cb0231503702becef16032c27 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 2 Dec 2025 15:59:16 +0700 Subject: [PATCH 05/14] fmt --- key-wallet/src/account/account_collection.rs | 10 ++- key-wallet/src/account/account_type.rs | 3 +- .../transaction_checking/account_checker.rs | 4 +- key-wallet/tests/derivation_tests.rs | 61 ++++++------------- 4 files changed, 31 insertions(+), 47 deletions(-) diff --git a/key-wallet/src/account/account_collection.rs b/key-wallet/src/account/account_collection.rs index d141498b3..eb9dbaaad 100644 --- a/key-wallet/src/account/account_collection.rs +++ b/key-wallet/src/account/account_collection.rs @@ -331,7 +331,10 @@ impl AccountCollection { account, key_class, } => { - let key = PlatformPaymentAccountKey { account, key_class }; + let key = PlatformPaymentAccountKey { + account, + key_class, + }; self.platform_payment_accounts.get(&key) } } @@ -391,7 +394,10 @@ impl AccountCollection { account, key_class, } => { - let key = PlatformPaymentAccountKey { account, key_class }; + let key = PlatformPaymentAccountKey { + account, + key_class, + }; self.platform_payment_accounts.get_mut(&key) } } diff --git a/key-wallet/src/account/account_type.rs b/key-wallet/src/account/account_type.rs index 7983ba9a6..95334d08f 100644 --- a/key-wallet/src/account/account_type.rs +++ b/key-wallet/src/account/account_type.rs @@ -426,8 +426,7 @@ impl AccountType { _ => return Err(crate::error::Error::InvalidNetwork), }; path.push( - ChildNumber::from_hardened_idx(*account) - .map_err(crate::error::Error::Bip32)?, + ChildNumber::from_hardened_idx(*account).map_err(crate::error::Error::Bip32)?, ); path.push( ChildNumber::from_hardened_idx(*key_class) diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index cf6a6e141..f79380e25 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -646,7 +646,9 @@ impl ManagedAccount { involved_addresses: involved_other_addresses, }, ManagedAccountType::PlatformPayment { - account, key_class, .. + account, + key_class, + .. } => AccountTypeMatch::PlatformPayment { account_index: *account, key_class: *key_class, diff --git a/key-wallet/tests/derivation_tests.rs b/key-wallet/tests/derivation_tests.rs index 17c148216..67a25b448 100644 --- a/key-wallet/tests/derivation_tests.rs +++ b/key-wallet/tests/derivation_tests.rs @@ -147,8 +147,7 @@ fn test_dip17_platform_payment_vector1_mainnet() { // Verify private key matches DIP-17 test vector let privkey_hex = hex::encode(xprv.private_key.secret_bytes()); assert_eq!( - privkey_hex, - "6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe", + privkey_hex, "6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe", "Private key mismatch for DIP-17 vector 1" ); @@ -158,8 +157,7 @@ fn test_dip17_platform_payment_vector1_mainnet() { let pubkey = PublicKey::new(xpub.public_key); let pubkey_hex = hex::encode(pubkey.to_bytes()); assert_eq!( - pubkey_hex, - "03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e", + pubkey_hex, "03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e", "Public key mismatch for DIP-17 vector 1" ); @@ -167,16 +165,12 @@ fn test_dip17_platform_payment_vector1_mainnet() { let pubkey_hash = pubkey.pubkey_hash(); let hash160_hex = hex::encode(pubkey_hash.to_byte_array()); assert_eq!( - hash160_hex, - "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", + hash160_hex, "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", "HASH160 mismatch for DIP-17 vector 1" ); // Create Platform P2PKH address and verify DIP-18 encoding - let addr = Address::new( - dashcore::Network::Dash, - Payload::PlatformPubkeyHash(pubkey_hash), - ); + let addr = Address::new(dashcore::Network::Dash, Payload::PlatformPubkeyHash(pubkey_hash)); assert_eq!( addr.to_string(), "DTjceJiEqrNkCsSizK65fojEANTKoQMtsR", @@ -211,10 +205,7 @@ fn test_dip17_platform_payment_vector1_testnet() { let pubkey_hash = pubkey.pubkey_hash(); // Create Platform P2PKH address and verify DIP-18 encoding - let addr = Address::new( - dashcore::Network::Testnet, - Payload::PlatformPubkeyHash(pubkey_hash), - ); + let addr = Address::new(dashcore::Network::Testnet, Payload::PlatformPubkeyHash(pubkey_hash)); assert_eq!( addr.to_string(), "dSqV2orinasFpYAMGQTLy6uYpW9Dnge563", @@ -248,8 +239,7 @@ fn test_dip17_platform_payment_vector2() { // Verify private key let privkey_hex = hex::encode(xprv_mainnet.private_key.secret_bytes()); assert_eq!( - privkey_hex, - "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523", + privkey_hex, "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523", "Private key mismatch for DIP-17 vector 2" ); @@ -260,8 +250,7 @@ fn test_dip17_platform_payment_vector2() { // Verify public key let pubkey_hex = hex::encode(pubkey_mainnet.to_bytes()); assert_eq!( - pubkey_hex, - "02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da", + pubkey_hex, "02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da", "Public key mismatch for DIP-17 vector 2" ); @@ -269,16 +258,13 @@ fn test_dip17_platform_payment_vector2() { let pubkey_hash_mainnet = pubkey_mainnet.pubkey_hash(); let hash160_hex = hex::encode(pubkey_hash_mainnet.to_byte_array()); assert_eq!( - hash160_hex, - "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", + hash160_hex, "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", "HASH160 mismatch for DIP-17 vector 2" ); // Verify mainnet address - let addr_mainnet = Address::new( - dashcore::Network::Dash, - Payload::PlatformPubkeyHash(pubkey_hash_mainnet), - ); + let addr_mainnet = + Address::new(dashcore::Network::Dash, Payload::PlatformPubkeyHash(pubkey_hash_mainnet)); assert_eq!( addr_mainnet.to_string(), "DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm", @@ -293,10 +279,8 @@ fn test_dip17_platform_payment_vector2() { let pubkey_testnet = PublicKey::new(xpub_testnet.public_key); let pubkey_hash_testnet = pubkey_testnet.pubkey_hash(); - let addr_testnet = Address::new( - dashcore::Network::Testnet, - Payload::PlatformPubkeyHash(pubkey_hash_testnet), - ); + let addr_testnet = + Address::new(dashcore::Network::Testnet, Payload::PlatformPubkeyHash(pubkey_hash_testnet)); assert_eq!( addr_testnet.to_string(), "dZoepEc46ivSfm3VYr8mJeA4hZXYytgkKZ", @@ -331,8 +315,7 @@ fn test_dip17_platform_payment_vector3_non_default_key_class() { // Verify private key let privkey_hex = hex::encode(xprv_mainnet.private_key.secret_bytes()); assert_eq!( - privkey_hex, - "cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db", + privkey_hex, "cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db", "Private key mismatch for DIP-17 vector 3" ); @@ -343,8 +326,7 @@ fn test_dip17_platform_payment_vector3_non_default_key_class() { // Verify public key let pubkey_hex = hex::encode(pubkey_mainnet.to_bytes()); assert_eq!( - pubkey_hex, - "0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3", + pubkey_hex, "0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3", "Public key mismatch for DIP-17 vector 3" ); @@ -352,16 +334,13 @@ fn test_dip17_platform_payment_vector3_non_default_key_class() { let pubkey_hash_mainnet = pubkey_mainnet.pubkey_hash(); let hash160_hex = hex::encode(pubkey_hash_mainnet.to_byte_array()); assert_eq!( - hash160_hex, - "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", + hash160_hex, "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", "HASH160 mismatch for DIP-17 vector 3" ); // Verify mainnet address - let addr_mainnet = Address::new( - dashcore::Network::Dash, - Payload::PlatformPubkeyHash(pubkey_hash_mainnet), - ); + let addr_mainnet = + Address::new(dashcore::Network::Dash, Payload::PlatformPubkeyHash(pubkey_hash_mainnet)); assert_eq!( addr_mainnet.to_string(), "DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo", @@ -376,10 +355,8 @@ fn test_dip17_platform_payment_vector3_non_default_key_class() { let pubkey_testnet = PublicKey::new(xpub_testnet.public_key); let pubkey_hash_testnet = pubkey_testnet.pubkey_hash(); - let addr_testnet = Address::new( - dashcore::Network::Testnet, - Payload::PlatformPubkeyHash(pubkey_hash_testnet), - ); + let addr_testnet = + Address::new(dashcore::Network::Testnet, Payload::PlatformPubkeyHash(pubkey_hash_testnet)); assert_eq!( addr_testnet.to_string(), "dFQSkGujaeDNwWTQDDVfbrurQ9ChXXYDov", From 09546cccb8202ec819df37cc3661b3ed6efced5a Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 5 Dec 2025 15:11:06 +0700 Subject: [PATCH 06/14] fix: error handling --- dash/src/address.rs | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/dash/src/address.rs b/dash/src/address.rs index 39228ffb9..0ce06d81f 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -78,6 +78,8 @@ pub enum Error { Base58(base58::Error), /// Bech32 encoding error. Bech32(bech32::Error), + /// Invalid hash length when parsing address. + InvalidHashLength(hashes::Error), /// The bech32 payload was empty. EmptyBech32Payload, /// The wrong checksum algorithm was used. See BIP-0350. @@ -121,6 +123,7 @@ impl fmt::Display for Error { match *self { Error::Base58(ref e) => write_err!(f, "base58 address encoding error"; e), Error::Bech32(ref e) => write_err!(f, "bech32 address encoding error"; e), + Error::InvalidHashLength(ref e) => write_err!(f, "invalid hash length"; e), Error::EmptyBech32Payload => write!(f, "the bech32 payload was empty"), Error::InvalidBech32Variant { expected, found } => write!(f, "invalid bech32 checksum variant found {:?} when {:?} was expected", found, expected), Error::InvalidWitnessVersion(v) => write!(f, "invalid witness script version: {}", v), @@ -149,6 +152,7 @@ impl std::error::Error for Error { match self { Base58(e) => Some(e), Bech32(e) => Some(e), + InvalidHashLength(e) => Some(e), UnparsableWitnessVersion(e) => Some(e), EmptyBech32Payload | InvalidBech32Variant { @@ -183,6 +187,13 @@ impl From for Error { } } +#[doc(hidden)] +impl From for Error { + fn from(e: hashes::Error) -> Error { + Error::InvalidHashLength(e) + } +} + /// The different types of addresses. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1537,33 +1548,33 @@ impl FromStr for Address { let (network, payload) = match data[0] { PUBKEY_ADDRESS_PREFIX_MAIN => { - (Network::Dash, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap())) + (Network::Dash, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..])?)) } SCRIPT_ADDRESS_PREFIX_MAIN => { - (Network::Dash, Payload::ScriptHash(ScriptHash::from_slice(&data[1..]).unwrap())) + (Network::Dash, Payload::ScriptHash(ScriptHash::from_slice(&data[1..])?)) } PUBKEY_ADDRESS_PREFIX_TEST => { - (Network::Testnet, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap())) + (Network::Testnet, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..])?)) } SCRIPT_ADDRESS_PREFIX_TEST => { - (Network::Testnet, Payload::ScriptHash(ScriptHash::from_slice(&data[1..]).unwrap())) + (Network::Testnet, Payload::ScriptHash(ScriptHash::from_slice(&data[1..])?)) } // DIP-18: Platform address prefixes PLATFORM_P2PKH_PREFIX_MAIN => ( Network::Dash, - Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap()), + Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..])?), ), PLATFORM_P2SH_PREFIX_MAIN => ( Network::Dash, - Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..]).unwrap()), + Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..])?), ), PLATFORM_P2PKH_PREFIX_TEST => ( Network::Testnet, - Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap()), + Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..])?), ), PLATFORM_P2SH_PREFIX_TEST => ( Network::Testnet, - Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..]).unwrap()), + Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..])?), ), x => return Err(Error::Base58(base58::Error::InvalidAddressVersion(x))), }; From b4c12aa2964b76606a0520aa10a79a8a58e1cdcf Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 5 Dec 2025 15:13:03 +0700 Subject: [PATCH 07/14] fix: gap limit --- key-wallet/src/gap_limit.rs | 3 +++ key-wallet/src/managed_account/managed_account_type.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/key-wallet/src/gap_limit.rs b/key-wallet/src/gap_limit.rs index 688d8af4a..4aebdaae5 100644 --- a/key-wallet/src/gap_limit.rs +++ b/key-wallet/src/gap_limit.rs @@ -21,6 +21,9 @@ pub const DEFAULT_COINJOIN_GAP_LIMIT: u32 = 30; /// Standard gap limit for special purpose keys (identity, provider keys) pub const DEFAULT_SPECIAL_GAP_LIMIT: u32 = 5; + +/// Gap limit for DIP-17 platform payment addresses +pub const DIP17_GAP_LIMIT: u32 = 20; /// Maximum gap limit to prevent excessive address generation pub const MAX_GAP_LIMIT: u32 = 1000; diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index d794b9563..d284854f9 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -2,7 +2,7 @@ use crate::account::account_collection::{DashpayContactIdentityId, DashpayOurUse use crate::account::StandardAccountType; use crate::gap_limit::{ DEFAULT_COINJOIN_GAP_LIMIT, DEFAULT_EXTERNAL_GAP_LIMIT, DEFAULT_INTERNAL_GAP_LIMIT, - DEFAULT_SPECIAL_GAP_LIMIT, + DEFAULT_SPECIAL_GAP_LIMIT, DIP17_GAP_LIMIT, }; use crate::{AccountType, AddressPool, DerivationPath}; @@ -688,7 +688,7 @@ impl ManagedAccountType { let pool = AddressPool::new_platform( path, crate::managed_account::address_pool::AddressPoolType::Absent, - DEFAULT_SPECIAL_GAP_LIMIT, // DIP-17 recommends gap limit of 20 + DIP17_GAP_LIMIT, network, key_source, )?; From ca7ddec28c329e2e47ca84c4a3d3a5b13b344379 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 5 Dec 2025 15:15:21 +0700 Subject: [PATCH 08/14] chore: add test --- key-wallet-ffi/src/types.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index e7e030ef9..dc28b1bf5 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -447,6 +447,13 @@ mod tests { let _ = FFIAccountType::DashpayExternalAccount.to_account_type(0); } + #[test] + #[should_panic(expected = "PlatformPayment cannot be converted to AccountType")] + fn test_platform_payment_to_account_type_panics() { + // This should panic because we cannot construct a Platform Payment account without indices + let _ = FFIAccountType::PlatformPayment.to_account_type(0); + } + #[test] #[should_panic(expected = "Cannot convert AccountType::DashpayReceivingFunds")] fn test_dashpay_receiving_funds_from_account_type_panics() { From 66e86abb67617924055f5f3443c2ffaa31f51ad0 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 5 Dec 2025 15:21:15 +0700 Subject: [PATCH 09/14] fmt --- dash/src/address.rs | 28 ++++++++++++---------------- key-wallet/src/gap_limit.rs | 1 + 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/dash/src/address.rs b/dash/src/address.rs index 0ce06d81f..b2227d948 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -1560,22 +1560,18 @@ impl FromStr for Address { (Network::Testnet, Payload::ScriptHash(ScriptHash::from_slice(&data[1..])?)) } // DIP-18: Platform address prefixes - PLATFORM_P2PKH_PREFIX_MAIN => ( - Network::Dash, - Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..])?), - ), - PLATFORM_P2SH_PREFIX_MAIN => ( - Network::Dash, - Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..])?), - ), - PLATFORM_P2PKH_PREFIX_TEST => ( - Network::Testnet, - Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..])?), - ), - PLATFORM_P2SH_PREFIX_TEST => ( - Network::Testnet, - Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..])?), - ), + PLATFORM_P2PKH_PREFIX_MAIN => { + (Network::Dash, Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..])?)) + } + PLATFORM_P2SH_PREFIX_MAIN => { + (Network::Dash, Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..])?)) + } + PLATFORM_P2PKH_PREFIX_TEST => { + (Network::Testnet, Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..])?)) + } + PLATFORM_P2SH_PREFIX_TEST => { + (Network::Testnet, Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..])?)) + } x => return Err(Error::Base58(base58::Error::InvalidAddressVersion(x))), }; diff --git a/key-wallet/src/gap_limit.rs b/key-wallet/src/gap_limit.rs index 4aebdaae5..9d4d5b088 100644 --- a/key-wallet/src/gap_limit.rs +++ b/key-wallet/src/gap_limit.rs @@ -24,6 +24,7 @@ pub const DEFAULT_SPECIAL_GAP_LIMIT: u32 = 5; /// Gap limit for DIP-17 platform payment addresses pub const DIP17_GAP_LIMIT: u32 = 20; + /// Maximum gap limit to prevent excessive address generation pub const MAX_GAP_LIMIT: u32 = 1000; From bc654634c0e64529eafb650e9fcab7a98dd610f5 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 10 Dec 2025 15:39:15 +0700 Subject: [PATCH 10/14] refactor: remove changes from dash crate --- dash/src/address.rs | 332 +----------------- dash/src/blockdata/constants.rs | 15 - .../src/managed_account/address_pool.rs | 50 +-- .../managed_account_collection.rs | 2 +- .../managed_account/managed_account_type.rs | 2 +- key-wallet/tests/derivation_tests.rs | 106 ++---- 6 files changed, 52 insertions(+), 455 deletions(-) diff --git a/dash/src/address.rs b/dash/src/address.rs index b2227d948..02e4ba5f0 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -48,9 +48,8 @@ use core::str::FromStr; use crate::base58; use crate::blockdata::constants::{ - MAX_SCRIPT_ELEMENT_SIZE, PLATFORM_P2PKH_PREFIX_MAIN, PLATFORM_P2PKH_PREFIX_TEST, - PLATFORM_P2SH_PREFIX_MAIN, PLATFORM_P2SH_PREFIX_TEST, PUBKEY_ADDRESS_PREFIX_MAIN, - PUBKEY_ADDRESS_PREFIX_TEST, SCRIPT_ADDRESS_PREFIX_MAIN, SCRIPT_ADDRESS_PREFIX_TEST, + MAX_SCRIPT_ELEMENT_SIZE, PUBKEY_ADDRESS_PREFIX_MAIN, PUBKEY_ADDRESS_PREFIX_TEST, + SCRIPT_ADDRESS_PREFIX_MAIN, SCRIPT_ADDRESS_PREFIX_TEST, }; use crate::blockdata::opcodes; use crate::blockdata::opcodes::all::*; @@ -78,8 +77,6 @@ pub enum Error { Base58(base58::Error), /// Bech32 encoding error. Bech32(bech32::Error), - /// Invalid hash length when parsing address. - InvalidHashLength(hashes::Error), /// The bech32 payload was empty. EmptyBech32Payload, /// The wrong checksum algorithm was used. See BIP-0350. @@ -123,7 +120,6 @@ impl fmt::Display for Error { match *self { Error::Base58(ref e) => write_err!(f, "base58 address encoding error"; e), Error::Bech32(ref e) => write_err!(f, "bech32 address encoding error"; e), - Error::InvalidHashLength(ref e) => write_err!(f, "invalid hash length"; e), Error::EmptyBech32Payload => write!(f, "the bech32 payload was empty"), Error::InvalidBech32Variant { expected, found } => write!(f, "invalid bech32 checksum variant found {:?} when {:?} was expected", found, expected), Error::InvalidWitnessVersion(v) => write!(f, "invalid witness script version: {}", v), @@ -152,7 +148,6 @@ impl std::error::Error for Error { match self { Base58(e) => Some(e), Bech32(e) => Some(e), - InvalidHashLength(e) => Some(e), UnparsableWitnessVersion(e) => Some(e), EmptyBech32Payload | InvalidBech32Variant { @@ -187,13 +182,6 @@ impl From for Error { } } -#[doc(hidden)] -impl From for Error { - fn from(e: hashes::Error) -> Error { - Error::InvalidHashLength(e) - } -} - /// The different types of addresses. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -209,12 +197,6 @@ pub enum AddressType { P2wsh, /// Pay to taproot. P2tr, - /// Platform payment P2PKH (DIP-18) - "D" prefix on mainnet, "d" on testnet. - /// These addresses are for Dash Platform and MUST NOT be used in Core chain transactions. - PlatformP2pkh, - /// Platform payment P2SH (DIP-18) - "P" prefix on mainnet, "p" on testnet. - /// These addresses are for Dash Platform multisig/scripts and MUST NOT be used in Core chain transactions. - PlatformP2sh, } impl fmt::Display for AddressType { @@ -225,8 +207,6 @@ impl fmt::Display for AddressType { AddressType::P2wpkh => "p2wpkh", AddressType::P2wsh => "p2wsh", AddressType::P2tr => "p2tr", - AddressType::PlatformP2pkh => "platform_p2pkh", - AddressType::PlatformP2sh => "platform_p2sh", }) } } @@ -240,8 +220,6 @@ impl FromStr for AddressType { "p2wpkh" => Ok(AddressType::P2wpkh), "p2wsh" => Ok(AddressType::P2wsh), "p2tr" => Ok(AddressType::P2tr), - "platform_p2pkh" => Ok(AddressType::PlatformP2pkh), - "platform_p2sh" => Ok(AddressType::PlatformP2sh), _ => Err(Error::UnknownAddressType(s.to_owned())), } } @@ -459,12 +437,6 @@ pub enum Payload { ScriptHash(ScriptHash), /// Segwit address. WitnessProgram(WitnessProgram), - /// Platform P2PKH address (DIP-18). - /// These addresses are for Dash Platform payments and MUST NOT be used in Core chain transactions. - PlatformPubkeyHash(PubkeyHash), - /// Platform P2SH address (DIP-18). - /// These addresses are for Dash Platform scripts and MUST NOT be used in Core chain transactions. - PlatformScriptHash(ScriptHash), } impl Payload { @@ -543,17 +515,10 @@ impl Payload { } /// Generates a script pubkey spending to this [Payload]. - /// - /// Note: For Platform addresses, this generates the equivalent Core chain script, - /// but Platform addresses should NOT be used in Core chain transactions. pub fn script_pubkey(&self) -> ScriptBuf { match *self { - Payload::PubkeyHash(ref hash) | Payload::PlatformPubkeyHash(ref hash) => { - ScriptBuf::new_p2pkh(hash) - } - Payload::ScriptHash(ref hash) | Payload::PlatformScriptHash(ref hash) => { - ScriptBuf::new_p2sh(hash) - } + Payload::PubkeyHash(ref hash) => ScriptBuf::new_p2pkh(hash), + Payload::ScriptHash(ref hash) => ScriptBuf::new_p2sh(hash), Payload::WitnessProgram(ref prog) => ScriptBuf::new_witness_program(prog), } } @@ -562,24 +527,16 @@ impl Payload { /// This function doesn't make any allocations. pub fn matches_script_pubkey(&self, script: &Script) -> bool { match *self { - Payload::PubkeyHash(ref hash) | Payload::PlatformPubkeyHash(ref hash) - if script.is_p2pkh() => - { + Payload::PubkeyHash(ref hash) if script.is_p2pkh() => { &script.as_bytes()[3..23] == >::as_ref(hash) } - Payload::ScriptHash(ref hash) | Payload::PlatformScriptHash(ref hash) - if script.is_p2sh() => - { + Payload::ScriptHash(ref hash) if script.is_p2sh() => { &script.as_bytes()[2..22] == >::as_ref(hash) } Payload::WitnessProgram(ref prog) if script.is_witness_program() => { &script.as_bytes()[2..] == prog.program.as_bytes() } - Payload::PubkeyHash(_) - | Payload::ScriptHash(_) - | Payload::WitnessProgram(_) - | Payload::PlatformPubkeyHash(_) - | Payload::PlatformScriptHash(_) => false, + Payload::PubkeyHash(_) | Payload::ScriptHash(_) | Payload::WitnessProgram(_) => false, } } @@ -655,55 +612,11 @@ impl Payload { /// is a script hash or pubkey hash, a reference to the hash is returned. fn inner_prog_as_bytes(&self) -> &[u8] { match self { - Payload::ScriptHash(hash) | Payload::PlatformScriptHash(hash) => hash.as_ref(), - Payload::PubkeyHash(hash) | Payload::PlatformPubkeyHash(hash) => hash.as_ref(), + Payload::ScriptHash(hash) => hash.as_ref(), + Payload::PubkeyHash(hash) => hash.as_ref(), Payload::WitnessProgram(prog) => prog.program().as_bytes(), } } - - /// Creates a Platform P2PKH payload from a public key (DIP-18). - /// - /// This creates a payload for Dash Platform payment addresses. - /// These addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn platform_p2pkh(pk: &PublicKey) -> Payload { - Payload::PlatformPubkeyHash(pk.pubkey_hash()) - } - - /// Creates a Platform P2PKH payload from a pubkey hash (DIP-18). - /// - /// This creates a payload for Dash Platform payment addresses. - /// These addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn platform_p2pkh_from_hash(hash: PubkeyHash) -> Payload { - Payload::PlatformPubkeyHash(hash) - } - - /// Creates a Platform P2SH payload from a script (DIP-18). - /// - /// This creates a payload for Dash Platform script addresses. - /// These addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn platform_p2sh(script: &Script) -> Result { - if script.len() > MAX_SCRIPT_ELEMENT_SIZE { - return Err(Error::ExcessiveScriptSize); - } - Ok(Payload::PlatformScriptHash(script.script_hash())) - } - - /// Creates a Platform P2SH payload from a script hash (DIP-18). - /// - /// This creates a payload for Dash Platform script addresses. - /// These addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn platform_p2sh_from_hash(hash: ScriptHash) -> Payload { - Payload::PlatformScriptHash(hash) - } - - /// Returns true if this is a Platform address payload (DIP-18). - pub fn is_platform(&self) -> bool { - matches!(self, Payload::PlatformPubkeyHash(_) | Payload::PlatformScriptHash(_)) - } } /// A utility struct to encode an address payload with the given parameters. @@ -717,10 +630,6 @@ pub struct AddressEncoding<'a> { pub p2sh_prefix: u8, /// hrp used in bech32 address (e.g. "bc" for "bc1..." addresses). pub bech32_hrp: &'a str, - /// base58 version byte for Platform P2PKH payloads (DIP-18). - pub platform_p2pkh_prefix: u8, - /// base58 version byte for Platform P2SH payloads (DIP-18). - pub platform_p2sh_prefix: u8, } /// Formats bech32 as upper case if alternate formatting is chosen (`{:#}`). @@ -753,18 +662,6 @@ impl<'a> fmt::Display for AddressEncoding<'a> { bech32::WriteBase32::write_u5(&mut bech32_writer, version.into())?; bech32::ToBase32::write_base32(&prog.as_bytes(), &mut bech32_writer) } - Payload::PlatformPubkeyHash(hash) => { - let mut prefixed = [0; 21]; - prefixed[0] = self.platform_p2pkh_prefix; - prefixed[1..].copy_from_slice(&hash[..]); - base58::encode_check_to_fmt(fmt, &prefixed[..]) - } - Payload::PlatformScriptHash(hash) => { - let mut prefixed = [0; 21]; - prefixed[0] = self.platform_p2sh_prefix; - prefixed[1..].copy_from_slice(&hash[..]); - base58::encode_check_to_fmt(fmt, &prefixed[..]) - } } } } @@ -1013,8 +910,6 @@ impl bincode::Decode for AddressType { 2 => Ok(AddressType::P2wpkh), 3 => Ok(AddressType::P2wsh), 4 => Ok(AddressType::P2tr), - 5 => Ok(AddressType::PlatformP2pkh), - 6 => Ok(AddressType::PlatformP2sh), _ => Err(bincode::error::DecodeError::OtherString("invalid address type".to_string())), } } @@ -1032,8 +927,6 @@ impl<'de> bincode::BorrowDecode<'de> for AddressType { 2 => Ok(AddressType::P2wpkh), 3 => Ok(AddressType::P2wsh), 4 => Ok(AddressType::P2tr), - 5 => Ok(AddressType::PlatformP2pkh), - 6 => Ok(AddressType::PlatformP2sh), _ => Err(bincode::error::DecodeError::OtherString("invalid address type".to_string())), } } @@ -1079,8 +972,6 @@ impl Address { match self.payload() { Payload::PubkeyHash(_) => Some(AddressType::P2pkh), Payload::ScriptHash(_) => Some(AddressType::P2sh), - Payload::PlatformPubkeyHash(_) => Some(AddressType::PlatformP2pkh), - Payload::PlatformScriptHash(_) => Some(AddressType::PlatformP2sh), Payload::WitnessProgram(prog) => { // BIP-141 p2wpkh or p2wsh addresses. match prog.version() { @@ -1116,24 +1007,11 @@ impl Address { Network::Regtest => "dsrt", other => unreachable!("Unknown network {other:?} – add explicit prefix"), }; - // DIP-18: Platform address prefixes - let platform_p2pkh_prefix = match self.network() { - Network::Dash => PLATFORM_P2PKH_PREFIX_MAIN, - Network::Testnet | Network::Devnet | Network::Regtest => PLATFORM_P2PKH_PREFIX_TEST, - other => unreachable!("Unknown network {other:?} – add explicit prefix"), - }; - let platform_p2sh_prefix = match self.network() { - Network::Dash => PLATFORM_P2SH_PREFIX_MAIN, - Network::Testnet | Network::Devnet | Network::Regtest => PLATFORM_P2SH_PREFIX_TEST, - other => unreachable!("Unknown network {other:?} – add explicit prefix"), - }; let encoding = AddressEncoding { payload: self.payload(), p2pkh_prefix, p2sh_prefix, bech32_hrp, - platform_p2pkh_prefix, - platform_p2sh_prefix, }; use fmt::Display; @@ -1223,50 +1101,6 @@ impl Address { Address::new(network, Payload::p2tr_tweaked(output_key)) } - /// Creates a Platform P2PKH address from a public key (DIP-18). - /// - /// This creates a Dash Platform payment address ("D" prefix on mainnet, "d" on testnet). - /// **Warning**: These addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn platform_p2pkh(pk: &PublicKey, network: Network) -> Address { - Address::new(network, Payload::platform_p2pkh(pk)) - } - - /// Creates a Platform P2PKH address from a pubkey hash (DIP-18). - /// - /// This creates a Dash Platform payment address ("D" prefix on mainnet, "d" on testnet). - /// **Warning**: These addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn platform_p2pkh_from_hash(hash: PubkeyHash, network: Network) -> Address { - Address::new(network, Payload::platform_p2pkh_from_hash(hash)) - } - - /// Creates a Platform P2SH address from a script (DIP-18). - /// - /// This creates a Dash Platform script address ("P" prefix on mainnet, "p" on testnet). - /// **Warning**: These addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn platform_p2sh(script: &Script, network: Network) -> Result { - Ok(Address::new(network, Payload::platform_p2sh(script)?)) - } - - /// Creates a Platform P2SH address from a script hash (DIP-18). - /// - /// This creates a Dash Platform script address ("P" prefix on mainnet, "p" on testnet). - /// **Warning**: These addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn platform_p2sh_from_hash(hash: ScriptHash, network: Network) -> Address { - Address::new(network, Payload::platform_p2sh_from_hash(hash)) - } - - /// Returns true if this is a Platform address (DIP-18). - /// - /// Platform addresses MUST NOT be used in Core chain transactions. - #[inline] - pub fn is_platform(&self) -> bool { - self.payload().is_platform() - } - /// Gets the address type of the address. /// /// # Returns @@ -1548,29 +1382,16 @@ impl FromStr for Address { let (network, payload) = match data[0] { PUBKEY_ADDRESS_PREFIX_MAIN => { - (Network::Dash, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..])?)) + (Network::Dash, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap())) } SCRIPT_ADDRESS_PREFIX_MAIN => { - (Network::Dash, Payload::ScriptHash(ScriptHash::from_slice(&data[1..])?)) + (Network::Dash, Payload::ScriptHash(ScriptHash::from_slice(&data[1..]).unwrap())) } PUBKEY_ADDRESS_PREFIX_TEST => { - (Network::Testnet, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..])?)) + (Network::Testnet, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap())) } SCRIPT_ADDRESS_PREFIX_TEST => { - (Network::Testnet, Payload::ScriptHash(ScriptHash::from_slice(&data[1..])?)) - } - // DIP-18: Platform address prefixes - PLATFORM_P2PKH_PREFIX_MAIN => { - (Network::Dash, Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..])?)) - } - PLATFORM_P2SH_PREFIX_MAIN => { - (Network::Dash, Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..])?)) - } - PLATFORM_P2PKH_PREFIX_TEST => { - (Network::Testnet, Payload::PlatformPubkeyHash(PubkeyHash::from_slice(&data[1..])?)) - } - PLATFORM_P2SH_PREFIX_TEST => { - (Network::Testnet, Payload::PlatformScriptHash(ScriptHash::from_slice(&data[1..])?)) + (Network::Testnet, Payload::ScriptHash(ScriptHash::from_slice(&data[1..]).unwrap())) } x => return Err(Error::Base58(base58::Error::InvalidAddressVersion(x))), }; @@ -2300,131 +2121,4 @@ mod tests { } } } - - // DIP-18 Platform Payment Address Tests - // Test vectors from DIP-0018: Dash Platform Payment Address Encodings - - #[test] - fn test_dip18_platform_p2pkh_mainnet() { - // DIP-18 test vector 1 - mainnet P2PKH - // HASH160: f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 - // Expected mainnet address: DTjceJiEqrNkCsSizK65fojEANTKoQMtsR - let hash160: PubkeyHash = "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525".parse().unwrap(); - let addr = Address::new(Dash, Payload::PlatformPubkeyHash(hash160)); - assert_eq!(addr.to_string(), "DTjceJiEqrNkCsSizK65fojEANTKoQMtsR"); - assert_eq!(addr.address_type(), Some(AddressType::PlatformP2pkh)); - assert!(addr.is_platform()); - - // Test round-trip parsing - let parsed = - Address::from_str("DTjceJiEqrNkCsSizK65fojEANTKoQMtsR").unwrap().assume_checked(); - assert_eq!(parsed.payload(), addr.payload()); - assert!(parsed.is_platform()); - } - - #[test] - fn test_dip18_platform_p2pkh_testnet() { - // DIP-18 test vector 1 - testnet P2PKH - // Using same HASH160 as mainnet to verify proper version byte encoding - // HASH160: f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 - // Note: The DIP shows a different testnet address because it uses a different - // derivation path (m/9'/1'/17'... vs m/9'/5'/17'...) which produces a different pubkey. - // Here we verify the encoding is correct for the same payload with testnet version byte. - let hash160: PubkeyHash = "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525".parse().unwrap(); - let addr = Address::new(Testnet, Payload::PlatformPubkeyHash(hash160)); - // Base58Check of [0x5a || f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 || checksum] - assert_eq!(addr.to_string(), "dc1oipbXSgBKGsovTV5CmL4RvduvRWRbsr"); - assert_eq!(addr.address_type(), Some(AddressType::PlatformP2pkh)); - assert!(addr.is_platform()); - - // Test round-trip parsing - let parsed = - Address::from_str("dc1oipbXSgBKGsovTV5CmL4RvduvRWRbsr").unwrap().assume_checked(); - assert_eq!(parsed.payload(), addr.payload()); - assert!(parsed.is_platform()); - } - - #[test] - fn test_dip18_platform_p2pkh_vector2() { - // DIP-18 test vector 2 - // HASH160: a5ff0046217fd1c7d238e3e146cc5bfd90832a7e - // Mainnet: DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm - let hash160: PubkeyHash = "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e".parse().unwrap(); - - let mainnet_addr = Address::new(Dash, Payload::PlatformPubkeyHash(hash160)); - assert_eq!(mainnet_addr.to_string(), "DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm"); - - // Testnet uses same HASH160 with version 0x5a - let testnet_addr = Address::new(Testnet, Payload::PlatformPubkeyHash(hash160)); - assert_eq!(testnet_addr.to_string(), "dUYzbDAwmo4BNb2dA41nLtSsiwoRzmDg3K"); - } - - #[test] - fn test_dip18_platform_p2pkh_vector3() { - // DIP-18 test vector 3 (non-default key_class) - // HASH160: 6d92674fd64472a3dfcfc3ebcfed7382bf699d7b - // Mainnet: DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo - let hash160: PubkeyHash = "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b".parse().unwrap(); - - let mainnet_addr = Address::new(Dash, Payload::PlatformPubkeyHash(hash160)); - assert_eq!(mainnet_addr.to_string(), "DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo"); - - // Testnet uses same HASH160 with version 0x5a - let testnet_addr = Address::new(Testnet, Payload::PlatformPubkeyHash(hash160)); - assert_eq!(testnet_addr.to_string(), "dPQeeyrQ9g9CLZD2Znvhuxp3PcWS2D5NCy"); - } - - #[test] - fn test_dip18_platform_p2sh() { - // DIP-18 P2SH test vector - // Script: 76a914000102030405060708090a0b0c0d0e0f101112131488ac - // HASH160(script): 43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636 - // Mainnet P2SH: Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k - // Testnet P2SH: pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh - let script_hash: ScriptHash = "43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636".parse().unwrap(); - - let mainnet_addr = Address::new(Dash, Payload::PlatformScriptHash(script_hash)); - assert_eq!(mainnet_addr.to_string(), "Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k"); - assert_eq!(mainnet_addr.address_type(), Some(AddressType::PlatformP2sh)); - assert!(mainnet_addr.is_platform()); - - let testnet_addr = Address::new(Testnet, Payload::PlatformScriptHash(script_hash)); - assert_eq!(testnet_addr.to_string(), "pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh"); - assert_eq!(testnet_addr.address_type(), Some(AddressType::PlatformP2sh)); - assert!(testnet_addr.is_platform()); - } - - #[test] - fn test_platform_address_parsing() { - // Test that Platform addresses can be parsed and distinguished from Core addresses - let platform_mainnet = - Address::from_str("DTjceJiEqrNkCsSizK65fojEANTKoQMtsR").unwrap().assume_checked(); - assert!(platform_mainnet.is_platform()); - assert_eq!(platform_mainnet.address_type(), Some(AddressType::PlatformP2pkh)); - - let platform_testnet = - Address::from_str("dSqV2orinasFpYAMGQTLy6uYpW9Dnge563").unwrap().assume_checked(); - assert!(platform_testnet.is_platform()); - assert_eq!(platform_testnet.address_type(), Some(AddressType::PlatformP2pkh)); - - // Core addresses should NOT be platform - let core_mainnet = - Address::from_str("Xci5rLWMqdQDy5uaCDVWEcTi6sfnkmqdUz").unwrap().assume_checked(); - assert!(!core_mainnet.is_platform()); - assert_eq!(core_mainnet.address_type(), Some(AddressType::P2pkh)); - } - - #[test] - fn test_platform_p2sh_parsing() { - // Test Platform P2SH address parsing - let mainnet_p2sh = - Address::from_str("Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k").unwrap().assume_checked(); - assert!(mainnet_p2sh.is_platform()); - assert_eq!(mainnet_p2sh.address_type(), Some(AddressType::PlatformP2sh)); - - let testnet_p2sh = - Address::from_str("pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh").unwrap().assume_checked(); - assert!(testnet_p2sh.is_platform()); - assert_eq!(testnet_p2sh.address_type(), Some(AddressType::PlatformP2sh)); - } } diff --git a/dash/src/blockdata/constants.rs b/dash/src/blockdata/constants.rs index 3a3c63cd2..294006339 100644 --- a/dash/src/blockdata/constants.rs +++ b/dash/src/blockdata/constants.rs @@ -53,21 +53,6 @@ pub const PUBKEY_ADDRESS_PREFIX_TEST: u8 = 140; /// Test (testnet, devnet, regtest) script address prefix. pub const SCRIPT_ADDRESS_PREFIX_TEST: u8 = 19; // 0x13 - -// DIP-18: Platform Payment Address Prefixes -/// Platform payment P2PKH address prefix (mainnet) - produces 'D' prefix. -pub const PLATFORM_P2PKH_PREFIX_MAIN: u8 = 30; -// 0x1e -/// Platform payment P2PKH address prefix (testnet/devnet/regtest) - produces 'd' prefix. -pub const PLATFORM_P2PKH_PREFIX_TEST: u8 = 90; -// 0x5a -/// Platform payment P2SH address prefix (mainnet) - produces 'P' prefix. -pub const PLATFORM_P2SH_PREFIX_MAIN: u8 = 56; -// 0x38 -/// Platform payment P2SH address prefix (testnet/devnet/regtest) - produces 'p' prefix. -pub const PLATFORM_P2SH_PREFIX_TEST: u8 = 117; -// 0x75 - /// The maximum allowed script size. pub const MAX_SCRIPT_ELEMENT_SIZE: usize = 520; /// How many blocks between halvings. diff --git a/key-wallet/src/managed_account/address_pool.rs b/key-wallet/src/managed_account/address_pool.rs index a8d89e631..50100d3be 100644 --- a/key-wallet/src/managed_account/address_pool.rs +++ b/key-wallet/src/managed_account/address_pool.rs @@ -394,39 +394,11 @@ impl AddressPool { } } - /// Create a new Platform payment address pool (DIP-17/DIP-18) - /// - /// This creates an address pool that generates Platform payment addresses - /// with DIP-18 encoding ("D" prefix on mainnet, "d" on testnet). - /// **Warning**: Platform addresses MUST NOT be used in Core chain transactions. - pub fn new_platform( - base_path: DerivationPath, - pool_type: AddressPoolType, - gap_limit: u32, - network: Network, - key_source: &KeySource, - ) -> Result { - let mut pool = Self::new_without_generation(base_path, pool_type, gap_limit, network); - pool.address_type = AddressType::PlatformP2pkh; - - // Generate addresses up to the gap limit if we have a key source - if !matches!(key_source, KeySource::NoKeySource) { - pool.generate_addresses(gap_limit, key_source, true)?; - } - - Ok(pool) - } - /// Set the address type for new addresses pub fn set_address_type(&mut self, address_type: AddressType) { self.address_type = address_type; } - /// Returns true if this is a Platform address pool (DIP-18) - pub fn is_platform(&self) -> bool { - matches!(self.address_type, AddressType::PlatformP2pkh | AddressType::PlatformP2sh) - } - /// Check if this is an internal (change) address pool pub fn is_internal(&self) -> bool { self.pool_type == AddressPoolType::Internal @@ -503,25 +475,9 @@ impl AddressPool { // Standard ECDSA address generation let dash_pubkey = dashcore::PublicKey::new(xpub.public_key); let network = self.network; - let address = match self.address_type { - AddressType::P2pkh => Address::p2pkh(&dash_pubkey, network), - AddressType::P2sh => { - // For P2SH, we'd need script information - // For now, default to P2PKH - Address::p2pkh(&dash_pubkey, network) - } - // DIP-18: Platform payment addresses - AddressType::PlatformP2pkh => Address::platform_p2pkh(&dash_pubkey, network), - AddressType::PlatformP2sh => { - // For Platform P2SH, we'd need script information - // For now, default to Platform P2PKH - Address::platform_p2pkh(&dash_pubkey, network) - } - _ => { - // For other address types, default to P2PKH - Address::p2pkh(&dash_pubkey, network) - } - }; + // Generate P2PKH address (Platform addresses use the same underlying + // hash but with different encoding handled by the Platform repo) + let address = Address::p2pkh(&dash_pubkey, network); let public_key_bytes = dash_pubkey.to_bytes(); (address, PublicKeyType::ECDSA(public_key_bytes.to_vec())) } diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index a2156a7c9..75720d900 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -609,7 +609,7 @@ impl ManagedAccountCollection { key_class, } => { // DIP-17/DIP-18: Platform Payment addresses - let addresses = AddressPool::new_platform( + let addresses = AddressPool::new( base_path, AddressPoolType::Absent, DEFAULT_SPECIAL_GAP_LIMIT, diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index d284854f9..e66546803 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -685,7 +685,7 @@ impl ManagedAccountType { let path = account_type .derivation_path(network) .unwrap_or_else(|_| DerivationPath::master()); - let pool = AddressPool::new_platform( + let pool = AddressPool::new( path, crate::managed_account::address_pool::AddressPoolType::Absent, DIP17_GAP_LIMIT, diff --git a/key-wallet/tests/derivation_tests.rs b/key-wallet/tests/derivation_tests.rs index 67a25b448..87478c921 100644 --- a/key-wallet/tests/derivation_tests.rs +++ b/key-wallet/tests/derivation_tests.rs @@ -112,30 +112,30 @@ fn test_public_key_derivation() { } // ============================================================================= -// DIP-17/DIP-18 Platform Payment Address Test Vectors +// DIP-17 Platform Payment Key Derivation Test Vectors // ============================================================================= // -// These tests verify the complete derivation and encoding of Platform Payment -// addresses as specified in DIP-0017 (HD Derivation) and DIP-0018 (Address Encoding). +// These tests verify the key derivation for Platform Payment addresses as +// specified in DIP-0017 (HD Derivation). The address encoding (DIP-18) uses +// bech32m with "dashevo"/"tdashevo" HRP and is implemented in the Platform repo. // // Test mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" // Passphrase: "" (empty) -/// DIP-17 Test Vector 1: Platform Payment address derivation (mainnet) +/// DIP-17 Test Vector 1: Platform Payment key derivation (mainnet) /// Path: m/9'/5'/17'/0'/0'/0 /// Expected private key: 6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe /// Expected pubkey: 03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e /// Expected HASH160: f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 -/// Expected mainnet address (DIP-18): DTjceJiEqrNkCsSizK65fojEANTKoQMtsR #[test] fn test_dip17_platform_payment_vector1_mainnet() { - use dashcore::address::{Address, Payload}; use dashcore::crypto::key::PublicKey; let mnemonic = Mnemonic::from_phrase( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language::English - ).unwrap(); + Language::English, + ) + .unwrap(); let seed = mnemonic.to_seed(""); let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); @@ -169,27 +169,20 @@ fn test_dip17_platform_payment_vector1_mainnet() { "HASH160 mismatch for DIP-17 vector 1" ); - // Create Platform P2PKH address and verify DIP-18 encoding - let addr = Address::new(dashcore::Network::Dash, Payload::PlatformPubkeyHash(pubkey_hash)); - assert_eq!( - addr.to_string(), - "DTjceJiEqrNkCsSizK65fojEANTKoQMtsR", - "DIP-18 mainnet address mismatch for vector 1" - ); + // Note: DIP-18 address encoding (bech32m with "dashevo" HRP) is in Platform repo } -/// DIP-17 Test Vector 1: Platform Payment address derivation (testnet) +/// DIP-17 Test Vector 1: Platform Payment key derivation (testnet) /// Path: m/9'/1'/17'/0'/0'/0 (note: coin_type 1' for testnet) -/// Expected testnet address (DIP-18): dSqV2orinasFpYAMGQTLy6uYpW9Dnge563 #[test] fn test_dip17_platform_payment_vector1_testnet() { - use dashcore::address::{Address, Payload}; use dashcore::crypto::key::PublicKey; let mnemonic = Mnemonic::from_phrase( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language::English - ).unwrap(); + Language::English, + ) + .unwrap(); let seed = mnemonic.to_seed(""); let wallet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); @@ -204,31 +197,26 @@ fn test_dip17_platform_payment_vector1_testnet() { let pubkey = PublicKey::new(xpub.public_key); let pubkey_hash = pubkey.pubkey_hash(); - // Create Platform P2PKH address and verify DIP-18 encoding - let addr = Address::new(dashcore::Network::Testnet, Payload::PlatformPubkeyHash(pubkey_hash)); - assert_eq!( - addr.to_string(), - "dSqV2orinasFpYAMGQTLy6uYpW9Dnge563", - "DIP-18 testnet address mismatch for vector 1" - ); + // Verify we can derive correctly (HASH160 will be used for bech32m encoding in Platform) + assert!(!pubkey_hash.to_byte_array().is_empty()); + + // Note: DIP-18 address encoding (bech32m with "tdashevo" HRP) is in Platform repo } -/// DIP-17 Test Vector 2: Platform Payment address (index 1) +/// DIP-17 Test Vector 2: Platform Payment key derivation (index 1) /// Path: m/9'/5'/17'/0'/0'/1 (mainnet) / m/9'/1'/17'/0'/0'/1 (testnet) /// Expected private key: eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523 /// Expected pubkey: 02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da /// Expected HASH160: a5ff0046217fd1c7d238e3e146cc5bfd90832a7e -/// Expected mainnet address: DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm -/// Expected testnet address: dZoepEc46ivSfm3VYr8mJeA4hZXYytgkKZ #[test] fn test_dip17_platform_payment_vector2() { - use dashcore::address::{Address, Payload}; use dashcore::crypto::key::PublicKey; let mnemonic = Mnemonic::from_phrase( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language::English - ).unwrap(); + Language::English, + ) + .unwrap(); // Test mainnet let seed = mnemonic.to_seed(""); @@ -262,16 +250,7 @@ fn test_dip17_platform_payment_vector2() { "HASH160 mismatch for DIP-17 vector 2" ); - // Verify mainnet address - let addr_mainnet = - Address::new(dashcore::Network::Dash, Payload::PlatformPubkeyHash(pubkey_hash_mainnet)); - assert_eq!( - addr_mainnet.to_string(), - "DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm", - "DIP-18 mainnet address mismatch for vector 2" - ); - - // Test testnet + // Test testnet derivation let wallet_testnet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); let path_testnet = DerivationPath::from_str("m/9'/1'/17'/0'/0'/1").unwrap(); let xprv_testnet = wallet_testnet.derive(&path_testnet).unwrap(); @@ -279,32 +258,27 @@ fn test_dip17_platform_payment_vector2() { let pubkey_testnet = PublicKey::new(xpub_testnet.public_key); let pubkey_hash_testnet = pubkey_testnet.pubkey_hash(); - let addr_testnet = - Address::new(dashcore::Network::Testnet, Payload::PlatformPubkeyHash(pubkey_hash_testnet)); - assert_eq!( - addr_testnet.to_string(), - "dZoepEc46ivSfm3VYr8mJeA4hZXYytgkKZ", - "DIP-18 testnet address mismatch for vector 2" - ); + // Verify testnet derivation produces valid hash + assert!(!pubkey_hash_testnet.to_byte_array().is_empty()); + + // Note: DIP-18 address encoding (bech32m) is in Platform repo } -/// DIP-17 Test Vector 3: Platform Payment address with non-default key_class +/// DIP-17 Test Vector 3: Platform Payment key derivation with non-default key_class /// Path: m/9'/5'/17'/0'/1'/0 (mainnet) / m/9'/1'/17'/0'/1'/0 (testnet) /// Note: key_class' = 1' instead of default 0' /// Expected private key: cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db /// Expected pubkey: 0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3 /// Expected HASH160: 6d92674fd64472a3dfcfc3ebcfed7382bf699d7b -/// Expected mainnet address: DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo -/// Expected testnet address: dFQSkGujaeDNwWTQDDVfbrurQ9ChXXYDov #[test] fn test_dip17_platform_payment_vector3_non_default_key_class() { - use dashcore::address::{Address, Payload}; use dashcore::crypto::key::PublicKey; let mnemonic = Mnemonic::from_phrase( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language::English - ).unwrap(); + Language::English, + ) + .unwrap(); // Test mainnet with key_class' = 1' let seed = mnemonic.to_seed(""); @@ -338,15 +312,6 @@ fn test_dip17_platform_payment_vector3_non_default_key_class() { "HASH160 mismatch for DIP-17 vector 3" ); - // Verify mainnet address - let addr_mainnet = - Address::new(dashcore::Network::Dash, Payload::PlatformPubkeyHash(pubkey_hash_mainnet)); - assert_eq!( - addr_mainnet.to_string(), - "DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo", - "DIP-18 mainnet address mismatch for vector 3" - ); - // Test testnet with key_class' = 1' let wallet_testnet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); let path_testnet = DerivationPath::from_str("m/9'/1'/17'/0'/1'/0").unwrap(); @@ -355,11 +320,8 @@ fn test_dip17_platform_payment_vector3_non_default_key_class() { let pubkey_testnet = PublicKey::new(xpub_testnet.public_key); let pubkey_hash_testnet = pubkey_testnet.pubkey_hash(); - let addr_testnet = - Address::new(dashcore::Network::Testnet, Payload::PlatformPubkeyHash(pubkey_hash_testnet)); - assert_eq!( - addr_testnet.to_string(), - "dFQSkGujaeDNwWTQDDVfbrurQ9ChXXYDov", - "DIP-18 testnet address mismatch for vector 3" - ); + // Verify testnet derivation produces valid hash + assert!(!pubkey_hash_testnet.to_byte_array().is_empty()); + + // Note: DIP-18 address encoding (bech32m) is in Platform repo } From 8e331a2033f4c216e0887b7197219807aa1ab5ee Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 10 Dec 2025 16:32:41 +0700 Subject: [PATCH 11/14] fix --- key-wallet/src/account/account_type.rs | 3 +-- key-wallet/src/managed_account/managed_account_type.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/key-wallet/src/account/account_type.rs b/key-wallet/src/account/account_type.rs index 47a9eac55..3286ddb1c 100644 --- a/key-wallet/src/account/account_type.rs +++ b/key-wallet/src/account/account_type.rs @@ -85,8 +85,7 @@ pub enum AccountType { }, /// Platform Payment account (DIP-17) /// Path: m/9'/coin_type'/17'/account'/key_class'/index - /// These addresses are encoded with DIP-18 format ("D"/"d" prefix for P2PKH). - /// **Warning**: Platform addresses MUST NOT be used in Core chain transactions. + /// Address encoding (DIP-18 bech32m) is handled by the Platform repo. PlatformPayment { /// Account index (hardened) - default 0' account: u32, diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index e66546803..a45e8e766 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -106,8 +106,7 @@ pub enum ManagedAccountType { }, /// Platform Payment account (DIP-17) /// Path: m/9'/coin_type'/17'/account'/key_class'/index - /// These addresses are encoded with DIP-18 format ("D"/"d" prefix for P2PKH). - /// **Warning**: Platform addresses MUST NOT be used in Core chain transactions. + /// Address encoding (DIP-18 bech32m) is handled by the Platform repo. PlatformPayment { /// Account index (hardened) account: u32, From a5ab863556fb3e9e51c0cc4f87ab305b8bfecde0 Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:22:44 +0700 Subject: [PATCH 12/14] Apply suggestion from @xdustinface (remove DIP18 mention) Co-authored-by: Kevin Rombach <35775977+xdustinface@users.noreply.github.com> --- key-wallet-ffi/include/key_wallet_ffi.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index a1cfd6b94..11d2924a1 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -93,7 +93,7 @@ typedef enum { DASHPAY_RECEIVING_FUNDS = 11, DASHPAY_EXTERNAL_ACCOUNT = 12, /* - Platform Payment address (DIP17/DIP18) - Path: m/9'/5'/17'/account'/key_class'/index + Platform Payment address (DIP17) - Path: m/9'/5'/17'/account'/key_class'/index */ PLATFORM_PAYMENT = 13, } FFIAccountType; From bb330d18735554f4304a8d55bb72065d60988c7b Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 12 Dec 2025 13:48:10 +0700 Subject: [PATCH 13/14] fix: use dip17 gap limit --- key-wallet-ffi/include/key_wallet_ffi.h | 2 +- key-wallet/src/managed_account/managed_account_collection.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 11d2924a1..a1cfd6b94 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -93,7 +93,7 @@ typedef enum { DASHPAY_RECEIVING_FUNDS = 11, DASHPAY_EXTERNAL_ACCOUNT = 12, /* - Platform Payment address (DIP17) - Path: m/9'/5'/17'/account'/key_class'/index + Platform Payment address (DIP17/DIP18) - Path: m/9'/5'/17'/account'/key_class'/index */ PLATFORM_PAYMENT = 13, } FFIAccountType; diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 75720d900..5ada3f50a 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -7,7 +7,7 @@ use crate::account::account_collection::{DashpayAccountKey, PlatformPaymentAccou use crate::account::account_type::AccountType; use crate::gap_limit::{ DEFAULT_COINJOIN_GAP_LIMIT, DEFAULT_EXTERNAL_GAP_LIMIT, DEFAULT_INTERNAL_GAP_LIMIT, - DEFAULT_SPECIAL_GAP_LIMIT, + DEFAULT_SPECIAL_GAP_LIMIT, DIP17_GAP_LIMIT, }; use crate::managed_account::address_pool::{AddressPool, AddressPoolType}; use crate::managed_account::managed_account_type::ManagedAccountType; @@ -612,7 +612,7 @@ impl ManagedAccountCollection { let addresses = AddressPool::new( base_path, AddressPoolType::Absent, - DEFAULT_SPECIAL_GAP_LIMIT, + DIP17_GAP_LIMIT, network, key_source, )?; From 089e7f2a7504d24ac37f103b3707cda832b62684 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 12 Dec 2025 14:13:10 +0700 Subject: [PATCH 14/14] fix: remove dip18 mentions --- key-wallet-ffi/include/key_wallet_ffi.h | 2 +- key-wallet-ffi/src/transaction_checking.rs | 2 +- key-wallet-ffi/src/types.rs | 2 +- key-wallet/src/transaction_checking/account_checker.rs | 4 ++-- key-wallet/src/transaction_checking/wallet_checker.rs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index a1cfd6b94..83b4ce810 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -93,7 +93,7 @@ typedef enum { DASHPAY_RECEIVING_FUNDS = 11, DASHPAY_EXTERNAL_ACCOUNT = 12, /* - Platform Payment address (DIP17/DIP18) - Path: m/9'/5'/17'/account'/key_class'/index + Platform Payment address (DIP-17) - Path: m/9'/5'/17'/account'/key_class'/index */ PLATFORM_PAYMENT = 13, } FFIAccountType; diff --git a/key-wallet-ffi/src/transaction_checking.rs b/key-wallet-ffi/src/transaction_checking.rs index 4074ae953..fdadd8600 100644 --- a/key-wallet-ffi/src/transaction_checking.rs +++ b/key-wallet-ffi/src/transaction_checking.rs @@ -461,7 +461,7 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( .. } => { // Note: Platform Payment addresses are NOT used in Core chain transactions - // per DIP17/DIP18. This branch should never be reached in practice. + // per DIP-17. This branch should never be reached in practice. let ffi_match = FFIAccountMatch { account_type: 13, // PlatformPayment account_index: *account_index, diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index dc28b1bf5..52715c69c 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -266,7 +266,7 @@ pub enum FFIAccountType { ProviderPlatformKeys = 10, DashpayReceivingFunds = 11, DashpayExternalAccount = 12, - /// Platform Payment address (DIP17/DIP18) - Path: m/9'/5'/17'/account'/key_class'/index + /// Platform Payment address (DIP-17) - Path: m/9'/5'/17'/account'/key_class'/index PlatformPayment = 13, } diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index f79380e25..3ea246d95 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -103,7 +103,7 @@ pub enum AccountTypeMatch { account_index: u32, involved_addresses: Vec, }, - /// Platform Payment account (DIP17/DIP18) + /// Platform Payment account (DIP-17) /// Note: Platform addresses are NOT used in Core chain transactions PlatformPayment { account_index: u32, @@ -385,7 +385,7 @@ impl ManagedAccountCollection { matches } AccountTypeToCheck::PlatformPayment => { - // Platform Payment addresses (DIP17/DIP18) are NOT used in Core chain transactions. + // Platform Payment addresses (DIP-17) are NOT used in Core chain transactions. // They are only for Platform-side payments. This account type should never match // any Core chain transaction by design. Vec::new() diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 71fc3d6c1..be9c35165 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -145,7 +145,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { .. } => { // Platform Payment addresses are NOT used in Core chain transactions. - // This branch should never be reached by design (per DIP17/DIP18). + // This branch should never be reached by design (per DIP-17). None } };