Skip to content

fix(miner): validate cumulative special transaction state per package - #7570

Open
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:fix/assetunlock-package-accounting
Open

fix(miner): validate cumulative special transaction state per package#7570
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:fix/assetunlock-package-accounting

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 10, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

  • Block template construction accounted for credit-pool and EHF state only for the package root before discovering and sorting its unconfirmed ancestors.
  • A prioritized child could therefore hide an over-limit AssetUnlock transaction in its ancestor set. The invalid package would be added to the candidate block and the final credit-pool replay would abort getblocktemplate instead of skipping the package.
  • Stateful checks also ran before fee, size, finality, and InstantSend gates, so a later-rejected candidate could contaminate accounting for subsequent packages.

This pull request is based directly on develop and does not depend on another pull request.

What was done?

  • Move cumulative special-transaction accounting after all non-mutating package checks and topological sorting.
  • Replay every AssetLock and AssetUnlock member in block order through a package-atomic credit-pool operation that restores amounts and indexes on failure.
  • Trial EHF signal updates on a package-local copy and commit them only after credit-pool validation succeeds.
  • Remove miner dependencies that were used only by the former root-only AssetUnlock check.
  • Add unit coverage for mid-package amount and index rollback.
  • Add functional coverage proving that a valid AssetUnlock ancestor package is mined while an over-limit ancestor package is excluded without aborting template construction.

How Has This Been Tested?

  • Built dashd and test_dash locally on macOS arm64 using depends.
  • Ran evo_assetlocks_tests/credit_pool_package_atomicity.
  • Ran feature_asset_locks.py end to end. The new test fails on vulnerable code with failed-creditpool-unlock-too-much and passes with this change.
  • Ran the targeted Python lint and mypy checks for feature_asset_locks.py.
  • Ran lint-whitespace.py and git diff --check.
  • Performed an independent adversarial review of package ordering, rollback behavior, modified-entry handling, and tracker commit points.

Breaking Changes

None. Consensus validation and transaction serialization are unchanged; this changes block-template package selection so invalid packages are skipped instead of poisoning or aborting template construction.

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

This pull request was created by Codex.

@coderabbitai

coderabbitai Bot commented Aug 10, 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: dd418ee0-1ab8-4ce9-9289-1c7a65011631

📥 Commits

Reviewing files that changed from the base of the PR and between 1fbf489 and 929b5a4.

📒 Files selected for processing (7)
  • doc/release-notes-7570.md
  • src/evo/creditpool.cpp
  • src/evo/creditpool.h
  • src/node/miner.cpp
  • src/node/miner.h
  • src/test/evo_assetlocks_tests.cpp
  • test/functional/feature_asset_locks.py
💤 Files with no reviewable changes (1)
  • src/node/miner.h

Walkthrough

Block template creation now validates Asset Lock/Unlock transactions at package scope. Credit-pool state rolls back when any transaction in a package fails. EHF signal duplicates are checked before package acceptance. BlockAssembler dependencies were updated. Unit and functional tests cover rollback, valid ancestor packages, and packages rejected for exceeding withdrawal limits.

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

Sequence Diagram(s)

sequenceDiagram
  participant BlockAssembler
  participant TransactionPackage
  participant CCreditPoolDiff
  participant BlockTemplate
  BlockAssembler->>TransactionPackage: sort ancestor package
  BlockAssembler->>CCreditPoolDiff: validate Asset Lock/Unlock transactions
  CCreditPoolDiff-->>BlockAssembler: accept or reject package atomically
  BlockAssembler->>BlockTemplate: include valid package
Loading

Possibly related PRs

  • dashpay/dash#7437: Introduces shared-collateral Asset Unlock validation related to withdrawal-limit handling.
  • dashpay/dash#7473: Also changes block-package validation in src/node/miner.cpp.

Suggested reviewers: knst

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: package-level validation of cumulative special-transaction state in the miner.
Description check ✅ Passed The description directly explains the package-level validation changes, rollback behavior, skipped invalid packages, and related tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@github-actions

Copy link
Copy Markdown

Potential PR merge conflicts

This is advisory only. It does not block CI, but it marks PRs that will likely need a rebase depending on merge order.

If this PR merges first

These open PRs will likely need a rebase:

@thepastaclaw

thepastaclaw commented Aug 10, 2026

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit 929b5a4)

@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: 076c8c6efd

ℹ️ 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".

Comment thread src/evo/creditpool.cpp

bool CCreditPoolDiff::ProcessLockUnlockTransactions(const std::vector<CTransactionRef>& txs, TxValidationState& state)
{
auto initialIndexes = newIndexes;

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 cloning all prior unlock indexes per package

When a template contains many independent Asset Unlock transactions, this copies every index accumulated from all previously accepted packages before processing each subsequent package. Because newIndexes grows by one per unlock, assembling an unlock-heavy block now performs O(n²) node allocations and hash insertions, which can substantially delay repeated getblocktemplate calls for blocks containing thousands of withdrawals. Track only the indexes inserted by the current package and erase those on rollback, rather than cloning the entire set.

AGENTS.md reference: AGENTS.md:L172-L172

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.

Final validation — Codex/Sol only (Phase 2 disabled)

The package-level credit-pool and EHF accounting is logically sound, and the new tests cover the intended rollback behavior. One in-scope performance issue remains: cloning the cumulative unlock-index set for every package makes unlock-heavy block-template construction quadratic while holding both cs_main and the mempool lock.
Source: reviewer backends: gpt-5.6-sol (Codex general) and gpt-5.6-sol (Codex dash-core-commit-history); final verifier backend: gpt-5.6-sol (Codex). openclaw-agent/cliproxy/gpt-5.6-sol was orchestration-only and is not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

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/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 1 suggestion(s)

🤖 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 `src/evo/creditpool.cpp`:
- [SUGGESTION] src/evo/creditpool.cpp:325-340: Avoid copying all accepted unlock indexes for every package
  `newIndexes` contains the indexes from every Asset Unlock already accepted into the candidate block, so copying the entire set before each package causes O(n²) hash-node allocations across independent unlock packages. A 2 MB template can contain thousands of small Asset Unlock transactions because the withdrawal limit constrains their total amount rather than their count. Once the amount limit is exhausted, each additional unlock package still copies all previously accepted indexes before immediately failing. This work occurs inside `CreateNewBlock()` while both `cs_main` and the mempool lock are held. Record only the indexes inserted by this invocation and erase those during rollback; the amount fields can continue using scalar snapshots.

Comment thread src/evo/creditpool.cpp
Comment on lines +325 to +340
bool CCreditPoolDiff::ProcessLockUnlockTransactions(const std::vector<CTransactionRef>& txs, TxValidationState& state)
{
auto initialIndexes = newIndexes;
const auto initialLocked = sessionLocked;
const auto initialUnlocked = sessionUnlocked;

for (const auto& tx : txs) {
if (ProcessLockUnlockTransaction(*tx, state)) continue;

newIndexes = std::move(initialIndexes);
sessionLocked = initialLocked;
sessionUnlocked = initialUnlocked;
return false;
}
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Suggestion: Avoid copying all accepted unlock indexes for every package

newIndexes contains the indexes from every Asset Unlock already accepted into the candidate block, so copying the entire set before each package causes O(n²) hash-node allocations across independent unlock packages. A 2 MB template can contain thousands of small Asset Unlock transactions because the withdrawal limit constrains their total amount rather than their count. Once the amount limit is exhausted, each additional unlock package still copies all previously accepted indexes before immediately failing. This work occurs inside CreateNewBlock() while both cs_main and the mempool lock are held. Record only the indexes inserted by this invocation and erase those during rollback; the amount fields can continue using scalar snapshots.

Suggested change
bool CCreditPoolDiff::ProcessLockUnlockTransactions(const std::vector<CTransactionRef>& txs, TxValidationState& state)
{
auto initialIndexes = newIndexes;
const auto initialLocked = sessionLocked;
const auto initialUnlocked = sessionUnlocked;
for (const auto& tx : txs) {
if (ProcessLockUnlockTransaction(*tx, state)) continue;
newIndexes = std::move(initialIndexes);
sessionLocked = initialLocked;
sessionUnlocked = initialUnlocked;
return false;
}
return true;
}
bool CCreditPoolDiff::ProcessLockUnlockTransactions(const std::vector<CTransactionRef>& txs, TxValidationState& state)
{
const auto initialLocked = sessionLocked;
const auto initialUnlocked = sessionUnlocked;
std::vector<uint64_t> packageIndexes;
packageIndexes.reserve(txs.size());
for (const auto& tx : txs) {
const bool isUnlock = tx->IsSpecialTxVersion() && tx->nType == TRANSACTION_ASSET_UNLOCK;
if (ProcessLockUnlockTransaction(*tx, state)) {
if (isUnlock) {
const auto payload = GetTxPayload<CAssetUnlockPayload>(*tx);
assert(payload);
packageIndexes.emplace_back(payload->getIndex());
}
continue;
}
for (const uint64_t index : packageIndexes) {
newIndexes.erase(index);
}
sessionLocked = initialLocked;
sessionUnlocked = initialUnlocked;
return false;
}
return true;
}

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.

2 participants