intermittent issue in wallet_txn_clone.py (Windows ucrt) while starting network thread? #34367

issue maflcko openend this issue on January 21, 2026
  1. maflcko commented at 12:17 pm on January 21, 2026: member

    https://github.com/bitcoin/bitcoin/actions/runs/20090968443/job/57638966158?pr=32800#step:13:203:

     077/276 - wallet_txn_clone.py failed, Duration: 1 s
     1
     2stdout:
     32025-12-10T08:04:27.500134Z TestFramework (INFO): PRNG seed is: 4018092284830106117
     4
     5
     6
     7stderr:
     8
     9
    10Combine the logs and print the last 99999999 lines ...
    11
    12============
    13Combined log for D:\a\_temp/test_runner_₿_🏃_20251210_075632/wallet_txn_clone_196:
    14============
    15
    16 test  2025-12-10T08:04:27.500134Z TestFramework (INFO): PRNG seed is: 4018092284830106117 
    17 test  2025-12-10T08:04:27.500433Z TestFramework (DEBUG): Setting up network thread 
    

    Given that nothing else is printed on stdout, and stderr, the test must have failed before setup_chain, because that should log as well.

    This leaves something with the network thread. However, if the thread throws an exception, it should still print to stderr. If it kills the program silently, it seems like an upstream python bug.

  2. maflcko added the label Tests on Jan 21, 2026
  3. maflcko added the label CI failed on Jan 21, 2026
  4. maflcko added the label Windows on Jan 21, 2026
  5. maflcko commented at 12:18 pm on January 21, 2026: member

    So this seems like an upstream Python bug.

    Closing for now, but discussion can continue, if someone finds something.

  6. fanquake closed this on Jan 21, 2026

  7. fanquake commented at 4:37 pm on March 12, 2026: member
  8. fanquake reopened this on Mar 12, 2026

  9. l0rinc commented at 8:54 pm on March 12, 2026: contributor

    Asked an agent to play around with it on Windows, and it flagged https://github.com/bitcoin/bitcoin/blob/f7e0c3d3d370a4e5b4060fefcb9e8d83e2533bbc/test/functional/test_framework/p2p.py#L743-L744 as a potential source of the problem.

    I haven’t investigated if it’s total garbage (since I haven’t worked with that area of the code yet), but the changes the agent made locally were:

    • p2p.py:736: NetworkThread no longer creates the asyncio loop in __init__. It now creates and registers the loop inside run(), adds _loop_ready, and makes close() tolerate startup failures.
    • test_framework.py:258: the startup wait now checks network_event_loop is not None and is_running() instead of assuming the loop object already exists.
     0diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py
     1index 7a7cce6176..2468b4acf6 100755
     2--- a/test/functional/test_framework/p2p.py
     3+++ b/test/functional/test_framework/p2p.py
     4@@ -740,16 +740,28 @@ class NetworkThread(threading.Thread):
     5
     6         NetworkThread.listeners = {}
     7         NetworkThread.protos = {}
     8-        if platform.system() == 'Windows':
     9-            asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    10-        NetworkThread.network_event_loop = asyncio.new_event_loop()
    11+        self._loop_ready = threading.Event()
    12
    13     def run(self):
    14         """Start the network thread."""
    15-        self.network_event_loop.run_forever()
    16+        try:
    17+            if platform.system() == 'Windows':
    18+                asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    19+            # Create and register the loop on the worker thread instead of
    20+            # constructing it on the main thread and handing it across.
    21+            NetworkThread.network_event_loop = asyncio.new_event_loop()
    22+            asyncio.set_event_loop(self.network_event_loop)
    23+            self._loop_ready.set()
    24+            self.network_event_loop.run_forever()
    25+        finally:
    26+            self._loop_ready.set()
    27
    28     def close(self, *, timeout):
    29         """Close the connections and network event loop."""
    30+        if not self._loop_ready.wait(timeout=timeout) or self.network_event_loop is None:
    31+            self.join(timeout)
    32+            NetworkThread.network_event_loop = None
    33+            return
    34         self.network_event_loop.call_soon_threadsafe(self.network_event_loop.stop)
    35         wait_until_helper_internal(lambda: not self.network_event_loop.is_running(), timeout=timeout)
    36         self.network_event_loop.close()
    37diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
    38index 1f95756445..f968d6cc56 100755
    39--- a/test/functional/test_framework/test_framework.py
    40+++ b/test/functional/test_framework/test_framework.py
    41@@ -258,7 +258,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
    42         self.log.debug('Setting up network thread')
    43         self.network_thread = NetworkThread()
    44         self.network_thread.start()
    45-        self.wait_until(lambda: self.network_thread.network_event_loop.is_running())
    46+        self.wait_until(lambda: self.network_thread.network_event_loop is not None and self.network_thread.network_event_loop.is_running())
    47
    48         if self.options.usecli:
    
  10. maflcko commented at 8:59 pm on March 12, 2026: member
    Interesting. It would be good to ask the agent to write a patch to make this fully reproducible, so that the patch can be tested and reviewed easier. (Or possibly a stand-alone minimal reproducer for the bug and the fix)
  11. l0rinc commented at 9:51 pm on March 12, 2026: contributor

    After a few iterations it might have found something useful:

     0win@WIN-A2EHOAU4JET:/mnt/my_storage/bitcoin$ python3 contrib/devtools/networkthread_repro.py --mode legacy --iterations 1 --assert-loop-owned-by-runner --stop-on-failure
     1iteration 1/1: child exited with return code 1
     2Traceback (most recent call last):
     3  File "/mnt/c/my_storage/bitcoin/contrib/devtools/networkthread_repro.py", line 280, in <module>
     4    sys.exit(main())
     5  File "/mnt/c/my_storage/bitcoin/contrib/devtools/networkthread_repro.py", line 275, in main
     6    return run_child(args)
     7  File "/mnt/c/my_storage/bitcoin/contrib/devtools/networkthread_repro.py", line 139, in run_child
     8    exercise(
     9  File "/mnt/c/my_storage/bitcoin/contrib/devtools/networkthread_repro.py", line 128, in exercise
    10    assert_loop_owned_by_runner(thread)
    11  File "/mnt/c/my_storage/bitcoin/contrib/devtools/networkthread_repro.py", line 107, in assert_loop_owned_by_runner
    12    raise RuntimeError(
    13RuntimeError: event loop created on thread 139945949421568 but run on thread 139945935119936
    14
    15win@WIN-A2EHOAU4JET:/mnt/my_storage/bitcoin$ python3 contrib/devtools/networkthread_repro.py --mode fixed --iterations 1 --assert-loop-owned-by-runner --stop-on-failure
    16iteration 1/1: ok
    17completed 1 iterations in mode=fixed with 1 cycle(s) per child
    

    agent explanation:

     0Problem
     1
     2The intermittent wallet_txn_clone.py failure is not really about that test. The process dies in the shared functional test setup, after Setting up network thread and before setup_chain(). The
     3suspicious part was p2p.py:44: the old NetworkThread pattern created the asyncio event loop on the main thread and then ran it on a different worker thread. On Windows, that is the narrowest code
     4path consistent with the silent startup failure.
     5
     6Solution
     7
     8The fix in p2p.py:64 and test_framework.py:258 changes ownership so the worker thread creates and registers its own event loop. It also makes startup/shutdown more defensive:
     9
    10- wait until the loop exists before checking is_running()
    11- track loop readiness explicitly
    12- clean up correctly even if startup fails early
    13
    14What the reproducer demonstrates
    15
    16The standalone reproducer in networkthread_repro.py:44 does two things:
    17
    18- legacy mode reproduces the old pattern: loop created on thread A, run on thread B
    19- fixed mode reproduces the new pattern: loop created and run on the same thread
    20
    21With --assert-loop-owned-by-runner, it makes that difference deterministic:
    22
    23- legacy fails with RuntimeError: event loop created on thread X but run on thread Y
    24- fixed passes with the same command
    25
    26So the reproducer does not claim to force the original Windows interpreter crash on Linux. What it does prove is the exact behavioral difference the fix is making, and it also provides a child-
    27process stress harness that can still be run on Windows to probe the original crash more directly.
    

    agent patch with devtools reproducer:

      0diff --git a/contrib/devtools/README.md b/contrib/devtools/README.md
      1index 50e807814c..29d40aaf14 100644
      2--- a/contrib/devtools/README.md
      3+++ b/contrib/devtools/README.md
      4@@ -110,3 +110,32 @@ Example usage:
      5
      6     cd .../src
      7     ../contrib/devtools/circular-dependencies.py {*,*/*,*/*/*}.{h,cpp}
      8+
      9+networkthread_repro.py
     10+======================
     11+
     12+A standalone reproducer for the Windows-only `NetworkThread` startup issue from
     13+`test/functional/test_framework/p2p.py`. It isolates the two patterns under
     14+review:
     15+
     16+- `legacy`: create the asyncio loop on the main thread, then run it on a worker
     17+  thread
     18+- `fixed`: create and register the loop inside the worker thread
     19+
     20+The script runs each attempt in a child process so interpreter crashes or hangs
     21+do not abort the full stress run.
     22+
     23+Example usage:
     24+
     25+```bash
     26+python3 contrib/devtools/networkthread_repro.py --mode legacy --iterations 500
     27+python3 contrib/devtools/networkthread_repro.py --mode fixed --iterations 500
     28+```
     29+
     30+To make the thread-ownership bug deterministic for review, require the event
     31+loop to be created on the same thread that runs it:
     32+
     33+```bash
     34+python3 contrib/devtools/networkthread_repro.py --mode legacy --assert-loop-owned-by-runner --stop-on-failure
     35+python3 contrib/devtools/networkthread_repro.py --mode fixed --assert-loop-owned-by-runner --stop-on-failure
     36+```
     37diff --git a/contrib/devtools/networkthread_repro.py b/contrib/devtools/networkthread_repro.py
     38new file mode 100644
     39index 0000000000..229ca92aad
     40--- /dev/null
     41+++ b/contrib/devtools/networkthread_repro.py
     42@@ -0,0 +1,280 @@
     43+#!/usr/bin/env python3
     44+# Copyright (c) 2026-present The Bitcoin Core developers
     45+# Distributed under the MIT software license, see the accompanying
     46+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
     47+"""Stress the NetworkThread startup pattern from issue [#34367](/bitcoin-bitcoin/34367/).
     48+
     49+This script isolates the suspected bug pattern without requiring a build or the
     50+functional test framework:
     51+
     52+- legacy: create the asyncio event loop on the main thread, then start a worker
     53+  thread that runs it.
     54+- fixed: create and register the event loop inside the worker thread itself.
     55+
     56+Run each mode repeatedly in subprocesses. If the legacy pattern triggers an
     57+interpreter crash or hang on Windows, the parent process can still report which
     58+iteration failed and preserve the child output.
     59+"""
     60+
     61+import argparse
     62+import asyncio
     63+import faulthandler
     64+from pathlib import Path
     65+import platform
     66+import subprocess
     67+import sys
     68+import threading
     69+import time
     70+
     71+
     72+def configure_windows_policy() -> None:
     73+    if platform.system() == "Windows":
     74+        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
     75+
     76+
     77+def wait_until(predicate, *, timeout: float, check_interval: float = 0.01) -> None:
     78+    deadline = time.monotonic() + timeout
     79+    while time.monotonic() < deadline:
     80+        if predicate():
     81+            return
     82+        time.sleep(check_interval)
     83+    raise TimeoutError("predicate did not become true before timeout")
     84+
     85+
     86+class LegacyNetworkThread(threading.Thread):
     87+    def __init__(self):
     88+        super().__init__(name="LegacyNetworkThread")
     89+        configure_windows_policy()
     90+        self.loop_creator_thread_id = threading.get_ident()
     91+        self.loop_runner_thread_id = None
     92+        self.loop = asyncio.new_event_loop()
     93+
     94+    def run(self) -> None:
     95+        self.loop_runner_thread_id = threading.get_ident()
     96+        self.loop.run_forever()
     97+
     98+    def close(self, *, timeout: float) -> None:
     99+        self.loop.call_soon_threadsafe(self.loop.stop)
    100+        self.join(timeout)
    101+        if self.is_alive():
    102+            raise RuntimeError("network thread did not stop")
    103+        self.loop.close()
    104+
    105+
    106+class FixedNetworkThread(threading.Thread):
    107+    def __init__(self):
    108+        super().__init__(name="FixedNetworkThread")
    109+        self.loop = None
    110+        self.loop_ready = threading.Event()
    111+        self.loop_creator_thread_id = None
    112+        self.loop_runner_thread_id = None
    113+
    114+    def run(self) -> None:
    115+        try:
    116+            configure_windows_policy()
    117+            self.loop_runner_thread_id = threading.get_ident()
    118+            self.loop_creator_thread_id = self.loop_runner_thread_id
    119+            self.loop = asyncio.new_event_loop()
    120+            asyncio.set_event_loop(self.loop)
    121+            self.loop_ready.set()
    122+            self.loop.run_forever()
    123+        finally:
    124+            self.loop_ready.set()
    125+
    126+    def close(self, *, timeout: float) -> None:
    127+        if not self.loop_ready.wait(timeout=timeout) or self.loop is None:
    128+            self.join(timeout)
    129+            if self.is_alive():
    130+                raise RuntimeError("network thread never created its event loop")
    131+            return
    132+        self.loop.call_soon_threadsafe(self.loop.stop)
    133+        self.join(timeout)
    134+        if self.is_alive():
    135+            raise RuntimeError("network thread did not stop")
    136+        self.loop.close()
    137+
    138+
    139+def build_thread(mode: str):
    140+    if mode == "legacy":
    141+        return LegacyNetworkThread()
    142+    if mode == "fixed":
    143+        return FixedNetworkThread()
    144+    raise ValueError(f"unknown mode: {mode}")
    145+
    146+
    147+def assert_loop_owned_by_runner(thread) -> None:
    148+    if thread.loop_creator_thread_id != thread.loop_runner_thread_id:
    149+        raise RuntimeError(
    150+            "event loop created on thread "
    151+            f"{thread.loop_creator_thread_id} but run on thread {thread.loop_runner_thread_id}"
    152+        )
    153+
    154+
    155+def exercise(
    156+    mode: str,
    157+    *,
    158+    cycles: int,
    159+    startup_timeout: float,
    160+    shutdown_timeout: float,
    161+    assert_loop_owned_by_runner_mode: bool,
    162+) -> None:
    163+    for cycle in range(cycles):
    164+        thread = build_thread(mode)
    165+        thread.start()
    166+        try:
    167+            wait_until(lambda: thread.loop is not None and thread.loop.is_running(), timeout=startup_timeout)
    168+            wait_until(lambda: thread.loop_runner_thread_id is not None, timeout=startup_timeout)
    169+            if assert_loop_owned_by_runner_mode:
    170+                assert_loop_owned_by_runner(thread)
    171+            callback_ran = threading.Event()
    172+            thread.loop.call_soon_threadsafe(callback_ran.set)
    173+            wait_until(callback_ran.is_set, timeout=startup_timeout)
    174+            print(f"child cycle {cycle + 1}/{cycles}: ok", flush=True)
    175+        finally:
    176+            thread.close(timeout=shutdown_timeout)
    177+
    178+
    179+def run_child(args) -> int:
    180+    faulthandler.enable(all_threads=True)
    181+    exercise(
    182+        args.mode,
    183+        cycles=args.cycles_per_child,
    184+        startup_timeout=args.startup_timeout,
    185+        shutdown_timeout=args.shutdown_timeout,
    186+        assert_loop_owned_by_runner_mode=args.assert_loop_owned_by_runner,
    187+    )
    188+    return 0
    189+
    190+
    191+def run_parent(args) -> int:
    192+    script_path = Path(__file__).resolve()
    193+    failures = 0
    194+    for iteration in range(args.iterations):
    195+        cmd = [
    196+            sys.executable,
    197+            str(script_path),
    198+            "--child",
    199+            "--mode",
    200+            args.mode,
    201+            "--cycles-per-child",
    202+            str(args.cycles_per_child),
    203+            "--startup-timeout",
    204+            str(args.startup_timeout),
    205+            "--shutdown-timeout",
    206+            str(args.shutdown_timeout),
    207+        ]
    208+        if args.assert_loop_owned_by_runner:
    209+            cmd.append("--assert-loop-owned-by-runner")
    210+        try:
    211+            completed = subprocess.run(
    212+                cmd,
    213+                capture_output=True,
    214+                text=True,
    215+                timeout=args.child_timeout,
    216+                check=False,
    217+            )
    218+        except subprocess.TimeoutExpired as exc:
    219+            failures += 1
    220+            print(f"iteration {iteration + 1}/{args.iterations}: timeout after {args.child_timeout}s", flush=True)
    221+            if exc.stdout:
    222+                print(exc.stdout, end="")
    223+            if exc.stderr:
    224+                print(exc.stderr, end="", file=sys.stderr)
    225+            if args.stop_on_failure:
    226+                return 1
    227+            continue
    228+
    229+        if completed.returncode == 0:
    230+            print(f"iteration {iteration + 1}/{args.iterations}: ok", flush=True)
    231+            continue
    232+
    233+        failures += 1
    234+        print(
    235+            f"iteration {iteration + 1}/{args.iterations}: child exited with return code {completed.returncode}",
    236+            flush=True,
    237+        )
    238+        if completed.stdout:
    239+            print(completed.stdout, end="")
    240+        if completed.stderr:
    241+            print(completed.stderr, end="", file=sys.stderr)
    242+        if args.stop_on_failure:
    243+            return 1
    244+
    245+    if failures:
    246+        print(f"{failures} child process failures observed in {args.iterations} iterations", flush=True)
    247+        return 1
    248+
    249+    print(
    250+        f"completed {args.iterations} iterations in mode={args.mode} with {args.cycles_per_child} cycle(s) per child",
    251+        flush=True,
    252+    )
    253+    return 0
    254+
    255+
    256+def parse_args():
    257+    parser = argparse.ArgumentParser(
    258+        description="Stress the legacy and fixed NetworkThread startup patterns from issue [#34367](/bitcoin-bitcoin/34367/).",
    259+    )
    260+    parser.add_argument(
    261+        "--mode",
    262+        choices=["legacy", "fixed"],
    263+        default="legacy",
    264+        help="which startup pattern to exercise",
    265+    )
    266+    parser.add_argument(
    267+        "--iterations",
    268+        type=int,
    269+        default=100,
    270+        help="number of child processes to run in parent mode",
    271+    )
    272+    parser.add_argument(
    273+        "--cycles-per-child",
    274+        type=int,
    275+        default=1,
    276+        help="number of thread start/stop cycles to run inside each child process",
    277+    )
    278+    parser.add_argument(
    279+        "--startup-timeout",
    280+        type=float,
    281+        default=5.0,
    282+        help="seconds to wait for the event loop to start",
    283+    )
    284+    parser.add_argument(
    285+        "--shutdown-timeout",
    286+        type=float,
    287+        default=5.0,
    288+        help="seconds to wait for the worker thread to stop",
    289+    )
    290+    parser.add_argument(
    291+        "--child-timeout",
    292+        type=float,
    293+        default=15.0,
    294+        help="seconds to allow a child process before the parent reports a timeout",
    295+    )
    296+    parser.add_argument(
    297+        "--stop-on-failure",
    298+        action="store_true",
    299+        help="stop the parent loop after the first failed child process",
    300+    )
    301+    parser.add_argument(
    302+        "--assert-loop-owned-by-runner",
    303+        action="store_true",
    304+        help="fail if the event loop is created on a different thread than the one that runs it",
    305+    )
    306+    parser.add_argument(
    307+        "--child",
    308+        action="store_true",
    309+        help=argparse.SUPPRESS,
    310+    )
    311+    return parser.parse_args()
    312+
    313+
    314+def main() -> int:
    315+    args = parse_args()
    316+    if args.child:
    317+        return run_child(args)
    318+    return run_parent(args)
    319+
    320+
    321+if __name__ == "__main__":
    322+    sys.exit(main())
    323diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py
    324index 7a7cce6176..2468b4acf6 100755
    325--- a/test/functional/test_framework/p2p.py
    326+++ b/test/functional/test_framework/p2p.py
    327@@ -740,16 +740,28 @@ class NetworkThread(threading.Thread):
    328
    329         NetworkThread.listeners = {}
    330         NetworkThread.protos = {}
    331-        if platform.system() == 'Windows':
    332-            asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    333-        NetworkThread.network_event_loop = asyncio.new_event_loop()
    334+        self._loop_ready = threading.Event()
    335
    336     def run(self):
    337         """Start the network thread."""
    338-        self.network_event_loop.run_forever()
    339+        try:
    340+            if platform.system() == 'Windows':
    341+                asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    342+            # Create and register the loop on the worker thread instead of
    343+            # constructing it on the main thread and handing it across.
    344+            NetworkThread.network_event_loop = asyncio.new_event_loop()
    345+            asyncio.set_event_loop(self.network_event_loop)
    346+            self._loop_ready.set()
    347+            self.network_event_loop.run_forever()
    348+        finally:
    349+            self._loop_ready.set()
    350
    351     def close(self, *, timeout):
    352         """Close the connections and network event loop."""
    353+        if not self._loop_ready.wait(timeout=timeout) or self.network_event_loop is None:
    354+            self.join(timeout)
    355+            NetworkThread.network_event_loop = None
    356+            return
    357         self.network_event_loop.call_soon_threadsafe(self.network_event_loop.stop)
    358         wait_until_helper_internal(lambda: not self.network_event_loop.is_running(), timeout=timeout)
    359         self.network_event_loop.close()
    360diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
    361index 1f95756445..f968d6cc56 100755
    362--- a/test/functional/test_framework/test_framework.py
    363+++ b/test/functional/test_framework/test_framework.py
    364@@ -258,7 +258,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
    365         self.log.debug('Setting up network thread')
    366         self.network_thread = NetworkThread()
    367         self.network_thread.start()
    368-        self.wait_until(lambda: self.network_thread.network_event_loop.is_running())
    369+        self.wait_until(lambda: self.network_thread.network_event_loop is not None and self.network_thread.network_event_loop.is_running())
    370
    371         if self.options.usecli:
    372             if not self.supports_cli:
    

    I didn’t spend enough time with it to understand it deeply, hope it’s not a complete waste of time, the explanation does sound reasonable.

  12. maflcko referenced this in commit fab0b5d17e on Mar 13, 2026
  13. maflcko commented at 9:32 am on March 13, 2026: member

    Yeah, I don’t understand thread-safety of Windows sockets either. However, it seems a good shot and harmless try. Draft commit:

     0commit fab0b5d17ea6f5e2357c41fe647cfbc1f8d4af21 (HEAD -> 2603-test-windows-network-thread-socket-safe, origin/2603-test-windows-network-thread-socket-safe)
     1Author: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>
     2Date:   Fri Mar 13 10:03:49 2026 +0100
     3
     4    test: Move event loop creation to network thread
     5    
     6    This should fix [#34367](/bitcoin-bitcoin/34367/)
     7    
     8    I am not familiar with Windows sockets thread-safety, but creating the event loop on the main thread, but running it in the network thread, could lead to a Windows fast abort (without any stderr):
     9    
    10    ```
    11    77/276 - wallet_txn_clone.py failed, Duration: 1 s
    12    
    13    stdout:
    14    2025-12-10T08:04:27.500134Z TestFramework (INFO): PRNG seed is: 4018092284830106117
    15    
    16    stderr:
    17    
    18    Combine the logs and print the last 99999999 lines ...
    19    ============
    20    Combined log for D:\a\_temp/test_runner_₿_🏃_20251210_075632/wallet_txn_clone_196:
    21    ============
    22     test  2025-12-10T08:04:27.500134Z TestFramework (INFO): PRNG seed is: 4018092284830106117
    23     test  2025-12-10T08:04:27.500433Z TestFramework (DEBUG): Setting up network thread
    24    ```
    25    
    26    Also, I couldn't find any docs that require the loop must be created on the thread that runs them:
    27    
    28    * https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.new_event_loop
    29    * https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_forever
    30    
    31    However, the patch seems trivial to review, harmless, and easy to revert, so it may be a good try to fix the intermittent Windows Python crash.
    32
    33diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py
    34index 7a7cce6176..68118af20b 100755
    35--- a/test/functional/test_framework/p2p.py
    36+++ b/test/functional/test_framework/p2p.py
    37@@ -740,12 +740,12 @@ class NetworkThread(threading.Thread):
    38 
    39         NetworkThread.listeners = {}
    40         NetworkThread.protos = {}
    41-        if platform.system() == 'Windows':
    42-            asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    43-        NetworkThread.network_event_loop = asyncio.new_event_loop()
    44 
    45     def run(self):
    46         """Start the network thread."""
    47+        if platform.system() == 'Windows':
    48+            asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    49+        NetworkThread.network_event_loop = asyncio.new_event_loop()
    50         self.network_event_loop.run_forever()
    51 
    52     def close(self, *, timeout):
    53diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
    54index fb0fc0af6b..95bfc964d6 100755
    55--- a/test/functional/test_framework/test_framework.py
    56+++ b/test/functional/test_framework/test_framework.py
    57@@ -260,7 +260,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
    58         self.log.debug('Setting up network thread')
    59         self.network_thread = NetworkThread()
    60         self.network_thread.start()
    61-        self.wait_until(lambda: self.network_thread.network_event_loop.is_running())
    62+        self.wait_until(lambda: self.network_thread.network_event_loop is not None and self.network_thread.network_event_loop.is_running())
    63 
    64         if self.options.usecli:
    65             if not self.supports_cli:
    
  14. DrahtBot added this to the milestone 33.0 on Mar 13, 2026
  15. maflcko commented at 11:18 am on March 13, 2026: member

    I guess there is no rush here, as the issue only happens once every 3 months.

    We can try #34820 and see if anything changes.

    If it happens again, I’ll probably submit my above patch. If it disappears, I guess we can close this for 33.x (added a milestone, so it isn’t forgotten).

  16. maflcko referenced this in commit fa050da980 on Mar 13, 2026
  17. maflcko removed this from the milestone 33.0 on Mar 13, 2026
  18. sedited closed this on Mar 18, 2026

  19. fanquake referenced this in commit 72d6c88165 on Mar 19, 2026
  20. fanquake reopened this on Mar 26, 2026

  21. fanquake commented at 11:10 am on March 26, 2026: member
  22. maflcko commented at 11:20 am on March 26, 2026: member

    It could be related, but it is different, because it does not mute before the network thread creation in Python, but after the msghand thread creation in bitcoind:

     0 node1 2026-03-26T02:50:47.892243Z [init] [noui.cpp:56] [noui_InitMessage] init message: Verifying blocks 
     1 node1 2026-03-26T02:50:47.892263Z [init] [validation.cpp:4628] [VerifyDB] Verifying last 6 blocks at level 3 
     2 node1 2026-03-26T02:50:47.892293Z [init] [validation.cpp:4637] [VerifyDB] Verification progress: 0% 
     3 node1 2026-03-26T02:50:47.892706Z [init] [validation.cpp:4645] [VerifyDB] Verification progress: 16% 
     4 node1 2026-03-26T02:50:47.893030Z [init] [validation.cpp:4645] [VerifyDB] Verification progress: 33% 
     5 node1 2026-03-26T02:50:47.893338Z [init] [validation.cpp:4645] [VerifyDB] Verification progress: 50% 
     6 node1 2026-03-26T02:50:47.893698Z [init] [validation.cpp:4645] [VerifyDB] Verification progress: 66% 
     7 node1 2026-03-26T02:50:47.894033Z [init] [validation.cpp:4645] [VerifyDB] Verification progress: 83% 
     8 node1 2026-03-26T02:50:47.894362Z [init] [validation.cpp:4645] [VerifyDB] Verification progress: 99% 
     9 node1 2026-03-26T02:50:47.894384Z [init] [validation.cpp:4739] [VerifyDB] Verification: No coin database inconsistencies in last 6 blocks (6 transactions) 
    10 node1 2026-03-26T02:50:47.894425Z [init] [init.cpp:1414] [InitAndLoadChainstate] Block index and chainstate loaded 
    11 node1 2026-03-26T02:50:47.894490Z [init] [init.cpp:1949] [AppInitMain] Setting NODE_NETWORK in non-prune mode 
    12 node1 2026-03-26T02:50:47.894701Z [init] [init.cpp:2080] [AppInitMain] block tree size = 200 
    13 node1 2026-03-26T02:50:47.894725Z [init] [init.cpp:2093] [AppInitMain] nBestHeight = 199 
    14 node1 2026-03-26T02:50:47.894858Z [init] [net.cpp:3332] [BindListenPort] Bound to 127.0.0.1:13389 
    15 node1 2026-03-26T02:50:47.894903Z [init] [noui.cpp:56] [noui_InitMessage] init message: Starting network threads 
    16 node1 2026-03-26T02:50:47.895016Z [init] [net.cpp:3528] [Start] DNS seeding disabled 
    17 node1 2026-03-26T02:50:47.895107Z [initload] [util/thread.cpp:20] [TraceThread] initload thread start 
    18 node1 2026-03-26T02:50:47.895180Z [initload] [node/mempool_persist.cpp:49] [LoadMempool] Failed to open mempool file. Continuing anyway. 
    19 node1 2026-03-26T02:50:47.895201Z [initload] [util/thread.cpp:22] [TraceThread] initload thread exit 
    20 node1 2026-03-26T02:50:47.895340Z [net] [util/thread.cpp:20] [TraceThread] net thread start 
    21 node1 2026-03-26T02:50:47.895406Z [addcon] [util/thread.cpp:20] [TraceThread] addcon thread start 
    22 node1 2026-03-26T02:50:47.895575Z [init] [noui.cpp:56] [noui_InitMessage] init message: Done loading 
    23 node1 2026-03-26T02:50:47.895720Z [msghand] [util/thread.cpp:20] [TraceThread] msghand thread start 
    24
    25(mute)
    

    Could make sense to open a new issue. Also it was on msvcrt, not on ucrt.

  23. fanquake commented at 12:33 pm on March 26, 2026: member
    Moved to #34925.
  24. fanquake closed this on Mar 26, 2026


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-03-30 00:13 UTC

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