diff --git a/dash/src/sml/address.rs b/dash/src/sml/address.rs index 5eafec8e8..8ec1757ce 100644 --- a/dash/src/sml/address.rs +++ b/dash/src/sml/address.rs @@ -40,7 +40,7 @@ impl Decodable for SocketAddr { let ipv6 = Ipv6Addr::from_bits(ip); - if let Some(ipv4) = ipv6.to_ipv4() { + if let Some(ipv4) = ipv6.to_ipv4_mapped() { Ok(SocketAddr::V4(SocketAddrV4::new(ipv4, port))) } else { Ok(SocketAddr::V6(SocketAddrV6::new(ipv6, port, 0, 0))) @@ -86,6 +86,20 @@ mod tests { assert_eq!(writer, decoded_writer); } + #[test] + fn encode_decode_unspecified_preserves_bytes() { + // An all-zero (`::`) address must round-trip to the same 16 zero bytes. Decoding it as + // IPv4 `0.0.0.0` would re-encode with the `::ffff:` mapped prefix and corrupt the bytes, + // which in turn breaks the masternode entry hash for entries with an unset service. + let original = [0u8; 18]; + let mut reader = &original[..]; + let decoded = SocketAddr::consensus_decode(&mut reader).unwrap(); + + let mut writer = Vec::new(); + decoded.consensus_encode(&mut writer).unwrap(); + assert_eq!(writer, original); + } + #[test] fn encode_decode_ipv6() { let address = SocketAddr::V6(SocketAddrV6::new( diff --git a/dash/src/sml/masternode_list_entry/hash.rs b/dash/src/sml/masternode_list_entry/hash.rs index 51f5932cf..dae58fd15 100644 --- a/dash/src/sml/masternode_list_entry/hash.rs +++ b/dash/src/sml/masternode_list_entry/hash.rs @@ -1,13 +1,58 @@ use hashes::{Hash, sha256d}; -use crate::consensus::Encodable; use crate::sml::masternode_list_entry::MasternodeListEntry; impl MasternodeListEntry { pub fn calculate_entry_hash(&self) -> sha256d::Hash { let mut writer = Vec::new(); - - self.consensus_encode(&mut writer).expect("encoding failed"); + self.consensus_encode_body(&mut writer).expect("encoding failed"); sha256d::Hash::hash(&writer) } } + +#[cfg(test)] +mod tests { + use hashes::Hash; + + use crate::consensus::deserialize; + use crate::network::message_sml::MnListDiff; + + // Ground-truth entry hashes produced by Dash Core's `CSimplifiedMNListEntry::CalcHash` + // (`CHashWriter(SER_GETHASH, ...)`) for the matching entries in this fixture. `SER_GETHASH` + // omits the `SER_NETWORK`-gated leading `version`, so the pre-image is the wire body without + // that field. Hashing the full wire (with `version`) yields different values and fails here. + // The first case is a `version` 1 entry, the second a `version` 2 Evo entry, exercising both + // the legacy path and the `nType`/platform fields. + #[test] + fn entry_hash_matches_core_calc_hash() { + let bytes: &[u8] = + include_bytes!("../../../tests/data/test_DML_diffs/mn_list_diff_0_2227096.bin"); + let diff: MnListDiff = deserialize(bytes).expect("expected to deserialize"); + + let cases = [ + ( + "0008858d870b0aa7967c39a551fc953e4e7fa602f19ba1fc805c218f87f41cb6", + "759c929f9d225554a09a8ad817bfaf555847547097495e08d3ba316529b65426", + ), + ( + "000c898c950a9c4a4d1eb3c227ab6d65ab652b44010e25f6dbe7a673e4bb52de", + "045c5f8ae528d32d0e694ddb9d652794d41b89db5f7eaee703beef62b35e4903", + ), + ]; + + for (pro_reg_tx_hash_hex, expected_entry_hash_hex) in cases { + let entry = diff + .new_masternodes + .iter() + .find(|e| hex::encode(e.pro_reg_tx_hash.to_byte_array()) == pro_reg_tx_hash_hex) + .expect("expected entry present in fixture"); + + assert_eq!( + hex::encode(entry.calculate_entry_hash().to_byte_array()), + expected_entry_hash_hex, + "entry hash for {} must match Dash Core's CalcHash", + pro_reg_tx_hash_hex + ); + } + } +} diff --git a/dash/src/sml/masternode_list_entry/mod.rs b/dash/src/sml/masternode_list_entry/mod.rs index 494205230..e26197fe6 100644 --- a/dash/src/sml/masternode_list_entry/mod.rs +++ b/dash/src/sml/masternode_list_entry/mod.rs @@ -153,15 +153,22 @@ impl PartialOrd for MasternodeListEntry { } } -impl Encodable for MasternodeListEntry { - fn consensus_encode(&self, writer: &mut W) -> Result { +impl MasternodeListEntry { + /// Encodes everything after the leading `version`, shared by the wire format and the hash + /// pre-image. Core's `CSimplifiedMNListEntry::CalcHash` uses `CHashWriter(SER_GETHASH, ...)`, + /// and `SER_GETHASH` does not set `SER_NETWORK`, so the leading `version` (which is + /// `SER_NETWORK`-gated in Core's `SERIALIZE_METHODS`) is excluded from the hash but present on + /// the wire. Every remaining field keys off the `version` member, identical in both contexts. + fn consensus_encode_body( + &self, + writer: &mut W, + ) -> Result { debug_assert_eq!( matches!(self.service_address, MasternodeNetInfo::Legacy(_)), self.version < 3, "Legacy service address must be used iff version < 3" ); let mut len = 0; - len += self.version.consensus_encode(writer)?; len += self.pro_reg_tx_hash.consensus_encode(writer)?; if let Some(confirmed_hash) = self.confirmed_hash { len += confirmed_hash.consensus_encode(writer)?; @@ -196,6 +203,15 @@ impl Encodable for MasternodeListEntry { } } +impl Encodable for MasternodeListEntry { + fn consensus_encode(&self, writer: &mut W) -> Result { + let mut len = 0; + len += self.version.consensus_encode(writer)?; + len += self.consensus_encode_body(writer)?; + Ok(len) + } +} + impl Decodable for MasternodeListEntry { fn consensus_decode(reader: &mut R) -> Result { let version: u16 = Decodable::consensus_decode(reader)?;