Skip to content

feat(rpc): add mining-transaction snapshot proofs - #7107

Merged
PastaPastaPasta merged 10 commits into
dashpay:developfrom
PastaPastaPasta:platform-sdk-compact-proof
Sep 18, 2026
Merged

PastaPastaPasta merged 10 commits into
dashpay:developfrom
PastaPastaPasta:platform-sdk-compact-proof

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Jan 17, 2026 •

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Allow Platform SDKs to authenticate newer quorum keys and EvoNode records from an independently pinned Core snapshot using compact ordinary certificate proofs. Implements the mining-transaction proof format in DIP #175.

What was done?

Each DASHNC02 link carries a ChainLock certificate and the complete next quorum's mining transaction with a transaction Merkle path. Consecutive X11 headers bridge unavailable ChainLocks at mining height. The final coinbase authenticates both roots; an optional bootstrap envelope opens the requested Platform quorum and up to fifteen eligible EvoNodes.

getquorumproofchain generates evidence from a checkpoint block hash and minimum target height. verifyquorumproofchain takes the full independently trusted snapshot, bounded binary proof, and optional freshness floor. Verification enforces canonical framing, strict certificate-height progress, BLS/X11 linkage, positional Merkle shape, and cumulative resource budgets. The wire and HTTP bootstrap response are limited to 1 MiB; certificates and total ancestor headers are each limited to 4,096.

getchainlockbyheight and proof generation read historical evidence from disk on demand. ChainLock code finds coinbase-carried certificates using exponential/binary search over consensus-monotonic certified heights, with a bounded cache for one request. The existing mined-commitment database locates mining transactions. Bounded in-memory LRU caches reuse successful Basic BLS certificate checks, canonical commitment parsing, historical signing-quorum selection and mining-block locations across requests and overlapping ranges. Keys bind the complete verification inputs or exact block history; selection cache misses check the active branch under cs_main, and mining-location hits must belong to the request chain. Every request still checks its checkpoint, ancestry, inclusion paths, roots and budgets. No additional index, startup scan, persistent proof manager, or block-processing hooks are needed. Required historical blocks must be retained; missing/pruned data is an explicit error. Construction uses a fixed chain view, performs bulk disk reads and verification outside cs_main, and checks the target carrier (or live signed block) is still active before returning. The final certificate can come from the existing ChainLock manager before a later coinbase embeds it; historical handoffs still come from disk.

The trust model assumes historically authenticated ChainLock quorums remain honest. This verifier does not reconstruct DKG, full Core consensus, or exact active signer eligibility. No consensus/signing rules change, trusted setup, proof VM, GPU, or new P2P messages are introduced.

How Has This Been Tested?

Locally built on Apple ARM64 with the repository's prebuilt dependencies: full no-wallet build with the experimental shared kernel and linked dash-chainstate, plus a full wallet-enabled build. All 40 selected cases in llmq_chainlock_tests, quorum_proofs_tests, and validation_chainstatemanager_tests pass. Coverage includes disk-backed historical lookup across repeated/skipped signatures, shorter-chain requests, missing block data, real testnet proof roundtrip and tampering, resource limits, and multiple-chainstate lifecycle behavior.

rpc_help.py and feature_quorum_proof_chain.py pass with and without wallet support, including positional/named CLI arguments, mixed HTTP batches, malformed input, freshness, and restart checks. feature_llmq_chainlocks.py passes real multi-node ChainLock creation, historical RPC lookup, and its existing reorg/restart checks. Cppcheck, Python flake8/mypy, format strings, circular dependency, assertion, test-suite-name, changed-line formatting, and whitespace checks pass.

The shared real testnet fixture is 3,469 raw proof bytes; the matching SDK bootstrap with one quorum and one EvoNode is 4,506 bytes. Mining-transaction reference encodings of real testnet 90/180/366-day histories measured 85,827/159,536/314,357 gzip bytes, before final record openings. These are testnet observations, not mainnet guarantees. Real mainnet/testnet archive RPC generation is now benchmarked at 90/180/366-day spans in the performance report. All 66 measured requests verified; byte-identical proofs were returned across stock, profiling and cold-block-file runs. On an Apple M4 Max SSD, the year-long mainnet proof took 5.52 s with cold block files (0.33 s in block loading), and testnet took 24.61 s (0.36 s in block loading). Mainnet proof sizes were 38,732 / 73,800 / 151,070 gzip bytes. The memoization follow-up compares 90 real archive requests across baseline, signature/parsing-only and final implementations. Repeated year requests fall from 5.37 to 0.91 s mainnet and 24.59 to 1.53 s testnet; the first six-month query after a year query takes 0.61 / 0.78 s. First requests after restart remain 5.25 / 23.69 s. Every proof and bootstrap is byte-identical to the baseline. The final implementation builds with and without wallet support, passes all 51 selected unit tests plus proof RPC/help functional tests in both builds, and adds warmed-cache mutation and concurrent-verification regressions. The full-stack integration report records real mainnet/testnet Core → quorum-list-server → native SDK validation, including live Platform epoch queries with the default verified provider, year-long histories, and six rejected HTTP fault cases. This exposed and fixed SDK quorum-hash byte order, RPC transport timeouts, and availability of a final live ChainLock before its later coinbase carrier. Browser-to-live-server, Swift/FFI, the DAPI proof-serving route, and production deployment remain outside this run.

Breaking Changes

None to released interfaces or consensus. Generation supports mainnet/testnet; independent fixture verification is also testable on regtest.

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

@github-actions

github-actions Bot commented Jan 17, 2026 •

Copy link
Copy Markdown

✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.

@coderabbitai

coderabbitai Bot commented Jan 17, 2026 •

Copy link
Copy Markdown

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds a new LLMQ quorum-proof subsystem: public headers and implementation (src/llmq/quorumproofs.{h,cpp} and src/llmq/quorumproofdata.h) implementing CQuorumProofManager with chainlock indexing, Merkle proof construction/verification, proof-chain build/verify APIs, EvoDB persistence and migrations. Wires the manager into LLMQContext, CQuorumBlockProcessor, CSpecialTxProcessor/CChainstateHelper, and init migration logic. Exposes RPCs (getchainlockbyheight, getquorumproofchain, verifyquorumproofchain), new quorum-scanning/selection helpers, fast-path mined-commitment access, and adds unit, regression, and functional tests plus test runner entries.

Sequence Diagram(s)

sequenceDiagram
    participant Client as RPC Client
    participant RPC as getquorumproofchain
    participant ProofMgr as CQuorumProofManager
    participant EvoDB as CEvoDB
    participant QBProc as CQuorumBlockProcessor
    participant Chain as CChain

    Client->>RPC: Call getquorumproofchain(checkpoint, target)
    RPC->>ProofMgr: BuildProofChain(checkpoint, target, qman, chain, block_man)
    ProofMgr->>EvoDB: Read stored quorum/coinbase proof data
    ProofMgr->>QBProc: Fetch mined commitments / block metadata
    ProofMgr->>Chain: Traverse headers between checkpoint and targets
    ProofMgr->>ProofMgr: Construct Merkle & coinbase proofs per step
    ProofMgr-->>RPC: Return QuorumProofChain (JSON + hex)
    RPC-->>Client: Respond with proof
Loading
sequenceDiagram
    participant Client as RPC Client
    participant RPC as verifyquorumproofchain
    participant ProofMgr as CQuorumProofManager
    participant EvoDB as CEvoDB
    participant QBProc as CQuorumBlockProcessor
    participant Crypto as BLS Crypto

    Client->>RPC: Call verifyquorumproofchain(checkpoint, proof, expected)
    RPC->>ProofMgr: VerifyProofChain(checkpoint, proof, expected_llmq, expected_quorumHash)
    ProofMgr->>ProofMgr: Check header continuity, sizes, limits
    loop per proof element
        ProofMgr->>EvoDB: Optionally validate chainlock index entries
        ProofMgr->>QBProc: Verify commitments and Merkle roots against block data
        ProofMgr->>Crypto: Verify chainlock/quorum signatures and public keys
    end
    ProofMgr-->>RPC: Return QuorumProofVerifyResult (valid/error)
    RPC-->>Client: Respond with verification result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main feature: RPC support for mining-transaction snapshot proofs. It is concise and related to the quorum proof implementation and new proof RPCs.
Description check ✅ Passed The description directly explains the quorum proof feature, RPCs, verification behavior, security limits, testing, performance, and compatibility impact.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 2

🤖 Fix all issues with AI agents
In `@src/llmq/quorumproofs.cpp`:
- Around line 494-499: The code uses
step.quorum->m_quorum_base_block_index->GetAncestor(step.chainlockHeight) which
fails when step.chainlockHeight is ahead of the base block height; replace this
lookup with active_chain[step.chainlockHeight] (using the active_chain
parameter) to safely access the block at chainlockHeight with bounds checking;
update the block lookup in the function where FindChainlockCoveringBlock results
are used so it references active_chain[step.chainlockHeight] instead of
GetAncestor on m_quorum_base_block_index.

