Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/script/interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1238,7 +1238,7 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& 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);
Expand All @@ -1259,7 +1259,7 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& 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);
Expand Down Expand Up @@ -1997,6 +1997,11 @@ bool GenericTransactionSignatureChecker<T>::CheckSchnorrSignature(std::span<cons
return true;
}

bool BaseSignatureChecker::VerifyDataSigPQC(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& hash) const
{
return pubkey.Verify(hash, sig);
}

template <class T>
bool GenericTransactionSignatureChecker<T>::VerifyPQCSignature(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& sighash) const
{
Expand Down
10 changes: 10 additions & 0 deletions src/script/interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& hash) const;

virtual bool CheckTemplateVerifyHash(std::span<const unsigned char> expected, ScriptError* serror = nullptr) const
{
return false;
Expand Down Expand Up @@ -392,6 +397,11 @@ class DeferringSignatureChecker : public BaseSignatureChecker
return m_checker.CheckPQCSignature(sig, pubkey, sigversion, execdata, serror);
}

bool VerifyDataSigPQC(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& hash) const override
{
return m_checker.VerifyDataSigPQC(sig, pubkey, hash);
}

bool CheckTemplateVerifyHash(std::span<const unsigned char> expected, ScriptError* serror = nullptr) const override
{
return m_checker.CheckTemplateVerifyHash(expected, serror);
Expand Down
23 changes: 22 additions & 1 deletion src/script/sigcache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<const unsigned char> 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<std::shared_mutex> lock(cs_sigcache);
Expand Down Expand Up @@ -108,3 +118,14 @@ bool CachingTransactionSignatureChecker::VerifyPQCSignature(std::span<const unsi
if (observer) observer->Verified(valid, insert);
return valid;
}

bool CachingTransactionSignatureChecker::VerifyDataSigPQC(std::span<const unsigned char> 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;
}
7 changes: 6 additions & 1 deletion src/script/sigcache.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint256, SignatureCacheHasher> map_type;
map_type setValid;
std::shared_mutex cs_sigcache;
Expand All @@ -75,6 +77,8 @@ class SignatureCache

void ComputeEntryPQC(uint256& entry, const uint256& hash, std::span<const unsigned char> sig, const CPQCPubKey& pubkey) const;

void ComputeEntryDataSigPQC(uint256& entry, const uint256& hash, std::span<const unsigned char> sig, const CPQCPubKey& pubkey) const;

bool Get(const uint256& entry, const bool erase);

void Set(const uint256& entry);
Expand All @@ -96,6 +100,7 @@ class CachingTransactionSignatureChecker : public TransactionSignatureChecker
bool VerifyECDSASignature(const std::vector<unsigned char>& vchSig, const CPubKey& vchPubKey, const uint256& sighash) const override;
bool VerifySchnorrSignature(std::span<const unsigned char> sig, const XOnlyPubKey& pubkey, const uint256& sighash) const override;
bool VerifyPQCSignature(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& sighash) const override;
bool VerifyDataSigPQC(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& hash) const override;
};

#endif // QBIT_SCRIPT_SIGCACHE_H
78 changes: 54 additions & 24 deletions src/test/pqc_sigcache_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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));
}
}

Expand Down
59 changes: 59 additions & 0 deletions src/test/txvalidationcache_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned char>;
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<unsigned char>(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()
Loading