Skip to content

script: cache verified PQC data signatures - #267

Closed
djh58 wants to merge 1 commit into
1.x.xfrom
djh58/pqc-datasig-cache
Closed

djh58 wants to merge 1 commit into
1.x.xfrom
djh58/pqc-datasig-cache

Conversation

@djh58

@djh58 djh58 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Summary

OP_CHECKDATASIGPQC and OP_CHECKDATASIGADDPQC verified their signature directly, so no check could reuse an earlier result. Mempool acceptance verified a valid data signature in both its policy and its consensus pass, and block connection could not reuse either result. This routes data signatures through the signature checker so that verified ones are cached like transaction signatures.

What changed:

  • src/script/interpreter.{h,cpp}: data signatures verify through a new virtual BaseSignatureChecker::VerifyDataSigPQC(sig, pubkey, hash). The default verifies directly, as before, so every other checker behaves as it did. DeferringSignatureChecker forwards it. EvalCheckDataSigPQC takes the checker and calls it.
    • The interpreter's size, hash type and validation weight checks run before the checker is called and are unchanged.
  • src/script/sigcache.{h,cpp}: CachingTransactionSignatureChecker looks the check up in, and stores a verified data signature in, a separate 'D' domain of the signature cache (ComputeEntryDataSigPQC, its own salted hasher). A data signature never matches a transaction-signature ('P') entry for the same bytes.
  • As for transaction signatures, a failed verification is never cached.

Why failed verifications stay uncached: a failing signature can be changed without changing the cost of verifying it, so a cache of failures keyed on the signature would not reduce repeated verification work. A failed PQC verification costs about 0.45 µs per witness byte, close to the about 0.34 µs per byte of a failed Schnorr check in tapscript. Reducing the cost of repeated failed verifications is a separate, local-policy question (for example, per-peer accounting of failed verification time in mempool acceptance).

Script validity is unchanged.

Refs #259

Testing

  • Built locally.
  • Ran focused unit or functional tests for the changed area.
  • Ran lint or formatting checks relevant to this change.
  • Not run. Reason:

Built with cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_FUZZ_BINARY=OFF (targets test_qbit qbitd qbit-cli). Results at this head, 8e5f5b809d:

Changed and new unit tests:

  • pqc_sigcache_tests/data_signatures_use_the_cache replaces data_signatures_bypass_the_cache, which asserted the old direct verification. It checks that a verified data signature is stored in its own domain and emits no transaction-signature cache events; that a failed one is verified on every check and never stored; that a transaction-signature entry for the same bytes does not satisfy it; that a consuming checker (as in block connection) uses the stored entry; and that a non-storing checker adds nothing. Results, script errors and the remaining validation weight match the uncached checker.
  • txvalidationcache_tests/pqc_data_signature_reuse: mempool acceptance verifies a valid OP_CHECKDATASIGPQC signature once (the consensus pass reuses the policy pass). A failed data signature is verified on every submission and the transaction is rejected each time.

Unit tests: build/bin/test_qbit --run_test='pqc_tests:pqc_sigcache_tests:txvalidationcache_tests:script_tests:net_tests:net_peer_connection_tests:net_peer_eviction_tests:netbase_tests:denialofservice_tests:peerman_tests:headers_sync_chainwork_tests:blockencodings_tests': 134 cases, 597,759 assertions, no errors.

Functional tests: build/test/functional/test_runner.py --jobs=4: 20 of 20 runs passed.

  • feature_p2mr (data-signature spends)
  • feature_auxpow, feature_auxpow_commitment_activation, feature_minchainwork, feature_minchainwork_requested_block
  • p2p_block_sync (v1, v2), p2p_compactblocks, p2p_compactblocks_blocksonly, p2p_compactblocks_hb (v1, v2), p2p_fingerprint, p2p_getdata
  • p2p_headers_sync_with_minchainwork, p2p_ibd_stalling (v1, v2), p2p_initial_headers_sync, p2p_invalid_messages, p2p_sendheaders, p2p_unrequested_blocks

Not run: the fuzz and bench binaries were not built. src/test/fuzz/script_sigcache.cpp and src/bench/checkinputs_p2mr_pqc.cpp use interfaces this PR does not change.

Target Branch

  • This PR targets main or a maintainer-requested release branch such as 0.1.x.

This targets 1.x.x for v1.0.1 (#181 prepares that release), and then 1.1.0-rc2.

Risk / Review Notes

  • Consensus, script, crypto, wallet, P2P, release, CI, or security-sensitive behavior changed.
  • No consensus, script, crypto, wallet, P2P, release, CI, or security-sensitive behavior changed.

Notes:

  • No validity change. The checker only changes whether a verified result is reused; VerifyDataSigPQC on every non-caching checker is the original direct pubkey.Verify(hash, sig).
  • Cache key: SHA256(nonce || 'D' || 31 zero bytes || hash || pubkey || sig). The interpreter enforces the 32-byte public key and the fixed signature size before every call, so the concatenation is unambiguous. CPQCPubKey::Verify reads only the key, the hash and the signature, and no script flag reaches it, so a cached result cannot differ from a fresh verification.
  • Cache behaviour matches the existing ECDSA, Schnorr and PQC paths: store only on success and only when store is set, and consume (erase) on a hit when not storing.
  • Mixed versions: no P2P, serialization or storage format change.

Docs / Process Impact

Choose exactly one:

  • I updated public docs because this PR changes user-visible behavior, integration guidance, release/process guidance, or expected validation.
  • No public docs update needed. Reason: internal signature-cache behaviour only; no RPC, option, policy or consensus change.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

OP_CHECKDATASIGPQC and OP_CHECKDATASIGADDPQC verified their signature
directly, so no check could reuse an earlier result: mempool acceptance
verified a valid data signature in both its policy and its consensus
pass, and block connection could not reuse either result.

- Data signatures verify through a new BaseSignatureChecker method. The
  default verifies directly, as before.
- CachingTransactionSignatureChecker looks the check up in, and stores a
  verified data signature in, a separate 'D' domain of the signature
  cache, so a data signature never matches a transaction-signature entry
  for the same bytes.
- As for transaction signatures, a failed verification is never cached.
- The interpreter's size, hash type and validation weight checks run
  before the checker is called and are unchanged.

Failed verifications stay uncached on purpose. A failing signature can be
changed without changing the cost of verifying it, so a cache of failures
keyed on the signature would not reduce repeated verification work. A
failed PQC verification costs about 0.45 us per witness byte, close to
the about 0.34 us per byte of a failed Schnorr check in tapscript.

Script validity is unchanged.

Refs #259

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@djh58

djh58 commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T14:59:00.954864Z 8e5f5b8 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 8e5f5b809d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kiwidream

Copy link
Copy Markdown
Member

Closing in favour of #265, which implements the same fix for #259. Both PRs add a BaseSignatureChecker hook for OP_CHECKDATASIG(ADD)PQC and cache verified data signatures in a separate salted 'D' domain. Both cache successes only, using the store/consume rules and shared budget from #162.

We're consolidating to one PR per v1.0.1 release-gate finding (see #174):

#265 was opened first and Codex approved it. An independent adversarial review found no consensus, cache-soundness, checker-forwarding or concurrency problems. #265 also adds a release-notes fragment that records the remaining denial-of-service limitation.

If #267 has anything #265 lacks, please comment on #265 and we'll fold it in.

@kiwidream kiwidream closed this Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants