The PR branch HEAD was 8a18a12a232 at the time of this review club meeting.
Notes
Currently Bitcoin Core code always runs within a single operating system
process. So if you are
running bitcoind, then you are running node and wallet code together in the
same process. If you are running bitcoin-qt, then you are running node,
wallet, and GUI code in the same process. Running all code in one process does
not provide a lot of isolation, so it means a crash in wallet code could bring
down the node, or a vulnerablility in the node could expose wallet data.
Additionally it’s not possible to run the node and wallet on different machines,
or to stop and start the GUI independently of the node, or use the GUI to
connect to nodes or wallets on headless machines.
PR #10102 starts to move away
from the single process model by adding basic support needed for bitcoin node,
wallet, and GUI code to run in different processes and communicate with each
other internally. Followups
#19460 and
#19461 expand on it to allow
wallet and GUI processes to be started and stopped independently, and allow the
processes to communicate with each other externally, and run on different
machines.
PR #10102, combined with the
--enable-multiprocess build option, builds a new bitcoin-node executable
that can be used as a drop-in replacement for bitcoind, and a new
bitcoin-gui executable that can be used a drop-in replacement of bitcoin-qt.
The new executables are used the same way as the previous ones and don’t provide
any new features externally, but internally they will spawn multiple processes
and use process separation. Instructions for building and testing #10102 can be
found in
doc/multiprocess.md
and can be done in a few commands on linux:
cd <BITCOIN_SOURCE_DIRECTORY>
make -C depends NO_QT=1 MULTIPROCESS=1
CONFIG_SITE=$PWD/depends/x86_64-pc-linux-gnu/share/config.site ./configure
make
src/bitcoin-node -regtest-printtoconsole-debug=ipc
When this is run, bitcoin-node will spawn a bitcoin-wallet executable to run
wallet code. The node and wallet processes will communicate across a socket,
with the node code controlling the wallet with
interfaces::WalletClient
methods, and the wallet code calling the node with
interfaces::Chain
methods. Similarly when GUI support is enabled, bitcoin-gui will spawn a
bitcoin-node process, and control it by calling
interfaces::Node
methods, and control wallets by calling
interface::Wallet
methods.
#10102 adds the plumbing that allows gui <-> node <-> wallet cross-process
communication to work transparently, without changing existing code, only
adding new code. This is possible because the interfaces in src/interfaces/
(interfaces::Chain, interfaces::Node, interfaces::Wallet described above)
are all abstract classes with virtual methods, so different implementations can
be substituted without changes to calling code. This PR adds new implementations
of each interface that forward method calls from one process to another. In
early versions of this PR, adding new implementations of each interface required
adding a lot of boilerplate code. Every method in every interface, and every
argument and return value of every method had to had custom C++ code written
that would handle the method call serialization and forwarding from the calling
process to the called process. For example, the
Wallet::encryptWallet
abstract method:
which is the same method that would have been called directly if code were
running in a single process instead of multiple processes.
Because the ipc::capnp::WalletImpl::encryptWallet and
ipc::capnp::WalletServer::encryptWallet methods above and all similar methods
just contain boilerplate code forwarding arguments and return values, newer
versions of this PR no longer define these methods manually, and instead
generate them automatically. (You can see these methods in generated
src/ipc/capnp/wallet.capnp.proxy-client.c++ and
src/ipc/capnp/wallet.capnp.proxy-server.c++ files after building this PR, but
they are not part of the PR source code). The code generation means that adding
a new method or changing an existing method signature now just requires editing
a single line in the interface’s
.capnp
file:
The most significant commits are the “Add canp serialization…” and “Add capnp
wrapper…” commits which add Cap’n Proto schema
definitions describing Bitcoin’s struct
and interface types that are shared between processes:
(common.capnp,
chain.capnp,
node.capnp,
wallet.capnp).
These commits also add a lot of glue code in BuildField / ReadField/
BuildMessage / ReadMessage function. There is a lot of ugly C++ template
syntax needed to declare the function types, but the actual function bodies are
pretty straightforward and just copy information between Bitcoin Core’s native
data structures and corresponding capnp messages.
The other two significant commits in this PR are the “Make bitcoin-gui spawn a
bitcoin-node process” commit and “Make bitcoin-node spawn a bitcoin-wallet
process” commit. These commits don’t add much new code. Instead they strip out
wallet initialiatation code from the BitcoinNodeInit class (so the
bitcoin-node process no longer has wallet functionality linked in) and strip
out node and wallet initialization code from the BitcoinGuiInit class (so the
bitcoin-gui class no longer has node and wallet functionality linked in. Then
they add spawnProcess calls in appropriate places to start making
cross-process calls. A good entry point into these two commits is to search for
the new spawnProcess calls they add.
The other commits in this PR are needed for completeness but aren’t related to
its core functionality. They are smaller and just update dependencies, logging,
and test code.
What are some disadvantages of separating bitcoin node, wallet, and GUI into separate processes?
What are some benefits?
Did you try building with --enable-multiprocess ? And did you try running with debug=1 to
view IPC logging?
Why is it important that Cap’n Proto types and libraries are only accessed in the
src/ipc/capnp/ directory, and not in any other public header files?
What is the difference between node and wallet running on different processes vs different
threads? Why must they communicate via an IPC framework instead of calling each other’s functions
normally?
Where are the spawnProcess calls added in the “Make bitcoin-gui spawn…” and “Make
bitcoin-node spawn…” commits? What type of interface pointer is requested from each of the
spawned processes?
<svav> One thing I would say is interfaces::WalletClient and interface::Wallet are very similarly named, so you don't get any hint of what is different about them.
<ryanofsky> Right WalletClient is the interface the bitcoin-node uses to control bitcoin-wallet, Walelt is the interface bitcoin-gui uses to control bitcoin-wallet
<michaelfolkson> Just wasn't clear to me initially whether this was a PR covering the whole project or a prototype or whether it was just one PR in a series
<ryanofsky> I wonder what a better name for WalletClient might be. Really it only exists for backwards compatability so bitcoin-node will continue to load wallets by default. More ideally bitcoin-wallet would just connect to node, I think
<ryanofsky> svav, do you think interfaces::Wallet is not a good name for the interface used to control a Wallet? It seems to me interfaces::Wallet is a good name, but only WalletClient is the bad name
<michaelfolkson> For what its worth I think the names are fine as well they are well documented (ideally both in the code and in the separate multiprocess doc)
<svav> Well, as a suggestion, you could have interfaces::WalletForNode and interface::WalletForGUI so they are differentiated. I'm just saying at the moment, they both seem very similar.
<lightlike> i'd say one disadvantage is that it's slower if things get serialized at one process, sent over a socket, deserialized by the another process
<lightlike> advantages: there could be alternative implementations for parts, such as GUI or wallet; separating code like this makes it easier to be sure that parts that should be independent really are
<ryanofsky> Yeah main thing I like about it is it forces codebase to be more modular, and can let you run a node continuosly and connect wallets and gui instances to it as needed
<michaelfolkson> I wasn't sure what I'd see. Like should the wallet only communicate the node when the wallet wants to make a transaction or wants to know its balance
<michaelfolkson> I guess in an idealized world there would be less cross talk and they would only talk when absolutely necessary or when they need something. But in reality we are dealing with Satoshi's inheritance so I'm not sure what the idealized state would be
<michaelfolkson> A oversimplification would be the wallet only hears from the node say every block? Or if the wallet wants to make a transaction or hasn't figured out the UTXOs it controls
<ryanofsky> michaelfolkson, very much agree. I think removing the most egregious polling is probably low hanging fruit for cleanup prs (independent of anything in 10102), and would also be good for improving gui performance
<michaelfolkson> And the node has to ask the wallet if it is interested in every transaction as the node doesn't know any prior keys, addresses. I guess that's a lot of chatter in itself
<ryanofsky> Q4 Why is it important that Cap'n Proto types and libraries are only accessed in the `src/ipc/capnp/` directory, and not in any other public header files?
<lightlike> 1) its better to keep it in one place so we can easily add alternatives to capnproto, 2) to be able to still build bitcoind without having capnproto installed
<ryanofsky> Yes to both. Goal is to make it possible to run without multiprocess code, and make the dependency confined and easier to replace if needed
<ryanofsky> Q5 What is the difference between node and wallet running on different processes vs different threads? Why must they communicate via an IPC framework instead of calling each other's functions normally?
<ryanofsky> Right I'm also assuming supporting multiprocess + single process with everything in one repo is not more work than just supporting multiprocess
<ryanofsky> lightlike, that would be a good experiment. I think there should be no change in performance really unless the wallet is somehow slowing down IBD
<michaelfolkson> svav: If users like the trade-offs offered by single process rather than multiprocess you shouldn't take it away (assuming the trade-offs are significant)
<ryanofsky> Q6 Where are the `spawnProcess` calls added in the "Make bitcoin-gui spawn..." and "Make bitcoin-node spawn..." commits? What type of interface pointer is requested from each of the spawned processes?
<ryanofsky> If you are running `bitcoind` executable `auto wallet_client = node.init->makeWalletClient(*node.chain);` is not null, so no need to call spawnProcess
<ryanofsky> The idea is bitcoind has wallet code linked in, doesn't need to spawn anything. bitcoin-node doesn't have wallet code linked in so needs to spawn a bitcoin-wallet process
<ryanofsky> Hours up but feel free to ask me any questions. I learned I need to actually do something about the WalletClient name (this complaint has been made before!)
<michaelfolkson> One final question. Could you re-introduce non security critical shared state between the wallet and node just for say addresses that the wallet is tracking? To reduce the chatter?
<lightlike> michaelfolkson: you mean as an alternative way enabled only for non-multiprocess builds? wouldn't otherwise the multiprocess build be impossible?
<lightlike> isn't one idea of the mulitprocess build that it could also apply in situations where sharing state in other ways would be physically impossible (like different processes running on different computers)?
<ryanofsky> Easiest way to implement shared state would be to have it reside inside the bitcoin-node process. Of course it would be technically possible to have it somewhere else too though
<ryanofsky> lightlike, I think I'm just talking about "shared state" at a high level, like what information each process knows about. Not shared memory literally
<ryanofsky> Yeah, I would avoid using shared memory / shared files by default unless there was some big advantage to using that because it adds many limitations
<michaelfolkson> The wallet can't do anything without the node so it is not like the wallet process can continue doing useful stuff if the node goes down. So the wallet giving the node a bit of extra information that isn't strictly node information could make sense