Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
85 changes: 84 additions & 1 deletion dash/src/sml/masternode_list/masternode_helpers.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
use std::net::IpAddr;

use crate::ProTxHash;
use crate::sml::masternode_list::MasternodeList;
use crate::{ProTxHash, PubkeyHash};

impl MasternodeList {
/// Every masternode in the list whose voting key hash matches
/// `voting_key_id`, returned as their registration proTxHashes.
///
/// Mirrors dashj's `MasternodeList.getMasternodesByVotingKey(votingKeyId)`
/// — the lookup contested-username voting uses to resolve which
/// masternode(s) a given voting key is entitled to cast a vote for.
/// `key_id_voting` is the 20-byte hash160 of the voting public key; a
/// single voting key can back more than one masternode, so the result is
/// a `Vec` (empty when no entry matches).
pub fn masternodes_by_voting_key(&self, voting_key_id: &PubkeyHash) -> Vec<ProTxHash> {
self.masternodes
.values()
.filter(|node| node.masternode_list_entry.key_id_voting == *voting_key_id)
.map(|node| node.masternode_list_entry.pro_reg_tx_hash)
.collect()
}

pub fn has_valid_masternode(&self, pro_reg_tx_hash: &ProTxHash) -> bool {
self.masternodes
.get(pro_reg_tx_hash)
Expand Down Expand Up @@ -46,3 +63,69 @@ pub fn reverse_cmp_sup(lhs: [u8; 32], rhs: [u8; 32]) -> bool {
// equal
false
}

#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};

use hashes::Hash;

use crate::bls_sig_utils::BLSPublicKey;
use crate::sml::masternode_list::MasternodeList;
use crate::sml::masternode_list_entry::{
EntryMasternodeType, MasternodeListEntry, MasternodeNetInfo,
};
use crate::{BlockHash, ProTxHash, PubkeyHash};

/// Build a `MasternodeList` from `(proTxHash-seed, voting-key-id)` pairs so
/// each entry gets a distinct proTxHash and a caller-chosen voting key.
fn list_from(entries: Vec<(u8, [u8; 20])>) -> MasternodeList {
let masternodes = entries
.into_iter()
.map(|(seed, voting_key_id)| {
let mut hash_bytes = [0u8; 32];
hash_bytes[0] = seed;
let pro_tx_hash = ProTxHash::from_byte_array(hash_bytes);
let entry = MasternodeListEntry {
version: 1,
pro_reg_tx_hash: pro_tx_hash,
confirmed_hash: None,
service_address: MasternodeNetInfo::Legacy(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(10, 0, 0, seed),
9999,
))),
operator_public_key: BLSPublicKey::from([0u8; 48]),
key_id_voting: PubkeyHash::from_byte_array(voting_key_id),
is_valid: true,
mn_type: EntryMasternodeType::Regular,
};
(pro_tx_hash, entry.into())
})
.collect();
MasternodeList::build(masternodes, Default::default(), BlockHash::from_byte_array([0u8; 32]), 0)
.build()
}

