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
22 changes: 16 additions & 6 deletions key-wallet/src/account/account_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,13 @@ impl AccountType {
]))
}
Self::DashpayReceivingFunds {
index: account_index,
user_identity_id,
friend_identity_id,
..
} => {
// Base DashPay root + account 0' + user_id/friend_id (non-hardened per DIP-14/DIP-15)
// Base DashPay root + account' + user_id/friend_id (non-hardened per DIP-14/DIP-15).
// The account is the sender's DashPay account (DIP-15 "Account Reference"),
// hardened; defaults to 0 but may differ for multi-account wallets.
let mut path = match network {
Network::Mainnet => {
DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_MAINNET)
Expand All @@ -483,7 +485,10 @@ impl AccountType {
DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_TESTNET)
}
};
path.push(ChildNumber::from_hardened_idx(0).map_err(crate::error::Error::Bip32)?);
path.push(
ChildNumber::from_hardened_idx(*account_index)
.map_err(crate::error::Error::Bip32)?,
);
path.push(ChildNumber::Normal256 {
index: *user_identity_id,
});
Expand All @@ -493,11 +498,13 @@ impl AccountType {
Ok(path)
}
Self::DashpayExternalAccount {
index: account_index,
user_identity_id,
friend_identity_id,
..
} => {
// Base DashPay root + account 0' + friend_id/user_id (non-hardened per DIP-14/DIP-15)
// Base DashPay root + account' + friend_id/user_id (non-hardened per DIP-14/DIP-15).
// The account is the sender's DashPay account (DIP-15 "Account Reference"),
// hardened; defaults to 0 but may differ for multi-account wallets.
let mut path = match network {
Network::Mainnet => {
DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_MAINNET)
Expand All @@ -506,7 +513,10 @@ impl AccountType {
DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_TESTNET)
}
};
path.push(ChildNumber::from_hardened_idx(0).map_err(crate::error::Error::Bip32)?);
path.push(
ChildNumber::from_hardened_idx(*account_index)
.map_err(crate::error::Error::Bip32)?,
);
path.push(ChildNumber::Normal256 {
index: *friend_identity_id,
});
Expand Down
79 changes: 79 additions & 0 deletions key-wallet/src/tests/account_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,3 +549,82 @@ fn test_account_derivation_path_uniqueness() {
paths.push(path_str);
}
}

#[test]
fn test_dashpay_account_index_in_derivation_path() {
use crate::bip32::ChildNumber;

// The DashPay friendship path is m/9'/coin'/15'/account'/<id>/<id> (DIP-15 "Account
// Reference" + DIP-14 256-bit indices). The account segment is the sender's DashPay
// account and MUST reflect the account index carried on the account type — not a fixed
// 0'. A multi-account wallet that derived every relationship under account 0 would
// watch the wrong addresses and miss funds paid to a non-zero account. Account 0 must
// stay byte-identical to the legacy single-account path (backward compatibility).
let network = Network::Testnet;
let user_id = [0x11u8; 32];
let friend_id = [0x22u8; 32];

for account in [0u32, 1, 7, 42] {
// Receiving funds: identity ids ordered user/friend.
let recv = AccountType::DashpayReceivingFunds {
index: account,
user_identity_id: user_id,
friend_identity_id: friend_id,
};
let recv_path = recv.derivation_path(network).unwrap();
let recv_comps: Vec<ChildNumber> = recv_path.clone().into();
assert_eq!(recv_comps.len(), 6, "root(3) + account' + two 256-bit ids");
assert_eq!(
recv_comps[3],
ChildNumber::Hardened {
index: account
},
"receiving account segment must honor the account index"
);
assert_eq!(
recv_comps[4],
ChildNumber::Normal256 {
index: user_id
}
);
assert_eq!(
recv_comps[5],
ChildNumber::Normal256 {
index: friend_id
}
);
assert!(
recv_path.to_string().starts_with(&format!("m/9'/1'/15'/{}'/", account)),
"receiving path should carry account {}': got {}",
account,
recv_path
);

// External account: the reverse channel, ids ordered friend/user.
let ext = AccountType::DashpayExternalAccount {
index: account,
user_identity_id: user_id,
friend_identity_id: friend_id,
};
let ext_comps: Vec<ChildNumber> = ext.derivation_path(network).unwrap().into();
assert_eq!(
ext_comps[3],
ChildNumber::Hardened {
index: account
},
"external account segment must honor the account index"
);
assert_eq!(
ext_comps[4],
ChildNumber::Normal256 {
index: friend_id
}
);
assert_eq!(
ext_comps[5],
ChildNumber::Normal256 {
index: user_id
}
);
}
}
Loading