Skip to content

Spark: Speed up reindex proof verification#1865

Closed
navidR wants to merge 8 commits into
firoorg:masterfrom
navidR:dev/navidr/reindex-spark-speedup
Closed

Spark: Speed up reindex proof verification#1865
navidR wants to merge 8 commits into
firoorg:masterfrom
navidR:dev/navidr/reindex-spark-speedup

Conversation

@navidR

@navidR navidR commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

This patch speeds up reindex after Spark activation by removing duplicate Spark spend proof verification and making the remaining Grootle proof verification cheaper. During reindex/import, Spark spend proofs no longer need to be fully verified in the early CheckBlock path and then verified again during ConnectBlock; instead, the expensive proof check is deferred to the ConnectBlock path before Spark state is committed, while mempool, wallet, and VerifyDB proof checks remain enforced. The Grootle verifier is also optimized by avoiding a large intermediate commitment list and feeding the equivalent multi-scalar multiplication terms directly into secp256k1. In the 855k-860k Spark-heavy window, reindex time dropped from 1:26:13 to about 13:12, a 6.5x speedup and 84.7% wall-time reduction.

navidR added 8 commits June 15, 2026 13:17
Keep non-wallet VerifyDB paths from honoring fVerifySparkSpendProof=false for Spark spend proofs, while preserving wallet DB load behavior by marking that call as wallet validation. Extend the Spark tampered-proof regression test to cover both paths.
Do not mark non-stateful CheckBlock results as fully cached for blocks containing Spark spends. This keeps default proof verification and VerifyDB behavior intact while preventing soft-passed Spark proof-input cases from poisoning block.fChecked.\n\nAdd Spark regressions for VerifyDB cache bypass, wallet-load cheap validation with proof skipping, and BatchProofContainer collection around tampered spends.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

Release Notes

  • New Features

    • Added deferred transaction proof verification for improved performance.
    • Enhanced proof result tracking and caching for verification operations.
  • Bug Fixes

    • Improved error handling in cryptographic operations with explicit validation checks.
    • Strengthened detection of invalid or tampered transaction proofs.
  • Tests

    • Added comprehensive test coverage for proof verification boundary conditions and mutation scenarios.

Walkthrough

This PR overhaults Spark spend proof verification to support deferred and batched checking. It hardens low-level crypto (MultiExponent error propagation, Grootle batch-verify accumulation), adds a SparkSpendProofVerificationResult enum and proof-input-hash caching, rewrites BatchProofContainer cover-set batching, threads deferral control through CheckTransaction/CheckBlock/ConnectBlock/AcceptBlock, and adds comprehensive tests.

Changes

Spark Spend Proof Deferred/Batched Verification

Layer / File(s) Summary
MultiExponent error handling and Grootle batch verify rewrite
src/secp256k1/src/cpp/MultiExponent.cpp, src/libspark/grootle.cpp, src/libspark/test/grootle_test.cpp
MultiExponent::get_multiple() now throws on scratch allocation or ecmult_multi_var failure. Grootle batch verify switches to separate S[i]/V[i] scalar tracking, adds per-proof I_last decomposition and effective-size validation, and emits two points per index in the final multiexp. New boundary_sizes_and_mutations test covers valid boundary cases and targeted negative mutations.
SpendTransaction cover-set size stricter validation
src/libspark/spend_transaction.cpp
The Grootle batching loop in SpendTransaction::verify now rejects missing cover_set_id entries, zero sizes, and sizes exceeding the full cover set.
SparkSpendProofVerificationResult enum and public API contracts
src/spark/state.h, src/validation.h
Adds SparkSpendProofVerificationResult enum (NotApplicable, Verified, Deferred, Batched) and extends CheckSparkTransaction, CheckTransaction, and CheckBlock declarations with deferral/result parameters.
Spark state proof-input-hash caching and deferral logic
src/spark/state.cpp
Replaces async/in-progress cached state with a ProofCheckState keyed on a deterministic proof-input hash. CheckSparkSpendTransaction gains deferral/batching gating conditions, converts missing coin-group data to soft Deferred outcomes, invalidates stale cache entries, and stores bounded verified results. CheckSparkTransaction initializes the result to NotApplicable and forwards new parameters.
BatchProofContainer clear() and cover-set batching rewrite
src/batchproof_container.h, src/batchproof_container.cpp
Adds clear() to reset stored transactions and collection state. finalize() clears tempSparkTransactions after transfer. batch_spark() is rewritten to compute per-cover-set maxHeight via GetCoinGroupInfo/GetCoinSetForSpend, cache cover sets by id, and throw std::invalid_argument for missing or empty data.
CheckTransaction, CheckBlock, ConnectBlock, AcceptBlock, and walletdb wiring
src/validation.cpp, src/wallet/walletdb.cpp
Threads deferral flags and result out-parameters through CheckTransaction and CheckBlock. Moves batched proof finalization earlier in ConnectBlock (before persistent state updates) with exception-based block rejection. AcceptBlock derives fDeferSparkSpendProofVerification from dbp != NULL. walletdb.cpp adapts to the new CheckTransaction signature.
Spark integration tests
src/test/spark_tests.cpp
Extends checktransaction with tampered-spend assertions covering deferred proof verification, CheckBlock fChecked gating, BatchProofContainer batched verification success and throw behavior, ConnectBlock rejection without recording used linking tags, and deferred-vs-verified CheckBlock comparisons.

