policy: allow more than one OP_RETURN outputs per tx #32381

pull darosior wants to merge 1 commits into bitcoin:master from darosior:2504_fixthefiltersyo changing 3 files +7 −14
  1. darosior commented at 8:21 pm on April 29, 2025: member

    Please keep conceptual discussions on the corresponding mailing list thread.

    Because of the restriction on the number of OP_RETURN outputs per transaction, people are designing protocols that store data in non-pruneable and unspendable outputs. Lift this restriction to stop preventing them from using a less harmful way of achieving the same.

    This is an alternative to #32359 with the smallest change that addresses the concern i raised on the mailing list thread above. Notably this does not remove any command line option and keeps the limit in place on the size of each OP_RETURN output. I still believe we should do #32359, as i signaled there. Hopefully this can be seen as a first step toward this goal, which addresses the short term harm without wasting everyone’s time.

  2. DrahtBot commented at 8:22 pm on April 29, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32381.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept NACK nsvrn, luke-jr, chrisguida, moth-oss, jesterhodl
    Concept ACK polespinasa, stevenroose
    Approach NACK ajtowns

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #32359 (Remove arbitrary limits on OP_Return (datacarrier) outputs by petertodd)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. darosior marked this as ready for review on Apr 29, 2025
  4. DrahtBot added the label TX fees and policy on Apr 29, 2025
  5. policy: drop restrictions on the number of OP_RETURN outputs per tx
    This restriction is leading people to store data in more harmful ways. See the mailing list thread
    for details and conceptual discussion:
        https://gnusha.org/pi/bitcoindev/rhfyCHr4RfaEalbfGejVdolYCVWIyf84PT2062DQbs5-eU8BPYty5sGyvI3hKeRZQtVC7rn_ugjUWFnWCymz9e9Chbn7FjWJePllFhZRKYk=@protonmail.com
    5fd80bd662
  6. darosior force-pushed on Apr 29, 2025
  7. l0rinc commented at 8:28 pm on April 29, 2025: contributor
    Would it be possible to summarize the pros and cons raised across the internets in the PR description - if for no other reason, to avoid repeated comments on the same topic? Maybe it would help bring the two sides together on this admittedly contentious issue, where both have valid concerns and good arguments.
  8. nsvrn commented at 8:33 pm on April 29, 2025: contributor
    “allow” when no one is asking for this and has no positive effect or other basis, on that reason Concept NACK
  9. in test/functional/mempool_accept.py:330 in 5fd80bd662
    326@@ -327,11 +327,13 @@ def run_test(self):
    327             rawtxs=[tx.serialize().hex()],
    328         )
    329         tx = tx_from_hex(raw_tx_reference)
    330+        tx.vout[0].nValue = int(tx.vout[0].nValue / 2)
    


    polespinasa commented at 10:10 pm on April 29, 2025:
    Why is this needed?
  10. polespinasa commented at 10:17 pm on April 29, 2025: contributor

    uACK

    Code lgtm, just a small question on the test code.

  11. portlandhodl commented at 2:38 am on April 30, 2025: contributor

    Concept Ack

    Same code is already being applied to at least 3-5% of hashrate today at MARA.

  12. luke-jr commented at 3:00 am on April 30, 2025: member
    Concept NACK. At a minimum, it would need to tally up the total length (including the sizes of additional amount + script size fields) and ensure it remains under the datacarriersize limit. But also, such “protocols” are abusive and should not exist. Even if they were to be tolerated, they could just as well divide the existing datacarrier space without having multiple outputs.
  13. chrisguida commented at 3:00 am on April 30, 2025: none

    Concept NACK, same reason as on #32359 (comment).

    The proper solution is to filter inscriptions and other new spamming techniques, not to keep appeasing the spammers. The more we appease them, the more “economic demand” there will be to create spam.

  14. adamdecaf commented at 4:46 am on April 30, 2025: none

    Concept Nack

    Other coins can and have implemented wasteful experiments. There’s no reason or authority to make Bitcoin into a shitcoin. Leave the hardest money on the planet alone and let it fix the world.

  15. moth-oss commented at 10:27 am on April 30, 2025: none

    Concept NACK

    Removing op_return output limit will be ineffective in making a dent in the UTXO bloat because of the segwit discount vs cost of putting data in op_return. We should focus on addressing the UTXO bloat issue head on vs hoping that people will pay more just to store data “the right way”.

  16. stevenroose commented at 12:09 pm on April 30, 2025: contributor
    ACK
  17. jesterhodl commented at 1:41 pm on April 30, 2025: none

    The rationale for these changes as outlined on the ML is to facilitate Citrea and nudge them in a more efficient direction of storing data in OP_RETURN, instead of taproot witness. As I understand they plan to use 1 OP_RETURN of 80 bytes and 2 taproot outputs of 32 bytes, totaling 3 data chunks of 144 bytes in sum.

    Removing the limit on count of OP_RETURNs is therefore much over what’s realistically attempted by them and removal of safety measure introduces risk of data insertions which subsequently would increase cost to run nodes. Which is why I concept NACK.

    Directional suggestion: instead of removing the limit on count, you could let node runners set the policy via something like opreturncount and set the default to 2. That way the change is mild, doesn’t remove safety, and lets Citrea regroup and still make it in one tx.

  18. instagibbs commented at 1:50 pm on April 30, 2025: member

    ~0

    If we’re twiddling a knob to upset the least amount of people we probably should just twiddle it to the value we’re considering (plus some buffer). Anything that results in 150 bytes or beyond? @jesterhodl while I understand your main point I don’t think adding more args in the node makes sense.

  19. darosior commented at 2:45 pm on April 30, 2025: member
    @jesterhodl conceptual discussions should go on the mailing list. If you do take it there please understand the topic before sharing your strong opinion, this has nothing to do with Taproot witnesses. I just blocked you, which should prevent you from commenting further on this PR and wasting everyone’s time.
  20. ajtowns commented at 3:26 pm on April 30, 2025: contributor

    Approach NACK for me – there’s no point having a limit that’s trivially avoided by just having many OP_RETURNs, and forcing people to waste blockspace by encoding redundant nValue and OP_RETURN OP_PUSH instructions when they would have been happy with the same data in a single output doesn’t seem desirable either.

    IMO it would make sense to:

    • raise the default limit to a value that avoids people creating unprunable burn addresses,
    • remove the (unconfigurable) limit on number of OP_RETURNs in a transaction
    • apply the existing limit across all OP_RETURNs in a transaction

    I think a patch like this would achieve that (ignoring the functional tests):

      0diff --git a/src/policy/policy.h b/src/policy/policy.h
      1index 2151ec13dd0..0b302aa31a3 100644
      2--- a/src/policy/policy.h
      3+++ b/src/policy/policy.h
      4@@ -73,10 +73,10 @@ static constexpr unsigned int DEFAULT_DESCENDANT_SIZE_LIMIT_KVB{101};
      5 /** Default for -datacarrier */
      6 static const bool DEFAULT_ACCEPT_DATACARRIER = true;
      7 /**
      8- * Default setting for -datacarriersize. 80 bytes of data, +1 for OP_RETURN,
      9+ * Default setting for -datacarriersize. 160 bytes of data, +1 for OP_RETURN,
     10  * +2 for the pushdata opcodes.
     11  */
     12-static const unsigned int MAX_OP_RETURN_RELAY = 83;
     13+static const unsigned int MAX_OP_RETURN_RELAY = 163;
     14 /**
     15  * An extra transaction can be added to a package, as long as it only has one
     16  * ancestor and is no larger than this. Not really any reason to make this
     17@@ -136,7 +136,7 @@ CAmount GetDustThreshold(const CTxOut& txout, const CFeeRate& dustRelayFee);
     18
     19 bool IsDust(const CTxOut& txout, const CFeeRate& dustRelayFee);
     20
     21-bool IsStandard(const CScript& scriptPubKey, const std::optional<unsigned>& max_datacarrier_bytes, TxoutType& whichType);
     22+bool IsStandard(const CScript& scriptPubKey, TxoutType& whichType);
     23
     24 /** Get the vout index numbers of all dust outputs */
     25 std::vector<uint32_t> GetDust(const CTransaction& tx, CFeeRate dust_relay_rate);
     26diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp
     27index 32a31ea653c..fdc2fdb9cdf 100644
     28--- a/src/policy/policy.cpp
     29+++ b/src/policy/policy.cpp
     30@@ -76,7 +76,7 @@ std::vector<uint32_t> GetDust(const CTransaction& tx, CFeeRate dust_relay_rate)
     31     return dust_outputs;
     32 }
     33
     34-bool IsStandard(const CScript& scriptPubKey, const std::optional<unsigned>& max_datacarrier_bytes, TxoutType& whichType)
     35+bool IsStandard(const CScript& scriptPubKey, TxoutType& whichType)
     36 {
     37     std::vector<std::vector<unsigned char> > vSolutions;
     38     whichType = Solver(scriptPubKey, vSolutions);
     39@@ -91,10 +91,6 @@ bool IsStandard(const CScript& scriptPubKey, const std::optional<unsigned>& max_
     40             return false;
     41         if (m < 1 || m > n)
     42             return false;
     43-    } else if (whichType == TxoutType::NULL_DATA) {
     44-        if (!max_datacarrier_bytes || scriptPubKey.size() > *max_datacarrier_bytes) {
     45-            return false;
     46-        }
     47     }
     48
     49     return true;
     50@@ -137,17 +133,22 @@ bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_dat
     51         }
     52     }
     53
     54-    unsigned int nDataOut = 0;
     55+    unsigned int datacarrier_bytes_left = max_datacarrier_bytes.value_or(0);
     56     TxoutType whichType;
     57     for (const CTxOut& txout : tx.vout) {
     58-        if (!::IsStandard(txout.scriptPubKey, max_datacarrier_bytes, whichType)) {
     59+        if (!::IsStandard(txout.scriptPubKey, whichType)) {
     60             reason = "scriptpubkey";
     61             return false;
     62         }
     63
     64-        if (whichType == TxoutType::NULL_DATA)
     65-            nDataOut++;
     66-        else if ((whichType == TxoutType::MULTISIG) && (!permit_bare_multisig)) {
     67+        if (whichType == TxoutType::NULL_DATA) {
     68+            unsigned int size = txout.scriptPubKey.size();
     69+            if (size > datacarrier_bytes_left) {
     70+                reason = "datacarrier";
     71+                return false;
     72+            }
     73+            datacarrier_bytes_left -= size;
     74+        } else if ((whichType == TxoutType::MULTISIG) && (!permit_bare_multisig)) {
     75             reason = "bare-multisig";
     76             return false;
     77         }
     78@@ -159,12 +160,6 @@ bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_dat
     79         return false;
     80     }
     81
     82-    // only one OP_RETURN txout is permitted
     83-    if (nDataOut > 1) {
     84-        reason = "multi-op-return";
     85-        return false;
     86-    }
     87-
     88     return true;
     89 }
     90
     91diff --git a/src/test/fuzz/key.cpp b/src/test/fuzz/key.cpp
     92index c7ff2f3a28e..50001a1d820 100644
     93--- a/src/test/fuzz/key.cpp
     94+++ b/src/test/fuzz/key.cpp
     95@@ -151,12 +151,12 @@ FUZZ_TARGET(key, .init = initialize_key)
     96         assert(fillable_signing_provider_pub.HaveKey(pubkey.GetID()));
     97
     98         TxoutType which_type_tx_pubkey;
     99-        const bool is_standard_tx_pubkey = IsStandard(tx_pubkey_script, std::nullopt, which_type_tx_pubkey);
    100+        const bool is_standard_tx_pubkey = IsStandard(tx_pubkey_script, which_type_tx_pubkey);
    101         assert(is_standard_tx_pubkey);
    102         assert(which_type_tx_pubkey == TxoutType::PUBKEY);
    103
    104         TxoutType which_type_tx_multisig;
    105-        const bool is_standard_tx_multisig = IsStandard(tx_multisig_script, std::nullopt, which_type_tx_multisig);
    106+        const bool is_standard_tx_multisig = IsStandard(tx_multisig_script, which_type_tx_multisig);
    107         assert(is_standard_tx_multisig);
    108         assert(which_type_tx_multisig == TxoutType::MULTISIG);
    109
    110diff --git a/src/test/fuzz/script.cpp b/src/test/fuzz/script.cpp
    111index 07a49e039fb..9aa50559cbd 100644
    112--- a/src/test/fuzz/script.cpp
    113+++ b/src/test/fuzz/script.cpp
    114@@ -53,7 +53,7 @@ FUZZ_TARGET(script, .init = initialize_script)
    115     }
    116
    117     TxoutType which_type;
    118-    bool is_standard_ret = IsStandard(script, std::nullopt, which_type);
    119+    bool is_standard_ret = IsStandard(script, which_type);
    120     if (!is_standard_ret) {
    121         assert(which_type == TxoutType::NONSTANDARD ||
    122                which_type == TxoutType::NULL_DATA ||
    123diff --git a/src/test/multisig_tests.cpp b/src/test/multisig_tests.cpp
    124index 29a73d03d25..b996f3cc061 100644
    125--- a/src/test/multisig_tests.cpp
    126+++ b/src/test/multisig_tests.cpp
    127@@ -144,7 +144,7 @@ BOOST_AUTO_TEST_CASE(multisig_IsStandard)
    128
    129     const auto is_standard{[](const CScript& spk) {
    130         TxoutType type;
    131-        bool res{::IsStandard(spk, std::nullopt, type)};
    132+        bool res{::IsStandard(spk, type)};
    133         if (res) {
    134             BOOST_CHECK_EQUAL(type, TxoutType::MULTISIG);
    135         }
    136diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp
    137index 1375672a418..ba4b6a69b4b 100644
    138--- a/src/test/transaction_tests.cpp
    139+++ b/src/test/transaction_tests.cpp
    140@@ -859,14 +859,14 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
    141     CheckIsNotStandard(t, "scriptpubkey");
    142
    143     // MAX_OP_RETURN_RELAY-byte TxoutType::NULL_DATA (standard)
    144-    t.vout[0].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"_hex;
    145+    t.vout[0].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"_hex;
    146     BOOST_CHECK_EQUAL(MAX_OP_RETURN_RELAY, t.vout[0].scriptPubKey.size());
    147     CheckIsStandard(t);
    148
    149     // MAX_OP_RETURN_RELAY+1-byte TxoutType::NULL_DATA (non-standard)
    150-    t.vout[0].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3800"_hex;
    151+    t.vout[0].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3800"_hex;
    152     BOOST_CHECK_EQUAL(MAX_OP_RETURN_RELAY + 1, t.vout[0].scriptPubKey.size());
    153-    CheckIsNotStandard(t, "scriptpubkey");
    154+    CheckIsNotStandard(t, "datacarrier");
    155
    156     // Data payload can be encoded in any way...
    157     t.vout[0].scriptPubKey = CScript() << OP_RETURN << ""_hex;
    158@@ -888,21 +888,25 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
    159     t.vout[0].scriptPubKey = CScript() << OP_RETURN;
    160     CheckIsStandard(t);
    161
    162-    // Only one TxoutType::NULL_DATA permitted in all cases
    163+    // Multiple TxoutType::NULL_DATA now permitted
    164     t.vout.resize(2);
    165     t.vout[0].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"_hex;
    166     t.vout[0].nValue = 0;
    167     t.vout[1].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"_hex;
    168     t.vout[1].nValue = 0;
    169-    CheckIsNotStandard(t, "multi-op-return");
    170+    CheckIsStandard(t);
    171
    172     t.vout[0].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"_hex;
    173     t.vout[1].scriptPubKey = CScript() << OP_RETURN;
    174-    CheckIsNotStandard(t, "multi-op-return");
    175+    CheckIsStandard(t);
    176
    177     t.vout[0].scriptPubKey = CScript() << OP_RETURN;
    178     t.vout[1].scriptPubKey = CScript() << OP_RETURN;
    179-    CheckIsNotStandard(t, "multi-op-return");
    180+    CheckIsStandard(t);
    181+
    182+    t.vout[0].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"_hex;
    183+    t.vout[1].scriptPubKey = CScript() << OP_RETURN << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"_hex;
    184+    CheckIsNotStandard(t, "datacarrier");
    185
    186     // Check large scriptSig (non-standard if size is >1650 bytes)
    187     t.vout.resize(1);
    
  21. darosior commented at 7:12 pm on April 30, 2025: member

    My intention with this PR was to have a first step that is consensual among regular contributors, seeing some expressed doubts with #32359 (i counted @danielabrozzoni @polespinasa and @andrewtoth but i may have missed some in the middle of all the spam).

    Seeing that it is not, i don’t think there is any use in keeping this PR open. We should address the concerns of those contributors and do #32359 instead.

  22. darosior closed this on Apr 30, 2025

  23. Sjors commented at 7:19 pm on April 30, 2025: member
    I already suspected that even a variant of #32359 that retains the -datacarrier configuration option would meet just as much resistance. This PR does far less than that. So the response here confirms, at least to me, that there’s no point in having any reduced scope alternative.
  24. ajtowns commented at 0:33 am on May 1, 2025: contributor

    My intention with this PR was to have a first step that is consensual among regular contributors,

    The minimal change would just be to bump the default value of datacarrier, afaics.

  25. Sjors commented at 9:55 am on May 1, 2025: member
    @ajtowns yes, that would be even simpler than this PR, though I doubt it will suddenly be embraced.
  26. l0rinc commented at 11:41 am on May 1, 2025: contributor
    These changes caught many people off guard, and the perceived urgency understandably triggered strong reactions. For proposals this contentious, it might help to start with an educational phase: GitHub issues, blog posts, podcasts, conference talks, and perhaps even a draft BIP, so the rationale is clear and both positions are properly steel-manned before opening a PR (ideally only after the subject has become mundane). Without that groundwork, it is easy to understand why some view the process as heavy-handed and regard moderation of off-topic comments on GitHub as censorship.
  27. sonnyxsm commented at 2:03 pm on May 1, 2025: none
    this is not your wastebucket.
  28. darosior commented at 3:40 pm on May 1, 2025: member

    Would it be possible to summarize the pros and cons raised across the internets in the PR description - if for no other reason, to avoid repeated comments on the same topic?

    I gave the rationale for this on the mailing list. I also gave more background in a blog post i just published.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-05-05 12:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me