Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion dash/src/sml/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down Expand Up @@ -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(
Expand Down
51 changes: 48 additions & 3 deletions dash/src/sml/masternode_list_entry/hash.rs
Original file line number Diff line number Diff line change
@@ -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
);
}
}
}
22 changes: 19 additions & 3 deletions dash/src/sml/masternode_list_entry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,22 @@ impl PartialOrd for MasternodeListEntry {
}
}

impl Encodable for MasternodeListEntry {
fn consensus_encode<W: Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
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<W: Write + ?Sized>(
&self,
writer: &mut W,
) -> Result<usize, std::io::Error> {
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)?;
Expand Down Expand Up @@ -196,6 +203,15 @@ impl Encodable for MasternodeListEntry {
}
}

impl Encodable for MasternodeListEntry {
fn consensus_encode<W: Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
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<R: Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
let version: u16 = Decodable::consensus_decode(reader)?;
Expand Down
Loading