Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 11 additions & 11 deletions key-wallet/src/account/eddsa_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,13 @@ impl
))
}

/// Derive an Ed25519-based address at a specific chain and index.
/// Derive an Ed25519-based pseudo-address at a specific chain and index.
///
/// Creates a P2PKH-style address from the hash160 of the Ed25519 public key.
/// The payload is the Tenderdash node ID of the Ed25519 public key
/// (`SHA256(pubkey)[0..20]`, the CometBFT convention) wrapped in a
/// P2PKH-style address — the same value ProRegTx carries as
/// `platform_node_id`, so it can be matched against on-chain evonode
/// registrations. NOT hash160, which Dash only uses for ECDSA key hashes.
fn derive_address_at(
&self,
address_pool_type: AddressPoolType,
Expand All @@ -342,17 +346,13 @@ impl
let ed25519_pubkey =
self.derive_public_key_at(address_pool_type, index, use_hardened_with_priv_key)?;

// Get the Ed25519 public key bytes (32 bytes for Ed25519)
let pubkey_bytes = ed25519_pubkey.to_bytes();
let node_id = crate::derivation_slip10::tenderdash_node_id(&ed25519_pubkey.to_bytes());

// Create a P2PKH address from the hash160 of the Ed25519 public key
// This uses the same hash160 (SHA256 + RIPEMD160) as ECDSA addresses
use dashcore::hashes::{hash160, Hash};
let pubkey_hash = hash160::Hash::hash(&pubkey_bytes);

// Create the address from the public key hash
use dashcore::address::Payload;
let payload = Payload::PubkeyHash(pubkey_hash.into());
use dashcore::hashes::Hash;
let pubkey_hash = dashcore::PubkeyHash::from_slice(&node_id)
.expect("Tenderdash node id is exactly 20 bytes");
let payload = Payload::PubkeyHash(pubkey_hash);
Comment thread
QuantumExplorer marked this conversation as resolved.
Outdated
Ok(Address::new(self.network, payload))
}

Expand Down
17 changes: 17 additions & 0 deletions key-wallet/src/derivation_slip10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ pub use dashcore::ed25519_dalek::VerifyingKey as Ed25519PublicKey;
// Use DerivationPath from bip32
pub use crate::bip32::DerivationPath;

/// Compute the Tenderdash/CometBFT node ID for an Ed25519 public key: the
/// first 20 bytes of a single SHA-256 of the 32-byte public key.
///
/// This is the canonical value of the `platform_node_id` field in ProRegTx /
/// ProUpServTx payloads for evonodes: it is what dashmate derives from
/// `node_key.json` and what Tenderdash announces on the platform P2P network,
/// so it is the only value that can match an on-chain registration.
///
/// Note this is **not** `hash160` (SHA-256 + RIPEMD-160). Dash uses hash160
/// for the ECDSA owner/voting key hashes in the same payload, but the
/// platform node id follows the CometBFT node-ID convention.
pub fn tenderdash_node_id(ed25519_public_key_bytes: &[u8; 32]) -> [u8; 20] {
use dashcore_hashes::sha256;
let hash = sha256::Hash::hash(ed25519_public_key_bytes);
hash.as_byte_array()[..20].try_into().expect("SHA-256 yields 32 bytes")
}

