Skip to content

Commit 7e49d15

Browse files
authored
Merge pull request #86 from instagibbs/2025-08-ikey-inq-29
Implement BIP 349 validation (OP_INTERNALKEY)
2 parents fe05f96 + 4287a3a commit 7e49d15

File tree

5 files changed

+86
-12
lines changed

5 files changed

+86
-12
lines changed

src/binana/internalkey.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"binana": [2024, 4, 0],
3+
"deployment": "INTERNALKEY",
4+
"scriptverify": true,
5+
"scriptverify_discourage": true,
6+
"opcodes": {
7+
"INTERNALKEY": "0xcb"
8+
}
9+
}

src/script/interpreter.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,19 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
13641364
break;
13651365
}
13661366

1367+
case OP_INTERNALKEY: {
1368+
// OP_INTERNALKEY is only available in Tapscript
1369+
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) {
1370+
return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
1371+
}
1372+
// Always present in Tapscript
1373+
assert(flags & SCRIPT_VERIFY_INTERNALKEY);
1374+
assert(sigversion == SigVersion::TAPSCRIPT);
1375+
assert(execdata.m_internal_key);
1376+
stack.emplace_back(execdata.m_internal_key->begin(), execdata.m_internal_key->end());
1377+
break;
1378+
}
1379+
13671380
default:
13681381
return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
13691382
}

src/test/data/tx_valid.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,14 @@
520520
[[["1111111111111111111111111111111111111111111111111111111111111111", 0, "0x00 0x14 0x751e76e8199196d454941c45d1b3a323f1433bd6", 5000000]],
521521
"0100000000010111111111111111111111111111111111111111111111111111111111111111110000000000ffffffff0130244c0000000000fd02014cdc1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111175210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac02483045022100c1a4a6581996a7fdfea77d58d537955a5655c1d619b6f3ab6874f28bb2e19708022056402db6fede03caae045a3be616a1a2d0919a475ed4be828dc9ff21f24063aa01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", "NONE"],
522522

523+
["Test OP_INTERNALKEY"],
524+
[[["e2f2baee9c59389b34e39742ce05debf64aaa7a00fbdab88614f4d3c133186d5",
525+
0,
526+
"1 0x20 0xa9e62de0f9782710f702214fc81c0f0f90fb3537987b3685caad6d52db305447",
527+
155000]],
528+
"02000000000101d58631133c4d4f6188abbd0fa0a7aa64bfde05ce4297e3349b38599ceebaf2e20000000000ffffffff01f0490200000000002251202ca3bc76489a54904ad2507005789afc1e6b362b451be89f69de39ddf9ba8abf0223cb2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac000000000",
529+
"DISCOURAGE_INTERNALKEY"],
530+
523531
["Test OP_CHECKSIGFROMSTACK"],
524532
[[["e2f2baee9c59389b34e39742ce05debf64aaa7a00fbdab88614f4d3c133186d5",
525533
0,

test/functional/feature_taproot.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
OP_EQUAL,
6868
OP_EQUALVERIFY,
6969
OP_IF,
70+
OP_INTERNALKEY,
7071
OP_NOP,
7172
OP_NOT,
7273
OP_NOTIF,
@@ -666,6 +667,22 @@ def byte_popper(expr):
666667

667668
# === Actual test cases ===
668669

670+
def spenders_internalkey_active():
671+
672+
secs = [generate_privkey() for _ in range(8)]
673+
pubs = [compute_xonly_pubkey(sec)[0] for sec in secs]
674+
675+
spenders = []
676+
677+
scripts = [
678+
("ik", CScript([OP_INTERNALKEY, OP_EQUAL])),
679+
]
680+
681+
tap = taproot_construct(pubs[0], scripts)
682+
683+
add_spender(spenders, "ik/success", tap=tap, leaf="ik", inputs=[pubs[0]], failure={"inputs": [pubs[1]]})
684+
685+
return spenders
669686

670687
def spenders_taproot_active():
671688
"""Return a list of Spenders for testing post-Taproot activation behavior."""
@@ -1255,7 +1272,7 @@ def predict_sigops_ratio(n, dummy_size):
12551272
# For the standard non-witness p2sh case, we need inputs to be minimal push opcodes (not witness stack elements)
12561273
# so we use arb non-0 byte push via valid pubkey
12571274
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)
1258-
1275+
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)
12591276
return spenders
12601277

12611278

@@ -1299,6 +1316,25 @@ def bip348_csfs_spenders_nonstandard():
12991316

13001317
return spenders
13011318

1319+
def bip349_ik_spenders_nonstandard():
1320+
"""Spenders for testing that pre-active INTERNALKEY usage is discouraged but valid"""
1321+
1322+
spenders = []
1323+
1324+
sec = generate_privkey()
1325+
pub, _ = compute_xonly_pubkey(sec)
1326+
scripts = [
1327+
("stilltrue", CScript([OP_INTERNALKEY])),
1328+
("still_opsuccess", CScript([OP_RETURN, OP_INTERNALKEY])),
1329+
]
1330+
tap = taproot_construct(pub, scripts)
1331+
1332+
# Valid prior to activation but nonstandard
1333+
add_spender(spenders, "discouraged_ik/stilltrue", tap=tap, leaf="stilltrue", standard=False)
1334+
add_spender(spenders, "discouraged_ik/still_opsuccess", tap=tap, leaf="still_opsuccess", standard=False)
1335+
1336+
return spenders
1337+
13021338
def bip348_csfs_spenders():
13031339
secs = [generate_privkey() for _ in range(2)]
13041340
pubs = [compute_xonly_pubkey(sec)[0] for sec in secs]
@@ -1417,7 +1453,8 @@ def skip_test_if_missing_module(self):
14171453

14181454
def set_test_params(self):
14191455
self.num_nodes = 1
1420-
self.extra_args = [["-vbparams=checksigfromstack:0:3999999999"]]
1456+
self.extra_args = [["-vbparams=checksigfromstack:0:3999999999",
1457+
"-vbparams=internalkey:0:3999999999"]]
14211458
self.setup_clean_chain = True
14221459

14231460
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):
18901927
def run_test(self):
18911928
self.gen_test_vectors()
18921929

1893-
self.log.info("CSFS Pre-activation tests...")
1930+
self.log.info("CSFS and IK Pre-activation tests...")
18941931
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["status"],"defined")
1932+
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["internalkey"]["heretical"]["status"],"defined")
18951933
self.generate(self.nodes[0], 144)
18961934
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["status"],"started")
1897-
signal_ver = int(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["signal_activate"], 16)
1935+
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["internalkey"]["heretical"]["status"],"started")
1936+
signal_ver_csfs = int(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["signal_activate"], 16)
1937+
signal_ver_ik = int(self.nodes[0].getdeploymentinfo()["deployments"]["internalkey"]["heretical"]["signal_activate"], 16)
18981938

1899-
self.test_spenders(self.nodes[0], bip348_csfs_spenders_nonstandard(), input_counts=[1, 2])
1939+
self.test_spenders(self.nodes[0], bip348_csfs_spenders_nonstandard() + bip349_ik_spenders_nonstandard(), input_counts=[1, 2])
19001940

1901-
self.log.info("Activating CSFS")
1941+
self.log.info("Activating CSFS and IK")
19021942
now = self.nodes[0].getblock(self.nodes[0].getbestblockhash())["time"]
1903-
coinbase_tx = create_coinbase(self.nodes[0].getblockcount() + 1)
1904-
block = create_block(hashprev=int(self.nodes[0].getbestblockhash(), 16), ntime=now, coinbase=coinbase_tx, version=signal_ver)
1905-
block.solve()
1906-
self.nodes[0].submitblock(block.serialize().hex())
1943+
for signal in [signal_ver_csfs, signal_ver_ik]:
1944+
coinbase_tx = create_coinbase(self.nodes[0].getblockcount() + 1)
1945+
block = create_block(hashprev=int(self.nodes[0].getbestblockhash(), 16), ntime=now, coinbase=coinbase_tx, version=signal)
1946+
block.solve()
1947+
self.nodes[0].submitblock(block.serialize().hex())
1948+
now += 1
19071949
self.generate(self.nodes[0], 288)
19081950
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["checksigfromstack"]["heretical"]["status"],"active")
1951+
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["internalkey"]["heretical"]["status"],"active")
19091952

19101953
self.log.info("Post-activation tests...")
1911-
consensus_spenders = spenders_taproot_active() + bip348_csfs_spenders()
1954+
consensus_spenders = spenders_taproot_active() + bip348_csfs_spenders() + spenders_internalkey_active()
19121955
self.test_spenders(self.nodes[0], consensus_spenders, input_counts=[1, 2, 2, 2, 2, 3])
19131956
# Run each test twice; once in isolation, and once combined with others. Testing in isolation
19141957
# means that the standardness is verified in every test (as combined transactions are only standard

test/functional/test_framework/script.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ def __new__(cls, n):
255255
OP_CHECKSIGADD = CScriptOp(0xba)
256256

257257
OP_CHECKSIGFROMSTACK = CScriptOp(0xcc)
258+
OP_INTERNALKEY = CScriptOp(0xcb)
258259

259260
OP_INVALIDOPCODE = CScriptOp(0xff)
260261

@@ -968,7 +969,7 @@ def taproot_construct(pubkey, scripts=None, *, keyver=None, treat_internal_as_in
968969
return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked, keyver)
969970

970971
def is_op_success(o):
971-
if o in [OP_CAT, OP_CHECKSIGFROMSTACK]:
972+
if o in [OP_CAT, OP_CHECKSIGFROMSTACK, OP_INTERNALKEY]:
972973
return False
973974

974975
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)

0 commit comments

Comments
 (0)