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
60 changes: 49 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,11 @@ 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 payload = Payload::PubkeyHash(dashcore::PubkeyHash::from_byte_array(node_id));
Ok(Address::new(self.network, payload))
}

Expand Down Expand Up @@ -534,6 +532,46 @@ mod tests {
assert!(result.is_err());
}

/// `derive_address_at` must produce Tenderdash-node-id payloads
/// (SHA256(pubkey)[0..20]) — pinned to the same golden vector as
/// `tests::provider_key_derivation_tests` (platform node key 0 at
/// m/9'/5'/3'/4'/0' for the BIP39 "abandon…about" seed, cross-checked
/// with an independent Ed25519 implementation).
#[test]
fn test_derive_address_at_uses_tenderdash_node_id() {
use dashcore::hashes::Hash;

let seed = hex::decode(
"5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc1\
9a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4",
)
.unwrap();
let account = EdDSAAccount::from_seed(
None,
AccountType::ProviderPlatformKeys,
&seed,
Network::Mainnet,
)
.expect("create platform keys account from seed");

let master =
ExtendedEd25519PrivKey::new_master(Network::Mainnet, &seed).expect("master from seed");
let account_xpriv = master
.derive_priv(
&AccountType::ProviderPlatformKeys.derivation_path(Network::Mainnet).unwrap(),
)
.expect("derive account xpriv");

let address = account
.derive_address_at(AddressPoolType::AbsentHardened, 0, Some(account_xpriv))
.expect("derive platform node pseudo-address");

let dashcore::address::Payload::PubkeyHash(hash) = address.payload() else {
panic!("platform pseudo-addresses use P2PKH-style payloads");
};
assert_eq!(hex::encode(hash.to_byte_array()), "302f2615e6955cce8ed3cff81e8011bfd3a2991f");
}

#[test]
fn test_derive_identity_key() {
let seed = [5u8; 32];
Expand Down
19 changes: 19 additions & 0 deletions key-wallet/src/derivation_slip10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ 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);
let mut node_id = [0u8; 20];
node_id.copy_from_slice(&hash.as_byte_array()[..20]);
node_id
}

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 @@ -122,6 +122,7 @@ pub enum DerivedKey {
/// BLS public key (48 bytes)
BLS(Vec<u8>),
/// EdDSA public key (32 bytes)
#[cfg(feature = "eddsa")]
EdDSA(Vec<u8>),
}

Expand Down Expand Up @@ -490,14 +491,26 @@ impl AddressPool {

(address, PublicKeyType::BLS(public_key_bytes))
}
#[cfg(feature = "eddsa")]
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 payload = Payload::PubkeyHash(dashcore::PubkeyHash::from_byte_array(node_id));
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