Repository navigation
fix: guard cached governance object flags with the object lock - #7790
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (1)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review. WalkthroughGovernance-object cache reads and vote counting now use the object mutex. Deletion state updates and deletion eligibility checks use synchronized values. Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The reviewed synchronization and deletion paths show no actionable new failure. The change is mergeable after normal checks. Architecture SummaryArchitecture risk: 🔵 Low · up to The change affects 1 system. Changed systems: Architecture concerns Review detailsSystems and components
Before / after behavior
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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 |
|
✅ Final review complete — no blockers (commit 96767a4) · triage: normal |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Verified the supplied reviews against exact head c157ef0; no actionable in-scope defects were found. The atomic accesses and explicit copy-constructor loads remove races on the five flags, while single-store recomputation preserves threshold logic, sticky deletion, dirty-cache ordering, and serialization without exposing temporary reset values. This was static verification only: the supplied CI snapshot shows successful formatting and preliminary checks, queued ARM64 container jobs, and no project build/test results yet; the author's reported test results were not independently rerun.
Review provenance
Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
lowbygpt-6.1-sol(effort low) — The diff is a small, contained concurrency fix that makes five cached flags atomic and removes transient reset values without changing vote thresholds or introducing intricate critical-surface logic. - Phase 1 reviewers:
glm-5.3-flash— general (completed, effort high); agentphase1-reviewer - Phase 1 model:
glm-5.3-flash— zai quota: 5h 99% left, weekly 69% left; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort medium); agentphase2-reviewer
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Dead cache members fCachedLocalValidity and strLocalValidityError are written but never read — Routine pre-existing cleanup outside this PR's scope. The members are declared in src/governance/object.h:155-156, copied in object.cpp:360-361, and updated in UpdateLocalValidity() at object.cpp:706; GetStateJson() recomputes validity rather than consuming them. Their removal is not required for this atomic-flag fix and does not meet the bar for an exceptional high-value follow-up.
- Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.
Make CountMatchingVotes() a private helper that runs with the object
lock already held, and take the lock in the public Get{Yes,No,Abstain}Count()
wrappers instead. No behavior change; this lets a caller count several
signals inside one critical section.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CGovernanceObjectVoteFile already has a copy constructor that rebuilds mapVoteIndex against its own list, but no copy assignment (the implicit one cannot be instantiated because CGovernanceVote is not assignable). Add one with the same rebuild semantics so an owner can copy the vote file after construction, e.g. while holding a lock on the source. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gobject get could report fCachedFunding/fCachedValid/fCachedEndorsed values that never reflected the votes, and every read of these plain bools raced with their writers: - GetStateJson() and the copy constructor (used by gobject list/diff and the Qt proposal list) read the flags with no lock on the object held. - UpdateSentinelVariables() is reached from CheckAndRemove() (cs_main + cs_store) and from IsSuperblockTriggered() (cs_sb only, including via the `masternode winners` RPC without cs_main), so no outer lock serialized the writers. - UpdateSentinelVariables() first reset the flags to their defaults and only then counted votes, so a concurrent reader could see the defaults. Mark fCachedFunding, fCachedValid, fCachedDelete, fCachedEndorsed and fDirtyCache GUARDED_BY(cs), matching fExpired and nDeletionTime, and make the getters take the lock. UpdateSentinelVariables() now counts the votes and writes every flag inside a single critical section, so readers see either the previous or the new result, concurrent recomputes cannot interleave flag by flag, and clearing fDirtyCache is ordered with the vote counts under the same lock that vote writers already hold. Setting fCachedDelete and nDeletionTime now happens together through MarkForDeletion(), so fCachedDelete implies a non-zero deletion time. GetStateJson() reads all four flags under one lock so `gobject get` returns a consistent snapshot, and the copy constructor copies all cs-guarded state while holding the source object's lock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
c157ef0 to
6010327
Compare
CheckAndRemove() read GetDeletionTime() and then IsSetCachedDelete() in separate calls. IsSuperblockTriggered() can mark a trigger for deletion concurrently (it runs under cs_sb only, e.g. from `masternode winners`), so CheckAndRemove() could see a zero deletion time followed by a set delete flag, compute a time since deletion of "now - 0", and erase the object immediately instead of after GOVERNANCE_DELETION_DELAY. fCachedDelete is never cleared, and the previous commit makes it change together with nDeletionTime under the object lock, so reading the flag first guarantees that a set delete flag is paired with its deletion time. fExpired is unaffected: it is only set by SuperblockManager::Clean(), which runs from CheckAndRemove() itself and stamps the deletion time before this loop. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
6010327 to
96767a4
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified the complete four-commit PR diff at 96767a4, including callers, flag writers, serialization, copy semantics, and lock ordering; no actionable in-scope defects were found. The object lock protects cached-flag recomputation and snapshots, vote-file assignment rebuilds destination-owned iterators, and flag-before-timestamp reads prevent premature deletion. Verification was static only: the supplied CI snapshot shows successful completed builds, lint, and nowallet tests, while ASan, TSan, SQLite, standard Linux tests, and the multiprocess build remain pending.
🔴 0 blocking | 🟡 0 suggestion(s) | 💬 0 nitpick(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The change coordinates governance cache locking, copy semantics and deletion ordering across several files, but the diff does not introduce a large or intricate change to a qualifying critical surface. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort high); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify the current code and confirm that no unresolved issues remain.
No unresolved findings remain from the prior review on this head.
…lang-analyzer null dereference) 92365cc fix(qt): reserve the coin vector in the coin control test helper (Konstantin Akimov) 9c6ccd0 fix(qt): clear ItemIsEnabled through QFlags::setFlag in coin control test (Konstantin Akimov) 7daa67d fix(test): assert the chain tip before dereferencing it in reward reallocation tests (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented `develop` CI is red since the merge of #7260 (aec4994). Both `linux64_asan-build` and `linux64_multiprocess-build` pass on the merge of #7790 (2ca083a) and on the head of #7584 (fed2f49), and both are red on the head of #7260 (3034e98) and on its merge commit. #7260 was merged with those two jobs failing. 1. `linux64_asan` fails in `test_dash-qt`: `CoinControlTreeWidgetTests::disabledCoinsSkipped()` triggers an UBSan implicit-integer-sign-change inside `QFlags::operator&`. The test, added by #7260 in 3034e98, clears a flag with `flags() & ~Qt::ItemIsEnabled`. `~Qt::ItemIsEnabled` is evaluated as a plain `int` (-33) and QFlags converts it to its unsigned storage type. 2. `linux64_multiprocess` reports a clang static analyzer diagnostic in `test/block_reward_reallocation_tests.cpp:209`: `tip->nBits` dereferences a pointer the analyzer considers possibly null, because `GetMnRewardEraAfter()` (via `DeploymentActiveAfter()`) accepts a null `pindexPrev`. The pattern dates back to #7339 (7f2058e) and the file was last touched by #7536 in August, so this diagnostic is not caused by #7260's diff. The analyzer runs in that job because the tidy step operates on the `make distdir` copy of the tree, which does not include `src/.clang-tidy`, so clang-tidy falls back to its default `clang-analyzer-*` checks. I could not read the job log (needs credentials) to confirm that this diagnostic alone fails the step, so this part is fixed on the evidence of the pasted output. ## What was done? - `src/qt/test/coincontroltreewidgettests.cpp`: clear the flag with `flags().setFlag(Qt::ItemIsEnabled, false)`, which stays inside the `QFlags` type. - `src/test/block_reward_reallocation_tests.cpp`: wrap every chain-tip lookup that feeds `GetMnRewardEraAfter()` in `Assert()` and include `util/check.h`. The chain is never empty at those points; the assertion documents it for the analyzer and the reader. This is test code, so a fatal check is acceptable. ## How Has This Been Tested? see CI ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone Top commit has no ACKs. Tree-SHA512: cd7d524e35ec6039f5b72ae05cfd498c83c00d842a617bf412de87a5759eb7fbd74f5b96698e9a3aab27174d8ac8942f5624bbe49a1028c4d9d0fbc3cfb3dd33
…t lock 96767a4 fix: read deletion flags before deletion time in CheckAndRemove (pasta) ebb27a3 fix: guard cached governance object flags with the object lock (pasta) 94df4f1 refactor: give CGovernanceObjectVoteFile a copy assignment operator (pasta) 059bf00 refactor: require cs in CGovernanceObject::CountMatchingVotes (pasta) Pull request description: ## Issue being fixed or feature implemented `gobject get` can report wrong values for `fCachedFunding`, `fCachedValid` and `fCachedEndorsed` while the node is recomputing them. For example, a proposal that has reached the funding threshold can come back as `fCachedFunding: false`, and a proposal the network voted invalid can come back as `fCachedValid: true`. The reads are also a data race on plain `bool` members. That is undefined behavior, and ThreadSanitizer reports it (see below). There are two causes: 1. The cached flags (`fCachedFunding`, `fCachedValid`, `fCachedDelete`, `fCachedEndorsed`, `fDirtyCache`) have no lock. Readers take no lock: - `CGovernanceObject::GetStateJson()`, used by `gobject get`. - The copy constructor, used by `gobject list`, `gobject diff` and the Qt proposal list through `GetAllNewerThan()`. No single outer lock covers the writers either: - `CGovernanceManager::CheckAndRemove()` holds `cs_main` + `cs_store`. - `SuperblockManager::IsSuperblockTriggered()` holds only `cs_sb`. It is called from block-template creation and block validation, and also from the `masternode winners` RPC without `cs_main`. - The vote path writes `fDirtyCache` while holding the object's own `cs`. 2. `CGovernanceObject::UpdateSentinelVariables()` first resets `fCachedFunding`/`fCachedEndorsed` to `false` and `fCachedValid` to `true`. It then counts votes, and only after that writes the real values. A reader that runs during this window sees the defaults instead of the old or the new result. There is also a related ordering bug. `fCachedDelete` was set before the lock that sets `nDeletionTime`. `CheckAndRemove()` reads `GetDeletionTime()` and then `IsSetCachedDelete()` in separate calls. If `IsSuperblockTriggered()` marks a trigger for deletion in between, `CheckAndRemove()` sees a set delete flag with a zero deletion time, computes `now - 0`, and erases the object immediately instead of after `GOVERNANCE_DELETION_DELAY`. Block validation itself is not affected. Its `IsSuperblockTriggered()` call holds `cs_main` and `cs_sb`, which already serializes it against the other writers. The visible impact is on RPC/GUI output, plus undefined behavior under the C++ memory model. ### Reproduction A throwaway test case (not part of this PR) in `governance_vote_processing_tests` reuses its single-masternode fixture. The masternode votes YES on funding and NO on validity, so with `nGovernanceMinQuorum=1` the correct state never changes: `fCachedFunding=true`, `fCachedValid=false`. One thread calls `UpdateSentinelVariables()` in a loop, as `CheckAndRemove()` / `IsSuperblockTriggered()` do. The main thread calls `GetStateJson()` for 3 seconds, as `gobject get` does, and counts reads that disagree with the correct state. <details> <summary>Test case</summary> ```cpp BOOST_AUTO_TEST_CASE(proof_cached_flags_read_during_recompute) { auto& govman = *m_node.govman; const CGovernanceObject proposal{MakeProposal(uint256::ONE)}; const uint256 parent_hash{proposal.GetHash()}; govman.AddGovernanceObjectForTesting(proposal); auto obj = govman.FindGovernanceObject(parent_hash); BOOST_REQUIRE(obj != nullptr); CGovernanceVote funding{MakeVote(parent_hash, VOTE_SIGNAL_FUNDING, VOTE_OUTCOME_YES)}; SignWithVotingKey(funding, mn_voting_key); CGovernanceVote invalid{MakeVote(parent_hash, VOTE_SIGNAL_VALID, VOTE_OUTCOME_NO)}; SignWithOperatorKey(invalid, mn_operator_key); CGovernanceException exception; uint256 hash_to_request; BOOST_REQUIRE(govman.ProcessVote(funding, exception, hash_to_request)); BOOST_REQUIRE(govman.ProcessVote(invalid, exception, hash_to_request)); const auto mn_list{tip_mn_list()}; obj->UpdateSentinelVariables(mn_list); BOOST_REQUIRE(obj->IsSetCachedFunding()); BOOST_REQUIRE(!obj->IsSetCachedValid()); std::atomic<bool> stop{false}; std::thread writer([&] { while (!stop) obj->UpdateSentinelVariables(mn_list); }); int reads{0}, wrong_funding{0}, wrong_valid{0}; const auto deadline{std::chrono::steady_clock::now() + 3s}; while (std::chrono::steady_clock::now() < deadline) { const UniValue state{obj->GetStateJson(*m_node.chainman, mn_list, /*local_valid_key=*/"fLocalValidity")}; ++reads; if (!state["fCachedFunding"].get_bool()) ++wrong_funding; if (state["fCachedValid"].get_bool()) ++wrong_valid; } stop = true; writer.join(); BOOST_TEST_MESSAGE(strprintf("GetStateJson reads=%d fCachedFunding=false seen=%d fCachedValid=true seen=%d", reads, wrong_funding, wrong_valid)); BOOST_CHECK_EQUAL(wrong_funding, 0); BOOST_CHECK_EQUAL(wrong_valid, 0); } ``` </details> Results against the unfixed tree (c14104b): ``` # regular debug build GetStateJson reads=198415 fCachedFunding=false seen=76428 fCachedValid=true seen=193173 # --with-sanitizers=thread GetStateJson reads=29726 fCachedFunding=false seen=0 fCachedValid=true seen=29726 WARNING: ThreadSanitizer: data race Write of size 1 ... by thread T11: #0 CGovernanceObject::UpdateSentinelVariables(CDeterministicMNList const&) object.cpp:996 Previous read of size 1 ... by main thread: #0 CGovernanceObject::IsSetCachedValid() const object.h:196 #1 CGovernanceObject::GetStateJson(...) const object.cpp:617 ``` ## What was done? The flags are now protected by the object's existing `cs` mutex, the same lock that already guards `fExpired`, `nDeletionTime` and the vote maps. Clang's thread-safety analysis enforces it. 1. **`refactor: require cs in CGovernanceObject::CountMatchingVotes`.** `CountMatchingVotes()` becomes a private helper with `EXCLUSIVE_LOCKS_REQUIRED(cs)`. The public `Get{Yes,No,Abstain}Count()` wrappers take the lock. No behavior change. This lets one critical section count several signals. 2. **`refactor: give CGovernanceObjectVoteFile a copy assignment operator`.** `CGovernanceObjectVoteFile` has a copy constructor that rebuilds `mapVoteIndex` (list iterators) against its own list, but no copy assignment. The implicit one can't be instantiated because `CGovernanceVote` is not assignable. This commit adds one with the same rebuild semantics, so the copy constructor in commit 3 can copy the vote file while holding the source's lock. A unit test checks that a copy-assigned file is still usable after its source is destroyed. 3. **`fix: guard cached governance object flags with the object lock`.** - The five flags are `GUARDED_BY(cs)`, and the `IsSetCached*()`/`IsSetDirtyCache()` getters take the lock, like `IsSetExpired()`. - `UpdateSentinelVariables()` counts the votes and writes every flag inside one `LOCK(cs)`. Each flag is assigned its computed value directly, with no reset to a default first. This means: - readers see either the previous result or the new one; - two recomputes running at the same time can't interleave flag by flag; - `fDirtyCache = false` is ordered with the vote counts under the same lock that vote writers already hold, so a vote arriving during a recompute still marks the object dirty. - `PrepareDeletion()` and `UpdateSentinelVariables()` share a private `MarkForDeletion()` helper that sets `fCachedDelete` and `nDeletionTime` together, so `fCachedDelete` implies a non-zero deletion time. - `GetStateJson()` reads all four flags under one lock, so `gobject get` returns a consistent snapshot. - The copy constructor copies all `cs`-guarded state while holding `other.cs`. - The computed values are the same as before. 4. **`fix: read deletion flags before deletion time in CheckAndRemove`.** `CheckAndRemove()` now reads `IsSetCachedDelete()`/`IsSetExpired()` before `GetDeletionTime()`. - `fCachedDelete` is never cleared, and it now changes together with `nDeletionTime`, so a set delete flag is always paired with its deletion time. - `fExpired` is only set by `SuperblockManager::Clean()`. That runs from `CheckAndRemove()` itself and stamps the deletion time before this loop. ### Locking notes - No code holds a governance object's `cs` while calling the getters, `UpdateSentinelVariables()`, `PrepareDeletion()`, `GetStateJson()`, or the copy constructor. The external `cs` holders only touch the vote file. - `UpdateSentinelVariables()` and `GetStateJson()` take no other lock while holding `cs`. - The `cs_main`/`cs_store`/`cs_sb` → `obj.cs` edges already existed through `GetYesCount()` and the old `UpdateSentinelVariables()`. No new lock-order edges are introduced. ### Intentionally left unchanged `fCachedLocalValidity`, `strLocalValidityError` and `fUnparsable` follow the same unguarded pattern. However, `GetStateJson()` recomputes local validity instead of reading the cache, and I found no concurrent reader of those fields, so they are out of scope here. ## How Has This Been Tested? macOS arm64, Apple clang. Built with depends using `--disable-hardening --enable-debug --enable-crash-hooks --enable-stacktraces --enable-suppress-external-warnings --without-gui --enable-tests --disable-bench --disable-fuzz-binary --enable-werror`. This enables `-Wthread-safety` as an error. - **Each commit builds on its own.** - **Thread-safety annotations are enforced.** As a mutation check, I removed the `LOCK(cs)` in `GetStateJson()`. The build then fails with `passing variable 'fCachedValid' by reference requires holding mutex 'cs' [-Werror,-Wthread-safety-reference]`. - **Reproduction above with this PR:** 0 wrong reads across three runs (148887, 155774 and 184427 reads). - **ThreadSanitizer build (`--with-sanitizers=thread`) with this PR.** The reproduction ran with a second reader added that also copy-constructs the object each iteration (the `gobject list` path). Result: no ThreadSanitizer warnings and 0 wrong reads. The same binary built from the unfixed tree reports the data race shown above. - **Regression test status.** The reproduction is not included as a regression test. It is a timing-dependent multithreaded test that runs for several seconds, and this race can't be checked deterministically in a unit test. The `CheckAndRemove()` ordering fix has the same limitation; I verified it by reviewing every writer of `fCachedDelete`, `fExpired` and `nDeletionTime`. - **New unit test:** `governance_vote_sync_tests/copy_assigned_vote_file_indexes_its_own_votes`. - `src/test/test_dash --run_test='governance*'`: `*** No errors detected`. - `test/functional/test_runner.py feature_governance.py feature_governance_cl.py feature_governance_objects.py`: all passed. - `git diff -U0 develop | contrib/devtools/clang-format-diff.py -p1` is clean except for the `LogPrint` in `CheckAndRemove()`. That call keeps its existing layout because `test/lint/lint-logs.py` needs the format string's `\n"` on the same line as `LogPrint(`. `lint-logs`, `lint-format-strings`, `lint-includes`, `lint-whitespace`, `lint-assertions`, `lint-include-guards`, `lint-locale-dependence` and `lint-circular-dependencies` pass locally. ## Breaking Changes None. RPC output fields and values are unchanged. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 748663c6af9c62b7f5a15083667fc5ed44907c896f0c7567aab0b08ccb9d908114b6e23c179d88773fb69e190fcb6770170a8f05469636337b67f480a1a7c2a9 (cherry picked from commit 2ca083a)
fbe6526 Merge #7807: feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment (pasta) 57fd2d9 Merge #7776: fix: keep a pending ProUpServTx across a registrar update that keeps the operator key (pasta) ca69801 Merge #7803: fix: return non-zero amount of accounts after sethdseed (pasta) 57a8f07 Merge #7796: backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection (pasta) 4eb82f6 Merge #7786: fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note (pasta) 70626a0 Merge #7793: backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) (pasta) 250a3cd Merge #7790: fix: guard cached governance object flags with the object lock (pasta) 700c195 Merge #7787: fix: only sign EHF signals inside the deployment's start/timeout window (pasta) a3b0f3c Merge #7785: fix: restore the txindex requirement when governance validation is enabled (pasta) 2d1735d Merge #7794: fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate (pasta) d9f6592 Merge #7783: fix: stop fish completion from re-running the typed dash-cli command (pasta) 89d44c8 Merge #7795: backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) (pasta) a1d7411 Merge #7779: fix!: report legacy evonode Platform addresses when listdiff changes its address (pasta) 73f6ea2 Merge #7789: fix(qt): report malformed lang/font settings instead of aborting at startup (pasta) 40de493 Merge #7781: fix(rpc): use a consistent tip height in quorum dkgstatus (pasta) 7d1a90d Merge #7791: fix(net): request object votes from nPeersPerHashMax peers (pasta) 51b145c Merge #7778: test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (pasta) Pull request description: ## Issue being fixed or feature implemented Backports for v24.0.0-rc.3. `v24.0.x` was cut at the v24.0.0-rc.2 tag (1e239d4), and develop has since moved to 24.1.0 (#7775), so rc.3 is built from this branch instead of being tagged on develop like rc.1 and rc.2. ## What was done? Cherry-picked (`git cherry-pick -m1 -x`) every develop merge since rc.2 that carries `backport-candidate-24.0.x`, plus #7778, which #7807's test needs. They are in develop merge order: - #7778 test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (test-only. Needed because #7807's `feature_evo_shares_activation.py` depends on its `mine_quorum_single_member()` fix and fails without it.) - #7791 fix(net): request object votes from nPeersPerHashMax peers - #7781 fix(rpc): use a consistent tip height in quorum dkgstatus - #7789 fix(qt): report malformed lang/font settings instead of aborting at startup - #7779 fix!: report legacy evonode Platform addresses when listdiff changes its address - #7795 backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) - #7783 fix: stop fish completion from re-running the typed dash-cli command - #7794 fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate - #7785 fix: restore the txindex requirement when governance validation is enabled - #7787 fix: only sign EHF signals inside the deployment's start/timeout window - #7790 fix: guard cached governance object flags with the object lock - #7793 backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) - #7786 fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note. Includes partial bitcoin#36170 (5f40d56). Upstream's dedicated-onion-bind changes to `feature_torcontrol.py` and `p2p_private_broadcast.py` are not included, because those tests depend on upstream commits (5693833, e74d54e) that Dash does not have. - #7796 backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection - #7803 fix: return non-zero amount of accounts after sethdseed - #7776 fix: keep a pending ProUpServTx across a registrar update that keeps the operator key - #7807 feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment One pick needed conflict resolution, in a test only, described in its commit message: #7793, `test/functional/wallet_migration.py`. `test_conflict_txs()` comes from bitcoin#28542 (#7762), which is not on this branch, so it is left out. The C++ part of #7793 is identical to develop. Merged since rc.2 but intentionally not included: #7775 (24.1 version bump), #7260 and #7804 (new Qt feature and its CI fix), #7584 (refactor), routine upstream backports #7257, #7759, #7762, #7768, #7788, #7801, and test/CI-only changes #7777 and #7797. No version change is needed. `configure.ac` is already 24.0.0 with `CLIENT_VERSION_IS_RELEASE` false, so the build takes its version string from the `v24.0.0-rc.3` tag, as it did for rc.2. ## How Has This Been Tested? Built on macOS arm64 against depends with `--enable-debug --enable-werror` (dashd, dash-qt, tests). - `make check`: all unit test suites pass, including `test_dash-qt`. - Functional tests, all passing: `feature_evo_shares_activation.py`, `feature_masternode_shares.py`, `feature_mnehf.py`, `feature_governance.py --descriptors`, `feature_governance_objects.py`, `feature_governance_txindex_devnet.py`, `feature_proxy.py`, `feature_notifications.py`, `feature_llmq_connections.py`, `feature_llmq_chainlocks_automatic.py`, `feature_llmq_singlenode.py`, `p2p_instantsend.py`, `rpc_blockchain.py` (v1 and v2 transport), `rpc_netinfo.py`, `rpc_quorum.py`, `rpc_verifychainlock.py`, `wallet_hd.py` (legacy and descriptors), `wallet_migration.py`. - `feature_bind_extra.py` (from #7786) only runs on Linux and was skipped locally, so CI covers it. Every touched file was diffed against develop. The remaining differences come only from develop PRs that are not part of this backport. ## Breaking Changes Carried over from the picked PRs: - #7807: consensus change behind the new `evo_shares` deployment (bit 14). Mainnet and testnet are unaffected until it activates. Devnets with `v24` already active and an EvoNode carrying several owner payouts need to check or reset. New RPC `protx shared_register_prepare_evo`. - #7785: outside regtest, a node with `-txindex=0` and without `-disablegovernance` refuses to start again, as before v23.1.0. - #7786: carries a partial bitcoin#36170. A node started with `-bind` but no specific `-bind=<addr:port>=onion` now refuses to start while `-listenonion` is enabled. That is the default when listening, even without Tor configured. A wildcard onion bind is also refused. Such nodes need `-bind=127.0.0.1:<port>=onion` or `-listenonion=0`. Nodes without `-bind`, including those using only `-whitebind`, keep the default `127.0.0.1:9996` (`19996` testnet) onion target. See `doc/release-notes-7300.md`. - #7779: `protx listdiff` now includes `platform_p2p` and `platform_https` for a legacy-address evonode whose core P2P address changes. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 8cb01bdb0234f31b5dd2c105b69673de148922991a7523627395c0e0ccfa22b94be5ffbecc842398d443bc90fd9baf2eee6a9cf6fcdbbd1626eb00b93726a55a
Issue being fixed or feature implemented
gobject getcan report wrong values forfCachedFunding,fCachedValidandfCachedEndorsedwhile the node is recomputing them. For example, a proposal that has reached the funding threshold can come back asfCachedFunding: false, and a proposal the network voted invalid can come back asfCachedValid: true. The reads are also a data race on plainboolmembers. That is undefined behavior, and ThreadSanitizer reports it (see below).There are two causes:
The cached flags (
fCachedFunding,fCachedValid,fCachedDelete,fCachedEndorsed,fDirtyCache) have no lock. Readers take no lock:CGovernanceObject::GetStateJson(), used bygobject get.gobject list,gobject diffand the Qt proposal list throughGetAllNewerThan().No single outer lock covers the writers either:
CGovernanceManager::CheckAndRemove()holdscs_main+cs_store.SuperblockManager::IsSuperblockTriggered()holds onlycs_sb. It is called from block-template creation and block validation, and also from themasternode winnersRPC withoutcs_main.fDirtyCachewhile holding the object's owncs.CGovernanceObject::UpdateSentinelVariables()first resetsfCachedFunding/fCachedEndorsedtofalseandfCachedValidtotrue. It then counts votes, and only after that writes the real values. A reader that runs during this window sees the defaults instead of the old or the new result.There is also a related ordering bug.
fCachedDeletewas set before the lock that setsnDeletionTime.CheckAndRemove()readsGetDeletionTime()and thenIsSetCachedDelete()in separate calls. IfIsSuperblockTriggered()marks a trigger for deletion in between,CheckAndRemove()sees a set delete flag with a zero deletion time, computesnow - 0, and erases the object immediately instead of afterGOVERNANCE_DELETION_DELAY.Block validation itself is not affected. Its
IsSuperblockTriggered()call holdscs_mainandcs_sb, which already serializes it against the other writers. The visible impact is on RPC/GUI output, plus undefined behavior under the C++ memory model.Reproduction
A throwaway test case (not part of this PR) in
governance_vote_processing_testsreuses its single-masternode fixture. The masternode votes YES on funding and NO on validity, so withnGovernanceMinQuorum=1the correct state never changes:fCachedFunding=true,fCachedValid=false. One thread callsUpdateSentinelVariables()in a loop, asCheckAndRemove()/IsSuperblockTriggered()do. The main thread callsGetStateJson()for 3 seconds, asgobject getdoes, and counts reads that disagree with the correct state.Test case
Results against the unfixed tree (c14104b):
What was done?
The flags are now protected by the object's existing
csmutex, the same lock that already guardsfExpired,nDeletionTimeand the vote maps. Clang's thread-safety analysis enforces it.refactor: require cs in CGovernanceObject::CountMatchingVotes.CountMatchingVotes()becomes a private helper withEXCLUSIVE_LOCKS_REQUIRED(cs). The publicGet{Yes,No,Abstain}Count()wrappers take the lock. No behavior change. This lets one critical section count several signals.refactor: give CGovernanceObjectVoteFile a copy assignment operator.CGovernanceObjectVoteFilehas a copy constructor that rebuildsmapVoteIndex(list iterators) against its own list, but no copy assignment. The implicit one can't be instantiated becauseCGovernanceVoteis not assignable. This commit adds one with the same rebuild semantics, so the copy constructor in commit 3 can copy the vote file while holding the source's lock. A unit test checks that a copy-assigned file is still usable after its source is destroyed.fix: guard cached governance object flags with the object lock.GUARDED_BY(cs), and theIsSetCached*()/IsSetDirtyCache()getters take the lock, likeIsSetExpired().UpdateSentinelVariables()counts the votes and writes every flag inside oneLOCK(cs). Each flag is assigned its computed value directly, with no reset to a default first. This means:fDirtyCache = falseis ordered with the vote counts under the same lock that vote writers already hold, so a vote arriving during a recompute still marks the object dirty.PrepareDeletion()andUpdateSentinelVariables()share a privateMarkForDeletion()helper that setsfCachedDeleteandnDeletionTimetogether, sofCachedDeleteimplies a non-zero deletion time.GetStateJson()reads all four flags under one lock, sogobject getreturns a consistent snapshot.cs-guarded state while holdingother.cs.fix: read deletion flags before deletion time in CheckAndRemove.CheckAndRemove()now readsIsSetCachedDelete()/IsSetExpired()beforeGetDeletionTime().fCachedDeleteis never cleared, and it now changes together withnDeletionTime, so a set delete flag is always paired with its deletion time.fExpiredis only set bySuperblockManager::Clean(). That runs fromCheckAndRemove()itself and stamps the deletion time before this loop.Locking notes
cswhile calling the getters,UpdateSentinelVariables(),PrepareDeletion(),GetStateJson(), or the copy constructor. The externalcsholders only touch the vote file.UpdateSentinelVariables()andGetStateJson()take no other lock while holdingcs.cs_main/cs_store/cs_sb→obj.csedges already existed throughGetYesCount()and the oldUpdateSentinelVariables(). No new lock-order edges are introduced.Intentionally left unchanged
fCachedLocalValidity,strLocalValidityErrorandfUnparsablefollow the same unguarded pattern. However,GetStateJson()recomputes local validity instead of reading the cache, and I found no concurrent reader of those fields, so they are out of scope here.How Has This Been Tested?
macOS arm64, Apple clang. Built with depends using
--disable-hardening --enable-debug --enable-crash-hooks --enable-stacktraces --enable-suppress-external-warnings --without-gui --enable-tests --disable-bench --disable-fuzz-binary --enable-werror. This enables-Wthread-safetyas an error.LOCK(cs)inGetStateJson(). The build then fails withpassing variable 'fCachedValid' by reference requires holding mutex 'cs' [-Werror,-Wthread-safety-reference].--with-sanitizers=thread) with this PR. The reproduction ran with a second reader added that also copy-constructs the object each iteration (thegobject listpath). Result: no ThreadSanitizer warnings and 0 wrong reads. The same binary built from the unfixed tree reports the data race shown above.CheckAndRemove()ordering fix has the same limitation; I verified it by reviewing every writer offCachedDelete,fExpiredandnDeletionTime.governance_vote_sync_tests/copy_assigned_vote_file_indexes_its_own_votes.src/test/test_dash --run_test='governance*':*** No errors detected.test/functional/test_runner.py feature_governance.py feature_governance_cl.py feature_governance_objects.py: all passed.git diff -U0 develop | contrib/devtools/clang-format-diff.py -p1is clean except for theLogPrintinCheckAndRemove(). That call keeps its existing layout becausetest/lint/lint-logs.pyneeds the format string's\n"on the same line asLogPrint(.lint-logs,lint-format-strings,lint-includes,lint-whitespace,lint-assertions,lint-include-guards,lint-locale-dependenceandlint-circular-dependenciespass locally.Breaking Changes
None. RPC output fields and values are unchanged.
Checklist:
🤖 Generated with Claude Code