Skip to content

policy: add a fee floor for unproven multisig key operands - #456

Draft
Volkrad21 wants to merge 41 commits into
bitcoinknots:29.x-knotsfrom
Volkrad21:policy/multisig-sigop-fee
Draft

Volkrad21 wants to merge 41 commits into
bitcoinknots:29.x-knotsfrom
Volkrad21:policy/multisig-sigop-fee

Conversation

@Volkrad21

@Volkrad21 Volkrad21 commented Oct 11, 2026 •

Copy link
Copy Markdown

Behavior

This draft implements an experimental relay-policy fee floor for unproven key operands in executed multisig input scripts, not a consensus rule or a general data filter. The default is 1,000 sats per charged operand, with one unproven operand free per executed script. For a successful canonical m-of-n spend, the charged count is max(0, n - m - 1): 2-of-2 and 2-of-3 have no additional floor; 1-of-15 has a 13,000-sat floor. Counts accumulate across inputs. The floor and ordinary fee requirements both apply; their amounts are not added.

This PR implements an idea proposed by Pepe Lapiu. It does not necessarily represent Volkrad's personal view.

Executed-operand accounting

The normal policy script-validation pass now collects optional observations rather than using the old static push-size estimate for admission:

  • Legacy/v0 CHECKMULTISIG and CHECKMULTISIGVERIFY contribute actual key operands minus successful signature matches. Dynamic counts and stack transformations are handled by the existing interpreter. Empty signatures in a legal false-result/NOT construction do not earn structural threshold credit.
  • A tapscript is in scope only when CHECKSIGADD actually executes. Its executed CHECKSIG, CHECKSIGVERIFY and CHECKSIGADD operands without a valid Schnorr signature are counted, including multi_a's initial CHECKSIG. This is a script-level scope rule, not tracing of a shared accumulator. Upgradable key types do not earn credit merely for succeeding without verification under relaxed flags.
  • Multiple operations in one script share its free operand. Counts refer to operand occurrences, not distinct key byte strings. The subtraction is guarded, so zero cannot underflow.
  • Unrelated hashes/data, witness padding and unexecuted branches do not become key operands because of their size. Standalone CHECKSIG, pure data scripts and Taproot key-path spends have no key-fee floor.
  • Only successful input validation supplies authoritative observations. There is no second speculative EvalScript pass. A bounded count cache uses wtxid and flags alongside full-script validity; missing count metadata cannot be mistaken for zero. Consensus validation does not enforce this floor.

-minrelaymultisigexcesskeyfee=<n> sets integer satoshis per charged operand; zero disables it. -corepolicy disables it unless explicitly overridden.

CPFP and Lightning

A floor failure is reconsiderable. In the existing child-with-unconfirmed-parents admission path, newly evaluated package members must pay the sum of their floors in actual fees, including any charged child. Already-admitted/mempool transactions and prioritisation deltas do not provide reusable fee credit. Ordinary minimum relay fee, topology, replacement, signature and mempool-limit rules remain in force. testmempoolaccept retains individual-fee semantics; eligible submitpackage submissions use package accounting.

This is not arbitrary package relay or a permanent parent/child binding, and does not guarantee joint mining or survival after eviction/replacement. P2P interoperability remains untested.

The authoritative counter no longer uses a Lightning template exemption: ordinary funding 2-of-2 and HTLC 2-of-2 paths prove both operands; to_local and standard HTLC revocation/preimage paths do not execute multisig. These paths have no additional floor. This does not establish compatibility with every future Lightning script or presigned protocol.

Wallet funding

Signed creation obtains counts from its existing signing verification, adjusts change or retries coin selection with bounded retries/max-fee checks, then checks the final signed path against the actual fee. A more expensive final path fails creation rather than silently producing an underfunded transaction.

Unsigned funding still uses a static estimate, including the earlier stack-identity normalization and exact BOLT #3 template allowance; neither controls admission. Complex scripts can be over- or underestimated. Unsigned Taproot funding reserves the largest estimate among known leaves unless options.multisig_fee supplies a nonnegative integer total floor in satoshis. Zero can express a planned key-path spend. This is not a relay override; a different eventual path may need re-funding or an eligible package.

