Skip to content

test: fix remaining feature_asset_locks flake from autonomous MnEHF signal txs - #7509

Open
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:test/7310-asset-locks-mnehf-mempool
Open

test: fix remaining feature_asset_locks flake from autonomous MnEHF signal txs#7509
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:test/7310-asset-locks-mnehf-mempool

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 2, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Follow-up to #7411, which closed #7310. The same test kept failing after that
closure, so the fix was only partial.

Sorting the post-closure reports on #7310 by the tree they actually ran on
splits them in two:

This PR fixes the second one and the race that lets it happen.

What was done?

Two related changes.

1. mine_quorum() only waited for the commitment of the type it was mining.

#7411 added a wait for the mining node to hold the final commitment, but only
for llmq_type, the type the caller asked for. The block mine_quorum() then
generates carries a commitment for every LLMQ type whose mining window is
open, and in regtest all test types share dkgInterval = 24, so they all
finalize on the same block. A type whose real commitment has not reached the
mining node yet is mined as a null commitment
(src/llmq/blockprocessor.cpp:842-846); null commitments are accepted without
being recorded as mined (src/llmq/blockprocessor.cpp:322-331), so that quorum
is silently skipped for the whole cycle.

Captured from a failing run, at final-commitment block 178, for the undriven
llmq_test:

GetMineableCommitments cf height[178] content: { created nversion[3] quorumIndex[0] }
ProcessCommitment -- processing commitment for block height=178, type=100,
  quorumHash=1ffd275a..., signers=0, validMembers=0,
  quorumPublicKey=000000000000...0000

created rather than cached is the mining node synthesising a null commitment
because it held no real one.

mine_quorum() now waits for the mining node to hold every commitment the
masternodes actually produced for that quorum hash, not just the driven type.
Only commitments the masternodes already have are awaited, so a type whose DKG
legitimately produced nothing cannot hold a test up.

2. check_mempool_size() asserted a global mempool count.

regtest's llmqTypeMnhf is LLMQ_TEST (src/chainparams.cpp:929), so a skipped
llmq_test quorum defers the one-shot V24 MnEHF signal transaction to a later
cycle. CEHFSignalsHandler submits it from the masternodes on their own
(src/llmq/ehf_signals.cpp:118) at a moment the test does not control, while
check_mempool_size() compared getmempoolinfo()['size'] against
self.mempool_size, which only ever modelled the test's own transactions:

node2 ... Special EHF TX is created hash=1feb94ce...
node2 ... IsValidMNActivation: set MnEHF for bit=12 is valid   (bit 12 = DEPLOYMENT_V24)
node0 ... accepted 1feb94ce... (poolsz 1 txn, 0 kB)
test  ... AssertionError: not(1 == 0)

The count now excludes MnEHF signal transactions, so the assertion still says
exactly what it said before about the transactions this test submits, and no
longer depends on when the masternodes submit theirs. Deliberately not a
wait_until on the mempool size: that would also pass if an asset-unlock
transaction wrongly lingered, which is what the assertion exists to catch.

mine_cycle_quorum() has the same gap and does not wait for commitments at all,
but it drives rotated (dip0024) quorums and no failure in this family was traced
to it, so it is left alone.

How Has This Been Tested?

Built from upstream/develop at efe6dec7b9a on macOS arm64,
configure --prefix=depends/aarch64-apple-darwin25.3.0 --disable-bench, make -j13.
Sequential control: feature_asset_locks.py passes in 167 s, matching CI's
passing runtime.

The natural rate of this flake is only ~3% locally, and it is gated by a discrete
precursor rather than a timing window, so raising --jobs does not amplify it —
at -j20 the machine slows uniformly, which gives the llmq_test commitment
more time to arrive and suppressed the precursor entirely (0/20). The
before/after was therefore measured against a forced precursor: an
experiment-only patch, not part of this PR, that removes the "Mine block to empty
mempool" generate() in test_asset_unlocks — the block that would otherwise
sweep the MnEHF transaction. Same assertion, same transaction, same mechanism.

Run Result
15x parallel, forced precursor, without this PR 15/15 failed, every one the not(1 == 0) signature
15x parallel, forced precursor, with this PR 15/15 passed
15x parallel, unforced, without this PR 1/15 failed (the natural ~3% rate)
15x parallel, unforced, with this PR 30/30 passed across 2 batches; a 3rd batch is excluded, see below

