The Bitcoin node’s wallet’s code
Here are some notes on aspects of the wallet included with the Bitcoin node. It is very possible there are errors or mistaken interpretations of the source code here. I started the day spring cleaning but somehow have ended up on the couch writing this.
Edit 2020/10/06: Added detail on notifications.
One or more wallets are loaded when the node is started using the
-wallet <wallet-name> command-line argument. Multiple wallets can be loaded at the same time by providing multiple instances of this command-line argument.
Wallets are loaded in a locked mode where the wallet password has not been provided, and no encrypted data like private keys can be accessed. This means that the JSON-RPC API calls will error for wallet-related calls with errors relating to this. The
walletpassphrase JSON-RPC call can be used to provide the wallet passphrase to the node and “unlock” it with a provided time period after which the passphrase will be forgotten and the wallet re-locked.
Wallets are accessed using wallet-related JSON-RPC calls, and the name of the desired wallet is specified in the URL following the leading
A wallet maintains two keypools, one is the internal keypool and the other is the external keypool. These are populated with unused keys and by default will have 1000 keys in each as defined in the source code by
DEFAULT_KEYPOOL_SIZE although the user can override it with a custom value by specifying
-keypool <count>on startup.
keypoolrefill JSON-RPC call can be used to forceably refill the keypool or if a new count is provided, allocate a higher amount. In general an unlocked wallet should automatically ensure that it’s keypools are refilled, but it will not do this if the wallet is locked.
In theory a wallet can derive keys from the master public key without the password and only needs to obtain the passphrase when it needs to access or derive keys from the master private key. In the case of the node wallet it stores the processed passphrase and uses that to decrypt master keys on demand and then to derive keys from them.
The key pool derives keys from the master key with a derivation path of
m/<account>'/<is change>'/n’ where
n is the index of the derived child key.
<account> is always
0. It uses an
<is change> value of
1 for internal keys, and
0 for external keys. This supports the assumption that it uses a separate keypool for each of what are conventionally referred to as receiving and change “addresses”.
A wallet that has not had a passphrase set is unencrypted. It can be encrypted using the
encryptwallet JSON-RPC call. All values like master private keys are then stored encoded with the passphrase. When the user provides the passphrase with the
walletpassphrase JSON-RPC call, the given passphrase is used to decode all these values ephemerally and they must all validate as decodable correctly. These are then while the wallet is unlocked decrypted on demand.
In the “full node” world of Bitcoin Core where blocks are small and everyone receives blocks to process, the concept of a watch-only wallet exists. This works by knowing the public key usage to watch out for and looking for them in a block. In a blockchain that actually gets used where the blocks become larger and larger, this is impractical for many reasons and not just the one alluded to here.
The node allows the importing of addresses and will watch for them; the
importaddress JSON-RPC call can be used for this. Interestingly it supports both importing actual P2PKH and P2SH addresses and literal output scripts provided in hex form. It will presumably obtain occurrences of these and include them in the wallet history and balances, and as new transactions are detected alter those accordingly.
While the wallet will provide public keys that are watch-only on request, the situations where this allows a locked wallet to do anything meaningful is at this time unknown.
When something needs a key, it requests either an external or internal one. This marks the key as reserved in memory, and prevents it being dispensed to another caller. It is assumed that whatever successfully employs that key results in the key being marked as used, and fully removed from the relevant key pool.
It is worth noting that the node wallet is integrated with the node. This means that it has the benefit of having the full mempool and having at least had all of the blocks locally, and being directly connected to the P2P network. A loaded wallet receives these node-sourced notifications:
These give a view of all the transactions related to the wallet. For the most part as long as the wallet is open and the node is connected to the P2P network, this should ensure that it has the full history of all key/address/script usage. The
-rescan command-line argument can be used to prompt a rescan, but cannot guarantee success in the case of a pruned node.
When importing new sources of keys, like public keys, scripts or addresses, the JSON-RPC the user can specifies (although it is optional and defaults to
true) if the wallet should rescan the blockchain for usage of these keys. This will refuse to do the import if they enable this, and the node is pruned. The
importprunedfunds JSON-RPC call is provided so that the user can provide the supplementary data on spends and receipts that a pruned node cannot scan for.
In the Bitcoin node each address has an account name and a purpose associated with it in the address book. When an operation is performed, a lookup is made there for each given output script and the presence of the given account name (if provided) is used to filter the addresses. The used purposes appear to be
"receive"(set for every JSON-RPC requested address).
A default account was referred to by
"” and all accounts with
”*". An account is persisted with the last address that was reserved for it if there is one, this is what the
getaccountaddress JSON-RPC call returns.
Most if not all references to accounts are marked as deprecated. This is a concept that was going away in Bitcoin Core, and would possibly have been going away in Bitcoin SV if the node wallet itself wasn’t becoming unusable with increasing blockchain usage.
Listing the entries in an account uses an accounting entry system. This shows debits and credits to each account. It is possible to move a transaction between accounts and this would apply a debit to one account and a corresponding credit to another. Listing the entries in an account (or all accounts) is just a matter of iterating over these entries, which are pre-ordered.
These appear to have largely been for the benefit of user interface.
There is one option to provide external wallet related notifications, and it executes an external command when a wallet transaction changes. The user provides the path to a command with the
walletnotify command-line argument (e.g. guessing
--walletnotify="~/something %s") where the
%s is replaced by the transaction id.
Bitcoin Core has since the date of forking been updated with additional replacements:
%w: The wallet name (but not on windows due to lack of shell escaping support).
%b: The hex block hash if confirmed or
%h: The block height if confirmed or
The Bitcoin SV inherited notifications:
- The transaction is added to the wallet.
- The transaction has a valid block hash already and it is changing to something else.
- The transaction was flagged as abandoned but has been added (was broadcast and entered the mempool?).
- The transaction index value changes, -1 means it is conflicted and a non-zero block hash is the earliest block it is conflicted with. It also defaults to -1, so… When the merkle proof is set on the transaction, the index value is set to the index of the transaction in the block (used for merkle proof verification).
The Bitcoin Core updated notification cases:
- The transaction is added to the wallet.
- The transaction state “index”(?) changes from one value to another.
- In mempool (State of transaction added to mempool).
- Confirmed (State of transaction confirmed in a block).
- Conflicted (State of rejected transaction that conflicts with a confirmed block).
- Inactive (State of transaction not confirmed or conflicting with a known block and not in the mempool. May conflict with the mempool, or with an unknown block, or be abandoned, never broadcast, or rejected from the mempool for another reason).
- Inactive (abandoned).