Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use crate::blockdata::transaction::special_transaction::SpecialTransactionBasePa
use crate::bls_sig_utils::BLSPublicKey;
use crate::consensus::{Decodable, Encodable, encode};
use crate::hash_types::{InputsHash, PubkeyHash, SpecialTransactionPayloadHash};
use crate::platform_node_id::PlatformNodeId;
use crate::prelude::*;
use crate::{Address, Network, OutPoint, ScriptBuf, VarInt, io};

Expand Down Expand Up @@ -108,7 +109,11 @@ pub struct ProviderRegistrationPayload {
pub script_payout: ScriptBuf,
pub inputs_hash: InputsHash,
pub signature: Vec<u8>,
pub platform_node_id: Option<PubkeyHash>,
/// Tenderdash/CometBFT node ID of the masternode's Platform node:
/// `SHA256(ed25519_public_key)` truncated to 20 bytes. Not a `hash160`
/// public key hash. Only present for high-performance (Evo) masternodes
/// on payload version 2+.
pub platform_node_id: Option<PlatformNodeId>,
pub platform_p2p_port: Option<u16>,
pub platform_http_port: Option<u16>,
}
Expand All @@ -131,7 +136,7 @@ impl ProviderRegistrationPayload {
script_payout: ScriptBuf,
inputs_hash: InputsHash,
signature: Vec<u8>,
platform_node_id: Option<PubkeyHash>,
platform_node_id: Option<PlatformNodeId>,
platform_p2p_port: Option<u16>,
platform_http_port: Option<u16>,
) -> Self {
Expand Down Expand Up @@ -221,10 +226,7 @@ impl SpecialTransactionBasePayloadEncodable for ProviderRegistrationPayload {
len += self.inputs_hash.consensus_encode(&mut s)?;

if self.version >= 2 && self.masternode_type == ProviderMasternodeType::HighPerformance {
len += self
.platform_node_id
.unwrap_or_else(PubkeyHash::all_zeros)
.consensus_encode(&mut s)?;
len += self.platform_node_id.unwrap_or_default().consensus_encode(&mut s)?;
len += self.platform_p2p_port.unwrap_or_default().consensus_encode(&mut s)?;
len += self.platform_http_port.unwrap_or_default().consensus_encode(&mut s)?;
}
Expand Down Expand Up @@ -267,7 +269,7 @@ impl Decodable for ProviderRegistrationPayload {
let mut platform_http_port = None;

if version >= 2 && provider_type == ProviderMasternodeType::HighPerformance {
platform_node_id = Some(PubkeyHash::consensus_decode(r)?);
platform_node_id = Some(PlatformNodeId::consensus_decode(r)?);
platform_p2p_port = Some(u16::consensus_decode(r)?);
platform_http_port = Some(u16::consensus_decode(r)?);
}
Expand Down Expand Up @@ -721,6 +723,41 @@ mod tests {
assert_eq!(actual, want);
}

/// A v2 high-performance payload with absent platform fields still
/// writes the zero-filled node id and ports on the wire.
#[test]
fn v2_high_performance_none_platform_fields_encode_zero_filled() {
let payload = ProviderRegistrationPayload {
version: 2,
masternode_type: ProviderMasternodeType::HighPerformance,
masternode_mode: 0,
collateral_outpoint: OutPoint {
txid: Txid::all_zeros(),
vout: 0,
},
service_address: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from_bits(0), 0)),
owner_key_hash: PubkeyHash::all_zeros(),
operator_public_key: BLSPublicKey::from([0; 48]),
voting_key_hash: PubkeyHash::all_zeros(),
operator_reward: 0,
script_payout: ScriptBuf::new(),
inputs_hash: InputsHash::all_zeros(),
signature: vec![],
platform_node_id: None,
platform_p2p_port: None,
platform_http_port: None,
};

let mut encoded = Vec::new();
payload.consensus_encode(&mut encoded).unwrap();

let decoded: ProviderRegistrationPayload =
deserialize(&encoded).expect("deserialize zero-filled payload");
assert_eq!(decoded.platform_node_id, Some(crate::PlatformNodeId::from_byte_array([0; 20])));
assert_eq!(decoded.platform_p2p_port, Some(0));
assert_eq!(decoded.platform_http_port, Some(0));
}

#[test]
fn test_payload_version_2_encoding_and_decoding() {
let payload_bytes = hex!(
Expand All @@ -729,6 +766,19 @@ mod tests {
let payload: ProviderRegistrationPayload =
deserialize(&payload_bytes).expect("deserialize payload");

assert_eq!(payload.version, 2);
assert_eq!(payload.masternode_type, ProviderMasternodeType::HighPerformance);
assert_eq!(
payload.platform_node_id,
Some(
"4cd2ca50b36e0a2bb1b6b29da140448b47eeb7a1"
.parse()
.expect("valid platform node id hex")
)
);
assert_eq!(payload.platform_p2p_port, Some(26656));
assert_eq!(payload.platform_http_port, Some(443));

let mut serialized_payload_bytes = Vec::new();

payload.consensus_encode(&mut serialized_payload_bytes).expect("serialize payload");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use crate::blockdata::transaction::special_transaction::provider_registration::P
use crate::bls_sig_utils::BLSSignature;
use crate::consensus::{Decodable, Encodable, encode};
use crate::hash_types::{InputsHash, SpecialTransactionPayloadHash, Txid};
use crate::platform_node_id::PlatformNodeId;
use crate::{ScriptBuf, VarInt, io};

/// ProTx version constants
Expand All @@ -68,8 +69,10 @@ pub struct ProviderUpdateServicePayload {
pub port: u16,
pub script_payout: ScriptBuf,
pub inputs_hash: InputsHash,
// Platform fields (only for BasicBLS version and Evo masternode type)
pub platform_node_id: Option<[u8; 20]>,
// Platform fields (only for BasicBLS version and Evo masternode type).
// The node ID is a Tenderdash/CometBFT node ID (SHA256 of the ed25519
// public key truncated to 20 bytes), not a hash160 public key hash.
pub platform_node_id: Option<PlatformNodeId>,
pub platform_p2p_port: Option<u16>,
pub platform_http_port: Option<u16>,
pub payload_sig: BLSSignature,
Expand All @@ -88,7 +91,7 @@ impl ProviderUpdateServicePayload {
port: u16,
script_payout: ScriptBuf,
inputs_hash: InputsHash,
platform_node_id: Option<[u8; 20]>,
platform_node_id: Option<PlatformNodeId>,
platform_p2p_port: Option<u16>,
platform_http_port: Option<u16>,
payload_sig: BLSSignature,
Expand Down Expand Up @@ -147,7 +150,7 @@ impl SpecialTransactionBasePayloadEncodable for ProviderUpdateServicePayload {
if self.version >= ProTxVersion::BasicBLS as u16
&& self.mn_type == Some(ProviderMasternodeType::HighPerformance as u16)
{
len += s.write(&self.platform_node_id.unwrap_or([0u8; 20]))?;
len += self.platform_node_id.unwrap_or_default().consensus_encode(&mut s)?;
len += self.platform_p2p_port.unwrap_or_default().consensus_encode(&mut s)?;
len += self.platform_http_port.unwrap_or_default().consensus_encode(&mut s)?;
}
Expand Down Expand Up @@ -199,11 +202,7 @@ impl Decodable for ProviderUpdateServicePayload {
== ProTxVersion::BasicBLS as u16
&& mn_type == Some(ProviderMasternodeType::HighPerformance as u16)
{
let node_id = {
let mut buf = [0u8; 20];
r.read_exact(&mut buf)?;
buf
};
let node_id = PlatformNodeId::consensus_decode(r)?;
let p2p_port = u16::consensus_decode(r)?;
let http_port = u16::consensus_decode(r)?;
(Some(node_id), Some(p2p_port), Some(http_port))
Expand Down Expand Up @@ -244,6 +243,7 @@ mod tests {
use crate::consensus::{Decodable, Encodable, deserialize};
use crate::hash_types::InputsHash;
use crate::internal_macros::hex;
use crate::platform_node_id::PlatformNodeId;
use crate::{Network, ScriptBuf, Transaction, Txid};

#[test]
Expand Down Expand Up @@ -406,7 +406,7 @@ mod tests {
port: 0,
script_payout: ScriptBuf::from(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
inputs_hash: InputsHash::all_zeros(),
platform_node_id: Some([0; 20]),
platform_node_id: Some(PlatformNodeId::from_byte_array([0; 20])),
platform_p2p_port: Some(0),
platform_http_port: Some(0),
payload_sig: BLSSignature::from([0; 96]),
Expand All @@ -422,6 +422,37 @@ mod tests {
assert_eq!(decoded, original);
}

/// A v2 Evo payload with absent platform fields still writes the
/// zero-filled node id and ports on the wire.
#[test]
fn v2_evo_masternode_none_platform_fields_encode_zero_filled() {
let original = ProviderUpdateServicePayload {
version: 2,
mn_type: Some(1), // HighPerformance (Evo)
pro_tx_hash: Txid::all_zeros(),
ip_address: 0,
port: 0,
script_payout: ScriptBuf::new(),
inputs_hash: InputsHash::all_zeros(),
platform_node_id: None,
platform_p2p_port: None,
platform_http_port: None,
payload_sig: BLSSignature::from([0; 96]),
};

let mut encoded = Vec::new();
original.consensus_encode(&mut encoded).unwrap();

// version(2) + mn_type(2) + pro_tx_hash(32) + ip(16) + port(2) +
// script(1) + inputs_hash(32) + node_id(20) + p2p(2) + http(2) + sig(96)
assert_eq!(encoded.len(), 207);

let decoded = ProviderUpdateServicePayload::consensus_decode(&mut &encoded[..]).unwrap();
assert_eq!(decoded.platform_node_id, Some(PlatformNodeId::from_byte_array([0; 20])));
assert_eq!(decoded.platform_p2p_port, Some(0));
assert_eq!(decoded.platform_http_port, Some(0));
}

#[test]
fn test_protx_update_v2_block_parsing() {
use crate::blockdata::block::Block;
Expand Down
2 changes: 2 additions & 0 deletions dash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pub mod ephemerealdata;
pub mod error;
pub mod hash_types;
pub mod merkle_tree;
pub mod platform_node_id;
pub mod policy;
pub mod pow;
pub mod sign_message;
Expand Down Expand Up @@ -142,6 +143,7 @@ pub use crate::hash_types::{
};
pub use crate::merkle_tree::MerkleBlock;
pub use crate::network::constants::{Network, ParseNetworkError};
pub use crate::platform_node_id::PlatformNodeId;
pub use crate::pow::{CompactTarget, Target, Work};
pub use crate::transaction::outpoint::OutPoint;
pub use crate::transaction::txin::TxIn;
Expand Down
142 changes: 142 additions & 0 deletions dash/src/platform_node_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Rust Dash Library
// Written by
// The Rust Dash developers
//
// To the extent possible under law, the author(s) have dedicated all
// copyright and related and neighboring rights to this software to
// the public domain worldwide. This software is distributed without
// any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication
// along with this software.
// If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
//

//! Dash Platform (Tenderdash/CometBFT) node ID.
//!
//! A platform node ID is the first 20 bytes of `SHA256(ed25519_public_key)`,
//! following the CometBFT node-ID convention. Although it is 20 bytes on the
//! wire like a [`PubkeyHash`](crate::PubkeyHash), it is *not* a `hash160`
//! public key hash, so it gets its own type to keep the two conventions from
//! being conflated.

#[cfg(feature = "bincode")]
use bincode::{Decode, Encode};
use hashes::{Hash, sha256};
use internals::impl_array_newtype;

use crate::consensus::{Decodable, Encodable, encode};
use crate::internal_macros::impl_bytes_newtype;
use crate::io;

/// A Dash Platform (Tenderdash/CometBFT) node ID.
///
/// This is `SHA256(ed25519_public_key)` truncated to 20 bytes, not a `hash160`
/// public key hash. The consensus encoding is the raw 20 bytes, identical to
/// the encoding of a 20-byte key hash.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "bincode", derive(Encode, Decode))]
pub struct PlatformNodeId([u8; 20]);

impl_array_newtype!(PlatformNodeId, u8, 20);
impl_bytes_newtype!(PlatformNodeId, 20);

impl PlatformNodeId {
/// Constructs a platform node ID from its raw 20 bytes.
pub const fn from_byte_array(bytes: [u8; 20]) -> Self {
PlatformNodeId(bytes)
}

/// Returns the raw 20 bytes of the node ID.
pub const fn to_byte_array(self) -> [u8; 20] {
self.0
}

/// Returns a reference to the raw 20 bytes of the node ID.
pub const fn as_byte_array(&self) -> &[u8; 20] {
&self.0
}

/// Derives the node ID from an Ed25519 public key per the CometBFT
/// convention: `SHA256(public_key)[0..20]`.
pub fn from_ed25519_public_key(public_key_bytes: &[u8; 32]) -> Self {
let digest = sha256::Hash::hash(public_key_bytes);
let mut bytes = [0u8; 20];
bytes.copy_from_slice(&digest.to_byte_array()[..20]);
PlatformNodeId(bytes)
}
}

impl Encodable for PlatformNodeId {
fn consensus_encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
self.0.consensus_encode(w)
}
}

impl Decodable for PlatformNodeId {
fn consensus_decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, encode::Error> {
Ok(PlatformNodeId(<[u8; 20]>::consensus_decode(r)?))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::consensus::{deserialize, serialize};

#[test]
fn consensus_round_trip() {
let node_id = PlatformNodeId::from_byte_array([0xAB; 20]);
let encoded = serialize(&node_id);
assert_eq!(encoded.len(), 20);
assert_eq!(encoded, [0xAB; 20]);
let decoded: PlatformNodeId = deserialize(&encoded).expect("decode node id");
assert_eq!(decoded, node_id);
}

/// Persisted masternode-list snapshots were written while
/// `platform_node_id` was typed as `PubkeyHash`; the bincode layout must
/// stay identical so old snapshots keep decoding.
#[cfg(feature = "bincode")]
#[test]
fn bincode_layout_matches_pubkey_hash() {
use hashes::Hash;

let bytes = [0xCD; 20];
let config = bincode::config::standard();
let node_id_bytes = bincode::encode_to_vec(PlatformNodeId::from_byte_array(bytes), config)
.expect("encode node id");
let pubkey_hash_bytes =
bincode::encode_to_vec(crate::PubkeyHash::from_byte_array(bytes), config)
.expect("encode pubkey hash");
assert_eq!(node_id_bytes, pubkey_hash_bytes);
}

#[test]
fn hex_display_and_parse_round_trip() {
let hex = "4cd2ca50b36e0a2bb1b6b29da140448b47eeb7a1";
let node_id: PlatformNodeId = hex.parse().expect("parse node id hex");
assert_eq!(node_id.to_string(), hex);
assert_eq!(format!("{:?}", node_id), hex);
assert!("abcd".parse::<PlatformNodeId>().is_err(), "wrong-length hex must fail");
assert!("zz".repeat(20).parse::<PlatformNodeId>().is_err(), "non-hex input must fail");
}

#[cfg(feature = "serde")]
#[test]
fn serde_json_round_trip_as_hex_string() {
let node_id = PlatformNodeId::from_byte_array([0x11; 20]);
let json = serde_json::to_string(&node_id).expect("serialize node id");
assert_eq!(json, format!("\"{}\"", "11".repeat(20)));
let back: PlatformNodeId = serde_json::from_str(&json).expect("deserialize node id");
assert_eq!(back, node_id);
}

#[test]
fn from_ed25519_public_key_is_truncated_sha256() {
let public_key = [7u8; 32];
let digest = sha256::Hash::hash(&public_key);
let node_id = PlatformNodeId::from_ed25519_public_key(&public_key);
assert_eq!(node_id.as_byte_array()[..], digest.to_byte_array()[..20]);
}
}
6 changes: 4 additions & 2 deletions dash/src/sml/masternode_list_entry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::internal_macros::impl_consensus_encoding;
use crate::sml::masternode_list_entry::net_info::{
Bip155Network, ExtNetInfo, NetInfoEntry, NetInfoPurpose,
};
use crate::{ProTxHash, PubkeyHash};
use crate::{PlatformNodeId, ProTxHash, PubkeyHash};

#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "bincode", derive(Encode, Decode))]
Expand All @@ -29,7 +29,9 @@ pub enum EntryMasternodeType {
Regular,
HighPerformance {
platform_http_port: u16,
platform_node_id: PubkeyHash,
/// Tenderdash/CometBFT node ID (`SHA256(ed25519_pubkey)[0..20]`),
/// not a `hash160` public key hash.
platform_node_id: PlatformNodeId,
},
}

Expand Down
Loading
Loading