[LLM] Prevent retained fallback permits from deadlocking addnode reconnections #36622

issue instagibbs opened this issue on October 23, 2026
  1. instagibbs commented at 1:32 PM on October 23, 2026: member

    LLM report LUP-30432 verbatim:

    ReconnectionInfo retains a live CountingSemaphoreGrant while waiting in m_reconnections. DisconnectNodes() moves a disconnected V2 peer's grant into this queue when ShouldReconnectV1() is true (src/net.cpp:1976-1984), so removing the connection does not free its slot. ThreadOpenAddedConnections() then blocks acquiring semAddnode at the top of its loop (net.cpp:3075), before reaching PerformReconnections() (3096). When all eight manual permits are held by pending fallbacks, the worker cannot process the queue that owns the permits it needs.

    A concrete affected deployment uses -connect=0 and eight configured -addnode endpoints. Those endpoints, or an interceptor causing equivalent handshake failures, can accept the V2 connections, receive at least 24 bytes, send nothing, and close after the worker's initial connection pass. All grants remain queued. Once the 60-second retry sleep ends, the only reconnection worker blocks indefinitely; -connect=0 prevents ThreadOpenConnections from being started. The node remains disconnected even after the endpoints become healthy, and replacing the configured addnodes cannot free the retained grants. Restarting clears the state. This requires influence over selected outbound connections; arbitrary inbound peers alone cannot trigger it. Other configurations may recover through another worker or a released permit.

    The regression extends the existing functional test with eight local listeners, requires actual V1 retries, and times out on HEAD's blocked accept. Python syntax was checked and validate_poc accepted the diff; execution was unavailable because no built bitcoind exists in the worktree. Searches for reconnections/semaphore, ThreadOpenAddedConnections, and fallback/deadlock found no matching prior report.

    PoC:

    diff --git a/test/functional/p2p_v2_transport.py b/test/functional/p2p_v2_transport.py
    --- a/test/functional/p2p_v2_transport.py
    +++ b/test/functional/p2p_v2_transport.py
    @@ -5,7 +5,9 @@
     """
     Test v2 transport
     """
    +from contextlib import ExitStack
     import socket
    +import time
     
     from test_framework.messages import MAGIC_BYTES, NODE_P2P_V2
     from test_framework.test_framework import BitcoinTestFramework
    @@ -168,5 +170,62 @@
                     self.wait_until(lambda: peer_id not in [p["id"] for p in self.nodes[0].getpeerinfo()])
     
     
    +        self.test_addnode_fallback_at_capacity()
    +
    +    def test_addnode_fallback_at_capacity(self):
    +        self.log.info("V1 fallback must progress when all addnode permits are held")
    +        with ExitStack() as stack:
    +            listeners = []
    +            addnodes = []
    +            for _ in range(8):
    +                listener = stack.enter_context(socket.socket(socket.AF_INET, socket.SOCK_STREAM))
    +                listener.bind(("127.0.0.1", 0))
    +                listener.listen(2)
    +                listener.settimeout(20 * self.options.timeout_factor)
    +                listeners.append(listener)
    +                addnodes.append(f"-addnode=127.0.0.1:{listener.getsockname()[1]}")
    +
    +            # Disable the other reconnection worker and keep handshakes alive
    +            # until all eight manual connection permits have been consumed.
    +            self.restart_node(0, extra_args=[
    +                "-v2transport=1", "-connect=0", "-peertimeout=9999",
    +                *addnodes,
    +            ])
    +            initial = []
    +            for listener in listeners:
    +                peer = stack.enter_context(listener.accept()[0])
    +                peer.settimeout(20 * self.options.timeout_factor)
    +                initial.append(peer)
    +                received = b""
    +                while len(received) < 24:
    +                    chunk = peer.recv(24 - len(received))
    +                    assert chunk, "V2 initiator closed before sending its handshake"
    +                    received += chunk
    +
    +            assert_equal(len(self.nodes[0].getpeerinfo()), 8)
    +            # The worker waits 500ms after its last initial connect, processes
    +            # pending fallbacks, then sleeps 60s. Close only after that first
    +            # pass, so our fallbacks are queued during the retry sleep.
    +            time.sleep(2 * self.options.timeout_factor)
    +            for peer in initial:
    +                # Send no bytes: this makes ShouldReconnectV1() return true.
    +                peer.close()
    +
    +            expected_prefix = MAGIC_BYTES["regtest"] + b"version" + bytes(5)
    +            for listener in listeners:
    +                listener.settimeout(75 * self.options.timeout_factor)
    +                # HEAD times out here: every permit is retained in
    +                # m_reconnections, and the only worker waits for a new permit
    +                # before it can process that queue. A fix must resume fallback.
    +                retry = stack.enter_context(listener.accept()[0])
    +                retry.settimeout(20 * self.options.timeout_factor)
    +                prefix = b""
    +                while len(prefix) < len(expected_prefix):
    +                    chunk = retry.recv(len(expected_prefix) - len(prefix))
    +                    assert chunk, "Fallback connection closed before VERSION"
    +                    prefix += chunk
    +                assert_equal(prefix, expected_prefix)
    +
    +
     if __name__ == '__main__':
         V2TransportTest(__file__).main()
    
  2. mzumsande commented at 2:35 PM on October 26, 2026: contributor

    LLM report LUP-30432 verbatim:

    https://github.com/bitcoin/bitcoin/blob/master/doc/AI_POLICY.md

    If you are opening an issue, you should be able to describe the problem in your own words.

    We should change the AI policy if we want to accept public issues copy-pasted from LLMs. Doesn't seem fair to me if it is only applied to new contributors.

  3. instagibbs commented at 2:41 PM on October 26, 2026: member

    I can pretend it's not from LLM but I spent time verifying the claim, and do not think this report should get lost. I think it's a bit silly to require a rewrite just because.

  4. maflcko commented at 3:13 PM on October 26, 2026: member

    I wonder what the goal here is? I presume the same thread/topic is also tracked in private repos such as https://github.com/orgs/bitcoin-security-council/repositories . If the goal is to have a public list of LLM generated issues, that seems fine, but it would be good to tag them with a label like LLM finding, no external pulls accepted (or so). Otherwise, there could be a risk that random LLM bots try to send pulls with "fixes"?

    Again, not against this. Just wondering about the larger picture and outcome here.

  5. instagibbs commented at 3:16 PM on October 26, 2026: member

    Otherwise, there could be a risk that random LLM bots try to send pulls with "fixes"?

    <img width="1080" height="632" alt="Image" src="https://github.com/user-attachments/assets/80cefcdb-6e20-4fe0-803e-c99298f1d9bb" />

  6. sedited commented at 3:27 PM on October 26, 2026: contributor

    I think it's a bit silly to require a rewrite just because.

    I don't think it is. To me it seems less efficient to make everybody parse the report from LLM-speak to english instead of doing it once as the reporter / author of the issue.

  7. instagibbs commented at 3:30 PM on October 26, 2026: member

    I don't care enough for this one. Nevermind.

  8. instagibbs closed this on Oct 26, 2026

  9. maflcko commented at 7:03 AM on October 28, 2026: member

    5. > Otherwise, there could be a risk that random LLM bots try to send pulls with "fixes"? <img alt="Image" width="1080" height="632" src="https://private-user-images.githubusercontent.com/5767891/684979169-80cefcdb-6e20-4fe0-803e-c99298f1d9bb.png?jwt=">

    Heh, I understand that modern LLMs are capable, and that some projects are converging toward "issues-are-the-new-prs". Again, no objection if we want that as well, but I think issues would still need to be high quality and human-curated.

    I don't think we want a full LLM-issues + LLM-pulls workflow. Also, each LLM-pull needs a reviewer/mentor for remotely prompting the LLM (if needed). So to me it seems easier if the reviewer/mentor just used their own LLM to create the pull. Alternatively, there could be a projected-curated LLM that accepts prompts from contributors. However fanning stuff out to random OpenClaw instances often eats reviewer/mentor capacity without resulting in a merge anyway. At least that is my impression. Recall that a good chunk of Monday morning this week was spent by several reviewers to close 10+ irrelevant LLM-pulls.

  10. instagibbs commented at 12:52 PM on October 28, 2026: member

    @maflcko my point wasn't it's good or bad that they do it, but that me closing this issue has 0 effect on it. They're going to open hundreds of issues and PRs, no matter what our policy is.

    edit: let's be consistent, or drop the charage, in other words

  11. kanzure commented at 12:57 PM on October 28, 2026: contributor

    I think "LLM that accepts prompts from contributors" is a good idea compared to the alternative where drive-by users submit text already generated by LLMs. It would be even better if drive-by users could contribute credits for LLMs instead of contributing token outputs. Something odd about this thread, though, is that Mr. Sanders is not a drive-by contributor...

    (While I'm ranting about this, I think the framing that makes sense here is that these and other open-source software projects are human projects with human collaborators. People can use AI tools, but they shouldn't expect these LLM tools to be first-class collaborators with the humans. Nor does using these tools make you an active collaborator. The only way to become a collaborator is to actually collaborate with at least one other person on the project. Collaboration isn't just posting an issue or a pull request, but actually getting involved in the project and understanding what's going on and how people are working together. You have to actually work with at least one other human being in the project, unless your goal was just to post text out of sense of vanity. People can collaborate with any person they want, but there's no way to force them into a relationship with an AI tool that they don't want to work with.)


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-10-30 09:40 UTC