consensus: disable min difficulty blocks on testnet4 after height 151,200 #34420

pull batmanbytes wants to merge 9 commits into bitcoin:master from batmanbytes:testnet4-fix changing 6 files +166 −6
  1. batmanbytes commented at 3:08 pm on January 27, 2026: none

    Summary

    Disable the min-difficulty block rule on testnet4 after block height 151,200 (epoch 75 boundary).

    Motivation

    Testnet4’s min-difficulty rule (allowing difficulty-1 blocks after 20 minutes) is being exploited, causing ~85-90% of blocks to be CPU-mined min-difficulty blocks. This results in a race of CPU miners broadcasting blocks at exactly the second that it’s acceptable. This race is so intense that CPU miners have figured out sending empty blocks, as they propagate faster than those that contain transactions, resulting in a network where transactions are confirmed at only the remaining ASIC blocks, that have ~1 hour average block times.

    After the fork:

    • Min-difficulty blocks will no longer be allowed
    • Normal difficulty retargeting resumes

    Changes

    • Add nMinDifficultyBlocksForkHeight consensus parameter (default 0 = disabled)
    • Set fork height to 151,200 for testnet4 (epoch 75 boundary, ~20,000 blocks from current height ~130,000)
    • Modify GetNextWorkRequired() to disable min-difficulty rule at fork height 151,200
    • Modify PermittedDifficultyTransition() to enforce the new rules
    • Modify WaitAndCreateNewBlock() in miner to respect the fork
    • Add unit tests covering pre-fork, at-fork, and post-fork behavior

    Test Plan

    • ./build/bin/test_bitcoin --run_test=pow_tests - all 21 tests pass

    Discussion

    [bitcoin-dev mailing list thread #1](https://groups.google.com/g/bitcoindev/c/iVLHJ1HWhoU) [bitcoin-dev mailing list thread #2](https://groups.google.com/g/bitcoindev/c/Jsv1VYpewuU)
    Bitcointalk thread

  2. DrahtBot added the label Consensus on Jan 27, 2026
  3. DrahtBot commented at 3:08 pm on January 27, 2026: 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/34420.

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

  4. DrahtBot added the label CI failed on Jan 27, 2026
  5. batmanbytes force-pushed on Jan 27, 2026
  6. consensus: disable min difficulty blocks on testnet4 after height 151200 2aa8902ca6
  7. test: add unit tests for testnet4 min-difficulty fork 2123e4d5e6
  8. batmanbytes force-pushed on Jan 27, 2026
  9. DrahtBot removed the label CI failed on Jan 28, 2026
  10. luke-jr commented at 6:14 am on January 29, 2026: member
    It’s a testnet. Any reason not to make it retroactive?
  11. stwenhao commented at 6:59 am on January 29, 2026: none

    Any reason not to make it retroactive?

    Well, it is possible to remove 20 minutes rule entirely from the source code. But that would mean testnet3 and testnet4 in their current form, would be entirely removed, and then supported only by the old Bitcoin Core version.

  12. batmanbytes commented at 10:11 am on January 29, 2026: none

    It’s a testnet. Any reason not to make it retroactive?

    A few reasons from the top of my head:

    • it’s disruptive for developers and services currently using testnet4
    • there are around 4 million test coins “pre-mined”, laying around at faucets.
    • resetting to a new testnet5 would require miners to point their hashrate elsewhere. With the current change, they just get more test coins, with the same hashrate.
    • a new testnet5 would not be supported by old Bitcoin Core version nodes, but a forked testnet4 would be fully supported.

    In my opinion, the simplification from going retroactive is marginal and doesn’t justify the disruption, especially with the fork height logic being only a few lines.

  13. stwenhao commented at 12:03 pm on January 29, 2026: none

    a new testnet5 would not be supported by old Bitcoin Core version nodes, but a forked testnet4 would be fully supported.

    In case of a hard-fork, old versions will not follow these changes anyway. If you want them, to end up in the same chain, then a different rule is needed, for example that blocks with more than 20 minutes delay, should be simply invalid (except difficulty adjustment, where you can use the real time). And then, if all ASIC blocks will be less than 20 minutes away, then they will be accepted by the old network, and will reorg CPU blocks, created in the meantime.

  14. DrahtBot added the label CI failed on Jan 30, 2026
  15. maflcko removed the label CI failed on Jan 31, 2026
  16. sedited requested review from fjahr on Mar 8, 2026
  17. in src/node/miner.cpp:400 in 2aa8902ca6 outdated
    393@@ -394,8 +394,13 @@ std::unique_ptr<CBlockTemplate> WaitAndCreateNewBlock(ChainstateManager& chainma
    394         // Must release m_tip_block_mutex before locking cs_main, to avoid deadlocks.
    395         LOCK(::cs_main);
    396 
    397-        // On test networks return a minimum difficulty block after 20 minutes
    398-        if (!tip_changed && allow_min_difficulty) {
    399+        // On test networks return a minimum difficulty block after 20 minutes,
    400+        // but only if we haven't reached the fork height that disables this feature.
    401+        const auto& consensus = chainman.GetParams().GetConsensus();
    402+        const int nNextHeight = chainman.ActiveChain().Tip()->nHeight + 1;
    


    fjahr commented at 10:07 am on March 9, 2026:
    applies across the whole change: we don’t use the hungarian style notation anymore for new variables
  18. in src/pow.cpp:19 in 2aa8902ca6 outdated
    15@@ -16,10 +16,26 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHead
    16     assert(pindexLast != nullptr);
    17     unsigned int nProofOfWorkLimit = UintToArith256(params.powLimit).GetCompact();
    18 
    19+    // Check if min difficulty blocks are allowed at this height.
    


    fjahr commented at 10:16 am on March 9, 2026:

    This code currently runs for every network every time. This should all be nexted within a if (params.fPowAllowMinDifficultyBlocks) block at least.

    I would also like this generally extracted into a function so that it doesn’t polute the rest of the code in this function and also doesn’t need to be duplicated across PermittedDifficultyTransition. Ideally the whole min-difficulty handling could be extracted then.

  19. in src/test/pow_tests.cpp:324 in 2123e4d5e6
    319+    int non_retarget_height = 151201;  // Not divisible by 2016
    320+    BOOST_CHECK(!PermittedDifficultyTransition(consensus, non_retarget_height, some_difficulty, nProofOfWorkLimit));
    321+    BOOST_CHECK(PermittedDifficultyTransition(consensus, non_retarget_height, some_difficulty, some_difficulty));
    322+
    323+    // Further after fork: same strict rules
    324+    non_retarget_height = 152000;
    


    fjahr commented at 10:23 am on March 9, 2026:
    below here you use a variable for the value you have hardcoded above many times. It would probably be the most readable if you have a constant “fork height” for the whole test file and then use fork height - 1 or fork height +1 etc. in the different scenarios.
  20. in src/test/pow_tests.cpp:323 in 2123e4d5e6 outdated
    318+    // For non-retarget heights, difficulty must stay the same
    319+    int non_retarget_height = 151201;  // Not divisible by 2016
    320+    BOOST_CHECK(!PermittedDifficultyTransition(consensus, non_retarget_height, some_difficulty, nProofOfWorkLimit));
    321+    BOOST_CHECK(PermittedDifficultyTransition(consensus, non_retarget_height, some_difficulty, some_difficulty));
    322+
    323+    // Further after fork: same strict rules
    


    fjahr commented at 10:23 am on March 9, 2026:
    Should also have a test that the next retargeting period is based on the 1m
  21. in src/pow.cpp:31 in 2aa8902ca6 outdated
    26+    // At the fork height, reset difficulty to 1,000,000 to compensate for the
    27+    // artificially high difficulty caused by min-difficulty blocks.
    28+    // We use 1,000,000 instead of 1 (powLimit) to avoid a block storm.
    29+    if (params.nMinDifficultyBlocksForkHeight != 0 && nNextHeight == params.nMinDifficultyBlocksForkHeight) {
    30+        arith_uint256 target = UintToArith256(params.powLimit);
    31+        target /= 1000000;
    


    fjahr commented at 10:52 am on March 9, 2026:
    The 1m reset seems kind of random. It should be pretty easy to get a good approximation of what the real hashrate would be on the network without min-difficulty blocks and this way have a more calm transition.

    batmanbytes commented at 7:18 pm on March 13, 2026:

    As I have written in the mailing list:

    The number 1 million was selected arbitrarily to get the best of both worlds: (1) it won’t result in a block storm (as it would if difficulty was reset to 1) and (2) it won’t result in very long block intervals. If there weren’t any difficulty reset, and difficulty would just be calculated as normal, it’d take around an hour for a block to be found, and this would mean epoch 75 alone would take around three months instead of two weeks.

    The alternative would be to do something like new_difficulty = difficulty / 6. I just feel that this can also be a bit arbitrary, as there aren’t always 6 CPU blocks for every ASIC block.


    fjahr commented at 8:30 pm on March 13, 2026:
    Could be that the reason there are not always 6 CPU blocks is because some miners run the re-org code that I talked about. But why can’t you calculate a hypothetical difficulty based on a chain that would be include only all the non-min difficulty blocks? Unless I am missing something the min-difficulty blocks are just “filler” within the “real” blocks. If the min-difficulty exception wouldn’t exist, likely the same miners would have be running and found the same number of blocks they did now (potentially even a few more since they might lose a few seconds probably from reacting to the 6 min-difficulty blocks). So if you look at the chain without the min-difficulty blocks you should be able to get the “true” tip height. And looking at the hash rate that could be observed you should be able to calculate a “true” difficulty we should have had by now.

    stwenhao commented at 11:52 am on March 16, 2026:

    And looking at the hash rate that could be observed you should be able to calculate a “true” difficulty we should have had by now.

    As I wrote on bitcointalk, testnet4 produced around 30k more blocks, than it should. So, the difficulty can be just left as it is. For a while, we could have one block per hour, and then, things would naturally adjust to the proper level.

    So, I think the difficulty should be left untouched. Then, the slowdown will compensate the speedup from the early days, when the difficulty climbed from 1 to the real level, and from CPU miners. And even then, I am not sure, that it would take 30k blocks, so we will still have some overproduction, even if the difficulty will be left as it is.


    batmanbytes commented at 8:16 am on March 18, 2026:

    @fjahr I had not thought of doing this. So, what you are proposing is to completely ignore the min-difficulty blocks at difficulty calculation in the difficulty adjustment of the 75h epoch. This is the most appropriate way to approach this, but code simplicity is also a priority.

    I don’t like @stwenhao’s proposal, because I hate 1 hour block interval, but it is the simplest to implement.


    stwenhao commented at 11:57 am on March 18, 2026:

    Another reason to leave the difficulty untouched, is to check, how fast the network will keep moving, when all blocks with CPU difficulty will be rejected. Because the whole reason, why this 20 minute rule was introduced in the first place, was to avoid the situation, when many ASICs joined the network, and then left it at some high difficulty. And then, the remaining testers were unable to move the chain forward.

    So, if the difficulty will be bumped by someone (for example by some mainnet mining pool, when they will use testnet to test some things) and there will be no new blocks for a long time, then would you suggest making yet another hard-fork, to lower it again?

    I hate 1 hour block interval

    And you don’t hate the fact, that testnet4 overproduced around 30k blocks?

    0genesis_time=1714777860
    1current_time=1773830000
    2time_difference=current_time-genesis_time=59052140
    3blocks=time_difference/600=98420
    4current_height=126440
    5overproduction=current_height-blocks=28020
    6adjustments=overproduction/2016=13.8
    

    I think going from one block per hour to one block per 10 minutes will take less than 13 adjustments, which means, that if the difficulty will be left as it is, then we will still have some overproduction.

  22. fjahr commented at 11:51 am on March 9, 2026: contributor

    I don’t have a strong opinion whether the issue needs to be fixed with a fork or not, that’s something that should be judged by the users and I don’t have a good picture if enough people are annoyed by this. The re-orgs have the nice effect of stress testing application robustness in this regard, something that was always promised for signet but never implemented afaik. If users would like to kick out the difficulty exception and find the PoW-powered Testnet more useful this way, then I would be happy to support it. In the development of Testnet4 early on I favored removing the exception but allowing non-standard tx relay but this was rejected by reviewers. I am not sure if there is renewed interest in changing relay along with this change but I doubt it.

    To be fair, the idea of removing the exception was posted on the mailing list and bitcoin talk and I could not find any serious push-back in these discussions. To at least there appear to be no strong critique to removing the exception. Just typical bike shedding :) I have only skimmed it though for now. Maybe a delving post would be nice as well.

    The non-fork fix to this is still available as re-orging the min-difficulty blocks, implemented in #31117 which I will refresh to help with discussion. I know some miners were running this code but I am not sure how large the share is. Per fork observer the re-orgs seem to have been reduced lately though. Additionally we could configure miners to always mine 2 hours in the future. This would prevent the exception from being usable typically. I preferred the above re-org variant only because it still allowed for the distribution non-standard transactions but if we are talking about removing the exception altogether with a hardfork I guess we ignore that aspect now.

    Another tricky question seems on how this could be introduced with the least disruption. Since this has missed v31 and roughly the current activation height is around the v32 release, ideally I think activation should be pulled forward and if enough Testnet4 miners could be convinced to run the patch it would obviously be much easier to merge this PR into Core to have it in v32. If you are doing the outreach to miners part @batmanbytes I think that would be motivating to get more review here short term.

    Last but not least you should also open an accompanying amendment pull to BIP94: https://github.com/bitcoin/bips/blob/master/bip-0094.mediawiki

    Only skimmed the code superficially and left some initial comments.

  23. refactor: extract min-difficulty fork logic into helpers and clean up style bde4327d98
  24. batmanbytes commented at 7:32 pm on March 13, 2026: none

    I have introduced AllowMinDifficultyBlocks() and GetMinDifficultyForkBits() to deduplicate min-difficulty fork handling between GetNextWorkRequired() and GetNextWorkRequired().

    I have also renamed the variables from Hungarian notation to snake_case, replaced hardcoded fork heights in tests with consensus-driven values and added a test for retarget behavior after the fork. @fjahr you can review the changes. I have not changed the fork height (151200). Maybe I should change it to one year after 151200? (151200 + 2016 x 26 = 203616). I am not sure when it would be the ideal block.

  25. in src/node/miner.cpp:401 in bde4327d98
    398-        if (!tip_changed && allow_min_difficulty) {
    399+        // On test networks return a minimum difficulty block after 20 minutes,
    400+        // but only if we haven't reached the fork height that disables this feature.
    401+        const auto& consensus = chainman.GetParams().GetConsensus();
    402+        const int next_height = chainman.ActiveChain().Tip()->nHeight + 1;
    403+        if (!tip_changed && allow_min_difficulty && AllowMinDifficultyBlocks(consensus, next_height)) {
    


    fjahr commented at 8:17 pm on March 13, 2026:

    Even the consensus and next_height fetching should be skipped IMO

    0        if (!tip_changed && allow_min_difficulty) {
    1            const auto& consensus = chainman.GetParams().GetConsensus();
    2            const int next_height = chainman.ActiveChain().Tip()->nHeight + 1;
    3            if (AllowMinDifficultyBlocks(consensus, next_height)) {
    
  26. in src/pow.cpp:44 in bde4327d98
    39+    if (auto fork_bits = GetMinDifficultyForkBits(params, next_height)) {
    40+        return *fork_bits;
    41+    }
    42+
    43+    const bool allow_min_difficulty_blocks = AllowMinDifficultyBlocks(params, next_height);
    44+
    


    fjahr commented at 8:19 pm on March 13, 2026:
    This should all also move into the block that checks if (params.fPowAllowMinDifficultyBlocks), still still runs for every call of GetNextWorkRequired on every network and that is just not necessary.
  27. in src/pow.cpp:122 in bde4327d98
    118+    if (AllowMinDifficultyBlocks(params, height)) return true;
    119+
    120+    // At the fork height, only the reset to difficulty 1,000,000 is valid
    121+    if (auto fork_bits = GetMinDifficultyForkBits(params, height)) {
    122+        return new_nbits == *fork_bits;
    123+    }
    


    fjahr commented at 8:23 pm on March 13, 2026:
    This also still runs for every network on every call, same as in the other cases I commented.
  28. fjahr commented at 8:36 pm on March 13, 2026: contributor

    I have not changed the fork height (151200). Maybe I should change it to one year after 151200? (151200 + 2016 x 26 = 203616). I am not sure when it would be the ideal block.

    I have no idea, this is something that is probably best discussed on the mailing list and/or delving bitcoin alongside the a PR to the BIP. While I am giving you some practical feedback on the code here, I think should focus on making the BIP PR and asking these questions to a wider audience. There are other implementations and libraries that implement Testnet4 with it’s difficulty exception, you will need to get them on board as well and you likely won’t reach many of them here. If they don’t engage right away you might need to be more proactive and open issues in their repos to get at least some acknowledgement of the project if you want to make progress quickly (but of course still be respectful).

  29. refactor: guard min-difficulty fork logic behind fPowAllowMinDifficultyBlocks 7dbc166a51
  30. disable min difficulty blocks on testnet4 without resetting difficulty 6396377a4a
  31. StevenSteiner commented at 5:19 am on March 21, 2026: none

    I don’t have a strong opinion whether the issue needs to be fixed with a fork or not, that’s something that should be judged by the users and I don’t have a good picture if enough people are annoyed by this. The re-orgs have the nice effect of stress testing application robustness in this regard, something that was always promised for signet but never implemented afaik. If users would like to kick out the difficulty exception and find the PoW-powered Testnet more useful this way, then I would be happy to support it. In the development of Testnet4 early on I favored removing the exception but allowing non-standard tx relay but this was rejected by reviewers. I am not sure if there is renewed interest in changing relay along with this change but I doubt it.

    To be fair, the idea of removing the exception was posted on the mailing list and bitcoin talk and I could not find any serious push-back in these discussions. To at least there appear to be no strong critique to removing the exception. Just typical bike shedding :) I have only skimmed it though for now. Maybe a delving post would be nice as well.

    The non-fork fix to this is still available as re-orging the min-difficulty blocks, implemented in #31117 which I will refresh to help with discussion. I know some miners were running this code but I am not sure how large the share is. Per fork observer the re-orgs seem to have been reduced lately though. Additionally we could configure miners to always mine 2 hours in the future. This would prevent the exception from being usable typically. I preferred the above re-org variant only because it still allowed for the distribution non-standard transactions but if we are talking about removing the exception altogether with a hardfork I guess we ignore that aspect now.

    Another tricky question seems on how this could be introduced with the least disruption. Since this has missed v31 and roughly the current activation height is around the v32 release, ideally I think activation should be pulled forward and if enough Testnet4 miners could be convinced to run the patch it would obviously be much easier to merge this PR into Core to have it in v32. If you are doing the outreach to miners part @batmanbytes I think that would be motivating to get more review here short term.

    Getting Testnet miners to mine the patch will not be an issue. Everyone wants this fixed, including the people doing it.

    Assuming the patch is fair and in line with Bitcoin.

    Just remove the funky difficulty stuff and let us know what to mine.

    It’s a bad thing for the “quick” miner, but that’s OK, and they are good folks who want to see Bitcoin thrive.

    Our company is stamped into some of the miners txid’s from a good customer of ours, so you can be assured our customer friend has the blocks required to hit the numbers needed to get this all straightened out.

    You can verify my service is being mined into the blocks you discuss. Example: https://mempool.space/testnet4/block/0000000000808d8edc161db8811c943f817899005863a46ea724661a00b615d3

    I’ve also spoken with Portland from MARA, and he definitely wants a fix. He expressed interest in helping if needed.

    I’m happy to arrange a conversation with our customer miner if interested, but ya, I don’t think a talk is needed if we are just dropping the dif rule and catching >80% of the blocks for a while. (This doesn’t mean the miner will for sure play ball, but they are good folks.)

    Thanks everyone.

    Respectfully,

    Steven Steiner BayAreaCoins

  32. consensus: delay min-difficulty fork to height 201600 (epoch 100) 76efab69b3
  33. batmanbytes renamed this:
    consensus: disable min difficulty blocks on testnet4 after height 151200
    consensus: disable min difficulty blocks on testnet4 after height 201,600
    on Mar 22, 2026
  34. batmanbytes commented at 1:17 pm on March 22, 2026: none

    Quoting myself from Bitcointalk

    A few changes I made:

    • I increased fork height to block 201,600 (the epoch 100 boundary), since we need more time for further discussion.
    • I removed the difficulty adjustment reset to difficulty 1,000,000. One great argument for having no difficulty resets > is that it is less complicated in the code, and I believe this means it has higher chances of getting merged.

    I have also created a thread on delvingbitcoin: https://delvingbitcoin.org/t/disabling-min-difficulty-blocks-on-testnet4/2350 @fjahr what do you think?

  35. fjahr commented at 7:21 pm on March 22, 2026: contributor

    @fjahr what do you think?

    On the fork height increase I still can’t say much other than what I already wrote above, that’s something that you will need feedback from the wider community. But I think no difficulty adjustment is a good idea, less code to maintain and bikeshed over. Thanks for opening the delving thread.

  36. Testing fork for height 129,024 21caca0d05
  37. Revert back to fork height 151,200 83d39a7a02
  38. batmanbytes renamed this:
    consensus: disable min difficulty blocks on testnet4 after height 201,600
    consensus: disable min difficulty blocks on testnet4 after height 151,200
    on Apr 5, 2026
  39. Comment corrections (changed "hard fork" to "soft fork", because it is not a hard fork without difficulty adjustment to 1 million) 32a5e57a03

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: 2026-04-05 15:13 UTC

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