Skip to content
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the changes in this file are unrelated to reservations, 2 PRs would make things easier to review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — pulled out. masternodes_by_voting_key and its tests are independent of the reservation work (different crate, and nothing on the reservation path calls it — its only callers are its own tests), so I reverted the file to its dev state. This PR is now reservations-only; the lookup can land in its own PR. Done in c59911b.

/// `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