From f283edf8f0d6266a40c5c98e1e2ddc2f7e7d1cd4 Mon Sep 17 00:00:00 2001 From: Capri Date: Mon, 31 Aug 2026 19:23:30 +0000 Subject: [PATCH] Add post-quantum wallet integration --- src/chainparams.cpp | 4 +- src/interfaces/chain.h | 4 + src/node/interfaces.cpp | 9 + src/outputtype.cpp | 14 +- src/outputtype.h | 4 + src/script/interpreter.cpp | 6 +- src/script/sign.cpp | 6 +- src/validation.cpp | 28 +- src/wallet/rpcwallet.cpp | 17 + src/wallet/salvage.cpp | 2 +- src/wallet/scriptpubkeyman.cpp | 693 ++++++++++++++++++++++++++++++++- src/wallet/scriptpubkeyman.h | 54 +++ src/wallet/wallet.cpp | 37 +- src/wallet/walletdb.cpp | 116 +++++- src/wallet/walletdb.h | 35 ++ 15 files changed, 1013 insertions(+), 16 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 15fbf01..8e2fd4e 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -475,9 +475,9 @@ public: consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay // VEXTA quantum-resistant witness v2/v3. - // Integrated but intentionally not active yet. + // Always active on regtest for development and testing. consensus.vDeployments[Consensus::DEPLOYMENT_PQR].bit = 3; - consensus.vDeployments[Consensus::DEPLOYMENT_PQR].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_PQR].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; consensus.vDeployments[Consensus::DEPLOYMENT_PQR].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_PQR].min_activation_height = 0; diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 1a63f25..1f15ca6 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -5,6 +5,7 @@ #ifndef DIGIBYTE_INTERFACES_CHAIN_H #define DIGIBYTE_INTERFACES_CHAIN_H +#include #include // For CTransactionRef #include // For util::SettingsValue @@ -96,6 +97,9 @@ public: //! any blocks) virtual std::optional getHeight() = 0; + //! Return whether a versionbits deployment is active at the current chain tip. + virtual bool isDeploymentActive(Consensus::DeploymentPos deployment) = 0; + //! Get block hash. Height must be valid or this function will abort. virtual uint256 getBlockHash(int height) = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index f5621d1..375f298 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -441,6 +441,15 @@ public: } return std::nullopt; } + bool isDeploymentActive(Consensus::DeploymentPos deployment) override + { + LOCK(::cs_main); + const CBlockIndex* tip = Assert(m_node.chainman)->ActiveChain().Tip(); + if (!tip) { + return false; + } + return DeploymentActiveAt(*tip, Params().GetConsensus(), deployment); + } uint256 getBlockHash(int height) override { LOCK(::cs_main); diff --git a/src/outputtype.cpp b/src/outputtype.cpp index d84ccb9..a706970 100644 --- a/src/outputtype.cpp +++ b/src/outputtype.cpp @@ -20,6 +20,8 @@ static const std::string OUTPUT_TYPE_STRING_LEGACY = "legacy"; static const std::string OUTPUT_TYPE_STRING_P2SH_SEGWIT = "p2sh-segwit"; static const std::string OUTPUT_TYPE_STRING_BECH32 = "bech32"; static const std::string OUTPUT_TYPE_STRING_BECH32M = "bech32m"; +static const std::string OUTPUT_TYPE_STRING_MLDSA = "mldsa"; +static const std::string OUTPUT_TYPE_STRING_SLHDSA = "slhdsa"; bool ParseOutputType(const std::string& type, OutputType& output_type) { @@ -35,6 +37,12 @@ bool ParseOutputType(const std::string& type, OutputType& output_type) } else if (type == OUTPUT_TYPE_STRING_BECH32M) { output_type = OutputType::BECH32M; return true; + } else if (type == OUTPUT_TYPE_STRING_MLDSA) { + output_type = OutputType::MLDSA; + return true; + } else if (type == OUTPUT_TYPE_STRING_SLHDSA) { + output_type = OutputType::SLHDSA; + return true; } return false; } @@ -46,6 +54,8 @@ const std::string& FormatOutputType(OutputType type) case OutputType::P2SH_SEGWIT: return OUTPUT_TYPE_STRING_P2SH_SEGWIT; case OutputType::BECH32: return OUTPUT_TYPE_STRING_BECH32; case OutputType::BECH32M: return OUTPUT_TYPE_STRING_BECH32M; + case OutputType::MLDSA: return OUTPUT_TYPE_STRING_MLDSA; + case OutputType::SLHDSA: return OUTPUT_TYPE_STRING_SLHDSA; } // no default case, so the compiler can warn about missing cases assert(false); } @@ -65,7 +75,9 @@ CTxDestination GetDestinationForKey(const CPubKey& key, OutputType type) return witdest; } } - case OutputType::BECH32M: {} // This function should never be used with BECH32M, so let it assert + case OutputType::BECH32M: + case OutputType::MLDSA: + case OutputType::SLHDSA: {} // These functions should never be used with non-EC output types, so let it assert } // no default case, so the compiler can warn about missing cases assert(false); } diff --git a/src/outputtype.h b/src/outputtype.h index 976737f..24c708e 100644 --- a/src/outputtype.h +++ b/src/outputtype.h @@ -20,6 +20,8 @@ enum class OutputType { P2SH_SEGWIT, BECH32, BECH32M, + MLDSA, + SLHDSA, }; static constexpr auto OUTPUT_TYPES = std::array{ @@ -27,6 +29,8 @@ static constexpr auto OUTPUT_TYPES = std::array{ OutputType::P2SH_SEGWIT, OutputType::BECH32, OutputType::BECH32M, + OutputType::MLDSA, + OutputType::SLHDSA, }; [[nodiscard]] bool ParseOutputType(const std::string& str, OutputType& output_type); diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index c44962b..d8bb113 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -2045,8 +2045,12 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion, // VEXTA quantum-resistant witness: // v2 = ML-DSA-65, v3 = SLH-DSA/SPHINCS+-128s. // - // Before softfork activation, retain unknown-witness semantics. + // Before softfork activation, retain unknown-witness consensus semantics, + // but reject these spends under standard relay policy. if (!(flags & SCRIPT_VERIFY_PQR)) { + if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) { + return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM); + } return set_success(serror); } diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 7c194f3..d67ce33 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -257,6 +257,8 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator case TxoutType::NONSTANDARD: case TxoutType::NULL_DATA: case TxoutType::WITNESS_UNKNOWN: + case TxoutType::WITNESS_V2_MLDSA: + case TxoutType::WITNESS_V3_SLHDSA: return false; case TxoutType::PUBKEY: if (!CreateSig(creator, sigdata, provider, sig, CPubKey(vSolutions[0]), scriptPubKey, sigversion)) return false; @@ -389,7 +391,9 @@ bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreato sigdata.scriptWitness.stack = std::move(result); } result.clear(); - } else if (solved && whichType == TxoutType::WITNESS_UNKNOWN) { + } else if (whichType == TxoutType::WITNESS_UNKNOWN || + whichType == TxoutType::WITNESS_V2_MLDSA || + whichType == TxoutType::WITNESS_V3_SLHDSA) { sigdata.witness = true; } diff --git a/src/validation.cpp b/src/validation.cpp index 0153fbc..80af40d 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -583,6 +583,26 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) if (fRequireStandard && !IsStandardTx(tx, reason)) return state.Invalid(TxValidationResult::TX_NOT_STANDARD, reason); + // Do not relay transactions creating post-quantum outputs before PQR activation. + if (fRequireStandard) { + const CBlockIndex* tip = m_active_chainstate.m_chain.Tip(); + const bool pqr_active = + tip && DeploymentActiveAt(*tip, Params().GetConsensus(), Consensus::DEPLOYMENT_PQR); + + if (!pqr_active) { + for (const CTxOut& txout : tx.vout) { + std::vector> solutions; + const TxoutType type = Solver(txout.scriptPubKey, solutions); + if (type == TxoutType::WITNESS_V2_MLDSA || + type == TxoutType::WITNESS_V3_SLHDSA) { + return state.Invalid( + TxValidationResult::TX_NOT_STANDARD, + "pqr-premature"); + } + } + } + } + // Do not work on transactions that are too small. // A transaction with 1 segwit input and 1 P2WPHK output has non-witness size of 82 bytes. // Transactions smaller than this are not relayed to mitigate CVE-2017-12842 by not relaying @@ -932,7 +952,13 @@ bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws, Prec const CTransaction& tx = *ws.m_ptx; TxValidationState& state = ws.m_state; - constexpr unsigned int scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS; + unsigned int scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS; + + const CBlockIndex* tip = m_active_chainstate.m_chain.Tip(); + if (tip && + DeploymentActiveAt(*tip, Params().GetConsensus(), Consensus::DEPLOYMENT_PQR)) { + scriptVerifyFlags |= SCRIPT_VERIFY_PQR; + } // Check input scripts and signatures. // This is done last to help prevent CPU exhaustion denial-of-service attacks. diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 1b0999f..a29b50c 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -275,6 +275,13 @@ static RPCHelpMan getnewaddress() } } + if ((output_type == OutputType::MLDSA || output_type == OutputType::SLHDSA) && + !pwallet->chain().isDeploymentActive(Consensus::DEPLOYMENT_PQR)) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Post-quantum addresses are not available before PQR activation"); + } + CTxDestination dest; std::string error; if (!pwallet->GetNewDestination(output_type, label, dest, error)) { @@ -320,6 +327,11 @@ static RPCHelpMan getrawchangeaddress() if (output_type == OutputType::BECH32M && pwallet->GetLegacyScriptPubKeyMan()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Legacy wallets cannot provide bech32m addresses"); } + if (output_type == OutputType::MLDSA || output_type == OutputType::SLHDSA) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Post-quantum address types cannot be used for change addresses"); + } } CTxDestination dest; @@ -3137,6 +3149,11 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out, if (!ParseOutputType(options["change_type"].get_str(), out_type)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown change type '%s'", options["change_type"].get_str())); } + if (out_type == OutputType::MLDSA || out_type == OutputType::SLHDSA) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Post-quantum address types cannot be used for change addresses"); + } coinControl.m_change_type.emplace(out_type); } diff --git a/src/wallet/salvage.cpp b/src/wallet/salvage.cpp index 29e896b..03298ab 100644 --- a/src/wallet/salvage.cpp +++ b/src/wallet/salvage.cpp @@ -19,7 +19,7 @@ typedef std::pair, std::vector > KeyVa static bool KeyFilter(const std::string& type) { - return WalletBatch::IsKeyType(type) || type == DBKeys::HDCHAIN; + return WalletBatch::IsKeyType(type) || type == DBKeys::HDCHAIN || type == DBKeys::PQHDCHAIN; } bool RecoverDatabaseFile(const fs::path& file_path, bilingual_str& error, std::vector& warnings) diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 9ced1d0..e56f920 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -2,10 +2,15 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include +#include +#include #include #include #include +#include #include