Comment thread
QuantumExplorer marked this conversation as resolved.
/// Extended Ed25519 private key for SLIP-0010
#[derive(Clone)]
pub struct ExtendedEd25519PrivKey {
Expand Down
23 changes: 18 additions & 5 deletions key-wallet/src/managed_account/address_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,13 +491,26 @@ impl AddressPool {
(address, PublicKeyType::BLS(public_key_bytes))
}
DerivedKey::EdDSA(public_key_bytes) => {
// EdDSA addresses use Hash160 of the public key bytes
use dashcore::hashes::{hash160, Hash};
let pubkey_hash = hash160::Hash::hash(&public_key_bytes);
// EdDSA pool entries key on the Tenderdash node ID
// (SHA256(pubkey)[0..20]) — the value ProRegTx carries as
// `platform_node_id` — wrapped in a P2PKH-style payload so it
// can live in the pool's address index. NOT hash160: a
// hash160-keyed entry can never match an on-chain evonode
// registration.
let pubkey_arr: &[u8; 32] =
public_key_bytes.as_slice().try_into().map_err(|_| {
Error::InvalidParameter(format!(
"EdDSA public key must be 32 bytes, got {}",
public_key_bytes.len()
))
})?;
let node_id = crate::derivation_slip10::tenderdash_node_id(pubkey_arr);

// Create P2PKH address from the hash
use dashcore::address::Payload;
let payload = Payload::PubkeyHash(pubkey_hash.into());
use dashcore::hashes::Hash;
let pubkey_hash = dashcore::PubkeyHash::from_slice(&node_id)
.expect("Tenderdash node id is exactly 20 bytes");
let payload = Payload::PubkeyHash(pubkey_hash);
let address = Address::new(self.network, payload);

(address, PublicKeyType::EdDSA(public_key_bytes))
Expand Down
64 changes: 64 additions & 0 deletions key-wallet/src/tests/provider_key_derivation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,70 @@ fn ed25519_platform_node_keys_match_slip10_reference() {
);
}

/// The platform node id must follow the Tenderdash/CometBFT convention —
/// `SHA256(pubkey)[0..20]` — not hash160, or wallet-side matching can never
/// hit on-chain evonode registrations. See
/// <https://github.com/dashpay/rust-dashcore/issues/883>.
#[cfg(feature = "eddsa")]
#[test]
fn ed25519_platform_node_id_matches_tenderdash_convention() {
use crate::account::eddsa_account::EdDSAAccount;
use crate::derivation_slip10::tenderdash_node_id;

// Platform node key 0 (m/9'/5'/3'/4'/0') for TEST_SEED — the same key the
// SLIP-0010 vector above pins. Public key and node id cross-checked with
// an independent Ed25519 implementation (pyca/cryptography).
let seed = hex::decode(TEST_SEED_HEX).unwrap();
let sk0 = EdDSAAccount::platform_node_key_at(&seed, Network::Mainnet, 0).unwrap();
let pubkey = sk0.verifying_key();
assert_eq!(
hex::encode(pubkey.to_bytes()),
"3130c14339391cf26a68d86879e180ee9a16b660f5aa91f560f67c0abe8cf789"
);
assert_eq!(
hex::encode(tenderdash_node_id(&pubkey.to_bytes())),
"302f2615e6955cce8ed3cff81e8011bfd3a2991f"
);
}

/// The managed ProviderPlatformKeys pool must key its entries by the
/// Tenderdash node id, since that payload is what ProRegTx matching compares
/// against the on-chain `platform_node_id` field.
#[cfg(feature = "eddsa")]
#[test]
fn platform_pool_entries_are_keyed_by_tenderdash_node_id() {
use crate::derivation_slip10::ExtendedEd25519PrivKey;
use crate::managed_account::managed_account_trait::ManagedAccountTrait;
use crate::wallet::managed_wallet_info::ManagedWalletInfo;
use dashcore::hashes::Hash;

let wallet = test_wallet(Network::Mainnet);
let mut info = ManagedWalletInfo::from_wallet_with_name(&wallet, "node-id".to_string(), 0);

let seed = hex::decode(TEST_SEED_HEX).unwrap();
let master = ExtendedEd25519PrivKey::new_master(Network::Mainnet, &seed).unwrap();
let account_xpriv = master
.derive_priv(&AccountType::ProviderPlatformKeys.derivation_path(Network::Mainnet).unwrap())
.unwrap();

let (verifying_key, entry) = info
.provider_platform_keys_managed_account_mut()
.expect("platform managed account")
.next_eddsa_platform_key(account_xpriv, true)
.expect("derive platform key 0");

assert_eq!(
hex::encode(verifying_key.to_bytes()),
"3130c14339391cf26a68d86879e180ee9a16b660f5aa91f560f67c0abe8cf789"
);
// The pool entry's payload is the Tenderdash node id — the value a real
// ProRegTx carries — so registration matching can hit it.
let dashcore::address::Payload::PubkeyHash(hash) = entry.address.payload() else {
panic!("platform pool entries use P2PKH-style payloads");
};
assert_eq!(hex::encode(hash.to_byte_array()), "302f2615e6955cce8ed3cff81e8011bfd3a2991f");
}

/// The gate-free provider-key entry points must work identically on resident
/// (non-watch-only) and watch-only accounts, and must match the dashbls
/// reference vectors. See
Expand Down
4 changes: 3 additions & 1 deletion key-wallet/src/transaction_checking/account_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,9 @@ impl ManagedCoreFundsAccount {
_ => return None,
};

// Check if platform_node_id matches any of our address hashes
// Check if platform_node_id matches any of our pool entries.
// Pool payloads hold Tenderdash node ids (SHA256(pubkey)[..20]),
// matching the on-chain field's convention.
for (address, &addr_index) in &addresses.address_index {
if let Payload::PubkeyHash(addr_hash) = address.payload() {
if addr_hash == platform_node_id {
Expand Down
Loading