Relationship to #422 and limitations

This takes inspiration from #422's unproven-key concept, but chooses a fee floor instead of datacarrier classification. Executed-operand accounting is a different, narrower scope than static script-data accounting, not proof that this PR is universally better.

The free operand is intentional. Payload in dead branches, non-key data and other encodings remains outside this rule; independent datacarrier policies still apply. This is not an unbypassable spam classifier. The floor is evaluated after script verification, so no early relay-CPU protection is claimed. The NACK's historical sample has not been replayed under this revision, and no new impact percentages or speedup claims are made. Presigned charged transactions and unsupported packages remain compatibility concerns.

Validation status — still draft

Python syntax checks for the mempool and wallet functional-test files and git diff --check passed. This revision has not been compiled; C++ tests and node-based functional tests have not run. Local Windows configuration was attempted but blocked before C++ compilation by dependency-tool process/handle and SDK registry access errors.

Added/updated cases cover real ECDSA/Schnorr spends, dynamic key counts, mixed-script non-key data, witness-supplied operands, false multisig results, cache misses/eviction and witness/flag isolation, Lightning paths, wallet funding, and package fee boundaries/deduplication. Test existence is not evidence they pass. Interpreter instrumentation, wallet integration and package handling need runtime validation and independent review before merge readiness can be assessed.

Avoid executing scriptSig when only the final pushed redeem script is needed.
@jasonsopko

Copy link
Copy Markdown

NACK at this default. Tested 6836789 on a Release build with -corepolicy=0 (no wallet, so spend.cpp is read only).

The floor refuses most real multisig spends. Over blocks 976024 to 976740, 5,980 of the 6,568 transactions it applies to (a CHECKMULTISIG input, not a bpub reveal, nested P2SH-P2WSH included) paid less than 500 sat per key: 4,016 of the 4,390 plain 2-of-3 spends and 1,534 of the 1,714 plain 2-of-2 spends. They pay a median 132 sat per key today. A pre-signed 2-of-2 at 300 sat (2 sat/vB) relays on 29.4.2 (the plumb6 build) alone or with a CPFP child; here nothing under 1,000 sat passes, child or not: min multisig sigop fee not met, 300 < 1000 (2 multisig sigops at 500 sat each).

mempool_multisig_sigop_fee.py fails on this branch under the -corepolicy=0 it sets: the spend pays everything to an OP_RETURN output and is refused as bare-datacarrier before the floor runs.

Against the encoder it targets: the 725 bpub reveals in the same window paid 0.405 BTC and would have needed 14.45 BTC here, 35.7x. knots#422 already counts data in all 725 at its allowance of 10 and in none of the 6,611 others with a multisig input, because it counts only the keys no signature proves. A floor on those keys would land on the 14 unsigned keys of a 1-of-15 and on one key of a 2-of-3.

OP_CHECKSIGADD is not counted, so a tapscript multi_a leaf pays nothing here. Relay cost is unchanged: 400 refused transactions per script form from one inbound peer cost 0.13 to 0.27 ms each, against 0.14 to 0.29 ms on the plumb6 build, RPC median under 1 ms, peer still connected.

Scripts, logs and the per-transaction data: https://gist.github.com/jasonsopko/d07b6b8044f2b02a3ebea87711dab9e7

@mynpub

mynpub commented Oct 13, 2026

Copy link
Copy Markdown

Can multisig just be disabled until expiration of RDTS?

@Volkrad21 Volkrad21 changed the title policy: require fee per multisig input sigop policy: charge a fee for unproven multisig keys Oct 13, 2026
Restore consistent excess-key integration, retain dropped key pushes, and share input counting with the wallet. Fund the floor from change or coin reselection and inspect the selected Taproot satisfaction. Require a witness for ambiguous unsigned Taproot funding.

Add regression cases. Validation: static review and git diff --check only; not compiled and tests not run.
@Volkrad21 Volkrad21 changed the title policy: charge a fee for unproven multisig keys policy: add a fee floor for estimated unproven input keys Oct 13, 2026
@pepehodler-byte

