Skip to content

Conversation

@psolstice
Copy link
Contributor

PR intention

Removes duplicate checks for ZK proofs, when spark spend verification fails gives it a second chance, because it might have failed due to incomplete anonymity set at the time of the check

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 23, 2025

Walkthrough

Refactors Spark spend proof verification in src/spark/state.cpp from an eager in-memory caching approach to an asynchronous, thread-pool-backed validation flow with recheck semantics. Introduces a do/while loop to manage pending and in-progress checks, enqueuing verification tasks and handling both stateful and non-stateful verification pathways.

Changes

Cohort / File(s) Change Summary
Asynchronous Spark Spend Verification
src/spark/state.cpp
Replaced synchronous in-memory cache lookup with asynchronous validation flow. Removed eager cached-check block; added do/while recheck loop managing pending/in-progress checks. Integrated thread pool-based verification for non-stateful checks with future storage. Enhanced state management via fChecked, fResult, and fRecheckNeeded flags. Adjusted exception handling and result propagation to support deferred verification pathways.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant State as state.cpp
    participant Pool as Proof ThreadPool
    participant Cache as gCheckedSparkSpendTransactions

    rect rgb(200, 220, 250)
    Note over State: New Asynchronous Flow
    Caller->>State: Verify Spark Spend (non-stateful)
    State->>State: Enter do/while (fRecheckNeeded)
    
    alt Future exists in cache
        State->>Cache: Check prior in-progress result
        Cache-->>State: Future completed?
        alt Result ready
            State->>State: Mark fChecked=true, extract result
        else Result pending
            State->>State: fRecheckNeeded=true (retry)
        end
    else No cached future
        State->>Pool: Enqueue verification task
        Pool-->>Cache: Store future (fChecked=false)
        State->>State: fRecheckNeeded=true (poll next iteration)
    end
    
    State->>State: Loop until !fRecheckNeeded
    
    alt fChecked && result valid
        State-->>Caller: Return verified
    else Stateful required
        State->>State: Direct synchronous verification
        State-->>Caller: Return result
    else Defer verification
        State-->>Caller: Return true (async pending)
    end
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Asynchronous flow logic: Verify correctness of the do/while recheck loop, including loop termination conditions and state transitions.
  • Thread pool integration: Ensure proper future handling, race conditions, and lifecycle management of gCheckProofThreadPool tasks.
  • State management: Trace transitions of fChecked, fResult, and fRecheckNeeded flags across all code paths to prevent inconsistent states.
  • Exception handling: Confirm that error paths correctly propagate and don't leak partial/stale results into the cache.
  • Stateful vs. non-stateful paths: Validate logic branching and ensure both paths produce correct verification outcomes.

Possibly related PRs

Poem

🐰 Hops from sync to async streams,
Rechecks dance through thread-pool dreams,
Futures cached, results deferred,
Each proof verified and heard!

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improve performance on Spark ZK proof verification' directly relates to the main change, which optimizes ZK proof verification by implementing asynchronous validation and recheck mechanisms.
Description check ✅ Passed The PR description provides the PR intention section explaining the changes (removing duplicate checks and adding recheck logic), meeting the template requirements despite the optional 'Code changes brief' section being omitted.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch zk-proof-refactor-bottleneck-fix

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/spark/state.cpp (1)

794-800: No critical issues found. The recheck logic is correctly bounded to a maximum of 2 loop iterations.

The embedded analysis in the review comment was accurate: fRecheckNeeded is reset to false at the start of each do-block iteration (line 769), preventing infinite loops. When an async proof check fails and the entry is erased, the second iteration immediately falls through without re-entering the async path because the entry no longer exists. If fStatefulSigmaCheck is true, the subsequent synchronous verification at line 831 will properly validate or reject the proof.

The assumption that failures may stem from an incomplete anonymity set is reasonable and documented in the code comments. However, the suggestion to add telemetry/logging for recheck frequency and success rates is valid—currently there is no logging for this specific path, only for the "already checked" scenario (line 778). This would improve production observability.

The retries are not "unnecessary"—they serve a legitimate purpose of re-validating once the anonymity set may have grown. The concern about invalid proofs passing through is addressed by the bounded retry and subsequent synchronous verification.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7acba96 and 52d97f0.

📒 Files selected for processing (1)
  • src/spark/state.cpp (2 hunks)
🔇 Additional comments (1)
src/spark/state.cpp (1)

822-839: LGTM with a note on exception handling.

The post-loop logic correctly handles the different verification paths:

  • Cached verified proof → use cached result
  • Stateful check needed → verify synchronously
  • Non-stateful check → return true and defer verification

The exception handler at lines 837-839 sets passVerify = false, which is appropriate. However, if an exception occurs while the lock is held, the cache state in gCheckedSparkSpendTransactions may be left inconsistent (e.g., an entry with fChecked=false but no checkInProgress future). Subsequent checks of the same transaction might encounter unexpected states. Consider whether cleanup logic is needed in the exception path.

Comment on lines +784 to +792
// wait for the check to complete
auto future = checkState.checkInProgress;
cs_checkedSparkSpendTransactions.unlock();
bool result = future->get();
cs_checkedSparkSpendTransactions.lock();

checkState.fChecked = true;
checkState.fResult = result;
checkState.checkInProgress = nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Race condition with dangling reference after unlock/relock.

Lines 786-788 unlock the critical section, wait for the future, then relock. However, lines 790-792 access the checkState reference that was obtained before unlocking. Between the unlock and relock, another thread could erase or modify the gCheckedSparkSpendTransactions[hashTx] entry, causing checkState to become a dangling reference. Accessing it afterward leads to undefined behavior.

Apply this fix to safely handle the state after relocking:

                     else if (checkState.checkInProgress && fStatefulSigmaCheck) {
                         // wait for the check to complete
                         auto future = checkState.checkInProgress;
                         cs_checkedSparkSpendTransactions.unlock();
                         bool result = future->get();
                         cs_checkedSparkSpendTransactions.lock();
 
-                        checkState.fChecked = true;
-                        checkState.fResult = result;
-                        checkState.checkInProgress = nullptr;
+                        // Re-lookup entry as it may have changed while unlocked
+                        if (gCheckedSparkSpendTransactions.count(hashTx)) {
+                            auto& updatedCheckState = gCheckedSparkSpendTransactions[hashTx];
+                            updatedCheckState.fChecked = true;
+                            updatedCheckState.fResult = result;
+                            updatedCheckState.checkInProgress = nullptr;
 
-                        if (!result) {
-                            // unfortunately, it's possible that the proof was checked and failed
-                            // because the anonymity set was incomplete at the time of checking. We need
-                            // to recheck the proof again
-                            fRecheckNeeded = true;
-                            gCheckedSparkSpendTransactions.erase(hashTx);
-                        }
-                        else
-                            fChecked = true;
+                            if (!result) {
+                                // unfortunately, it's possible that the proof was checked and failed
+                                // because the anonymity set was incomplete at the time of checking. We need
+                                // to recheck the proof again
+                                fRecheckNeeded = true;
+                                gCheckedSparkSpendTransactions.erase(hashTx);
+                            }
+                            else
+                                fChecked = true;
+                        }
                     }

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@aleflm aleflm left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks.

@reubenyap reubenyap merged commit f8a2737 into master Nov 26, 2025
37 of 38 checks passed
@reubenyap reubenyap deleted the zk-proof-refactor-bottleneck-fix branch November 26, 2025 15:36
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.

5 participants