In `@src/llmq/quorumproofs.h`:
- Around line 1-6: Run the project's clang-format on the changed header to
resolve CI formatting failures: apply clang-format (or clang-format-diff) to
src/llmq/quorumproofs.h and reformat the file so it matches the repository style
(fix whitespace, alignment, include ordering, and brace/indent rules) and
re-stage the changes; the header guard BITCOIN_LLMQ_QUORUMPROOFS_H can be used
to locate the file and verify the corrected formatting.
🧹 Nitpick comments (7)
src/rpc/quorums.cpp (1)

1311-1330: Validate input object structure in ParseCheckpointFromRPC.

The helper function directly accesses keys like checkpointObj["block_hash"] and checkpointObj["chainlock_quorums"] without first verifying they exist. If a user provides a malformed checkpoint object missing required keys, this will throw a less informative exception.

Consider adding existence checks or using .find() with appropriate error messages for better RPC error handling.

♻️ Suggested improvement
 static llmq::QuorumCheckpoint ParseCheckpointFromRPC(const UniValue& checkpointObj)
 {
+    if (!checkpointObj.exists("block_hash") || !checkpointObj.exists("height") || 
+        !checkpointObj.exists("chainlock_quorums")) {
+        throw JSONRPCError(RPC_INVALID_PARAMETER, "Checkpoint must contain block_hash, height, and chainlock_quorums");
+    }
+
     llmq::QuorumCheckpoint checkpoint;
     checkpoint.blockHash = ParseHashV(checkpointObj["block_hash"], "block_hash");
     // ... rest unchanged
test/functional/feature_quorum_proof_chain.py (2)

49-78: Consider catching specific JSONRPCException instead of broad Exception.

The broad except Exception catches can mask unexpected failures. For RPC error handling in tests, catching JSONRPCException specifically would be more precise and help detect actual test failures vs expected "not found" responses.

♻️ Suggested improvement
+from test_framework.authproxy import JSONRPCException
+
 # In test_chainlock_index:
         for h in range(tip_height, 200, -1):
             try:
                 cl_info = self.nodes[0].getchainlockbyheight(h)
                 # ... success handling
-            except Exception:
+            except JSONRPCException:
                 continue

115-142: Consider removing or using the build_checkpoint helper.

The build_checkpoint method is defined but never called in the test. If it's intended for future use with getquorumproofchain/verifyquorumproofchain tests, consider either:

  1. Adding tests that exercise these RPCs using this helper, or
  2. Adding a TODO comment explaining the intended future use

Currently, the test only covers getchainlockbyheight but not the proof chain generation/verification RPCs.

Would you like me to help draft additional test cases for getquorumproofchain and verifyquorumproofchain RPCs?

src/test/quorum_proofs_tests.cpp (1)

199-224: Consider adding FromJson roundtrip verification.

The test verifies ToJson output structure but doesn't complete the roundtrip by parsing with FromJson. Consider adding a full roundtrip test to ensure JSON serialization is bidirectional.

💡 Suggested enhancement
     BOOST_CHECK_EQUAL(json["height"].getInt<int>(), 1000);
+
+    // Verify FromJson roundtrip
+    llmq::QuorumCheckpoint parsed = llmq::QuorumCheckpoint::FromJson(json);
+    BOOST_CHECK(parsed.blockHash == checkpoint.blockHash);
+    BOOST_CHECK_EQUAL(parsed.height, checkpoint.height);
+    BOOST_CHECK_EQUAL(parsed.chainlockQuorums.size(), checkpoint.chainlockQuorums.size());
 }
src/llmq/quorumproofs.cpp (2)

158-178: Consider adding JSON field existence validation.

FromJson directly accesses JSON fields without checking existence first. If a caller provides malformed JSON missing required fields, the error message may be unclear. Consider validating field presence.

💡 Suggested improvement
 QuorumCheckpoint QuorumCheckpoint::FromJson(const UniValue& obj)
 {
     QuorumCheckpoint checkpoint;
 
+    if (!obj.exists("blockHash") || !obj.exists("height") || !obj.exists("chainlockQuorums")) {
+        throw std::runtime_error("Missing required fields in checkpoint JSON");
+    }
+
     checkpoint.blockHash = uint256S(obj["blockHash"].get_str());
     checkpoint.height = obj["height"].getInt<int32_t>();

230-253: Consider consolidating duplicated merkle proof verification logic.

The static VerifyMerkleProof function duplicates the logic in QuorumMerkleProof::Verify. Consider having one call the other to reduce code duplication.

💡 Suggested refactor
 static bool VerifyMerkleProof(const uint256& leafHash,
                                const std::vector<uint256>& merklePath,
                                const std::vector<bool>& merklePathSide,
                                const uint256& expectedRoot)
 {
-    if (merklePath.size() != merklePathSide.size()) {
-        return false;
-    }
-
-    if (merklePath.size() > MAX_MERKLE_PATH_LENGTH) {
-        return false;
-    }
-
-    uint256 current = leafHash;
-    for (size_t i = 0; i < merklePath.size(); ++i) {
-        if (merklePathSide[i]) {
-            current = Hash(current, merklePath[i]);
-        } else {
-            current = Hash(merklePath[i], current);
-        }
-    }
-
-    return current == expectedRoot;
+    QuorumMerkleProof proof;
+    proof.merklePath = merklePath;
+    proof.merklePathSide = merklePathSide;
+    return proof.Verify(leafHash, expectedRoot);
 }
src/llmq/quorumproofs.h (1)

209-222: Move DB_CHAINLOCK_BY_HEIGHT to an anonymous namespace or make it inline.

The static const std::string in a header creates a separate copy in each translation unit that includes this header. For a string constant used as a DB key, this wastes memory. Consider using inline constexpr (C++17) or moving to an anonymous namespace in the .cpp file.

💡 Suggested fix

Move to the .cpp file within an anonymous namespace:

// In quorumproofs.cpp
namespace {
const std::string DB_CHAINLOCK_BY_HEIGHT = "q_clh";
} // anonymous namespace

Or if it must remain in the header (C++17):

-static const std::string DB_CHAINLOCK_BY_HEIGHT = "q_clh";
+inline constexpr std::string_view DB_CHAINLOCK_BY_HEIGHT = "q_clh";

Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/llmq/quorumproofs.h Outdated

@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: 2

🤖 Fix all issues with AI agents
In `@src/llmq/quorumproofs.cpp`:
- Around line 582-670: The header-continuity check against proof.headers assumes
consecutive blocks but BuildProofChain only supplies commitment blocks, and the
code does not tie each quorum proof to the chainlock-signed block; update the
proof verification so each QuorumCommitmentProof is anchored to its chainlock:
for each qProof, require chainlock.blockHash == header.GetHash() (using the
header selected for that qProof) instead of enforcing proof.headers are strictly
consecutive, or alternatively modify BuildProofChain to include all intermediate
headers up to chainlock.nHeight and keep the continuity check; ensure you change
the loop that checks proof.headers continuity and the place that fetches const
CBlockHeader& header = proof.headers[proofIdx] so it selects the header matching
chainlock.blockHash (or add headers in BuildProofChain) and verify
chainlock.blockHash equality before any merkle/signature checks.

In `@src/test/quorum_proofs_regression_tests.cpp`:
- Around line 147-188: The test adds two headers (chain.headers) but only one
quorum proof (chain.quorumProofs), causing VerifyProofChain to abort on a
headers/ proofs count mismatch; add a second llmq::QuorumCommitmentProof for
header2 so counts match. Create another qProof (copying the first
llmq::QuorumCommitmentProof setup used for qProof), give it a distinct
commitment.quorumHash (e.g., uint256::THREE or similar), set
qProof.chainlockIndex consistent with existing clEntry usage, assign a
coinbaseTx (CMutableTransaction mtx like before) and push_back this second
qProof into chain.quorumProofs so chain.headers.size() ==
chain.quorumProofs.size().
♻️ Duplicate comments (1)
src/llmq/quorumproofs.cpp (1)

496-499: Chainlock block lookup can return nullptr when the chainlock height is ahead of the base block.

Line 496-499 uses GetAncestor(...), which only walks backward; for chainlock heights greater than the quorum base block height this yields nullptr. Prefer looking up by height on active_chain (as already flagged).

🧹 Nitpick comments (2)
src/llmq/quorumproofs.cpp (1)

59-83: Avoid duplicate merkle-proof verification logic.

Line 59-83 and Line 231-254 implement the same hashing loop/DoS checks. Consider delegating to a single helper to prevent drift.

Also applies to: 231-254

test/functional/feature_quorum_proof_chain.py (1)

55-76: Avoid swallowing unexpected RPC errors.

Bare except Exception masks real failures in the scan loops; catching JSONRPCException keeps intent while preserving unexpected errors.

♻️ Suggested refinement (apply similarly to other loops)
-from test_framework.util import assert_equal, assert_raises_rpc_error
+from test_framework.util import assert_equal, assert_raises_rpc_error
+from test_framework.authproxy import JSONRPCException
@@
-            except Exception:
-                continue
+            except JSONRPCException as e:
+                if e.error.get("code") != -5:
+                    raise
+                self.log.debug(f"Height {h} not chainlocked yet: {e}")
+                continue
@@
-            except Exception:
-                continue
+            except JSONRPCException as e:
+                if e.error.get("code") != -5:
+                    raise
+                self.log.debug(f"Height {h} not chainlocked yet: {e}")
+                continue
@@
-        try:
-            cl_quorums = self.nodes[0].quorum("list", llmq_type)
-        except Exception:
-            # If quorum list fails, try with different type
-            cl_quorums = []
+        try:
+            cl_quorums = self.nodes[0].quorum("list", llmq_type)
+        except JSONRPCException as e:
+            self.log.debug(f"quorum list failed for type {llmq_type}: {e}")
+            cl_quorums = []
@@
-            except Exception:
-                continue
+            except JSONRPCException as e:
+                self.log.debug(f"quorum info failed for {qhash}: {e}")
+                continue

Also applies to: 86-95, 120-136

Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/test/quorum_proofs_regression_tests.cpp Outdated
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

current performance of proof generation:

==============================================
QUORUM PROOF CHAIN SCALING BENCHMARK

Target: LLMQ_100_67 quorum at height 2407200

Test | Time (s) | Steps | Size (bytes)
--------------------------+----------------------+----------------------+---------------------
~30 hours (683 blocks) | 0.125 ( -0.011) | 1 ( +0) | 1320 ( +0)
~7 days (4178 blocks) | 0.137 ( -0.022) | 4 ( +0) | 5489 ( -140)
~30 days (17430 blocks) | 0.187 ( -0.016) | 16 ( +0) | 22209 ( -307)
~6 months (103830 blocks) | 0.492 ( -0.080) | 89 ( +0) | 122701 ( +1114)
~12 months (210390 blocks) | 0.896 ( -0.194) | 181 ( +0) | 249494 ( +2139)

Legend: current (delta vs baseline) - negative delta = improvement

@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: 4

Caution

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

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

1-3: Fix clang-format diffs reported by CI.

The clang-format diff check is failing; please run the repo’s clang-format (or clang-format-diff) on the touched hunks and re‑stage.

🤖 Fix all issues with AI agents
In `@src/llmq/quorumproofs.cpp`:
- Around line 703-704: The ProofStep being pushed uses pProofBlock for the
mined-block pointer but documentation and fallback logic expect
ProofStep::pMinedBlockIndex to point to the block where the commitment was
mined; change the construction passed to proofSteps.push_back to use the mined
block pointer (the variable that represents the mined block for
currentCommitment) instead of pProofBlock so the fallback merkle proof builder
reads the correct block when cached data is missing (update the arguments around
proofSteps.push_back/currentCommitment to supply the mined-block index rather
than pProofBlock).
- Around line 53-85: ComputeSigningCommitmentIndex currently silently returns 0
when a rotated quorum's signer (computed from selectionHash and
llmq_params.signingActiveQuorumCount) is not found in commitments, which can
mis-attribute signers; update the rotated branch in
ComputeSigningCommitmentIndex to treat a missing quorumIndex as an explicit
failure: after computing signer, if no commitments[i].quorumIndex matches,
either throw a descriptive exception (e.g., std::runtime_error) or use an
explicit error return (e.g., return SIZE_MAX) and document that callers must
handle this error, and update any callers of ComputeSigningCommitmentIndex to
handle the new failure path; reference symbols: ComputeSigningCommitmentIndex,
llmq_params.useRotation, signingActiveQuorumCount, selectionHash,
commitments[i].quorumIndex.

