Repository navigation
fix(dash-spv): take the non-rotating member-selection ChainLock from the work block's coinbase - #1284
Conversation
|
Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Candidate extraction and signature checks dash/src/blockdata/transaction/special_transaction/coinbase.rs, crypto/src/bls.rs, crypto/Cargo.toml |
CoinbasePayload::chain_lock_candidate checks payload version, height, best ChainLock height, and signature presence and value. BlsSignature::is_valid parses the signature according to its scheme. Tests cover accepted and rejected candidates and signatures. |
Engine ChainLock records and lifecycle dash-spv/src/sml_engine.rs, dash-spv/src/sml_engine/helpers.rs |
The engine records each list’s block hash and optional valid coinbase ChainLock. Rollback restores these records, while pruning and truncation remove records with their lists. |
Work-block requests and quorum validation dash-spv/src/sml_engine.rs, dash-spv/src/sml_engine/validation.rs |
A held work-block list is requested from its own block when no matching ChainLock record exists. Quorum validation uses the work block’s coinbase ChainLock. Tests cover request behavior, validation, restoration, and candidate recording. |
Estimated code review effort: 3 (Moderate) | ~30 minutes
Change: Bug fix
Sequence Diagram(s)
sequenceDiagram
participant MasternodeListEngine
participant QRInfo
participant CoinbasePayload
participant QuorumValidator
MasternodeListEngine->>MasternodeListEngine: Check for a ChainLock record matching the work-block hash
MasternodeListEngine->>QRInfo: Request the work-block list when no matching record exists
QRInfo->>MasternodeListEngine: Provide the work-block diff
MasternodeListEngine->>CoinbasePayload: Read the candidate at the diff height
MasternodeListEngine->>MasternodeListEngine: Store the block hash and candidate with the list
QuorumValidator->>MasternodeListEngine: Read the work-block ChainLock data
Possibly related issues
dashpay/rust-dashcore#1262— The PR uses the work block’s coinbase ChainLock rather thanquorumsCLSigsfor non-rotating quorum validation.dashpay/rust-dashcore#1233— The PR requests the work-block list when its ChainLock record is missing. It does not add attempt limits or request withdrawal.
Suggested reviewers: kwvg
Merge Risk: 
Minimal · up to 310a9
-
No actionable issue is established from the available evidence. The change appears mergeable after normal checks.
Pre-merge checks: 
5
-
Full details: Title check
-
Explanation
The title names the main change: non-rotating quorum member selection now takes its ChainLock from the work block's coinbase. The changeset matches this. The coinbase ChainLock candidate, the validate_quorum change, and the ChainLock record in the engine all serve that goal. The title is in the conventional commit format and is specific. It is slightly long at 94 characters, but it is still a single clear sentence.
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 18 functions across 5 files. (1 skipped: 1 unsupported.)
-
Finishing Touches
-
Generate docstrings
-
- Commit to this branch
- Create a new PR
Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
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.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #1284 +/- ##
==========================================
+ Coverage 82.00% 82.06% +0.05%
==========================================
Files 290 290
Lines 75955 76201 +246
==========================================
+ Hits 62287 62532 +245
- Misses 13668 13669 +1
|
|
Bots are done — your move: post |
|
/self-reviewed |
|
Ready for review — files with no dedicated owner: QuantumExplorer or ZocoLini or xdustinface · |
ZocoLini
left a comment
There was a problem hiding this comment.
The fix itself looks right. I checked the cited Dash Core code (GetNonNullCoinbaseChainlock, GetHashModifierFromWorkBlock, BuildSimplifiedMNListDiff) and every masternode_lists mutation path, and found no correctness bug. The problem is scope: 984 added lines, about 170 of them production code, for a change that needs far less. Please limit this PR to the code that resolves the non-rotating part of #1262, plus tests for its new paths, using test_case.
Remove from production code
- The
retry_given_up_work_listschange. It is unrelated to #1262 and nothing in this fix depends on it. That means themanager.rschanges and their test,MockHeaderStorage::unreadableand the rest of theheader_storage.rschanges,pub(crate)onscan_missing_work_block_lists, and the..MockHeaderStorage::default()line. If it's worth having, it should be its own PR. LLMQModifierType::from_work_blockand its test. The existing constructor already does this:new_quorum_modifier_type(llmq_type, hash, height, chain_lock.unwrap_or(BLSSignature::from([0; 96])), network). The v20 check gives the same result, because a version 3 coinbase already implies a height at or above activation.- The
quorumsCLSigsdebug log invalidate_quorum. It doesn't change behaviour. - The
best_cl_height/best_cl_signaturefield docs. They are unrelated to the fix. - The
u32inchain_lock_candidate's return value. The only caller discards it, so returnOption<BLSSignature>.
I applied 1 to 3 locally on 9b384c7. The sml_engine and sync::masternodes tests in dash-spv (99) and the quorum_modifier_type and coinbase tests in dashcore (18) all pass, including the PR's own remaining tests.
Reduce the tests
sync_manager.rs has 168 added lines and no production change, so it adds no new path to cover.
Remove:
manager_asking_for_a_held_work_block_list,a_work_block_list_asked_for_from_its_own_block_is_appliedandan_unanswered_work_block_list_from_its_own_block_is_given_up_and_retried_once. These exercise the #1258 path, and the request itself is already asserted at engine level.the_snapshot_leaves_the_coinbase_chain_locks_out. It tests something that wasn't added.a_diff_applied_twice_records_the_same_chain_lock.
Merge with test_case:
a_restart_asks_for_the_work_block_coinbase_again,without_the_coinbase_chain_lock_a_quorum_is_skippedanda_persisted_invalid_quorum_verifies_once_its_coinbase_is_readinto one test, with the persisted status and served signature as cases: skipped first, then verified after the self-diff.a_non_rotating_quorum_verifies_whatever_quorums_cl_sigs_servesandthe_work_block_coinbase_chooses_the_membersinto one test, with the diff edit and expected status as cases.chain_lock_candidate_follows_get_non_null_coinbase_chainlock: one case per clause instead of the array, without near-duplicates.is_valid_follows_dash_core_is_valid: one case per input.
Keep a_self_diff_records_its_coinbase_chain_lock_only_once_proven and coinbase_chain_locks_go_with_their_lists.
That should leave roughly 440 added lines, around 300 of them tests.
Dash Core selects a non-rotating quorum's members with the ChainLock in its work block's coinbase (`GetHashModifierFromWorkBlock`, `src/llmq/utils.cpp:87-97`), read by `GetNonNullCoinbaseChainlock` (`src/evo/cbtx.cpp:200-211`). Add the checks it makes: - `BlsSignature::is_valid`: Dash Core's `CBLSSignature::IsValid()`, false for all-zero bytes, bytes the parser refuses and the identity element (`src/bls/bls.h:90-122`). - `CoinbasePayload::chain_lock_candidate`: the payload's `bestCLSignature` when its version, height, non-zero signature and height difference pass Dash Core's checks, leaving `IsValid()` to the caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the work block's coinbase Dash Core selects a non-rotating quorum's members with a modifier built from the ChainLock in its work block's coinbase, or from the work block's hash when the coinbase carries none (`GetHashModifierFromWorkBlock`, `src/llmq/utils.cpp:87-97`). dash-spv used the signature a diff serves in `quorumsCLSigs` instead, which no coinbase commits to. A different or all-zero value there selected other members, and a quorum whose commitment is valid on chain was marked `Invalid`. - The engine records each list's coinbase ChainLock, with the list's block hash, when it takes a diff's list: after the coinbase proof and roots pass. The record is pruned, truncated and rolled back with the lists, and is not part of the snapshot, whose format is unchanged. - `validate_quorum` passes that record to `new_quorum_modifier_type`. Without one the quorum is `Skipped(MissingChainLock)`. - A work-block list held without its record, as after a restart, is asked for as a diff from its own block, which Dash Core serves (`src/evo/smldiff.cpp:181-183`), through the existing work-block list queue, deadline and retry. Part of #1262. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9b384c7 to
310a9d3
Compare
|
Thanks for checking it against Core. I've applied all of it, and pushed 310a9d3 (rebased onto current Production code
Tests
Tests come to about 340 lines rather than ~300. Most of the difference is the two tests you asked to keep. 🤖 Posted autonomously by Claude on behalf of pasta. |
|
Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post |
|
Bots are done — your move: post |
|
/self-reviewed 310a9d3 |
dash-spv selects a non-rotating quorum's members using the ChainLock signature that a diff serves in
quorumsCLSigs. No coinbase commits to that value. Since #1200 the commitments themselves are proven against their block, butquorumsCLSigsis not. If a diff serves a different signature there, or an all-zero one in place of a real one, dash-spv selects other members. A commitment that is valid on chain then fails its signature check and is storedInvalid, and ChainLocks from that quorum are refused.Dash Core takes the signature from the work block's own coinbase (cites are to Dash Core
developat 15db47875):GetHashModifierFromWorkBlockbuilds the modifier from(llmqType, workBlock.nHeight, bestCLSignature)when the work block's coinbase carries a ChainLock, and from(llmqType, workBlock.hash)otherwise (src/llmq/utils.cpp:87-97).GetNonNullCoinbaseChainlockcounts the coinbase ChainLock only if four checks pass: the payload is version 3 or later, its height is the block's,bestCLSignature.IsValid()holds, andbestCLHeightDiffis below the height (src/evo/cbtx.cpp:200-211).IsValid()is false for all-zero bytes, for bytes the parser refuses, and for the identity element (src/bls/bls.h:90-122).quorumsCLSigsrepeats that value for each new quorum, with an all-zero signature when there is none (src/evo/smldiff.cpp:87-102).This PR does the same for non-rotating quorums. Part of #1262; rotated quarters follow in a separate PR.
What changes
BlsSignature::is_valid(Core'sCBLSSignature::IsValid(), on the existing parser) andCoinbasePayload::chain_lock_candidate. The latter returns the payload'sbestCLSignaturewhen the version, height, zero-signature and height-difference checks ofGetNonNullCoinbaseChainlockpass.validate_quorum: passes the work block's record to the existingLLMQModifierType::new_quorum_modifier_type, with the zero signature when the coinbase carries none. Without a record for that block, the quorum isSkipped(MissingChainLock). The constructor's v20 height check gives the same result as Core: a version 3 coinbase exists only once v20 is active at its block (src/evo/cbtx.cpp:40-43).src/evo/smldiff.cpp:181-183), and the reply carries just the coinbase and its merkle proof. These requests use the existing work-block list path from fix(dash-spv)!: keep masternode sync live on bad or missing diffs #1258: they are queued after the QRInfo and with tip updates, given up atMNLISTDIFF_DEADLINE, and retried once. Dash Core replies withmnlistdiffonly when it can build the diff (src/net_processing.cpp:5615-5618), so fix(dash-spv)!: keep masternode sync live on bad or missing diffs #1258's deadline and single retry bound these requests like any other work-block list.Non-rotating quorums restored as
VerifiedstayVerified. A persistedInvalidorSkippedquorum is validated again. While its work-block list is held without a coinbase record, it isSkipped(MissingChainLock)until the diff from that block arrives, and then it is validated under the coinbase ChainLock. Without the list it isSkipped(MissedList), as before.#1202 adds a coinbase field at the same points in
apply_diff_to_base,ListsAndCycles/QRInfoRollbackandtruncate_above. Whichever of the two lands second rebases onto the other.Changelog
quorumsCLSigsvalue of a diff.Tests
non_rotating_members_follow_the_work_block_coinbase_not_quorums_cl_sigs(three cases on the 2240504 fixture). The first two are the issue's: aLlmqtype100_67quorum'squorumsCLSigsvalue is replaced, in every diff that brings it, with the zero signature or with another block's ChainLock, and the quorum must still verify. Ondevboth fail withInvalid(AllCommitmentAggregatedSignatureNotValid). In the third case, a different real ChainLock in the work block's coinbase makes the quorumInvalid.a_restored_quorum_waits_for_its_work_block_coinbase(two cases: persistedUnknownserved the right signature, persistedInvalidserved a wrong one). After a snapshot round trip the quorum isSkipped(MissingChainLock)and its held work-block list is requested from its own block. Once that diff is applied, the quorum isVerified.a_self_diff_records_its_coinbase_chain_lock_only_once_proven(two cases, on the 2518986 fixture with coinbase checks on): after a restart, a diff from the newest list's block to itself is refused if its coinbase proof or roots fail, and records nothing. The untampered one applies and records the coinbase ChainLock.coinbase_chain_locks_go_with_their_lists, plus the existing refused-QRInfo rollback tests and work-block request tests, now asserting the record.is_valid_follows_dash_core_is_valid(one case per input) andchain_lock_candidate_follows_get_non_null_coinbase_chainlock(one case percbtx.cpp:206-207clause, plus the accepted case).cargo test -p dash-spvwith the dashd regtest integration tests.🤖 Generated with Claude Code
PR Hygiene ·
310a9d3crypto/Cargo.toml,crypto/src/bls.rs,dash/src/blockdata/transaction/special_transaction/coinbase.rs) — QuantumExplorer or ZocoLini or xdustinfacedash-spv(dash-spv/src/sml_engine.rs,dash-spv/src/sml_engine/helpers.rs,dash-spv/src/sml_engine/validation.rs) — QuantumExplorer or ZocoLini or xdustinfaceWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.Summary by CodeRabbit