#[test]
fn masternodes_by_voting_key_filters_and_collects() {
let key_a = [0xAAu8; 20];
let key_b = [0xBBu8; 20];
// Two masternodes share voting key A, one uses key B.
let list = list_from(vec![(1, key_a), (2, key_b), (3, key_a)]);

let mut matched = list.masternodes_by_voting_key(&PubkeyHash::from_byte_array(key_a));
// Order is BTreeMap (proTxHash) order; sort the seed byte for a stable assert.
matched.sort_by_key(|h| h.to_byte_array()[0]);
assert_eq!(matched.len(), 2);
assert_eq!(matched[0].to_byte_array()[0], 1);
assert_eq!(matched[1].to_byte_array()[0], 3);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

let single = list.masternodes_by_voting_key(&PubkeyHash::from_byte_array(key_b));
assert_eq!(single.len(), 1);
assert_eq!(single[0].to_byte_array()[0], 2);

// A voting key no masternode uses yields an empty vec.
let none = list.masternodes_by_voting_key(&PubkeyHash::from_byte_array([0xCCu8; 20]));
assert!(none.is_empty());
}
}
1 change: 1 addition & 0 deletions key-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub use managed_account::address_pool::{AddressInfo, AddressPool, KeySource, Poo
pub use managed_account::managed_account_type::ManagedAccountType;
pub use managed_account::managed_platform_account::ManagedPlatformAccount;
pub use managed_account::platform_address::PlatformP2PKHAddress;
pub use managed_account::ReservationToken;
pub use mnemonic::{Language, Mnemonic};
pub use seed::Seed;
pub use signer::{ExtendedPubKeySigner, Signer, SignerMethod, TransactionCategory};
Expand Down
32 changes: 31 additions & 1 deletion key-wallet/src/managed_account/managed_core_funds_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::managed_account::address_pool;
use crate::managed_account::managed_account_trait::ManagedAccountTrait;
use crate::managed_account::managed_account_type::ManagedAccountType;
use crate::managed_account::managed_core_keys_account::ManagedCoreKeysAccount;
use crate::managed_account::reservation::ReservationSet;
use crate::managed_account::reservation::{ReservationSet, ReservationToken};
use crate::managed_account::transaction_record::{
InputDetail, OutputDetail, OutputRole, TransactionDirection,
};
Expand Down Expand Up @@ -130,6 +130,36 @@ impl ManagedCoreFundsAccount {
self.reservations.release(tx.input.iter().map(|input| &input.previous_output));
}

/// Owner-guarded release of `tx`'s input reservations: releases an input
/// only if it is *still owned by* `token`, the [`ReservationToken`] returned
/// when this build reserved its inputs (from
/// [`build_unsigned_reserved`]/[`build_signed_reserved`]).
///
/// This is the release a caller must use when it abandons a transaction
/// *after having `.await`ed something* between reserving and releasing —
/// above all the platform broadcast path, which reserves an
/// asset-lock/deferred send's inputs, awaits the broadcast, and on a
/// `Rejected` result releases them. During that await key-wallet's TTL sweep
/// can invisibly reclaim the reservation and a different concurrent build can
/// re-reserve the same outpoint (under a new token, same wallet generation).
/// The unconditional [`Self::release_reservation`] would then free the other
/// build's inputs, letting coin selection hand them to a second transaction —
/// a double-spend window. Passing the original token makes the release a
/// no-op for any input that has since changed owners, closing that window.
/// The check is atomic under the reservation set's mutex, so no sweep or
/// re-reserve can interleave between "is it still mine?" and the removal.
///
/// The platform layer cannot make this safe on its own because the sweep
/// happens inside key-wallet where it has no visibility; the owner check must
/// live here. See `dashpay/platform#4185`.
///
/// [`build_unsigned_reserved`]: crate::wallet::managed_wallet_info::transaction_builder::TransactionBuilder::build_unsigned_reserved
/// [`build_signed_reserved`]: crate::wallet::managed_wallet_info::transaction_builder::TransactionBuilder::build_signed_reserved
pub fn release_reservation_if_owner(&self, tx: &Transaction, token: ReservationToken) {
let outpoints: Vec<OutPoint> = tx.input.iter().map(|input| input.previous_output).collect();
self.reservations.release_if_owner(&outpoints, token);
}

/// Get a reference to the inner keys-account state.
pub fn keys(&self) -> &ManagedCoreKeysAccount {
&self.keys
Expand Down
1 change: 1 addition & 0 deletions key-wallet/src/managed_account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ pub mod transaction_record;
pub use managed_account_ref::{ManagedAccountRef, ManagedAccountRefMut, OwnedManagedCoreAccount};
pub use managed_core_funds_account::ManagedCoreFundsAccount;
pub use managed_core_keys_account::ManagedCoreKeysAccount;
pub use reservation::ReservationToken;
Loading