A third unforced 15x batch ran while the host was under an unrelated load spike
(load average 145 on a 14-core machine, from desktop applications rather than
the test run) and lost 14 of 15 copies to block-sync, mempool-sync,
recovered-signature and RPC timeouts. Those are host-contention failures, not
this signature, and are excluded from the table rather than counted as passes.
Across all 45 unforced runs with this PR there were zero not(1 == 0)
occurrences.

In the forced-and-unfixed runs the single mempool entry was confirmed to be the
MnEHF transaction, e.g. 872ca79e... created by CEHFSignalsHandler and
accepted on node0 at poolsz 1 txn.

Because the framework change affects every test that mines quorums, all 15 such
tests were run: feature_dip4_coinbasemerkleroots, feature_llmq_connections,
feature_llmq_data_recovery, feature_llmq_dkg_intake, feature_llmq_dkgerrors,
feature_llmq_evo, feature_llmq_rotation, feature_llmq_signing,
feature_llmq_simplepose (both variants), feature_mnehf,
feature_notifications, feature_protx_version, p2p_instantsend,
p2p_platform_ban, p2p_quorum_data. All passed except feature_protx_version,
which failed at test_revoke_protx waiting for getconnectioncount() == 0
(feature_protx_version.py:241). That is the pre-existing #6702 flake, not a
regression here: it reproduces identically on unmodified develop (1/8 failed
at -j8 on a clean tree vs 3/8 with this PR, the same line 241 signature in
every case), and it is unrelated to quorum commitments.

test/lint/lint-python.py passes.

Breaking Changes

None. Test-only change.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone

mine_quorum() waited for the mining node to hold the final commitment only for the llmq type it was asked to mine. The block it then generates carries a commitment for every llmq type whose mining window is open, and in regtest all test types share dkgInterval=24, so they all finalize on the same block.

A type whose real commitment has not reached the mining node in time is mined as a null commitment (src/llmq/blockprocessor.cpp:842-846). Null commitments are accepted without being recorded as mined (src/llmq/blockprocessor.cpp:322-331), so that quorum is silently skipped for the whole cycle. Seen at block 178 of a failing feature_asset_locks.py run, where llmq_test got signers=0, validMembers=0, quorumPublicKey=0...0 while the driven llmq_test_instantsend commitment was mined correctly.

Wait for the mining node to hold every commitment the masternodes actually produced. Types whose DKG legitimately produced nothing are not awaited, so this cannot hold up a test that expects a type not to form.
check_mempool_size() compared getmempoolinfo()['size'] against self.mempool_size, which only models the transactions the test itself submits. Masternodes submit the one-shot V24 MnEHF signal transaction on their own as soon as a quorum able to sign it exists (src/llmq/ehf_signals.cpp:118), at a moment the test does not control.

When that lands between the test's last generate() and a check expecting an empty mempool, the test fails with 'not(1 == 0)'. This is the failure mode still reported on dashpay#7310 after dashpay#7411 closed it, and it still reproduces on current develop.

Count only the transactions this test submits. Deliberately not a wait_until on the mempool size: that would also pass if an asset unlock transaction wrongly lingered, which is what the assertion exists to catch.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 35562c57-4afe-43ca-ab21-9ef1c5de700c

📥 Commits

Reviewing files that changed from the base of the PR and between 862ef3c and 03ff1ff.

📒 Files selected for processing (2)
  • test/functional/feature_asset_locks.py
  • test/functional/test_framework/test_framework.py

Walkthrough

The asset-lock test adds MNEHF_SIGNAL_TX_TYPE and excludes MnEHF signal transactions from mempool-size checks. The test framework adds quorum commitment type discovery and waits for all commitment types produced by participating masternodes before mining a quorum.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Masternodes
  participant DashTestFramework
  participant MiningNode
  Masternodes->>DashTestFramework: Provide commitment types
  DashTestFramework->>MiningNode: Check quorum commitment types
  MiningNode-->>DashTestFramework: Return non-null types
  DashTestFramework->>DashTestFramework: Wait for all produced types
  DashTestFramework->>MiningNode: Mine quorum after synchronization
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the feature test flake and the autonomous MnEHF signal transactions that cause it.
Description check ✅ Passed The description directly explains the MnEHF mempool fix, quorum commitment race fix, testing, and scope.
Linked Issues check ✅ Passed The changes address the linked feature_asset_locks quorum timeout by waiting for all produced commitments before mining.
Out of Scope Changes check ✅ Passed The changes remain within functional-test reliability and quorum commitment handling related to the linked issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Aug 2, 2026

