ThreadPrivateBroadcast keeps opening connections until all planned sends for a privately broadcast transaction are done (since #36277 also after the transaction has been received back). If every address addrman returns for the picked network is a peer we already have a connection to, OpenNetworkConnection() rejects it immediately and the loop retries every 100 ms, logging Failed to connect to ... will retry to a different address each time. On a node that only knows a few onion/I2P peers this spins for as long as the sends remain pending, which can be indefinitely.
I ran into this while testing v32.0rc4 (https://github.com/bitcoin/bitcoin/issues/36315#issuecomment-6235074058, observation 2): a regtest sender reachable only through Tor with a single .onion address in addrman logged ~600 Failed to connect lines per minute for as long as I watched, with no SOCKS traffic, after the transaction had already been delivered and received back.
This PR checks the conditions that make OpenNetworkConnection() refuse an address (already connected, banned or discouraged) before attempting the connection, and treats such picks like invalid ones: after 100 unusable picks in a row the thread waits 500 ms between picks and logs about it once per 100 picks instead of on every attempt. Behaviour for usable addresses is unchanged.
Same scenario with this change (master, two isolated regtest nodes over the real Tor network): the transaction was delivered in 16 s, no Failed to connect lines, and one Connections needed but addrman keeps returning unusable addresses, will keep retrying line every ~50 s while the remaining sends stay pending.
Tested with p2p_private_broadcast.py, p2p_private_broadcast_cap.py, p2p_private_broadcast_retry_v1.py, p2p_private_broadcast_completion.py and feature_proxy.py.
The rejection conditions are now checked both here and in OpenNetworkConnection(). Having OpenNetworkConnection() report whether it rejected the address up front or the attempt failed would avoid that duplication, but seemed out of scope for this fix.
Disclosure: AI (Claude Fable 5.1) was used to find this issue and to draft this PR.