Sequence Diagram(s)

sequenceDiagram
  participant AcceptBlock
  participant CheckBlock
  participant CheckTransaction
  participant CheckSparkSpendTransaction
  participant BatchProofContainer
  participant ConnectBlock

  rect rgba(70, 130, 180, 0.5)
    Note over AcceptBlock,CheckBlock: Block acceptance path
    AcceptBlock->>AcceptBlock: fDeferSparkSpendProofVerification = (dbp != NULL)
    AcceptBlock->>CheckBlock: CheckBlock(..., fDeferSparkSpendProofVerification)
    CheckBlock->>CheckTransaction: fVerifySparkSpendProof, &sparkSpendProofResult
    CheckTransaction->>CheckSparkSpendTransaction: fVerifySparkSpendProof, sparkSpendProofResult
    CheckSparkSpendTransaction-->>CheckTransaction: Verified | Deferred | Batched
    CheckBlock->>CheckBlock: gate fChecked on fSparkSpendProofsComplete
  end

  rect rgba(60, 179, 113, 0.5)
    Note over ConnectBlock,BatchProofContainer: Block connection path
    ConnectBlock->>CheckBlock: fDeferSparkSpendProofVerification=true
    ConnectBlock->>BatchProofContainer: finalize()
    ConnectBlock->>BatchProofContainer: verify() [before persistent state updates]
    BatchProofContainer-->>ConnectBlock: throws std::invalid_argument on failure
    ConnectBlock->>ConnectBlock: reject block on exception
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • firoorg/firo#1665: Modifies the same Spark spend proof verification caching and result-tracking flow in src/spark/state.cpp and src/spark/state.h.
  • firoorg/firo#1787: Modifies Spark/Grootle spend-proof verification internals in src/spark/state.cpp and src/libspark/grootle.cpp, overlapping directly with the BatchProofContainer and Grootle batching changes here.
  • firoorg/firo#1723: Adds thread-pool concurrency gating and async scheduling to CheckSparkSpendTransaction in src/spark/state.cpp, which this PR rewrites and simplifies.

Suggested labels

Spark, size:XL

Suggested reviewers

  • psolstice
  • levonpetrosyan93
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.14% 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 and concisely summarizes the main objective of the changeset: optimizing Spark reindex proof verification performance.
Description check ✅ Passed The description is comprehensive, covering the problem being solved, architectural changes made, and quantified performance improvements, though it could reference the template structure.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@navidR

navidR commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added Spark size:XL This PR changes 500-999 lines, ignoring generated files labels Jun 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/batchproof_container.cpp (1)

44-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear batch state when verification throws.

batch_spark() only clears sparkTransactions on success. Any throw before Line 134 leaves the singleton holding the failed batch, and init() only clears tempSparkTransactions, so the next batch can be poisoned unless every caller remembers to call clear().

Proposed exception-safe cleanup
 void BatchProofContainer::verify() {
-    if (!fCollectProofs) {
-        batch_spark();
-    }
-    fCollectProofs = false;
+    try {
+        if (!fCollectProofs) {
+            batch_spark();
+        }
+        fCollectProofs = false;
+    } catch (...) {
+        clear();
+        throw;
+    }
 }

Also applies to: 127-134