In `@src/rpc/quorums.cpp`:
- Around line 1497-1499: The file src/rpc/quorums.cpp is failing clang-format;
run the formatter (e.g. clang-format-diff.py -p1 or clang-format) on the file
and apply the changes so the RPC registration lines for "evo" entries (functions
getchainlockbyheight, getquorumproofchain, verifyquorumproofchain) match the
project's style; update the file with the formatted whitespace/commas/alignment
and re-run CI to ensure clang-format diffs are resolved.
- Around line 1406-1470: The handler verifyquorumproofchain currently parses
expectedType and calls llmq_ctx.quorum_proof_manager->VerifyProofChain without
validating the LLMQ type; add a guard after parsing expectedType (the value
produced by static_cast<Consensus::LLMQType>(request.params[3].getInt<int>()))
to ensure it is a known/defined LLMQ type and return a clear RPC error
(valid=false with an explanatory message or throw RPC_INVALID_PARAMETER) if it
is not, before calling VerifyProofChain on proofChain/checkpoint.
♻️ Duplicate comments (1)
src/llmq/quorumproofs.cpp (1)

851-910: Header continuity check conflicts with proof layout; chainlocks aren’t anchored to headers.

The headers here are the commitment-mined blocks, which are typically not consecutive, so the strict prevBlockHash chain will reject multi-step proofs. Also, the chainlock signature isn’t tied to any header hash, so an unrelated header chain could still satisfy the merkle proofs. Consider either including intermediate headers up to the chainlock block, or anchoring each proof by requiring chainlock.blockHash == header.GetHash() and adjusting generation/verification accordingly.

🧹 Nitpick comments (2)
src/llmq/blockprocessor.cpp (1)

167-211: Consider consolidating the merkle-path helper.

This helper is duplicated in src/llmq/quorumproofs.cpp. Extracting a single shared implementation will reduce the risk of subtle divergence later.

src/llmq/quorumproofs.h (1)

8-18: Make the header self-contained for UniValue / std::map / std::string.

These types are used directly but the header doesn’t include their declarations. If they aren’t pulled transitively, this header won’t compile on its own. Consider adding explicit includes (or a UniValue forward declaration if you prefer to keep the include light).

🛠️ Proposed fix
 `#include` <llmq/types.h>
 `#include` <primitives/block.h>
 `#include` <primitives/transaction.h>
 `#include` <serialize.h>
 `#include` <uint256.h>
+#include <univalue.h>
 
+#include <map>
+#include <string>
 `#include` <set>
 `#include` <vector>

Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/rpc/quorums.cpp Outdated
Comment thread src/rpc/quorums.cpp

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

Some nits, haven't reviewed logic yet

Comment thread src/evo/specialtxman.cpp Outdated
Comment thread src/evo/specialtxman.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread test/functional/feature_quorum_proof_chain.py Outdated
Comment thread test/functional/feature_quorum_proof_chain.py Outdated
Comment thread test/functional/feature_quorum_proof_chain.py Outdated
Comment thread src/rpc/quorums.cpp Outdated
Comment thread src/rpc/quorums.cpp Outdated
Comment thread src/rpc/quorums.cpp Outdated
Comment thread src/rpc/quorums.cpp Outdated

@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

🤖 Fix all issues with AI agents
In `@src/llmq/quorumproofs.cpp`:
- Around line 867-897: Ensure the header at proof.headers[proofIdx] actually
matches the chainlock's signed block hash by checking header.GetHash() ==
chainlock.blockHash before using it for merkle verification; if the check fails,
set result.error (e.g. "Header does not match chainlock block hash in proof %d")
and return result. Insert this validation immediately after obtaining const
CBlockHeader& header = proof.headers[proofIdx] and prior to any merkle proof
verifications (coinbase and quorum commitment).
🧹 Nitpick comments (7)
src/llmq/quorumproofs.cpp (3)

1-1: Nit: Copyright year should be 2026.

As noted in a past review comment, the current date is January 2026, but the copyright header says 2025.

🔧 Suggested fix
-// Copyright (c) 2025 The Dash Core developers
+// Copyright (c) 2026 The Dash Core developers

294-338: Consider extracting BuildMerkleProofPath to a shared utility.

This function is duplicated verbatim from src/llmq/blockprocessor.cpp (lines 170-210). Consider moving it to a shared header (e.g., src/consensus/merkle.h or a new src/llmq/merkle_utils.h) to avoid duplication and ensure both implementations stay in sync.


1156-1160: Progress percentage calculation is a rough estimate.

The progress calculation indexed_count / 10 is a rough estimate that may not reflect actual progress. For example, if there are 500 total quorums, progress would cap at ~50% before completing. Consider tracking the total count upfront for accurate progress display, or document that this is an approximate indicator.

