-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: derive rotation cycle key from the cycle boundary block #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/rotated-quorums-index
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -631,6 +631,19 @@ impl MasternodeListEngine { | |||||||||||||||||||||||||||||
| .map(|quorum_entry| quorum_entry.llmq_type) | ||||||||||||||||||||||||||||||
| .unwrap_or(self.network.isd_llmq_type()); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // IS locks reference the cycle boundary block hash as their `cyclehash` field, | ||||||||||||||||||||||||||||||
| // so `rotated_quorums_per_cycle` must be keyed by that hash. | ||||||||||||||||||||||||||||||
| #[cfg(feature = "quorum_validation")] | ||||||||||||||||||||||||||||||
| let cycle_boundary_hash = { | ||||||||||||||||||||||||||||||
| let cycle_boundary_height = h_height + WORK_DIFF_DEPTH; | ||||||||||||||||||||||||||||||
| *self.block_container.get_hash(&cycle_boundary_height).ok_or( | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion [medium confidence]: No test for missing cycle boundary block (error path) The new Suggested fix
Suggested change
AI context{
"file": "dash/src/sml/masternode_list_engine/mod.rs",
"line": 639,
"severity": "suggestion",
"confidence": "medium",
"flaggedBy": [
"Testing & Coverage"
],
"title": "No test for missing cycle boundary block (error path)",
"fix": "#[test]\nfn feed_qr_info_returns_error_when_cycle_boundary_block_missing() {\n // Build engine whose block_container does NOT contain the cycle boundary\n // block, then assert that feed_qr_info re"
} |
||||||||||||||||||||||||||||||
| QuorumValidationError::RequiredBlockNotPresent( | ||||||||||||||||||||||||||||||
| BlockHash::all_zeros(), | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion [high confidence]: BlockHash::all_zeros() sentinel breaks RequiredBlockNotPresent contract All existing call sites of Suggested fix
Suggested change
AI context{
"file": "dash/src/sml/masternode_list_engine/mod.rs",
"line": 641,
"severity": "suggestion",
"confidence": "high",
"flaggedBy": [
"Security & Safety",
"Architecture & Design"
],
"title": "BlockHash::all_zeros() sentinel breaks RequiredBlockNotPresent contract",
"fix": "// In QuorumValidationError:\n#[error(\"Required block at height {0} not present: {1}\")]\nRequiredBlockAtHeightNotPresent(u32, String),\n\n// At the call site:\nQuorumValidationError::RequiredBlockAtHeightN"
} |
||||||||||||||||||||||||||||||
| format!("cycle boundary at height {cycle_boundary_height}"), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| )? | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if let Some((quorum_snapshot_at_h_minus_4c, mn_list_diff_at_h_minus_4c)) = | ||||||||||||||||||||||||||||||
| quorum_snapshot_and_mn_list_diff_at_h_minus_4c | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
|
|
@@ -725,9 +738,6 @@ impl MasternodeListEngine { | |||||||||||||||||||||||||||||
| LLMQEntryVerificationStatus, | ||||||||||||||||||||||||||||||
| )> = Vec::new(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let cycle_key = | ||||||||||||||||||||||||||||||
| qualified_last_commitment_per_index.first().map(|q| q.quorum_entry.quorum_hash); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for rotated_quorum in qualified_last_commitment_per_index.iter_mut() { | ||||||||||||||||||||||||||||||
| tracing::debug!( | ||||||||||||||||||||||||||||||
| " Current cycle quorum: hash={}, raw_quorum_index={:?}, map_key={:?}", | ||||||||||||||||||||||||||||||
|
|
@@ -762,13 +772,9 @@ impl MasternodeListEngine { | |||||||||||||||||||||||||||||
| *status = rotated_quorum.verified.clone(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if let Some(key) = cycle_key { | ||||||||||||||||||||||||||||||
| let cycle_map = build_cycle_quorum_map( | ||||||||||||||||||||||||||||||
| qualified_last_commitment_per_index, | ||||||||||||||||||||||||||||||
| rotation_quorum_type, | ||||||||||||||||||||||||||||||
| )?; | ||||||||||||||||||||||||||||||
| *self.rotated_quorums_per_cycle.entry(key).or_default() = cycle_map; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| let cycle_map = | ||||||||||||||||||||||||||||||
| build_cycle_quorum_map(qualified_last_commitment_per_index, rotation_quorum_type)?; | ||||||||||||||||||||||||||||||
| *self.rotated_quorums_per_cycle.entry(cycle_boundary_hash).or_default() = cycle_map; | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion [medium confidence]: Removing empty-list guard creates spurious empty entries in rotated_quorums_per_cycle The old code guarded both the Suggested fix
Suggested change
AI context{
"file": "dash/src/sml/masternode_list_engine/mod.rs",
"line": 777,
"severity": "suggestion",
"confidence": "medium",
"flaggedBy": [
"Correctness & Logic",
"Architecture & Design"
],
"title": "Removing empty-list guard creates spurious empty entries in rotated_quorums_per_cycle",
"fix": "// Option 1 – guard the whole block (mirrors old semantics)\nif !qualified_last_commitment_per_index.is_empty() {\n let cycle_map = build_cycle_quorum_map(qualified_last_commitment_per_index, rotatio"
} |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Apply collected updates after iteration to avoid borrow conflicts | ||||||||||||||||||||||||||||||
| for (heights, quorum_type, quorum_hash, new_status) in updates { | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚫 Required [high confidence]: The variable Suggested fix
Suggested change
AI context{
"file": "dash/src/sml/masternode_list_engine/mod.rs",
"line": 781,
"severity": "required",
"confidence": "high",
"flaggedBy": [
"Architecture & Design"
],
"title": "`cycle_boundary_hash` used outside its `#[cfg(feature = \"quorum_validation\")]` guard",
"fix": "#[cfg(feature = \"quorum_validation\")]\n{\n let cycle_boundary_hash = { /* ... */ };\n if let Some((snap, diff)) = quorum_snapshot_and_mn_list_diff_at_h_minus_4c {\n /* ... */\n *self.ro"
} |
||||||||||||||||||||||||||||||
|
|
@@ -859,12 +865,10 @@ impl MasternodeListEngine { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion [medium confidence]: New else-branch calls build_cycle_quorum_map unconditionally; no test for empty qualified_last_commitment_per_index Previously the else-branch was guarded by Suggested fix
Suggested change
AI context{
"file": "dash/src/sml/masternode_list_engine/mod.rs",
"line": 865,
"severity": "suggestion",
"confidence": "medium",
"flaggedBy": [
"Testing & Coverage"
],
"title": "New else-branch calls build_cycle_quorum_map unconditionally; no test for empty qualified_last_commitment_per_index",
"fix": "Add a unit test that feeds a `qr_info` whose `qualified_last_commitment_per_index` is empty (no quorum-snapshot branch) and asserts the expected outcome — either an empty map entry is created or an er"
} |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } else if let Some(cycle_key) = | ||||||||||||||||||||||||||||||
| qualified_last_commitment_per_index.first().map(|q| q.quorum_entry.quorum_hash) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| let cycle_map = | ||||||||||||||||||||||||||||||
| build_cycle_quorum_map(qualified_last_commitment_per_index, rotation_quorum_type)?; | ||||||||||||||||||||||||||||||
| *self.rotated_quorums_per_cycle.entry(cycle_key).or_default() = cycle_map; | ||||||||||||||||||||||||||||||
| *self.rotated_quorums_per_cycle.entry(cycle_boundary_hash).or_default() = cycle_map; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #[cfg(not(feature = "quorum_validation"))] | ||||||||||||||||||||||||||||||
|
|
@@ -1187,7 +1191,7 @@ mod tests { | |||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
| use crate::sml::masternode_list::MasternodeList; | ||||||||||||||||||||||||||||||
| use crate::sml::masternode_list_engine::{ | ||||||||||||||||||||||||||||||
| MasternodeListEngine, MasternodeListEngineBlockContainer, | ||||||||||||||||||||||||||||||
| MasternodeListEngine, MasternodeListEngineBlockContainer, WORK_DIFF_DEPTH, | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
| use crate::sml::quorum_entry::qualified_quorum_entry::{ | ||||||||||||||||||||||||||||||
| QualifiedQuorumEntry, VerifyingChainLockSignaturesType, | ||||||||||||||||||||||||||||||
|
|
@@ -1437,6 +1441,8 @@ mod tests { | |||||||||||||||||||||||||||||
| .expect("expected to apply diff"); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let h_work_block_hash = qr_info.mn_list_diff_h.block_hash; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| masternode_list_engine | ||||||||||||||||||||||||||||||
| .feed_qr_info::<fn(&BlockHash) -> Result<u32, ClientDataRetrievalError>>( | ||||||||||||||||||||||||||||||
| qr_info, true, true, None, | ||||||||||||||||||||||||||||||
|
|
@@ -1452,6 +1458,24 @@ mod tests { | |||||||||||||||||||||||||||||
| .1, | ||||||||||||||||||||||||||||||
| &[Llmqtype400_85, Llmqtype50_60, Llmqtype400_60], | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Verify the cycle map is keyed by the cycle boundary hash, not a quorum hash. | ||||||||||||||||||||||||||||||
| let h_work_height = masternode_list_engine | ||||||||||||||||||||||||||||||
| .block_container | ||||||||||||||||||||||||||||||
| .get_height(&h_work_block_hash) | ||||||||||||||||||||||||||||||
| .expect("h work block must be in container after feed_qr_info"); | ||||||||||||||||||||||||||||||
| let expected_cycle_boundary_hash = masternode_list_engine | ||||||||||||||||||||||||||||||
| .block_container | ||||||||||||||||||||||||||||||
| .get_hash(&(h_work_height + WORK_DIFF_DEPTH)) | ||||||||||||||||||||||||||||||
| .copied() | ||||||||||||||||||||||||||||||
| .expect("cycle boundary hash must be in container after feed_qrinfo_heights_to_engine"); | ||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||
| masternode_list_engine | ||||||||||||||||||||||||||||||
| .rotated_quorums_per_cycle | ||||||||||||||||||||||||||||||
| .contains_key(&expected_cycle_boundary_hash), | ||||||||||||||||||||||||||||||
| "rotated_quorums_per_cycle should be keyed by the cycle boundary hash {}", | ||||||||||||||||||||||||||||||
| expected_cycle_boundary_hash | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Suggestion [medium confidence]: cycle_boundary_hash lookup now fails the call even when verify_rotated_quorums is false
The
cycle_boundary_hashblock is evaluated unconditionally (before theif verify_rotated_quorumsbranch), so any caller that passesverify_rotated_quorums: false— e.g. during initial sync when the cycle boundary block may not yet be in the container — will now receiveRequiredBlockNotPresentwhere previously they would have succeeded with a no-op insertion. The PR description frames this as intentional ('fails loudly'), but it silently tightens the precondition offeed_qr_infowithout a corresponding update to documentation or caller sites. At minimum, document thatcycle_boundary_heightmust be present inblock_containerregardless of theverify_rotated_quorumsflag.AI context
{ "file": "dash/src/sml/masternode_list_engine/mod.rs", "line": 637, "severity": "suggestion", "confidence": "medium", "flaggedBy": [ "Security & Safety" ], "title": "cycle_boundary_hash lookup now fails the call even when verify_rotated_quorums is false" }