Repository navigation
script: cache verified P2MR data-signature checks (v1.0.1) - #265
Merged
Merged
Conversation
Route OP_CHECKDATASIGPQC and OP_CHECKDATASIGADDPQC verification through a new BaseSignatureChecker::VerifyPQCDataSignature hook. The default runs the primitive, so every checker that does not override it (signing, dummy, miniscript, test and fuzz checkers) behaves as before, and DeferringSignatureChecker forwards it to the checker it wraps. CachingTransactionSignatureChecker caches verified data-signature successes in the shared signature cache under a separately salted 'D' domain, keyed by the QbitDataSigPQC digest, public key and signature, with the same store/consume rules and shared budget as #162. Failures are never cached. The interpreter still charges validation weight and runs every size check before calling the checker, so validity, weight and error codes are unchanged. A valid data signature does not commit to the spending transaction, so a leaf such as <msg> <pk> OP_CHECKDATASIGPQC OP_NOT could be replayed across mutated transactions, each costing a full SLH-DSA verification during mempool admission. Such replays now cost one verification. Tests: pqc_sigcache_tests replaces data_signatures_bypass_the_cache with data_signatures_use_their_own_cache_domain, adds data_signature_replays_verify_once, and extends concurrent_checks_of_one_signature to data signatures. txvalidationcache_tests adds pqc_data_signature_reuse_across_mutated_spends. Refs #259 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Refs #259 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refs #259. Targets the v1.0.1 release line (
1.x.xat4fb9dbeac3).1.x.xis frozen for v1.0.1 (#181), so a maintainer must lift the freeze before merging, and the v1.0.1 release notes in #181 need this entry (fragment:doc/release-notes-265.md).OP_CHECKDATASIGPQC/OP_CHECKDATASIGADDPQCcalledCPQCPubKey::Verifydirectly, bypassing the signature cache that #162 added for P2MR transaction signatures. A data-signature digest does not commit to the spending transaction, and v30.2's reject filter is keyed by txid/wtxid, so a leaf such as<msg> <pk> OP_CHECKDATASIGPQC OP_NOTwith one valid signature could be replayed in mutated transactions. Each replay cost a full SLH-DSA verification (~1.9 ms for a 3,755-byte witness) during mempool admission. With this change such replays cost one verification.BaseSignatureChecker::VerifyPQCDataSignature(sig, pubkey, datasig_hash).EvalCheckDataSigPQCstill charges validation weight and runs every pubkey, message and signature-size check first, then calls the hook instead of the primitive. The default runs the primitive, so behaviour is unchanged for every checker that does not override it.CachingTransactionSignatureCheckeroverrides the hook. Verified successes are stored in the shared cuckoo cache under a separately salted'D'domain, keyed bySHA256(nonce || 'D' || 0^31 || datasig digest || pubkey || sig). The store/consume rules (Get(entry, !store), insert only whenstoreand valid) and the shared budget are the same as script: cache successful P2MR PQC signature checks #162. Inputs the interpreter never passes (an invalid key or a wrong-size signature) skip the cache. Failures are never cached.Checker audit (every
BaseSignatureCheckersubclass and wrapper)DeferringSignatureChecker(and soSignatureExtractorCheckerinsign.cpp,SignatureWeightCheckerinfeebumper.h)GenericTransactionSignatureChecker/TransactionSignatureChecker/MutableTransactionSignatureCheckerCachingTransactionSignatureChecker'D'DummySignatureChecker(sign.cpp), miniscriptTestSignatureChecker,P2MRBoundarySignatureChecker, fuzzFuzzedSignatureCheckerandCheckerContextBAD_OPCODE.Known limitation: residual DoS (not fixed here)
The coordinator decided against a negative (failure) cache. Anyone holding one valid signature can mint unlimited distinct invalid tuples that each cost a full verification, for example by editing top-layer WOTS or auth-path bytes. A failure cache would therefore stop only exact replays. Anyone can mint distinct full-cost invalid PQC signatures for free, on both the data-signature and transaction-signature paths. Because v30.2 doesn't punish peers that relay consensus-invalid transactions, a peer can cost about 0.5 µs of message-handler and
cs_maintime per witness byte.On the transaction-signature variant in #259: P2MR v1 accepts every Taproot hash type, including
SIGHASH_NONE|ANYONECANPAY(0x82). A valid transaction signature whose sighash excludes the mutated fields is therefore already absorbed by #162's positive cache, because the sighash is unchanged and the entry hits. Only the invalid-signature case remains, and it is the limitation above.Follow-up options, not implemented here (another worker owns
net_processing):Engineering practices applied
'P','S'and'E'domains, with planted entries tested in both directions.ScopedPQCVerificationCountershows N replays cost 1 backend call.shared_mutexand cuckoo cache are reused; no new locks.Testing
cmake --build build -j12(RelWithDebInfo,BUILD_TESTS=ON,BUILD_BENCH=ON, wallet on, GUI off); clean.git diff --check,lint-includes.py,lint-include-guards.py,lint-circular-dependencies.py,lint-locale-dependence.py,lint-tests.py,lint-files.py, all pass.Results:
test_qbit --run_test=pqc_sigcache_tests: pass (*** No errors detected).data_signature_replays_verify_oncecovers acceptance (a). For bothCHECKDATASIGPQCandCHECKDATASIGADDPQCfailing leaves, 8 mutated replays cost 8 backend calls uncached and 1 in store mode. Each result isfalse/EVAL_FALSE, identical to the uncached checker. Consume mode hits with 0 calls, or verifies without storing on a cold cache. Weight and final stack are identical uncached, cold and warm under v2 and legacy weight (e.g.weight_left122/122/122 and 75/75/75).data_signatures_bypass_the_cache→data_signatures_use_their_own_cache_domaincovers acceptance (d). It keeps the original check that a transaction-signature entry does not satisfy a data-signature check, and adds the reverse direction, byte binding, failures never stored, and the guard.concurrent_checks_of_one_signaturecovers acceptance (e):hits=32 misses=64 verified_ok=16 verified_fail=48 backend_calls=64for 8 threads × 3 rounds × 4 tuples.test_qbit --run_test=txvalidationcache_tests: pass. Newpqc_data_signature_reuse_across_mutated_spendscovers the ATMP criterion:ProcessTransactionsubmissions reusing one valid data-signature witness. Events are[MISS, VERIFIED_OK(inserted)]then[HIT]×3, for 1 backend verify in total. Every submission is rejected withmempool-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element).script_p2mr_tests,p2mr_datapqchash_tests,pqc_tests,script_tests,miniscript_tests: pass. P2MR v1 vector and conformance coverage lives inscript_p2mr_tests,pqc_sigcache_tests(14-vector witness corpus) andp2mr_datapqchash_tests.build/bin/test_qbit(all suites, run under the host lock withMemoryMax=20G): pass (*** No errors detected, 4m43s).ctestand unfilteredbench_qbit, which the release-gate rules forbid on this host because ofbench_sanity_checkmemory use. No functional tests were run: the change is confined to the in-process script checker, and the unit-level ATMP test coversProcessTransaction.Local
/code-review mediumfound no correctness issue. Items considered and left as is:DUMMY_CHECKERverifying data signatures for real: this is the pre-existing behaviour, which consensus neutrality requires us to keep.Target Branch
mainor a maintainer-requested release branch such as0.1.x. It targets1.x.xfor v1.0.1.Risk / Review Notes
Notes:
Docs / Process Impact
doc/release-notes-265.mdcovers the cache change and the known limitation; there is no other public doc describing the bypass.🤖 Generated with Claude Code