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
29 changes: 29 additions & 0 deletions .github/workflows/manki.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Manki Review

on:
pull_request:
types: [opened, synchronize]
issue_comment:
types: [created, edited]
pull_request_review:
types: [submitted]

permissions:
contents: read
pull-requests: write
issues: write
id-token: write

concurrency:
group: manki-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
cancel-in-progress: false

jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: manki-review/manki@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
18 changes: 18 additions & 0 deletions .manki.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Manki — AI Code Review Configuration
# https://github.com/manki-review/manki

auto_review: true
auto_approve: true

exclude_paths:
- "*.lock"
- "fuzz/**"

instructions: |
This is a Rust implementation of the Dash cryptocurrency protocol.
Pay special attention to:
- Consensus-critical code paths — changes here can cause chain splits
- FFI safety for C/Swift bindings in dash-spv-* crates
- Proper error handling — prefer Result over unwrap/expect in library code
- DIP (Dash Improvement Proposal) compliance for protocol changes
- Memory safety in hash/crypto implementations
73 changes: 14 additions & 59 deletions dash/src/sml/masternode_list_engine/message_request_verification.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::BTreeMap;

use hashes::Hash;

use crate::hash_types::QuorumOrderingHash;
Expand All @@ -12,7 +14,7 @@ impl MasternodeListEngine {
fn is_lock_potential_quorums(
&self,
instant_lock: &InstantLock,
) -> Result<&Vec<QualifiedQuorumEntry>, MessageVerificationError> {
) -> Result<&BTreeMap<u16, QualifiedQuorumEntry>, MessageVerificationError> {
// Retrieve the cycle hash from the Instant Lock
let cycle_hash = instant_lock.cyclehash;

Expand All @@ -29,11 +31,11 @@ impl MasternodeListEngine {
.ok_or(MessageVerificationError::CycleHashNotPresent(cycle_hash))?;

tracing::debug!("Found {} quorums for cyclehash {}", quorums.len(), cycle_hash);
for q in quorums.iter() {
for (idx, q) in quorums.iter() {
tracing::debug!(
" Quorum: hash={}, index={:?}, height at block_container={:?}",
" Quorum: index={}, hash={}, height at block_container={:?}",
idx,
q.quorum_entry.quorum_hash,
q.quorum_entry.quorum_index,
self.block_container.get_height(&q.quorum_entry.quorum_hash)
);
}
Expand Down Expand Up @@ -126,16 +128,13 @@ impl MasternodeListEngine {
quorum_index
);

// Find the quorum by its quorum_index field.
let quorum = quorums
.iter()
.find(|q| q.quorum_entry.quorum_index == Some(quorum_index as i16))
.ok_or({
MessageVerificationError::QuorumIndexNotFound(
quorum_index as u16,
instant_lock.cyclehash,
)
})?;
// Look up the quorum by index.
let quorum = quorums.get(&(quorum_index as u16)).ok_or({
MessageVerificationError::QuorumIndexNotFound(
quorum_index as u16,
instant_lock.cyclehash,
)
})?;

tracing::debug!(
"IS lock selected quorum: hash={}, index={:?}, public_key={}, verified={:?}",
Expand Down Expand Up @@ -540,50 +539,6 @@ mod tests {
mn_list_engine.verify_chain_lock(&chain_lock).expect("expected to verify chain lock");
}

/// Test that quorums are looked up by their quorum_index field, not by array position.
#[test]
pub fn is_lock_quorum_lookup_by_quorum_index_not_array_position() {
let block_hex =
include_str!("../../../tests/data/test_DML_diffs/masternode_list_engine.hex");
let data = hex::decode(block_hex).expect("decode hex");
let mut mn_list_engine: MasternodeListEngine =
bincode::decode_from_slice(&data, bincode::config::standard())
.expect("expected to decode")
.0;

let lock_data = hex::decode("01018d53e7997ead57409750942af0d5e0aafc06f852a9a52308f4781b6a8220298f00000000c6f9d8c63dd15937ea70aaddb7890daad42c91bf6818e2bf76d183d6f2d9215b4b5f84978fad9dde7ab52bdcc0674be891e9029cc1ef0cb01200000000000000a27c98836c4c04653ab81eb4e07ddfc2c8c2c1036b75247969c05a4f25451cd78913a971f1899d9f2bddec9cf8e0104004f72f20c2856453e5aa3bcd2a8200670ec28feda38f67cc400fc72ef1966956656ec0765478c9d16e9a9e470c07f9ed").expect("expected valid hex");
let lock: InstantLock = deserialize(lock_data.as_slice()).expect("expected to deserialize");

// Get the original result
let (original_quorum, _, original_index) =
mn_list_engine.is_lock_quorum(&lock).expect("expected to get quorum");
let expected_quorum_hash = original_quorum.quorum_entry.quorum_hash;
let expected_quorum_index = original_quorum.quorum_entry.quorum_index;
assert_eq!(original_index, 23);
assert_eq!(expected_quorum_index, Some(23));

// Reverse the quorum array order so array positions no longer match quorum indices
let cycle_hash = lock.cyclehash;
if let Some(quorums) = mn_list_engine.rotated_quorums_per_cycle.get_mut(&cycle_hash) {
quorums.reverse();

// Verify the quorum with index 23 is no longer at position 23
let quorum_at_pos_23 = quorums.get(23);
assert!(
quorum_at_pos_23.is_none()
|| quorum_at_pos_23.unwrap().quorum_entry.quorum_index != Some(23),
"after reversing, quorum at position 23 should not have quorum_index 23"
);
}

// The lookup should still find the correct quorum by its quorum_index field
let (found_quorum, _, found_index) =
mn_list_engine.is_lock_quorum(&lock).expect("expected to find quorum by index");
assert_eq!(found_index, 23);
assert_eq!(found_quorum.quorum_entry.quorum_hash, expected_quorum_hash);
assert_eq!(found_quorum.quorum_entry.quorum_index, expected_quorum_index);
}

/// Test that QuorumIndexNotFound error is returned when the required quorum index is missing.
#[test]
pub fn is_lock_quorum_not_found_error() {
Expand All @@ -607,7 +562,7 @@ mod tests {
// Remove the quorum with index 23 from the cycle
let cycle_hash = lock.cyclehash;
if let Some(quorums) = mn_list_engine.rotated_quorums_per_cycle.get_mut(&cycle_hash) {
quorums.retain(|q| q.quorum_entry.quorum_index != Some(23));
quorums.remove(&23);
}

// Now the lookup should return QuorumIndexNotFound
Expand Down
Loading
Loading