Copy link
Copy Markdown

⛔ Blockers found — Sonnet deferred (commit 03ff1ff)
Canonical validated blockers: 1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03ff1ffec9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

expected = set()
for mn in mninfos:
expected |= self.node_quorum_commitment_types(mn.get_node(self), quorum_hash)
return expected <= self.node_quorum_commitment_types(self.nodes[0], quorum_hash)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for auxiliary DKG handlers to finalize

When an undriven LLMQ type's independent phase-handler thread is slower than the driven type, none of the queried masternodes may advertise that auxiliary commitment on the first poll. In that case expected contains only the driven commitment—which the miner already has—so this subset check succeeds immediately; the following block can still be mined before the slower handler adds and relays its real commitment, recreating the null-commitment/skipped-cycle behavior this change is intended to prevent. Establish that the auxiliary handlers have finished, or otherwise stabilize the expected type set, before accepting this condition.

AGENTS.md reference: AGENTS.md:L163-L165

Useful? React with 👍 / 👎.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The MnEHF filtering correctly preserves asset-lock mempool accounting, but the new miner synchronization helper does not reliably discover every auxiliary commitment. Because auxiliary LLMQ handlers finalize independently, the predicate can succeed before a slower handler publishes its commitment, leaving the skipped-cycle race this PR is intended to fix.

Source: reviewers codex/general=gpt-5.6-sol(completed); codex/dash-core-commit-history=gpt-5.6-sol(completed); verifier=codex/verifier=gpt-5.6-sol(completed); coordinator=openclaw-agent/cliproxy/gpt-5.6-sol(orchestration-only).

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `test/functional/test_framework/test_framework.py`:
- [BLOCKING] test/functional/test_framework/test_framework.py:2177-2181: Stabilize auxiliary commitment discovery before mining
  This predicate can succeed before an auxiliary LLMQ handler has finalized. The preceding `wait_for_quorum_commitment()` only waits for the explicitly driven type, so the first poll here may find that type alone on every masternode and immediately confirm that the miner has it. Each LLMQ type runs in an independent phase-handler thread (`src/llmq/net_dkg.cpp:605-610`), and reaching phase 6 does not close the race: `WaitForNextPhase()` publishes the phase before control returns to `FinalizeCommitments()` and `AddMineableCommitment()` at `src/llmq/net_dkg.cpp:788-792`. A slower auxiliary handler can therefore add its real commitment just after this subset check returns, while the following block has already synthesized a null commitment for that type. Establish that all relevant auxiliary handlers have completed finalization before deriving the expected set, while retaining an explicit way to represent a completed DKG that legitimately produced no commitment.

Comment on lines +2177 to +2181
def check_miner_commitments():
expected = set()
for mn in mninfos:
expected |= self.node_quorum_commitment_types(mn.get_node(self), quorum_hash)
return expected <= self.node_quorum_commitment_types(self.nodes[0], quorum_hash)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Stabilize auxiliary commitment discovery before mining

This predicate can succeed before an auxiliary LLMQ handler has finalized. The preceding wait_for_quorum_commitment() only waits for the explicitly driven type, so the first poll here may find that type alone on every masternode and immediately confirm that the miner has it. Each LLMQ type runs in an independent phase-handler thread (src/llmq/net_dkg.cpp:605-610), and reaching phase 6 does not close the race: WaitForNextPhase() publishes the phase before control returns to FinalizeCommitments() and AddMineableCommitment() at src/llmq/net_dkg.cpp:788-792. A slower auxiliary handler can therefore add its real commitment just after this subset check returns, while the following block has already synthesized a null commitment for that type. Establish that all relevant auxiliary handlers have completed finalization before deriving the expected set, while retaining an explicit way to represent a completed DKG that legitimately produced no commitment.

source: ['codex']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: feature_asset_locks intermittent llmq_test_platform quorum timeout

2 participants