diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h
index b35a55fd289..435151c5dda 100644
--- a/src/interfaces/mining.h
+++ b/src/interfaces/mining.h
@@ -64,6 +64,9 @@ public:
      *                      for the next block should rise (default infinite).
      *
      * @returns a new BlockTemplate or nothing if the timeout occurs.
+     *
+     * On testnet this will additionally return a template with difficulty 1 if
+     * the tip is more than 20 minutes old.
      */
     virtual std::unique_ptr<BlockTemplate> waitNext(const node::BlockWaitOptions options = {}) = 0;
 };
diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
index 3fd7fc1bf27..50207c658d8 100644
--- a/src/node/interfaces.cpp
+++ b/src/node/interfaces.cpp
@@ -958,6 +958,7 @@ public:
         auto now{NodeClock::now()};
         const auto deadline = now + options.timeout;
         const MillisecondsDouble tick{1000};
+        const bool allow_min_difficulty{chainman().GetParams().GetConsensus().fPowAllowMinDifficultyBlocks};
 
         do {
             bool tip_changed{false};
@@ -982,6 +983,14 @@ public:
             // Must release m_tip_block_mutex before locking cs_main, to avoid deadlocks.
             LOCK(::cs_main);
 
+            // On test networks return a minimum difficulty block after 20 minutes
+            if (!tip_changed && allow_min_difficulty) {
+                const NodeClock::time_point tip_time{std::chrono::seconds{chainman().ActiveChain().Tip()->GetBlockTime()}};
+                if (now > tip_time + 20min) {
+                    tip_changed = true;
+                }
+            }
+
             /**
              * We determine if fees increased compared to the previous template by generating
              * a fresh template. There may be more efficient ways to determine how much
diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt
index b0dd27894d3..6e245aefb34 100644
--- a/src/test/CMakeLists.txt
+++ b/src/test/CMakeLists.txt
@@ -121,6 +121,7 @@ add_executable(test_bitcoin
   streams_tests.cpp
   sync_tests.cpp
   system_tests.cpp
+  testnet4_miner_tests.cpp
   timeoffsets_tests.cpp
   torcontrol_tests.cpp
   transaction_tests.cpp
diff --git a/src/test/testnet4_miner_tests.cpp b/src/test/testnet4_miner_tests.cpp
new file mode 100644
index 00000000000..54ca8e32cfe
--- /dev/null
+++ b/src/test/testnet4_miner_tests.cpp
@@ -0,0 +1,75 @@
+// Copyright (c) 2025 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <common/system.h>
+#include <interfaces/mining.h>
+#include <node/miner.h>
+#include <util/time.h>
+#include <validation.h>
+
+#include <test/util/setup_common.h>
+
+#include <boost/test/unit_test.hpp>
+
+using interfaces::BlockTemplate;
+using interfaces::Mining;
+using node::BlockAssembler;
+using node::BlockWaitOptions;
+
+namespace testnet4_miner_tests {
+
+struct Testnet4MinerTestingSetup : public Testnet4Setup {
+    std::unique_ptr<Mining> MakeMining()
+    {
+        return interfaces::MakeMining(m_node);
+    }
+};
+} // namespace testnet4_miner_tests
+
+BOOST_FIXTURE_TEST_SUITE(testnet4_miner_tests, Testnet4MinerTestingSetup)
+
+BOOST_AUTO_TEST_CASE(MiningInterface)
+{
+    auto mining{MakeMining()};
+    BOOST_REQUIRE(mining);
+
+    BlockAssembler::Options options;
+    std::unique_ptr<BlockTemplate> block_template;
+
+    // Set node time a few minutes past the testnet4 genesis block
+    const int64_t genesis_time{WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Tip()->GetBlockTime())};
+    SetMockTime(genesis_time + 3 * 60);
+
+    block_template = mining->createNewBlock(options);
+    BOOST_REQUIRE(block_template);
+
+    // The template should use the mocked system time
+    BOOST_REQUIRE_EQUAL(block_template->getBlockHeader().nTime, genesis_time + 3 * 60);
+
+    const BlockWaitOptions wait_options{.timeout = MillisecondsDouble{0}, .fee_threshold = 1};
+
+    // waitNext() should return nullptr because there is no better template
+    auto should_be_nullptr = block_template->waitNext(wait_options);
+    BOOST_REQUIRE(should_be_nullptr == nullptr);
+
+    // This remains the case when exactly 20 minutes have gone by
+    {
+        LOCK(cs_main);
+        SetMockTime(m_node.chainman->ActiveChain().Tip()->GetBlockTime() + 20 * 60);
+    }
+    should_be_nullptr = block_template->waitNext(wait_options);
+    BOOST_REQUIRE(should_be_nullptr == nullptr);
+
+    // One second later the difficulty drops and it returns a new template
+    // Note that we can't test the actual difficulty change, because the
+    // difficulty is already at 1.
+    {
+        LOCK(cs_main);
+        SetMockTime(m_node.chainman->ActiveChain().Tip()->GetBlockTime() + 20 * 60 + 1);
+    }
+    block_template = block_template->waitNext(wait_options);
+    BOOST_REQUIRE(block_template);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h
index 33ad2584573..57bea9086b9 100644
--- a/src/test/util/setup_common.h
+++ b/src/test/util/setup_common.h
@@ -130,6 +130,12 @@ struct RegTestingSetup : public TestingSetup {
         : TestingSetup{ChainType::REGTEST} {}
 };
 
+/** Identical to TestingSetup, but chain set to testnet4 */
+struct Testnet4Setup : public TestingSetup {
+    Testnet4Setup()
+        : TestingSetup{ChainType::TESTNET4} {}
+};
+
 class CBlock;
 struct CMutableTransaction;
 class CScript;