🤖 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 `@src/batchproof_container.cpp` around lines 44 - 49, The verify() method in
BatchProofContainer does not clean up state when batch_spark() throws an
exception, leaving the singleton with a failed batch. Wrap the batch_spark()
call in a try-catch block (or use RAII with a cleanup object) to ensure that
sparkTransactions is properly cleared or reset even if batch_spark() throws
before completing, preventing the next batch from being poisoned. The cleanup
should occur in the catch handler before re-throwing the exception, ensuring
exception safety while maintaining the fCollectProofs flag reset.
🧹 Nitpick comments (2)
src/validation.cpp (1)

4161-4161: ⚡ Quick win

Move the CheckBlock opening brace to the next line.

This changed function definition doesn’t follow the repository’s Linux brace style for function definitions.

♻️ Proposed style fix
-bool CheckBlock(const CBlock& block, CValidationState& state, const Consensus::Params& consensusParams, bool fCheckPOW, bool fCheckMerkleRoot, int nHeight, bool isVerifyDB, bool fDeferSparkSpendProofVerification) {
+bool CheckBlock(const CBlock& block, CValidationState& state, const Consensus::Params& consensusParams, bool fCheckPOW, bool fCheckMerkleRoot, int nHeight, bool isVerifyDB, bool fDeferSparkSpendProofVerification)
+{

As per coding guidelines, “Linux brace style: new line for namespaces, classes, and function definitions.”

🤖 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 `@src/validation.cpp` at line 4161, The function definition for CheckBlock in
src/validation.cpp (line 4161) has its opening brace on the same line as the
function signature, which violates the repository's Linux brace style
guidelines. Move the opening brace to a new line following the function
signature so that CheckBlock follows the required style where function
definitions have their opening brace on a separate line, consistent with
repository conventions.

Source: Coding guidelines

src/test/spark_tests.cpp (1)

643-711: ⚡ Quick win

Use scoped cleanup for BatchProofContainer global state in this test.

This test toggles singleton state (fCollectProofs, init(), clear()) across many branches. A scoped guard would prevent state leakage if an unexpected exception/abort occurs mid-test.

Suggested pattern
+    BatchProofContainer* batchProofContainer = BatchProofContainer::get_instance();
+    const bool originalCollectProofs = batchProofContainer->fCollectProofs;
+    struct BatchProofContainerStateGuard {
+        BatchProofContainer* c;
+        bool original;
+        ~BatchProofContainerStateGuard() {
+            c->clear();
+            c->fCollectProofs = original;
+            c->init();
+        }
+    } batchGuard{batchProofContainer, originalCollectProofs};
-
-    BatchProofContainer* batchProofContainer = BatchProofContainer::get_instance();
🤖 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 `@src/test/spark_tests.cpp` around lines 643 - 711, The test manually toggles
`batchProofContainer` global state (setting `fCollectProofs`, calling `init()`
and `clear()`) across multiple test branches, which risks state leakage if an
exception or assertion failure occurs between initialization and cleanup.
Implement a scoped guard class (RAII pattern) that wraps `batchProofContainer`
state management—ensure the guard's constructor handles initialization (setting
`fCollectProofs` and calling `init()`) and the destructor automatically calls
`clear()` regardless of whether the scope exits normally or via exception.
Replace the manual state toggles throughout the test with instantiations of this
scoped guard to ensure cleanup always happens.
🤖 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 `@src/batchproof_container.cpp`:
- Around line 80-83: Add range validation for uint64_t cover-set IDs before
narrowing to int. At line 80 where cover_set_id is used in the
GetCoinGroupInfo() call and at line 108 where it is used in the
GetCoinSetForSpend() call, validate that the uint64_t value falls within the
valid range for int (typically 0 to INT_MAX) before performing the conversion.
If the value is out of range, throw an appropriate exception to prevent silent
narrowing that could cause incorrect cover set lookups.

In `@src/spark/state.cpp`:
- Around line 845-879: The exception handling in both locations is too broad and
conflates internal/runtime errors with actual proof verification failures. In
src/spark/state.cpp lines 845-879, the catch block for std::exception is
converting all exceptions to passVerify=false, which masks internal errors.
Replace the bare catch block to let internal exceptions propagate (either via
return state.Error(...) or by not catching them) while only the verifier result
from spark::SpendTransaction::verify should determine passVerify. Similarly, in
src/batchproof_container.cpp lines 121-130, do not catch and suppress exceptions
from the batch verification logic; instead rethrow internal/runtime failures and
reserve the invalid_argument exception for actual proof verification failures
only.
- Line 761: The batching path sets fNeedFullCoverSets based on !useBatching
(line 761), which causes cover_sets to contain empty vectors when batching is
enabled. However, proofInputHash still includes these empty cover_sets in its
computation, creating a different hash from prior non-batched verification of
the same spend. This causes the cache invalidation logic in the range 841-854 to
incorrectly erase valid cache entries. Fix this by either computing the
proofInputHash using only stable proof inputs that remain consistent between
batched and non-batched paths (excluding cover_sets when they are not
materialized), or by adding a condition in the cache invalidation section to
skip erasing entries when full cover sets were not available (when useBatching
is true).

In `@src/validation.cpp`:
- Around line 2815-2819: The Spark batch is being verified twice: once in the
new pre-state-update verification block at lines 2815-2819 where you call
batchProofContainer->verify() inside the try block, and again later in
ActivateBestChain at line 3817. Remove the later verification call at line 3817
in the ActivateBestChain function, or if that call is in a location that affects
other logic, conditionally skip it after the earlier verification has already
succeeded to prevent duplicate verification and avoid verifying after the block
has been connected.

---

Outside diff comments:
In `@src/batchproof_container.cpp`:
- Around line 44-49: The verify() method in BatchProofContainer does not clean
up state when batch_spark() throws an exception, leaving the singleton with a
failed batch. Wrap the batch_spark() call in a try-catch block (or use RAII with
a cleanup object) to ensure that sparkTransactions is properly cleared or reset
even if batch_spark() throws before completing, preventing the next batch from
being poisoned. The cleanup should occur in the catch handler before re-throwing
the exception, ensuring exception safety while maintaining the fCollectProofs
flag reset.

---

Nitpick comments:
In `@src/test/spark_tests.cpp`:
- Around line 643-711: The test manually toggles `batchProofContainer` global
state (setting `fCollectProofs`, calling `init()` and `clear()`) across multiple
test branches, which risks state leakage if an exception or assertion failure
occurs between initialization and cleanup. Implement a scoped guard class (RAII
pattern) that wraps `batchProofContainer` state management—ensure the guard's
constructor handles initialization (setting `fCollectProofs` and calling
`init()`) and the destructor automatically calls `clear()` regardless of whether
the scope exits normally or via exception. Replace the manual state toggles
throughout the test with instantiations of this scoped guard to ensure cleanup
always happens.

In `@src/validation.cpp`:
- Line 4161: The function definition for CheckBlock in src/validation.cpp (line
4161) has its opening brace on the same line as the function signature, which
violates the repository's Linux brace style guidelines. Move the opening brace
to a new line following the function signature so that CheckBlock follows the
required style where function definitions have their opening brace on a separate
line, consistent with repository conventions.
🪄 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: 069270b1-db23-4b85-ab9f-8e40e2137f99

📥 Commits

Reviewing files that changed from the base of the PR and between 7fabe1e and c566eac.

📒 Files selected for processing (12)
  • src/batchproof_container.cpp
  • src/batchproof_container.h
  • src/libspark/grootle.cpp
  • src/libspark/spend_transaction.cpp
  • src/libspark/test/grootle_test.cpp
  • src/secp256k1/src/cpp/MultiExponent.cpp
  • src/spark/state.cpp
  • src/spark/state.h
  • src/test/spark_tests.cpp
  • src/validation.cpp
  • src/validation.h
  • src/wallet/walletdb.cpp

Comment on lines 80 to +83
int cover_set_id = idAndHash.first;
if (!cover_sets.count(cover_set_id)) {
std::vector<spark::Coin> cover_set;
sparkState->GetCoinSet(cover_set_id, cover_set);
cover_sets[cover_set_id] = cover_set;
}
spark::CSparkState::SparkCoinGroupInfo coinGroup;
if (!sparkState->GetCoinGroupInfo(cover_set_id, coinGroup))
throw std::invalid_argument("Spark batch verification missing cover set");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect Spark cover-set ID types and state API parameter types.
# Expectation: uint64_t spend cover-set IDs are either range-checked before int APIs,
# or the CSparkState APIs are widened consistently.

rg -nP -C3 '\bgetBlockHashes\s*\(|\bGetCoinGroupInfo\s*\(|\bGetCoinSetForSpend\s*\('
rg -nP -C3 'std::map\s*<\s*uint64_t\s*,\s*uint256\s*>|unordered_map\s*<\s*uint64_t'

Repository: firoorg/firo

Length of output: 27235


🏁 Script executed:

# Read the batchproof_container.cpp file to see the actual code and context
cat -n src/batchproof_container.cpp | sed -n '70,115p'

Repository: firoorg/firo

Length of output: 2551


🏁 Script executed:

# Search for any range validation or checks on cover_set_id or ID values
rg -n 'cover_set_id|std::numeric_limits|INT_MAX|int::max' src/batchproof_container.cpp

Repository: firoorg/firo

Length of output: 182


🏁 Script executed:

# Check the types of idAndBlockHashes to confirm uint64_t
rg -n -B5 'getBlockHashes' src/batchproof_container.cpp | head -20

Repository: firoorg/firo

Length of output: 396


🏁 Script executed:

# Check if there's any documentation or comments about the ID range
rg -n -A3 'getBlockHashes|setBlockHashes' src/libspark/spend_transaction.h

Repository: firoorg/firo

Length of output: 318


🏁 Script executed:

# Check for any other places where uint64_t cover-set IDs are converted to int
rg -n 'GetCoinGroupInfo|GetCoinSetForSpend' src/batchproof_container.cpp

Repository: firoorg/firo

Length of output: 194


🏁 Script executed:

# Look for how GetLatestCoinID is used and what range of values it returns
rg -n 'GetLatestCoinID' src/spark/state.cpp | head -10

Repository: firoorg/firo

Length of output: 304


🏁 Script executed:

# Check the GetCoinGroupInfo and GetCoinSetForSpend function signatures
rg -n -A2 'bool GetCoinGroupInfo|int GetCoinSetForSpend' src/spark/state.h

Repository: firoorg/firo

Length of output: 268


🏁 Script executed:

# Search for any documentation or limits on coin group IDs
rg -n 'group_id|coinGroupID|cover.*set.*id' src/spark/state.h | head -20

Repository: firoorg/firo

Length of output: 261


🏁 Script executed:

# Check the GetLatestCoinID implementation to see what values it can produce
sed -n '1209,1215p' src/spark/state.cpp

Repository: firoorg/firo

Length of output: 225


🏁 Script executed:

# Look at how the coin group IDs are managed - are they always expected to fit in int?
rg -n 'coinGroups\[' src/spark/state.cpp | head -5

Repository: firoorg/firo

Length of output: 337


🏁 Script executed:

# Check the serialization of spend transaction to see if there's control over ID ranges
rg -n -B3 -A3 'set_id_blockHash\s*=' src/libspark/spend_transaction.cpp | head -20

Repository: firoorg/firo

Length of output: 286


🏁 Script executed:

# Check what values latestCoinId can reach - is it bounded?
rg -n 'latestCoinId' src/spark/state.h | head -10

Repository: firoorg/firo

Length of output: 82


🏁 Script executed:

# Check if there are any comments or documentation about ID ranges in state.h
rg -n -B2 -A2 'latestCoinId|coinGroups' src/spark/state.h | head -30

Repository: firoorg/firo

Length of output: 317


🏁 Script executed:

# Verify the type of latestCoinId member variable
rg -n 'int.*latestCoinId' src/spark/state.h

Repository: firoorg/firo

Length of output: 82


🏁 Script executed:

# Check if there's any place where latestCoinId is initialized or set to specific values
rg -n 'latestCoinId\s*=' src/spark/state.cpp | head -15

Repository: firoorg/firo

Length of output: 241


🏁 Script executed:

# Check if there's validation on the coin group IDs anywhere during transaction processing
rg -n -B3 -A3 'setBlockHashes' src/spark/state.cpp

Repository: firoorg/firo

Length of output: 38


🏁 Script executed:

# Check how selection.first is used and where it comes from
sed -n '98,115p' src/batchproof_container.cpp

Repository: firoorg/firo

Length of output: 811


🏁 Script executed:

# Verify the data flow - where do sparkTransactions come from?
rg -n 'sparkTransactions' src/batchproof_container.cpp

Repository: firoorg/firo

Length of output: 666


🏁 Script executed:

# Check if there's any validation in the constructor or entry point
rg -n -B5 'BatchProofContainer::verify' src/batchproof_container.cpp | head -20

Repository: firoorg/firo

Length of output: 187


🏁 Script executed:

# Check if there's any other location where these uint64_t IDs might be used without validation
rg -n 'getBlockHashes()' src/ -A5 | grep -E 'getBlockHashes|static_cast.*int|int.*=' | head -20

Repository: firoorg/firo

Length of output: 646


🏁 Script executed:

# Verify that there's no existing validation in sparkState methods that would catch out-of-range IDs
sed -n '1199,1210p' src/spark/state.cpp

Repository: firoorg/firo

Length of output: 339


Add range validation before narrowing uint64_t cover-set IDs to int.

The getBlockHashes() API returns std::map<uint64_t, uint256>, but both GetCoinGroupInfo() and GetCoinSetForSpend() expect int parameters. Without validation, an out-of-range serialized ID can be silently narrowed to an incorrect group ID, causing batch verification to process coins against the wrong cover set.

Line 80 uses implicit narrowing; line 108 uses explicit casting—neither validates the range. Add a range check before both conversions:

Proposed range guard
+#include <limits>
 `#include` <stdexcept>
 `#include` <unordered_map>
         auto& idAndBlockHashes = itr.getBlockHashes();
         for (const auto& idAndHash : idAndBlockHashes) {
-            int cover_set_id = idAndHash.first;
+            if (idAndHash.first > static_cast<uint64_t>(std::numeric_limits<int>::max()))
+                throw std::invalid_argument("Spark batch verification cover set id out of range");
+
+            int cover_set_id = static_cast<int>(idAndHash.first);
             spark::CSparkState::SparkCoinGroupInfo coinGroup;
             if (!sparkState->GetCoinGroupInfo(cover_set_id, coinGroup))
                 throw std::invalid_argument("Spark batch verification missing cover set");
         const int nCoins = sparkState->GetCoinSetForSpend(
                 &chainActive,
                 selection.second.maxHeight,
-                static_cast<int>(selection.first),
+                static_cast<int>(selection.first),
                 blockHash,
                 cover_set,
                 setHash);

Also applies to: line 108

🤖 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 `@src/batchproof_container.cpp` around lines 80 - 83, Add range validation for
uint64_t cover-set IDs before narrowing to int. At line 80 where cover_set_id is
used in the GetCoinGroupInfo() call and at line 108 where it is used in the
GetCoinSetForSpend() call, validate that the uint64_t value falls within the
valid range for int (typically 0 to INT_MAX) before performing the conversion.
If the value is out of range, throw an appropriate exception to prevent silent
narrowing that could cause incorrect cover set lookups.

Comment thread src/spark/state.cpp
!isCheckWallet &&
sparkTxInfo &&
!sparkTxInfo->fInfoIsComplete;
const bool fNeedFullCoverSets = !useBatching;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid invalidating verified proof-cache entries from the batching path.

When useBatching is true, cover_sets intentionally contains empty vectors, but proofInputHash still includes cover_sets. That makes the hash differ from a prior non-batched verification for the same spend, so Line 853 erases a valid cache entry and forces duplicate batch verification. Use a cache key based on stable proof inputs available in both paths, or skip cache invalidation when full cover sets were not materialized.

Based on PR context, this path is meant to eliminate redundant Spark spend proof verification during deferred/batched validation.

Also applies to: 841-854

🤖 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 `@src/spark/state.cpp` at line 761, The batching path sets fNeedFullCoverSets
based on !useBatching (line 761), which causes cover_sets to contain empty
vectors when batching is enabled. However, proofInputHash still includes these
empty cover_sets in its computation, creating a different hash from prior
non-batched verification of the same spend. This causes the cache invalidation
logic in the range 841-854 to incorrectly erase valid cache entries. Fix this by
either computing the proofInputHash using only stable proof inputs that remain
consistent between batched and non-batched paths (excluding cover_sets when they
are not materialized), or by adding a condition in the cache invalidation
section to skip erasing entries when full cover sets were not available (when
useBatching is true).

Comment thread src/spark/state.cpp
Comment on lines +845 to +879
try {
{
LOCK(cs_checkedSparkSpendTransactions);
auto it = gCheckedSparkSpendTransactions.find(hashTx);
if (it != gCheckedSparkSpendTransactions.end()) {
auto& checkState = it->second;
if (checkState.proofInputHash != proofInputHash) {
LogPrintf("CheckSparkSpendTransaction: cached proof inputs changed for tx %s\n", hashTx.ToString());
gCheckedSparkSpendTransactions.erase(it);
} else if (checkState.fChecked) {
if (!checkState.fResult)
return state.DoS(100, false, REJECT_INVALID, "CheckSparkSpendTransaction: previously checked and failed");

LogPrintf("CheckSparkSpendTransaction: already checked tx %s\n", hashTx.ToString());
fChecked = true;
}
}
}
catch (const std::exception &) {
passVerify = false;

if (fChecked) {
passVerify = true;
setProofResult(SparkSpendProofVerificationResult::Verified);
} else if (useBatching) {
passVerify = true;
batchProofContainer->add(*spend);
setProofResult(SparkSpendProofVerificationResult::Batched);
} else {
passVerify = spark::SpendTransaction::verify(*spend, cover_sets);
if (passVerify)
setProofResult(SparkSpendProofVerificationResult::Verified);
}
}
catch (const std::exception &) {
passVerify = false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not convert internal verifier exceptions into invalid-proof results. Both paths catch every std::exception, so runtime failures from the crypto verifier become passVerify=false / “batch verification failed” instead of surfacing as internal validation errors.

  • src/spark/state.cpp#L845-L879: let internal/runtime exceptions propagate to the outer CheckSparkTransaction catch or return state.Error(...); only a verifier false result should mean “invalid proof.”
  • src/batchproof_container.cpp#L121-L130: do not collapse verifier exceptions into passed=false; rethrow internal/runtime failures and reserve the invalid_argument("Spark batch verification failed...") path for actual proof verification failures.

Based on PR context, low-level Spark/Grootle verification errors are now propagated via exceptions, so callers need to preserve that distinction.

📍 Affects 2 files
  • src/spark/state.cpp#L845-L879 (this comment)
  • src/batchproof_container.cpp#L121-L130
🤖 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 `@src/spark/state.cpp` around lines 845 - 879, The exception handling in both
locations is too broad and conflates internal/runtime errors with actual proof
verification failures. In src/spark/state.cpp lines 845-879, the catch block for
std::exception is converting all exceptions to passVerify=false, which masks
internal errors. Replace the bare catch block to let internal exceptions
propagate (either via return state.Error(...) or by not catching them) while
only the verifier result from spark::SpendTransaction::verify should determine
passVerify. Similarly, in src/batchproof_container.cpp lines 121-130, do not
catch and suppress exceptions from the batch verification logic; instead rethrow
internal/runtime failures and reserve the invalid_argument exception for actual
proof verification failures only.

Comment thread src/validation.cpp
Comment on lines +2815 to +2819
if (batchProofContainer->fCollectProofs) {
try {
batchProofContainer->finalize();
batchProofContainer->verify();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid verifying the same Spark batch again after ConnectTip commits.

This new pre-state-update verification is still followed by the existing BatchProofContainer::verify() in ActivateBestChain at Line 3817. Remove or make that later call a no-op after this block succeeds; otherwise Spark batches are verified twice, and any later exception happens after the block has already been connected.

🐛 Proposed direction
-        // Do batch verification if we reach 1 day old block,
-        BatchProofContainer* batchProofContainer = BatchProofContainer::get_instance();
-        batchProofContainer->fCollectProofs = ((GetSystemTimeInSeconds() - pindexNewTip->GetBlockTime()) > 86400) && GetBoolArg("-batching", true);
-        batchProofContainer->verify();
+        // Spark batch verification is completed inside ConnectBlock before state updates.
🤖 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 `@src/validation.cpp` around lines 2815 - 2819, The Spark batch is being
verified twice: once in the new pre-state-update verification block at lines
2815-2819 where you call batchProofContainer->verify() inside the try block, and
again later in ActivateBestChain at line 3817. Remove the later verification
call at line 3817 in the ActivateBestChain function, or if that call is in a
location that affects other logic, conditionally skip it after the earlier
verification has already succeeded to prevent duplicate verification and avoid
verifying after the block has been connected.

@navidR navidR closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files Spark

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant