Skip to content
Open
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
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 on lines +116 to +121

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the referenced file and nearby implementation/test.
FILE="dash/src/sml/masternode_list/masternode_helpers.rs"
if [ -f "$FILE" ]; then
  echo "== file exists =="
  wc -l "$FILE"
  echo
  echo "== ast outline =="
  ast-grep outline "$FILE" || true
  echo
  echo "== referenced lines =="
  sed -n '1,180p' "$FILE" | cat -n
else
  echo "file not found: $FILE"
  echo
  echo "== possible matches =="
  fd -i 'masternode_helpers' || true
fi

echo
echo "== search for masternodes_by_voting_key =="
rg -n "masternodes_by_voting_key|BTreeMap|Masternode" dash/src/sml || true

Repository: dashpay/rust-dashcore

Length of output: 43457


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Rust availability =="
if command -v rustc >/dev/null 2>&1; then rustc --version; else echo "rustc not available"; fi
if command -v rustup >/dev/null 2>&1; then rustup --version; else echo "rustup not available"; fi

echo
echo "== standalone BTreeMap iterator order probe =="
python3 - <<'PY'
from collections import OrderedDict

def bst_order(keys):
    # Python dicts do not model BTreeMap behavior, so run this only if rustc is available.
    return None

def sort_mimic(keys):
    return sorted(keys)

print({0: [1,3], (1,3): [1,3]})
PY

if command -v rustc >/dev/null 2>&1; then
  tmpdir="$(mktemp -d)"
  cat > "$tmpdir/btreemap_order.rs" <<'RS'
use std::collections::BTreeMap;

fn main() {
    let mut map: BTreeMap<u8, &'static str> = BTreeMap::new();
    map.insert(3, "back");
    map.insert(1, "front");
    map.insert(2, "middle");

    let result: Vec<u8> = map.values().map(|_| 0).collect();
    println!("{:?}", result);
}
RS
  # Only compile/read the probe; do not execute program behavior in the repo.
  rustc --edition 2021 "$tmpdir/btreemap_order.rs" -o "$tmpdir/btreemap_order"
  "$tmpdir/btreemap_order"
else
  echo "skipped executable BTreeMap probe: rustc unavailable"
fi

Repository: dashpay/rust-dashcore

Length of output: 353


🌐 Web query:

Rust std::collections::BTreeMap Documentation iteration order ascending key Order

💡 Result:

In Rust, the std::collections::BTreeMap is an ordered map that stores its entries in ascending order based on the keys [1][2][3]. Because BTreeMap maintains its elements in a sorted state, any iterator produced by the map—such as those returned by iter, keys, values, or into_iter—will yield items in ascending key order [1][2][3]. The requirement for this behavior is that the key type must implement the Ord trait, providing a total order for all keys in the map [1][2][3]. This iteration order is a fundamental property of the BTreeMap data structure and is guaranteed by the Rust standard library documentation [1][2][3].

Citations:


Document masternodes_by_voting_key ordering guarantees.

self.masternodes is a sorted BTreeMap, but the public docs do not state that masternodes_by_voting_key returns matches in ascending ProTxHash order. Add that ordering promise if callers need it, and keep the test assertions without masking that guarantee.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash/src/sml/masternode_list/masternode_helpers.rs` around lines 116 - 121,
Document in masternodes_by_voting_key that matching masternodes are returned in
ascending ProTxHash order, based on the underlying sorted BTreeMap. Update the
test to assert the returned order directly rather than sorting matched before
checking the expected seed values.


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