Skip to content

Conversation

@fjahr
Copy link
Contributor

@fjahr fjahr commented Dec 27, 2025

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

There is a dependency on #32061 but it only really needs one commit which is cherry-picked here in first position (add LineReader). I hope that a first chunk of that PR can be sliced off and reviewed independently so this PR here is not blocked by it.

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.

@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 27, 2025

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

Code Coverage & Benchmarks

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

Reviews

See the guideline for information on the review process.

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

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

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #34242 (Prepare string and net utils for future HTTP operations by pinheadmz)
  • #32061 (Replace libevent with our own HTTP and socket-handling implementation by pinheadmz)

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.

@sedited
Copy link
Contributor

sedited commented Dec 27, 2025

Concept ACK

@pinheadmz
Copy link
Member

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

@fjahr fjahr force-pushed the 2025-12-torcontrol-take-3 branch from 3ec2c0e to a830323 Compare December 27, 2025 15:31
@DrahtBot
Copy link
Contributor

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

Hints

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.

@fjahr
Copy link
Contributor Author

fjahr commented Dec 27, 2025

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.

@pablomartin4btc
Copy link
Member

Concept ACK

@waketraindev
Copy link
Contributor

waketraindev commented Dec 28, 2025

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.

Copy link
Contributor

@janb84 janb84 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK 8d6f1b0

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.

@fjahr fjahr force-pushed the 2025-12-torcontrol-take-3 branch 2 times, most recently from 67f9f14 to 4966648 Compare December 28, 2025 22:01
@DrahtBot
Copy link
Contributor

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

Hints

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.

@fjahr
Copy link
Contributor Author

fjahr commented Dec 28, 2025

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.

@waketraindev
Copy link
Contributor

waketraindev commented Dec 28, 2025

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

Matthew Zipkin and others added 2 commits December 31, 2025 18:05
This is a helper struct to parse HTTP messages from data in buffers
from sockets. HTTP messages begin with headers which are
CRLF-terminated lines (\n or \r\n) followed by an arbitrary amount of
body data. Whitespace is trimmed from the field lines but not the body.

https://httpwg.org/specs/rfc9110.html#rfc.section.5
@fjahr fjahr force-pushed the 2025-12-torcontrol-take-3 branch from 4966648 to 122ff0a Compare December 31, 2025 17:05
@fjahr
Copy link
Contributor Author

fjahr commented Dec 31, 2025

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.

@DrahtBot
Copy link
Contributor

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

Hints

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.

Copy link
Member

@pinheadmz pinheadmz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 3de90a1

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 ;-).

Show Signature
-----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

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

*
*/
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)
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3a1f8c0

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 191 to 194
if (m_recv_buffer.size() > MAX_LINE_LENGTH) {
LogWarning("tor: Disconnecting because MAX_LINE_LENGTH exceeded");
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c675b6f

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +196 to +195
// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c675b6f

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@fjahr fjahr force-pushed the 2025-12-torcontrol-take-3 branch from 3de90a1 to d3ce644 Compare January 17, 2026 14:25
@fjahr fjahr force-pushed the 2025-12-torcontrol-take-3 branch from d3ce644 to 2808981 Compare January 17, 2026 14:36
Copy link
Contributor Author

@fjahr fjahr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +196 to +195
// 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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 191 to 194
if (m_recv_buffer.size() > MAX_LINE_LENGTH) {
LogWarning("tor: Disconnecting because MAX_LINE_LENGTH exceeded");
return false;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

*
*/
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)
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants