Repository navigation
policy: Count odd pushes in a multisig script as data - #5
Open
jasonsopko wants to merge 15 commits into
Open
jasonsopko wants to merge 15 commits into
jasonsopko wants to merge 15 commits into
Conversation
The run rule from #319 changes DatacarrierBytes for every node with no way to turn it off on its own. Give it its own option, default on, and let -corepolicy reset it like the other Knots spam filters. With the option off, counting is exactly what it was before #319: only the last push before OP_DROP counts, and OP_2DROP ends nothing. The test framework runs nodes with -corepolicy, so the two functional tests that rely on the rule now ask for it, and mempool_datacarrier.py checks that -rejectbareenvelopes=0 lets the large envelope through.
… bytes The rule only recognised a key in a 33 or 65 byte push, so the same payload written as tapscript went free: a 1-of-15 costs 396 datacarrier bytes under P2WSH and nothing at all under a taproot leaf, for 32 bytes of publishable space per key rather than 31. A 32 byte push is not a key on sight, since nothing separates an x-only key from a hash until something spends it. Count it where a checksig-family opcode claims it instead, which leaves the preimage of a hashlock alone. OP_CHECKSIGADD is how tapscript writes an m-of-n, so credit its canonical shape as OP_CHECKMULTISIG's is already credited: a run of <pubkey> OP_CHECKSIG (<pubkey> OP_CHECKSIGADD)* <k> OP_NUMEQUAL credits k, whatever size its keys are. Without it every honest tapscript multisig past three keys would be charged for the keys it does not spend, which is the opposite of the point: a 20-of-20 would cost 561 bytes. The OP_CHECKSIG that opens the run has credited one of the k already, so only the remaining k-1 are added there. Padding the run, reordering it or naming a k it cannot satisfy forfeits the credit rather than earning one, so working around the rule raises the charge instead of lowering it. An opcode that leaves the stack as it found it does not hide the key beneath it from the opcode that spends it, so it does not buy the count off either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MeQuiSdZEYJs491niM5zeo
Checks the taproot leaf against the P2WSH script that says the same thing, shape by shape, so the two are charged alike rather than only plausibly. Covers the hashlock shapes a 32 byte push must not be mistaken for a key in, the padded and inflated runs that forfeit the credit, the OP_CHECKSIG that opens a run crediting one of the k and not two, and the key stranded on the stack that neither rule reaches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MeQuiSdZEYJs491niM5zeo
Give #422's rule its own option, default on, reset by -corepolicy. #422 skips keys in a dropped push run because the caller counts the run whole; with -rejectbareenvelopes=0 the caller counts only the last push before an OP_DROP, so only that key is skipped. The allowance of 10 unproven keys per script is #422's own since its 2026-10-04 push; this commit no longer changes it. Over 961640 to 975264 (3,278,236 transactions) it refuses 90 of the 92 bpub reveals since the fork; the two it misses are single-input 1-of-8 and 1-of-9 reveals carrying 465 bytes. The one other refusal is a Liquid emergency spend through the 2-of-3 recovery branch, which leaves 16 keys unsigned. The widest real wallet since the fork leaves 10 keys unsigned, a 2-of-3 vault with timelocked 2-of-3 and 3-of-6 recovery branches spent with two signatures; the unit test keeps that spend.
In a 1-of-n OP_CHECKMULTISIG with the signing key last, consensus never examines the other key slots, so they take any push up to the element size limit. A 37 or 100 byte push there counted as neither a key nor data, which let 14 slots carry 1,400 bytes per input past -rejectfakemultisig (#4). In a script that runs OP_CHECKMULTISIG or OP_CHECKMULTISIGVERIFY, count every push of 6 or more bytes that is no key as data in full, outside the allowance of 10 keys. A push is left out when it is a digest a hash lock compares (the size the hash opcode before it outputs, followed by OP_EQUAL or OP_EQUALVERIFY), or when the drop and envelope rules count it already. In a witness script a 65 byte push counts too: segwit policy refuses an uncompressed key wherever it checks one, and ten such slots would otherwise carry 650 bytes inside the allowance. A push of 5 bytes or less counts too when another push follows it, with nothing between but OP_NOP, OP_CODESEPARATOR or an upgradable NOP. A script reads a number with the opcode after it, and a key count, the one number with keys pushed after it, is left alone. Without this a bare slot took a push of 5 bytes or less free. One with any other opcode after it, such as OP_CHECKSEQUENCEVERIFY, still does. Neither rule needs a literal <n>, so a computed key count and no-ops between the slots are covered too.
The case from #4: a 1-of-15 with the signing key last, whose 14 other slots hold 37 or 100 byte pushes. Also 5 and 1 byte slots, a computed key count, no-ops between the slots, 32 byte slots, OP_CHECKMULTISIGVERIFY, OP_EQUAL with no hash opcode before it, 65 byte slots in a witness script and short pushes in a dropped run, and what real multisig scripts carry besides keys: hash locks, an OP_FALSE OP_IF tag, dropped pushes, timelock numbers, a key count above 16 and uncompressed keys in a redeem script. The functional test spends the real case and a 1-of-20 with 5 byte slots on regtest, refused with the filter on, and the 100 byte case accepted with -rejectfakemultisig=0.
This was referenced Oct 19, 2026
jasonsopko
force-pushed
the
29.x-plumb
branch
2 times, most recently
from
October 29, 2026 17:24
2d196f5 to
66d4bfc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Counts the pushes in a multisig script that cannot be a key as data under
-rejectfakemultisig, and takes Kyle's later fix from knots#422 for a tapscript k from 128, with Chris's two one-line casts from privkeyio/bitcoin#2. For #4.Why
In a 1-of-n
OP_CHECKMULTISIGwith the signing key last, consensus never examines the other key slots, so they take any push (#4). A 37 or 100 byte push there counted as neither a key nor data: 14 slots carry 1,400 bytes per input past the filter.The rule, in a script that runs
OP_CHECKMULTISIGorOP_CHECKMULTISIGVERIFY:OP_EQUALorOP_EQUALVERIFY), or when the drop and envelope rules count it already.OP_NOP,OP_CODESEPARATORor an upgradable NOP. A script reads a number with the opcode after it, and a key count, the one number with keys pushed after it, is left alone. Without this, nine chained 1-of-10s with bare 5 byte slots carry 405 bytes in one input.Unlike the slot rule #4 suggests, none of this needs a literal key count, so a computed count and no-ops between the slots are covered too.
knots#422 has moved past the commit Plumb merges. Kyle's eea2467 is here, its code as it is there and its tests next to Plumb's older ones: a tapscript
multi_ak from 128 is written80 00, and Plumb read that as not minimal and counted the keys. Chris's 06a878f38a and d6ec42b98f from privkeyio/bitcoin#2 follow it under his name: a cast MSVC needs inMultiACount, and one in a test helper that the k test needs past 255 keys. knots#422 has both inside its own commits.How I tested
OP_NOPandOP_CODESEPARATORbetween slots, 32 byte slots,OP_CHECKMULTISIGVERIFY,OP_EQUALwith no hash opcode before it, 65 byte slots in a witness script and short pushes in a dropped run, and for what real multisig scripts carry besides keys: hash locks, anOP_FALSE OP_IFtag, dropped pushes, timelock numbers, a key count above 16, uncompressed keys in a redeem script. The functional test spends the -rejectfakemultisig ignores key slots whose push is not 33 or 65 bytes #4 case and a 1-of-20 with 5 byte slots on regtest: each is refused with the filter on, and the 100 byte case is accepted with-rejectfakemultisig=0.This tree refuses the first six at the data carrier check, before the fee check plumb7 refuses them at. The peer stayed connected throughout, and the median
getblockcounttook under 1 ms. Over four runs of this same bitcoind today the plain row read 4.35 to 4.85 ms, and plumb7's 4.19 to 4.36 ms.GitHub will show a conflict in
test/functional/mempool_datacarrier.pyagainst 29.x-plumb. The test merge kept both blocks. It also added/*dead_branches=*/trueto the fivescript_testscalls this PR adds that passbare_envelopes, since plumb7'sDatacarrierBytesStrtakesdead_branchesfirst; GitHub does not flag those.Risk and rollback
Policy only, on by default with the filter. A real multisig spend now has bytes counted toward
-datacarriersizeif it holds a push of 6 or more bytes that cannot be a key, no hash lock digest and not counted by the drop or envelope rules, or a short push that another push follows. None since the fork has either, and no multisig witness script since the fork has a 65 byte push. With-rejectdeadbranchestoo, a push in a dead branch of a multisig script counts under both rules: 37 bytes there count as 37 + 41. At the defaults that changes nothing, because the dead-branch bytes alone get the input refused (txn-datacarrier-nonstandard); it matters only with-acceptnonstddatacarrier=1.-rejectfakemultisig=0turns the whole filter off, and reverting the merge backs this out.Still unsure about
OP_DUP OP_SHA256 <32 bytes> OP_EQUALcarries 448 bytes, and a 1-of-20 and a 1-of-18 in one input carry 1,152, accepted on regtest. A cap on digests per script, as on keys, would bound it: no multisig script since the fork has more than one digest beforeOP_EQUALor 11 beforeOP_EQUALVERIFY.<37 bytes> <key> OP_NIP OP_CHECKSIGand<37 bytes> <key> OP_SWAP OP_DROP OP_CHECKSIGdo. Inside one, so does a push of 5 bytes or less with any opcode but a push or a no-op after it: a 1-of-15 whose 14 slots are<5 bytes> OP_CHECKSEQUENCEVERIFYor<5 bytes> OP_SIZE OP_DROPis accepted on regtest. That costs 7 or 8 bytes of script for 5 of data, where a bare slot costs 6.OP_ELSEhalf ofOP_FALSE OP_IF ... OP_ELSE ... OP_ENDIFis skipped as part of the envelope: 15 slots of 100 bytes there count 2 bytes. At the defaults those 2 bytes get the input refused (txn-datacarrier-nonstandard). With-acceptnonstddatacarrier=1they are held to-datacarriersizeinstead, so it passes.