test/functional/feature_quorum_proof_chain.py (3)

114-141: build_checkpoint helper is defined but never called.

The build_checkpoint method is implemented but not used in run_test. If this is intended for future test expansion (e.g., testing getquorumproofchain/verifyquorumproofchain), consider adding a TODO comment. Otherwise, it could be removed to avoid dead code.


55-77: Consider catching specific JSONRPCException instead of bare Exception.

The try-except-continue pattern is common in test iteration, but catching a specific exception type would be more precise and avoid masking unexpected errors:

🔧 Suggested improvement
+from test_framework.authproxy import JSONRPCException
 ...
         for h in range(tip_height, 0, -1):
             try:
                 cl_info = self.nodes[0].getchainlockbyheight(h)
                 ...
                 return
-            except Exception:
+            except JSONRPCException:
                 continue

43-46: Consider adding tests for getquorumproofchain and verifyquorumproofchain RPCs.

The functional test covers getchainlockbyheight but not the proof chain generation/verification RPCs. The build_checkpoint helper suggests these were planned. Adding coverage would validate the end-to-end proof chain workflow.

Would you like me to help draft additional test cases for these RPCs?

src/rpc/quorums.cpp (1)

1376-1381: Inconsistent LLMQ type parsing between RPCs.

getquorumproofchain uses getInt<int>() (line 1377) while verifyquorumproofchain uses getInt<uint8_t>() (line 1462) for parsing the LLMQ type. Since LLMQType is enum class LLMQType : uint8_t, consider using uint8_t consistently:

🔧 Suggested fix
-    const Consensus::LLMQType targetType = static_cast<Consensus::LLMQType>(request.params[2].getInt<int>());
+    const Consensus::LLMQType targetType = static_cast<Consensus::LLMQType>(request.params[2].getInt<uint8_t>());

Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/llmq/blockprocessor.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/rpc/quorums.cpp Outdated
Comment thread src/rpc/quorums.cpp Outdated
Comment thread src/rpc/quorums.cpp
@@ -1246,6 +1247,232 @@ static RPCHelpMan submitchainlock()
}


static RPCHelpMan getchainlockbyheight()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

let's maybe split this RPC (and chainlock index) into its own PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I guess we could; but we need the chain lock index to support this behavior

@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: 4

🤖 Fix all issues with AI agents
In `@src/rpc/quorums.cpp`:
- Around line 1350-1358: The RPC help entries in the RPCResult construction use
snake_case keys (e.g., "quorum_proofs", "proof_hex") that don't match the actual
JSON output (camelCase like quorumProofs, proofHex, quorumPublicKey); update the
RPCResult key strings inside the RPC response description to use the camelCase
names emitted by the implementation (adjust entries such as
"quorum_proofs"->"quorumProofs", "proof_hex"->"proofHex", and any similar keys
like "quorum_public_key"->"quorumPublicKey"), and make the same changes for the
other occurrence mentioned (the block around the later entries referenced) so
the help text matches the real JSON field names.

