Skip to content

test: stabilize asset locks functional test#7411

Merged
PastaPastaPasta merged 1 commit into
dashpay:developfrom
PastaPastaPasta:codex/fix-feature-asset-locks-flake
Jul 8, 2026
Merged

test: stabilize asset locks functional test#7411
PastaPastaPasta merged 1 commit into
dashpay:developfrom
PastaPastaPasta:codex/fix-feature-asset-locks-flake

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Fixes #7310.

feature_asset_locks.py could fail intermittently under high concurrency while waiting for a freshly mined llmq_test_platform quorum or while syncing mempools during an asset-unlock reorg path.

What was done?

  • Wait for the actual mining node to have the non-null quorum commitment before mining the final-commitment block in mine_quorum().
  • Allow generate_batch() to use block-only sync for the asset-unlock invalidation padding where mempool equality is not part of the assertion.
  • Make asset-unlock request ID handling reusable and deterministic.
  • Select an asset-unlock index that maps to the intended oldest active platform quorum instead of relying on a bounded probabilistic search.
  • Fix the asset-unlock quorum-selection assertion to use the transaction's actual request ID.

How Has This Been Tested?

  • Built from latest upstream/develop with make -j8.
  • Reproduced the issue locally before the fix with 20 concurrent feature_asset_locks.py runs.
  • Ran python3.9 test/functional/feature_asset_locks.py --tmpdir=/private/tmp/dash_assetlocks_fixed_solo2 --portseed=301.
  • Ran 20 concurrent feature_asset_locks.py instances using seeds 100-119 with --timeout-factor=4; all 20 passed.

Breaking Changes

None.

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

Supersedes closed PR #7409, which was accidentally closed during review-followup branch recovery.

This pull request was created by Codex.

@thepastaclaw

thepastaclaw commented Jul 7, 2026

Copy link
Copy Markdown

✅ Review complete (commit 0b993eb)

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

⚠️ Potential Merge Conflicts Detected

This PR has potential conflicts with the following open PRs:

Please coordinate with the authors of these PRs to avoid merge conflicts.

@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: 0b993eb9df

ℹ️ 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_quorum_hash = self.nodes[0].quorum('list')['llmq_test_platform'][-1]
# Quorum selection depends on the request ID. Scan 100 candidates to
# avoid missing the target oldest quorum by chance.
for index in range(start_index, start_index + 100):

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 Avoid scanning into indexes reused later

When this helper is called with start_index=501 or 601, the widened + 100 scan includes the hard-coded indexes reused later in the same test (520-522 and 620-622). If the first candidate that maps to the oldest quorum lands on one of those values, create_assetunlock() has already produced a recovered signature for that request ID; the later unlock with the same index but a different withdrawal has a different msgHash, so platformsign refuses it as a conflicting vote and the test times out/fails. Keep this search range disjoint from the later indexes or reserve the selected index.

Useful? React with 👍 / 👎.

@UdjinM6 UdjinM6 Jul 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

same as #7409 (comment): is there a reason the range was bumped this much? nvm, I get it now

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR refactors functional test code for asset-unlock quorum handling. In feature_asset_locks.py, new helper methods compute asset-unlock request IDs and locate the oldest matching platform quorum deterministically, replacing prior retry loops; generate_batch() gains an optional sync callback. In test_framework.py, a new node_has_quorum_commitment() helper is extracted from wait_for_quorum_commitment() and reused, and mine_quorum() now waits on a single node's commitment instead of all online masternodes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

Skipped: changes are test-only refactors of existing control flow (replacing retry loops with deterministic lookups, extracting a helper function) without new multi-component feature interactions.

Estimated code review effort

3 (Moderate) | ~20 minutes

Related issues: None specified in the provided context.

Related PRs: None specified in the provided context.

Suggested labels: tests, refactor

Suggested reviewers: None specified in the provided context.

🐰 A quorum once chased in a loop,
Now found with one deterministic swoop,
Requests get IDs, oldest gets picked,
Test framework's node-check, neatly ticked,
Hop along, the tests now run true!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: stabilizing the asset locks functional test.
Description check ✅ Passed The description matches the changeset and explains the test flake fix and related refactors.
Linked Issues check ✅ Passed The PR addresses #7310 by changing quorum wait behavior in the functional test path that was timing out.
Out of Scope Changes check ✅ Passed The added helpers and test-framework changes all support the stated asset-lock and quorum stabilization goals.
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.

@coderabbitai coderabbitai 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.

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 `@test/functional/test_framework/test_framework.py`:
- Around line 2203-2206: The quorum setup in mine_quorum still waits for every
mninfos_online node to receive the commitment before checking the miner, which
keeps the mining path gated on full DKG propagation. Remove the retained
all-masternode wait and make the flow in mine_quorum wait only for the mining
node’s commitment, using node_has_quorum_commitment on self.nodes[0] before
proceeding.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6507e04b-79b1-4d3a-aad8-7cf3aee42fec

📥 Commits

Reviewing files that changed from the base of the PR and between 976e15a and 0b993eb.

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

Comment on lines 2203 to +2206
self.wait_for_quorum_commitment(q, mninfos_online, llmq_type=llmq_type)

self.log.info("Waiting final commitment on mining node")
self.wait_until(lambda: self.node_has_quorum_commitment(self.nodes[0], q, llmq_type), timeout=15)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove the retained all-masternode commitment wait.

Line 2203 still blocks on every mninfos_online node before Line 2206 checks the miner, so mine_quorum() remains gated on non-mining-node DKG propagation. For this stabilization path, wait only for the node that will build the commitment block.

Proposed fix
-        self.log.info("Waiting final commitment")
-        self.wait_for_quorum_commitment(q, mninfos_online, llmq_type=llmq_type)
-
         self.log.info("Waiting final commitment on mining node")
         self.wait_until(lambda: self.node_has_quorum_commitment(self.nodes[0], q, llmq_type), timeout=15)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.wait_for_quorum_commitment(q, mninfos_online, llmq_type=llmq_type)
self.log.info("Waiting final commitment on mining node")
self.wait_until(lambda: self.node_has_quorum_commitment(self.nodes[0], q, llmq_type), timeout=15)
self.log.info("Waiting final commitment on mining node")
self.wait_until(lambda: self.node_has_quorum_commitment(self.nodes[0], q, llmq_type), timeout=15)
🤖 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 `@test/functional/test_framework/test_framework.py` around lines 2203 - 2206,
The quorum setup in mine_quorum still waits for every mninfos_online node to
receive the commitment before checking the miner, which keeps the mining path
gated on full DKG propagation. Remove the retained all-masternode wait and make
the flow in mine_quorum wait only for the mining node’s commitment, using
node_has_quorum_commitment on self.nodes[0] before proceeding.

@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.

Code Review

Source: reviewer lanes: opus (Claude general and dash-core-commit-history failed before JSON), gpt-5.5 (Codex general and dash-core-commit-history); verifier: gpt-5.5 (Codex, recovered after opus verifier failed).

Verified the PR at 0b993eb against upstream/develop. The changes are scoped to functional test stabilization and the added helpers preserve the existing asset-unlock request ID, quorum selection, and DKG commitment checks without introducing an in-scope defect.

inode.invalidateblock(block_asset_unlock)
self.validate_credit_pool_balance(locked)
self.generate_batch(25)
self.generate_batch(25, sync_fun=lambda: self.sync_blocks())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why this line is updated? how sync-mempools + sync-blocks is worse than just sync-blocks?

Is it for better performance? Or any other reason?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good question. This is intentional for the flake tracked in #7310: after invalidating block_asset_unlock, the disconnected asset-unlock txs can legitimately sit in different node mempools. This section only needs the replacement chain to advance and assert the credit-pool state stays at locked; mempool equality is not part of the invariant here.

The old default generate_batch() did sync_all(), and one of the recorded #7310 failures timed out exactly here in sync_mempools() with one node having txs and the others empty. Passing sync_blocks() keeps the chain-state synchronization needed by this reorg path without gating on nondeterministic mempool convergence.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

one of the recorded #7310 failures timed out exactly here in sync_mempools()

Passing sync_blocks() keeps the chain-state synchronization needed by this reorg path without gating on nondeterministic mempool convergence.

Failed sync of mempool probably points out to some broken connections between masternodes or consensus diversification ; otherwise mempools will be synced. And removing sync_mempools on this stage would just cause it hang on the later steps at some point.

I'd suggest to revert this change.

Otherwise, PR looks fine for me.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I dug into this more, and I think the block-only sync here is intentional.

The important details are:

  1. Right before this invalidation path, the test already does a normal generate(...), which defaults to sync_all() (sync_blocks() + sync_mempools()). So the nodes should already be normally synced before invalidateblock().

  2. This invalidation is deep: block_asset_unlock is roughly 46 blocks below the tip (HEIGHT_DIFF_EXPIRING is 48, and the asset-unlock block is mined about two blocks after that height is recorded).

  3. Core only tries to resurrect disconnected transactions into the mempool for the first 10 disconnected blocks:

    (++disconnected <= 10)

    The nearby comment says that for deep invalidation, keeping the mempool up to date is probably futile.

  4. invalidateblock() is a local RPC path. Any tx resurrection is local AcceptToMemoryPool(...) cleanup; it is not a relay step that guarantees all peers converge to the same mempool.

So the thing this part of the test needs to prove is: after replacing the invalidated chain, all nodes accept the same blocks and keep the expected credit-pool state. sync_blocks() plus validate_credit_pool_balance(...) checks that. Requiring exact mempool equality immediately after a deep invalidation is asserting something Core does not guarantee.

The test does return to normal synchronization after reconsiderblock(): the later generate(...) uses default sync_all(), so mempool sync is not being skipped forever, only in the deep-invalidation window where it is not a reliable invariant.

I think the best follow-up is to keep this sync_blocks() call and add a short code comment explaining the deep-invalidation/mempool-resurrection reason.

@knst knst left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM 0b993eb

@PastaPastaPasta PastaPastaPasta requested a review from UdjinM6 July 7, 2026 20:17

@UdjinM6 UdjinM6 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.

utACK 0b993eb

@PastaPastaPasta PastaPastaPasta merged commit cbab254 into dashpay:develop Jul 8, 2026
50 of 61 checks passed
@UdjinM6 UdjinM6 added this to the 24 milestone Jul 8, 2026
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

4 participants