From 4aa4b899cb135ff90f433998d3d42b0c1429600e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 2 Jul 2026 22:38:51 -0400 Subject: [PATCH 01/13] policy: count bare data envelopes (OP_2DROP/pushnum runs) as datacarrier --- src/script/script.cpp | 9 +++++---- src/test/script_tests.cpp | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/script/script.cpp b/src/script/script.cpp index fbb52fee7f9b..74b82305f98d 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -372,6 +372,7 @@ std::pair CScript::DatacarrierBytes(const size_t remaining_outpu opcodetype opcode, last_opcode{OP_INVALIDOPCODE}; std::vector push_data; unsigned int inside_noop{0}, inside_conditional{0}; + auto is_push = [](opcodetype op) { return op <= OP_16 && op != OP_RESERVED; }; // OP_0..OP_16 push data/numbers CScript::const_iterator opcode_it = begin(), data_began = begin(); for (CScript::const_iterator it = begin(); it < end(); last_opcode = opcode) { opcode_it = it; @@ -406,10 +407,10 @@ std::pair CScript::DatacarrierBytes(const size_t remaining_outpu } else if (opcode == OP_IF && last_opcode == OP_FALSE) { inside_noop = 1; data_began = opcode_it; - // Match OP_DROP - } else if (opcode <= OP_PUSHDATA4) { - data_began = opcode_it; - } else if (opcode == OP_DROP && last_opcode <= OP_PUSHDATA4) { + // Match OP_DROP, or a run of /pushnums ... OP_DROP/OP_2DROP (bare/BIP-110 envelope) + } else if (is_push(opcode)) { + if (!is_push(last_opcode)) data_began = opcode_it; // only reset at the start of a push run + } else if ((opcode == OP_DROP || opcode == OP_2DROP) && is_push(last_opcode)) { counted += it - data_began; } } diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp index 4b9ae35722a2..22b579a1e02e 100644 --- a/src/test/script_tests.cpp +++ b/src/test/script_tests.cpp @@ -1525,6 +1525,10 @@ BOOST_AUTO_TEST_CASE(script_DataCarrierBytes) BOOST_CHECK_EQUAL("0+6", DatacarrierBytesStr(CScript() << OP_FALSE << OP_IF << OP_TRUE << OP_IF << OP_ENDIF << OP_ENDIF)); // pushing then immediately dropping is data: length(1), zero(11), OP_DROP BOOST_CHECK_EQUAL("0+13", DatacarrierBytesStr(CScript() << zeros(11) << OP_DROP)); + // bare/BIP-110 envelope: a run of pushes balanced by OP_2DROP is all data + BOOST_CHECK_EQUAL("0+25", DatacarrierBytesStr(CScript() << zeros(11) << zeros(11) << OP_2DROP)); + // pushnums interleaved in the run don't strand earlier pushes: 12 + 1 (OP_7) + 12 + 1 (OP_2DROP) + BOOST_CHECK_EQUAL("0+26", DatacarrierBytesStr(CScript() << zeros(11) << OP_7 << zeros(11) << OP_2DROP)); // OLGA data obfuscated as p2wsh const auto olga_header = CScript() << OP_0 << "003e7374616d703a000000000000000000000000000000000000000000000000"_hex; BOOST_CHECK_EQUAL("0+82", DatacarrierBytesStr(olga_header, 2)); From e13330128567a1fb20efd2b74006b9256cd2b495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 2 Jul 2026 22:38:51 -0400 Subject: [PATCH 02/13] test: functional coverage for bare datacarrier envelopes --- test/functional/mempool_datacarrier.py | 56 ++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/functional/mempool_datacarrier.py b/test/functional/mempool_datacarrier.py index 540597199994..ffad09a90348 100755 --- a/test/functional/mempool_datacarrier.py +++ b/test/functional/mempool_datacarrier.py @@ -15,6 +15,7 @@ CScript, OP_1, OP_2DROP, + OP_7, OP_DROP, OP_RETURN, taproot_construct, @@ -92,6 +93,50 @@ def test_opnet_transaction(self, node: TestNode, success: bool) -> None: self.wallet.sendrawtransaction, from_node=node, tx_hex=tx_hex) + def test_bare_envelope(self, node: TestNode, data_len: int, success: bool, interleave_pushnum: bool = False) -> None: + # A bare inscription envelope: ... balanced by OP_2DROP, + # with no OP_IF wrapper (the shape ordinals/ord#4545 uses under BIP-110). + # A pushnum interleaved in the run (which ord's parser accepts as payload) + # must not strand the large push from datacarrier counting. + if interleave_pushnum: + envelope_script = CScript([randbytes(data_len), OP_7, randbytes(1), OP_2DROP, OP_DROP, OP_1]) + else: + envelope_script = CScript([b'ord', randbytes(data_len), OP_2DROP, OP_1]) + internal_key = b'\x01' * 32 + tap = taproot_construct(internal_key, [("leaf", envelope_script), ("dummy", CScript([OP_1]))]) + leaf = tap.leaves["leaf"] + control_block = bytes([leaf.version | tap.negflag]) + tap.internal_pubkey + leaf.merklebranch + assert len(control_block) == 65 + + utxo = self.wallet.get_utxo() + funding_tx = CTransaction() + funding_tx.vin = [CTxIn(COutPoint(int(utxo['txid'], 16), utxo['vout']))] + funding_value = int(utxo['value'] * 100_000_000) - 1000 + funding_tx.vout = [CTxOut(funding_value, tap.scriptPubKey)] + funding_tx.version = 2 + self.wallet.sign_tx(funding_tx) + funding_tx.rehash() + self.nodes[0].sendrawtransaction(funding_tx.serialize().hex()) + self.generate(self.nodes[0], 1, sync_fun=self.sync_blocks) + + spend_tx = CTransaction() + spend_tx.version = 2 + spend_tx.vin = [CTxIn(COutPoint(int(funding_tx.hash, 16), 0))] + spend_tx.vout = [CTxOut(funding_value - 1000, tap.scriptPubKey)] + spend_tx.wit.vtxinwit = [CTxInWitness()] + spend_tx.wit.vtxinwit[0].scriptWitness.stack = [ + bytes(envelope_script), # tapscript + control_block, # control block (65 bytes) + ] + tx_hex = spend_tx.serialize().hex() + + if success: + self.wallet.sendrawtransaction(from_node=node, tx_hex=tx_hex) + assert spend_tx.rehash() in node.getrawmempool(True) + else: + assert_raises_rpc_error(-26, "txn-datacarrier-exceeded", + self.wallet.sendrawtransaction, from_node=node, tx_hex=tx_hex) + def run_test(self): self.wallet = MiniWallet(self.nodes[0]) @@ -141,6 +186,17 @@ def run_test(self): self.log.info("Testing an OPNet transaction (just pushing 'op') with -datacarriersize=2.") self.test_opnet_transaction(node=self.nodes[3], success=False) + self.log.info("Testing a bare OP_2DROP envelope larger than the default -datacarriersize.") + self.test_bare_envelope(node=self.nodes[0], data_len=MAX_OP_RETURN_RELAY, success=False) + + self.log.info("Testing a bare OP_2DROP envelope smaller than the default -datacarriersize.") + # data_len >= 2 keeps the payload a minimal push; a 1-byte push of 1..16 or 0x81 would be + # rejected by MINIMALDATA in script verification rather than accepted by the datacarrier check. + self.test_bare_envelope(node=self.nodes[0], data_len=2, success=True) + + self.log.info("Testing a bare envelope with a pushnum interleaved in the push run.") + self.test_bare_envelope(node=self.nodes[0], data_len=MAX_OP_RETURN_RELAY, success=False, interleave_pushnum=True) + if __name__ == '__main__': DataCarrierTest(__file__).main() From 6507ed3e75a05ad1f54884f3d0a69c70bb4decca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Wed, 30 Sep 2026 16:41:35 -0400 Subject: [PATCH 03/13] test: the 30 kB no-op leaf is a data envelope under the new run rule --- test/functional/feature_taproot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index 7e00ff083525..60afe2c0b553 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -1037,8 +1037,10 @@ def big_spend_inputs(ctx): add_spender(spenders, "tapscript/emptysigs/checksig", leaf="t12", **common, inputs=[b'', getter("sign")], failure={"leaf": "t13"}, **ERR_UNKNOWN_PUBKEY) add_spender(spenders, "tapscript/emptysigs/nochecksigverify", leaf="t12", **common, inputs=[b'', getter("sign")], failure={"leaf": "t20"}, **ERR_UNKNOWN_PUBKEY) add_spender(spenders, "tapscript/emptysigs/checksigadd", leaf="t14", **common, inputs=[b'', getter("sign")], failure={"leaf": "t15"}, **ERR_UNKNOWN_PUBKEY) - # Test that scripts over 10000 bytes (and over 201 non-push ops) are acceptable. - add_spender(spenders, "tapscript/no10000limit", leaf="t19", **SINGLE_SIG, **common) + # Test that scripts over 10000 bytes (and over 201 non-push ops) are acceptable by consensus. + # The leaf opens with 10001 repeats of OP_0 OP_0 OP_2DROP, which datacarrier policy counts as + # a data envelope, so it is only spendable in a block rather than through the mempool. + add_spender(spenders, "tapscript/no10000limit", leaf="t19", standard=False, **SINGLE_SIG, **common) # Test that a stack size of 1000 elements is permitted, but 1001 isn't. add_spender(spenders, "tapscript/1000stack", leaf="t21", **SINGLE_SIG, **common, failure={"leaf": "t22"}, **ERR_STACK_SIZE) # Test that an input stack size of 1000 elements is permitted, but 1001 isn't. From 5e3153edef46f36565ee608a1826fe3bf0d5ecd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Sun, 4 Oct 2026 21:11:55 -0400 Subject: [PATCH 04/13] test: keep the sigops ratio script 2 push under the datacarrier limit --- test/functional/feature_taproot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index 60afe2c0b553..1651179452cc 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -1063,7 +1063,7 @@ def big_spend_inputs(ctx): # n OP_CHECKSIGVERIFYs and 1 OP_CHECKSIGADD, but also one unexecuted OP_CHECKSIGVERIFY. lambda n, pk: (CScript([OP_DROP, pk, OP_0, OP_IF, OP_2DUP, OP_CHECKSIGVERIFY, OP_ENDIF] + [OP_2DUP, OP_CHECKSIGVERIFY] * n + [OP_2, OP_SWAP, OP_CHECKSIGADD, OP_3, OP_EQUAL]), n + 1), # n OP_CHECKSIGVERIFYs and 1 OP_CHECKSIGADD, but also one unexecuted OP_CHECKSIG. - lambda n, pk: (CScript([random.randbytes(220), OP_2DROP, pk, OP_1, OP_NOTIF, OP_2DUP, OP_CHECKSIG, OP_VERIFY, OP_ENDIF] + [OP_2DUP, OP_CHECKSIGVERIFY] * n + [OP_4, OP_SWAP, OP_CHECKSIGADD, OP_5, OP_EQUAL]), n + 1), + lambda n, pk: (CScript([random.randbytes(75), OP_2DROP, pk, OP_1, OP_NOTIF, OP_2DUP, OP_CHECKSIG, OP_VERIFY, OP_ENDIF] + [OP_2DUP, OP_CHECKSIGVERIFY] * n + [OP_4, OP_SWAP, OP_CHECKSIGADD, OP_5, OP_EQUAL]), n + 1), # n OP_CHECKSIGVERIFYs and 1 OP_CHECKSIGADD, but also one unexecuted OP_CHECKSIGADD. lambda n, pk: (CScript([OP_DROP, pk, OP_1, OP_IF, OP_ELSE, OP_2DUP, OP_6, OP_SWAP, OP_CHECKSIGADD, OP_7, OP_EQUALVERIFY, OP_ENDIF] + [OP_2DUP, OP_CHECKSIGVERIFY] * n + [OP_8, OP_SWAP, OP_CHECKSIGADD, OP_9, OP_EQUAL]), n + 1), # n+1 OP_CHECKSIGs, but also one OP_CHECKSIG with an empty signature. From 55ca8414ae8a39051405f394912ff8ff8eabddd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Sun, 4 Oct 2026 19:04:52 -0400 Subject: [PATCH 05/13] policy: count pubkeys no signature can prove as datacarrier bytes --- src/script/script.cpp | 133 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/src/script/script.cpp b/src/script/script.cpp index 74b82305f98d..44da23c8ba16 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -8,10 +8,12 @@ #include #include #include +#include #include