In `@test/functional/feature_quorum_proof_chain.py`:
- Line 364: The log call uses an unnecessary f-string prefix in the message;
update the call to self.log.info by removing the leading "f" so the literal
string "Mining blocks to embed chainlock signatures..." is passed (locate the
self.log.info(...) invocation in the
test/functional/feature_quorum_proof_chain.py file and replace the f-string with
a plain string).
- Around line 329-337: Adjust the indentation of the two assert_raises_rpc_error
calls so they align correctly with the surrounding code to satisfy flake8 E128;
locate the lines calling self.nodes[0].getquorumproofchain with parameters
(checkpoint, checkpoint['chainlock_quorums'][0]['quorum_hash'], 999) and
(checkpoint, fake_hash, llmq_type) and re-indent the continued argument lines to
align under the first argument of each function call (keeping the same
arguments: checkpoint, quorum hash / fake_hash, llmq_type) so the wrapped
parameters are vertically aligned.
- Around line 12-14: Replace broad except Exception handlers used around RPC
"scan" calls with a specific except JSONRPCException to only catch the expected
"not found" RPC error; import JSONRPCException from test_framework.authproxy (or
the project's authproxy module) and in those except blocks bind the exception
(e.g., except JSONRPCException as e:) to assert or check the error message,
while re-raising or letting other unexpected exceptions propagate. Apply this
change to the handlers referenced (the import area and the try/except blocks
around the scan RPC in the ranges shown: the import block near the top and the
try/except blocks currently at 94-115 and 125-134), ensuring unexpected
exceptions are not swallowed.
♻️ Duplicate comments (1)
src/llmq/quorumproofs.cpp (1)

759-772: Bind the header to the chainlock’s signed block hash.

Right now the proof can pair a valid chainlock signature for block A with an unrelated header/merkle root for block B. Validate the header hash matches chainlock.blockHash before merkle proof checks.

🛠️ Suggested guard
         const CBlockHeader& header = proof.headers[proofIdx];
+        if (header.GetHash() != chainlock.blockHash) {
+            result.error = strprintf("Header does not match chainlock block hash in proof %d", proofIdx);
+            return result;
+        }
🧹 Nitpick comments (1)
src/llmq/blockprocessor.cpp (1)

606-634: Fast-path hash assumes SER_DISK == SER_GETHASH — add a guard/test.

If CFinalCommitment serialization ever diverges, this path would silently compute a different hash than SerializeHash and poison proofs. Consider a debug-only assertion or a unit test to lock the invariant. If you use the assert, add <cassert> if it's not already included.

🛠️ Suggested debug guard
-            return Hash(MakeByteSpan(ssValue).first(ssValue.size() - 32));
+            const uint256 fast_hash = Hash(MakeByteSpan(ssValue).first(ssValue.size() - 32));
+#ifdef DEBUG
+            auto [commitment, _] = GetMinedCommitment(llmqType, quorumHash);
+            if (!commitment.IsNull()) {
+                assert(fast_hash == ::SerializeHash(commitment));
+            }
+#endif
+            return fast_hash;

Comment thread src/rpc/quorums.cpp Outdated
Comment thread test/functional/feature_quorum_proof_chain.py Outdated
Comment thread test/functional/feature_quorum_proof_chain.py Outdated
Comment thread test/functional/feature_quorum_proof_chain.py Outdated
@github-actions

github-actions Bot commented Feb 3, 2026

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

PastaPastaPasta added a commit to PastaPastaPasta/dash that referenced this pull request Mar 29, 2026
- Change ComputeSigningCommitmentIndex to return std::optional<size_t>
  to avoid silent fallback that could mis-attribute signers
- Remove unnecessary fallback path in BuildProofChain (migration ensures
  all historical commitments are indexed)
- Remove legacy header continuity check that incorrectly assumed
  consecutive blocks (headers are from commitment blocks spaced by DKG
  intervals)
- Add LLMQ type validation in verifyquorumproofchain RPC
- Use uint8_t for LLMQ type cast (matches enum class : uint8_t)
- Reduce cs_main lock scope using WITH_LOCK
- Fix /*optional=*/ syntax and RPC example placeholder
- Change int32_t to int for chainlockedHeight (style consistency)
- Update regression test for count mismatch validation
- Fix functional test params (5,3) and remove unnecessary delay

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta force-pushed the platform-sdk-compact-proof branch from e7a1527 to e4343c7 Compare March 29, 2026 15:16
@PastaPastaPasta
PastaPastaPasta force-pushed the platform-sdk-compact-proof branch from e4343c7 to 17297dc Compare March 29, 2026 16:20
@thepastaclaw

thepastaclaw commented Mar 30, 2026 •

Copy link
Copy Markdown
Collaborator

⚠️ DEGRADED — Queued for automated review — 19th in line, estimated start in ~4 h (commit 725f722)
Estimated review time once started: ~25 min (two-phase automated review; median of recent runs).
The primary review models are currently out of quota; this review will run on stand-in models and be marked as degraded.

  • Request priority review — click to move this review to the front of the queue.

@github-actions

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

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

Code Review

Two blocking issues in the new trustless quorum proof chain: (1) chainlock signature verification is deduplicated by height only, allowing an attacker to splice a forged ChainlockProofEntry that shares an nHeight with a legitimate entry and bypass BLS verification entirely; (2) the chainlock index is keyed only by chainlocked height and is unconditionally erased on disconnect, so reorgs can drop entries still referenced by active blocks. Several lower-severity concerns also need attention: the verifier ignores the checkpoint anchor and never links headers, signingQuorumType is taken from untrusted input, and a migration assert can abort the node on corrupt data.

🔴 2 blocking | 🟡 4 suggestion(s) | 💬 2 nitpick(s)

Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/evo/specialtxman.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Comment thread src/test/quorum_proofs_regression_tests.cpp Outdated
Comment thread src/llmq/quorumproofs.h Outdated
Comment thread src/llmq/quorumproofs.cpp Outdated
Use exponential bracketing and binary search over the certified-height ordering enforced by CheckCbTxBestChainlock. Preserve the request-local cache, disk-read budget and explicit unavailable-data errors.

Keep null carriers only before the first certificate in the fixture, and cover a repeated-certificate span longer than the old linear scan budget.
A checkpoint can retire a quorum that signing selection still uses for the next seven blocks. Return an explicit unavailable route from the builder for this case and try later certificates inside the original bounded target window.

Keep missing data and other construction errors fatal, and preserve latest-certificate semantics for an omitted height. Add a disk-backed generation regression exercising the RPC, signing offset, later-target verification, exhausted history and unavailable checkpoint data.
@PastaPastaPasta
PastaPastaPasta force-pushed the platform-sdk-compact-proof branch from cfa4aa3 to 8f7d38c Compare September 16, 2026 02:55
@thepastaclaw thepastaclaw removed the pastaclaw:changes-requested thepastaclaw's latest review requested changes label Sep 16, 2026
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

Addressed the latest review at 8f7d38c:

  • Retry later proof targets when the signing offset selects a quorum retired at the checkpoint; keep the original search window and explicit data/construction errors.
  • Use bounded exponential/binary search for historical certificates; add long repeated-certificate coverage and a generation-level mining-boundary regression.
  • Move RPC-dependent functional coverage into the RPC introduction commit and add the DIP Translations cleanup. #175 proof-layering/trust-model rationale to implementation commit 02f1303.

Validation: full local debug/wallet build; all 46 selected ChainLock, proof and chainstate-manager unit cases; proof RPC, RPC-help and multi-node ChainLocks functional tests; cppcheck, changed-line clang-format, includes/guards, assertions, test-suite names, circular dependencies, format strings and whitespace. The proof functional test initially hit a local RPC-port collision, then caught an error-message regression which is fixed; the final run passes. History repair preserves the exact tested final tree, and all eight commits have valid signatures.

@thepastaclaw please re-review the current head. I am monitoring CI and follow-up feedback.


🤖 Posted autonomously by Codex on behalf of pasta.

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

Re-review — Final validation — Phase 2 only (queue backlog)

The implementation addresses the previously reported proof-generation, historical lookup, parsing, locking, and test-ordering issues. One security issue remains in proof verification: the proof authenticates the initial commitment under the trusted quorum root but does not prove that later mining evidence is after or descended from the trusted checkpoint. The process-wide chain snapshot cache also retains raw block-index pointers without an explicit chainstate-lifetime key.

🔴 1 blocking | 🟡 4 suggestion(s)

3 finding(s) not shown inline (the lines are not part of this PR's diff)

🟡 Suggestion: Fold the network-independent parsing repair into the proof introduction
<commit:569dbb0364>:1

Commit 569dbb0364 repairs ParseCommitment, which was introduced by 02f1303da9, and adjusts the corresponding fixture. Because this is a correction to the proof layer rather than a separate feature, fold it into 02f1303da9 so the proof implementation is independently usable at the commit that introduces it.

source: gpt-6-astra (phase2-reviewer: dash-core-commit-history)

🟡 Suggestion: Squash the CLI conversion fix into the RPC introduction
<commit:aa92b2b2bf>:1

Commit aa92b2b2bf corrects argument conversion introduced by d251014485 and adds coverage for that same RPC interface. Fold it into d251014485, retaining the positional and named CLI checks, so the RPC is introduced with its intended argument handling rather than through a later repair commit.

source: gpt-6-astra (phase2-reviewer: dash-core-commit-history)

🟡 Suggestion: Fold the target-selection repair into the RPC feature commit
<commit:8f7d38c402>:1

Commit 8f7d38c402 repairs minimum-height target selection introduced by d251014485, including the builder API adjustment and regression coverage. Fold it into d251014485 while retaining the useful retirement/signing-offset rationale, so the RPC feature does not enter permanent history with a known target-selection defect repaired only by a later commit.

source: gpt-6-astra (phase2-reviewer: dash-core-commit-history)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 2: gpt-6-astra (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: critical by gpt-6-astra (effort low) — The large, intricate diff adds cryptographic BLS/X11 certificate verification, Merkle-proof and peer-facing RPC deserialization paths in src/llmq/quorumproofs.cpp and src/rpc/quorums.cpp, directly changing critical signature and network-input handling surfaces.
  • Phase 1 reviewers: not run (skipped for throughput: 45 PRs queued, above the 10 limit)
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — dash-core-commit-history (completed, effort xhigh); agent phase2-reviewer
🤖 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/rpc/quorums.cpp`:
- [SUGGESTION] src/rpc/quorums.cpp:49-64: The global chain snapshot cache can retain block-index pointers across chainstate lifetimes
  `g_proof_chain_snapshot` stores a `CChain` containing raw `CBlockIndex*` values, while invalidation is based only on the current tip pointer. The cache is process-wide and is not cleared when a `ChainstateManager` is destroyed. The chainstate-manager tests exercise restart behavior, but they do not establish that this independent RPC cache cannot retain pointers into a destroyed block index. If a later chainstate reuses the same tip address, `GetProofChainSnapshot()` can return the old snapshot and subsequent RPC code can dereference stale block-index pointers. Key the cache by the owning chain manager or block manager identity as well as the tip, or clear the cache as part of chainstate teardown.

In `<commit:569dbb0364>`:
- [SUGGESTION] <commit:569dbb0364>:1: Fold the network-independent parsing repair into the proof introduction
  Commit `569dbb0364` repairs `ParseCommitment`, which was introduced by `02f1303da9`, and adjusts the corresponding fixture. Because this is a correction to the proof layer rather than a separate feature, fold it into `02f1303da9` so the proof implementation is independently usable at the commit that introduces it.

In `<commit:aa92b2b2bf>`:
- [SUGGESTION] <commit:aa92b2b2bf>:1: Squash the CLI conversion fix into the RPC introduction
  Commit `aa92b2b2bf` corrects argument conversion introduced by `d251014485` and adds coverage for that same RPC interface. Fold it into `d251014485`, retaining the positional and named CLI checks, so the RPC is introduced with its intended argument handling rather than through a later repair commit.

In `<commit:8f7d38c402>`:
- [SUGGESTION] <commit:8f7d38c402>:1: Fold the target-selection repair into the RPC feature commit
  Commit `8f7d38c402` repairs minimum-height target selection introduced by `d251014485`, including the builder API adjustment and regression coverage. Fold it into `d251014485` while retaining the useful retirement/signing-offset rationale, so the RPC feature does not enter permanent history with a known target-selection defect repaired only by a later commit.

In `src/llmq/quorumproofs.cpp`:
- [BLOCKING] src/llmq/quorumproofs.cpp:315-337: Proof verification never links the certificate chain to the trusted checkpoint
  (existing thread: https://github.com/dashpay/dash/pull/7107#discussion_r4014543097)
  `Verify()` verifies the seed commitment against `anchor.quorumRoot`, then verifies each certificate and only the local continuity between that certificate header and its quorum-mining transaction. It never requires the first mining header, or any ancestor header, to descend from `anchor.blockHash`, and it does not require an introduced commitment's mining height to be after `anchor.height`. A proof can therefore introduce a commitment mined before the trusted checkpoint and use a later valid certificate for it, without proving that this handoff belongs to the history represented by the trusted snapshot. The generator rejects mining blocks at or before the checkpoint, but an independent verifier must enforce the same invariant; otherwise proof soundness depends on the generator being honest rather than on the proof itself. Add an explicit checkpoint-to-mining-history binding or encode and verify the required post-checkpoint boundary in each handoff.

Comment thread src/rpc/quorums.cpp
Comment on lines 49 to +64
using node::NodeContext;

namespace {
std::mutex g_proof_chain_mutex;
std::shared_ptr<const CChain> g_proof_chain_snapshot;
const CBlockIndex* g_proof_chain_tip{nullptr};

std::shared_ptr<const CChain> GetProofChainSnapshot(CBlockIndex* tip)
{
std::lock_guard lock(g_proof_chain_mutex);
if (!g_proof_chain_snapshot || g_proof_chain_tip != tip) {
auto snapshot = std::make_shared<CChain>();
snapshot->SetTip(*tip);
g_proof_chain_snapshot = std::move(snapshot);
g_proof_chain_tip = tip;
}

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.

🟡 Suggestion: The global chain snapshot cache can retain block-index pointers across chainstate lifetimes

g_proof_chain_snapshot stores a CChain containing raw CBlockIndex* values, while invalidation is based only on the current tip pointer. The cache is process-wide and is not cleared when a ChainstateManager is destroyed. The chainstate-manager tests exercise restart behavior, but they do not establish that this independent RPC cache cannot retain pointers into a destroyed block index. If a later chainstate reuses the same tip address, GetProofChainSnapshot() can return the old snapshot and subsequent RPC code can dereference stale block-index pointers. Key the cache by the owning chain manager or block manager identity as well as the tip, or clear the cache as part of chainstate teardown.

source: gpt-6-astra (phase2-reviewer: general)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 5885d7b. The proof-chain snapshot cache is now scoped by both the owning ChainstateManager and active-chain tip, so a snapshot from a prior chainstate cannot be reused by a later manager. The change builds successfully with make -j2 src/dashd.


🤖 Posted autonomously by Codex on behalf of pasta.

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.

Withdrawn (re-reviewed at 5885d7ba): This finding did not survive verification on the current head.

@thepastaclaw thepastaclaw added the pastaclaw:changes-requested thepastaclaw's latest review requested changes label Sep 16, 2026
@thepastaclaw thepastaclaw removed the pastaclaw:changes-requested thepastaclaw's latest review requested changes label Sep 16, 2026

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

Re-review — Final validation — Phase 2 only (queue backlog)

The implementation addresses the historical lookup, cache lifetime, rotated-quorum continuation, RPC parsing, and prior performance defects. One blocking security issue remains: proof verification accepts a trusted checkpoint object but does not cryptographically connect the checkpoint block hash to the supplied header/certificate chain, relying instead on an undocumented implicit no-disjoint-ChainLocks assumption.

🔴 1 blocking

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 2: gpt-6-astra (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: critical by gpt-6-astra (effort low) — This is a large, intricate change that directly modifies BLS certificate verification, ChainLock signature handling, quorum key authentication, peer-facing proof serialization/deserialization, and historical proof storage/retrieval across core RPC and LLMQ code.
  • Phase 1 reviewers: not run (skipped for throughput: 15 PRs queued, above the 10 limit)
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — dash-core-commit-history (completed, effort xhigh); agent phase2-reviewer
🤖 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/llmq/quorumproofs.cpp`:
- [BLOCKING] src/llmq/quorumproofs.cpp:325-337: Proof verification never links the certificate chain to the trusted checkpoint
  (existing thread: https://github.com/dashpay/dash/pull/7107#discussion_r4014543097)
  `Verify()` checks that the supplied anchor equals the trusted checkpoint and validates the trusted quorum root, but `anchor.blockHash` is only required to be present and is never compared with the supplied headers or an ancestry bridge. Each link validates its own local header continuity, but the first link is not required to descend from the checkpoint and successive links are not required to share one continuous header chain. Consequently, a proof assembled from valid ChainLock certificates and commitment transactions on a different branch can satisfy the verifier while presenting a checkpoint block hash that is merely metadata. The PR description explicitly presents this as authentication from an independently pinned Core snapshot; the stated assumption that historically authenticated quorums remain honest does not itself make the checkpoint-to-proof relationship an enforced invariant. Include a verifiable header bridge from `anchor.blockHash` to the first proof link and enforce continuity across subsequent links, or change the proof/trust-model documentation so the checkpoint block hash is not represented as an authenticated input.

@thepastaclaw thepastaclaw added the pastaclaw:changes-requested thepastaclaw's latest review requested changes label Sep 16, 2026
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

"blocker" from thepastaclaw is the expected design. more of a review system bug it seems

QuorumProofChain::Verify required a mined quorum commitment to have no
inputs, no outputs, a zero lock time and exactly transaction version 3,
and Coinbase() capped the coinbase at 4,096 outputs. Consensus requires
none of this: CheckTransaction only allows rather than requires empty
vin/vout for TRANSACTION_QUORUM_COMMITMENT, IsSpecialTxVersion() accepts
any version at or above 3, nothing reads a mined commitment's lock time,
and a coinbase is bounded only by MAX_STANDARD_TX_SIZE.

A miner running custom software could therefore mine a consensus-valid
commitment that no proof can carry, at no cost and indistinguishably
from an honest block. A quorum's commitment is mined exactly once and a
second non-null commitment for the same quorum is rejected, so that
quorum's key could never be proven again: every handoff that needs it
fails for getquorumproofchain and for independent verifiers alike.

The transaction is already bound to the ChainLock-signed header by its
Merkle path, so these checks add no security. Keep the payload type,
version and height checks, the coinbase scriptSig bounds consensus does
enforce, and the 100,000 byte limit, which matches MAX_STANDARD_TX_SIZE.

DIP dashpay#175 steps 5 and 7 state the same restrictions and need the matching
update so independent verifiers stay in agreement with Core.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@UdjinM6

UdjinM6 commented Sep 18, 2026

Copy link
Copy Markdown

pls see UdjinM6@725f722

@thepastaclaw thepastaclaw removed the pastaclaw:changes-requested thepastaclaw's latest review requested changes label Sep 18, 2026

@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 725f722

pls make sure to update the DIP draft too

--- a/dip-pasta-compact-quorum-proofs.md
+++ b/dip-pasta-compact-quorum-proofs.md
@@ -278,11 +278,15 @@ (step 5, "Verify the mining transaction")
    The transaction index must be nonzero, and ancestor count must be less than
-   certificate height. Require a complete, canonical v3 quorum-commitment
-   transaction with no inputs or outputs, zero locktime, and a v1 payload whose
-   height equals certificate height minus ancestor count.
+   certificate height. Require a complete, canonical quorum-commitment
+   transaction of special version 3 or above, with a v1 payload whose height
+   equals certificate height minus ancestor count. Its inputs, outputs and
+   locktime MUST NOT be constrained: consensus does not restrict them, and the
+   Merkle path already binds the transaction to the signed header.
@@ -291,10 +295,12 @@ (step 7, "Verify the target block and coinbase")
    coinbase's Merkle path at transaction index zero against the signed header.
-   Require a complete v3 coinbase transaction with one coinbase input, a scriptSig
-   of 1–100 bytes, and 1–4,096 outputs. Its payload must be v3, with height equal
+   Require a complete coinbase transaction of special version 3 or above with one
+   coinbase input, a scriptSig of 1–100 bytes, and at least one output. The
+   output count MUST NOT be capped beyond the transaction blob limit, which
+   consensus also enforces. Its payload must be v3 or above, with height equal
    to the signed height, `bestCLHeightDiff` less than that height, and a nonzero
    quorum root. Read the quorum and masternode roots from this payload.

PastaPastaPasta added a commit to PastaPastaPasta/dips that referenced this pull request Sep 18, 2026
Steps 5 and 7 required a mined quorum commitment to have no inputs, no
outputs and a zero locktime, pinned both the commitment and the coinbase
to exactly special version 3, and capped the coinbase at 4,096 outputs.
Consensus requires none of this: CheckTransaction only allows rather
than requires empty vin/vout for a quorum commitment, a special version
is any version at or above 3, nothing reads a mined commitment's
locktime, and a coinbase output count is bounded only by transaction
size.

An independent verifier written to the old text would therefore reject
proofs Dash Core accepts, and a miner running custom software could mine
a consensus-valid commitment that no proof could carry, at no cost and
indistinguishably from an honest block. A quorum's commitment is mined
exactly once, so that quorum's key could never be proven again.

The Merkle path already binds each transaction to the ChainLock-signed
header, so the dropped constraints added no security. The payload type,
version and height checks, the scriptSig bounds consensus does enforce,
and the transaction blob limit all stay.

Matches dashpay/dash#7107, so Core and independent verifiers stay in
agreement. The wire format is unchanged and the committed test vector is
unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

Thanks for the utACK and for writing out the spec diff.

DIP draft updated in dashpay/dips#175 as 7303d03 — your diff applied as given, so steps 5 and 7 now match this PR:

  • Step 5 no longer constrains the mined commitment's inputs, outputs or locktime, and accepts any special version at or above 3.
  • Step 7 accepts a coinbase of special version 3 or above with at least one output, with no cap beyond the transaction blob limit, and a payload of v3 or above.

I cross-checked the new wording against the code rather than just applying the text:

DIP wording Code
commitment "special version 3 or above" tx->IsSpecialTxVersion(), i.e. nVersion >= SPECIAL_VERSION (3)
commitment "a v1 payload" payload->nVersion == 1 (unchanged, so left exact)
coinbase "scriptSig of 1–100 bytes" !scriptSig.empty() && scriptSig.size() <= 100
coinbase "at least one output" !tx->vout.empty()
coinbase payload "v3 or above" payload->nVersion >= CCbTx::Version::CLSIG_AND_BALANCE, where CLSIG_AND_BALANCE = 3
"transaction blob limit" the retained 100,000-byte check, matching MAX_STANDARD_TX_SIZE{100000} and the DIP's existing "Transaction blob

The wire format is unchanged — only validation strictness was relaxed — so the committed test-vector.json and bootstrap.bin are unaffected, and real_testnet_wire_and_crypto still passes.

On validation of 725f722 here: built clean, and quorum_proofs_tests / evo_deterministicmns_tests / block_reward_reallocation_tests pass (13 cases). I also confirmed the new test is a real regression test by reverting quorumproofs.cpp alone and keeping the test — it fails on quorum transaction envelope.


🤖 Posted autonomously by Codex on behalf of pasta.

@PastaPastaPasta
PastaPastaPasta merged commit e88aba8 into dashpay:develop Sep 18, 2026
31 checks passed
@PastaPastaPasta
PastaPastaPasta deleted the platform-sdk-compact-proof branch September 18, 2026 21:00
@UdjinM6 UdjinM6 added this to the 24 milestone Sep 18, 2026
knst added a commit that referenced this pull request Sep 21, 2026
cf70026 lint: drop top-level const from ExpectedType's return type (pasta)
b7ca886 lint: reserve vector capacity before push_back loops (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  `develop` currently fails the `linux64_multiprocess-build / Build source` → "Run linters" step (`ci/dash/lint-tidy.sh`).

  #7693 backported bitcoin#26905, which replaced the explicit `WarningsAsErrors` allowlist in `src/.clang-tidy` with `WarningsAsErrors: '*'` (and added a matching `-warnings-as-errors=-clang-diagnostic-old-style-cast,-google-readability-casting` to `ci/dash/lint-tidy.sh` so the C-style-cast checks stay advisory). Every other enabled check is now a hard error, which promoted five pre-existing diagnostics from warnings to build failures:

  ```
  llmq/quorumproofs.cpp:220:38: error: 'push_back' is called inside a loop [performance-inefficient-vector-operation]
  llmq/quorumproofs.cpp:405:43: error: 'push_back' is called inside a loop [performance-inefficient-vector-operation]
  rpc/quorums.cpp:1569:25:     error: 'push_back' is called inside a loop [performance-inefficient-vector-operation]
  rpc/output_script.cpp:133:17: error: 'push_back' is called inside a loop [performance-inefficient-vector-operation]
  rpc/util.cpp:890:1:          error: return type 'const std::optional<UniValue::VType>' is 'const'-qualified at the top level [readability-const-return-type]
  ```

  The three quorum-proof sites came in with #7107; the two `rpc/` sites are older code that was only ever a warning before.

  The failure is not visible on PRs branched before #7693 merged, because CI checks out the PR head and therefore uses that branch's `.clang-tidy`. It shows up on any PR whose merge base is at or after #7693 — e.g. https://github.com/dashpay/dash/actions/runs/35530545254/job/106137443857.

  ## What was done?

  - `hashes.reserve(...)` / `leaves.reserve(...)` / `pubkeys.reserve(...)` ahead of the four flagged loops. The sizes are known up front in every case, so this is also a small genuine improvement.
  - Dropped the top-level `const` from `ExpectedType`'s by-value return type in `src/rpc/util.cpp`. It never conferred anything.

  No behavior changes.

  ## How Has This Been Tested?

  Reproduced the failure and verified the fix locally with clang-tidy 19 (same major version as CI's `LLVM_VERSION`), running the repo's `src/.clang-tidy` config against a compilation database for the four affected translation units:

  - Before: the two `llmq/quorumproofs.cpp` errors reproduce verbatim, including the `[performance-inefficient-vector-operation,-warnings-as-errors]` tag from CI.
  - After: zero `performance-inefficient-vector-operation` and zero `readability-const-return-type` diagnostics across all four files.

  All four translation units compile clean (`rpc/util.cpp`, `rpc/output_script.cpp`, `rpc/quorums.cpp`, `llmq/quorumproofs.cpp`) on an `aarch64-apple-darwin` depends build. Full CI will cover the rest.

  ## Breaking Changes

  None.

  ## Checklist:
  - [x] 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
  - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_

  🤖 Generated with [Claude Code](https://claude.com/claude-code)

ACKs for top commit:
  knst:
    utACK cf70026

Tree-SHA512: f8394e31839ff2fdca2da7d515a29b7e28f252e457be8a55678098e79abfdc25d36aaa05cdf78bc8edd066c4ef354cc0d7cf814873e3e92dfa245c195a34b96c
PastaPastaPasta added a commit that referenced this pull request Sep 23, 2026
0363826 doc: add release notes for proof generation caching (pasta)
1988be2 perf(rpc): reuse active quorum sets and verify the finished proof once (pasta)
e8904c2 fix(llmq): make proof caches independent of process-wide BLS state (pasta)
c9a20b7 perf(llmq): memoize mining transactions, payload parsing and signer selection (pasta)
37f5af9 perf(chainlock): memoize coinbase ChainLocks and extend the proof chain snapshot (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  `getquorumproofchain` (#7107) is what Platform SDK clients, the quorum list server and eventually DAPI call to get a verifiable Platform quorum key. On mainnet a request from the SDK's embedded checkpoint (block 2,400,000, about 143k blocks behind the tip) took 1.9–2.4 s on every call, even when the same proof had just been built. On testnet it was 2.7 s from the SDK checkpoint and grew with the checkpoint's age. The node answers the same few requests for every client at a given tip, so this is repeated work.

  Profiling a warm mainnet request with `perf` and the release build's debug symbols showed the time was not disk and not BLS signature verification (the existing certificate cache already covers that). It was decoding BLS points out of data the node validated when each block was connected:

  | Share of a warm request | Where | Why |
  |---|---|---|
  | ~60% | `CoinbaseChainLockReader::Read` | The ChainLock binary search reads about 40 carrier coinbases per proof step and deserializes each `CCbTx`, which decompresses and subgroup-checks its G2 signature, although the search only compares heights. |
  | ~20% | `CChain::SetTip` in `GetProofChainSnapshot` | Each new tip rebuilt the proof snapshot from genesis (about 2.5M entries). |
  | ~15% | Commitment deserialization | Finding one quorum's mining transaction fully deserialized every commitment in its mining block (a G1 key and two G2 signatures each). |

  On testnet, whose ChainLock quorums rotate every hour instead of every 12 hours, a fourth cost dominates: `SelectCommitmentForSigning` re-reading and decoding commitments from the evo database for each candidate height. The built proof is also verified three times per request, re-parsing every mining and coinbase transaction each time.

  ## What was done?

  Five commits. Proof bytes and RPC output are unchanged.

  **1. `perf(chainlock)`: memoize coinbase ChainLocks and extend the proof chain snapshot**
  - A process-wide cache maps each carrier block hash to what its coinbase says: whether it carries a ChainLock, the height offset, and the raw 96 signature bytes. All existing validation (coinbase shape, `CCbTx` version, `nHeight`, `bestCLHeightDiff` bound) runs before an entry is stored. A block hash fixes the block's contents, so entries never go stale and a reorg simply stops asking for orphaned hashes.
  - `CoinbaseChainLockReader` no longer decodes signatures. It returns height, signed block hash, carrier and signature bytes; `CoinbaseChainLock::Signed()` decodes on demand through a small cache of decoded points. Only certificates that go into a proof are decoded.
  - `GetProofChainSnapshot` seeds each new snapshot from the previous one via a new `CChain::CopyFrom`, so `SetTip` only rewrites the heights that changed (new blocks, or the fork on a reorg). Old snapshots stay immutable for requests still using them.

  **2. `perf(llmq)`: memoize mining transactions, payload parsing and signer selection**
  - `MiningTransaction()` caches each quorum's mining transaction and Merkle path by (mining block hash, type, quorum hash). On a miss it matches commitments on their raw type byte and quorum hash (fixed offsets in `CFinalCommitmentTxPayload`) before deserializing, so only the matching commitment is decoded.
  - Inside `QuorumProofChain::Verify`, mining and coinbase payload parsing is cached by transaction bytes.
  - `DetermineChainlockSigningCommitment` now caches height → commitment hash (131,072 entries) plus a deduplicated commitment table (4,096), replacing a single 8,192-entry cache of full commitments that was too small for testnet's hourly quorums. The selection itself still runs under `cs_main` with the existing reorg guard; the cache writes moved outside it.

  **3. `fix(llmq)`: make proof caches independent of process-wide BLS state** (from review of 1–2)
  - `CBLSSignature` deserialization depends on the global `bls::bls_legacy_scheme`. The carrier cache therefore stores signature bytes read straight from the payload rather than a re-encoded decoded value, and decoded signatures are cached per scheme value. Without this, a request racing a deep reorg across V19 could cache a carrier as having no ChainLock for the life of the process.
  - Each carrier's Merkle root is checked before its result is cached, because `ReadBlockFromDisk` only checks the header and the result now persists.
  - The reader's result exposes `height`, `block_hash` and `signature_bytes` as separate fields instead of a `ChainLockSig` with an empty signature, so a caller cannot use an undecoded signature by mistake.
  - Oversized commitments in caller-supplied proofs (`verifyquorumproofchain`) are not cached, and the coinbase cache keeps only the fields verification reads.

  **4. `perf(rpc)`: reuse active quorum sets and verify the finished proof once**

  Per-phase timers on a warm mainnet request after commits 1–3 showed where the remaining ~0.25 s went:

  | Phase | Time |
  |---|---|
  | Active quorum set at the checkpoint (inside `Build`) | 75–95 ms |
  | Active quorum set at the target (to open the requested quorum record) | 70–80 ms |
  | EvoNode records (masternode list and entry hashes at the target) | 13–17 ms |
  | Route building | 27–33 ms |
  | Verifying the finished proof, three times | about 25 ms |

  Reading an active set means 88 commitments from the evo database with their BLS points decoded, so that was about 60% of the request.
  - `ActiveCommitments` caches each block's sorted set by block hash, and only after it hashes to that block's own quorum root with no Merkle mutation. Sorting uses precomputed hashes instead of re-hashing in the comparator.
  - `StateAt` caches each block's proof state (network, height, hash, both roots) by block hash, so the checkpoint and target blocks are not read from disk on every call.
  - The RPC no longer re-verifies the proof for the bootstrap and the `target` field. `Build()` already requires `proof.Verify(proof.anchor) == StateAt(targetIndex)`, so the RPC reuses the target block's state. A new `EncodeBootstrap(proof, state, records)` overload checks that the supplied state matches the proof's own target coinbase and header (no BLS work) before encoding.
  - The target's masternode list and entry hashes are cached for EvoNode records, and only after they hash to the block's masternode root.

  **5. `doc`: release notes.**

  ## Cost of the changes

  **Memory.** Every cache is a bounded LRU. Worst case with every cache full, computed from `sizeof` of each cached value on an x86_64 release build plus `unordered_map` node overhead at each cache's eviction threshold:

  | Cache | Entries at threshold | Worst case |
  |---|---|---|
  | Coinbase ChainLocks | 36,864 | 5.9 MiB |
  | Decoded ChainLock signatures | 9,216 | 3.6 MiB |
  | Mining transaction per quorum | 2,304 | 1.8 MiB |
  | Parsed mining payloads (verify) | 4,608 | 2.0 MiB |
  | Parsed coinbase payloads (verify) | 1,152 | 0.2 MiB |
  | Signer selection, height → hash | 147,456 | 14.6 MiB |
  | Signer selection, commitments | 4,608 | 5.4 MiB |
  | Active quorum set per block | 72 | 9 MiB |
  | Proof state per block | 288 | under 0.1 MiB |
  | Masternode list and entry hashes per target | 8 | up to 16 MiB |
  | **New total** | | **about 58 MiB** |
  | Removed: old signer cache | 9,216 | −10.8 MiB |

  About 47 MiB net at worst; the release note says "up to about 50 MiB". The masternode row assumes 8 cached targets of about 3,000 simplified masternode entries at 584 bytes each (measured with `sizeof` on the build) plus heap data and hashes; the list itself is shared with the node's own per-block cache when that already holds it. Memory is only used on nodes that serve these RPCs, and grows with the range of checkpoints requested. Whole-process RSS did not show it measurably: on mainnet, stock dashd itself varied by 64 MB between two restarts (1,641 vs 1,705 MB after the same proof sweep), and the patched build landed between them at 1,659 MB.

  **CPU on a cache miss.** Unchanged in kind. A miss does the same block read and decode as before, plus one Merkle root check per newly cached carrier and one hash per cache key.

  **Behaviour change on pruned nodes.** Cached results outlive the block data they came from. If block data is pruned after a proof was generated, a later identical request can still succeed from the cache, while on a freshly started node it fails with a missing-data error. Correctness is unaffected, but results are not reproducible across restarts in that case. Proof generation still requires the historical blocks, and the anchor (`StateAt`) and final target block are always read from disk. This is in the release note.

  **First request after startup is not faster.** Mainnet from the SDK checkpoint went from about 10 s to about 8.5 s. That request is the one that fills the caches. A background warm-up at startup would fix it and is left for a follow-up.

  **Code.** About 600 lines added and 90 removed across `chainlock/clsig.cpp/.h`, `llmq/quorumproofs.cpp/.h`, `rpc/quorums.cpp`, `chain.h` and tests, plus a release note. The caches are local to the proof code, each guarded by its own mutex; none is held while another lock is taken, and `cs_main` is always taken before a cache mutex where both are needed.

  ## How Has This Been Tested?

  **Unit tests** (`test_dash --run_test=quorum_proofs_tests,llmq_chainlock_tests,skiplist_tests,evo_simplifiedmns_tests`, Linux x86_64, rebased on `develop` e93d1b4): all pass. New cases:
  - `historical_coinbase_lookup_from_disk`: a carrier read under the opposite BLS scheme flag is cached correctly; a memoized carrier survives removal of its block data; unread missing data still throws.
  - `consensus_valid_envelopes_are_provable`: two decoy commitments (same quorum hash under another type, another hash under the same type) mined ahead of the real one; the proof picks the real one at index 3.
  - `copyfrom_then_settip_matches_fresh_chain`: `CopyFrom` then `SetTip` to higher, lower and forked tips matches a freshly built chain and leaves the source untouched.
  - `consensus_valid_envelopes_are_provable` also checks that the verified proof state equals the target block's own state (the invariant the RPC now relies on), that the cached active set is sorted and hashes to the checkpoint's quorum root, that the new `EncodeBootstrap` overload is byte-identical to the old one, and that it refuses a state with a wrong quorum root or height.

  **Byte-identical output.** Stock (nightly at 86771f6) and patched binaries ran against the same synced mainnet and testnet nodes with the target pinned. All 7 mainnet requests (checkpoints from V20 activation to 200 blocks before the target, node counts 0, 4 and 15) and all 5 testnet requests produced identical `sha256` of the full RPC result, on first call after restart and on repeats.

  **Timings** (Xeon E5-2687W v4, ZFS, same node data, pinned target; warm = repeated after the first call):

  | Request | Stock warm | After commit 3 | After commit 4 |
  |---|---|---|---|
  | Mainnet, SDK checkpoint 2,400,000 | 1.9–2.4 s | 0.25–0.55 s | 0.08–0.09 s |
  | Mainnet, 200 blocks before the target | 0.19–0.22 s | 0.19–0.3 s | 0.02 s |
  | Mainnet, V20 activation checkpoint 1,987,776 | 6.6–7.4 s | 0.6–0.7 s | 0.24 s |
  | Testnet, SDK checkpoint 1,548,500, repeated | 2.7 s | 0.3–0.5 s | 0.02–0.03 s |
  | Testnet, checkpoint 1,500,000 | 9.4 s | 0.3–0.6 s | see below |
  | Testnet, V20 activation checkpoint 905,100 | 74 s | 45 s | 45 s |

  In the mixed testnet run, where each request evicts the previous one's route, the warm times for checkpoints older than the SDK's stay at 7–45 s after commit 4, as before; that cost is route building, which commit 4 does not touch. The first request after restart is also unchanged by commit 4 (mainnet SDK checkpoint about 7.6 s).

  The last row is the known limit: from testnet's V20 checkpoint a single request touches about 9,000 signer selections and 80,000 commitment reads, more than the caches hold, so repeats still miss. Callers serving proofs publicly should set a minimum checkpoint height.

  An independent code review of commits 1–2 produced the fixes in commit 3, and a second review of commit 4 produced its Merkle-mutation checks, the state check in the new `EncodeBootstrap` overload, the root check on cached masternode lists, the `StateAt` cache and the stronger tests. Each build was also compiled with GCC 11, which the `linux64_sqlite` CI job uses.

  ## Breaking Changes

  None. RPC arguments, results and proof bytes are unchanged. See the pruned-node note under Cost.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] I have added or updated relevant unit/integration/functional/e2e tests
  - [x] I have made corresponding changes to the documentation
  - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_

  🤖 Generated with [Claude Code](https://claude.com/claude-code)

Top commit has no ACKs.

Tree-SHA512: dd908fd4234896d8966771f375634923564b8fe97ea78ebc9bdc8371e31fbafc86cd077db964cc81ca3cc25032eb406750ff79e6b12aeb4b72cf2c91aa45d72a
PastaPastaPasta pushed a commit that referenced this pull request Sep 24, 2026
…hain note

getquorumproofchain, verifyquorumproofchain and getchainlockbyheight were added by #7107 and have not shipped in any release, so a note that says they "now reuse work across calls" describes a change from behaviour no user has ever seen.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

RPC Some notable changes to RPC params/behaviour/descriptions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants