diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index c1b00b109d..4237f9feb8 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -434,7 +434,7 @@ static bool EvalChecksigP2MR(const valtype& sig, const valtype& pubkey, ScriptEx return true; } -static bool EvalCheckDataSigPQC(const valtype& sig, const valtype& msg_hash, const valtype& pubkey, ScriptExecutionData& execdata, unsigned int flags, ScriptError* serror, bool& success) +static bool EvalCheckDataSigPQC(const valtype& sig, const valtype& msg_hash, const valtype& pubkey, ScriptExecutionData& execdata, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror, bool& success) { success = !sig.empty(); if (success) { @@ -464,7 +464,7 @@ static bool EvalCheckDataSigPQC(const valtype& sig, const valtype& msg_hash, con const CPQCPubKey pqc_pubkey{pubkey}; const uint256 datasig_hash = ComputeQbitDataSigPQCHash(msg_hash); - if (!pqc_pubkey.Verify(datasig_hash, sig)) { + if (!checker.VerifyDataSigPQC(sig, pqc_pubkey, datasig_hash)) { return set_error(serror, SCRIPT_ERR_P2MR_SIG); } return true; @@ -1238,7 +1238,7 @@ bool EvalScript(std::vector >& stack, const CScript& const valtype& pubkey = stacktop(-1); bool success{false}; - if (!EvalCheckDataSigPQC(sig, msg_hash, pubkey, execdata, flags, serror, success)) return false; + if (!EvalCheckDataSigPQC(sig, msg_hash, pubkey, execdata, flags, checker, serror, success)) return false; popstack(stack); popstack(stack); popstack(stack); @@ -1259,7 +1259,7 @@ bool EvalScript(std::vector >& stack, const CScript& const valtype& pubkey = stacktop(-1); bool success{false}; - if (!EvalCheckDataSigPQC(sig, msg_hash, pubkey, execdata, flags, serror, success)) return false; + if (!EvalCheckDataSigPQC(sig, msg_hash, pubkey, execdata, flags, checker, serror, success)) return false; popstack(stack); popstack(stack); popstack(stack); @@ -1997,6 +1997,11 @@ bool GenericTransactionSignatureChecker::CheckSchnorrSignature(std::span sig, const CPQCPubKey& pubkey, const uint256& hash) const +{ + return pubkey.Verify(hash, sig); +} + template bool GenericTransactionSignatureChecker::VerifyPQCSignature(std::span sig, const CPQCPubKey& pubkey, const uint256& sighash) const { diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 33e23e0f99..9d88e656ef 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -307,6 +307,11 @@ class BaseSignatureChecker return false; } + /** Verify a PQC data signature over its tagged message hash. Unlike the + * other checks this does not depend on a transaction, so every checker + * verifies; the result depends only on the arguments. */ + virtual bool VerifyDataSigPQC(std::span sig, const CPQCPubKey& pubkey, const uint256& hash) const; + virtual bool CheckTemplateVerifyHash(std::span expected, ScriptError* serror = nullptr) const { return false; @@ -392,6 +397,11 @@ class DeferringSignatureChecker : public BaseSignatureChecker return m_checker.CheckPQCSignature(sig, pubkey, sigversion, execdata, serror); } + bool VerifyDataSigPQC(std::span sig, const CPQCPubKey& pubkey, const uint256& hash) const override + { + return m_checker.VerifyDataSigPQC(sig, pubkey, hash); + } + bool CheckTemplateVerifyHash(std::span expected, ScriptError* serror = nullptr) const override { return m_checker.CheckTemplateVerifyHash(expected, serror); diff --git a/src/script/sigcache.cpp b/src/script/sigcache.cpp index ecc830cd43..79f5ab219b 100644 --- a/src/script/sigcache.cpp +++ b/src/script/sigcache.cpp @@ -24,16 +24,20 @@ SignatureCache::SignatureCache(const size_t max_size_bytes) // We want the nonce to be 64 bytes long to force the hasher to process // this chunk, which makes later hash computations more efficient. We // just write our 32-byte entropy, and then pad with 'E' for ECDSA, - // 'S' for Schnorr and 'P' for PQC (followed by 0 bytes). + // 'S' for Schnorr, 'P' for PQC and 'D' for PQC data signatures + // (followed by 0 bytes). static constexpr unsigned char PADDING_ECDSA[32] = {'E'}; static constexpr unsigned char PADDING_SCHNORR[32] = {'S'}; static constexpr unsigned char PADDING_PQC[32] = {'P'}; + static constexpr unsigned char PADDING_DATASIG_PQC[32] = {'D'}; m_salted_hasher_ecdsa.Write(nonce.begin(), 32); m_salted_hasher_ecdsa.Write(PADDING_ECDSA, 32); m_salted_hasher_schnorr.Write(nonce.begin(), 32); m_salted_hasher_schnorr.Write(PADDING_SCHNORR, 32); m_salted_hasher_pqc.Write(nonce.begin(), 32); m_salted_hasher_pqc.Write(PADDING_PQC, 32); + m_salted_hasher_datasig_pqc.Write(nonce.begin(), 32); + m_salted_hasher_datasig_pqc.Write(PADDING_DATASIG_PQC, 32); const auto [num_elems, approx_size_bytes] = setValid.setup_bytes(max_size_bytes); LogInfo("Using %zu MiB out of %zu MiB requested for signature cache, able to store %zu elements", @@ -58,6 +62,12 @@ void SignatureCache::ComputeEntryPQC(uint256& entry, const uint256& hash, std::s hasher.Write(hash.begin(), 32).Write(pubkey.data(), pubkey.size()).Write(sig.data(), sig.size()).Finalize(entry.begin()); } +void SignatureCache::ComputeEntryDataSigPQC(uint256& entry, const uint256& hash, std::span sig, const CPQCPubKey& pubkey) const +{ + CSHA256 hasher = m_salted_hasher_datasig_pqc; + hasher.Write(hash.begin(), 32).Write(pubkey.data(), pubkey.size()).Write(sig.data(), sig.size()).Finalize(entry.begin()); +} + bool SignatureCache::Get(const uint256& entry, const bool erase) { std::shared_lock lock(cs_sigcache); @@ -108,3 +118,14 @@ bool CachingTransactionSignatureChecker::VerifyPQCSignature(std::spanVerified(valid, insert); return valid; } + +bool CachingTransactionSignatureChecker::VerifyDataSigPQC(std::span sig, const CPQCPubKey& pubkey, const uint256& hash) const +{ + uint256 entry; + m_signature_cache.ComputeEntryDataSigPQC(entry, hash, sig, pubkey); + if (m_signature_cache.Get(entry, !store)) return true; + // As for transaction signatures, only a verified success is cached. + if (!TransactionSignatureChecker::VerifyDataSigPQC(sig, pubkey, hash)) return false; + if (store) m_signature_cache.Set(entry); + return true; +} diff --git a/src/script/sigcache.h b/src/script/sigcache.h index 004f9a7d45..2246855db6 100644 --- a/src/script/sigcache.h +++ b/src/script/sigcache.h @@ -54,10 +54,12 @@ class PQCSignatureCacheObserver class SignatureCache { private: - //! Entries are SHA256(nonce || 'E', 'S' or 'P' || 31 zero bytes || signature hash || public key || signature): + //! Entries are SHA256(nonce || 'E', 'S', 'P' or 'D' || 31 zero bytes || signature hash || public key || signature), + //! where 'P' is a PQC transaction signature and 'D' a PQC data signature: CSHA256 m_salted_hasher_ecdsa; CSHA256 m_salted_hasher_schnorr; CSHA256 m_salted_hasher_pqc; + CSHA256 m_salted_hasher_datasig_pqc; typedef CuckooCache::cache map_type; map_type setValid; std::shared_mutex cs_sigcache; @@ -75,6 +77,8 @@ class SignatureCache void ComputeEntryPQC(uint256& entry, const uint256& hash, std::span sig, const CPQCPubKey& pubkey) const; + void ComputeEntryDataSigPQC(uint256& entry, const uint256& hash, std::span sig, const CPQCPubKey& pubkey) const; + bool Get(const uint256& entry, const bool erase); void Set(const uint256& entry); @@ -96,6 +100,7 @@ class CachingTransactionSignatureChecker : public TransactionSignatureChecker bool VerifyECDSASignature(const std::vector& vchSig, const CPubKey& vchPubKey, const uint256& sighash) const override; bool VerifySchnorrSignature(std::span sig, const XOnlyPubKey& pubkey, const uint256& sighash) const override; bool VerifyPQCSignature(std::span sig, const CPQCPubKey& pubkey, const uint256& sighash) const override; + bool VerifyDataSigPQC(std::span sig, const CPQCPubKey& pubkey, const uint256& hash) const override; }; #endif // QBIT_SCRIPT_SIGCACHE_H diff --git a/src/test/pqc_sigcache_tests.cpp b/src/test/pqc_sigcache_tests.cpp index ff82bd1f28..450523a0fd 100644 --- a/src/test/pqc_sigcache_tests.cpp +++ b/src/test/pqc_sigcache_tests.cpp @@ -794,11 +794,12 @@ BOOST_AUTO_TEST_CASE(concurrent_checks_of_one_signature) BOOST_CHECK(!observed.cache.Get(EntryPQC(observed.cache, invalid), /*erase=*/false)); } -BOOST_AUTO_TEST_CASE(data_signatures_bypass_the_cache) +BOOST_AUTO_TEST_CASE(data_signatures_use_the_cache) { - // The cache is scoped to transaction signatures. OP_CHECKDATASIGPQC calls - // the primitive directly: under a caching checker it emits no cache - // events, stores nothing and cannot be satisfied by a cache entry. + // OP_CHECKDATASIGPQC verifies through the checker. A caching checker + // stores a verified data signature in its own signature cache domain and + // never stores a failed one. Results, script errors and the remaining + // validation weight match the uncached checker. CPQCKey key; key.MakeNewKey(); BOOST_REQUIRE(key.IsValid()); @@ -829,31 +830,60 @@ BOOST_AUTO_TEST_CASE(data_signatures_bypass_the_cache) return EvalOutcome{{result, err}, execdata.m_validation_weight_left}; }; + const auto check = [&](const valtype& witness_sig, const BaseSignatureChecker& checker, const EvalOutcome& expected, uint64_t expected_verifications, const std::string& label) { + ScopedPQCVerificationCounter verifications; + BOOST_CHECK_MESSAGE(eval(witness_sig, checker) == expected, label); + BOOST_CHECK_MESSAGE(verifications.GetCount() == expected_verifications, label << ": backend calls " << verifications.GetCount()); + }; + TupleContext ctx; - ObservedCache observed{1 << 20}; - const auto uncached_checker{ctx.Uncached()}; - const auto caching_checker{ctx.Caching(observed.cache, /*store=*/true)}; + const EvalOutcome valid{eval(sig, ctx.Uncached())}; + const EvalOutcome invalid{eval(invalid_sig, ctx.Uncached())}; + BOOST_CHECK(valid.outcome == (ScriptOutcome{true, SCRIPT_ERR_OK})); + BOOST_CHECK(invalid.outcome == (ScriptOutcome{false, SCRIPT_ERR_P2MR_SIG})); - const EvalOutcome valid_uncached{eval(sig, uncached_checker)}; - BOOST_CHECK(valid_uncached.outcome == (ScriptOutcome{true, SCRIPT_ERR_OK})); - for (int i{0}; i < 2; ++i) { - ScopedPQCVerificationCounter verifications; - BOOST_CHECK(eval(sig, caching_checker) == valid_uncached); - BOOST_CHECK_EQUAL(verifications.GetCount(), 1U); - observed.CheckEvents({}, "valid data signature"); + { + ObservedCache observed{1 << 20}; + uint256 valid_entry; + observed.cache.ComputeEntryDataSigPQC(valid_entry, datasig_hash, sig, pubkey); + uint256 invalid_entry; + observed.cache.ComputeEntryDataSigPQC(invalid_entry, datasig_hash, invalid_sig, pubkey); + + const auto storer{ctx.Caching(observed.cache, /*store=*/true)}; + check(sig, storer, valid, 1, "valid data signature, cold"); + check(sig, storer, valid, 0, "valid data signature, warm"); + BOOST_CHECK(observed.cache.Get(valid_entry, /*erase=*/false)); + // Data signatures use their own domain and emit no transaction-signature events. + BOOST_CHECK(!observed.cache.Get(EntryPQC(observed.cache, {datasig_hash, pubkey, sig}), /*erase=*/false)); + + // A failed data signature is verified on every check and never stored. + // A transaction-signature entry for the same bytes does not satisfy it. + observed.cache.Set(EntryPQC(observed.cache, {datasig_hash, pubkey, invalid_sig})); + check(invalid_sig, storer, invalid, 1, "invalid data signature"); + check(invalid_sig, storer, invalid, 1, "invalid data signature, repeated"); + BOOST_CHECK(!observed.cache.Get(invalid_entry, /*erase=*/false)); + + // A consuming checker, as in block connection, uses the stored entry. + const auto consumer{ctx.Caching(observed.cache, /*store=*/false)}; + check(sig, consumer, valid, 0, "valid data signature, consume"); + check(invalid_sig, consumer, invalid, 1, "invalid data signature, consume"); + BOOST_CHECK(!observed.cache.Get(invalid_entry, /*erase=*/false)); + observed.CheckEvents({}, "data signatures"); } - BOOST_CHECK(!observed.cache.Get(EntryPQC(observed.cache, {datasig_hash, pubkey, sig}), /*erase=*/false)); - // Prime the transaction-signature entry for the failing tuple. A data - // signature check that consulted the cache would now succeed. - const EvalOutcome invalid_uncached{eval(invalid_sig, uncached_checker)}; - BOOST_CHECK(invalid_uncached.outcome == (ScriptOutcome{false, SCRIPT_ERR_P2MR_SIG})); - observed.cache.Set(EntryPQC(observed.cache, {datasig_hash, pubkey, invalid_sig})); { - ScopedPQCVerificationCounter verifications; - BOOST_CHECK(eval(invalid_sig, caching_checker) == invalid_uncached); - BOOST_CHECK_EQUAL(verifications.GetCount(), 1U); - observed.CheckEvents({}, "invalid data signature with a primed entry"); + // Without storing, the cache gains no entry. + SignatureCache cache{1 << 20}; + const auto consumer{ctx.Caching(cache, /*store=*/false)}; + for (int i{0}; i < 2; ++i) { + check(sig, consumer, valid, 1, "valid data signature, non-store"); + check(invalid_sig, consumer, invalid, 1, "invalid data signature, non-store"); + } + uint256 entry; + cache.ComputeEntryDataSigPQC(entry, datasig_hash, sig, pubkey); + BOOST_CHECK(!cache.Get(entry, /*erase=*/false)); + cache.ComputeEntryDataSigPQC(entry, datasig_hash, invalid_sig, pubkey); + BOOST_CHECK(!cache.Get(entry, /*erase=*/false)); } } diff --git a/src/test/txvalidationcache_tests.cpp b/src/test/txvalidationcache_tests.cpp index 0e5679af3b..0d4fdcb1c1 100644 --- a/src/test/txvalidationcache_tests.cpp +++ b/src/test/txvalidationcache_tests.cpp @@ -638,4 +638,63 @@ BOOST_FIXTURE_TEST_CASE(pqc_policy_consensus_reuse, P2MRPQCAdmissionSetup) } } +BOOST_FIXTURE_TEST_CASE(pqc_data_signature_reuse, P2MRPQCAdmissionSetup) +{ + // Mempool acceptance verifies a valid OP_CHECKDATASIGPQC signature once: + // the consensus pass reuses the result of the policy pass. A failed data + // signature is never cached; it is verified on every submission and the + // transaction is rejected each time. + using valtype = std::vector; + CPQCKey key; + key.MakeNewKey(); + BOOST_REQUIRE(key.IsValid()); + const CPQCPubKey pubkey{key.GetPubKey()}; + const uint256 msg_hash{(HashWriter{} << std::string{"pqc data signature reuse"}).GetSHA256()}; + const uint256 datasig_hash{ComputeQbitDataSigPQCHash(msg_hash)}; + valtype sig; + uint32_t signature_counter{0}; + BOOST_REQUIRE(key.Sign(datasig_hash, sig, signature_counter)); + valtype invalid_sig{sig}; + invalid_sig.back() ^= 1; + + const valtype control_block{static_cast(P2MR_LEAF_VERSION_V1 | 1)}; + const CScript leaf_script{CScript{} << valtype(msg_hash.begin(), msg_hash.end()) << valtype(pubkey.begin(), pubkey.end()) << OP_CHECKDATASIGPQC}; + const valtype leaf_bytes(leaf_script.begin(), leaf_script.end()); + const uint256 merkle_root{ComputeP2MRMerkleRoot(control_block, ComputeP2MRLeafHash(P2MR_LEAF_VERSION_V1, leaf_bytes))}; + const CScript script_pubkey{CScript{} << OP_2 << valtype(merkle_root.begin(), merkle_root.end())}; + + const CAmount coin_value{m_coinbase_txns[0]->vout[0].nValue / 2}; + const CMutableTransaction funding{CreateValidMempoolTransaction( + /*input_transactions=*/{m_coinbase_txns[0]}, + /*inputs=*/{COutPoint{m_coinbase_txns[0]->GetHash(), 0}}, + /*input_height=*/COINBASE_MATURITY, + /*input_signing_keys=*/{coinbaseKey}, + /*outputs=*/{CTxOut{coin_value, script_pubkey}}, + /*submit=*/false)}; + CreateAndProcessBlock({funding}, P2MROpTrueScript()); + + const auto submit = [&](const valtype& witness_sig, CAmount fee, uint64_t expected_verifications, const std::string& label) { + CMutableTransaction tx; + tx.version = 2; + tx.vin.emplace_back(COutPoint{funding.GetHash(), 0}); + tx.vin[0].scriptWitness.stack = {witness_sig, leaf_bytes, control_block}; + tx.vout.emplace_back(coin_value - fee, P2MROpTrueScript()); + ScopedPQCVerificationCounter verifications; + const MempoolAcceptResult result{WITH_LOCK(cs_main, return m_node.chainman->ProcessTransaction(MakeTransactionRef(tx)))}; + BOOST_TEST_MESSAGE(label << ": backend calls " << verifications.GetCount() << " reject=" << result.m_state.GetRejectReason()); + BOOST_CHECK_MESSAGE(verifications.GetCount() == expected_verifications, label << ": backend calls " << verifications.GetCount()); + return result; + }; + + for (const CAmount fee : {100'000, 100'001}) { + const std::string label{strprintf("invalid data signature, fee %d", fee)}; + const MempoolAcceptResult result{submit(invalid_sig, fee, /*expected_verifications=*/1, label)}; + BOOST_CHECK_MESSAGE(result.m_result_type == MempoolAcceptResult::ResultType::INVALID, label); + BOOST_CHECK_EQUAL(result.m_state.GetRejectReason(), "mempool-script-verify-flag-failed (PQC signature verification failed)"); + } + + const MempoolAcceptResult result{submit(sig, 100'002, /*expected_verifications=*/1, "valid data signature")}; + BOOST_CHECK_MESSAGE(result.m_result_type == MempoolAcceptResult::ResultType::VALID, result.m_state.ToString()); +} + BOOST_AUTO_TEST_SUITE_END()