In the background to the private key import GUI and early misfeatures in the private key import GUI posts, I explained the first big feature I implemented for the Dogecoin core and one of the early pieces of feedback another contributor provided.
In short, I’d written the code in a way that meant the entire GUI looked like it was frozen if you were to import a private key and rescan the entire blockchain.
Okay, it did more than look frozen. It was frozen.
Don’t Defrost; Avoid the Freeze
The way to fix this is to let the GUI do its own work to display the dialog, validate its contents, take any GUI-specific actions on the buttons and other widgets, and then perform any wallet operations in a separate thread.
If you haven’t used threads before in programming, there’s a lot to learn, but the way to think about them is that they are separate parts of the same program that run independently of each other but sometimes share important pieces of memory. (If you have used threads before, it’s important to remember that sometimes it’s better to fib about what’s really happening because it makes a better story.)
In the Dogecoin core, a lot of things happen in different threads. Two good examples are processing network connections to other nodes (if you’re online) and managing the GUI (if you’re running the GUI). It’s all one program, but the core can send and receive information to and from other nodes while you’re scrolling through your transaction list. The network doesn’t have to stop while you open the debug window and you don’t have to wait for the GUI to refresh while you’re receiving a new transaction block.
Things Hold Things (and manage what they hold)
In addition to threads, you have to know a couple of things about classes and objects. A class describes some data and behavior that’s related, and an object is a concrete thing that holds that data and can perform that behavior. A class describes an object, and that object behaves as the class describes.
There are a lot of classes and objects in the Dogecoin core. There’s a class
that represents your wallet (public and private keys). When you’re running the
core, there’s a single object (usually called pwallet
). There’s just one, not
because you can’t have multiple wallets (though we won’t have multiple wallets
until the 1.21 release), but because we don’t want multiple different
representations of the same wallet, because then things could get out of sync
in really confusing ways.
Anyhow, the next important behavior of this PR happens in a new class I wrote
called
ImportKeysDialog
.
You don’t have to read C++ to follow along; the important thing to know is that
this bundle of code gathers up some data and behavior all together so that I
can refer to the thing as a whole. It knows some basic information about what
happened in the importkeys dialog in the GUI, so it can do the appropriate
things whenever a user clicks on a button (import, cancel, reset the dialog).
It also has a piece of data called a QThread
, which is what Qt uses to manage
threads.
With that thread, I can do lots of things, but I really only want to do two: kick off a new operation and wait for it to complete.
To make the GUI work well, I need this class. I don’t need this thread
in
most classes related to the GUI. Most dialogs and GUI operations don’t need
one, because they’re not doing anything behind the scenes that could take
minutes or hours to complete. This one does.
If you look at the implementation of the ImportKeysDialog class, you’ll see a lot of things you expect. There’s code to create the private key import dialog, destroy the dialog (more on that in a moment), do the appropriate thing depending on the button clicked, et cetera.
The real work of this work gets done in the importKey
method on line 84 in
the linked PR. There are a couple of interesting blocks that verify that the
private key is a valid private
key.
Can You Keep a Secret?
Take special note of the calls to resetDialogValues()
(line 90) and
vchSecret.SetString("")
(lines 95 and 102). When Patrick wrote the original
ticket for Operation Such Frensly, he explicitly called out the need to be very
secure with this information. If this code isn’t written very carefully, the
user’s private key could hang around in memory somewhere in Qt or the validator
code for a long time, especially if this dialog stays open until the blockchain
scan finishes.
The secure way to manage this is to overwrite the memory used to store the private key and pass it around as soon as possible, so it’s in memory only as long as necessary.
That’s what this code does.
Let’s Import and Scan!
Line 124 actually adds the key, or at least attempts to. (Things can fail in a lot of ways. It’s important to write code to handle every failure case you can think of.)
If that works, then it’s time to consider whether to rescan the block chain.
Lines 132 through
148
do a lot, if the user requested a rescan. They create a new instance of a
private class called ImportKeyExecutor
, move it to a new thread independent
of the current GUI thread, and connect that class to some important actions,
like stopping work in the thread and destroying the object. All of this setup
lets line 147 do a little Qt magic to start rescanning the given wallet from
the Dogecoin Genesis block.
All This Indirection, and For What?
If it seems like everything is jumping around, welcome to threaded GUI coding!
You might want to read up on Signals and Slots in Qt, but that’s not essential to understand everything that’s happening here.
At this point, the user has provided a valid private key in the import dialog, checked the Rescan checkbox, and hit the Import button. The key is valid and gets inserted into the wallet successfully, and all of this complex work behind the scenes has…
… popped up a new dialog that says “Rescanning…”. That dialog will hang out
for a while while this tiny little ImportKeyExecutor
class does one thing in
its rescan()
method:
it calls a method on the wallet object called ScanForWalletTransactions
and
then quits its thread.
Okay, it also writes a couple of things to the debug log too.
That’s it.
All of this work to be able to run a couple of methods on a wallet (remember
pwallet
?).
The effect, however, is that there’s visible behavior now. There’s a modal dialog saying “Rescanning…” and it doesn’t immediately look like the program has frozen. It’ll take forever, but it looks like something is actually happening, because something is actually happening.
If someone were to come along later and make improvements to the code, there are a few… but that’s a subject for one more post.