pepehodler-byte commented Oct 13, 2026 •

Copy link
Copy Markdown

Can multisig just be disabled until expiration of RDTS?

Hi friend. Removing important and useful features every time we get a new spam attack is not really wise. And it could also lock many people out of their cold stored coin.

This filter will likely not affect 99.8% of the transactions. 0.18% of the transactions will have to pay ~0.6¢ in miner fees to stay in line with policy. And ~0.02% of transactions will have to pay ~1¢ to 50¢ in miner fees.

But the spammers will feel the brunt of this filter, having to pay $10 to $60 in miner fees.
These figures are based on a 1 week / 1008 blocks sample scan on the chain ending earlier this week.

I should also say that a weakness of this proposal is that violent price swings could make the filter too aggressive, or too weak. So we have to rely on individual nodes to adjust the setting appropriately, or for the devs to adjust defaults with frequent releases.

With suggested settings and current price, the spammer would have to pay over $1500 of miner fees per day to keep up his current volume, instead of the less than $60/day he is currently spending.

… [skip ci]

Collect optional observations from normal script validation instead of using
key-sized pushes as the admission counter. Preserve one free unproven operand
per executed script. Carry observations through the validity cache and wallet
signing, and aggregate actual fees/floors for supported package admission.

Add regression cases and document the narrower executed-path scope.
Validation: Python syntax and diff checks only. Native build configuration
blocked before C++ compilation; unit and functional runtime tests not run.
@Volkrad21 Volkrad21 changed the title policy: add a fee floor for estimated unproven input keys policy: add a fee floor for unproven multisig key operands Oct 13, 2026
@Volkrad21

Copy link
Copy Markdown
Author

@jasonsopko Thank you for the concrete reproducer, measurements and historical sample. They exposed real problems with the earlier all-keys floor, including its impact on ordinary and presigned multisig transactions, the missing CPFP path, the OP_RETURN test failure and the missing CHECKSIGADD coverage.

The draft has been reworked substantially in response:

  • It now targets unproven multisig key operands, with one free per executed script, at a configurable default of 1,000 sats per charged operand. Successful canonical 2-of-2 and 2-of-3 spends have no additional floor; 1-of-15 has thirteen charged operands (13,000 sats), not fifteen. The ordinary fee requirements still apply.
  • Admission counts operands and successful signature checks from the normal script-validation pass, including tapscript CHECKSIGADD. It no longer infers admission charges from generic key-sized pushes or signature-shaped padding. Dynamic counts and stack transformations use the actual interpreter results. Unrelated data and unexecuted branches are outside this rule.
  • A floor failure is reconsiderable. The existing child-with-unconfirmed-parents admission path now compares the actual fees of newly evaluated members with the sum of their floors. A charged child is included; already-admitted fees and local prioritisation deltas cannot be reused. This is not support for every package topology or a guarantee that parent and child stay together after admission.
  • The fee tests use spendable outputs rather than the OP_RETURN output that masked the intended check. Additional cases cover mixed scripts, dynamic counts, cache isolation, Lightning paths and package boundaries. Signed wallet creation uses execution observations; unsigned funding remains an estimate and can require re-funding for the final path.

The design follows #422's unproven-key idea. The preference here is to explore a graduated fee floor rather than classifying those operands as datacarrier usage. That is a design preference, not evidence that this proposal is universally better than #422. The executed-path scope is also deliberately narrower: the free operand, dead-branch payload and other data encodings remain limitations.

Your relay-CPU measurements are not refuted by these changes. This floor is checked after script verification; no early CPU protection is claimed. The historical sample has not been replayed under this revision, so I am not claiming new rejection percentages or that all presigned-transaction compatibility concerns are resolved.

Validation status: Python syntax checks and git diff --check passed, but this revision has not been compiled, and its C++/node-based tests have not run. Local Windows build configuration was blocked before compilation. In particular, the interpreter instrumentation, wallet integration and package changes still require runtime validation and independent review. The PR remains a draft; the changes above are implementation details, not a claim that your NACK has been fully resolved.

For attribution, this proposal implements an idea from Pepe Lapiu and does not necessarily represent Volkrad's personal view.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants