From c7a59b414b85eb542633af5f69971da467adadfe Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Mon, 18 Aug 2025 15:47:45 -0400 Subject: [PATCH 1/3] consensus: implement OP_INTERNALKEY BIP349 --- src/binana/internalkey.json | 9 +++++++++ src/script/interpreter.cpp | 13 +++++++++++++ test/functional/test_framework/script.py | 3 ++- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/binana/internalkey.json diff --git a/src/binana/internalkey.json b/src/binana/internalkey.json new file mode 100644 index 0000000000000..bd96f648d8201 --- /dev/null +++ b/src/binana/internalkey.json @@ -0,0 +1,9 @@ +{ + "binana": [2024, 4, 0], + "deployment": "INTERNALKEY", + "scriptverify": true, + "scriptverify_discourage": true, + "opcodes": { + "INTERNALKEY": "0xcb" + } +} diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 67a77d6f34fa3..0e67b9940c995 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -1364,6 +1364,19 @@ bool EvalScript(std::vector >& stack, const CScript& break; } + case OP_INTERNALKEY: { + // OP_INTERNALKEY is only available in Tapscript + if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) { + return set_error(serror, SCRIPT_ERR_BAD_OPCODE); + } + // Always present in Tapscript + assert(flags & SCRIPT_VERIFY_INTERNALKEY); + assert(sigversion == SigVersion::TAPSCRIPT); + assert(execdata.m_internal_key); + stack.emplace_back(execdata.m_internal_key->begin(), execdata.m_internal_key->end()); + break; + } + default: return set_error(serror, SCRIPT_ERR_BAD_OPCODE); } diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index ac80f30a71187..0a33838b12761 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -255,6 +255,7 @@ def __new__(cls, n): OP_CHECKSIGADD = CScriptOp(0xba) OP_CHECKSIGFROMSTACK = CScriptOp(0xcc) +OP_INTERNALKEY = CScriptOp(0xcb) OP_INVALIDOPCODE = CScriptOp(0xff) @@ -968,7 +969,7 @@ def taproot_construct(pubkey, scripts=None, *, keyver=None, treat_internal_as_in return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked, keyver) def is_op_success(o): - if o in [OP_CAT, OP_CHECKSIGFROMSTACK]: + if o in [OP_CAT, OP_CHECKSIGFROMSTACK, OP_INTERNALKEY]: return False 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) From 6c3fa1eec055f9182c6d0736d07faf12e199d99e Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Thu, 13 Mar 2025 15:41:46 -0400 Subject: [PATCH 2/3] test: basic BIP349 unit test --- src/test/data/tx_valid.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/data/tx_valid.json b/src/test/data/tx_valid.json index 5ad400255bbfe..aeb18647b182a 100644 --- a/src/test/data/tx_valid.json +++ b/src/test/data/tx_valid.json @@ -520,6 +520,14 @@ [[["1111111111111111111111111111111111111111111111111111111111111111", 0, "0x00 0x14 0x751e76e8199196d454941c45d1b3a323f1433bd6", 5000000]], "0100000000010111111111111111111111111111111111111111111111111111111111111111110000000000ffffffff0130244c0000000000fd02014cdc1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111175210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac02483045022100c1a4a6581996a7fdfea77d58d537955a5655c1d619b6f3ab6874f28bb2e19708022056402db6fede03caae045a3be616a1a2d0919a475ed4be828dc9ff21f24063aa01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", "NONE"], + ["Test OP_INTERNALKEY"], + [[["e2f2baee9c59389b34e39742ce05debf64aaa7a00fbdab88614f4d3c133186d5", + 0, + "1 0x20 0xa9e62de0f9782710f702214fc81c0f0f90fb3537987b3685caad6d52db305447", + 155000]], +"02000000000101d58631133c4d4f6188abbd0fa0a7aa64bfde05ce4297e3349b38599ceebaf2e20000000000ffffffff01f0490200000000002251202ca3bc76489a54904ad2507005789afc1e6b362b451be89f69de39ddf9ba8abf0223cb2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac000000000", + "DISCOURAGE_INTERNALKEY"], + ["Test OP_CHECKSIGFROMSTACK"], [[["e2f2baee9c59389b34e39742ce05debf64aaa7a00fbdab88614f4d3c133186d5", 0, From 4287a3a03efdc786202c892b9ca8ed2a4e289e1f Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Mon, 23 Jun 2025 09:34:11 -0400 Subject: [PATCH 3/3] test: functional test for bip349 --- test/functional/feature_taproot.py | 65 +++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index c2d7c98843cb0..693cbd69d81f7 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -67,6 +67,7 @@ OP_EQUAL, OP_EQUALVERIFY, OP_IF, + OP_INTERNALKEY, OP_NOP, OP_NOT, OP_NOTIF, @@ -666,6 +667,22 @@ def byte_popper(expr): # === Actual test cases === +def spenders_internalkey_active(): + + secs = [generate_privkey() for _ in range(8)] + pubs = [compute_xonly_pubkey(sec)[0] for sec in secs] + + spenders = [] + + scripts = [ + ("ik", CScript([OP_INTERNALKEY, OP_EQUAL])), + ] + + tap = taproot_construct(pubs[0], scripts) + + add_spender(spenders, "ik/success", tap=tap, leaf="ik", inputs=[pubs[0]], failure={"inputs": [pubs[1]]}) + + return spenders def spenders_taproot_active(): """Return a list of Spenders for testing post-Taproot activation behavior.""" @@ -1255,7 +1272,7 @@ def predict_sigops_ratio(n, dummy_size): # For the standard non-witness p2sh case, we need inputs to be minimal push opcodes (not witness stack elements) # so we use arb non-0 byte push via valid pubkey add_spender(spenders, "compat/nocsfs", p2sh=p2sh, witv0=witv0, standard=p2sh or witv0, script=CScript([OP_IF, b'', b'', pubs[0], OP_CHECKSIGFROMSTACK, OP_DROP, OP_ENDIF]), inputs=[pubs[0], b''], failure={"inputs": [pubs[0], pubs[0]]}, **ERR_UNDECODABLE) - + add_spender(spenders, "compat/noik", p2sh=p2sh, witv0=witv0, standard=p2sh or witv0, script=CScript([OP_IF, OP_INTERNALKEY, OP_RETURN, OP_ENDIF]), inputs=[pubs[0], b''], failure={"inputs": [pubs[0], pubs[0]]}, **ERR_UNDECODABLE) return spenders @@ -1299,6 +1316,25 @@ def bip348_csfs_spenders_nonstandard(): return spenders +def bip349_ik_spenders_nonstandard(): + """Spenders for testing that pre-active INTERNALKEY usage is discouraged but valid""" + + spenders = [] + + sec = generate_privkey() + pub, _ = compute_xonly_pubkey(sec) + scripts = [ + ("stilltrue", CScript([OP_INTERNALKEY])), + ("still_opsuccess", CScript([OP_RETURN, OP_INTERNALKEY])), + ] + tap = taproot_construct(pub, scripts) + + # Valid prior to activation but nonstandard + add_spender(spenders, "discouraged_ik/stilltrue", tap=tap, leaf="stilltrue", standard=False) + add_spender(spenders, "discouraged_ik/still_opsuccess", tap=tap, leaf="still_opsuccess", standard=False) + + return spenders + def bip348_csfs_spenders(): secs = [generate_privkey() for _ in range(2)] pubs = [compute_xonly_pubkey(sec)[0] for sec in secs] @@ -1417,7 +1453,8 @@ def skip_test_if_missing_module(self): def set_test_params(self): self.num_nodes = 1 - self.extra_args = [["-vbparams=checksigfromstack:0:3999999999"]] + self.extra_args = [["-vbparams=checksigfromstack:0:3999999999", + "-vbparams=internalkey:0:3999999999"]] self.setup_clean_chain = True def block_submit(self, node, txs, msg, err_msg, cb_pubkey=None, fees=0, sigops_weight=0, witness=False, accept=False): @@ -1890,25 +1927,31 @@ def pr(node): def run_test(self): self.gen_test_vectors() - self.log.info("CSFS Pre-activation tests...") + self.log.info("CSFS and IK Pre-activation tests...") assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["status"],"defined") + assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["internalkey"]["heretical"]["status"],"defined") self.generate(self.nodes[0], 144) assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["status"],"started") - signal_ver = int(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["signal_activate"], 16) + assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["internalkey"]["heretical"]["status"],"started") + signal_ver_csfs = int(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["signal_activate"], 16) + signal_ver_ik = int(self.nodes[0].getdeploymentinfo()["deployments"]["internalkey"]["heretical"]["signal_activate"], 16) - self.test_spenders(self.nodes[0], bip348_csfs_spenders_nonstandard(), input_counts=[1, 2]) + self.test_spenders(self.nodes[0], bip348_csfs_spenders_nonstandard() + bip349_ik_spenders_nonstandard(), input_counts=[1, 2]) - self.log.info("Activating CSFS") + self.log.info("Activating CSFS and IK") now = self.nodes[0].getblock(self.nodes[0].getbestblockhash())["time"] - coinbase_tx = create_coinbase(self.nodes[0].getblockcount() + 1) - block = create_block(hashprev=int(self.nodes[0].getbestblockhash(), 16), ntime=now, coinbase=coinbase_tx, version=signal_ver) - block.solve() - self.nodes[0].submitblock(block.serialize().hex()) + for signal in [signal_ver_csfs, signal_ver_ik]: + coinbase_tx = create_coinbase(self.nodes[0].getblockcount() + 1) + block = create_block(hashprev=int(self.nodes[0].getbestblockhash(), 16), ntime=now, coinbase=coinbase_tx, version=signal) + block.solve() + self.nodes[0].submitblock(block.serialize().hex()) + now += 1 self.generate(self.nodes[0], 288) assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["status"],"active") + assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["internalkey"]["heretical"]["status"],"active") self.log.info("Post-activation tests...") - consensus_spenders = spenders_taproot_active() + bip348_csfs_spenders() + consensus_spenders = spenders_taproot_active() + bip348_csfs_spenders() + spenders_internalkey_active() self.test_spenders(self.nodes[0], consensus_spenders, input_counts=[1, 2, 2, 2, 2, 3]) # Run each test twice; once in isolation, and once combined with others. Testing in isolation # means that the standardness is verified in every test (as combined transactions are only standard