Skip to content
Merged
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
22 changes: 22 additions & 0 deletions doc/release-notes-265.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
P2MR data-signature checks use the signature cache
==================================================

`OP_CHECKDATASIGPQC` and `OP_CHECKDATASIGADDPQC` checks now use the
signature cache, like P2MR transaction signatures since #162. Previously every
data-signature check ran a full SLH-DSA verification. A data signature does not
commit to the spending transaction, so a peer could resubmit one valid
signature in an unlimited number of mutated transactions, each costing a full
verification during mempool admission. Such replays now cost one verification.

Only verified successes are cached, in a separately salted domain, so an entry
for a transaction signature can never satisfy a data-signature check or the
reverse. Validity, validation weight and script errors are unchanged; this is
not a consensus change.

Known limitation: failures are not cached. Anyone holding one valid signature
can derive unlimited distinct invalid PQC signatures that each still cost a
full verification, on both the data-signature and transaction-signature paths.
Because this release does not penalize peers that relay consensus-invalid
transactions, a peer can cost a node about 0.5 µs of message-handling and
`cs_main` time per witness byte. Discouraging peers whose P2MR PQC signatures
fail, or budgeting script-validation cost per peer, are possible follow-ups.
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.VerifyPQCDataSignature(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::VerifyPQCDataSignature(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& datasig_hash) const
{
return pubkey.Verify(datasig_hash, sig);
}

template <class T>
bool GenericTransactionSignatureChecker<T>::VerifyPQCSignature(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& sighash) const
{
Expand Down
11 changes: 11 additions & 0 deletions src/script/interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,12 @@ class BaseSignatureChecker
return false;
}

/** Verify an OP_CHECKDATASIGPQC/OP_CHECKDATASIGADDPQC signature over its
* QbitDataSigPQC digest. The interpreter performs every weight, size and
* digest step first; this only decides whether the signature is valid.
* The default runs the primitive verification. */
virtual bool VerifyPQCDataSignature(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& datasig_hash) const;

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

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

bool CheckTemplateVerifyHash(std::span<const unsigned char> expected, ScriptError* serror = nullptr) const override
{
return m_checker.CheckTemplateVerifyHash(expected, serror);
Expand Down
34 changes: 33 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 transaction signatures 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_PQC_DATA[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_pqc_data.Write(nonce.begin(), 32);
m_salted_hasher_pqc_data.Write(PADDING_PQC_DATA, 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::ComputeEntryPQCData(uint256& entry, const uint256& datasig_hash, std::span<const unsigned char> sig, const CPQCPubKey& pubkey) const
{
CSHA256 hasher = m_salted_hasher_pqc_data;
hasher.Write(datasig_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,25 @@ bool CachingTransactionSignatureChecker::VerifyPQCSignature(std::span<const unsi
if (observer) observer->Verified(valid, insert);
return valid;
}

bool CachingTransactionSignatureChecker::VerifyPQCDataSignature(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& datasig_hash) const
{
// The interpreter only passes valid keys and exact-size signatures. Any
// other input fails without a backend call, and an invalid key adds no
// bytes to the entry, so keep such inputs out of the cache entirely.
if (!pubkey.IsValid() || sig.size() != PQC_SIG_SIZE) {
return TransactionSignatureChecker::VerifyPQCDataSignature(sig, pubkey, datasig_hash);
}
uint256 entry;
m_signature_cache.ComputeEntryPQCData(entry, datasig_hash, sig, pubkey);
PQCSignatureCacheObserver* const observer{m_signature_cache.GetPQCObserver()};
const bool hit{m_signature_cache.Get(entry, !store)};
if (observer) observer->Lookup(hit, !store);
if (hit) return true;
const bool valid{TransactionSignatureChecker::VerifyPQCDataSignature(sig, pubkey, datasig_hash)};
// Only a verified success is published to the cache.
const bool insert{valid && store};
if (insert) m_signature_cache.Set(entry);
if (observer) observer->Verified(valid, insert);
return valid;
}
13 changes: 9 additions & 4 deletions src/script/sigcache.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ static constexpr size_t DEFAULT_SCRIPT_EXECUTION_CACHE_BYTES{DEFAULT_VALIDATION_
static_assert(DEFAULT_VALIDATION_CACHE_BYTES == DEFAULT_SIGNATURE_CACHE_BYTES + DEFAULT_SCRIPT_EXECUTION_CACHE_BYTES);

/**
* Test-only observer of PQC transaction-signature cache activity. Production
* code never installs one; it must not influence validation results.
* Test-only observer of PQC transaction- and data-signature cache activity.
* Production code never installs one; it must not influence validation results.
*/
class PQCSignatureCacheObserver
{
public:
virtual ~PQCSignatureCacheObserver() = default;
//! Called once per PQC transaction-signature cache lookup.
//! Called once per PQC transaction- or data-signature cache lookup.
virtual void Lookup(bool hit, bool erase) = 0;
//! Called once per primitive PQC verification performed after a cache miss.
virtual void Verified(bool valid, bool inserted) = 0;
Expand All @@ -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' marks PQC transaction signatures and 'D' PQC data signatures (OP_CHECKDATASIG(ADD)PQC):
CSHA256 m_salted_hasher_ecdsa;
CSHA256 m_salted_hasher_schnorr;
CSHA256 m_salted_hasher_pqc;
CSHA256 m_salted_hasher_pqc_data;
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 ComputeEntryPQCData(uint256& entry, const uint256& datasig_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 VerifyPQCDataSignature(std::span<const unsigned char> sig, const CPQCPubKey& pubkey, const uint256& datasig_hash) const override;
};

#endif // QBIT_SCRIPT_SIGCACHE_H
Loading
Loading