fix(dash): make feed_qr_info resilient to missing rotation CL sigs#736
fix(dash): make feed_qr_info resilient to missing rotation CL sigs#736xdustinface merged 11 commits intov0.42-devfrom
feed_qr_info resilient to missing rotation CL sigs#736Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds rotation-ChainLock signature slot tracking, per-call QRInfo observability, and a previous-cycle validation/storage path. feed_qr_info now returns an optional QRInfoFeedResult; rotated-quorum cycles are stored only when every entry is Verified and rotation signatures for all four slots are present. Tests and fixtures updated. ChangesQRInfo Rotation Cycle Validation & Storage
Sequence DiagramsequenceDiagram
participant Test as Test Harness
participant Engine as MasternodeListEngine
participant QRInfo as QRInfo Input
participant MNList as MN List State
participant RotSigs as RotationSig Collector
participant Validator as Quorum Validator
participant Storage as RotatedQuorums Storage
Test->>Engine: feed_qr_info(qr_info, work_block, diffs)
activate Engine
Engine->>MNList: apply snapshots & diffs
MNList-->>Engine: updated MN list state
Engine->>RotSigs: collect rotation sigs (h-3c, h-2c, h-c, h)
RotSigs-->>Engine: signatures tuple or missing info
alt signatures_present
Engine->>Engine: determine alignment (current vs previous cycle)
alt previous_cycle_alignment
Engine->>Engine: try_load_previous_cycle_entries(work_block)
Engine->>Validator: validate_and_store_previous_cycle_quorums(entries, sigs)
Validator->>Validator: validate each rotated quorum
Validator->>Storage: store cycle if all entries Verified
Validator-->>Engine: validation results
else current_cycle_alignment
Engine->>Validator: validate current-cycle rotated quorums with sigs
Validator-->>Engine: validation results
end
Engine->>Engine: compute counts (rotated, newly_qualified, fully_verified)
Engine->>Storage: optionally store verified cycle
Engine-->>Test: Ok(Some(QRInfoFeedResult{...}))
else missing_signatures
Engine-->>Test: Ok(Some(QRInfoFeedResult{... stored_cycle_height: None })) or Err(QuorumValidationError)
end
deactivate Engine
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v0.42-dev #736 +/- ##
=============================================
+ Coverage 71.44% 71.50% +0.05%
=============================================
Files 320 320
Lines 68825 69078 +253
=============================================
+ Hits 49171 49393 +222
- Misses 19654 19685 +31
|
bdd23ad to
239da59
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with 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.
Inline comments:
In `@dash/src/sml/masternode_list_engine/mod.rs`:
- Line 123: Replace the panic-causing expect in the Some(...) expression by
propagating the optional values instead: instead of Some(slots.map(|(sig, _)|
sig.expect("all slots checked above"))), map the tuple iterator to the
Option<Sig> (| (sig, _) | sig) and use Option/Iterator helpers (e.g.,
collect::<Option<Vec<_>>>() or Iterator::transpose/Option::transpose) to convert
the sequence of Option<Sig> into an Option of a collection and return/propagate
None when any slot is missing; update the surrounding code to work with that
Option result rather than unwrapping with expect (locate the expression
referencing slots in masternode_list_engine::mod.rs and replace the expect usage
accordingly).
- Around line 1080-1103: The code currently writes into
rotated_quorums_per_cycle even when verify_rotated_quorums == false and not all
entries are Verified; change the insertion guard so you only call
build_cycle_quorum_map and store into self.rotated_quorums_per_cycle when the
incoming entries are fully verified. Concretely, before calling
build_cycle_quorum_map and assigning to
self.rotated_quorums_per_cycle.entry(cycle_key), require that
qualified_last_commitment_per_index.iter().all(|q| matches!(q.verified,
LLMQEntryVerificationStatus::Verified)) (or compare fully_verified_count ==
qualified_last_commitment_per_index.len()), and only proceed when that condition
is true and already_fully_verified is false; otherwise skip the insertion so
existing upgrade/early-return logic remains valid.
- Around line 556-560: The code currently derives cycle_hash via
entries.first().map(|q| q.quorum_entry.quorum_hash) which picks the first entry
in hash order and can mis-key cycles; instead locate the quorum whose
quorum_entry.quorum_index == Some(0) and use that entry's
quorum_entry.quorum_hash as the cycle key. Replace the entries.first() call with
something like entries.iter().find(|e| e.quorum_entry.quorum_index ==
Some(0)).map(|q| q.quorum_entry.quorum_hash) and keep the existing
rotated_quorums_per_cycle.contains_key(&cycle_hash) and return semantics
unchanged so previous-cycle quorums aren’t stored under the wrong key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b76549b6-bef9-4b10-abfc-1f4a3403d7c8
📒 Files selected for processing (1)
dash/src/sml/masternode_list_engine/mod.rs
Replace the provably safe but rule-violating `expect()` with `?` propagation on a destructured array. Behavior is unchanged: the missing-slot debug log still fires before the unwrap path, and `?` on already-checked `Some` values short-circuits cleanly. Addresses CodeRabbit review comment on PR dashpay#736 dashpay#736 (comment)
`try_load_previous_cycle_entries` derived its `cycle_hash` via `entries.first()` from a `BTreeMap<QuorumHash, _>`, so the key was the hash-order minimum rather than the cycle-boundary quorum. Dash Core defines `cycleHash` as the block hash at the cycle-base height, which equals the `quorum_hash` of the index-0 quorum, and IS lock verification looks up `rotated_quorums_per_cycle` with that key. Mis-keyed storage caused silent IS-lock verification failures for the previous cycle. Find the index-0 entry's `quorum_hash` instead, and short-circuit the already-stored check before allocating the `entries` vector. Addresses CodeRabbit review comment on PR dashpay#736 dashpay#736 (comment)
…fied
The `else if` branch in `feed_qr_info` skipped verification but still wrote into `rotated_quorums_per_cycle` whenever `already_fully_verified` was false. That contradicted the documented invariant ("a cycle is only stored when every entry is `Verified`") and could poison IS-lock BLS verification with unverified `quorum_public_key` values.
Mirror the verified-only gate from the `verify_rotated_quorums == true` path: only store when every entry is `Verified` (which can still hold via cache hits from `known_qualified_quorum_entry`).
Addresses CodeRabbit review comment on PR dashpay#736
dashpay#736 (comment)
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
dash/src/sml/masternode_list_engine/mod.rs (1)
937-938: ⚡ Quick win
cycle_keyderived via.first()— usequorum_index == Some(0)explicitly.Both the
verify_rotated_quorumspath (line 937) and theelsepath (line 1084) derive the cycle boundary key from.first(), relying on the wire order oflast_commitment_per_index. If a peer sends entries out of order, the wrong hash is used as the cycle key and subsequent IS-lock lookups silently fail. The same concern was raised fortry_load_previous_cycle_entriesand is already fixed there with an explicit index-0 find.🛠️ Proposed fix
- let cycle_key = - qualified_last_commitment_per_index.first().map(|q| q.quorum_entry.quorum_hash); + let cycle_key = qualified_last_commitment_per_index + .iter() + .find(|q| q.quorum_entry.quorum_index == Some(0)) + .map(|q| q.quorum_entry.quorum_hash);Apply the same change to the
else if let Some(cycle_key) = ...arm at line 1084.Also applies to: 1084-1085
🤖 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_engine/mod.rs` around lines 937 - 938, The cycle_key is currently taken with .first() from qualified_last_commitment_per_index which can be wrong if entries are out-of-order; change both places where cycle_key is derived (the binding named cycle_key in the verify_rotated_quorums path and the other else/if arm) to locate the index-0 entry explicitly by using .iter().find(|q| q.quorum_entry.quorum_index == Some(0)).map(|q| q.quorum_entry.quorum_hash) (mirroring the fix used in try_load_previous_cycle_entries) so the cycle boundary uses the quorum_index == Some(0) entry rather than the first vector element.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@dash/src/sml/masternode_list_engine/mod.rs`:
- Around line 1865-1867: The test
feed_qr_info_rejects_post_v20_with_missing_chainlock_signatures calls the helper
load_qrinfo_2240504_fixture which is gated by #[cfg(feature =
"quorum_validation")], causing compilation failures when the feature is
disabled; fix by adding the same #[cfg(feature = "quorum_validation")] gate to
the test (or conditionally skip the call by guarding inside the test) so the
test and helper have matching feature gating and compilation succeeds,
referencing the test function
feed_qr_info_rejects_post_v20_with_missing_chainlock_signatures and the helper
load_qrinfo_2240504_fixture.
- Around line 801-823: The variables maybe_sig_h_minus_3c, work_block_hash_h,
and maybe_sig_tip are always bound but only used under #[cfg(feature =
"quorum_validation")], causing unused-variable warnings when that feature is
off; fix this by moving their let bindings behind the same #[cfg(feature =
"quorum_validation")] guard (or splitting the surrounding code so the apply_diff
calls that produce maybe_sig_h_minus_3c and maybe_sig_tip and the assignment to
work_block_hash_h are only compiled when quorum_validation is enabled),
referencing the existing symbols (apply_diff, mn_list_diff_at_h_minus_3c,
mn_list_diff_h, mn_list_diff_tip, maybe_sig_h_minus_3c, work_block_hash_h,
maybe_sig_tip) so the bindings are only created in the same cfg scope where they
are consumed.
---
Nitpick comments:
In `@dash/src/sml/masternode_list_engine/mod.rs`:
- Around line 937-938: The cycle_key is currently taken with .first() from
qualified_last_commitment_per_index which can be wrong if entries are
out-of-order; change both places where cycle_key is derived (the binding named
cycle_key in the verify_rotated_quorums path and the other else/if arm) to
locate the index-0 entry explicitly by using .iter().find(|q|
q.quorum_entry.quorum_index == Some(0)).map(|q| q.quorum_entry.quorum_hash)
(mirroring the fix used in try_load_previous_cycle_entries) so the cycle
boundary uses the quorum_index == Some(0) entry rather than the first vector
element.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 45f1e5d2-442c-41bc-8f5e-6dee370f4a6d
📒 Files selected for processing (1)
dash/src/sml/masternode_list_engine/mod.rs
Gate the per-cycle rotation-sig bindings, the `RotationChainLockSignatureSlot` enum, its `Display` impl, and the `core::fmt` import behind `#[cfg(feature = "quorum_validation")]` so default-feature builds stay warning-free. Addresses CodeRabbit review comment on PR dashpay#736 dashpay#736 (comment)
…_signatures` test on `quorum_validation` The test calls `load_qrinfo_2240504_fixture` which is gated on `quorum_validation`. Without the matching gate the test fails to compile when the feature is off. Addresses CodeRabbit review comment on PR dashpay#736 dashpay#736 (comment)
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
dash/src/sml/masternode_list_engine/mod.rs (5)
866-871: 💤 Low valueField/order alignment between
sigs(3-tuple) and the 4-slot rotation set is load-bearing — consider a brief comment.
sigs = [s2, s1, s0]here is the[h-2c, h-c, h]triple consumed byapply_diff(mn_list_diff_tip, ..., sigs)asprevious_chain_lock_sigs: Option<[BLSSignature; 3]>for tip-rotation validation, and is independent of the 4-slotrotation_sigsassembled at lines 900–933. The two ordering conventions live a few lines apart and use very similar names; a one-line comment here noting "3-sig tuple for tip apply_diff; the separate 4-sig tuple at line 900 is forlast_commitment_per_indexalignment" would prevent future accidental reuse/swap.🤖 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_engine/mod.rs` around lines 866 - 871, Add a one-line clarifying comment next to the construction of sigs (the Some([s2, s1, s0]) from maybe_sig_h_minus_2c, maybe_sig_h_minus_c, maybe_sig_h) explaining that this 3-element array represents [h-2c, h-c, h] and is passed as previous_chain_lock_sigs into apply_diff (used for tip-rotation validation), and that it is distinct from the 4-slot rotation_sigs / last_commitment_per_index structure assembled later (lines ~900–933) so the order/size must not be confused or reused interchangeably.
988-989: 💤 Low value
Invalidearly-return short-circuits status mirroring for sibling entries.When any rotated quorum returns
Invalid,feed_qr_inforeturns immediately, so:
quorum_statusesis not updated for any sibling rotated quorum in thislast_commitment_per_index(the mirror loop at 994–1013 never runs).masternode_listsupdates collected from prior loops are also discarded.- The previous-cycle path at 882–885 already ran and is preserved (this is consistent with the test at 2025–2032).
This is defensible — an Invalid signal in any rotated quorum should poison the whole cycle — but it also means observability for the rejected cycle is asymmetric (some entries get statuses mirrored, others don't, depending on iteration order). If the intent is "reject the whole cycle atomically", consider mirroring
Skipped(InvalidSiblingInCycle)(or similar) for the remaining entries before returning, so the engine's view is consistent. Otherwise a brief comment noting the asymmetric mirror-on-error would help future readers.🤖 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_engine/mod.rs` around lines 988 - 989, feed_qr_info currently returns immediately when any rotated quorum yields Invalid, which leaves quorum_statuses and masternode_lists inconsistent for sibling entries in last_commitment_per_index; update feed_qr_info so that before returning on an Invalid from a rotated quorum it mirrors a consistent status for the remaining sibling entries (e.g. set their quorum_statuses to Skipped(InvalidSiblingInCycle) or a similar sentinel) and apply the same masternode_lists updates so the cycle is rejected atomically, or if you prefer not to change behavior add a clear comment in feed_qr_info explaining the asymmetric mirroring on early-return and why it’s acceptable.
2079-2092: 💤 Low valueSynthetic sibling quorums use all-zero pubkeys/sigs — depending on validation order, they may resolve to
Unknown/Skipped, not exercise the gate.The siblings (indices 1..active_count) carry zero pubkey, zero
all_commitment_aggregated_signature, and a constructedquorum_hashwhose work block is unknown to the engine. Whethervalidate_rotation_cycle_quorums_validation_statusesreturnsSkipped(missing context) orInvalid(signature mismatch) for them determines whetherfeed_qr_inforeaches the storage gate at line 1124 or returns early at line 988. Either path keeps the existing cycle intact, but only theSkippedpath actually exercises thestore_cycle_if_fully_verifieddowngrade-protection — theInvalidpath returns before storage runs.Combined with the
let _ =at line 2144, this test could pass without ever exercising the gate. Asserting the call's success (or assertingqualified_last_commitment_per_indexsiblings settle asSkipped/Unknownrather thanInvalid) would tighten the coverage.🤖 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_engine/mod.rs` around lines 2079 - 2092, The test creates sibling QuorumEntry items in last_commitment_per_index with all-zero pubkeys/signatures and unknown quorum_hash, which can yield either Skipped or Invalid and may let the test skip exercising store_cycle_if_fully_verified; fix by making the synthetic siblings deterministically validate as Skipped or valid: either (A) populate their quorum_public_key and all_commitment_aggregated_signature with non-zero dummy values and ensure quorum_hash maps to a known work block so validate_rotation_cycle_quorums_validation_statuses returns a non-Invalid status, or (B) explicitly assert the result of feed_qr_info (remove the `let _ =` usage) and/or assert that qualified_last_commitment_per_index siblings have the expected statuses (Skipped/Unknown) before proceeding so the test actually exercises store_cycle_if_fully_verified and the downgrade-protection gate.
1117-1129: 💤 Low value
verify_rotated_quorums == falsestorage path now only stores when entries are already cached as Verified — confirm this is the intended trust path.In this branch no validation runs;
fully_verified_countonly counts entries that arrived already-Verified viaknown_qualified_quorum_entry. Combined withstore_cycle_if_fully_verified'sall_entries_verifiedgate, this means a cycle gets stored from averify_rotated_quorums=falsecall only when every rotated entry was previously verified in a prior call. That's a sound trust property (no unverified writes), but it's a behavior change from before: afalsecall no longer ever inserts a fresh cycle. Worth confirming this matches the intent expressed in the PR objectives ("cycle is stored only when every entry isVerified").🤖 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_engine/mod.rs` around lines 1117 - 1129, The branch taken when verify_rotated_quorums == false only counts entries already marked LLMQEntryVerificationStatus::Verified (via known_qualified_quorum_entry) and then calls store_cycle_if_fully_verified(cycle_key, qualified_last_commitment_per_index, rotation_quorum_type), so a cycle will be stored only if every rotated entry was previously Verified; to fix, either (A) explicitly run the same verification path used when verify_rotated_quorums == true before counting/storing (i.e. validate entries here and update their status so fully_verified_count reflects newly-verified entries), or (B) make the storage call accept partial verification by changing store_cycle_if_fully_verified semantics, or (preferred) add an explicit comment/doc and a test asserting that verify_rotated_quorums == false never inserts fresh cycles; update code around qualified_last_commitment_per_index, fully_verified_count, known_qualified_quorum_entry, and store_cycle_if_fully_verified to implement the chosen approach.
768-773: ⚡ Quick winUpdate the sync_manager.rs caller to act on the new
QRInfoFeedResultfields.The
feed_qr_infocall at dash-spv/src/sync/masternodes/sync_manager.rs:238 discards the success-sideOption<QRInfoFeedResult>and only retries when a hard tip-level ChainLock error occurs. Two follow-ups are needed to realize the observability benefit:
- Check
all_fully_verified()to drive the existingqrinfo_retry_countlogic when a QRInfo settles as "applied but not fully verified". Currently onlyRequiredRotatedChainLockSigNotPresent(0, _)(tip offset) triggers retry; partial-verification states are silently accepted.- Surface
fully_verified_count,rotated_quorum_count, andstored_cycle_heightin metrics or logs so incomplete verification cycles are visible operationally.Without this caller update, cycles may remain unstored on the SPV side with no feedback loop.
🤖 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_engine/mod.rs` around lines 768 - 773, The caller of MasternodeListEngine::feed_qr_info (in sync_manager.rs around the current callsite) ignores the success-side QRInfoFeedResult; update that caller to inspect the returned Option<QRInfoFeedResult> and (1) use QRInfoFeedResult::all_fully_verified() to decide whether to increment/trigger the existing qrinfo_retry_count path (i.e., treat "applied but not fully verified" the same as the current RequiredRotatedChainLockSigNotPresent(0, _) retry condition) instead of silently accepting it, and (2) extract and emit/log the QRInfoFeedResult fields fully_verified_count, rotated_quorum_count, and stored_cycle_height to metrics or logging so incomplete verification cycles are observable operationally. Ensure you reference feed_qr_info, QRInfoFeedResult, all_fully_verified(), and the qrinfo_retry_count retry logic when making the change.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@dash/src/sml/masternode_list_engine/mod.rs`:
- Around line 2143-2154: The test currently swallows the result of
engine.feed_qr_info(thin_qr_info, false, true) which can mask failures and
weaken the anti-clobber assertion; update the test to explicitly assert the call
succeeds (e.g., unwrap/expect the Ok variant) so that the subsequent check of
engine.rotated_quorums_per_cycle.get(&cycle_key) against original_cycle truly
verifies the gate prevented a downgrade, or, if an Err is acceptable in some
configs, add an explicit comment and assert that rotated_quorums_per_cycle was
unchanged even on Err; target the engine.feed_qr_info call, the
rotated_quorums_per_cycle lookup by cycle_key, original_cycle comparison, and
any behavior around store_cycle_if_fully_verified in your change.
---
Nitpick comments:
In `@dash/src/sml/masternode_list_engine/mod.rs`:
- Around line 866-871: Add a one-line clarifying comment next to the
construction of sigs (the Some([s2, s1, s0]) from maybe_sig_h_minus_2c,
maybe_sig_h_minus_c, maybe_sig_h) explaining that this 3-element array
represents [h-2c, h-c, h] and is passed as previous_chain_lock_sigs into
apply_diff (used for tip-rotation validation), and that it is distinct from the
4-slot rotation_sigs / last_commitment_per_index structure assembled later
(lines ~900–933) so the order/size must not be confused or reused
interchangeably.
- Around line 988-989: feed_qr_info currently returns immediately when any
rotated quorum yields Invalid, which leaves quorum_statuses and masternode_lists
inconsistent for sibling entries in last_commitment_per_index; update
feed_qr_info so that before returning on an Invalid from a rotated quorum it
mirrors a consistent status for the remaining sibling entries (e.g. set their
quorum_statuses to Skipped(InvalidSiblingInCycle) or a similar sentinel) and
apply the same masternode_lists updates so the cycle is rejected atomically, or
if you prefer not to change behavior add a clear comment in feed_qr_info
explaining the asymmetric mirroring on early-return and why it’s acceptable.
- Around line 2079-2092: The test creates sibling QuorumEntry items in
last_commitment_per_index with all-zero pubkeys/signatures and unknown
quorum_hash, which can yield either Skipped or Invalid and may let the test skip
exercising store_cycle_if_fully_verified; fix by making the synthetic siblings
deterministically validate as Skipped or valid: either (A) populate their
quorum_public_key and all_commitment_aggregated_signature with non-zero dummy
values and ensure quorum_hash maps to a known work block so
validate_rotation_cycle_quorums_validation_statuses returns a non-Invalid
status, or (B) explicitly assert the result of feed_qr_info (remove the `let _
=` usage) and/or assert that qualified_last_commitment_per_index siblings have
the expected statuses (Skipped/Unknown) before proceeding so the test actually
exercises store_cycle_if_fully_verified and the downgrade-protection gate.
- Around line 1117-1129: The branch taken when verify_rotated_quorums == false
only counts entries already marked LLMQEntryVerificationStatus::Verified (via
known_qualified_quorum_entry) and then calls
store_cycle_if_fully_verified(cycle_key, qualified_last_commitment_per_index,
rotation_quorum_type), so a cycle will be stored only if every rotated entry was
previously Verified; to fix, either (A) explicitly run the same verification
path used when verify_rotated_quorums == true before counting/storing (i.e.
validate entries here and update their status so fully_verified_count reflects
newly-verified entries), or (B) make the storage call accept partial
verification by changing store_cycle_if_fully_verified semantics, or (preferred)
add an explicit comment/doc and a test asserting that verify_rotated_quorums ==
false never inserts fresh cycles; update code around
qualified_last_commitment_per_index, fully_verified_count,
known_qualified_quorum_entry, and store_cycle_if_fully_verified to implement the
chosen approach.
- Around line 768-773: The caller of MasternodeListEngine::feed_qr_info (in
sync_manager.rs around the current callsite) ignores the success-side
QRInfoFeedResult; update that caller to inspect the returned
Option<QRInfoFeedResult> and (1) use QRInfoFeedResult::all_fully_verified() to
decide whether to increment/trigger the existing qrinfo_retry_count path (i.e.,
treat "applied but not fully verified" the same as the current
RequiredRotatedChainLockSigNotPresent(0, _) retry condition) instead of silently
accepting it, and (2) extract and emit/log the QRInfoFeedResult fields
fully_verified_count, rotated_quorum_count, and stored_cycle_height to metrics
or logging so incomplete verification cycles are observable operationally.
Ensure you reference feed_qr_info, QRInfoFeedResult, all_fully_verified(), and
the qrinfo_retry_count retry logic when making the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c81e136a-1295-4232-ae9c-97c7f9d19bc6
📒 Files selected for processing (1)
dash/src/sml/masternode_list_engine/mod.rs
…_cycle` with direct gate test
The old integration-style test claimed to exercise the storage gate's anti-clobber behavior, but actually never reached the gate. The synthetic fake quorums in `last_commitment_per_index` had a single-element `signers` bitvector, so `validate_structure` rejected them with `InsufficientSigners` and `feed_qr_info` returned `Err` before `store_cycle_if_fully_verified` was ever called. The post-call assertion that `rotated_quorums_per_cycle[K]` was unchanged passed for a trivial reason: nothing wrote to it. The named invariant ("does not downgrade already-verified cycle") was not under test.
Replace it with a focused unit test that calls `store_cycle_if_fully_verified` directly and covers both short-circuit branches: (a) the cycle is already fully `Verified`, (b) at least one input entry is not `Verified`. The test asserts the gate returns `Ok(None)` and does not mutate `rotated_quorums_per_cycle` in either case.
Net change: -115/+44 lines, 1 of 2 unused test imports also removed.
Addresses CodeRabbit review comment on PR dashpay#736
dashpay#736 (comment)
…_cycle` with direct gate test
The old integration-style test claimed to exercise the storage gate's anti-clobber behavior, but actually never reached the gate. The synthetic fake quorums in `last_commitment_per_index` had a single-element `signers` bitvector, so `validate_structure` rejected them with `InsufficientSigners` and `feed_qr_info` returned `Err` before `store_cycle_if_fully_verified` was ever called. The post-call assertion that `rotated_quorums_per_cycle[K]` was unchanged passed for a trivial reason: nothing wrote to it. The named invariant ("does not downgrade already-verified cycle") was not under test.
Replace it with a focused unit test that calls `store_cycle_if_fully_verified` directly and covers both short-circuit branches: (a) the cycle is already fully `Verified`, (b) at least one input entry is not `Verified`. The test asserts the gate returns `Ok(None)` and does not mutate `rotated_quorums_per_cycle` in either case.
Net change: -115/+44 lines, 1 of 2 unused test imports also removed.
Addresses CodeRabbit review comment on PR dashpay#736
dashpay#736 (comment)
447a6e5 to
c9d5141
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@dash/src/sml/masternode_list_engine/mod.rs`:
- Around line 82-86: The field doc for fully_verified_count incorrectly states
it is "Always 0 when verify_rotated_quorums == false"; update the comment to
reflect that cached entries can contribute to the count: explain that when
verify_rotated_quorums is false the code still computes fully_verified_count
from qualified_last_commitment_per_index which may include prior Verified
statuses from known_qualified_quorum_entry (cache hits), so fully_verified_count
can be > 0 in that case. Mention the involved symbols fully_verified_count,
verify_rotated_quorums, qualified_last_commitment_per_index, and
known_qualified_quorum_entry so readers can locate the logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2e1b215a-f739-4515-b03f-10721d053158
📒 Files selected for processing (1)
dash/src/sml/masternode_list_engine/mod.rs
QRInfos that span a block range with no successful rotating DKG arrive with per-diff CL sigs missing. Previously this hard-errored with `RequiredRotatedChainLockSigNotPresent`. Now affected entries land as `Skipped(MissingRotationChainLockSigs)` and the cycle is left unstored, deferring IS lock verification for that cycle until a later QRInfo carries complete sigs. Tighten the `rotated_quorums_per_cycle` insertion gate: store a cycle only when every entry is `Verified`, and never overwrite an already-fully-verified cycle (a thin follow-up QRInfo must not downgrade prior trust). Add a previous-cycle storage path (`validate_and_store_previous_cycle_quorums`) that uses the 4 historical sigs `[h-3c, h-2c, h-c, h]` to validate and store the rotated quorums living on `masternode_lists[h]` under their cycle boundary hash. This enables IS lock verification for the previous cycle on a fresh sync, where `lastCommitmentPerIndex` only covers the current cycle. Change `feed_qr_info` to return `Result<Option<QRInfoFeedResult>>` so callers can observe rotated quorum counts, fully-verified counts, and which cycle (if any) was stored. New tests cover the soft-skip storage gate, corrupt aggregate-signature rejection, and the anti-clobber guarantee. The existing `validate_from_qr_info_and_mn_list_diffs` test is extended to assert both cycles are stored and to re-validate every stored quorum.
Replace the provably safe but rule-violating `expect()` with `?` propagation on a destructured array. Behavior is unchanged: the missing-slot debug log still fires before the unwrap path, and `?` on already-checked `Some` values short-circuits cleanly. Addresses CodeRabbit review comment on PR #736 #736 (comment)
`try_load_previous_cycle_entries` derived its `cycle_hash` via `entries.first()` from a `BTreeMap<QuorumHash, _>`, so the key was the hash-order minimum rather than the cycle-boundary quorum. Dash Core defines `cycleHash` as the block hash at the cycle-base height, which equals the `quorum_hash` of the index-0 quorum, and IS lock verification looks up `rotated_quorums_per_cycle` with that key. Mis-keyed storage caused silent IS-lock verification failures for the previous cycle. Find the index-0 entry's `quorum_hash` instead, and short-circuit the already-stored check before allocating the `entries` vector. Addresses CodeRabbit review comment on PR #736 #736 (comment)
…fied
The `else if` branch in `feed_qr_info` skipped verification but still wrote into `rotated_quorums_per_cycle` whenever `already_fully_verified` was false. That contradicted the documented invariant ("a cycle is only stored when every entry is `Verified`") and could poison IS-lock BLS verification with unverified `quorum_public_key` values.
Mirror the verified-only gate from the `verify_rotated_quorums == true` path: only store when every entry is `Verified` (which can still hold via cache hits from `known_qualified_quorum_entry`).
Addresses CodeRabbit review comment on PR #736
#736 (comment)
Gate the per-cycle rotation-sig bindings, the `RotationChainLockSignatureSlot` enum, its `Display` impl, and the `core::fmt` import behind `#[cfg(feature = "quorum_validation")]` so default-feature builds stay warning-free. Addresses CodeRabbit review comment on PR #736 #736 (comment)
…_signatures` test on `quorum_validation` The test calls `load_qrinfo_2240504_fixture` which is gated on `quorum_validation`. Without the matching gate the test fails to compile when the feature is off. Addresses CodeRabbit review comment on PR #736 #736 (comment)
…_info` Both the `verify_rotated_quorums == true` and `== false` branches in `feed_qr_info` had identical logic for gating storage of a rotation cycle into `rotated_quorums_per_cycle`: check that every entry is `Verified`, refuse to overwrite a cycle that is already fully verified, then build the cycle map and record the stored height. Move that logic into a single helper so the two call sites stay aligned.
…y_verified` `store_cycle_if_fully_verified` and `try_load_previous_cycle_entries` both decide whether to skip work based on what is already in `rotated_quorums_per_cycle`, but the predicates had drifted. The storage gate required a non-empty cycle with every entry `Verified`, while the previous-cycle loader only checked `contains_key`. The two now share a single `is_cycle_fully_verified` helper so the previous-cycle path also refuses to short-circuit on a partial entry.
…ment The `None`-return condition is now \"already fully verified\", not bare \"already stored\", to match the underlying `is_cycle_fully_verified` predicate.
…_cycle` with direct gate test
The old integration-style test claimed to exercise the storage gate's anti-clobber behavior, but actually never reached the gate. The synthetic fake quorums in `last_commitment_per_index` had a single-element `signers` bitvector, so `validate_structure` rejected them with `InsufficientSigners` and `feed_qr_info` returned `Err` before `store_cycle_if_fully_verified` was ever called. The post-call assertion that `rotated_quorums_per_cycle[K]` was unchanged passed for a trivial reason: nothing wrote to it. The named invariant ("does not downgrade already-verified cycle") was not under test.
Replace it with a focused unit test that calls `store_cycle_if_fully_verified` directly and covers both short-circuit branches: (a) the cycle is already fully `Verified`, (b) at least one input entry is not `Verified`. The test asserts the gate returns `Ok(None)` and does not mutate `rotated_quorums_per_cycle` in either case.
Net change: -115/+44 lines, 1 of 2 unused test imports also removed.
Addresses CodeRabbit review comment on PR #736
#736 (comment)
…rums == false` path The comment claimed `fully_verified_count` is always `0` when `verify_rotated_quorums == false`, but the else branch at the `feed_qr_info` storage step counts every entry of `qualified_last_commitment_per_index` whose `.verified` is `Verified`. Cache hits returned by `known_qualified_quorum_entry` retain their prior `Verified` status (the gate only stores fully-Verified cycles), so the count can be non-zero even with verification disabled. Update the doc to describe the actual behavior. Addresses CodeRabbit review comment on PR #736 #736 (comment)
c9d5141 to
25c3347
Compare
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
QuantumExplorer
left a comment
There was a problem hiding this comment.
Spent some time looking at this one, seems good, but it would be very hard for me to know if it is correct without spending a lot more time on it.
QRInfos that span a block range with no successful rotating DKG arrive with per-diff CL sigs missing. Previously this hard-errored with
RequiredRotatedChainLockSigNotPresent. Now affected entries land asSkipped(MissingRotationChainLockSigs)and the cycle is left unstored, deferring IS lock verification for that cycle until a later QRInfo carries complete sigs.Tighten the
rotated_quorums_per_cycleinsertion gate: store a cycle only when every entry isVerified, and never overwrite an already-fully-verified cycle (a thin follow-up QRInfo must not downgrade prior trust).Add a previous-cycle storage path (
validate_and_store_previous_cycle_quorums) that uses the 4 historical sigs[h-3c, h-2c, h-c, h]to validate and store the rotated quorums living onmasternode_lists[h]under their cycle boundary hash. This enables IS lock verification for the previous cycle on a fresh sync, wherelastCommitmentPerIndexonly covers the current cycle.Change
feed_qr_infoto returnResult<Option<QRInfoFeedResult>>so callers can observe rotated quorum counts, fully-verified counts, and which cycle (if any) was stored.New tests cover the soft-skip storage gate, corrupt aggregate-signature rejection, and the anti-clobber guarantee. The existing
validate_from_qr_info_and_mn_list_diffstest is extended to assert both cycles are stored and to re-validate every stored quorum.Summary by CodeRabbit
New Features
Tests