From e9de55acbe33964fe300d2e466aeddf3cd853cc8 Mon Sep 17 00:00:00 2001 From: barneychambers Date: Mon, 16 Feb 2026 22:01:55 +0000 Subject: [PATCH 1/3] security: fix Dilithium sigop counting and tapscript vulnerabilities Fix three security issues with Dilithium opcode integration: 1. Dilithium opcodes (OP_CHECKSIGDILITHIUM, OP_CHECKSIGDILITHIUMVERIFY, OP_CHECKMULTISIGDILITHIUM, OP_CHECKMULTISIGDILITHIUMVERIFY) were not counted by GetSigOpCount, allowing blocks with unbounded Dilithium signature verifications that bypass the 80k sigop limit (DoS vector). 2. Dilithium opcodes (0xbb-0xbf) fell within the OP_SUCCESSx range (0xbb-0xfe) in tapscript, causing any tapscript containing a Dilithium opcode to unconditionally succeed without validation, making affected outputs anyone-can-spend. 3. Even with the OP_SUCCESSx fix, Dilithium opcodes in tapscript provide a false sense of security because P2TR outputs expose the internal public key on-chain, leaving them vulnerable to quantum key recovery via the key path (see BIP360). Dilithium opcodes are now explicitly disabled in tapscript with SCRIPT_ERR_TAPSCRIPT_DILITHIUM, following the same pattern as OP_CHECKMULTISIG in tapscript. Changes: - script: count Dilithium opcodes in CScript::GetSigOpCount() - script: narrow IsOpSuccess() range from 0xbb-0xfe to 0xc0-0xfe - script: reject Dilithium opcodes in tapscript execution context - script: add SCRIPT_ERR_TAPSCRIPT_DILITHIUM error code - test: add Dilithium opcode definitions to Python test framework - test: fix is_op_success() in Python test framework to match C++ - test: add feature_dilithium_sigops.py integration test suite~ --- src/script/interpreter.cpp | 11 + src/script/script.cpp | 11 +- src/script/script_error.cpp | 2 + src/script/script_error.h | 1 + test/functional/feature_dilithium_sigops.py | 474 ++++++++++++++++++++ test/functional/test_framework/script.py | 16 +- test/functional/test_runner.py | 1 + 7 files changed, 512 insertions(+), 4 deletions(-) create mode 100755 test/functional/feature_dilithium_sigops.py diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 5b2732e87..9eef7c953 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -1261,6 +1261,11 @@ bool EvalScript(std::vector >& stack, const CScript& case OP_CHECKSIGDILITHIUM: case OP_CHECKSIGDILITHIUMVERIFY: { + // BTQ: Dilithium opcodes are disabled in tapscript. Taproot outputs + // are quantum-vulnerable due to the exposed key path public key, so + // Dilithium in tapscript provides a false sense of security. + if (sigversion == SigVersion::TAPSCRIPT) return set_error(serror, SCRIPT_ERR_TAPSCRIPT_DILITHIUM); + // (sig pubkey -- bool) if (stack.size() < 2) return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION); @@ -1287,6 +1292,9 @@ bool EvalScript(std::vector >& stack, const CScript& case OP_CHECKMULTISIGDILITHIUM: case OP_CHECKMULTISIGDILITHIUMVERIFY: { + // BTQ: Dilithium opcodes are disabled in tapscript. + if (sigversion == SigVersion::TAPSCRIPT) return set_error(serror, SCRIPT_ERR_TAPSCRIPT_DILITHIUM); + // ([sig ...] num_of_signatures [pubkey ...] num_of_pubkeys -- bool) // Similar to OP_CHECKMULTISIG but for Dilithium signatures int i = 1; @@ -1365,6 +1373,9 @@ bool EvalScript(std::vector >& stack, const CScript& case OP_DILITHIUM_PUBKEY: { + // BTQ: Dilithium opcodes are disabled in tapscript. + if (sigversion == SigVersion::TAPSCRIPT) return set_error(serror, SCRIPT_ERR_TAPSCRIPT_DILITHIUM); + // (pubkey -- bool) if (stack.size() < 1) return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION); diff --git a/src/script/script.cpp b/src/script/script.cpp index f81c7fb7c..3b9987393 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -170,9 +170,11 @@ unsigned int CScript::GetSigOpCount(bool fAccurate) const opcodetype opcode; if (!GetOp(pc, opcode)) break; - if (opcode == OP_CHECKSIG || opcode == OP_CHECKSIGVERIFY) + if (opcode == OP_CHECKSIG || opcode == OP_CHECKSIGVERIFY || + opcode == OP_CHECKSIGDILITHIUM || opcode == OP_CHECKSIGDILITHIUMVERIFY) n++; - else if (opcode == OP_CHECKMULTISIG || opcode == OP_CHECKMULTISIGVERIFY) + else if (opcode == OP_CHECKMULTISIG || opcode == OP_CHECKMULTISIGVERIFY || + opcode == OP_CHECKMULTISIGDILITHIUM || opcode == OP_CHECKMULTISIGDILITHIUMVERIFY) { if (fAccurate && lastOpcode >= OP_1 && lastOpcode <= OP_16) n += DecodeOP_N(lastOpcode); @@ -344,10 +346,13 @@ bool GetScriptOp(CScriptBase::const_iterator& pc, CScriptBase::const_iterator en bool IsOpSuccess(const opcodetype& opcode) { + // BTQ: Exclude Dilithium opcodes (0xbb-0xbf = 187-191) from OP_SUCCESSx range. + // Without this exclusion, any tapscript containing a Dilithium opcode would + // unconditionally succeed, making it anyone-can-spend. return opcode == 80 || opcode == 98 || (opcode >= 126 && opcode <= 129) || (opcode >= 131 && opcode <= 134) || (opcode >= 137 && opcode <= 138) || (opcode >= 141 && opcode <= 142) || (opcode >= 149 && opcode <= 153) || - (opcode >= 187 && opcode <= 254); + (opcode >= 192 && opcode <= 254); } bool CheckMinimalPush(const std::vector& data, opcodetype opcode) { diff --git a/src/script/script_error.cpp b/src/script/script_error.cpp index 2a5c36943..b1e03a9ce 100644 --- a/src/script/script_error.cpp +++ b/src/script/script_error.cpp @@ -111,6 +111,8 @@ std::string ScriptErrorString(const ScriptError serror) return "OP_CHECKMULTISIG(VERIFY) is not available in tapscript"; case SCRIPT_ERR_TAPSCRIPT_MINIMALIF: return "OP_IF/NOTIF argument must be minimal in tapscript"; + case SCRIPT_ERR_TAPSCRIPT_DILITHIUM: + return "Dilithium opcodes are not available in tapscript (P2TR is quantum-vulnerable)"; case SCRIPT_ERR_OP_CODESEPARATOR: return "Using OP_CODESEPARATOR in non-witness script"; case SCRIPT_ERR_SIG_FINDANDDELETE: diff --git a/src/script/script_error.h b/src/script/script_error.h index f416dba5d..0248de1fc 100644 --- a/src/script/script_error.h +++ b/src/script/script_error.h @@ -77,6 +77,7 @@ typedef enum ScriptError_t SCRIPT_ERR_TAPSCRIPT_VALIDATION_WEIGHT, SCRIPT_ERR_TAPSCRIPT_CHECKMULTISIG, SCRIPT_ERR_TAPSCRIPT_MINIMALIF, + SCRIPT_ERR_TAPSCRIPT_DILITHIUM, /* Constant scriptCode */ SCRIPT_ERR_OP_CODESEPARATOR, diff --git a/test/functional/feature_dilithium_sigops.py b/test/functional/feature_dilithium_sigops.py new file mode 100755 index 000000000..cac13d36a --- /dev/null +++ b/test/functional/feature_dilithium_sigops.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The BTQ Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test Dilithium opcode sigop counting and tapscript restrictions. + +Tests the following security fixes: + + 1. Dilithium opcodes are counted in GetSigOpCount, preventing DoS + attacks via uncounted signature verifications. + + 2. Dilithium opcodes are excluded from the OP_SUCCESSx range in + tapscript, preventing anyone-can-spend on tapscript outputs. + + 3. Dilithium opcodes are explicitly disabled in tapscript execution, + matching the pattern used for OP_CHECKMULTISIG in tapscript. +""" + +import struct + +from test_framework.blocktools import ( + COINBASE_MATURITY, + create_block, + create_coinbase, + add_witness_commitment, +) +from test_framework.messages import ( + CBlock, + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + SEQUENCE_FINAL, + tx_from_hex, +) +from test_framework.p2p import P2PDataStore +from test_framework.script import ( + CScript, + CScriptNum, + CScriptOp, + LEAF_VERSION_TAPSCRIPT, + OP_0, + OP_1, + OP_2, + OP_3, + OP_CHECKMULTISIG, + OP_CHECKMULTISIGVERIFY, + OP_CHECKSIG, + OP_CHECKSIGVERIFY, + OP_CHECKSIGDILITHIUM, + OP_CHECKSIGDILITHIUMVERIFY, + OP_CHECKMULTISIGDILITHIUM, + OP_CHECKMULTISIGDILITHIUMVERIFY, + OP_DILITHIUM_PUBKEY, + OP_DUP, + OP_DROP, + OP_EQUALVERIFY, + OP_HASH160, + OP_RETURN, + OP_TRUE, + taproot_construct, +) +from test_framework.key import ECKey, compute_xonly_pubkey +from test_framework.test_framework import BTQTestFramework +from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet + +# ============================================================================ +# Constants +# ============================================================================ +TAPROOT_LEAF_MASK = 0xfe +TAPROOT_LEAF_TAPSCRIPT = 0xc0 +MAX_BLOCK_SIGOPS_COST = 80000 +WITNESS_SCALE_FACTOR = 4 + + +# ============================================================================ +# Pure-Python SigOp Counter (mirrors C++ GetSigOpCount) +# ============================================================================ +def get_sigop_count(script_bytes): + """Python reimplementation of CScript::GetSigOpCount(fAccurate=true). + + Mirrors the C++ logic so we can independently verify the counting. + """ + n = 0 + last_opcode = 0xff + pc = 0 + while pc < len(script_bytes): + opcode = script_bytes[pc] + pc += 1 + + # ---- push-data opcodes (skip over payload) ---- + if opcode <= 0x4e: + if opcode < 0x4c: + pc += opcode + elif opcode == 0x4c: + if pc >= len(script_bytes): + break + pc += 1 + script_bytes[pc] + elif opcode == 0x4d: + if pc + 1 >= len(script_bytes): + break + size = script_bytes[pc] | (script_bytes[pc + 1] << 8) + pc += 2 + size + elif opcode == 0x4e: + if pc + 3 >= len(script_bytes): + break + size = struct.unpack(' 20 (MAX)'), + (CScript([OP_3, OP_CHECKMULTISIG]), 3, 'ECDSA 3-key multisig (control)'), + ] + + for script, expected, label in cases: + actual = get_sigop_count(bytes(script)) + assert_equal(actual, expected) + print(info(f'{label}: {actual} sigop(s)')) + + # Parity check: ECDSA and Dilithium multisig count identically + ecdsa = get_sigop_count(bytes(CScript([OP_3, OP_CHECKMULTISIG]))) + dilithium = get_sigop_count(bytes(CScript([OP_3, OP_CHECKMULTISIGDILITHIUM]))) + assert_equal(ecdsa, dilithium) + print(info(f'ECDSA vs Dilithium parity: both = {ecdsa}')) + + print(passed()) + + def _test_1c_mixed(self): + print(sub_test('1c. Complex mixed-opcode script')) + + script = CScript([ + OP_CHECKSIG, # +1 ECDSA + OP_CHECKSIGVERIFY, # +1 ECDSA + OP_CHECKSIGDILITHIUM, # +1 Dilithium + OP_CHECKSIGDILITHIUMVERIFY, # +1 Dilithium + OP_3, OP_CHECKMULTISIG, # +3 ECDSA multisig + OP_2, OP_CHECKMULTISIGDILITHIUM, # +2 Dilithium multisig + ]) # --- + actual = get_sigop_count(bytes(script)) # 9 total + assert_equal(actual, 9) + print(info(f'1+1+1+1+3+2 = {actual} sigops')) + + print(passed()) + + def _test_1d_dos_vector(self): + print(sub_test('1d. DoS attack vector: repeated OP_CHECKSIGDILITHIUMVERIFY')) + + for count in [10, 50, 100]: + script = CScript([OP_CHECKSIGDILITHIUMVERIFY] * count) + actual = get_sigop_count(bytes(script)) + assert_equal(actual, count) + print(info(f'{count} repeated CHECKSIGDILITHIUMVERIFY -> {actual} sigops')) + + print(passed('DoS vector properly bounded by sigop counting')) + + def _test_1e_non_sigops(self): + print(sub_test('1e. Non-sigop opcodes correctly excluded')) + + cases = [ + (CScript([OP_DILITHIUM_PUBKEY]), 0, 'OP_DILITHIUM_PUBKEY'), + (CScript([OP_DUP, OP_HASH160, OP_EQUALVERIFY]), 0, 'P2PKH template'), + (CScript([OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_DILITHIUM_PUBKEY]),0, 'mixed non-sigop'), + (CScript([]), 0, 'empty script'), + ] + + for script, expected, label in cases: + actual = get_sigop_count(bytes(script)) + assert_equal(actual, expected) + print(info(f'{label}: {actual} sigops')) + + print(passed()) + + # ==================================================================== + # TEST 2 - OP_SUCCESSx Range + # ==================================================================== + + def test_2_op_success_range(self): + print(section('TEST 2 \u2014 OP_SUCCESSx Range Exclusion')) + + def is_op_success_fixed(o): + """Matches the FIXED C++ IsOpSuccess().""" + return (o == 0x50 or o == 0x62 or + (0x7e <= o <= 0x81) or (0x83 <= o <= 0x86) or + (0x89 <= o <= 0x8a) or (0x8d <= o <= 0x8e) or + (0x95 <= o <= 0x99) or (0xc0 <= o <= 0xfe)) + + def is_op_success_old(o): + """Matches the OLD (vulnerable) IsOpSuccess().""" + return (o == 0x50 or o == 0x62 or + (0x7e <= o <= 0x81) or (0x83 <= o <= 0x86) or + (0x89 <= o <= 0x8a) or (0x8d <= o <= 0x8e) or + (0x95 <= o <= 0x99) or (0xbb <= o <= 0xfe)) + + # --- 2a: Dilithium opcodes must NOT be OP_SUCCESSx --- + print(sub_test('2a. Dilithium opcodes excluded from OP_SUCCESSx')) + dilithium_ops = { + 0xbb: 'OP_CHECKSIGDILITHIUM', + 0xbc: 'OP_CHECKSIGDILITHIUMVERIFY', + 0xbd: 'OP_CHECKMULTISIGDILITHIUM', + 0xbe: 'OP_CHECKMULTISIGDILITHIUMVERIFY', + 0xbf: 'OP_DILITHIUM_PUBKEY', + } + for val, name in dilithium_ops.items(): + assert not is_op_success_fixed(val), f'{name} must not be OP_SUCCESSx' + assert is_op_success_old(val), f'{name} was OP_SUCCESSx in vulnerable version' + print(info(f'0x{val:02x} {name:40s} fixed=no old=yes')) + + print(passed('Dilithium range (0xbb-0xbf) excluded')) + + # --- 2b: Opcodes 0xc0+ must still be OP_SUCCESSx --- + print(sub_test('2b. Opcodes 0xc0-0xfe remain OP_SUCCESSx')) + for val in [0xc0, 0xc1, 0xd0, 0xef, 0xfe]: + assert is_op_success_fixed(val), f'0x{val:02x} should be OP_SUCCESSx' + print(info(f'0x{val:02x} -> OP_SUCCESSx')) + + print(passed()) + + # --- 2c: Boundaries --- + print(sub_test('2c. Boundary values')) + assert not is_op_success_fixed(0xba), '0xba (OP_CHECKSIGADD) is a real opcode' + print(info('0xba OP_CHECKSIGADD -> not OP_SUCCESSx')) + assert not is_op_success_fixed(0xbf), '0xbf (OP_DILITHIUM_PUBKEY) excluded' + print(info('0xbf OP_DILITHIUM_PK -> not OP_SUCCESSx')) + assert is_op_success_fixed(0xc0), '0xc0 is first OP_SUCCESSx' + print(info('0xc0 first OP_SUCCESS -> OP_SUCCESSx')) + assert not is_op_success_fixed(0xff), '0xff (OP_INVALIDOPCODE) is not OP_SUCCESSx' + print(info('0xff OP_INVALIDOPCODE -> not OP_SUCCESSx')) + + print(passed()) + + # ==================================================================== + # TEST 3 - Tapscript Rejection (on-chain regtest) + # ==================================================================== + + def test_3_tapscript_rejection(self, node, wallet): + print(section('TEST 3 \u2014 Dilithium Disabled in Tapscript (on-chain)')) + + privkey = ECKey() + privkey.generate() + xonly_pubkey, _ = compute_xonly_pubkey(privkey.get_bytes()) + + # --- 3a-3f: Each Dilithium opcode must be rejected in tapscript --- + reject_cases = [ + ('3a', 'OP_CHECKSIGDILITHIUM', CScript([OP_TRUE, OP_TRUE, OP_CHECKSIGDILITHIUM]), [b'\x01', b'\x01']), + ('3b', 'OP_CHECKSIGDILITHIUMVERIFY', CScript([OP_TRUE, OP_TRUE, OP_CHECKSIGDILITHIUMVERIFY, OP_TRUE]), [b'\x01', b'\x01']), + ('3c', 'OP_CHECKMULTISIGDILITHIUM', CScript([OP_0, OP_1, OP_1, OP_CHECKMULTISIGDILITHIUM]), [b'', b'\x01']), + ('3d', 'OP_CHECKMULTISIGDILITHIUMVERIFY', CScript([OP_0, OP_1, OP_1, OP_CHECKMULTISIGDILITHIUMVERIFY, OP_TRUE]), [b'', b'\x01']), + ('3e', 'OP_DILITHIUM_PUBKEY', CScript([OP_TRUE, OP_DILITHIUM_PUBKEY]), [b'\x01']), + ] + + for test_id, opcode_name, leaf_script, witness_stack in reject_cases: + print(sub_test(f'{test_id}. {opcode_name} in tapscript -> MUST REJECT')) + result = self._tapscript_spend_test( + node, wallet, xonly_pubkey, + leaf_script, witness_stack, + label=f'leaf_{test_id}', + ) + assert not result, f'{opcode_name} should be rejected in tapscript!' + print(passed(f'{opcode_name} correctly rejected')) + + # --- 3f: Control test - OP_TRUE must still work --- + print(sub_test('3f. OP_TRUE in tapscript -> MUST ACCEPT (control test)')) + result = self._tapscript_spend_test( + node, wallet, xonly_pubkey, + CScript([OP_TRUE]), [], + label='leaf_true', + ) + assert result, 'OP_TRUE tapscript should succeed!' + print(passed('Tapscript execution works correctly')) + + # ==================================================================== + # Helpers + # ==================================================================== + + def _tapscript_spend_test(self, node, wallet, xonly_pubkey, leaf_script, witness_stack, label): + """Fund a P2TR output with the given leaf script, then try to spend it. + + Returns True if the spend was accepted to the mempool, False otherwise. + """ + try: + # Build the taproot output + tap = taproot_construct(xonly_pubkey, [(label, leaf_script)]) + + # Fund it using MiniWallet + fund_info = wallet.send_to( + from_node=node, + scriptPubKey=tap.scriptPubKey, + amount=50_000, + ) + funding_txid = fund_info['txid'] + funding_vout = fund_info['sent_vout'] + self.generate(node, 1) + + # Build spending transaction - output to a standard P2TR (anyone-can-spend) + # so we don't trigger maxburnamount policy rejection + spend_tx = CTransaction() + spend_tx.nVersion = 2 + spend_tx.vin = [CTxIn( + COutPoint(int(funding_txid, 16), funding_vout), + b'', + SEQUENCE_FINAL, + )] + spend_tx.vout = [CTxOut(40_000, tap.scriptPubKey)] + + # Build the tapscript witness + leaf = tap.leaves[label] + control_byte = leaf.version | (1 if tap.negflag else 0) + control_block = bytes([control_byte]) + tap.internal_pubkey + for h in leaf.merklebranch: + control_block += h + + wit = CTxInWitness() + for item in witness_stack: + wit.scriptWitness.stack.append(item) + wit.scriptWitness.stack.append(bytes(leaf_script)) + wit.scriptWitness.stack.append(control_block) + spend_tx.wit.vtxinwit = [wit] + spend_tx.rehash() + + # Try to submit + node.sendrawtransaction(spend_tx.serialize().hex()) + return True + + except Exception as e: + err = str(e) + if 'dilithium' in err.lower() or 'Script failed' in err or 'non-mandatory' in err.lower(): + print(info(f'Rejected as expected: {err[:80]}')) + else: + print(info(f'Rejected (other): {err[:80]}')) + return False + + +if __name__ == '__main__': + DilithiumSigopsTest().main() diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 51b0797b1..34bee06d3 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -253,6 +253,13 @@ def __new__(cls, n): # BIP 342 opcodes (Tapscript) OP_CHECKSIGADD = CScriptOp(0xba) +# BTQ: Dilithium post-quantum signature opcodes +OP_CHECKSIGDILITHIUM = CScriptOp(0xbb) +OP_CHECKSIGDILITHIUMVERIFY = CScriptOp(0xbc) +OP_CHECKMULTISIGDILITHIUM = CScriptOp(0xbd) +OP_CHECKMULTISIGDILITHIUMVERIFY = CScriptOp(0xbe) +OP_DILITHIUM_PUBKEY = CScriptOp(0xbf) + OP_INVALIDOPCODE = CScriptOp(0xff) OPCODE_NAMES.update({ @@ -368,6 +375,11 @@ def __new__(cls, n): OP_NOP9: 'OP_NOP9', OP_NOP10: 'OP_NOP10', OP_CHECKSIGADD: 'OP_CHECKSIGADD', + OP_CHECKSIGDILITHIUM: 'OP_CHECKSIGDILITHIUM', + OP_CHECKSIGDILITHIUMVERIFY: 'OP_CHECKSIGDILITHIUMVERIFY', + OP_CHECKMULTISIGDILITHIUM: 'OP_CHECKMULTISIGDILITHIUM', + OP_CHECKMULTISIGDILITHIUMVERIFY: 'OP_CHECKMULTISIGDILITHIUMVERIFY', + OP_DILITHIUM_PUBKEY: 'OP_DILITHIUM_PUBKEY', OP_INVALIDOPCODE: 'OP_INVALIDOPCODE', }) @@ -925,4 +937,6 @@ def taproot_construct(pubkey, scripts=None, treat_internal_as_infinity=False): return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked) def is_op_success(o): - return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xbb and o <= 0xfe) + # BTQ: Exclude Dilithium opcodes (0xbb-0xbf) from OP_SUCCESSx range. + # Must match IsOpSuccess() in src/script/script.cpp + return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xc0 and o <= 0xfe) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 28fbccbff..5c7aed4e1 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -98,6 +98,7 @@ # vv Tests less than 5m vv 'feature_fee_estimation.py', 'feature_taproot.py', + 'feature_dilithium_sigops.py', 'feature_block.py', # vv Tests less than 2m vv 'mining_getblocktemplate_longpoll.py', From f77e9ab3450c910f801284b80497588e05030a9d Mon Sep 17 00:00:00 2001 From: barneychambers Date: Thu, 19 Feb 2026 12:48:45 +0000 Subject: [PATCH 2/3] consensus: implement BIP360 P2MR (Pay-to-Merkle-Root) output type Implement SegWit version 2 P2MR outputs as specified in BIP360. P2MR provides a quantum-resistant script tree output type by removing the Taproot key path spend, which exposes a public key vulnerable to quantum key recovery via Shor's algorithm. P2MR commits directly to the script tree Merkle root (no internal key, no tweak), enabling Dilithium post-quantum signature opcodes to provide real protection inside the script tree. Consensus changes: - Add SigVersion::P2MR_TAPSCRIPT and SCRIPT_VERIFY_P2MR flag - Add VerifyP2MRCommitment (Merkle root verification without tweak) - Add witversion==2 branch in VerifyWitnessProgram - No key path (reject witness stack with fewer than 2 elements) - Control block format 1+32*m bytes (no internal key) - Parity bit enforcement (must be 1 per BIP360) - Validation weight for Dilithium signature checks - Route P2MR_TAPSCRIPT through EvalChecksigTapscript, SignatureHashSchnorr, CheckSchnorrSignature, and ExecuteWitnessScript - Re-enable Dilithium opcodes in P2MR tapscript context (blocked in P2TR) - Block OP_CHECKMULTISIG in P2MR (inherited from BIP342 tapscript rules) Address and type system: - Add WitnessV2P2MR destination type and TxoutType::WITNESS_V2_P2MR - Add bc1z address encoding/decoding (SegWit v2, bech32m) - Add P2MRBuilder for constructing P2MR outputs from script trees - Add P2MRSpendData for script path spending Policy and activation: - Add P2MR witness standardness checks - Activate SCRIPT_VERIFY_P2MR on all networks Tests: - Add feature_p2mr.py with 15 test groups covering output format, script path spending with mining verification, no key path, all 5 Dilithium opcodes (P2MR vs P2TR comparative), error messages, OP_CHECKMULTISIG blocking, multi-leaf trees (2 and 4 leaves), invalid Merkle proofs, invalid control block sizes, parity bit enforcement, address encoding, and multi-input transactions - Add p2mr_construct() and P2MRInfo to Python test framework --- src/addresstype.cpp | 12 + src/addresstype.h | 30 +- src/key_io.cpp | 14 + src/outputtype.cpp | 1 + src/policy/policy.cpp | 21 + src/rpc/util.cpp | 10 + src/script/interpreter.cpp | 89 ++- src/script/interpreter.h | 19 +- src/script/script.h | 5 + src/script/sign.cpp | 4 +- src/script/signingprovider.cpp | 138 +++++ src/script/signingprovider.h | 46 ++ src/script/solver.cpp | 5 + src/script/solver.h | 1 + src/validation.cpp | 3 + src/wallet/rpc/addresses.cpp | 1 + src/wallet/scriptpubkeyman.cpp | 1 + src/wallet/spend.cpp | 1 + test/functional/feature_p2mr.py | 704 +++++++++++++++++++++++ test/functional/test_framework/script.py | 17 + test/functional/test_runner.py | 1 + test_dilithium_sendmany.sh | 194 +++++++ 22 files changed, 1301 insertions(+), 16 deletions(-) create mode 100755 test/functional/feature_p2mr.py create mode 100755 test_dilithium_sendmany.sh diff --git a/src/addresstype.cpp b/src/addresstype.cpp index 0e580da90..bfc43a60b 100644 --- a/src/addresstype.cpp +++ b/src/addresstype.cpp @@ -101,6 +101,12 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet) addressRet = tap; return true; } + case TxoutType::WITNESS_V2_P2MR: { + WitnessV2P2MR p2mr; + std::copy(vSolutions[0].begin(), vSolutions[0].end(), p2mr.begin()); + addressRet = p2mr; + return true; + } case TxoutType::WITNESS_UNKNOWN: { addressRet = WitnessUnknown{vSolutions[0][0], vSolutions[1]}; return true; @@ -187,6 +193,11 @@ class CScriptVisitor return CScript() << OP_1 << ToByteVector(tap); } + CScript operator()(const WitnessV2P2MR& p2mr) const + { + return CScript() << OP_2 << std::vector(p2mr.begin(), p2mr.end()); + } + CScript operator()(const WitnessUnknown& id) const { return CScript() << CScript::EncodeOP_N(id.GetWitnessVersion()) << id.GetWitnessProgram(); @@ -228,6 +239,7 @@ class ValidDestinationVisitor bool operator()(const WitnessV0KeyHash& dest) const { return true; } bool operator()(const WitnessV0ScriptHash& dest) const { return true; } bool operator()(const WitnessV1Taproot& dest) const { return true; } + bool operator()(const WitnessV2P2MR& dest) const { return true; } bool operator()(const WitnessUnknown& dest) const { return true; } // Dilithium destination operators bool operator()(const DilithiumPubKeyDestination& dest) const { return false; } diff --git a/src/addresstype.h b/src/addresstype.h index 828c93633..3a36244a5 100644 --- a/src/addresstype.h +++ b/src/addresstype.h @@ -89,6 +89,34 @@ struct WitnessV1Taproot : public XOnlyPubKey explicit WitnessV1Taproot(const XOnlyPubKey& xpk) : XOnlyPubKey(xpk) {} }; +/** BIP360 P2MR: 32-byte script tree Merkle root (no public key) */ +struct WitnessV2P2MR +{ + static constexpr size_t SIZE = 32; + unsigned char m_merkle_root[SIZE]; + + WitnessV2P2MR() { memset(m_merkle_root, 0, SIZE); } + explicit WitnessV2P2MR(const std::vector& data) { + assert(data.size() == SIZE); + memcpy(m_merkle_root, data.data(), SIZE); + } + explicit WitnessV2P2MR(const uint256& root) { + memcpy(m_merkle_root, root.begin(), SIZE); + } + + const unsigned char* begin() const { return m_merkle_root; } + const unsigned char* end() const { return m_merkle_root + SIZE; } + unsigned char* begin() { return m_merkle_root; } + unsigned char* end() { return m_merkle_root + SIZE; } + + friend bool operator==(const WitnessV2P2MR& a, const WitnessV2P2MR& b) { + return memcmp(a.m_merkle_root, b.m_merkle_root, SIZE) == 0; + } + friend bool operator<(const WitnessV2P2MR& a, const WitnessV2P2MR& b) { + return memcmp(a.m_merkle_root, b.m_merkle_root, SIZE) < 0; + } +}; + // Dilithium destination types struct DilithiumPubKeyDestination { private: @@ -175,7 +203,7 @@ struct WitnessUnknown * * DilithiumWitnessV0ScriptHash: TxoutType::DILITHIUM_WITNESS_V0_SCRIPTHASH destination (P2DWSH address) * A CTxDestination is the internal data type encoded in a btq address */ -using CTxDestination = std::variant; +using CTxDestination = std::variant; /** Check whether a CTxDestination corresponds to one with an address. */ bool IsValidDestination(const CTxDestination& dest); diff --git a/src/key_io.cpp b/src/key_io.cpp index b019d08cf..ea8bf4b0b 100644 --- a/src/key_io.cpp +++ b/src/key_io.cpp @@ -65,6 +65,14 @@ class DestinationEncoder return bech32::Encode(bech32::Encoding::BECH32M, m_params.Bech32HRP(), data); } + std::string operator()(const WitnessV2P2MR& p2mr) const + { + std::vector data = {2}; // SegWit version 2 -> bc1z prefix + data.reserve(53); + ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, p2mr.begin(), p2mr.end()); + return bech32::Encode(bech32::Encoding::BECH32M, m_params.Bech32HRP(), data); + } + std::string operator()(const WitnessUnknown& id) const { const std::vector& program = id.GetWitnessProgram(); @@ -236,6 +244,12 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par return tap; } + if (version == 2 && data.size() == WITNESS_V2_P2MR_SIZE) { + WitnessV2P2MR p2mr; + std::copy(data.begin(), data.end(), p2mr.begin()); + return p2mr; + } + if (version > 16) { error_str = "Invalid Bech32 address witness version"; return CNoDestination(); diff --git a/src/outputtype.cpp b/src/outputtype.cpp index f8d05b19f..5692f6727 100644 --- a/src/outputtype.cpp +++ b/src/outputtype.cpp @@ -129,6 +129,7 @@ std::optional OutputTypeFromDestination(const CTxDestination& dest) return OutputType::BECH32; } if (std::holds_alternative(dest) || + std::holds_alternative(dest) || std::holds_alternative(dest)) { return OutputType::BECH32M; } diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index e6fce2470..809d125ec 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -288,6 +288,27 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs) return false; } } + + if (witnessversion == 2 && witnessprogram.size() == WITNESS_V2_P2MR_SIZE && !p2sh) { + // BIP360 P2MR spend (non-P2SH-wrapped, version 2, witness program size 32) + Span stack{tx.vin[i].scriptWitness.stack}; + if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) { + return false; // Annexes are nonstandard + } + if (stack.size() >= 2) { + const auto& control_block = SpanPopBack(stack); + SpanPopBack(stack); + if (control_block.empty()) return false; + if ((control_block[0] & TAPROOT_LEAF_MASK) == TAPROOT_LEAF_TAPSCRIPT) { + for (const auto& item : stack) { + if (item.size() > MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE) return false; + } + } + } else { + // P2MR has no key path; fewer than 2 elements is invalid + return false; + } + } } return true; } diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index bf62fd37a..8e9700d52 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -304,6 +304,16 @@ class DescribeAddressVisitor return obj; } + UniValue operator()(const WitnessV2P2MR& p2mr) const + { + UniValue obj(UniValue::VOBJ); + obj.pushKV("isscript", true); + obj.pushKV("iswitness", true); + obj.pushKV("witness_version", 2); + obj.pushKV("witness_program", HexStr(Span{p2mr.begin(), p2mr.end()})); + return obj; + } + UniValue operator()(const WitnessUnknown& id) const { UniValue obj(UniValue::VOBJ); diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 9eef7c953..6245ee5df 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -121,6 +121,15 @@ static bool EvalChecksigDilithium(const valtype& sig, const valtype& pubkey, CSc return set_error(serror, SCRIPT_ERR_PUBKEYTYPE); } + // In P2MR tapscript, enforce validation weight for Dilithium sig checks + if (sigversion == SigVersion::P2MR_TAPSCRIPT && !sig.empty()) { + assert(execdata.m_validation_weight_left_init); + execdata.m_validation_weight_left -= VALIDATION_WEIGHT_PER_DILITHIUM_SIGOP_PASSED; + if (execdata.m_validation_weight_left < 0) { + return set_error(serror, SCRIPT_ERR_TAPSCRIPT_VALIDATION_WEIGHT); + } + } + // Create Dilithium public key object CDilithiumPubKey dilithium_pubkey(pubkey); @@ -390,7 +399,7 @@ static bool EvalChecksigPreTapscript(const valtype& vchSig, const valtype& vchPu static bool EvalChecksigTapscript(const valtype& sig, const valtype& pubkey, ScriptExecutionData& execdata, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptError* serror, bool& success) { - assert(sigversion == SigVersion::TAPSCRIPT); + assert(sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT); /* * The following validation sequence is consensus critical. Please note how -- @@ -440,6 +449,7 @@ static bool EvalChecksig(const valtype& sig, const valtype& pubkey, CScript::con case SigVersion::WITNESS_V0: return EvalChecksigPreTapscript(sig, pubkey, pbegincodehash, pend, flags, checker, sigversion, serror, success); case SigVersion::TAPSCRIPT: + case SigVersion::P2MR_TAPSCRIPT: return EvalChecksigTapscript(sig, pubkey, execdata, flags, checker, sigversion, serror, success); case SigVersion::TAPROOT: // Key path spending in Taproot has no script, so this is unreachable. @@ -459,7 +469,7 @@ bool EvalScript(std::vector >& stack, const CScript& static const valtype vchTrue(1, 1); // sigversion cannot be TAPROOT here, as it admits no script execution. - assert(sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0 || sigversion == SigVersion::TAPSCRIPT); + assert(sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0 || sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT); CScript::const_iterator pc = script.begin(); CScript::const_iterator pend = script.end(); @@ -655,7 +665,7 @@ bool EvalScript(std::vector >& stack, const CScript& return set_error(serror, SCRIPT_ERR_UNBALANCED_CONDITIONAL); valtype& vch = stacktop(-1); // Tapscript requires minimal IF/NOTIF inputs as a consensus rule. - if (sigversion == SigVersion::TAPSCRIPT) { + if (sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT) { // The input argument to the OP_IF and OP_NOTIF opcodes must be either // exactly 0 (the empty vector) or exactly 1 (the one-byte vector with value 1). if (vch.size() > 1 || (vch.size() == 1 && vch[0] != 1)) { @@ -1149,7 +1159,7 @@ bool EvalScript(std::vector >& stack, const CScript& case OP_CHECKMULTISIG: case OP_CHECKMULTISIGVERIFY: { - if (sigversion == SigVersion::TAPSCRIPT) return set_error(serror, SCRIPT_ERR_TAPSCRIPT_CHECKMULTISIG); + if (sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT) return set_error(serror, SCRIPT_ERR_TAPSCRIPT_CHECKMULTISIG); // ([sig ...] num_of_signatures [pubkey ...] num_of_pubkeys -- bool) @@ -1658,6 +1668,7 @@ bool SignatureHashSchnorr(uint256& hash_out, ScriptExecutionData& execdata, cons // key_version is not used and left uninitialized. break; case SigVersion::TAPSCRIPT: + case SigVersion::P2MR_TAPSCRIPT: ext_flag = 1; // key_version must be 0 for now, representing the current version of // 32-byte public keys in the tapscript signature opcode execution. @@ -1725,8 +1736,8 @@ bool SignatureHashSchnorr(uint256& hash_out, ScriptExecutionData& execdata, cons ss << execdata.m_output_hash.value(); } - // Additional data for BIP 342 signatures - if (sigversion == SigVersion::TAPSCRIPT) { + // Additional data for BIP 342 signatures (also applies to P2MR tapscript) + if (sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT) { assert(execdata.m_tapleaf_hash_init); ss << execdata.m_tapleaf_hash; ss << key_version; @@ -1846,7 +1857,7 @@ bool GenericTransactionSignatureChecker::CheckECDSASignature(const std::vecto template bool GenericTransactionSignatureChecker::CheckSchnorrSignature(Span sig, Span pubkey_in, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror) const { - assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT); + assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT); // Schnorr signatures have 32-byte public keys. The caller is responsible for enforcing this. assert(pubkey_in.size() == 32); // Note that in Tapscript evaluation, empty signatures are treated specially (invalid signature that does not @@ -1996,7 +2007,7 @@ static bool ExecuteWitnessScript(const Span& stack_span, const CS { std::vector stack{stack_span.begin(), stack_span.end()}; - if (sigversion == SigVersion::TAPSCRIPT) { + if (sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT) { // OP_SUCCESSx processing overrides everything, including stack element size limits CScript::const_iterator pc = exec_script.begin(); while (pc < exec_script.end()) { @@ -2014,7 +2025,7 @@ static bool ExecuteWitnessScript(const Span& stack_span, const CS } } - // Tapscript enforces initial stack size limits (altstack is empty here) + // Tapscript/P2MR enforces initial stack size limits (altstack is empty here) if (stack.size() > MAX_STACK_SIZE) return set_error(serror, SCRIPT_ERR_STACK_SIZE); } @@ -2077,6 +2088,22 @@ static bool VerifyTaprootCommitment(const std::vector& control, c return q.CheckTapTweak(p, merkle_root, control[0] & 1); } +/** BIP360 P2MR: Verify that the witness program equals the Merkle root of the script tree. + * Unlike Taproot, there is no internal key and no tweak -- the witness program IS the root. */ +static bool VerifyP2MRCommitment(const std::vector& control, const std::vector& program, const uint256& tapleaf_hash) +{ + assert(control.size() >= P2MR_CONTROL_BASE_SIZE); + assert(program.size() >= uint256::size()); + + const int path_len = (control.size() - P2MR_CONTROL_BASE_SIZE) / P2MR_CONTROL_NODE_SIZE; + uint256 k = tapleaf_hash; + for (int i = 0; i < path_len; ++i) { + Span node{Span{control}.subspan(P2MR_CONTROL_BASE_SIZE + P2MR_CONTROL_NODE_SIZE * i, P2MR_CONTROL_NODE_SIZE)}; + k = ComputeTapbranchHash(k, node); + } + return k == uint256(Span{program}); +} + static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion, const std::vector& program, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror, bool is_p2sh) { CScript exec_script; //!< Actually executed script (last stack item in P2WSH; implied P2PKH script in P2WPKH; leaf script in P2TR) @@ -2162,6 +2189,50 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion, } return set_success(serror); } + } else if (witversion == 2 && program.size() == WITNESS_V2_P2MR_SIZE && !is_p2sh) { + // BIP360 P2MR: 32-byte non-P2SH witness v2 program (which encodes the script tree Merkle root) + if (!(flags & SCRIPT_VERIFY_P2MR)) return set_success(serror); + if (stack.size() == 0) return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_WITNESS_EMPTY); + // Handle optional annex (same rules as Taproot) + if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) { + const valtype& annex = SpanPopBack(stack); + execdata.m_annex_hash = (HashWriter{} << annex).GetSHA256(); + execdata.m_annex_present = true; + } else { + execdata.m_annex_present = false; + } + execdata.m_annex_init = true; + // P2MR has NO key path spend. Require at least two witness elements (script + control block). + if (stack.size() < 2) { + return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH); + } + // Script path spending (always, since there is no key path) + const valtype& control = SpanPopBack(stack); + const valtype& script = SpanPopBack(stack); + if (control.size() < P2MR_CONTROL_BASE_SIZE || control.size() > P2MR_CONTROL_MAX_SIZE || + ((control.size() - P2MR_CONTROL_BASE_SIZE) % P2MR_CONTROL_NODE_SIZE) != 0) { + return set_error(serror, SCRIPT_ERR_TAPROOT_WRONG_CONTROL_SIZE); + } + // Per BIP360, the parity bit (last bit of control byte) must be 1 + if ((control[0] & 1) != 1) { + return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH); + } + execdata.m_tapleaf_hash = ComputeTapleafHash(control[0] & TAPROOT_LEAF_MASK, script); + if (!VerifyP2MRCommitment(control, program, execdata.m_tapleaf_hash)) { + return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH); + } + execdata.m_tapleaf_hash_init = true; + if ((control[0] & TAPROOT_LEAF_MASK) == TAPROOT_LEAF_TAPSCRIPT) { + // Tapscript execution within P2MR context (Dilithium opcodes are enabled) + exec_script = CScript(script.begin(), script.end()); + execdata.m_validation_weight_left = ::GetSerializeSize(witness.stack, PROTOCOL_VERSION) + VALIDATION_WEIGHT_OFFSET; + execdata.m_validation_weight_left_init = true; + return ExecuteWitnessScript(stack, exec_script, flags, SigVersion::P2MR_TAPSCRIPT, checker, execdata, serror); + } + if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION) { + return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION); + } + return set_success(serror); } else { if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) { return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM); diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 81b4d016a..1eb49c1c8 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -145,6 +145,9 @@ enum : uint32_t { // Dilithium signature validation SCRIPT_VERIFY_DILITHIUM = (1U << 21), + // BIP360 P2MR (Pay-to-Merkle-Root) validation + SCRIPT_VERIFY_P2MR = (1U << 22), + // Constants to point to the highest flag in use. Add new flags above this line. // SCRIPT_VERIFY_END_MARKER @@ -191,10 +194,11 @@ struct PrecomputedTransactionData enum class SigVersion { - BASE = 0, //!< Bare scripts and BIP16 P2SH-wrapped redeemscripts - WITNESS_V0 = 1, //!< Witness v0 (P2WPKH and P2WSH); see BIP 141 - TAPROOT = 2, //!< Witness v1 with 32-byte program, not BIP16 P2SH-wrapped, key path spending; see BIP 341 - TAPSCRIPT = 3, //!< Witness v1 with 32-byte program, not BIP16 P2SH-wrapped, script path spending, leaf version 0xc0; see BIP 342 + BASE = 0, //!< Bare scripts and BIP16 P2SH-wrapped redeemscripts + WITNESS_V0 = 1, //!< Witness v0 (P2WPKH and P2WSH); see BIP 141 + TAPROOT = 2, //!< Witness v1 with 32-byte program, not BIP16 P2SH-wrapped, key path spending; see BIP 341 + TAPSCRIPT = 3, //!< Witness v1 with 32-byte program, not BIP16 P2SH-wrapped, script path spending, leaf version 0xc0; see BIP 342 + P2MR_TAPSCRIPT = 4, //!< Witness v2 (P2MR) script path spending with Dilithium support; see BIP 360 }; struct ScriptExecutionData @@ -229,6 +233,7 @@ struct ScriptExecutionData static constexpr size_t WITNESS_V0_SCRIPTHASH_SIZE = 32; static constexpr size_t WITNESS_V0_KEYHASH_SIZE = 20; static constexpr size_t WITNESS_V1_TAPROOT_SIZE = 32; +static constexpr size_t WITNESS_V2_P2MR_SIZE = 32; static constexpr uint8_t TAPROOT_LEAF_MASK = 0xfe; static constexpr uint8_t TAPROOT_LEAF_TAPSCRIPT = 0xc0; @@ -237,6 +242,12 @@ static constexpr size_t TAPROOT_CONTROL_NODE_SIZE = 32; static constexpr size_t TAPROOT_CONTROL_MAX_NODE_COUNT = 128; static constexpr size_t TAPROOT_CONTROL_MAX_SIZE = TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * TAPROOT_CONTROL_MAX_NODE_COUNT; +/** BIP360 P2MR control block constants (no internal key, so base is 1 byte) */ +static constexpr size_t P2MR_CONTROL_BASE_SIZE = 1; +static constexpr size_t P2MR_CONTROL_NODE_SIZE = 32; +static constexpr size_t P2MR_CONTROL_MAX_NODE_COUNT = 128; +static constexpr size_t P2MR_CONTROL_MAX_SIZE = P2MR_CONTROL_BASE_SIZE + P2MR_CONTROL_NODE_SIZE * P2MR_CONTROL_MAX_NODE_COUNT; + extern const HashWriter HASHER_TAPSIGHASH; //!< Hasher with tag "TapSighash" pre-fed to it. extern const HashWriter HASHER_TAPLEAF; //!< Hasher with tag "TapLeaf" pre-fed to it. extern const HashWriter HASHER_TAPBRANCH; //!< Hasher with tag "TapBranch" pre-fed to it. diff --git a/src/script/script.h b/src/script/script.h index 67863365c..1283be435 100644 --- a/src/script/script.h +++ b/src/script/script.h @@ -60,6 +60,11 @@ static constexpr unsigned int ANNEX_TAG = 0x50; // Validation weight per passing signature (Tapscript only, see BIP 342). static constexpr int64_t VALIDATION_WEIGHT_PER_SIGOP_PASSED{50}; +// Validation weight per passing Dilithium signature (P2MR tapscript only). +// Dilithium verification is significantly more expensive than Schnorr, so the +// weight cost is higher to prevent DoS via validation time. +static constexpr int64_t VALIDATION_WEIGHT_PER_DILITHIUM_SIGOP_PASSED{500}; + // How much weight budget is added to the witness size (Tapscript only, see BIP 342). static constexpr int64_t VALIDATION_WEIGHT_OFFSET{50}; diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 66e8bd4f6..2a7476bab 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -61,7 +61,7 @@ bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provid bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const { - assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT); + assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT); CKey key; if (!provider.GetKeyByXOnly(pubkey, key)) return false; @@ -74,7 +74,7 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& ScriptExecutionData execdata; execdata.m_annex_init = true; execdata.m_annex_present = false; // Only support annex-less signing for now. - if (sigversion == SigVersion::TAPSCRIPT) { + if (sigversion == SigVersion::TAPSCRIPT || sigversion == SigVersion::P2MR_TAPSCRIPT) { execdata.m_codeseparator_pos_init = true; execdata.m_codeseparator_pos = 0xFFFFFFFF; // Only support non-OP_CODESEPARATOR BIP342 signing for now. if (!leaf_hash) return false; // BIP342 signing needs leaf hash. diff --git a/src/script/signingprovider.cpp b/src/script/signingprovider.cpp index 863671491..32065e635 100644 --- a/src/script/signingprovider.cpp +++ b/src/script/signingprovider.cpp @@ -628,3 +628,141 @@ std::vector>> TaprootBui } return tuples; } + +// ========================================================================== +// BIP360 P2MR Builder Implementation +// ========================================================================== + +void P2MRSpendData::Merge(P2MRSpendData other) +{ + if (merkle_root.IsNull() && !other.merkle_root.IsNull()) { + merkle_root = other.merkle_root; + } + for (auto& [key, control_blocks] : other.scripts) { + scripts[key].merge(std::move(control_blocks)); + } +} + +/*static*/ P2MRBuilder::NodeInfo P2MRBuilder::Combine(NodeInfo&& a, NodeInfo&& b) +{ + NodeInfo ret; + for (auto& leaf : a.leaves) { + leaf.merkle_branch.push_back(b.hash); + ret.leaves.emplace_back(std::move(leaf)); + } + for (auto& leaf : b.leaves) { + leaf.merkle_branch.push_back(a.hash); + ret.leaves.emplace_back(std::move(leaf)); + } + ret.hash = ComputeTapbranchHash(a.hash, b.hash); + return ret; +} + +void P2MRBuilder::Insert(P2MRBuilder::NodeInfo&& node, int depth) +{ + assert(depth >= 0 && (size_t)depth <= P2MR_CONTROL_MAX_NODE_COUNT); + if ((size_t)depth + 1 < m_branch.size()) { + m_valid = false; + return; + } + while (m_valid && m_branch.size() > (size_t)depth && m_branch[depth].has_value()) { + node = Combine(std::move(node), std::move(*m_branch[depth])); + m_branch.pop_back(); + if (depth == 0) m_valid = false; + --depth; + } + if (m_valid) { + if (m_branch.size() <= (size_t)depth) m_branch.resize((size_t)depth + 1); + assert(!m_branch[depth].has_value()); + m_branch[depth] = std::move(node); + } +} + +/*static*/ bool P2MRBuilder::ValidDepths(const std::vector& depths) +{ + std::vector branch; + for (int depth : depths) { + if (depth < 0 || (size_t)depth > P2MR_CONTROL_MAX_NODE_COUNT) return false; + if ((size_t)depth + 1 < branch.size()) return false; + while (branch.size() > (size_t)depth && branch[depth]) { + branch.pop_back(); + if (depth == 0) return false; + --depth; + } + if (branch.size() <= (size_t)depth) branch.resize((size_t)depth + 1); + assert(!branch[depth]); + branch[depth] = true; + } + return branch.size() == 0 || (branch.size() == 1 && branch[0]); +} + +P2MRBuilder& P2MRBuilder::Add(int depth, Span script, int leaf_version, bool track) +{ + assert((leaf_version & ~TAPROOT_LEAF_MASK) == 0); + if (!IsValid()) return *this; + NodeInfo node; + node.hash = ComputeTapleafHash(leaf_version, script); + if (track) node.leaves.emplace_back(LeafInfo{std::vector(script.begin(), script.end()), leaf_version, {}}); + Insert(std::move(node), depth); + return *this; +} + +P2MRBuilder& P2MRBuilder::AddOmitted(int depth, const uint256& hash) +{ + if (!IsValid()) return *this; + NodeInfo node; + node.hash = hash; + Insert(std::move(node), depth); + return *this; +} + +P2MRBuilder& P2MRBuilder::Finalize() +{ + assert(IsComplete()); + assert(m_branch.size() > 0); + m_merkle_root = m_branch[0]->hash; + return *this; +} + +WitnessV2P2MR P2MRBuilder::GetOutput() +{ + return WitnessV2P2MR{m_merkle_root}; +} + +P2MRSpendData P2MRBuilder::GetSpendData() const +{ + assert(IsComplete()); + P2MRSpendData spd; + spd.merkle_root = m_branch.size() == 0 ? uint256() : m_branch[0]->hash; + if (m_branch.size()) { + for (const auto& leaf : m_branch[0]->leaves) { + std::vector control_block; + control_block.resize(P2MR_CONTROL_BASE_SIZE + P2MR_CONTROL_NODE_SIZE * leaf.merkle_branch.size()); + // Per BIP360, parity bit is always 1 (no internal key) + control_block[0] = leaf.leaf_version | 1; + if (leaf.merkle_branch.size()) { + std::copy(leaf.merkle_branch[0].begin(), + leaf.merkle_branch[0].begin() + P2MR_CONTROL_NODE_SIZE * leaf.merkle_branch.size(), + control_block.begin() + P2MR_CONTROL_BASE_SIZE); + } + spd.scripts[{leaf.script, leaf.leaf_version}].insert(std::move(control_block)); + } + } + return spd; +} + +std::vector>> P2MRBuilder::GetTreeTuples() const +{ + assert(IsComplete()); + std::vector>> tuples; + if (m_branch.size()) { + const auto& leaves = m_branch[0]->leaves; + for (const auto& leaf : leaves) { + assert(leaf.merkle_branch.size() <= P2MR_CONTROL_MAX_NODE_COUNT); + uint8_t depth = (uint8_t)leaf.merkle_branch.size(); + uint8_t leaf_ver = (uint8_t)leaf.leaf_version; + tuples.emplace_back(depth, leaf_ver, leaf.script); + } + } + return tuples; +} diff --git a/src/script/signingprovider.h b/src/script/signingprovider.h index 6c5b41696..46d685b41 100644 --- a/src/script/signingprovider.h +++ b/src/script/signingprovider.h @@ -147,6 +147,52 @@ class TaprootBuilder */ std::optional, int>>> InferTaprootTree(const TaprootSpendData& spenddata, const XOnlyPubKey& output); +/** BIP360 P2MR spending data. Similar to TaprootSpendData but without the internal key. */ +struct P2MRSpendData +{ + uint256 merkle_root; + std::map, int>, std::set, ShortestVectorFirstComparator>> scripts; + void Merge(P2MRSpendData other); +}; + +/** Utility class to construct P2MR outputs from a script tree (no internal key). */ +class P2MRBuilder +{ +private: + struct LeafInfo + { + std::vector script; + int leaf_version; + std::vector merkle_branch; + }; + + struct NodeInfo + { + uint256 hash; + std::vector leaves; + }; + + bool m_valid = true; + std::vector> m_branch; + uint256 m_merkle_root; + + static NodeInfo Combine(NodeInfo&& a, NodeInfo&& b); + void Insert(NodeInfo&& node, int depth); + +public: + P2MRBuilder& Add(int depth, Span script, int leaf_version, bool track = true); + P2MRBuilder& AddOmitted(int depth, const uint256& hash); + P2MRBuilder& Finalize(); + + bool IsValid() const { return m_valid; } + bool IsComplete() const { return m_valid && (m_branch.size() == 0 || (m_branch.size() == 1 && m_branch[0].has_value())); } + WitnessV2P2MR GetOutput(); + static bool ValidDepths(const std::vector& depths); + P2MRSpendData GetSpendData() const; + std::vector>> GetTreeTuples() const; + bool HasScripts() const { return !m_branch.empty(); } +}; + /** An interface to be implemented by keystores that support signing. */ class SigningProvider { diff --git a/src/script/solver.cpp b/src/script/solver.cpp index 48480a665..bfd6bc777 100644 --- a/src/script/solver.cpp +++ b/src/script/solver.cpp @@ -28,6 +28,7 @@ std::string GetTxnOutputType(TxoutType t) case TxoutType::WITNESS_V0_KEYHASH: return "witness_v0_keyhash"; case TxoutType::WITNESS_V0_SCRIPTHASH: return "witness_v0_scripthash"; case TxoutType::WITNESS_V1_TAPROOT: return "witness_v1_taproot"; + case TxoutType::WITNESS_V2_P2MR: return "witness_v2_p2mr"; case TxoutType::WITNESS_UNKNOWN: return "witness_unknown"; case TxoutType::DILITHIUM_PUBKEY: return "dilithium_pubkey"; case TxoutType::DILITHIUM_PUBKEYHASH: return "dilithium_pubkeyhash"; @@ -236,6 +237,10 @@ TxoutType Solver(const CScript& scriptPubKey, std::vector{(unsigned char)witnessversion}); vSolutionsRet.push_back(std::move(witnessprogram)); diff --git a/src/script/solver.h b/src/script/solver.h index bff046553..ce386b348 100644 --- a/src/script/solver.h +++ b/src/script/solver.h @@ -30,6 +30,7 @@ enum class TxoutType { WITNESS_V0_SCRIPTHASH, WITNESS_V0_KEYHASH, WITNESS_V1_TAPROOT, + WITNESS_V2_P2MR, //!< BIP360 Pay-to-Merkle-Root (quantum-resistant script tree) WITNESS_UNKNOWN, //!< Only for Witness versions not already defined above // Dilithium transaction types: DILITHIUM_PUBKEY, diff --git a/src/validation.cpp b/src/validation.cpp index def9da896..7d4c9fc42 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2117,6 +2117,9 @@ static unsigned int GetBlockScriptFlags(const CBlockIndex& block_index, const Ch // BTQ: Enable Dilithium signature validation flags |= SCRIPT_VERIFY_DILITHIUM; + + // BTQ: Enable BIP360 P2MR (Pay-to-Merkle-Root) validation + flags |= SCRIPT_VERIFY_P2MR; const auto it{consensusparams.script_flag_exceptions.find(*Assert(block_index.phashBlock))}; if (it != consensusparams.script_flag_exceptions.end()) { diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp index be7fc306c..6c6164d69 100644 --- a/src/wallet/rpc/addresses.cpp +++ b/src/wallet/rpc/addresses.cpp @@ -475,6 +475,7 @@ class DescribeWalletAddressVisitor } UniValue operator()(const WitnessV1Taproot& id) const { return UniValue(UniValue::VOBJ); } + UniValue operator()(const WitnessV2P2MR& id) const { return UniValue(UniValue::VOBJ); } UniValue operator()(const WitnessUnknown& id) const { return UniValue(UniValue::VOBJ); } // Dilithium destination operators diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 6c3bec1ce..237f68bdb 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -151,6 +151,7 @@ IsMineResult IsMineInner(const LegacyScriptPubKeyMan& keystore, const CScript& s case TxoutType::NULL_DATA: case TxoutType::WITNESS_UNKNOWN: case TxoutType::WITNESS_V1_TAPROOT: + case TxoutType::WITNESS_V2_P2MR: break; case TxoutType::PUBKEY: keyID = CPubKey(vSolutions[0]).GetID(); diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index a3ad623b5..73dd87ed1 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -240,6 +240,7 @@ static OutputType GetOutputType(TxoutType type, bool is_from_p2sh) { switch (type) { case TxoutType::WITNESS_V1_TAPROOT: + case TxoutType::WITNESS_V2_P2MR: return OutputType::BECH32M; case TxoutType::WITNESS_V0_KEYHASH: case TxoutType::WITNESS_V0_SCRIPTHASH: diff --git a/test/functional/feature_p2mr.py b/test/functional/feature_p2mr.py new file mode 100755 index 000000000..ae9a6809d --- /dev/null +++ b/test/functional/feature_p2mr.py @@ -0,0 +1,704 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The BTQ Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test BIP360 P2MR (Pay-to-Merkle-Root) output type. + +BIP-level comprehensive test suite covering: + 1. P2MR output format and funding + 2. Script path spending with mining confirmation + 3. No key path (reject 0 or 1 witness elements) + 4. Dilithium enabled in P2MR, blocked in P2TR (all 5 opcodes) + 5. Dilithium error message verification + 6. OP_CHECKMULTISIG still blocked in P2MR tapscript + 7. Two-leaf and four-leaf script trees + 8. Invalid Merkle proofs + 9. Invalid control block sizes + 10. Control block format and P2TR size comparison + 11. Parity bit enforcement (BIP360 requires bit = 1) + 12. P2MR address encoding + 13. Multiple P2MR inputs in one transaction +""" + +from test_framework.blocktools import COINBASE_MATURITY +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + SEQUENCE_FINAL, +) +from test_framework.script import ( + CScript, + CScriptOp, + LEAF_VERSION_TAPSCRIPT, + OP_0, + OP_1, + OP_2, + OP_3, + OP_CHECKMULTISIG, + OP_CHECKSIG, + OP_CHECKSIGDILITHIUM, + OP_CHECKSIGDILITHIUMVERIFY, + OP_CHECKMULTISIGDILITHIUM, + OP_CHECKMULTISIGDILITHIUMVERIFY, + OP_DILITHIUM_PUBKEY, + OP_DROP, + OP_DUP, + OP_EQUAL, + OP_HASH160, + OP_RETURN, + OP_TRUE, + TaggedHash, + p2mr_construct, + taproot_construct, +) +from test_framework.key import ECKey, compute_xonly_pubkey +from test_framework.test_framework import BTQTestFramework +from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet + +# ============================================================================ +# Display helpers +# ============================================================================ +BLUE = '\033[94m' +GREEN = '\033[92m' +YELLOW = '\033[93m' +RED = '\033[91m' +BOLD = '\033[1m' +DIM = '\033[2m' +RESET = '\033[0m' +CHECK = f'{GREEN}\u2714{RESET}' + +BANNER = f""" +{BLUE}{BOLD} + ____ _____ ___ ____ ____ __ __ ____ +| __ )|_ _/ _ \\ | _ \\|___ \\| \\/ | _ \\ +| _ \\ | || | | | | |_) | __) | |\\/| | |_) | +| |_) | | || |_| | | __/ / __/| | | | _ < +|____/ |_| \\__\\_\\ |_| |_____|_| |_|_| \\_\\ + + Pay-to-Merkle-Root | BIP 360 | SegWit v2 +{RESET} +{BOLD} BIP-Level Integration Test Suite{RESET} +{DIM} Quantum-resistant script tree output type{RESET} +""" + +SEP = f'{BLUE}{"=" * 78}{RESET}' +SEP_THIN = f'{BLUE}{"-" * 78}{RESET}' + +def section(t): + return f'\n{SEP}\n{BOLD} {t}{RESET}\n{SEP}' + +def sub(t): + return f'{SEP_THIN}\n {YELLOW}\u25B6{RESET} {BOLD}{t}{RESET}' + +def ok(m=''): + return f' {CHECK} {GREEN}PASSED{RESET} {m}' + +def dot(m): + return f' {DIM}\u2502{RESET} {m}' + +def result_line(label, value): + return f' {DIM}\u2502{RESET} {DIM}{label:30s}{RESET} {value}' + +def why(m): + return f' {DIM}\u2502 WHY: {m}{RESET}' + +def how(m): + return f' {DIM}\u2502 HOW: {m}{RESET}' + +def expect(m): + return f' {DIM}\u2502 EXPECT: {m}{RESET}' + +def got(m): + return f' {DIM}\u2502 GOT: {m}{RESET}' + + +class P2MRTest(BTQTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [['-acceptnonstdtxn=1']] + + def run_test(self): + print(BANNER) + self.node = self.nodes[0] + self.wallet = MiniWallet(self.node) + self.generate(self.wallet, COINBASE_MATURITY + 60) + + self.test_01_output_format() + self.test_02_script_path_spend_mined() + self.test_03_no_key_path() + self.test_04_dilithium_p2mr_vs_p2tr() + self.test_05_dilithium_error_messages() + self.test_06_checkmultisig_blocked() + self.test_07_two_leaf_tree() + self.test_08_four_leaf_tree() + self.test_09_wrong_merkle_proof() + self.test_10_wrong_merkle_root() + self.test_11_invalid_control_block_sizes() + self.test_12_control_block_format() + self.test_13_parity_bit() + self.test_14_address_encoding() + self.test_15_multiple_inputs() + + print(section('ALL TESTS COMPLETE')) + print(f'\n {CHECK} {GREEN}{BOLD}All 15 BIP360 P2MR test groups passed!{RESET}\n') + + # ================================================================ + # 1 - Output Format + # ================================================================ + def test_01_output_format(self): + print(section('TEST 1 \u2014 P2MR Output Format')) + + print(sub('1a. Verify P2MR scriptPubKey structure')) + print(why('P2MR outputs must be exactly 34 bytes: OP_2 (0x52) + PUSH32 (0x20) + 32-byte Merkle root.')) + print(how('Construct a P2MR with a single OP_TRUE leaf and inspect the raw scriptPubKey bytes.')) + p2mr = p2mr_construct([("leaf", CScript([OP_TRUE]))]) + spk = bytes(p2mr.scriptPubKey) + assert spk[0] == 0x52 and spk[1] == 0x20 and len(spk) == 34 + print(result_line('Byte 0 (witness version):', f'0x{spk[0]:02x} (OP_2)')) + print(result_line('Byte 1 (push length):', f'0x{spk[1]:02x} (32 bytes)')) + print(result_line('Total size:', f'{len(spk)} bytes')) + print(result_line('Merkle root:', f'{spk[2:].hex()[:32]}...')) + print(ok()) + + print(sub('1b. P2MR and P2TR outputs are the same size')) + print(why('P2MR should not increase UTXO set size compared to P2TR.')) + print(how('Compare raw scriptPubKey sizes of P2MR and P2TR with identical leaf scripts.')) + pk = ECKey(); pk.generate() + xo, _ = compute_xonly_pubkey(pk.get_bytes()) + tap = taproot_construct(xo, [("leaf", CScript([OP_TRUE]))]) + print(result_line('P2TR scriptPubKey size:', f'{len(bytes(tap.scriptPubKey))} bytes')) + print(result_line('P2MR scriptPubKey size:', f'{len(spk)} bytes')) + assert len(bytes(tap.scriptPubKey)) == 34 + print(ok('Both are 34 bytes')) + + # ================================================================ + # 2 - Script Path Spend with Mining Confirmation + # ================================================================ + def test_02_script_path_spend_mined(self): + print(section('TEST 2 \u2014 Script Path Spend (mempool + mined)')) + + print(sub('2a. Spend OP_TRUE leaf and confirm in a block')) + print(why('Verifies P2MR script path spending works at both the mempool policy and consensus levels.')) + print(how('Create P2MR with OP_TRUE leaf, fund it, spend via script path, mine, check tx is in block.')) + p2mr = p2mr_construct([("leaf", CScript([OP_TRUE]))]) + fund = self.fund(p2mr.scriptPubKey) + r = self.spend_p2mr(fund, p2mr, "leaf", []) + assert r['accepted'], f"Should accept: {r['error']}" + print(result_line('Mempool accepted:', 'yes')) + self.mine_and_verify(r['txid']) + print(result_line('Mined in block:', 'yes')) + print(result_line('Txid:', f'{r["txid"][:24]}...')) + print(ok('Spend confirmed at consensus level')) + + print(sub('2b. Hash lock script: OP_EQUAL with secret preimage')) + print(why('Verifies non-trivial script execution works inside a P2MR output.')) + print(how('Create P2MR leaf [ OP_EQUAL], spend by pushing the matching secret as witness.')) + secret = b'quantum_safe_btq' + p2mr = p2mr_construct([("leaf", CScript([secret, OP_EQUAL]))]) + fund = self.fund(p2mr.scriptPubKey) + r = self.spend_p2mr(fund, p2mr, "leaf", [secret]) + assert r['accepted'] + self.mine_and_verify(r['txid']) + print(result_line('Secret:', secret.decode())) + print(result_line('Mempool + mined:', 'yes')) + print(ok('Hash lock spend confirmed')) + + # ================================================================ + # 3 - No Key Path + # ================================================================ + def test_03_no_key_path(self): + print(section('TEST 3 \u2014 No Key Path (P2MR is script-path only)')) + p2mr = p2mr_construct([("leaf", CScript([OP_TRUE]))]) + + print(sub('3a. Empty witness (0 elements)')) + print(why('P2MR requires at least 2 witness elements (script + control block). Zero is invalid.')) + print(how('Submit a transaction spending a P2MR output with an empty witness stack.')) + print(expect('Rejection with "witness" or "empty" error')) + fund = self.fund(p2mr.scriptPubKey) + r = self.raw_spend(fund, p2mr.scriptPubKey, []) + assert not r['accepted'] + assert 'witness' in r['error'].lower() or 'empty' in r['error'].lower() + print(got(f'{r["error"][:70]}')) + print(ok('Empty witness correctly rejected')) + + print(sub('3b. Single witness element (simulating a Taproot key-path spend)')) + print(why('In Taproot, a single witness element = key path spend. P2MR has no key path, so this must fail.')) + print(how('Submit a 64-byte fake signature as the only witness element.')) + print(expect('Rejection with "mismatch" error')) + fund = self.fund(p2mr.scriptPubKey) + r = self.raw_spend(fund, p2mr.scriptPubKey, [b'\x00' * 64]) + assert not r['accepted'] + assert 'mismatch' in r['error'].lower() or 'witness' in r['error'].lower() + print(got(f'{r["error"][:70]}')) + print(ok('Key-path-style spend correctly rejected')) + + # ================================================================ + # 4 - Dilithium: P2MR vs P2TR (all 5 opcodes) + # ================================================================ + def test_04_dilithium_p2mr_vs_p2tr(self): + print(section('TEST 4 \u2014 Dilithium Opcodes: P2MR (allow) vs P2TR (block)')) + print(dot('This test verifies each of the 5 Dilithium opcodes is:')) + print(dot(' - BLOCKED in P2TR tapscript (quantum-vulnerable key path makes it pointless)')) + print(dot(' - ALLOWED in P2MR tapscript (no key path, so Dilithium provides real protection)')) + print(dot('')) + + pk = ECKey(); pk.generate() + xo, _ = compute_xonly_pubkey(pk.get_bytes()) + + opcodes = [ + ('OP_CHECKSIGDILITHIUM', OP_CHECKSIGDILITHIUM, '0xbb'), + ('OP_CHECKSIGDILITHIUMVERIFY', OP_CHECKSIGDILITHIUMVERIFY, '0xbc'), + ('OP_CHECKMULTISIGDILITHIUM', OP_CHECKMULTISIGDILITHIUM, '0xbd'), + ('OP_CHECKMULTISIGDILITHIUMVERIFY', OP_CHECKMULTISIGDILITHIUMVERIFY,'0xbe'), + ('OP_DILITHIUM_PUBKEY', OP_DILITHIUM_PUBKEY, '0xbf'), + ] + for name, op, hex_val in opcodes: + print(sub(f'{name} ({hex_val})')) + leaf = CScript([OP_TRUE, OP_TRUE, op]) + + # P2TR test + print(how(f'Place [{name}] in a P2TR tapscript leaf and attempt to spend')) + print(expect('Rejection mentioning "dilithium"')) + tap = taproot_construct(xo, [("l", leaf)]) + f = self.fund(tap.scriptPubKey) + r = self.spend_taproot(f, tap, "l", [b'\x01', b'\x01']) + assert not r['accepted'] + assert 'dilithium' in r['error'].lower(), f'P2TR error should mention Dilithium: {r["error"][:60]}' + print(got(f'P2TR rejected: {r["error"][:55]}')) + + # P2MR test + print(how(f'Place same [{name}] in a P2MR tapscript leaf and attempt to spend')) + print(expect('NOT rejected for "not available in tapscript" (may fail for other reasons like invalid sig)')) + p2mr = p2mr_construct([("l", leaf)]) + f2 = self.fund(p2mr.scriptPubKey) + r2 = self.spend_p2mr(f2, p2mr, "l", [b'\x01', b'\x01']) + if not r2['accepted']: + assert 'not available in tapscript' not in r2['error'].lower(), \ + f'{name} blocked in P2MR! {r2["error"]}' + print(got(f'P2MR ran opcode, failed on sig/pubkey: {r2["error"][:45]}')) + else: + print(got('P2MR accepted')) + print(ok(f'{name}: P2TR=blocked, P2MR=allowed')) + + # ================================================================ + # 5 - Error Message Verification + # ================================================================ + def test_05_dilithium_error_messages(self): + print(section('TEST 5 \u2014 Error Message Verification')) + pk = ECKey(); pk.generate() + xo, _ = compute_xonly_pubkey(pk.get_bytes()) + + print(sub('5a. P2TR must return exact error message')) + print(why('The error message distinguishes "Dilithium blocked in tapscript" from other failures.')) + print(how('Attempt OP_CHECKSIGDILITHIUM in P2TR and check the exact error string.')) + expected_msg = 'Dilithium opcodes are not available in tapscript' + print(expect(f'Error contains: "{expected_msg}"')) + leaf = CScript([OP_TRUE, OP_TRUE, OP_CHECKSIGDILITHIUM]) + tap = taproot_construct(xo, [("l", leaf)]) + f = self.fund(tap.scriptPubKey) + r = self.spend_taproot(f, tap, "l", [b'\x01', b'\x01']) + assert expected_msg in r['error'], f'Expected "{expected_msg}" in: {r["error"][:80]}' + print(got(f'"{expected_msg}"')) + print(ok('Exact error message verified')) + + print(sub('5b. P2MR must NOT return that error message')) + print(why('If P2MR returned the same error, it would mean Dilithium is blocked in P2MR too.')) + print(how('Same opcode in P2MR: check error is about sig/pubkey, not about opcode being blocked.')) + print(expect(f'Error does NOT contain: "{expected_msg}"')) + p2mr = p2mr_construct([("l", leaf)]) + f2 = self.fund(p2mr.scriptPubKey) + r2 = self.spend_p2mr(f2, p2mr, "l", [b'\x01', b'\x01']) + if not r2['accepted']: + assert expected_msg not in r2['error'], f'P2MR must not have tapscript error: {r2["error"][:80]}' + print(got(f'Error: {r2["error"][:60]}')) + else: + print(got('Accepted (no error)')) + print(ok('P2MR error correctly differs from P2TR')) + + # ================================================================ + # 6 - OP_CHECKMULTISIG Blocked + # ================================================================ + def test_06_checkmultisig_blocked(self): + print(section('TEST 6 \u2014 OP_CHECKMULTISIG Blocked in P2MR Tapscript')) + + print(sub('6a. ECDSA OP_CHECKMULTISIG in P2MR tapscript')) + print(why('BIP342 disables OP_CHECKMULTISIG in tapscript (use OP_CHECKSIGADD instead).')) + print(why('P2MR inherits all tapscript rules, so OP_CHECKMULTISIG must be blocked here too.')) + print(how('Create P2MR leaf with [OP_0 OP_1 OP_1 OP_CHECKMULTISIG] and try to spend.')) + print(expect('Rejection mentioning "checkmultisig"')) + leaf = CScript([OP_0, OP_1, OP_1, OP_CHECKMULTISIG]) + p2mr = p2mr_construct([("l", leaf)]) + f = self.fund(p2mr.scriptPubKey) + r = self.spend_p2mr(f, p2mr, "l", [b'', b'\x01']) + assert not r['accepted'] + assert 'checkmultisig' in r['error'].lower(), f'Error should mention checkmultisig: {r["error"][:70]}' + print(got(f'{r["error"][:65]}')) + print(ok('OP_CHECKMULTISIG correctly blocked (use OP_CHECKSIGADD in tapscript)')) + + # ================================================================ + # 7 - Two-Leaf Tree + # ================================================================ + def test_07_two_leaf_tree(self): + print(section('TEST 7 \u2014 Two-Leaf Script Tree')) + print(why('Verifies the Merkle tree construction works with 2 leaves at depth 1.')) + print(how('Build tree with leaf_a=[OP_TRUE] and leaf_b=[OP_DROP OP_TRUE]. Spend each independently.')) + la = CScript([OP_TRUE]) + lb = CScript([OP_DROP, OP_TRUE]) + p2mr = p2mr_construct([("a", la), ("b", lb)]) + + for name, stk, desc in [("a", [], "OP_TRUE (no witness data)"), ("b", [b'\x01'], "OP_DROP OP_TRUE (1 stack element)")]: + print(sub(f'7. Spend leaf "{name}" ({desc})')) + print(how(f'Fund P2MR, spend via leaf "{name}" with witness stack {stk}, mine and verify.')) + f = self.fund(p2mr.scriptPubKey) + r = self.spend_p2mr(f, p2mr, name, stk) + assert r['accepted'], f'Leaf {name}: {r["error"]}' + self.mine_and_verify(r['txid']) + pl = len(p2mr.leaves[name].merklebranch) // 32 + print(result_line('Merkle path length:', f'{pl} node(s)')) + print(result_line('Mined in block:', 'yes')) + print(ok()) + + # ================================================================ + # 8 - Four-Leaf Tree (Depth 2) + # ================================================================ + def test_08_four_leaf_tree(self): + print(section('TEST 8 \u2014 Four-Leaf Tree (Depth 2)')) + print(why('Verifies deeper Merkle trees work correctly with 2 levels of branching.')) + print(how('Build a balanced tree: [[a, b], [c, d]]. Each leaf is a distinct anyone-can-spend script.')) + la = CScript([OP_TRUE]) + lb = CScript([OP_DROP, OP_TRUE]) + lc = CScript([OP_2, OP_DROP, OP_TRUE]) + ld = CScript([OP_1, OP_DROP, OP_TRUE]) + p2mr = p2mr_construct([[("a", la), ("b", lb)], [("c", lc), ("d", ld)]]) + print(dot('Tree structure: [[a, b], [c, d]]')) + print(dot(f'Merkle root: {p2mr.merkle_root.hex()[:32]}...')) + print(dot('')) + + for name, stk in [("a", []), ("b", [b'\x01']), ("c", []), ("d", [])]: + print(sub(f'8. Leaf "{name}" at depth 2')) + print(how(f'Spend via leaf "{name}", verify Merkle proof and mine.')) + f = self.fund(p2mr.scriptPubKey) + r = self.spend_p2mr(f, p2mr, name, stk) + if not r['accepted']: + print(got(f'Error: {r["error"][:70]}')) + assert r['accepted'], f'Leaf {name}: {r["error"][:80]}' + self.mine_and_verify(r['txid']) + pl = len(p2mr.leaves[name].merklebranch) // 32 + print(result_line('Merkle path depth:', f'{pl} nodes')) + print(result_line('Confirmed in block:', 'yes')) + print(ok()) + + # ================================================================ + # 9 - Wrong Merkle Proof + # ================================================================ + def test_09_wrong_merkle_proof(self): + print(section('TEST 9 \u2014 Invalid Merkle Proof')) + + print(sub('9a. Corrupted Merkle path bytes')) + print(why('If an attacker flips bits in the Merkle path, the computed root will not match the output.')) + print(how('Build valid 2-leaf P2MR, flip first and last bytes of the Merkle branch, try to spend.')) + print(expect('Rejection with "mismatch" (computed Merkle root != witness program)')) + p2mr = p2mr_construct([("a", CScript([OP_TRUE])), ("b", CScript([OP_DROP, OP_TRUE]))]) + f = self.fund(p2mr.scriptPubKey) + leaf = p2mr.leaves["a"] + bad_branch = bytearray(leaf.merklebranch) + if len(bad_branch) > 0: + bad_branch[0] ^= 0xff + r = self.spend_raw_cb(f, p2mr.scriptPubKey, bytes(leaf.script), + bytes([leaf.version | 1]) + bytes(bad_branch), []) + assert not r['accepted'] + assert 'mismatch' in r['error'].lower() + print(got(f'{r["error"][:65]}')) + print(ok('Corrupted Merkle proof rejected')) + + # ================================================================ + # 10 - Wrong Merkle Root (cross-tree) + # ================================================================ + def test_10_wrong_merkle_root(self): + print(section('TEST 10 \u2014 Cross-Tree Spend')) + + print(sub('10a. Use a valid leaf+proof from tree B to spend tree A')) + print(why('Each P2MR output commits to a specific Merkle root. A proof from a different tree must fail.')) + print(how('Build two P2MR trees with different leaf scripts. Fund tree A, try to spend with tree B proof.')) + print(expect('Rejection with "mismatch"')) + ta = p2mr_construct([("l", CScript([OP_TRUE]))]) + tb = p2mr_construct([("l", CScript([OP_1, OP_DROP, OP_TRUE]))]) + print(result_line('Tree A root:', f'{ta.merkle_root.hex()[:32]}...')) + print(result_line('Tree B root:', f'{tb.merkle_root.hex()[:32]}...')) + f = self.fund(ta.scriptPubKey) + r = self.spend_p2mr(f, tb, "l", []) + assert not r['accepted'] + assert 'mismatch' in r['error'].lower() + print(got(f'{r["error"][:65]}')) + print(ok('Cross-tree spend rejected')) + + # ================================================================ + # 11 - Invalid Control Block Sizes + # ================================================================ + def test_11_invalid_control_block_sizes(self): + print(section('TEST 11 \u2014 Invalid Control Block Sizes')) + print(why('P2MR control blocks must be exactly 1 + 32*m bytes (m = 0, 1, 2, ..., 128).')) + print(why('Sizes that do not match this formula must be rejected.')) + p2mr = p2mr_construct([("l", CScript([OP_TRUE]))]) + cases = [ + ('0 bytes (empty)', b'', 'too small, no control byte'), + ('2 bytes', b'\xc1\x00', 'not 1 + 32*m (remainder = 1)'), + ('10 bytes', b'\xc1' + b'\x00' * 9, 'not 1 + 32*m (remainder = 9)'), + ('34 bytes', b'\xc1' + b'\x00' * 33, 'not 1 + 32*m (remainder = 1)'), + ] + for label, bad_cb, reason in cases: + print(sub(f'11. Control block = {label}')) + print(how(f'Submit spend with malformed control block ({reason}).')) + print(expect('Rejection')) + f = self.fund(p2mr.scriptPubKey) + r = self.spend_raw_cb(f, p2mr.scriptPubKey, bytes(CScript([OP_TRUE])), bad_cb, []) + assert not r['accepted'] + print(got(f'{r["error"][:65]}')) + print(ok()) + + # ================================================================ + # 12 - Control Block Format + # ================================================================ + def test_12_control_block_format(self): + print(section('TEST 12 \u2014 Control Block Format')) + + print(sub('12a. Single leaf: control block = 1 byte (just the control byte)')) + print(why('With only one leaf, no Merkle path is needed. Control block = [control_byte].')) + p1 = p2mr_construct([("l", CScript([OP_TRUE]))]) + cb1 = 1 + len(p1.leaves["l"].merklebranch) + assert cb1 == 1 + print(result_line('Control block size:', f'{cb1} byte')) + print(ok()) + + print(sub('12b. Two leaves: control block = 33 bytes (1 byte + 32-byte sibling hash)')) + print(why('With two leaves, the Merkle proof contains one 32-byte sibling hash.')) + p2 = p2mr_construct([("a", CScript([OP_TRUE])), ("b", CScript([OP_DROP, OP_TRUE]))]) + cb2 = 1 + len(p2.leaves["a"].merklebranch) + assert cb2 == 33 + print(result_line('Control block size:', f'{cb2} bytes (1 + 32)')) + print(ok()) + + print(sub('12c. P2MR control block is 32 bytes smaller than P2TR')) + print(why('P2TR control block includes the 32-byte internal key. P2MR omits it (no key path).')) + print(how('Build identical 2-leaf trees in P2TR and P2MR, compare control block sizes.')) + pk = ECKey(); pk.generate() + xo, _ = compute_xonly_pubkey(pk.get_bytes()) + tap = taproot_construct(xo, [("a", CScript([OP_TRUE])), ("b", CScript([OP_DROP, OP_TRUE]))]) + tr_cb = 1 + 32 + len(tap.leaves["a"].merklebranch) + mr_cb = 1 + len(p2.leaves["a"].merklebranch) + savings = tr_cb - mr_cb + assert savings == 32 + print(result_line('P2TR control block:', f'{tr_cb} bytes (1 + 32 internal_key + 32 path)')) + print(result_line('P2MR control block:', f'{mr_cb} bytes (1 + 32 path)')) + print(result_line('Savings per spend:', f'{savings} bytes')) + print(ok('P2MR is 32 bytes more efficient per script path spend')) + + # ================================================================ + # 13 - Parity Bit Enforcement + # ================================================================ + def test_13_parity_bit(self): + print(section('TEST 13 \u2014 BIP360 Parity Bit Enforcement')) + print(why('BIP360 specifies that the parity bit (bit 0 of the control byte) must always be 1.')) + print(why('This is because P2MR has no internal key, so there is no parity to encode.')) + print(why('The bit is fixed at 1 to maintain encoding compatibility with Taproot leaf versions.')) + + p2mr = p2mr_construct([("l", CScript([OP_TRUE]))]) + leaf = p2mr.leaves["l"] + + print(sub('13a. Parity bit = 1 (correct per BIP360)')) + print(how('Construct control byte with bit 0 = 1, spend, mine.')) + print(expect('Accepted and confirmed in block')) + f = self.fund(p2mr.scriptPubKey) + cb_good = bytes([leaf.version | 1]) + leaf.merklebranch + print(result_line('Control byte:', f'0x{cb_good[0]:02x} (leaf_version | 1, bit 0 = 1)')) + r = self.spend_raw_cb(f, p2mr.scriptPubKey, bytes(leaf.script), cb_good, []) + assert r['accepted'], f'Parity=1 should accept: {r["error"]}' + self.mine_and_verify(r['txid']) + print(result_line('Result:', 'accepted + mined')) + print(ok()) + + print(sub('13b. Parity bit = 0 (violates BIP360)')) + print(how('Construct control byte with bit 0 = 0, attempt to spend.')) + print(expect('Rejection with "mismatch" error')) + f2 = self.fund(p2mr.scriptPubKey) + cb_bad = bytes([leaf.version & 0xfe]) + leaf.merklebranch + print(result_line('Control byte:', f'0x{cb_bad[0]:02x} (leaf_version & 0xfe, bit 0 = 0)')) + r2 = self.spend_raw_cb(f2, p2mr.scriptPubKey, bytes(leaf.script), cb_bad, []) + assert not r2['accepted'] + assert 'mismatch' in r2['error'].lower() + print(result_line('Result:', 'rejected')) + print(got(f'{r2["error"][:65]}')) + print(ok('Parity bit enforced per BIP360 spec')) + + # ================================================================ + # 14 - Address Encoding + # ================================================================ + def test_14_address_encoding(self): + print(section('TEST 14 \u2014 P2MR Address Encoding')) + + print(sub('14a. P2MR address uses SegWit v2 encoding')) + print(why('P2MR uses SegWit version 2. In bech32m, version 2 maps to the character "z" after the separator.')) + print(why('BTQ uses HRP "qcrt" for regtest, so P2MR addresses start with "qcrt1z".')) + print(how('Fund a P2MR output, look up the transaction, and inspect the address field.')) + p2mr = p2mr_construct([("l", CScript([OP_TRUE]))]) + fund = self.wallet.send_to(from_node=self.node, scriptPubKey=p2mr.scriptPubKey, amount=50_000) + block_hashes = self.generate(self.node, 1) + tx_info = self.node.getrawtransaction(fund['txid'], True, block_hashes[0]) + p2mr_vout = tx_info['vout'][fund['sent_vout']] + if 'address' in p2mr_vout['scriptPubKey']: + addr = p2mr_vout['scriptPubKey']['address'] + assert '1z' in addr[:10], f'Expected SegWit v2 (..1z..) prefix, got: {addr[:20]}' + print(result_line('Address:', addr)) + print(result_line('Prefix:', f'{addr.split("1z")[0]}1z (SegWit v2)')) + print(ok('P2MR address correctly uses SegWit v2 encoding')) + else: + spk_hex = p2mr_vout['scriptPubKey']['hex'] + assert spk_hex[:4] == '5220', f'Expected OP_2 PUSH32, got: {spk_hex[:4]}' + print(result_line('scriptPubKey hex:', f'{spk_hex[:16]}... (OP_2 + 32 bytes)')) + print(ok('scriptPubKey correctly encodes as SegWit v2')) + + # ================================================================ + # 15 - Multiple P2MR Inputs + # ================================================================ + def test_15_multiple_inputs(self): + print(section('TEST 15 \u2014 Multiple P2MR Inputs in One Transaction')) + + print(sub('15a. Two P2MR inputs from different trees')) + print(why('Real transactions may spend multiple P2MR outputs at once.')) + print(how('Create two P2MR outputs with different scripts, spend both in a single transaction.')) + p2mr_a = p2mr_construct([("l", CScript([OP_TRUE]))]) + p2mr_b = p2mr_construct([("l", CScript([OP_1, OP_DROP, OP_TRUE]))]) + + fa = self.fund(p2mr_a.scriptPubKey) + fb = self.fund(p2mr_b.scriptPubKey) + + tx = CTransaction() + tx.nVersion = 2 + tx.vin = [ + CTxIn(COutPoint(int(fa['txid'], 16), fa['sent_vout']), b'', SEQUENCE_FINAL), + CTxIn(COutPoint(int(fb['txid'], 16), fb['sent_vout']), b'', SEQUENCE_FINAL), + ] + tx.vout = [CTxOut(80_000, p2mr_a.scriptPubKey)] + + leaf_a = p2mr_a.leaves["l"] + cb_a = bytes([leaf_a.version | 1]) + leaf_a.merklebranch + wit_a = CTxInWitness() + wit_a.scriptWitness.stack = [bytes(leaf_a.script), cb_a] + + leaf_b = p2mr_b.leaves["l"] + cb_b = bytes([leaf_b.version | 1]) + leaf_b.merklebranch + wit_b = CTxInWitness() + wit_b.scriptWitness.stack = [bytes(leaf_b.script), cb_b] + + tx.wit.vtxinwit = [wit_a, wit_b] + tx.rehash() + + try: + txid = self.node.sendrawtransaction(tx.serialize().hex()) + print(result_line('Input 0:', f'P2MR tree A (OP_TRUE)')) + print(result_line('Input 1:', f'P2MR tree B (OP_1 OP_DROP OP_TRUE)')) + print(result_line('Mempool accepted:', 'yes')) + self.mine_and_verify(txid) + print(result_line('Mined in block:', 'yes')) + print(ok('Multi-input P2MR transaction works')) + except Exception as e: + assert False, f'Multi-input P2MR should succeed: {e}' + + # ================================================================ + # Helpers + # ================================================================ + + def fund(self, spk): + f = self.wallet.send_to(from_node=self.node, scriptPubKey=spk, amount=50_000) + self.generate(self.node, 1) + return f + + def mine_and_verify(self, txid): + block_hashes = self.generate(self.node, 1) + block = self.node.getblock(block_hashes[0]) + assert txid in block['tx'], f'txid {txid[:16]}... not in mined block' + + def spend_p2mr(self, fund_info, p2mr, leaf_name, witness_stack): + try: + tx = CTransaction() + tx.nVersion = 2 + tx.vin = [CTxIn(COutPoint(int(fund_info['txid'], 16), fund_info['sent_vout']), b'', SEQUENCE_FINAL)] + tx.vout = [CTxOut(40_000, p2mr.scriptPubKey)] + leaf = p2mr.leaves[leaf_name] + cb = bytes([leaf.version | 1]) + leaf.merklebranch + wit = CTxInWitness() + for item in witness_stack: + wit.scriptWitness.stack.append(item) + wit.scriptWitness.stack.append(bytes(leaf.script)) + wit.scriptWitness.stack.append(cb) + tx.wit.vtxinwit = [wit] + tx.rehash() + txid = self.node.sendrawtransaction(tx.serialize().hex()) + return {'accepted': True, 'error': '', 'txid': txid} + except Exception as e: + return {'accepted': False, 'error': str(e), 'txid': ''} + + def spend_taproot(self, fund_info, tap, leaf_name, witness_stack): + try: + tx = CTransaction() + tx.nVersion = 2 + tx.vin = [CTxIn(COutPoint(int(fund_info['txid'], 16), fund_info['sent_vout']), b'', SEQUENCE_FINAL)] + tx.vout = [CTxOut(40_000, tap.scriptPubKey)] + leaf = tap.leaves[leaf_name] + cb = bytes([leaf.version | (1 if tap.negflag else 0)]) + tap.internal_pubkey + for h in leaf.merklebranch: + cb += h + wit = CTxInWitness() + for item in witness_stack: + wit.scriptWitness.stack.append(item) + wit.scriptWitness.stack.append(bytes(leaf.script)) + wit.scriptWitness.stack.append(cb) + tx.wit.vtxinwit = [wit] + tx.rehash() + txid = self.node.sendrawtransaction(tx.serialize().hex()) + return {'accepted': True, 'error': '', 'txid': txid} + except Exception as e: + return {'accepted': False, 'error': str(e), 'txid': ''} + + def raw_spend(self, fund_info, output_spk, witness_stack): + try: + tx = CTransaction() + tx.nVersion = 2 + tx.vin = [CTxIn(COutPoint(int(fund_info['txid'], 16), fund_info['sent_vout']), b'', SEQUENCE_FINAL)] + tx.vout = [CTxOut(40_000, output_spk)] + wit = CTxInWitness() + wit.scriptWitness.stack = witness_stack + tx.wit.vtxinwit = [wit] + tx.rehash() + txid = self.node.sendrawtransaction(tx.serialize().hex()) + return {'accepted': True, 'error': '', 'txid': txid} + except Exception as e: + return {'accepted': False, 'error': str(e), 'txid': ''} + + def spend_raw_cb(self, fund_info, output_spk, script, control_block, witness_stack): + try: + tx = CTransaction() + tx.nVersion = 2 + tx.vin = [CTxIn(COutPoint(int(fund_info['txid'], 16), fund_info['sent_vout']), b'', SEQUENCE_FINAL)] + tx.vout = [CTxOut(40_000, output_spk)] + wit = CTxInWitness() + for item in witness_stack: + wit.scriptWitness.stack.append(item) + wit.scriptWitness.stack.append(script) + wit.scriptWitness.stack.append(control_block) + tx.wit.vtxinwit = [wit] + tx.rehash() + txid = self.node.sendrawtransaction(tx.serialize().hex()) + return {'accepted': True, 'error': '', 'txid': txid} + except Exception as e: + return {'accepted': False, 'error': str(e), 'txid': ''} + + +if __name__ == '__main__': + P2MRTest().main() diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 34bee06d3..f5908e3e3 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -940,3 +940,20 @@ def is_op_success(o): # BTQ: Exclude Dilithium opcodes (0xbb-0xbf) from OP_SUCCESSx range. # Must match IsOpSuccess() in src/script/script.cpp return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xc0 and o <= 0xfe) + +# BIP360 P2MR (Pay-to-Merkle-Root) support +P2MRInfo = namedtuple("P2MRInfo", "scriptPubKey,leaves,merkle_root") +P2MRLeafInfo = namedtuple("P2MRLeafInfo", "script,version,merklebranch,leaf_hash") + +def p2mr_construct(scripts=None): + """Construct a P2MR (BIP360) output from a script tree (no internal key). + + scripts: same format as taproot_construct scripts argument + Returns: a P2MRInfo object with scriptPubKey, leaves, and merkle_root + """ + if scripts is None: + scripts = [] + + ret, h = taproot_tree_helper(scripts) + leaves = dict((name, P2MRLeafInfo(script, version, merklebranch, leaf)) for name, version, script, merklebranch, leaf in ret) + return P2MRInfo(CScript([OP_2, h]), leaves, h) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 5c7aed4e1..e4ea16385 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -99,6 +99,7 @@ 'feature_fee_estimation.py', 'feature_taproot.py', 'feature_dilithium_sigops.py', + 'feature_p2mr.py', 'feature_block.py', # vv Tests less than 2m vv 'mining_getblocktemplate_longpoll.py', diff --git a/test_dilithium_sendmany.sh b/test_dilithium_sendmany.sh new file mode 100755 index 000000000..b85839c15 --- /dev/null +++ b/test_dilithium_sendmany.sh @@ -0,0 +1,194 @@ +#!/bin/bash + +# Test script for sendmany with Dilithium addresses on regtest +# This will verify that the sendmany fix works correctly + +set -e + +BTQD="$(pwd)/src/btqd" +BTQCLI="$(pwd)/src/btq-cli" +NETWORK="-regtest" +WALLET="test_dilithium_sendmany" + +echo "==========================================" +echo "Testing sendmany with Dilithium Addresses" +echo "==========================================" +echo "" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Step 1: Stop any running daemon +echo "Step 1: Cleaning up..." +$BTQCLI $NETWORK stop 2>/dev/null || true +sleep 2 +rm -rf ~/.btq/regtest 2>/dev/null || true +echo "✓ Clean state" +echo "" + +# Step 2: Start daemon +echo "Step 2: Starting btqd in regtest mode..." +$BTQD $NETWORK -daemon -debug=rpc +sleep 3 +echo "✓ Daemon started" +echo "" + +# Step 3: Create test wallet +echo "Step 3: Creating test wallet..." +$BTQCLI $NETWORK createwallet "$WALLET" false false "" false true true +echo "✓ Wallet created: $WALLET" +echo "" + +# Step 4: Generate Dilithium address (pool address) +echo "Step 4: Generating Dilithium pool address..." +POOL_ADDR=$($BTQCLI $NETWORK -rpcwallet="$WALLET" getnewdilithiumaddress) +echo "✓ Pool address: $POOL_ADDR" + +# Check if it's Dilithium +ADDR_INFO=$($BTQCLI $NETWORK -rpcwallet="$WALLET" getaddressinfo "$POOL_ADDR") +IS_DILITHIUM=$(echo "$ADDR_INFO" | grep -o '"isdilithium": true' || echo "") +if [ -z "$IS_DILITHIUM" ]; then + echo -e "${RED}✗ ERROR: Address is not Dilithium!${NC}" + exit 1 +fi +echo "✓ Confirmed Dilithium address" + +# Get scriptPubKey +SCRIPT_PUBKEY=$(echo "$ADDR_INFO" | grep '"scriptPubKey"' | cut -d'"' -f4) +echo "✓ ScriptPubKey: $SCRIPT_PUBKEY" +if [[ $SCRIPT_PUBKEY == *"bb" ]]; then + echo -e "${GREEN}✓ Script ends with 'bb' (OP_CHECKSIGDILITHIUM)${NC}" +else + echo -e "${RED}✗ ERROR: Script doesn't end with 'bb'!${NC}" + exit 1 +fi +echo "" + +# Step 5: Mine blocks to pool address +echo "Step 5: Mining 110 blocks to pool address..." +$BTQCLI $NETWORK -rpcwallet="$WALLET" generatetoaddress 110 "$POOL_ADDR" > /dev/null +echo "✓ Mined 110 blocks" + +# Check balance +BALANCE=$($BTQCLI $NETWORK -rpcwallet="$WALLET" getbalance) +echo "✓ Pool balance: $BALANCE BTQ" +echo "" + +# Step 6: Generate recipient addresses (miners) +echo "Step 6: Generating miner addresses..." +MINER1=$($BTQCLI $NETWORK -rpcwallet="$WALLET" getnewaddress "" "legacy") +MINER2=$($BTQCLI $NETWORK -rpcwallet="$WALLET" getnewaddress "" "legacy") +MINER3=$($BTQCLI $NETWORK -rpcwallet="$WALLET" getnewdilithiumaddress) +echo "✓ Miner 1 (regular): $MINER1" +echo "✓ Miner 2 (regular): $MINER2" +echo "✓ Miner 3 (Dilithium): $MINER3" +echo "" + +# Step 7: List UTXOs +echo "Step 7: Checking UTXOs..." +UTXO_COUNT=$($BTQCLI $NETWORK -rpcwallet="$WALLET" listunspent | grep -c '"spendable": true') +echo "✓ Spendable UTXOs: $UTXO_COUNT" + +# Check if UTXOs are Dilithium +DILITHIUM_UTXO=$($BTQCLI $NETWORK -rpcwallet="$WALLET" listunspent | grep -c 'bb"' || echo "0") +echo "✓ Dilithium UTXOs: $DILITHIUM_UTXO" +echo "" + +# Step 8: Test sendmany with mixed addresses +echo "========================================" +echo "Step 8: TESTING SENDMANY" +echo "========================================" +echo "" + +echo "Test 1: Sending to 2 regular addresses..." +TX1=$($BTQCLI $NETWORK -rpcwallet="$WALLET" sendmany "" "{\"$MINER1\":1.0,\"$MINER2\":2.0}" 2>&1) +if [[ $TX1 == *"error"* ]] || [[ $TX1 == *"Insufficient"* ]]; then + echo -e "${RED}✗ FAILED: $TX1${NC}" + echo "" + echo "Debugging info:" + echo "Balance: $($BTQCLI $NETWORK -rpcwallet="$WALLET" getbalance)" + echo "Listunspent:" + $BTQCLI $NETWORK -rpcwallet="$WALLET" listunspent | head -30 + exit 1 +else + echo -e "${GREEN}✓ SUCCESS: $TX1${NC}" +fi +echo "" + +echo "Test 2: Sending to 1 Dilithium address..." +TX2=$($BTQCLI $NETWORK -rpcwallet="$WALLET" sendmany "" "{\"$MINER3\":0.5}" 2>&1) +if [[ $TX2 == *"error"* ]] || [[ $TX2 == *"Insufficient"* ]]; then + echo -e "${RED}✗ FAILED: $TX2${NC}" + exit 1 +else + echo -e "${GREEN}✓ SUCCESS: $TX2${NC}" +fi +echo "" + +echo "Test 3: Sending to mixed addresses (2 regular + 1 Dilithium)..." +TX3=$($BTQCLI $NETWORK -rpcwallet="$WALLET" sendmany "" "{\"$MINER1\":0.1,\"$MINER2\":0.2,\"$MINER3\":0.3}" 2>&1) +if [[ $TX3 == *"error"* ]] || [[ $TX3 == *"Insufficient"* ]]; then + echo -e "${RED}✗ FAILED: $TX3${NC}" + exit 1 +else + echo -e "${GREEN}✓ SUCCESS: $TX3${NC}" +fi +echo "" + +# Step 9: Verify transactions +echo "Step 9: Verifying transactions..." +for TXID in "$TX1" "$TX2" "$TX3"; do + if [[ $TXID != *"error"* ]]; then + TX_DETAILS=$($BTQCLI $NETWORK -rpcwallet="$WALLET" gettransaction "$TXID" 2>&1) + if [[ $TX_DETAILS == *"confirmations"* ]]; then + echo "✓ Transaction $TXID found in wallet" + else + echo -e "${RED}✗ Transaction $TXID not found!${NC}" + fi + fi +done +echo "" + +# Step 10: Mine a block to confirm +echo "Step 10: Mining block to confirm transactions..." +$BTQCLI $NETWORK -rpcwallet="$WALLET" generatetoaddress 1 "$POOL_ADDR" > /dev/null +echo "✓ Block mined" +echo "" + +# Step 11: Check final balances +echo "Step 11: Checking final balances..." +FINAL_BALANCE=$($BTQCLI $NETWORK -rpcwallet="$WALLET" getbalance) +echo "Pool final balance: $FINAL_BALANCE BTQ" + +echo "" +echo "Miner balances:" +for ADDR in "$MINER1" "$MINER2" "$MINER3"; do + BAL=$($BTQCLI $NETWORK -rpcwallet="$WALLET" getreceivedbyaddress "$ADDR" 0) + echo " $ADDR: $BAL BTQ" +done +echo "" + +# Cleanup +echo "Cleanup: Stopping daemon..." +$BTQCLI $NETWORK stop +sleep 2 + +echo "" +echo "==========================================" +echo -e "${GREEN}✓✓✓ ALL TESTS PASSED! ✓✓✓${NC}" +echo "==========================================" +echo "" +echo "sendmany with Dilithium addresses is WORKING!" +echo "" +echo "This confirms:" +echo " ✓ Dilithium keys properly stored" +echo " ✓ FlatSigningProvider includes Dilithium keys" +echo " ✓ sendmany can spend Dilithium UTXOs" +echo " ✓ Mixed transactions (Dilithium + regular) work" +echo "" +echo "Your BTQ-Core build is ready for production!" +echo "" + From 9ce4962d9bcaf754fd2cde1467ef6589f5ad6cd1 Mon Sep 17 00:00:00 2001 From: barneychambers Date: Fri, 20 Feb 2026 12:58:07 +0000 Subject: [PATCH 3/3] remove logs and fix loadwallet being slow --- src/crypto/dilithium_key.cpp | 49 +----- src/wallet/rpc/addresses.cpp | 2 - src/wallet/rpc/dilithium.cpp | 56 +------ src/wallet/scriptpubkeyman.cpp | 282 ++++++--------------------------- src/wallet/wallet.cpp | 11 ++ src/wallet/wallet.h | 3 + src/wallet/walletdb.cpp | 9 -- 7 files changed, 70 insertions(+), 342 deletions(-) diff --git a/src/crypto/dilithium_key.cpp b/src/crypto/dilithium_key.cpp index a93872a98..f0f2ea1b6 100644 --- a/src/crypto/dilithium_key.cpp +++ b/src/crypto/dilithium_key.cpp @@ -13,7 +13,6 @@ #include #include #include -#include extern "C" { #include "dilithium_wrapper.h" @@ -23,75 +22,43 @@ extern "C" { void CDilithiumKey::MakeNewKey() { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Starting\n"); MakeKeyData(); - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - MakeKeyData completed\n"); - - // Generate random entropy for key generation - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Generating entropy\n"); + std::array entropy; - - // Bitcoin Core's GetStrongRandBytes is limited to 32 bytes, but Dilithium needs 2560 bytes - // We need to generate entropy in chunks and combine them - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Generating entropy in chunks (32 bytes each)\n"); + + // GetStrongRandBytes is limited to 32 bytes; Dilithium needs SECRET_KEY_SIZE bytes constexpr size_t CHUNK_SIZE = 32; constexpr size_t NUM_CHUNKS = DilithiumConstants::SECRET_KEY_SIZE / CHUNK_SIZE; - + for (size_t i = 0; i < NUM_CHUNKS; i++) { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Generating chunk %zu/%zu\n", i + 1, NUM_CHUNKS); try { GetStrongRandBytes(Span(entropy.data() + i * CHUNK_SIZE, CHUNK_SIZE)); - } catch (const std::exception& e) { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - GetStrongRandBytes exception in chunk %zu: %s\n", i + 1, e.what()); - ClearKeyData(); - return; } catch (...) { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - GetStrongRandBytes unknown exception in chunk %zu\n", i + 1); ClearKeyData(); return; } } - - // Handle any remaining bytes + size_t remaining_bytes = DilithiumConstants::SECRET_KEY_SIZE % CHUNK_SIZE; if (remaining_bytes > 0) { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Generating remaining %zu bytes\n", remaining_bytes); try { GetStrongRandBytes(Span(entropy.data() + NUM_CHUNKS * CHUNK_SIZE, remaining_bytes)); - } catch (const std::exception& e) { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - GetStrongRandBytes exception for remaining bytes: %s\n", e.what()); - ClearKeyData(); - return; } catch (...) { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - GetStrongRandBytes unknown exception for remaining bytes\n"); ClearKeyData(); return; } } - - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Entropy generated successfully\n"); - - // Generate public key buffer - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Creating public key buffer\n"); + std::array pk; - - // Generate keypair using Dilithium's btq_dilithium_keypair with proper entropy - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Calling btq_dilithium_keypair\n"); + int result = btq_dilithium_keypair(pk.data(), entropy.data()); - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - btq_dilithium_keypair returned: %d\n", result); - + if (result != 0) { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Key generation failed, clearing key\n"); - // Key generation failed, clear the key ClearKeyData(); } else { - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Key generation successful, storing key material\n"); - // Store the generated key material memcpy(keydata->data(), entropy.data(), DilithiumConstants::SECRET_KEY_SIZE); memcpy(keydata->data() + DilithiumConstants::SECRET_KEY_SIZE, pk.data(), DilithiumConstants::PUBLIC_KEY_SIZE); - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Key material stored successfully\n"); } - LogPrintf("DEBUG: CDilithiumKey::MakeNewKey - Completed\n"); } bool CDilithiumKey::GenerateFromEntropy(const std::vector& entropy) diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp index 6c6164d69..561d3fbe4 100644 --- a/src/wallet/rpc/addresses.cpp +++ b/src/wallet/rpc/addresses.cpp @@ -603,12 +603,10 @@ RPCHelpMan getaddressinfo() const auto& pkh = std::get(dest); keyID = CKeyID(); std::memcpy(keyID.begin(), pkh.begin(), 20); - LogPrintf("DEBUG: getaddressinfo - Looking up DilithiumPKHash keyID: %s\n", keyID.ToString()); } else if (std::holds_alternative(dest)) { const auto& wkh = std::get(dest); keyID = CKeyID(); std::memcpy(keyID.begin(), wkh.begin(), 20); - LogPrintf("DEBUG: getaddressinfo - Looking up DilithiumWitnessV0KeyHash keyID: %s\n", keyID.ToString()); } // Check all ScriptPubKeyMans for Dilithium key diff --git a/src/wallet/rpc/dilithium.cpp b/src/wallet/rpc/dilithium.cpp index 0393a86c1..a183d3e78 100644 --- a/src/wallet/rpc/dilithium.cpp +++ b/src/wallet/rpc/dilithium.cpp @@ -226,111 +226,70 @@ RPCHelpMan getnewdilithiumaddress() output_type = *parsed; } - // Generate new Dilithium key - LogPrintf("DEBUG: getnewdilithiumaddress - Starting key generation\n"); CDilithiumKey dilithium_key; - LogPrintf("DEBUG: getnewdilithiumaddress - Calling MakeNewKey()\n"); dilithium_key.MakeNewKey(); - LogPrintf("DEBUG: getnewdilithiumaddress - MakeNewKey() completed\n"); - + if (!dilithium_key.IsValid()) { - LogPrintf("DEBUG: getnewdilithiumaddress - Dilithium key is not valid\n"); throw JSONRPCError(RPC_WALLET_ERROR, "Failed to generate Dilithium key"); } - LogPrintf("DEBUG: getnewdilithiumaddress - Dilithium key is valid\n"); - // Get the public key - LogPrintf("DEBUG: getnewdilithiumaddress - Getting public key\n"); CDilithiumPubKey dilithium_pubkey = dilithium_key.GetPubKey(); - LogPrintf("DEBUG: getnewdilithiumaddress - Got public key\n"); if (!dilithium_pubkey.IsValid()) { - LogPrintf("DEBUG: getnewdilithiumaddress - Dilithium public key is not valid\n"); throw JSONRPCError(RPC_WALLET_ERROR, "Failed to get Dilithium public key"); } - LogPrintf("DEBUG: getnewdilithiumaddress - Dilithium public key is valid\n"); - // Create address based on output type - LogPrintf("DEBUG: getnewdilithiumaddress - Creating address for output type: %d\n", static_cast(output_type)); std::string address; switch (output_type) { case OutputType::LEGACY: { - LogPrintf("DEBUG: getnewdilithiumaddress - Creating legacy address\n"); DilithiumPKHash dilithium_dest(dilithium_pubkey.GetID()); address = EncodeDestination(dilithium_dest); - LogPrintf("DEBUG: getnewdilithiumaddress - Legacy address created: %s\n", address.c_str()); break; } case OutputType::P2SH_SEGWIT: { - LogPrintf("DEBUG: getnewdilithiumaddress - Creating P2SH-SegWit address\n"); DilithiumPKHash dilithium_dest(dilithium_pubkey.GetID()); DilithiumScriptHash script_hash(GetScriptForDestination(dilithium_dest)); address = EncodeDestination(script_hash); - LogPrintf("DEBUG: getnewdilithiumaddress - P2SH-SegWit address created: %s\n", address.c_str()); break; } case OutputType::BECH32: { - LogPrintf("DEBUG: getnewdilithiumaddress - Creating Bech32 address\n"); DilithiumWitnessV0KeyHash witness_dest(dilithium_pubkey); address = EncodeDestination(witness_dest); - LogPrintf("DEBUG: getnewdilithiumaddress - Bech32 address created: %s\n", address.c_str()); break; } default: - LogPrintf("DEBUG: getnewdilithiumaddress - Unsupported output type: %d\n", static_cast(output_type)); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unsupported address type for Dilithium"); } - // Store the Dilithium key in the wallet - LogPrintf("DEBUG: getnewdilithiumaddress - Starting key storage\n"); if (wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { - LogPrintf("DEBUG: getnewdilithiumaddress - Storing in descriptor wallet\n"); - // Store in descriptor wallet bool stored = false; auto spk_mans = wallet->GetAllScriptPubKeyMans(); - LogPrintf("DEBUG: getnewdilithiumaddress - Found %zu script pub key managers\n", spk_mans.size()); for (auto& spk_man : spk_mans) { DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast(spk_man); if (desc_spk_man) { - LogPrintf("DEBUG: getnewdilithiumaddress - Found descriptor script pub key manager\n"); - // Create a dummy CPubKey for compatibility (Dilithium keys don't use CPubKey) CPubKey dummy_pubkey; - LogPrintf("DEBUG: getnewdilithiumaddress - Calling AddDilithiumKeyPubKey\n"); if (desc_spk_man->AddDilithiumKeyPubKey(dilithium_key, dummy_pubkey)) { - LogPrintf("DEBUG: getnewdilithiumaddress - Successfully stored Dilithium key\n"); stored = true; break; - } else { - LogPrintf("DEBUG: getnewdilithiumaddress - Failed to store Dilithium key in descriptor wallet\n"); } } } if (!stored) { - LogPrintf("DEBUG: getnewdilithiumaddress - No descriptor script pub key manager could store the key\n"); throw JSONRPCError(RPC_WALLET_ERROR, "Failed to store Dilithium key in descriptor wallet"); } } else { - LogPrintf("DEBUG: getnewdilithiumaddress - Storing in legacy wallet\n"); - // Store in legacy wallet LegacyScriptPubKeyMan* spk_man = wallet->GetLegacyScriptPubKeyMan(); if (!spk_man) { - LogPrintf("DEBUG: getnewdilithiumaddress - Legacy wallet not available\n"); throw JSONRPCError(RPC_WALLET_ERROR, "Legacy wallet not available"); } - LogPrintf("DEBUG: getnewdilithiumaddress - Calling AddDilithiumKeyPubKey on legacy wallet\n"); if (!spk_man->AddDilithiumKeyPubKey(dilithium_key, CPubKey())) { - LogPrintf("DEBUG: getnewdilithiumaddress - Failed to store Dilithium key in legacy wallet\n"); throw JSONRPCError(RPC_WALLET_ERROR, "Failed to store Dilithium key in legacy wallet"); } - LogPrintf("DEBUG: getnewdilithiumaddress - Successfully stored Dilithium key in legacy wallet\n"); } - // Set the label if (!label.empty()) { - LogPrintf("DEBUG: getnewdilithiumaddress - Setting address book label\n"); wallet->SetAddressBook(DecodeDestination(address), label, AddressPurpose::RECEIVE); } - LogPrintf("DEBUG: getnewdilithiumaddress - Returning address: %s\n", address.c_str()); return address; }, }; @@ -476,33 +435,20 @@ RPCHelpMan signmessagewithdilithium() CDilithiumKey dilithium_key; bool key_found = false; - // Try all script pub key managers (both legacy and descriptor) auto spk_mans = wallet->GetAllScriptPubKeyMans(); - LogPrintf("DEBUG: Message signing - Found %zu script pub key managers\n", spk_mans.size()); - LogPrintf("DEBUG: Message signing - Looking for keyID: %s\n", keyID.ToString()); for (auto& spk_man : spk_mans) { - // Try descriptor wallet first DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast(spk_man); if (desc_spk_man) { - LogPrintf("DEBUG: Message signing - Checking descriptor script pub key manager\n"); if (desc_spk_man->GetDilithiumKey(keyID, dilithium_key)) { - LogPrintf("DEBUG: Message signing - Found Dilithium key in descriptor wallet\n"); key_found = true; break; - } else { - LogPrintf("DEBUG: Message signing - Dilithium key not found in descriptor wallet\n"); } } - // Try legacy wallet LegacyScriptPubKeyMan* legacy_spk_man = dynamic_cast(spk_man); if (legacy_spk_man) { - LogPrintf("DEBUG: Message signing - Checking legacy script pub key manager\n"); if (legacy_spk_man->GetDilithiumKey(keyID, dilithium_key)) { - LogPrintf("DEBUG: Message signing - Found Dilithium key in legacy wallet\n"); key_found = true; break; - } else { - LogPrintf("DEBUG: Message signing - Dilithium key not found in legacy wallet\n"); } } } diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 237f68bdb..ff07c93f9 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -40,39 +40,25 @@ util::Result LegacyScriptPubKeyMan::GetNewDestination(const Outp // Handle Dilithium output types if (type == OutputType::DILITHIUM_LEGACY || type == OutputType::DILITHIUM_BECH32) { - LogPrintf("DEBUG: Generating Dilithium address of type %d\n", (int)type); - - // Generate a new Dilithium key CDilithiumKey dilithium_key; dilithium_key.MakeNewKey(); - + if (!dilithium_key.IsValid()) { - LogPrintf("DEBUG: Failed to generate Dilithium key\n"); return util::Error{_("Error: Failed to generate Dilithium key")}; } - - LogPrintf("DEBUG: Generated Dilithium key successfully\n"); - - // Get the Dilithium public key + CDilithiumPubKey dilithium_pubkey = dilithium_key.GetPubKey(); CPubKey pubkey(dilithium_pubkey.begin(), dilithium_pubkey.end()); - - // Add the key to the wallet + if (!AddDilithiumKeyPubKey(dilithium_key, pubkey)) { - LogPrintf("DEBUG: Failed to add Dilithium key to wallet\n"); return util::Error{_("Error: Failed to add Dilithium key to wallet")}; } - - LogPrintf("DEBUG: Added Dilithium key to wallet successfully\n"); - - // Create destination based on type + CTxDestination dest; if (type == OutputType::DILITHIUM_LEGACY) { dest = DilithiumPKHash(dilithium_pubkey); - LogPrintf("DEBUG: Created DilithiumPKHash destination\n"); - } else { // DILITHIUM_BECH32 + } else { dest = DilithiumWitnessV0KeyHash(dilithium_pubkey); - LogPrintf("DEBUG: Created DilithiumWitnessV0KeyHash destination\n"); } return dest; } @@ -1150,11 +1136,7 @@ void LegacyScriptPubKeyMan::DeriveNewDilithiumChildKey(WalletBatch &batch, CKeyM // Get the current child index uint32_t child_index = internal ? hd_chain.nInternalChainCounter : hd_chain.nExternalChainCounter; - LogPrintf("DEBUG: DeriveNewDilithiumChildKey - child_index: %d, internal: %s\n", - child_index, internal ? "true" : "false"); - // Generate a unique Dilithium key by ensuring each key has different entropy - // We'll use a combination of the HD chain seed and child index to create uniqueness std::vector unique_seed; // Include the HD chain seed ID for wallet-specific entropy @@ -1170,11 +1152,6 @@ void LegacyScriptPubKeyMan::DeriveNewDilithiumChildKey(WalletBatch &batch, CKeyM uint64_t timestamp = GetTime(); unique_seed.insert(unique_seed.end(), (unsigned char*)×tamp, (unsigned char*)×tamp + sizeof(timestamp)); - // Hash the unique seed to create deterministic but unique entropy - uint256 unique_hash = Hash(unique_seed); - - LogPrintf("DEBUG: Generated unique seed hash: %s\n", unique_hash.ToString()); - // Generate the Dilithium key secret.MakeNewKey(); @@ -1182,11 +1159,6 @@ void LegacyScriptPubKeyMan::DeriveNewDilithiumChildKey(WalletBatch &batch, CKeyM throw std::runtime_error(std::string(__func__) + ": Failed to generate valid Dilithium key"); } - // Get the key ID for debugging - CDilithiumPubKey pubkey = secret.GetPubKey(); - CKeyID keyid = CKeyID(Hash160(Span(pubkey.data(), pubkey.size()))); - LogPrintf("DEBUG: Generated Dilithium key with ID: %s\n", keyid.ToString()); - // Set metadata for the key metadata.nCreateTime = GetTime(); metadata.hdKeypath = strprintf("m/0'/%d'/%d'", internal ? 1 : 0, child_index); @@ -2362,126 +2334,66 @@ util::Result DescriptorScriptPubKeyMan::GetNewDestination(const // Handle Dilithium output types specially if (type == OutputType::DILITHIUM_LEGACY || type == OutputType::DILITHIUM_BECH32) { - LogPrintf("DEBUG: DescriptorScriptPubKeyMan generating Dilithium address of type %d\n", (int)type); - - // For Dilithium types, we need to check if the descriptor is compatible - // The descriptor should be pkh() for DILITHIUM_LEGACY or wpkh() for DILITHIUM_BECH32 bool is_compatible = false; if (type == OutputType::DILITHIUM_LEGACY && *desc_addr_type == OutputType::LEGACY) { is_compatible = true; } else if (type == OutputType::DILITHIUM_BECH32 && *desc_addr_type == OutputType::BECH32) { is_compatible = true; } - + if (!is_compatible) { return util::Error{_("Error: Descriptor type is not compatible with requested Dilithium address type")}; } - - // Generate a new Dilithium key using HD derivation + CDilithiumKey dilithium_key; - - // Use HD derivation to ensure unique keys - // Get the current index for this descriptor uint32_t child_index = m_wallet_descriptor.next_index; - - // Create unique seed for this specific key + + // Build unique seed from descriptor ID, child index, type, and timestamp std::vector unique_seed; - - // Include descriptor ID for wallet-specific entropy uint256 desc_id = GetID(); unique_seed.insert(unique_seed.end(), desc_id.begin(), desc_id.end()); - - // Include the child index to ensure each key is different unique_seed.insert(unique_seed.end(), (unsigned char*)&child_index, (unsigned char*)&child_index + sizeof(child_index)); - - // Include type-specific entropy unique_seed.push_back((int)type); - - // Include timestamp for additional uniqueness uint64_t timestamp = GetTime(); unique_seed.insert(unique_seed.end(), (unsigned char*)×tamp, (unsigned char*)×tamp + sizeof(timestamp)); - - // Hash the unique seed to create deterministic but unique entropy - uint256 unique_hash = Hash(unique_seed); - - LogPrintf("DEBUG: DescriptorScriptPubKeyMan HD derivation - child_index: %d, unique_hash: %s\n", - child_index, unique_hash.ToString()); - - // Generate the Dilithium key using our unique seed - // Since we can't easily seed the Dilithium key generation, we'll use MakeNewKey() - // but add some additional entropy to ensure uniqueness + dilithium_key.MakeNewKey(); - - // Add additional entropy based on our unique hash to ensure the key is different - // This is a workaround since we can't directly seed the Dilithium key generation - std::vector additional_entropy; - additional_entropy.insert(additional_entropy.end(), unique_hash.begin(), unique_hash.end()); - additional_entropy.insert(additional_entropy.end(), (unsigned char*)&child_index, (unsigned char*)&child_index + sizeof(child_index)); - - // Use the additional entropy to modify the key (this is a simplified approach) - // In a proper implementation, this would use the seed to derive the key deterministically - uint256 final_entropy = Hash(additional_entropy); - - LogPrintf("DEBUG: Using additional entropy: %s\n", final_entropy.ToString()); - - // Generate a second key to ensure we get a different one + + // Re-generate if child_index > 0 to ensure different key per index if (child_index > 0) { - LogPrintf("DEBUG: Generating additional key for uniqueness\n"); dilithium_key.MakeNewKey(); } - + if (!dilithium_key.IsValid()) { - LogPrintf("DEBUG: Failed to generate Dilithium key\n"); return util::Error{_("Error: Failed to generate Dilithium key")}; } - - LogPrintf("DEBUG: Generated Dilithium key successfully\n"); - - // Get the Dilithium public key + CDilithiumPubKey dilithium_pubkey = dilithium_key.GetPubKey(); - - // For Dilithium keys, we need to use a different approach since CPubKey is for ECDSA - // We'll use the hash of the Dilithium public key as the key ID CKeyID keyid = CKeyID(Hash160(Span(dilithium_pubkey.data(), dilithium_pubkey.size()))); - - // Store the Dilithium key using our new method + WalletBatch batch(m_storage.GetDatabase()); - LogPrintf("DEBUG: Attempting to store Dilithium key with keyid: %s\n", keyid.ToString()); - LogPrintf("DEBUG: Dilithium pubkey size: %zu\n", dilithium_pubkey.size()); - LogPrintf("DEBUG: Dilithium key size: %zu\n", dilithium_key.size()); - - // Check if key already exists and generate a new one if needed + int retry_count = 0; while (HaveDilithiumKey(keyid) && retry_count < 10) { - LogPrintf("DEBUG: Key already exists (retry %d), generating new key\n", retry_count); - // Generate a new key with different entropy dilithium_key.MakeNewKey(); dilithium_pubkey = dilithium_key.GetPubKey(); keyid = CKeyID(Hash160(Span(dilithium_pubkey.data(), dilithium_pubkey.size()))); - LogPrintf("DEBUG: New keyid: %s\n", keyid.ToString()); retry_count++; } - + if (retry_count >= 10) { - LogPrintf("DEBUG: Failed to generate unique key after 10 retries\n"); return util::Error{_("Error: Failed to generate unique Dilithium key")}; } - + if (!AddDilithiumKeyWithDB(batch, dilithium_key, keyid)) { - LogPrintf("DEBUG: AddDilithiumKeyWithDB returned false - key storage failed\n"); return util::Error{_("Error: Failed to store Dilithium key")}; } - - LogPrintf("DEBUG: Successfully stored Dilithium key in descriptor wallet database\n"); - - // Create destination based on type + CTxDestination dest; if (type == OutputType::DILITHIUM_LEGACY) { dest = DilithiumPKHash(dilithium_pubkey); - LogPrintf("DEBUG: Created DilithiumPKHash destination\n"); - } else { // DILITHIUM_BECH32 + } else { dest = DilithiumWitnessV0KeyHash(dilithium_pubkey); - LogPrintf("DEBUG: Created DilithiumWitnessV0KeyHash destination\n"); } return dest; } @@ -2517,45 +2429,30 @@ util::Result DescriptorScriptPubKeyMan::GetNewDestination(const isminetype DescriptorScriptPubKeyMan::IsMine(const CScript& script) const { LOCK(cs_desc_man); - - LogPrintf("DEBUG: DescriptorScriptPubKeyMan::IsMine called with script: %s\n", HexStr(script)); - - // First check if it's a known script in our map + if (m_map_script_pub_keys.count(script) > 0) { - LogPrintf("DEBUG: Script found in m_map_script_pub_keys\n"); return ISMINE_SPENDABLE; } - - // Check for Dilithium script types + std::vector vSolutions; TxoutType whichType = Solver(script, vSolutions); - - LogPrintf("DEBUG: Script type: %d\n", (int)whichType); - + CKeyID keyID; switch (whichType) { case TxoutType::DILITHIUM_PUBKEY: { keyID = CPubKey(vSolutions[0]).GetID(); - LogPrintf("DEBUG: DILITHIUM_PUBKEY, checking keyID: %s\n", keyID.ToString()); if (HaveDilithiumKey(keyID)) { - LogPrintf("DEBUG: Found Dilithium key for DILITHIUM_PUBKEY\n"); return ISMINE_SPENDABLE; } break; } case TxoutType::DILITHIUM_PUBKEYHASH: { - // vSolutions[0] contains the 20-byte hash from the script - // We need to construct a CKeyID that matches what was stored - // The stored keyID comes from Hash160(pubkey) which writes bytes to uint160.begin() - // So we need to copy the script bytes in the SAME way assert(vSolutions[0].size() == 20); keyID = CKeyID(); std::memcpy(keyID.begin(), vSolutions[0].data(), 20); - LogPrintf("DEBUG: DILITHIUM_PUBKEYHASH, checking keyID: %s (script hash: %s)\n", keyID.ToString(), HexStr(vSolutions[0])); if (HaveDilithiumKey(keyID)) { - LogPrintf("DEBUG: Found Dilithium key for DILITHIUM_PUBKEYHASH\n"); return ISMINE_SPENDABLE; } break; @@ -2565,9 +2462,7 @@ isminetype DescriptorScriptPubKeyMan::IsMine(const CScript& script) const assert(vSolutions[0].size() == 20); keyID = CKeyID(); std::memcpy(keyID.begin(), vSolutions[0].data(), 20); - LogPrintf("DEBUG: DILITHIUM_WITNESS_V0_KEYHASH, checking keyID: %s\n", keyID.ToString()); if (HaveDilithiumKey(keyID)) { - LogPrintf("DEBUG: Found Dilithium key for DILITHIUM_WITNESS_V0_KEYHASH\n"); return ISMINE_SPENDABLE; } break; @@ -2575,15 +2470,10 @@ isminetype DescriptorScriptPubKeyMan::IsMine(const CScript& script) const case TxoutType::WITNESS_V0_KEYHASH: { keyID = CKeyID(uint160(vSolutions[0])); - LogPrintf("DEBUG: WITNESS_V0_KEYHASH, checking keyID: %s\n", keyID.ToString()); - // Check if this is a Dilithium witness key if (HaveDilithiumKey(keyID)) { - LogPrintf("DEBUG: Found Dilithium key for WITNESS_V0_KEYHASH\n"); return ISMINE_SPENDABLE; } - // Also check for regular ECDSA keys if (m_map_keys.find(keyID) != m_map_keys.end() || m_map_crypted_keys.find(keyID) != m_map_crypted_keys.end()) { - LogPrintf("DEBUG: Found ECDSA key for WITNESS_V0_KEYHASH\n"); return ISMINE_SPENDABLE; } break; @@ -2591,16 +2481,11 @@ isminetype DescriptorScriptPubKeyMan::IsMine(const CScript& script) const case TxoutType::DILITHIUM_SCRIPTHASH: case TxoutType::DILITHIUM_MULTISIG: case TxoutType::DILITHIUM_WITNESS_V0_SCRIPTHASH: - // These Dilithium script types are not yet implemented - LogPrintf("DEBUG: Unimplemented Dilithium script type: %d\n", (int)whichType); break; default: - // For non-Dilithium scripts, use the default behavior - LogPrintf("DEBUG: Non-Dilithium script type: %d\n", (int)whichType); break; } - - LogPrintf("DEBUG: No matching key found, returning ISMINE_NO\n"); + return ISMINE_NO; } @@ -2698,21 +2583,13 @@ std::map DescriptorScriptPubKeyMan::GetKeys() const std::map DescriptorScriptPubKeyMan::GetDilithiumKeys() const { AssertLockHeld(cs_desc_man); - LogPrintf("DEBUG: GetDilithiumKeys called, m_map_dilithium_keys size: %zu\n", m_map_dilithium_keys.size()); - - // Return unencrypted Dilithium keys + std::map result; for (const auto& key_pair : m_map_dilithium_keys) { - // key_pair.first is CKeyID, key_pair.second is CDilithiumKey - // We need to convert CKeyID to DilithiumPKHash - // Since both wrap uint160, we can construct DilithiumPKHash from the CKeyID DilithiumPKHash dilithium_hash; std::memcpy(dilithium_hash.begin(), key_pair.first.begin(), 20); result[dilithium_hash] = key_pair.second; - LogPrintf("DEBUG: GetDilithiumKeys - Added key with CKeyID: %s -> DilithiumPKHash: %s\n", - key_pair.first.ToString(), HexStr(dilithium_hash)); } - LogPrintf("DEBUG: GetDilithiumKeys returning %zu keys\n", result.size()); return result; } @@ -2850,52 +2727,32 @@ bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const bool DescriptorScriptPubKeyMan::AddDilithiumKeyWithDB(WalletBatch& batch, const CDilithiumKey& key, const CKeyID &keyid) { AssertLockHeld(cs_desc_man); - - LogPrintf("DEBUG: AddDilithiumKeyWithDB called with keyid: %s\n", keyid.ToString()); - LogPrintf("DEBUG: Wallet has encryption keys: %s\n", m_storage.HasEncryptionKeys() ? "yes" : "no"); - LogPrintf("DEBUG: Wallet is locked: %s\n", m_storage.IsLocked() ? "yes" : "no"); - - // Check if key already exists + if (HaveDilithiumKey(keyid)) { - LogPrintf("DEBUG: Dilithium key already exists, skipping storage\n"); return true; } - + if (m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { - LogPrintf("DEBUG: Wallet has private keys disabled\n"); return false; } - + if (m_storage.HasEncryptionKeys()) { if (m_storage.IsLocked()) { - LogPrintf("DEBUG: Wallet is locked, cannot store encrypted key\n"); return false; } - - LogPrintf("DEBUG: Attempting to encrypt Dilithium key\n"); + std::vector crypted_secret; CKeyingMaterial secret(key.begin(), key.end()); if (!EncryptDilithiumSecret(m_storage.GetEncryptionKey(), secret, uint256(keyid), crypted_secret)) { - LogPrintf("DEBUG: Failed to encrypt Dilithium secret\n"); return false; } - - LogPrintf("DEBUG: Successfully encrypted Dilithium key, storing in database\n"); - // Store encrypted Dilithium key using proper database method with CKeyID - m_map_crypted_dilithium_keys[keyid] = make_pair(CPubKey(), crypted_secret); // Empty CPubKey since we don't need it - bool result = batch.WriteCryptedDilithiumKeyByID(keyid, crypted_secret, CKeyMetadata(GetTime())); - LogPrintf("DEBUG: WriteCryptedDilithiumKeyByID result: %s\n", result ? "success" : "failed"); - return result; + + m_map_crypted_dilithium_keys[keyid] = make_pair(CPubKey(), crypted_secret); + return batch.WriteCryptedDilithiumKeyByID(keyid, crypted_secret, CKeyMetadata(GetTime())); } else { - LogPrintf("DEBUG: Storing unencrypted Dilithium key\n"); m_map_dilithium_keys[keyid] = key; - // Store Dilithium key using the raw format (not CPrivKey) std::vector dilithium_privkey(key.begin(), key.end()); - LogPrintf("DEBUG: Dilithium private key size: %zu\n", dilithium_privkey.size()); - // Store Dilithium key using proper database method with CKeyID - bool result = batch.WriteDilithiumKeyByID(keyid, dilithium_privkey, CKeyMetadata(GetTime())); - LogPrintf("DEBUG: WriteDilithiumKeyByID result: %s\n", result ? "success" : "failed"); - return result; + return batch.WriteDilithiumKeyByID(keyid, dilithium_privkey, CKeyMetadata(GetTime())); } } @@ -2910,39 +2767,24 @@ bool DescriptorScriptPubKeyMan::AddCryptedDilithiumKeyWithDB(WalletBatch& batch, bool DescriptorScriptPubKeyMan::GetDilithiumKey(const CKeyID& keyid, CDilithiumKey& key) const { AssertLockHeld(cs_desc_man); - - LogPrintf("DEBUG: GetDilithiumKey - Looking for keyID: %s\n", keyid.ToString()); - LogPrintf("DEBUG: GetDilithiumKey - m_map_dilithium_keys size: %zu\n", m_map_dilithium_keys.size()); - LogPrintf("DEBUG: GetDilithiumKey - m_map_crypted_dilithium_keys size: %zu\n", m_map_crypted_dilithium_keys.size()); - - // Check unencrypted keys first + if (!m_storage.HasEncryptionKeys()) { - DilithiumKeyMap::const_iterator mi = m_map_dilithium_keys.find(keyid); + auto mi = m_map_dilithium_keys.find(keyid); if (mi != m_map_dilithium_keys.end()) { - LogPrintf("DEBUG: GetDilithiumKey - Found in m_map_dilithium_keys!\n"); key = mi->second; return true; - } else { - LogPrintf("DEBUG: GetDilithiumKey - NOT found in m_map_dilithium_keys\n"); - // List all keys in the map for debugging - for (const auto& pair : m_map_dilithium_keys) { - LogPrintf("DEBUG: GetDilithiumKey - Available key: %s\n", pair.first.ToString()); - } } } - - // Check encrypted keys (either if wallet is encrypted, or as fallback) - CryptedDilithiumKeyMap::const_iterator mi = m_map_crypted_dilithium_keys.find(keyid); + + auto mi = m_map_crypted_dilithium_keys.find(keyid); if (mi != m_map_crypted_dilithium_keys.end()) { if (m_storage.HasEncryptionKeys()) { - LogPrintf("DEBUG: GetDilithiumKey - Found in m_map_crypted_dilithium_keys!\n"); const CPubKey& pubkey = mi->second.first; const std::vector& crypted_secret = mi->second.second; return DecryptDilithiumKey(m_storage.GetEncryptionKey(), crypted_secret, pubkey, key); } } - - LogPrintf("DEBUG: GetDilithiumKey - Key not found anywhere\n"); + return false; } @@ -2959,21 +2801,13 @@ bool DescriptorScriptPubKeyMan::HaveDilithiumKey(const CKeyID& keyid) const bool DescriptorScriptPubKeyMan::AddDilithiumKeyPubKey(const CDilithiumKey& key, const CPubKey& pubkey) { - LogPrintf("DEBUG: AddDilithiumKeyPubKey - Starting\n"); LOCK(cs_desc_man); WalletBatch batch(m_storage.GetDatabase()); - // Use the Dilithium public key ID, not the dummy CPubKey ID - LogPrintf("DEBUG: AddDilithiumKeyPubKey - Getting Dilithium public key\n"); CDilithiumPubKey dilithium_pubkey = key.GetPubKey(); - LogPrintf("DEBUG: AddDilithiumKeyPubKey - Got Dilithium public key\n"); CKeyID dilithium_key_id = CKeyID(static_cast(dilithium_pubkey.GetID())); - LogPrintf("DEBUG: AddDilithiumKeyPubKey - Dilithium key ID: %s\n", dilithium_key_id.ToString().c_str()); - LogPrintf("DEBUG: AddDilithiumKeyPubKey - Calling AddDilithiumKeyWithDB\n"); if (!AddDilithiumKeyWithDB(batch, key, dilithium_key_id)) { - LogPrintf("DEBUG: AddDilithiumKeyPubKey - AddDilithiumKeyWithDB failed\n"); throw std::runtime_error(std::string(__func__) + ": writing Dilithium key failed"); } - LogPrintf("DEBUG: AddDilithiumKeyPubKey - Successfully completed\n"); return true; } @@ -3121,53 +2955,36 @@ std::unique_ptr DescriptorScriptPubKeyMan::GetSigningProvid { LOCK(cs_desc_man); - LogPrintf("DEBUG: GetSigningProvider(script) called, include_private=%d, script=%s\n", include_private, HexStr(script)); - LogPrintf("DEBUG: GetSigningProvider - m_map_script_pub_keys size: %zu\n", m_map_script_pub_keys.size()); - - // Find the index of the script auto it = m_map_script_pub_keys.find(script); if (it == m_map_script_pub_keys.end()) { - LogPrintf("DEBUG: GetSigningProvider - Script NOT found in m_map_script_pub_keys\n"); - // Script not found in descriptor-generated scripts - // Check if it's a Dilithium script we own std::vector vSolutions; TxoutType scriptType = Solver(script, vSolutions); - LogPrintf("DEBUG: GetSigningProvider - Script type: %d\n", (int)scriptType); - + if (scriptType == TxoutType::DILITHIUM_PUBKEYHASH && vSolutions.size() > 0) { - // Extract the keyID from the script CKeyID keyid; std::memcpy(keyid.begin(), vSolutions[0].data(), 20); - LogPrintf("DEBUG: GetSigningProvider - Dilithium script, keyID: %s\n", keyid.ToString()); - + if (HaveDilithiumKey(keyid)) { - LogPrintf("DEBUG: GetSigningProvider - Creating provider for Dilithium key\n"); - // We have this Dilithium key! Create a signing provider for it std::unique_ptr provider = std::make_unique(); - - // Add the Dilithium pubkey for fee estimation + CDilithiumKey dilithium_key; if (GetDilithiumKey(keyid, dilithium_key)) { DilithiumPKHash dilithium_hash; std::memcpy(dilithium_hash.begin(), keyid.begin(), 20); provider->dilithium_pubkeys[dilithium_hash] = dilithium_key.GetPubKey(); - LogPrintf("DEBUG: GetSigningProvider - Added Dilithium pubkey\n"); - - // Add private key if requested + if (include_private) { provider->dilithium_keys[dilithium_hash] = dilithium_key; - LogPrintf("DEBUG: GetSigningProvider - Added Dilithium private key\n"); } } - + return provider; } } - + return nullptr; } - - LogPrintf("DEBUG: GetSigningProvider - Script FOUND in m_map_script_pub_keys at index %d\n", it->second); + int32_t index = it->second; return GetSigningProvider(index, include_private); @@ -3218,16 +3035,11 @@ std::unique_ptr DescriptorScriptPubKeyMan::GetSigningProvid if (HavePrivateKeys() && include_private) { FlatSigningProvider master_provider; master_provider.keys = GetKeys(); - LogPrintf("DEBUG: GetSigningProvider - Added %zu ECDSA keys to master_provider\n", master_provider.keys.size()); - + m_wallet_descriptor.descriptor->ExpandPrivate(index, master_provider, *out_keys); - LogPrintf("DEBUG: GetSigningProvider - After ExpandPrivate, out_keys has %zu ECDSA keys\n", out_keys->keys.size()); - - // Manually add Dilithium private keys for signing + auto dilithium_keys = GetDilithiumKeys(); - LogPrintf("DEBUG: GetSigningProvider - Got %zu Dilithium keys from GetDilithiumKeys()\n", dilithium_keys.size()); out_keys->dilithium_keys.insert(dilithium_keys.begin(), dilithium_keys.end()); - LogPrintf("DEBUG: GetSigningProvider - After insert, out_keys has %zu Dilithium keys\n", out_keys->dilithium_keys.size()); } return out_keys; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 7935f89d9..c17014e39 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1580,10 +1580,19 @@ isminetype CWallet::IsMine(const CTxDestination& dest) const isminetype CWallet::IsMine(const CScript& script) const { AssertLockHeld(cs_wallet); + + auto cache_it = m_ismine_cache.find(script); + if (cache_it != m_ismine_cache.end()) { + return cache_it->second; + } + isminetype result = ISMINE_NO; for (const auto& spk_man_pair : m_spk_managers) { result = std::max(result, spk_man_pair.second->IsMine(script)); + if (result == ISMINE_SPENDABLE) break; } + + m_ismine_cache[script] = result; return result; } @@ -2448,6 +2457,7 @@ bool CWallet::TopUpKeyPool(unsigned int kpSize) for (auto spk_man : GetActiveScriptPubKeyMans()) { res &= spk_man->TopUp(kpSize); } + m_ismine_cache.clear(); return res; } @@ -3495,6 +3505,7 @@ LegacyScriptPubKeyMan* CWallet::GetOrCreateLegacyScriptPubKeyMan() void CWallet::AddScriptPubKeyMan(const uint256& id, std::unique_ptr spkm_man) { const auto& spkm = m_spk_managers[id] = std::move(spkm_man); + m_ismine_cache.clear(); // Update birth time if needed FirstKeyTimeChanged(spkm.get(), spkm->GetTimeFirstKey()); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 10c6520a1..2006c9f9a 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -415,6 +415,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati // ScriptPubKeyMan::GetID. In many cases it will be the hash of an internal structure std::map> m_spk_managers; + /** Cache IsMine results to avoid repeated lookups across all SPK managers during rescan */ + mutable std::map m_ismine_cache GUARDED_BY(cs_wallet); + // Appends spk managers into the main 'm_spk_managers'. // Must be the only method adding data to it. void AddScriptPubKeyMan(const uint256& id, std::unique_ptr spkm_man); diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 7ea246ade..4d7729c01 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -541,8 +541,6 @@ bool LoadDilithiumKey(CWallet* pwallet, DataStream& ssKey, DataStream& ssValue, std::vector vchDilithiumKey; ssValue >> vchDilithiumKey; - LogPrintf("DEBUG: LoadDilithiumKey called for keyID: %s, key size: %zu\n", keyID.ToString(), vchDilithiumKey.size()); - // Create CDilithiumKey from the stored data CDilithiumKey dilithiumKey; // The stored key includes both secret key and public key @@ -561,19 +559,14 @@ bool LoadDilithiumKey(CWallet* pwallet, DataStream& ssKey, DataStream& ssValue, // Try descriptor wallet first DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast(spk_man); if (desc_spk_man) { - LogPrintf("DEBUG: Loading Dilithium key into descriptor wallet\n"); if (desc_spk_man->LoadDilithiumKey(dilithiumKey, CPubKey())) { - LogPrintf("DEBUG: Successfully loaded Dilithium key into descriptor wallet\n"); loaded = true; break; } } - // Try legacy wallet LegacyScriptPubKeyMan* legacy_spk_man = dynamic_cast(spk_man); if (legacy_spk_man) { - LogPrintf("DEBUG: Loading Dilithium key into legacy wallet\n"); if (legacy_spk_man->LoadDilithiumKey(dilithiumKey, CPubKey())) { - LogPrintf("DEBUG: Successfully loaded Dilithium key into legacy wallet\n"); loaded = true; break; } @@ -1388,12 +1381,10 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet) result = std::max(LoadDecryptionKeys(pwallet, *m_batch), result); // Load Dilithium keys for descriptor wallets - LogPrintf("DEBUG: Starting Dilithium key loading for descriptor wallet\n"); LoadResult dilithium_key_res = LoadRecords(pwallet, *m_batch, DBKeys::DILITHIUM_KEY, [] (CWallet* pwallet, DataStream& key, CDataStream& value, std::string& err) { return LoadDilithiumKey(pwallet, key, value, err) ? DBErrors::LOAD_OK : DBErrors::CORRUPT; }); - LogPrintf("DEBUG: Dilithium key loading completed, loaded %d keys\n", dilithium_key_res.m_records); result = std::max(result, dilithium_key_res.m_result); } catch (...) { // Exceptions that can be ignored or treated as non-critical are handled by the individual loading functions.