torcontrol: Remove libevent usage #34158

pull fjahr wants to merge 10 commits into bitcoin:master from fjahr:2025-12-torcontrol-take-3 changing 8 files +552 −314
  1. fjahr commented at 2:20 PM on December 27, 2025: contributor

    This is part of the effort to remove the libevent dependency from our code base: #31194

    The current approach tries to reuse existing code and follows roughly similar design decisions. It replaces the libevent-based async I/O with blocking I/O utilizing the existing Sock and CThreadInterrupt. The controller runs in a dedicated thread.

    There are some optional code modernizations thrown in made along the way (namings, constexpr etc.). These are not strictly necessary but make the end result with the new code more consistent.

  2. DrahtBot commented at 2:20 PM on December 27, 2025: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34158.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK janb84, pinheadmz, achow101
    Concept ACK pablomartin4btc, waketraindev, w0xlt
    Stale ACK sedited

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #31260 (scripted-diff: Type-safe settings retrieval by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • “Now that we know Tor is running setup the proxy for onion addresses” -> “Now that we know Tor is running set up the proxy for onion addresses” [grammar is missing “set” (“setup” reads as a typo), which slightly harms comprehension]

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • m_sock->Recv(buf, sizeof(buf), MSG_DONTWAIT) in src/torcontrol.cpp
    • self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) in test/functional/feature_torcontrol.py

    <sup>2026-04-03 20:10:39</sup>

  3. sedited commented at 2:35 PM on December 27, 2025: contributor

    Concept ACK

  4. pinheadmz commented at 2:38 PM on December 27, 2025: member

    concept ACK Quick review on github - where are you using LineReader?

  5. fjahr force-pushed on Dec 27, 2025
  6. DrahtBot added the label CI failed on Dec 27, 2025
  7. DrahtBot commented at 3:32 PM on December 27, 2025: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task macOS-cross to arm64: https://github.com/bitcoin/bitcoin/actions/runs/20540187798/job/59003472023</sub> <sub>LLM reason (✨ experimental): Compilation error in fuzz tests: undefined type ConnectionCB and invalid TorControlConnection initialization (nullptr) in torcontrol.cpp.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  8. fjahr commented at 3:35 PM on December 27, 2025: contributor

    where are you using LineReader?

    Heh, I did quite a bit of moving around after I got things working and seems like I picked from the branch where i didn't use it instead of the one where I did at the end. Pushed the right code for it now, using it in ProcessBuffer.

    Will work on fixing the fuzz test which I completely missed 🙃 That's what's failing the CI.

  9. pablomartin4btc commented at 4:24 PM on December 27, 2025: member

    Concept ACK

  10. fjahr force-pushed on Dec 28, 2025
  11. DrahtBot removed the label CI failed on Dec 28, 2025
  12. waketraindev commented at 3:33 PM on December 28, 2025: contributor

    Concept ACK

    There's some uncovered new code in the coverage report, https://corecheck.dev/bitcoin/bitcoin/pulls/34158 And some Sonarcloud warnings to check out.

  13. in src/torcontrol.cpp:137 in 872feec3eb
     146 | -    if (b_conn)
     147 | -        bufferevent_free(b_conn);
     148 | -    b_conn = nullptr;
     149 | +    if (m_sock) {
     150 | +        m_sock.reset();
     151 | +    }
    


    janb84 commented at 4:08 PM on December 28, 2025:
            m_sock.reset();
    

    NIT: I think you can remove the redundant check. calling reset() on a std::unique_ptr is safe even if the pointer is already null


    fjahr commented at 9:57 PM on December 28, 2025:

    done

  14. janb84 commented at 4:19 PM on December 28, 2025: contributor

    Concept ACK 8d6f1b02f5868d918bb72059eac3afb881ccafe3

    PR removes LibEvent usages from TorControl and cleans/modernizes the code a bit where touched.

    code review, build and test. I do not have sufficient exp. to say something about the fuzz improvement commit. Small NIT found while doing the code review.

  15. fjahr force-pushed on Dec 28, 2025
  16. fjahr force-pushed on Dec 28, 2025
  17. DrahtBot added the label CI failed on Dec 28, 2025
  18. DrahtBot commented at 10:02 PM on December 28, 2025: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/20560088214/job/59049502835</sub> <sub>LLM reason (✨ experimental): Python linting failed: an f-string had no placeholders (ruff warning F541) in test/functional/feature_torcontrol.py.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  19. fjahr commented at 10:16 PM on December 28, 2025: contributor

    Addressed feedback.

    There's some uncovered new code in the coverage report, https://corecheck.dev/bitcoin/bitcoin/pulls/34158

    The old code was largely uncovered as well but I have looked into it and added a simple functional test that provides some basic end-to-end coverage. I thought about unit tests too but I am not sure they provide much value aside from the functional and fuzz tests. But happy to be convinced otherwise if there are ideas for what they should cover.

    And some Sonarcloud warnings to check out.

    I don't know what that is and the screenshot doesn't allow me to see which lines are meant by the comment. Happy to look into them if you can transfer the comments matching to the correct lines here somehow.

  20. waketraindev commented at 11:14 PM on December 28, 2025: contributor

    I don't know what that is and the screenshot doesn't allow me to see which lines are meant by the comment. Happy to look into them if you can transfer the comments matching to the correct lines here somehow.

    You can see the warnings on https://corecheck.dev/bitcoin/bitcoin/pulls/34158 scroll down the page, above benchmarks, below uncovered included code

  21. DrahtBot removed the label CI failed on Dec 28, 2025
  22. fjahr force-pushed on Dec 31, 2025
  23. fjahr commented at 5:06 PM on December 31, 2025: contributor

    You can see the warnings on https://corecheck.dev/bitcoin/bitcoin/pulls/34158 scroll down the page, above benchmarks, below uncovered included code

    TIL, thanks. I addressed the ones that made sense to me.

  24. DrahtBot added the label CI failed on Dec 31, 2025
  25. DrahtBot commented at 6:18 PM on December 31, 2025: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task macOS-cross to arm64: https://github.com/bitcoin/bitcoin/actions/runs/20623563561/job/59230100443</sub> <sub>LLM reason (✨ experimental): Compilation failed due to use of std::jthread not available in the target standard library (missing C++20 jthread support).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  26. fjahr force-pushed on Dec 31, 2025
  27. DrahtBot removed the label CI failed on Dec 31, 2025
  28. in src/torcontrol.cpp:504 in 3a1f8c0947 outdated
     500 | @@ -501,12 +501,12 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply&
     501 |   *                  CookieString | ClientNonce | ServerNonce)
     502 |   *
     503 |   */
     504 | -static std::vector<uint8_t> ComputeResponse(const std::string &key, const std::vector<uint8_t> &cookie,  const std::vector<uint8_t> &clientNonce, const std::vector<uint8_t> &serverNonce)
     505 | +static std::vector<uint8_t> ComputeResponse(const std::string &key, const std::vector<uint8_t> &cookie,  const std::vector<uint8_t> &client_nonce, const std::vector<uint8_t> &serverNonce)
    


    pinheadmz commented at 4:16 PM on January 14, 2026:

    3a1f8c094798d09f41cd79c71c34afe2ea967394

    why change client_nonce but not serverNonce? I'm guessing because there is a m_client_nonce at the call site, but the member property isn't being explicitly handled here.


    fjahr commented at 2:39 PM on January 17, 2026:

    Yeah, I was initially focussing on the member variables in order to not let this get out of hand but the param here was touched too by accident. Doesn't seem too bad to change the serverNonce too as well as the other local variables in this function, so taking care of all of them now.

  29. in src/torcontrol.cpp:194 in c675b6f059 outdated
     210 | +bool TorControlConnection::ProcessBuffer()
     211 | +{
     212 | +    if (m_recv_buffer.size() > MAX_LINE_LENGTH) {
     213 | +        LogWarning("tor: Disconnecting because MAX_LINE_LENGTH exceeded");
     214 | +        return false;
     215 | +    }
    


    pinheadmz commented at 7:45 PM on January 15, 2026:

    c675b6f059dea662d9faa90df261de7bfbca13a1

    We won't ever get multiple lines in the receive buffer?


    fjahr commented at 2:37 PM on January 17, 2026:

    Right, that's a good catch. If the processing is too slow this could fail unnecessarily and LineReader should catch this already fine, so I am removing this.

  30. in src/torcontrol.cpp:200 in c675b6f059 outdated
     216 | +
     217 | +    // TODO: Maybe could give this as an option to LineReader instead
     218 | +    auto last_newline = std::find(m_recv_buffer.rbegin(), m_recv_buffer.rend(), std::byte{'\n'});
     219 | +    if (last_newline == m_recv_buffer.rend()) return true;
     220 | +    size_t complete = last_newline.base() - m_recv_buffer.begin();
     221 | +    util::LineReader reader({m_recv_buffer.data(), complete}, MAX_LINE_LENGTH);
    


    pinheadmz commented at 7:51 PM on January 15, 2026:

    c675b6f059dea662d9faa90df261de7bfbca13a1

    I'm still learning how torcontrol protocol works all together, but without complete context it seems to me that all this could be simplified:

    util::LineReader reader(m_recv_buffer, MAX_LINE_LENGTH);
    

    You shouldn't have to find '\n', that's LineReader's job ...?

    torcontrol unit and functional tests still pass like this.


    fjahr commented at 11:17 PM on January 16, 2026:

    The idea here is that we may receive an incomplete line in the stream just because of packets arriving that way and if we would ask the LineReader for it we would receive it but couldn't do anything with it. That's why I am looking for the last \n and then only process the buffer up until that point. But this is something that could be done by LineReader, too. So maybe there could be a bool param and then LineReader would only return the lines that end with \n but I didn't really spend time making up my mind if that is so much better. It looks cleaner here but this functionality would only have one user, so I don't know... Either way I will wait until LineReader is merged and then I might try to add the functionality there if you think it's better that way.

    There should be a test for this though, currently the tests all send the data nice and complete.


    pinheadmz commented at 2:50 PM on January 18, 2026:

    Oh right, of course, incomplete lines. I think this applies to HTTP as well, and should probably be a change in #34242 like, if LineReader reaches the end of the buffer before finding a \n it should return null. Right now it treats end of buffer the same as end of line, but I think thats not actually what we want.


    fjahr commented at 1:23 PM on January 19, 2026:

    I only glossed over the usage of LineReader in the rest of the http PR so far but yeah, that makes sense to me. And that would make my code here simpler :)


    pinheadmz commented at 6:03 PM on January 20, 2026:

    I'm about to push an update to #34242 that solves this. The patch below fails the partial-send test in feature_torcontrol.py with your current LineReader but passes when I rebase on the updated version:

    diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp
    index 94f925b8f4..bae7ac44a5 100644
    --- a/src/torcontrol.cpp
    +++ b/src/torcontrol.cpp
    @@ -188,12 +188,8 @@ bool TorControlConnection::ReceiveAndProcess()
     
     bool TorControlConnection::ProcessBuffer()
     {
    -    // TODO: Maybe could give this as an option to LineReader instead
    -    auto last_newline = std::find(m_recv_buffer.rbegin(), m_recv_buffer.rend(), std::byte{'\n'});
    -    if (last_newline == m_recv_buffer.rend()) return true;
    -    size_t complete = last_newline.base() - m_recv_buffer.begin();
    -    util::LineReader reader({m_recv_buffer.data(), complete}, MAX_LINE_LENGTH);
    -
    +    util::LineReader reader(m_recv_buffer, MAX_LINE_LENGTH);
    +    auto start = reader.it;
         while (auto line = reader.ReadLine()) {
             // Skip short lines
             if (line->size() < 4) continue;
    @@ -220,7 +216,7 @@ bool TorControlConnection::ProcessBuffer()
             }
         }
     
    -    m_recv_buffer.erase(m_recv_buffer.begin(), m_recv_buffer.begin() + complete);
    +    m_recv_buffer.erase(m_recv_buffer.begin(), m_recv_buffer.begin() + std::distance(start, reader.it));
         return true;
     }
     
    

    fjahr commented at 1:32 PM on January 21, 2026:

    Cool, thank you! I will wait until the latest conversations have been resolved before rebasing, hoping everything in #34242 is clear and it can be merged soon :)


    fjahr commented at 9:23 PM on January 23, 2026:

    Taken now!

  31. pinheadmz approved
  32. pinheadmz commented at 8:24 PM on January 15, 2026: member

    ACK 3de90a1159e606585aad32c876b6769d29f46ac1

    Reviewed each commit, built and ran tests on arm64/macos.

    Ran on mainnet with -onlynet=onion and observed tor connections in and out. Monitored bitcoind logs with -debug=tor and tor daemon logs for errors.

    I also used wireshark to capture torcontrol packets on port 9051 to compare this branch against release 30.2 and observed identical behavior 👍

    Checked the exponential retry backoff by killing the tor daemon process while bitcoind was running. Restarted tor after a few minutes and bitcoind continued as expected.

    This is a very nice simplification of torcontrol and also demonstrates how we can replace libevent in bitcoin-cli as well ;-).

    <details><summary>Show Signature</summary>

    -----BEGIN PGP SIGNED MESSAGE-----
    Hash: SHA256
    
    ACK 3de90a1159e606585aad32c876b6769d29f46ac1
    -----BEGIN PGP SIGNATURE-----
    
    iQIzBAEBCAAdFiEE5hdzzW4BBA4vG9eM5+KYS2KJyToFAmlpS64ACgkQ5+KYS2KJ
    yTrIVhAAg03Hk1Hu/lEhqpcr1M1aTaxsyNUha2MOtj8L9JMkxYaylX2LlHDzDACq
    g23L43GAECJtaMfSWH500Id9JqtWiNhn2tUhXLi9UEVDsbFsVf0usxDyF3kx2fec
    CalOFldTrIV8gFOhmS650lyxt0HfyePQ8yTZBH9jeXLrWoyjOU77cjYGzPC3/wUW
    gsVqLFFY5FOVzB789abu0ufsv3J6LrwAqwwxhsJpJ7IKhfrR0orX5IOMD4r0JMD6
    7lkU+4es68yyHUHnlZxnlhzZMMjaqxgA9/dglNivdP3glUqiEQ+dGVFtjZ0bepff
    gHjtm90wbFn59b+kPsTsiFVrYcBe6VBASljjx9QVxgPF8YW3rY64BIXjtOTNIduV
    enaiZgLDqkj8hq6mNSSjcbsnk2uDh+dHUTYNLnOaTjzilRXqiwf1SxFqnSnxDQ6f
    BESyS36JvznTXCctkaqMQ5KTFcjAOigLx+sPgaWrM0P0iioSvWaAXK1HICfWCCIZ
    RG1LS4b4fIIgdIhDjBgL+Mnfz7SoR3U0vvF1XUr6sN5wJVIpDhkzSOB5I9cIMS13
    NdbZ9idWLT4JW5sjXg3KZgVV6qADN3fVRvA0xww5B6halGD04qmO+R35nAifkm9h
    CYBuURa2xrSWsVTApWwQAM8O901vZicDq09jJqOflkgHzx6soRQ=
    =0WeV
    -----END PGP SIGNATURE-----
    

    pinheadmz's public key is on openpgp.org

    </details>

    Bitcoin Core client v30.99.0-3de90a1159e6 - server 70016/Satoshi:30.99.0/ - services wl2
    
    <->   type   net   serv  v  mping   ping send recv  txn  blk  hb addrp addrl  age id address                                                             version
     in        onion    wl2  2   1325   1325   46   46                   1          0  2 127.0.0.1:59473                                                     70016/Satoshi:30.0.0/
    out   full onion  nwcl2  2    931    931    4    0         0      1000          1  0 wffaznxih3nah5tbjpiifbn5v3fzmr5tqzcinacr3cjtnags45owvaqd.onion:8333 70016/Satoshi:30.0.0/
    out   full onion  nbwl2  2    934    934    4    0         0      1001          0  1 uathi7edrzgqsby7dikuqe7ehwpwjem2cy4mpsbahrsrtchjdymufxyd.onion:8333 70016/Satoshi:29.0.0/
    out   full onion    nwl  1   2810   2810   11    0                1000          0  3 t423yolpdj5udh2yyemopmjtenmmwtfx3rxcdtneda5g2rw6isnjo7id.onion:8333 70016/Satoshi:25.0.0/
                                   ms     ms  sec  sec  min  min                  min
    
            onion   total   block
    in          1       1
    out         3       3       0
    total       4       4
    
    Local addresses
    zai7kzajktbkqom3edw45zyxnn6e5z5t45d2m7dhmhilgpkz2l3frmad.onion     port   8333    score      4
    
  33. DrahtBot requested review from janb84 on Jan 15, 2026
  34. DrahtBot requested review from pablomartin4btc on Jan 15, 2026
  35. DrahtBot requested review from sedited on Jan 15, 2026
  36. fjahr force-pushed on Jan 17, 2026
  37. fjahr force-pushed on Jan 17, 2026
  38. DrahtBot added the label CI failed on Jan 17, 2026
  39. fjahr commented at 2:40 PM on January 17, 2026: contributor

    Addressed feedback from @pinheadmz , thanks for the review!

    EDIT: And also, I had actually lost sight of bitcoin-cli, I will look at that next to see what, if any, could be reusable from here to apply there.

  40. DrahtBot removed the label CI failed on Jan 17, 2026
  41. DrahtBot added the label Needs rebase on Jan 21, 2026
  42. fjahr force-pushed on Jan 21, 2026
  43. fjahr commented at 5:02 PM on January 21, 2026: contributor
  44. DrahtBot added the label CI failed on Jan 21, 2026
  45. DrahtBot removed the label Needs rebase on Jan 21, 2026
  46. fjahr commented at 10:30 PM on January 21, 2026: contributor

    CI failure seems unrelated but I am waiting for #34242 anyway, hoping that makes it in soon.

  47. sedited referenced this in commit 0871e104a2 on Jan 23, 2026
  48. DrahtBot added the label Needs rebase on Jan 23, 2026
  49. fjahr force-pushed on Jan 23, 2026
  50. fjahr commented at 9:19 PM on January 23, 2026: contributor

    Rebased since #34242 was merged.

  51. fjahr force-pushed on Jan 23, 2026
  52. DrahtBot removed the label Needs rebase on Jan 23, 2026
  53. in src/torcontrol.h:146 in 1545c062a5
     153 | -    CService service;
     154 | +    TorControlConnection m_conn;
     155 | +    std::string m_private_key;
     156 | +    std::string m_service_id;
     157 | +    bool m_reconnect;
     158 | +    float m_reconnect_timeout;
    


    pinheadmz commented at 2:59 PM on January 27, 2026:

    4004ce5fd0bf249e2912ac7535218bef93907834

    I thought it was funny at first to store a number of seconds for a timeout in a float, but I see it's multiplied by 1.5 as an exponential backoff.


    fjahr commented at 4:39 PM on January 30, 2026:

    Hm, yeah, I didn't really think much about it. I thought about this and seems to me like using std::chrono here is the nicer way of doing this, so I made that change.

  54. in src/torcontrol.cpp:158 in 1545c062a5
     168 | +    if (!m_sock->Wait(timeout, Sock::RECV, &event)) {
     169 | +        return false;
     170 | +    }
     171 | +    if (event & Sock::ERR) {
     172 | +        return false;
     173 | +    }
    


    pinheadmz commented at 3:06 PM on January 27, 2026:

    dea33b173e227656a2368e21bc8b24a51a881700

    You don't wanna disconnect if there's a socket error?


    fjahr commented at 4:39 PM on January 30, 2026:

    We should disconnect here if WaitForData returns false. However, when looking at it again, I think there might be some potential race if the socket is still connected somehow and calling Disconnect here shouldn't hurt, so I added that.

  55. in src/torcontrol.h:97 in 1545c062a5
      92 | +    bool WaitForData(std::chrono::milliseconds timeout) const;
      93 | +
      94 | +    /**
      95 | +     * Read available data from socket and process complete replies.
      96 | +     * Dispatches to registered reply handlers.
      97 | +     * @return true on success, false on connection error
    


    pinheadmz commented at 3:13 PM on January 27, 2026:

    dea33b173e227656a2368e21bc8b24a51a881700

    Also returns false at "at end of stream" which I don't think is totally correct since that will log "Error processing data from Tor control port" from ThreadControl().


    fjahr commented at 4:39 PM on January 30, 2026:

    (responding also to #34158 (review), marking the other one as resolved)

    Yeah, the docs weren't matching my real intent here. My thinking was that true simply means we are still connected and false that we are not anymore. Logging on the reason that we are not can be handled internally. I made this more consistent so at least it should be consistent now.

    I you insist that returning a bool isn't the right choice here I could use some enum as the return value (OK, ERR, END) to make the code more expressive. Would be fine for me, too.

  56. in src/torcontrol.cpp:183 in 1545c062a5
     196 | +    if (nread == 0) {
     197 | +        LogDebug(BCLog::TOR, "End of stream");
     198 |          return false;
     199 | -    struct evbuffer *buf = bufferevent_get_output(b_conn);
     200 | -    if (!buf)
     201 | +    }
    


    pinheadmz commented at 3:16 PM on January 27, 2026:

    dea33b173e227656a2368e21bc8b24a51a881700

    related to comment in torcontrol.h, I'm not sure return false is the right thing to do at end of stream.

  57. in src/torcontrol.cpp:119 in 1545c062a5
     125 | +        m_sock.reset();
     126 | +        return false;
     127 | +    }
     128 | +
     129 | +    // Set non-blocking mode on socket
     130 | +    if (!m_sock->SetNonBlocking()) {
    


    pinheadmz commented at 3:33 PM on January 27, 2026:

    dea33b173e227656a2368e21bc8b24a51a881700

    This would be already set if you used CreateSock() above.

    But as long as we're setting socket options here I wonder if we should also add SO_REUSEADDR (don't wait to rebind to the socket if there is a restart)


    fjahr commented at 4:39 PM on January 30, 2026:

    (responding also to #34158 (review), marking the other as resolved)

    I looked through that code and it seemed actually that ConnectDirectly() was an even better fit, so I am using that for now, let me know what you think.

  58. in src/torcontrol.cpp:103 in 1545c062a5
      98 | @@ -166,41 +99,141 @@ bool TorControlConnection::Connect(const std::string& tor_control_center, const
      99 |          return false;
     100 |      }
     101 |  
     102 | -    // Create a new socket, set up callbacks and enable notification bits
     103 | -    b_conn = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
     104 | -    if (!b_conn) {
     105 | +    // Create a new socket
     106 | +    SOCKET s = socket(control_service->GetSAFamily(), SOCK_STREAM, IPPROTO_TCP);
    


    pinheadmz commented at 3:38 PM on January 27, 2026:

    dea33b173e227656a2368e21bc8b24a51a881700

    Why not use CreateSock() here?

  59. pinheadmz approved
  60. pinheadmz commented at 3:59 PM on January 27, 2026: member

    re-ACK 1545c062a5973d7ec1aaf77d6224c5ad8a1686b2

    Changes since last review are rebasing on master to include LineReader (#34242 ), as well as switching std::bind to std::bind_front (#34353) and addressing nits from review.

    I dug a bit deeper into the socket code this time around as well and left some questions. Tested successfully again on mainnet and tried various edge cases like stopping and restarting the tor daemon while Bitcoin is running and switching the torcontrol port.

    This can be added in a follow-up PR but once torcontrol is rid of libevent, we can also add the option to connect via UNIX sockets, like #27375

    <details><summary>Show Signature</summary>

    -----BEGIN PGP SIGNED MESSAGE-----
    Hash: SHA256
    
    ACK 1545c062a5973d7ec1aaf77d6224c5ad8a1686b2
    -----BEGIN PGP SIGNATURE-----
    
    iQIzBAEBCAAdFiEE5hdzzW4BBA4vG9eM5+KYS2KJyToFAml43+sACgkQ5+KYS2KJ
    yTpOvQ/6A+msBLJZ307NW7aUWuAKMptOPPd0MlPWMtRIgZOEoepMlI4pwHW4mXMn
    yP6c4ixGhrbt7iwdLVQA/+bU06x+CUC5GZMgImJu6IdsrSBxlUCz/aCOphEGixom
    bsfF7EeULgX7ZOTgt4OMhgYX3riD5SmKbyAt18KjPqBcPAUkGTu1JprmB6O44yuX
    rp6jcrhfQ7RP6yY1n+9Pje6Oy9/x0oQCU476bosMpJLguyod68aI3oaDB70fO4j7
    rPI3HHhzQzkQ0vQ3KifgZwZje+OCKlm8JxJVMRDIwISWwZW0V3M3ouYK+xgffiTG
    BzcYyEXb13VDQlzWMGaqNE7hmTTa5rOqWUrZXv03QtluHcBXzaa50rHOj2eM491F
    mL2v6wV8lAepsWOowURvs1AsIcL6t3yjb3gmRh9wR4Ny3S+rzxo3TJEc0usS6vYy
    RSVtlz2RDWFf52PbchkfSIJ2c1dg4r5QlWi2lJaITebWh0IF2l+wMHFqBkf/jWmP
    X7q3ZV+RESiVVxHrmoTiDzEgIW09YxKftXeGNs7IHIbKQg7X4x3gZgsk5pGbvqsv
    qX7NxVLWPvzPThRvx/ckPHnZV2/Ee/RhjgIvKLtg30SphUfqjwr+dilF1QxyhUZc
    oLYKNkARab3U+6s8PIk4YPsr/4zsU/fn+COllKejImnlHuh2GgI=
    =kxiy
    -----END PGP SIGNATURE-----
    

    pinheadmz's public key is on openpgp.org

    </details>

  61. fjahr force-pushed on Jan 30, 2026
  62. DrahtBot removed the label CI failed on Jan 30, 2026
  63. pinheadmz approved
  64. pinheadmz commented at 7:34 PM on February 6, 2026: member

    re-ACK ddc4faafd2150aed08a5956638a8781ede234e4e

    Changes since last review were all addressing my review feedback, mostly just nits. Small change to chrono::duration for m_reconnect_timeout and a bigger change using ConnectDirectly() which is a very nice clean up.

    Tested again on macos/arm64 on main net. The 1.5x backoff reconnection time worked as expected when tor was killed. Inbound and outbound tor connections worked as expected.

    <details><summary>Show Signature</summary>

    -----BEGIN PGP SIGNED MESSAGE-----
    Hash: SHA256
    
    ACK ddc4faafd2150aed08a5956638a8781ede234e4e
    -----BEGIN PGP SIGNATURE-----
    
    iQIzBAEBCAAdFiEE5hdzzW4BBA4vG9eM5+KYS2KJyToFAmmGPz4ACgkQ5+KYS2KJ
    yTqwdw//WxV6osT65TFiXT3UElMY8eLXK/j5koJNJw/ZgJLNEDjnq69vIhRcY9Yq
    5I8NqMtQRA4EU/7yLl55RWExjNwXgFIGlP/oVu5/VKmL5+10e8fjztAuCjA6WW8F
    LIDP/X4Ol3K+uFypcYEyDWRVz129sdLN4B4QC85+18sU7BfZ0nGWfv5FLivhMq2z
    454Z49kf/rV17vBZRXeVU62En0h2sW0vGqiNG00hcviarlKRF5WXN0DQtkmS0oHK
    kfMwoFYe6xtcd1g66YZCRuOWadk00wI/v5+nyitUZdq0b5mhj0GcsQ+t5GbNocOo
    QEQktK2w32joyeJIkmvCHL0+Fmv3uiAFPyHlbQkoRQWCWmQIRJYti3wiNVKXEZ18
    AKWgY5cNiYlNp2DqE5wDI9PV603p7yvcQxvI2J1bzlW2gyInhRIYbhuclZxY8LHC
    iEZ88rrPtH1CN4Sh1xrZUbCpzt3mH9cOLMzKp7UJtgNWVtpKhe9gczUzl1uVKe6h
    FfsRZrZeBwr4G+8HNuQIcbzavG2b+vPIP6kQ3TjUTyMEf/1GpEU6MeumfuSNh0lO
    zZ0PSwC7li7V57gOytChrV7SNHESneM3j9BwAQFInF3GEk4K9AmQpZuik4z4nbnt
    IuUzz7hp5IX7COEcFfWUxZdEQwYKktiq3RuwrEXHsVAPaUm+HTE=
    =6wBj
    -----END PGP SIGNATURE-----
    

    pinheadmz's public key is on openpgp.org

    </details>

  65. in src/torcontrol.cpp:121 in a87e37f377 outdated
     181 |  {
     182 | -    if (b_conn) {
     183 | -        Disconnect();
     184 | -    }
     185 | +    if (!m_sock) return false;
     186 | +    std::string errmsg;
    


    sedited commented at 1:57 PM on February 8, 2026:

    Nit: How about logging this to debug or trace?


    fjahr commented at 11:20 PM on March 13, 2026:

    Sounds good to me, done. I went with debug logging.

  66. in src/torcontrol.cpp:706 in a87e37f377 outdated
     721 | -     */
     722 | -    if (!m_conn.Connect(m_tor_control_center, std::bind_front(&TorController::connected_cb, this),
     723 | -         std::bind_front(&TorController::disconnected_cb, this) )) {
     724 | -        LogWarning("tor: Re-initiating connection to Tor control port %s failed", m_tor_control_center);
     725 | -    }
     726 | +    LogDebug(BCLog::TOR, "Not connected to Tor control port %s, will retry", m_tor_control_center);
    


    sedited commented at 5:57 PM on February 8, 2026:

    Would a warn-level log line somewhere in the retry logic be useful?


    fjahr commented at 11:20 PM on March 13, 2026:

    There is a LogWarning in ThreadControl() when Connect() fails during retry, so warn-level is covered in retry I think. But let me know if you still think this should be extended.

  67. in src/torcontrol.cpp:722 in a87e37f377
     751 | +/**
     752 | + * TODO: TBD if introducing a global is the preferred approach here since we
     753 | + * usually try to avoid them. We could let init manage the lifecycle or make
     754 | + * this a part of NodeContext maybe instead.
     755 | + */
     756 | +static std::unique_ptr<TorController> g_tor_controller;
    


    sedited commented at 8:58 PM on February 8, 2026:

    Seems like moving this to node context is a simple change and gets rid of some boilerplate that is only covered by functional tests:

    <details> <summary>Click to expand diff</summary>

    diff --git a/src/init.cpp b/src/init.cpp
    index 841fdec5b2..07de0c63d3 100644
    --- a/src/init.cpp
    +++ b/src/init.cpp
    @@ -273 +273,3 @@ void Interrupt(NodeContext& node)
    -    InterruptTorControl();
    +    if (node.tor_controller) {
    +        node.tor_controller->Interrupt();
    +    }
    @@ -316 +318,4 @@ void Shutdown(NodeContext& node)
    -    StopTorControl();
    +    if (node.tor_controller) {
    +        node.tor_controller->Join();
    +        node.tor_controller.reset();
    +    }
    @@ -2141 +2146 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
    -        StartTorControl(onion_service_target);
    +        node.tor_controller = std::make_unique<TorController>(gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target);
    diff --git a/src/node/context.cpp b/src/node/context.cpp
    index d7d01b494e..164361601f 100644
    --- a/src/node/context.cpp
    +++ b/src/node/context.cpp
    @@ -19,0 +20 @@
    +#include <torcontrol.h>
    diff --git a/src/node/context.h b/src/node/context.h
    index 3a7488fd25..abe1bc8da2 100644
    --- a/src/node/context.h
    +++ b/src/node/context.h
    @@ -27,0 +28 @@ class PeerManager;
    +class TorController;
    @@ -92,0 +94 @@ struct NodeContext {
    +    std::unique_ptr<TorController> tor_controller;
    diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp
    index be17a57488..1dbc037df0 100644
    --- a/src/torcontrol.cpp
    +++ b/src/torcontrol.cpp
    @@ -715,29 +714,0 @@ fs::path TorController::GetPrivateKeyFile()
    -/****** Thread ********/
    -
    -/**
    - * TODO: TBD if introducing a global is the preferred approach here since we
    - * usually try to avoid them. We could let init manage the lifecycle or make
    - * this a part of NodeContext maybe instead.
    - */
    -static std::unique_ptr<TorController> g_tor_controller;
    -
    -void StartTorControl(CService onion_service_target)
    -{
    -    assert(!g_tor_controller);
    -    g_tor_controller = std::make_unique<TorController>(gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target);
    -}
    -
    -void InterruptTorControl()
    -{
    -    if (!g_tor_controller) return;
    -    LogInfo("tor: Thread interrupt");
    -    g_tor_controller->Interrupt();
    -}
    -
    -void StopTorControl()
    -{
    -    if (!g_tor_controller) return;
    -    g_tor_controller->Join();
    -    g_tor_controller.reset();
    -}
    -
    diff --git a/src/torcontrol.h b/src/torcontrol.h
    index 94561e3f40..ae4902fee0 100644
    --- a/src/torcontrol.h
    +++ b/src/torcontrol.h
    @@ -29,4 +28,0 @@ static const bool DEFAULT_LISTEN_ONION = true;
    -void StartTorControl(CService onion_service_target);
    -void InterruptTorControl();
    -void StopTorControl();
    -
    
    

    </details>


    fjahr commented at 11:20 PM on March 13, 2026:

    Oh, very nice! Applied in a separate commit. I imagined this would be harder to be honest.

  68. in src/torcontrol.cpp:567 in a87e37f377
     563 | @@ -501,7 +564,7 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply&
     564 |   *                  CookieString | ClientNonce | ServerNonce)
     565 |   *
     566 |   */
     567 | -static std::vector<uint8_t> ComputeResponse(const std::string &key, const std::vector<uint8_t> &cookie,  const std::vector<uint8_t> &client_nonce, const std::vector<uint8_t> &server_nonce)
     568 | +static std::vector<uint8_t> ComputeResponse(const std::string_view &key, const std::vector<uint8_t> &cookie,  const std::vector<uint8_t> &client_nonce, const std::vector<uint8_t> &server_nonce)
    


    sedited commented at 9:11 PM on February 8, 2026:

    Nit: Can pass key by value and in the spirit of this line's change the vectors could be spans?


    fjahr commented at 11:20 PM on March 13, 2026:

    Sounds good, done.

  69. in src/test/fuzz/torcontrol.cpp:52 in ddc4faafd2 outdated
      53 | +            [&] {
      54 | +                tor_controller.auth_cb(conn, tor_control_reply);
      55 |              },
      56 |              [&] {
      57 | -                tor_controller.auth_cb(dummy_tor_control_connection, tor_control_reply);
      58 | +                tor_controller.authchallenge_cb(conn, tor_control_reply);
    


    sedited commented at 9:29 PM on February 8, 2026:

    I'm getting a fuzzer failure that I'm guessing may be caused by a change in this function:

    <details> <summary>Click to expand logs</summary>

    [#740](/bitcoin-bitcoin/740/)	NEW    cov: 661 ft: 1014 corp: 25/78b lim: 4 exec/s: 0 rss: 110Mb L: 4/4 MS: 5 ChangeBit-CMP-ChangeByte-ChangeBinInt-ShuffleBytes- DE: "\373\000\000\000"-
    /usr/bin/../lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/stl_vector.h:1148:9: runtime error: reference binding to null pointer of type 'const value_type' (aka 'const std::basic_string<char>')
    SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/bin/../lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/stl_vector.h:1148:9 
    AddressSanitizer:DEADLYSIGNAL
    =================================================================
    ==21501==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (pc 0x64b69d0d8101 bp 0x7ffe53d6ec10 sp 0x7ffe53d6eb40 T0)
    ==21501==The signal is caused by a READ memory access.
    ==21501==Hint: address points to the zero page.
        [#0](/bitcoin-bitcoin/0/) 0x64b69d0d8101 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::size() const /usr/bin/../lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/basic_string.h:1072:16
        [#1](/bitcoin-bitcoin/1/) 0x64b69d0d8101 in SplitTorReplyLine(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /home/drgrid/bitcoin/src/torcontrol.cpp:230:20
        [#2](/bitcoin-bitcoin/2/) 0x64b69d0e3b7b in TorController::authchallenge_cb(TorControlConnection&, TorControlReply const&) /home/drgrid/bitcoin/src/torcontrol.cpp:582:48
        [#3](/bitcoin-bitcoin/3/) 0x64b69b8d6321 in torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_5::operator()() const /home/drgrid/bitcoin/src/test/fuzz/torcontrol.cpp:52:32
        [#4](/bitcoin-bitcoin/4/) 0x64b69b8d6321 in unsigned long CallOneOf<torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_3, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_4, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_5, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_6, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_7>(FuzzedDataProvider&, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_3, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_4, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_5, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_6, torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>)::$_7) /home/drgrid/bitcoin/src/test/fuzz/util.h:42:27
        [#5](/bitcoin-bitcoin/5/) 0x64b69b8d6321 in torcontrol_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>) /home/drgrid/bitcoin/src/test/fuzz/torcontrol.cpp:43:9
        [#6](/bitcoin-bitcoin/6/) 0x64b69bb79cd5 in std::function<void (std::span<unsigned char const, 18446744073709551615ul>)>::operator()(std::span<unsigned char const, 18446744073709551615ul>) const /usr/bin/../lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/std_function.h:591:9
        [#7](/bitcoin-bitcoin/7/) 0x64b69bb79cd5 in test_one_input(std::span<unsigned char const, 18446744073709551615ul>) /home/drgrid/bitcoin/src/test/fuzz/fuzz.cpp:88:5
        [#8](/bitcoin-bitcoin/8/) 0x64b69bb79cd5 in LLVMFuzzerTestOneInput /home/drgrid/bitcoin/src/test/fuzz/fuzz.cpp:216:5
        [#9](/bitcoin-bitcoin/9/) 0x64b69b115c14 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/drgrid/bitcoin/build_fuzz/bin/fuzz+0x1badc14) (BuildId: e39b175d556cbbeafdd76014c930896f3d6c8f6e)
        [#10](/bitcoin-bitcoin/10/) 0x64b69b115309 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/home/drgrid/bitcoin/build_fuzz/bin/fuzz+0x1bad309) (BuildId: e39b175d556cbbeafdd76014c930896f3d6c8f6e)
        [#11](/bitcoin-bitcoin/11/) 0x64b69b116af5 in fuzzer::Fuzzer::MutateAndTestOne() (/home/drgrid/bitcoin/build_fuzz/bin/fuzz+0x1baeaf5) (BuildId: e39b175d556cbbeafdd76014c930896f3d6c8f6e)
        [#12](/bitcoin-bitcoin/12/) 0x64b69b117655 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) (/home/drgrid/bitcoin/build_fuzz/bin/fuzz+0x1baf655) (BuildId: e39b175d556cbbeafdd76014c930896f3d6c8f6e)
        [#13](/bitcoin-bitcoin/13/) 0x64b69b10492f in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/drgrid/bitcoin/build_fuzz/bin/fuzz+0x1b9c92f) (BuildId: e39b175d556cbbeafdd76014c930896f3d6c8f6e)
        [#14](/bitcoin-bitcoin/14/) 0x64b69b12efb6 in main (/home/drgrid/bitcoin/build_fuzz/bin/fuzz+0x1bc6fb6) (BuildId: e39b175d556cbbeafdd76014c930896f3d6c8f6e)
        [#15](/bitcoin-bitcoin/15/) 0x70df0082a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
        [#16](/bitcoin-bitcoin/16/) 0x70df0082a28a in __libc_start_main csu/../csu/libc-start.c:360:3
        [#17](/bitcoin-bitcoin/17/) 0x64b69b0f9914 in _start (/home/drgrid/bitcoin/build_fuzz/bin/fuzz+0x1b91914) (BuildId: e39b175d556cbbeafdd76014c930896f3d6c8f6e)
    
    AddressSanitizer can not provide additional info.
    SUMMARY: AddressSanitizer: SEGV /home/drgrid/bitcoin/src/torcontrol.cpp:230:20 in SplitTorReplyLine(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)
    ==21501==ABORTING
    MS: 1 ChangeASCIIInt-; base unit: 66ab8fe3159190c0fece3131125619ef7349a19d
    0x84,0x33,0xdb,0x67,
    \2043\333g
    artifact_prefix='./'; Test unit written to ./crash-01d7dad05a5562f6797a2b566c99fb79187bc471
    Base64: hDPbZw==
    
    

    </details>

    I am able to reproduce the crash locally with:

    cat crash-01d7dad05a5562f6797a2b566c99fb79187bc471 | base64                                                                         
    hDPbZw==
    

    fjahr commented at 11:19 PM on March 13, 2026:

    Actually it seems it was caused by removing the guard in the fuzz harness a few lines above: https://github.com/bitcoin/bitcoin/pull/34158/commits/389e95fc43c5d002e937fcb86f3820a362fddf85#diff-f8f6ee76830a6bf2c9b60a56e2006e31a509c9e74a395c10cbf092ae978fd19aL63 That guard in the fuzz test should have been a guard in the implementation code IMO, but it wasn't there and I didn't even touch that code until now.

    Admittedly, I think I just removed that from the fuzzing test because I didn't understand why it was there and it seemed like handling of some libevent quirks that we shouldn't need anymore when we can do things properly. Since I didn't see a crash right away I assumed it was fine. I am now handling this explicitly in the implementation and adding that in the same commit as the fuzz change. I couldn't find any good explanation for why it was done this way originally.

  70. DrahtBot requested review from sedited on Feb 8, 2026
  71. w0xlt commented at 5:32 PM on March 10, 2026: contributor

    Concept ACK

  72. fjahr force-pushed on Mar 13, 2026
  73. in src/torcontrol.cpp:90 in 3becccca78
     144 | -    //  removed from the buffer. Everything left is an incomplete line.
     145 | -    if (evbuffer_get_length(input) > MAX_LINE_LENGTH) {
     146 | -        LogWarning("tor: Disconnecting because MAX_LINE_LENGTH exceeded");
     147 | -        self->Disconnect();
     148 | +
     149 | +    std::optional<CService> control_service = Lookup(tor_control_center, 9051, fNameLookup);
    


    sedited commented at 1:13 PM on March 14, 2026:

    Should this be DEFAULT_TOR_CONTROL_PORT instead of 9051?


    fjahr commented at 12:45 PM on March 15, 2026:

    Right, fixed. Also grepped for any other dangeling 9051s but didn't find any.

  74. in src/torcontrol.cpp:415 in 3becccca78 outdated
     429 | +                LogDebug(BCLog::TOR, "Lost connection to Tor control port");
     430 | +                disconnected_cb(m_conn);
     431 | +                continue;
     432 | +            }
     433 | +            // Just a timeout, continue waiting
     434 | +            continue;
    


    sedited commented at 1:27 PM on March 14, 2026:

    I'm a bit confused here. Doesn't a timeout return true?


    fjahr commented at 12:45 PM on March 15, 2026:

    Oh, right, that didn't make sense. I think the behavior that the comment/continue here implies is what we really want here, e.g. WaitForData only returns true when we actually have data. I made that change.

  75. in src/torcontrol.h:142 in 3becccca78
     149 | -    CService service;
     150 | +    TorControlConnection m_conn;
     151 | +    std::string m_private_key;
     152 | +    std::string m_service_id;
     153 | +    bool m_reconnect;
     154 | +    std::chrono::duration<double> m_reconnect_timeout{1.0};
    


    sedited commented at 1:33 PM on March 14, 2026:

    I think this initializer should be dropped, because we already set it in the constructor initializer list.


    fjahr commented at 12:45 PM on March 15, 2026:

    Makes sense, done.

  76. sedited commented at 1:46 PM on March 14, 2026: contributor

    Might have a few more comments after this, but this is looking good.

  77. fjahr force-pushed on Mar 15, 2026
  78. fjahr commented at 12:46 PM on March 15, 2026: contributor

    Addressed review comments from @sedited , thanks!

  79. sedited approved
  80. sedited commented at 12:05 PM on March 18, 2026: contributor

    ACK 045a4df9d469175822b1ecf37173bd1933e01cd1

  81. DrahtBot requested review from pinheadmz on Mar 18, 2026
  82. fanquake added this to the milestone 32.0 on Mar 19, 2026
  83. sedited requested review from vasild on Mar 19, 2026
  84. DrahtBot added the label Needs rebase on Mar 23, 2026
  85. refactor: Use constexpr in torcontrol where possible a36591d194
  86. refactor: Modernize member variable names in torcontrol 6bcb60354e
  87. refactor: Get rid of unnecessary newlines in logs 8444efbd4a
  88. fjahr force-pushed on Mar 24, 2026
  89. fjahr commented at 9:13 AM on March 24, 2026: contributor

    Rebased after #33414 was merged. Aside from the rebase I added coverage to the PoW defense enablement to the functional test.

  90. DrahtBot added the label CI failed on Mar 24, 2026
  91. DrahtBot commented at 9:55 AM on March 24, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task 32 bit ARM: https://github.com/bitcoin/bitcoin/actions/runs/23481729895/job/68327001748</sub> <sub>LLM reason (✨ experimental): CI failed to build the fuzz target due to a C++ compile error in src/test/fuzz/torcontrol.cpp: dummy_tor_control_connection is undefined.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  92. fjahr force-pushed on Mar 24, 2026
  93. DrahtBot removed the label Needs rebase on Mar 24, 2026
  94. DrahtBot removed the label CI failed on Mar 24, 2026
  95. sedited approved
  96. sedited commented at 12:00 PM on March 24, 2026: contributor

    Re-ACK 3b3bd97567d4dc17b262bf29e447f755d033ee38

  97. in src/torcontrol.cpp:174 in 3b3bd97567 outdated
     266 | -        bufferevent_free(b_conn);
     267 | -    b_conn = nullptr;
     268 | +    util::LineReader reader(m_recv_buffer, MAX_LINE_LENGTH);
     269 | +    auto start = reader.it;
     270 | +
     271 | +    while (auto line = reader.ReadLine()) {
    


    janb84 commented at 3:26 PM on March 24, 2026:
        while (true) {
            std::optional<std::string> line;
            try {
                line = reader.ReadLine();
            } catch (const std::runtime_error& e) {
                LogWarning("tor: Disconnecting because MAX_LINE_LENGTH exceeded: %s", e.what());
                return false;
            }
    
            if (!line) break;
            
    

    NIT: currently, the ProcessBuffer throws a runtime_error when a received line exceeds MAX_LINE_LENGTH, and this exception can/will unwind the torcontrol thread and crash the node.

    I have extended feature_torcontrol.py to show the issue:

    <details>

        def test_oversized_line_no_crash(self):
            self.log.info("Test that oversized Tor control response lines do not crash the node")
    
            tor_port = p2p_port(self.num_nodes + 4)
            mock_tor = MockTorControlServer(tor_port, manual_mode=True)
            mock_tor.start()
    
            self.restart_node(0, extra_args=[
                f"-torcontrol=127.0.0.1:{tor_port}",
                "-listenonion=1",
                "-debug=tor",
            ])
    
            # Wait for connection and PROTOCOLINFO command.
            mock_tor.conn_ready.wait(timeout=10)
            self.wait_until(lambda: len(mock_tor.received_commands) >= 1, timeout=10)
            assert_equal(mock_tor.received_commands[0], "PROTOCOLINFO 1")
    
            # Send one line longer than MAX_LINE_LENGTH (100000).
            mock_tor.send_raw("250-" + ("A" * 100001) + "\r\n")
    
            # Expected behavior: node crashes without patch, with patch only a disconnect.  
            ensure_for(duration=2, f=lambda: self.nodes[0].process.poll() is None)
    
            # Connection should be dropped and retried, causing another PROTOCOLINFO.
            self.wait_until(lambda: len(mock_tor.received_commands) >= 2, timeout=10)
            assert_equal(mock_tor.received_commands[1], "PROTOCOLINFO 1")
    
            mock_tor.stop()
    

    </details>

    The proposed fix catches this exception in ProcessBuffer, log it, and return false to drop/reconnect the Tor control connection instead of letting it bubble up and crash the node.


    pinheadmz commented at 6:37 PM on March 24, 2026:

    Good catch, could also wrap ProcessBuffer() in a try/catch at the callsite in ReceiveAndProcess() which disconnects for other reasons as well by returning false.


    fjahr commented at 7:43 PM on March 25, 2026:

    Thanks @janb84 , good catch. I decided to take make the fix in ProcessBuffer() like @pinheadmz suggested and took your test with minor edits!

  98. janb84 commented at 3:50 PM on March 24, 2026: contributor

    Concept ACK 3b3bd97567d4dc17b262bf29e447f755d033ee38

    Looks good, I reviewed the complete code again (it has been a while), found a possible NIT (with test).

  99. in test/functional/feature_torcontrol.py:98 in 7355bd4e9e
      93 | +    def set_test_params(self):
      94 | +        self.num_nodes = 1
      95 | +
      96 | +    def run_test(self):
      97 | +        self.log.info("Test Tor control basic functionality")
      98 | +        tor_port = p2p_port(self.num_nodes + 1)
    


    pinheadmz commented at 5:52 PM on March 24, 2026:

    7355bd4e9e862f66ab341f5d0aa2bfb10fb0833c

    nit: maybe could be called tor_control_port especially since we have tor_port() in test_framework/util.py for the actual SOCKS proxy


    fjahr commented at 7:42 PM on March 25, 2026:

    done

  100. in test/functional/feature_torcontrol.py:2 in 7355bd4e9e
       0 | @@ -0,0 +1,122 @@
       1 | +#!/usr/bin/env python3
       2 | +# Copyright (c) 2015-present The Bitcoin Core developers
    


    pinheadmz commented at 5:52 PM on March 24, 2026:

    7355bd4e9e862f66ab341f5d0aa2bfb10fb0833c

    nano-nit: can remove 2015-present


    fjahr commented at 7:42 PM on March 25, 2026:

    done

  101. pinheadmz commented at 6:48 PM on March 24, 2026: member

    code review 3b3bd97567d4dc17b262bf29e447f755d033ee38

    Minimal changes since last review. Catching the LineReader error as pointed out by @janb84 should be addressed, otherwise LGTM.

    Built and tested on macos/arm64. Ran with old and new versions of Tor and observed the PoW defenses on/off. Tried to break it by restarting tor, using alt port numbers, etc. Everything holds up. Two tiny nits in the functional test that are not deal breakers.

  102. DrahtBot requested review from pinheadmz on Mar 24, 2026
  103. fjahr force-pushed on Mar 25, 2026
  104. fjahr commented at 7:46 PM on March 25, 2026: contributor

    Addressed feedback from @janb84 and @pinheadmz . I am extending the functional test in a seperate commit and also do a small dedublication of the setup code for each test case because the file has grown a bit.

  105. janb84 commented at 8:11 PM on March 26, 2026: contributor

    ACK 3b81a0726c4d5fca12c8bb40f38ca8b88c250368

    lgtm.

    thanks for incorporating my suggestion .

  106. DrahtBot requested review from sedited on Mar 26, 2026
  107. in src/torcontrol.cpp:169 in 3b81a0726c
     255 | -        LogWarning("tor: Error connecting to address %s", tor_control_center);
     256 | +    m_recv_buffer.insert(m_recv_buffer.end(), buf, buf + nread);
     257 | +    try {
     258 | +        return ProcessBuffer();
     259 | +    } catch (const std::runtime_error& e) {
     260 | +        LogWarning("tor: Disconnecting because MAX_LINE_LENGTH exceeded: %s", e.what());
    


    pinheadmz commented at 2:48 PM on March 27, 2026:

    nit: redundant error message, and LineReader may throw other errors in the future. This log message turns into:

    tor: Disconnecting because MAX_LINE_LENGTH exceeded: max_line_length exceeded by LineReader


    fjahr commented at 10:49 AM on April 2, 2026:

    Fixed

  108. DrahtBot requested review from pinheadmz on Mar 27, 2026
  109. fjahr force-pushed on Apr 2, 2026
  110. fjahr commented at 10:51 AM on April 2, 2026: contributor

    Fixed the overly (mis)informative log warning, thanks @pinheadmz !

  111. torcontrol: Remove libevent usage
    Replace libevent-based approach with using the Sock class and CThreadInterrupt.
    eae193e750
  112. torcontrol: Move tor controller into node context
    Co-authored-by: sedited <seb.kung@gmail.com>
    b1869e9a2d
  113. fuzz: Improve torcontrol fuzz test
    Gets rid of the Dummy class and adds coverage of get_socks_cb.
    
    Also explicitly handles an exception case within Torcontrol rather than
    relying on a guard in the fuzz test.
    4117b92e67
  114. test: Add simple functional test for torcontrol 569383356e
  115. test: Add test for partial message handling in torcontrol 7dff9ec298
  116. test: Add torcontrol coverage for PoW defense enablement 84c1f32071
  117. test: Add test for exceeding max line length in torcontrol
    Also deduplicates the repetitive tor mock setup for each test case a bit.
    
    Co-authored-by: janb84 <githubjanb.drainer976@passmail.net>
    1401011f71
  118. in src/torcontrol.h:146 in ff742ba72c
     153 | -    float reconnect_timeout;
     154 | -    CService service;
     155 | +    TorControlConnection m_conn;
     156 | +    std::string m_private_key;
     157 | +    std::string m_service_id;
     158 | +    bool m_reconnect;
    


    janb84 commented at 3:05 PM on April 3, 2026:
        std::atomic<bool> m_reconnect;
    

    NIT: think I found a data race issue:

    There is no synchronization between the following accesses:

    • Interrupt() at torcontrol.cpp L371 writes m_reconnect = false from the main/destructor thread
    • ThreadControl() at torcontrol.cpp L393 and disconnected_cb() at torcontrol.cpp L730 read m_reconnect from the torcontrol thread

    How to reproduce:

    <details>

    Add a short wait to torcontrol.cpp L393

                if (!m_conn.Connect(m_tor_control_center)) {
                    LogWarning("tor: Initiating connection to Tor control port %s failed", m_tor_control_center);
                    std::this_thread::sleep_for(std::chrono::seconds(5)); // TSan race window
                    if (!m_reconnect) {
                        break;
                    }
    

    build with TSAN on :

    cmake -B build -DBUILD_GUI=OFF -DENABLE_IPC=OFF -DSANITIZERS=thread
    cmake --build build -j$(nproc)
    

    start the node with incorrect torcontrol string:

    ./build/bin/bitcoind -torcontrol=127.0.0.1:9999
    

    wait for the following waring to show:

    [warning] tor: Initiating connection to Tor control port 127.0.0.1:9999 failed
    

    You have now 5 seconds to shutdown the node (press ctrl-c once)

    This will result in a TSAN error:

    =================
    
    WARNING: ThreadSanitizer: data race (pid=80740)
    
    Read of size 1 at 0x725000002368 by thread T34:
    
    [#0](/bitcoin-bitcoin/0/) TorController::ThreadControl() /home/jan/projects/bitcoin/src/torcontrol.cpp:394 (bitcoind+0x5c6819) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#1](/bitcoin-bitcoin/1/) operator() /home/jan/projects/bitcoin/src/torcontrol.cpp:357 (bitcoind+0x5c6ba3) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#2](/bitcoin-bitcoin/2/) __invoke_impl<void, TorController::TorController(const std::string&, const CService&)::<lambda()>&> /usr/include/c++/14/bits/invoke.h:61 (bitcoind+0x5c6ba3) [#3](/bitcoin-bitcoin/3/) __invoke_r<void, TorController::TorController(const std::string&, const CService&)::<lambda()>&> /usr/include/c++/14/bits/invoke.h:111 (bitcoind+0x5c6ba3) [#4](/bitcoin-bitcoin/4/) _M_invoke /usr/include/c++/14/bits/std_function.h:290 (bitcoind+0x5c6ba3) [#5](/bitcoin-bitcoin/5/) std::function<void ()>::operator()() const /usr/include/c++/14/bits/std_function.h:591 (bitcoind+0xce949f) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#6](/bitcoin-bitcoin/6/) util::TraceThread(std::basic_string_view<char, std::char_traits<char> >, std::function<void ()>) /home/jan/projects/bitcoin/src/util/thread.cpp:21 (bitcoind+0xce949f) [#7](/bitcoin-bitcoin/7/) __invoke_impl<void, void (*)(std::basic_string_view<char>, std::function<void()>), char const*, TorController::TorController(const std::string&, const CService&)::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (bitcoind+0x5c1b34) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#8](/bitcoin-bitcoin/8/) __invoke<void (*)(std::basic_string_view<char>, std::function<void()>), char const*, TorController::TorController(const std::string&, const CService&)::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (bitcoind+0x5c1b34) [#9](/bitcoin-bitcoin/9/) _M_invoke<0, 1, 2> /usr/include/c++/14/bits/std_thread.h:301 (bitcoind+0x5c1b34) [#10](/bitcoin-bitcoin/10/) operator() /usr/include/c++/14/bits/std_thread.h:308 (bitcoind+0x5c1b34) [#11](/bitcoin-bitcoin/11/) _M_run /usr/include/c++/14/bits/std_thread.h:253 (bitcoind+0x5c1b34) [#12](/bitcoin-bitcoin/12/) <null> <null> (libstdc++.so.6+0xe1223) (BuildId: 133b71e0013695cc7832680a74edb51008c4fc4c)
    
    Previous write of size 1 at 0x725000002368 by main thread:
    
    [#0](/bitcoin-bitcoin/0/) TorController::Interrupt() /home/jan/projects/bitcoin/src/torcontrol.cpp:371 (bitcoind+0x5c219f) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#1](/bitcoin-bitcoin/1/) Interrupt(node::NodeContext&) /home/jan/projects/bitcoin/src/init.cpp:280 (bitcoind+0x1d148d) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#2](/bitcoin-bitcoin/2/) main /home/jan/projects/bitcoin/src/bitcoind.cpp:286 (bitcoind+0x16ed8f) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370)
    
    As if synchronized via sleep:
    
    [#0](/bitcoin-bitcoin/0/) nanosleep ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:397 (libtsan.so.2+0x54d2f) (BuildId: 99ef88596cb10a8ccf307fe1a9070f66a44b1624) [#1](/bitcoin-bitcoin/1/) void std::this_thread::sleep_for<long, std::ratio<1l, 1l> >(std::chrono::duration<long, std::ratio<1l, 1l> > const&) /usr/include/c++/14/bits/this_thread_sleep.h:80 (bitcoind+0x5c6804) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#2](/bitcoin-bitcoin/2/) TorController::ThreadControl() /home/jan/projects/bitcoin/src/torcontrol.cpp:393 (bitcoind+0x5c6804) [#3](/bitcoin-bitcoin/3/) operator() /home/jan/projects/bitcoin/src/torcontrol.cpp:357 (bitcoind+0x5c6ba3) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#4](/bitcoin-bitcoin/4/) __invoke_impl<void, TorController::TorController(const std::string&, const CService&)::<lambda()>&> /usr/include/c++/14/bits/invoke.h:61 (bitcoind+0x5c6ba3) [#5](/bitcoin-bitcoin/5/) __invoke_r<void, TorController::TorController(const std::string&, const CService&)::<lambda()>&> /usr/include/c++/14/bits/invoke.h:111 (bitcoind+0x5c6ba3) [#6](/bitcoin-bitcoin/6/) _M_invoke /usr/include/c++/14/bits/std_function.h:290 (bitcoind+0x5c6ba3) [#7](/bitcoin-bitcoin/7/) std::function<void ()>::operator()() const /usr/include/c++/14/bits/std_function.h:591 (bitcoind+0xce949f) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#8](/bitcoin-bitcoin/8/) util::TraceThread(std::basic_string_view<char, std::char_traits<char> >, std::function<void ()>) /home/jan/projects/bitcoin/src/util/thread.cpp:21 (bitcoind+0xce949f) [#9](/bitcoin-bitcoin/9/) __invoke_impl<void, void (*)(std::basic_string_view<char>, std::function<void()>), char const*, TorController::TorController(const std::string&, const CService&)::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (bitcoind+0x5c1b34) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#10](/bitcoin-bitcoin/10/) __invoke<void (*)(std::basic_string_view<char>, std::function<void()>), char const*, TorController::TorController(const std::string&, const CService&)::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (bitcoind+0x5c1b34) [#11](/bitcoin-bitcoin/11/) _M_invoke<0, 1, 2> /usr/include/c++/14/bits/std_thread.h:301 (bitcoind+0x5c1b34) [#12](/bitcoin-bitcoin/12/) operator() /usr/include/c++/14/bits/std_thread.h:308 (bitcoind+0x5c1b34) [#13](/bitcoin-bitcoin/13/) _M_run /usr/include/c++/14/bits/std_thread.h:253 (bitcoind+0x5c1b34) [#14](/bitcoin-bitcoin/14/) <null> <null> (libstdc++.so.6+0xe1223) (BuildId: 133b71e0013695cc7832680a74edb51008c4fc4c)
    
    Location is heap block of size 504 at 0x725000002200 allocated by main thread:
    
    [#0](/bitcoin-bitcoin/0/) operator new(unsigned long) ../../../../src/libsanitizer/tsan/tsan_new_delete.cpp:64 (libtsan.so.2+0x9c506) (BuildId: 99ef88596cb10a8ccf307fe1a9070f66a44b1624) [#1](/bitcoin-bitcoin/1/) std::__detail::_MakeUniq<TorController>::__single_object std::make_unique<TorController, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, CService&>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&, CService&) /usr/include/c++/14/bits/unique_ptr.h:1077 (bitcoind+0x1f2e88) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#2](/bitcoin-bitcoin/2/) AppInitMain(node::NodeContext&, interfaces::BlockAndHeaderTipInfo*) /home/jan/projects/bitcoin/src/init.cpp:2195 (bitcoind+0x1dd097) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#3](/bitcoin-bitcoin/3/) AppInit /home/jan/projects/bitcoin/src/bitcoind.cpp:242 (bitcoind+0x16f480) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#4](/bitcoin-bitcoin/4/) main /home/jan/projects/bitcoin/src/bitcoind.cpp:283 (bitcoind+0x16f480)
    
    Thread T34 'b-torcontrol' (tid=80775, running) created by main thread at:
    
    [#0](/bitcoin-bitcoin/0/) pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x568a6) (BuildId: 99ef88596cb10a8ccf307fe1a9070f66a44b1624) [#1](/bitcoin-bitcoin/1/) std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xe12f8) (BuildId: 133b71e0013695cc7832680a74edb51008c4fc4c) [#2](/bitcoin-bitcoin/2/) std::__detail::_MakeUniq<TorController>::__single_object std::make_unique<TorController, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, CService&>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&, CService&) /usr/include/c++/14/bits/unique_ptr.h:1077 (bitcoind+0x1f2e99) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#3](/bitcoin-bitcoin/3/) AppInitMain(node::NodeContext&, interfaces::BlockAndHeaderTipInfo*) /home/jan/projects/bitcoin/src/init.cpp:2195 (bitcoind+0x1dd097) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#4](/bitcoin-bitcoin/4/) AppInit /home/jan/projects/bitcoin/src/bitcoind.cpp:242 (bitcoind+0x16f480) (BuildId: ff735a5be1902404c57b420cdd0486bc15a94370) [#5](/bitcoin-bitcoin/5/) main /home/jan/projects/bitcoin/src/bitcoind.cpp:283 (bitcoind+0x16f480)
    
    SUMMARY: ThreadSanitizer: data race /home/jan/projects/bitcoin/src/torcontrol.cpp:394 in TorController::ThreadControl() 
    
    ==================
    

    .

    </details>


    fjahr commented at 8:11 PM on April 3, 2026:

    I think you are are correct, nice catch! I applied the change to use atomic, is seems like the most simple fix, thanks a lot!

  119. fjahr force-pushed on Apr 3, 2026
  120. fjahr commented at 8:12 PM on April 3, 2026: contributor

    Addressed latest feedback from @janb84 , thanks!

  121. janb84 commented at 10:56 AM on April 4, 2026: contributor

    re ACK 1401011f71c22f1652acb3cadcd0a5fb1a3c0d5d

    changes since last ACK:

    • changed over overly informative log warning,
    • fixed race cond.
  122. pinheadmz approved
  123. pinheadmz commented at 6:47 PM on April 7, 2026: member

    ACK 1401011f71c22f1652acb3cadcd0a5fb1a3c0d5d

    change since last review are minimal: fix a logging nit and make a bool used in multiple threads atomic.

    built and ran tests again on macos/arm64, played with the feature on mainnet

    <details><summary>Show Signature</summary>

    -----BEGIN PGP SIGNED MESSAGE-----
    Hash: SHA256
    
    ACK 1401011f71c22f1652acb3cadcd0a5fb1a3c0d5d
    -----BEGIN PGP SIGNATURE-----
    
    iQJPBAEBCAA5FiEE5hdzzW4BBA4vG9eM5+KYS2KJyToFAmnVUPYbFIAAAAAABAAO
    bWFudTIsMi41KzEuMTIsMCwzAAoJEOfimEtiick615MQAND4EBV6CCJRJHWTQ8fV
    VpXf/1yxZ4oMNXD4ZEBqEfjyJDoqT35c42l3OxLIYbgtM8DHsEV614bf0wVqhmbb
    ImNyUsJwCiZgloS4rSfqYgTGlzN1SEolSaEgkombztIgoVW3FhE1YOcOIToQv4qH
    i8m4SpX0CpgrpIFCLt/sdMFYmf1FapLztb3xEEit08Deds0RY19fpkQihkvx8dTi
    /9SOfIKsAhvSm3qcM5nKY5vP679UOnDq+AooTz70piypLTU8FfzJTVjzyQlMp/KF
    KaDLXqv9FbKi2Tz1bjQosXi6iJu5gX1Y/Q3PbiS2YTMB7jL4hmBi1GLBJRvPXwGr
    7dSQCgPczP0DU24AfGw5sFglXliMLWTVIpgQWI+r4cxYxyiZUm2fBVBsJoklMbKq
    95NntpsMS9IXXuAYPmAjIveWembC51y/7OSJ/VmzXUjg1BQrKWKAqCkgcrwZt83o
    G7IaMKXoSFhYooczV92MBeo4vdFng2WiWrfPMaRkEqgJlFXt4LA5Dd/cBZNIgJhd
    pVvgD6gPBZCMlp3qXQF/Ox+3S3fupjofixHzn9OvXkaKOOWm3ODEZqOa34An7Ken
    4/fYwHd34T1kqe6UGOoTD9NjKXjjfbLmzWgOKusyN9JCD0SMrosIGdGWz4npmAg4
    PBD2rYdy4Mx4mfagqlqjw5EX
    =q4UC
    -----END PGP SIGNATURE-----
    

    pinheadmz's public key is on openpgp.org

    </details>

  124. achow101 commented at 9:47 PM on April 8, 2026: member

    ACK 1401011f71c22f1652acb3cadcd0a5fb1a3c0d5d

  125. achow101 merged this on Apr 8, 2026
  126. achow101 closed this on Apr 8, 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-04-22 03:12 UTC

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