For years Bitcoin Core has shipped five separate user‑facing binaries.
The upcoming multiprocess work would add at least two more (bitcoin‑node,
bitcoin‑gui). Reviewers feared an explosion of filenames
and user confusion.
The PR introduces a single command‑line front‑end called bitcoin
that does no consensus or wallet work itself – it simply chooses and
exec()’s the appropriate helper binary:
bitcoin sub‑command
Traditional binary
Multiprocess binary (-m)
bitcoin gui …
bitcoin‑qt
bitcoin‑gui
bitcoin daemon …
bitcoind
bitcoin‑node
bitcoin rpc …
bitcoin‑cli -named …
bitcoin‑cli -named …
bitcoin wallet …
bitcoin‑wallet
bitcoin-wallet
bitcoin tx …
bitcoin‑tx
bitcoin-tx
The bitcoin wrapper therefore accomplishes the “side‑binaries + unified entry point”
idea discussed in issue #30983.
Windows: builds a quoted & escaped command line that
CommandLineToArgvW in the child process will parse identically to POSIX
argv rules.
Escaping rules follow the MSVCRT specification: backslashes are doubled only
when they precede a quote, and every internal quote is back‑slash‑escaped.
util::GetExePath() – attempts to resolve argv[0] into the executable
file path.
On Unix: uses either the literal argv[0] (if it contains a slash) or
searches each element of $PATH until a regular file is found.
On Windows: uses GetModuleFileNameW(nullptr, …).
Wrapper lookup logic (ExecCommand)
Determine the directory of the wrapper itself (resolves symlinks).
Try possible candidate paths for the target binary, in descending priority:
libexec dir – ${prefix}/libexec/<target> if wrapper is in ${prefix}/bin/
Windows installer “daemon” sub‑dir ${wrapper_dir}/daemon/<target>
Sibling – ${wrapper_dir}/<target>
Finally, rely on the system PATHonly if the wrapper itself was
invoked via PATH search (mitigates accidentally running an old system
bitcoind while testing a local build).
Call util::ExecVp() with each candidate, moving onto the next candidate
if it returns ENOENT (“No such file or directory”) and raising an exception
if a different error is returned or if there is no next candidate.
Build‑system & test changes
CMake option BUILD_BITCOIN_BIN (ON by default) builds/installs the
wrapper.
Functional test framework understands BITCOIN_CMD="bitcoin -m" so the
entire suite can be driven through the new CLI.
CI jobs for the multiprocess build now export that variable.
Static‑analysis suppression: the wrapper intentionally contains no FORTIFY
functions; security-check.py is taught to ignore it.
Documentation updates
Numerous docs now mention that bitcoin rpc, bitcoin daemon, etc. are
synonyms for the traditional commands, improving discoverability for new
users while remaining fully backwards‑compatible.
Review approach – did you test the wrapper? Did you try both
monolithic (bitcoin daemon) and multiprocess (bitcoin -m daemon)
modes? (requires -DENABLE_IPC=ON cmake option). Attempt to
run one of the strace or dtrace tracing commands suggested in bitcoin.cpp?
Any cross‑platform checks?
From issue #30983, four
packaging strategies were listed.
Which specific drawbacks of the “side‑binaries” approach does this PR
address?
In util::ExecVp() (Windows branch) why is a secondstd::vectorescaped_args needed instead of modifying argv in‑place?
Walk through the escaping algorithm in util::ExecVp for the argument
C:\Program Files\Bitcoin\bitcoin-qt.
What exact string is passed to _execvp()?
GetExePath() does not use readlink("/proc/self/exe") on Linux even
though it would be more direct. What advantages does the current
implementation have? What corner cases might it miss?
In ExecCommand, explain the purpose of the fallback_os_search Boolean.
Under what circumstances is it better to avoid letting the OS search for
the binary on the PATH?
The wrapper searches ${prefix}/libexeconly when it detects that it is
running from an installed bin/ directory. Why not always search
libexec?
The functional test layer now conditionally prepends bitcoin -m to every
command. How does this interact with backwards‑compatibility testing
where older releases are run in the same test suite?
The PR adds an exemption in security-check.py because the wrapper contains no
fortified glibc calls.
Why does it not contain them, and would adding a trivial printf to
bitcoin.cpp break reproducible builds under the current rules?
Discuss an alternative design: linking a static table of sub‑commands to
absolute paths at build time instead of computing them at run
time. What trade‑offs (deployment, relocatability, reproducibility)
influenced the chosen design?
Suppose a user installs only bitcoin (wrapper) and forgets to install
bitcoin-cli. Describe the failure mode when they run bitcoin rpc
getblockcount. Would it be better for the wrapper to pre‑check the
availability of the target binary?
(Forward‑looking) Once bitcoin-gui actually spawns bitcoin-node
automatically (after #10102
lands), what additional command‑line options or UX changes might the wrapper
need?
Typing bitcoin --version prints wrapper metadata, notbitcoind’s or bitcoin‑qt’s.
Is that the right UX?
Propose a mechanism for the wrapper to forward --version and --help to the underlying sub‑command when one is specified (e.g. bitcoin --version daemon).
The wrapper is agnostic to options such as -ipcbind passed down to bitcoin‑node.
Should the wrapper eventually enforce a policy (e.g. refuse to forward -ipcconnect unless -m is given)?
What might go wrong if a user mixes monolithic binaries with IPC flags?
BITCOIN_CMD="bitcoin -m" is parsed with shlex; spaces inside quotes are preserved.
Should the framework use an explicit list instead of shell parsing?
Would it ever make sense to ship only the wrapper in bin/ and relocate
all other executables to libexec/ to tidy PATH?
<ryanofsky> I'll squash the first two questions and ask if anybody looked at the code or tested the new command? What was your approach? Any feedback or questions you have before we begin?
<abubakarsadiq> Q: how will this wrapper approach fixes the ambiguity of the wallet command after https://github.com/bitcoin/bitcoin/pull/10102 what will happen to current bitcoin-wallet executable
<ryanofsky> abubakarsadiq, current code in #10102 just adds new IPC functionality to current executable. Different directions could be taken though. I don't think it shoudl matter too much to users if they are using the wrapper
<ryanofsky> i think it might be better to support `bitcoin grind` directly instead of requiring `bitcoin util grind` but that requires argsmanager changes
<ryanofsky> next question from the list is: 3. From issue #30983, four packaging strategies were listed. Which specific drawbacks of the “side‑binaries” approach does this PR address?
<abubakarsadiq> I think having multiple binaries in a single release. Instead of requiring users to call individual binaries, the binaries will be placed in the `libexec` directory and wrapped under the bitcoin command. users dont have to deal with the new binaries they should just know the commands
<ryanofsky> Another thing it provides is forward compatability. Wrapper lets us rename binaries, consolidate them, replace them without changing external interface.
<ryanofsky> 5. Walk through the escaping algorithm in util::ExecVp for the argument C:\Program Files\Bitcoin\bitcoin-qt. What exact string is passed to _execvp()?
<abubakarsadiq> 4. Any standalone quotes are escaped with a backslash.The resulting string passed to _execvp() is:"C:\\Program Files\\Bitcoin\\bitcoin-qt"
<ryanofsky> Yes that's close but backslashes only need to be escaped if followed by quotes. So in the example the only change made is to add quotes around the argument. Backslashes don't need to be escaped there
<ryanofsky> The only escaping this PR is doing is on windows where you have escape the argv[] array in a quirky way that the microsoft C runtime expects
<ryanofsky> 6. GetExePath() does not use readlink("/proc/self/exe") on Linux even though it would be more direct. What advantages does the current implementation have? What corner cases might it miss?
<ryanofsky> In ExecCommand, explain the purpose of the fallback_os_search Boolean. Under what circumstances is it better to avoid letting the OS search for the binary on the PATH?
<abubakarsadiq> When the wrapper executable is invoked using a specific path to prevent unintentionally use of a binary in PATH instead of the intended local binary.
<stickies-v> i think generally the wrapper and the individual binaries are all going to be shipped and compiled together, so searching locally is more robust?
<ryanofsky> I think when I first implemented it, I didn't want to rely on GetExePath working perfectly, and wanted to take advantage of OS native ability find binaries
<ryanofsky> but then Sjors pointed out searching PATH could be confusing for developers if they didnt' build everythign, so narrowed the use of PATH. but could make sense to drop it altogether
<stickies-v> not the same thing, but i stopped searching the system path with py-bitcoinkernel because it was leading to too much confusion and errors, so i allowed providing an explicit path env var instead which is hard to abuse
<ryanofsky> 8. The wrapper searches ${prefix}/libexec only when it detects that it is running from an installed bin/ directory. Why not always search libexec?
<ryanofsky> My answer to this was that wrapper should be conservative about what paths it tries to execute, and encourage standard PREFIX/{bin,libexec} layouts, not encourage packagers to create nonstandard layouts or work when binaries arranged in unexpected ways.
<ryanofsky> 9. The functional test layer now conditionally prepends bitcoin -m to every command. How does this interact with backwards‑compatibility testing where older releases are run in the same test suite?
<stickies-v> oh, is this because bitcoin core can also be shipped through package managers? because with our own build system we know it'll always be stored in the bin,libexec dirs?
<ryanofsky> stickies-v, yeah the code isn't just trying to be conservative abotu what it executes. It will execute binaries in paths explicitly listed on PATH and in places that seem to match expected layout, but avoid executing things in novel situations
<stickies-v> so on the one hand we need to be conservative in what we execute, but on the other hand there may be package managers that (for some reason, idk) are unable to store binaries in the {bin,libexec} dirs?
<ryanofsky> stickies-v, I don't think we need to be conservative, I just thought it would be a good starting point. I don't think package manager will need to choose divergent layouts probably, but we can find out and adapt
<hodlinator> So a test must explicitly call add_nodes() and pass in old versions, which will result in the old binaries being resolved.. and this resolution-logic will need to be updated to support the wrapper in the future.
<ryanofsky> Note: we won't have time to get to all questions listed so if any in particular someone wants to talk about (or some specific topic) feel free to suggest
<emzy> I'm in general concerned about searching for binaries to execute. There are many possible security problems. Better have it hardcoded to one path as libexec.
<ryanofsky> hodlinator, yes that sounds right. Test framework code right now just assumes all previous releases don't have a wrapper binary to call. So if we want to write new tests calling wrapper binaries in old releases something like that needs to change
<stickies-v> I was wondering why on windows we search the "daemon" dir - that's unrelated to our "bitcoin daemon" bin, right? Is this just a windows convention?
<ryanofsky> abubakarsadiq, yeah I think that's the general think that makes me not worried about executing binaries on the PATH. but maybe we could be conservative and never do that, it's reasonable thing to consider
<ryanofsky> 10. The PR adds an exemption in security-check.py because the wrapper contains no fortified glibc calls. Why does it not contain them, and would adding a trivial printf to bitcoin.cpp break reproducible builds under the current rules?
<ryanofsky> I think the answer adding is adding new calls should not break the build. If we add new calls they should be fortified based on build options.
<ryanofsky> Adding new calls should just allow the security-check.py exception to be removed. The exception is needed because it is checking for fortified symbols but the wrapper binary is so simple it doesn't contain any