Design of deterministic wallets support --------------------------------------- Goals: - Wallets to derive new keys deterministically using the BIP32 algorithm. - Seamless/silent upgrade of old wallets to use the new scheme - Support watching a key tree without knowing the private key - Integrate with existing features like key rotation, encryption and bloom filtering - Clean up and reduce the size of the Wallet class - Continue to allow import and usage of arbitrary external keys - Make it easier to support external HD wallet devices like Trezor Non-goals: - Expose multiple accounts or "wallets within a wallet" via the API. - Optional support for HD: all wallets after the change will be HD by default, even though they may have non-HD keys imported into them. - Actual support for hardware wallets API design ----------- Create a new KeyChain interface and provide BasicKeyChain, DeterministicKeyChain implementations. Wallets may contain multiple key chains. However only the last one is "active" in the sense that it will be used to create new keys. There's no way to change that. The Wallet class has most key handling code refactored out into KeyChainGroup, which handles multiplexing a BasicKeyChain (for random keys, if any), and zero or more DeterministicKeyChain. Wallet ends up just forwarding method calls to this class most of the time. Thus in this section where the Wallet API is discussed, it can be assumed that KeyChainGroup has the same API. Although individual key chain objects have their own locks and are expected to be thread safe, KeyChainGroup itself is not and is not exposed directly by Wallet: it's an implementation detail, and locked under the Wallet lock. The Wallet API changes to have an importKey method that works like addKey does today, and forwards to the BasicKeyChain. There's also a freshKey method that forwards to the active HD chain and requests a key for a specific purpose, specified by an enum parameter. The freshKey method supports requesting keys for the following purposes: - CHANGE - RECEIVE_FUNDS and may in future also have additional purposes like for micropayment channels, etc. These map to the notion of "accounts" as defined in the BIP32 spec, but otherwise should not be exposed in any user interfaces. freshKey is guaranteed to return a freshly generated key: it will not return the same key repeatedly. There is also a currentKey method that returns a stable key suitable for display in the user interface: it will be changed automatically when it's observed being used in a transaction. There can be multiple key chains. There is always: * 1 basic key chain, though it may be empty. * >=0 deterministic key chains Thus it's possible to have more than one deterministic key chain, but not more than one basic key chain. Multiple deterministic key chains become relevant when key rotation happens. Individual keys in a deterministic hierarchy do not rotate. Instead the rotation time is applied only to the seed. Either the whole key chain rotates or none of it does. This reflects the fact that a hierarchy only has one real secret and in the case of a wallet compromise, all keys derived from that secret must also be rotated. Multiple key chains within a wallet are NOT intended to support the following use cases: - Watching wallets - Hardware wallets From a user interface design perspective, it does not make much sense to have a wallet that can have multiple "subwallets", as it would rapidly get confusing especially when trying to spend money from multiple kinds of hierarchy at once. Key rotation is a special case because it is expected that (a) the private key material is always available on a rotating chain and (b) the only spends crafted for keys in these chains will be created internally by bitcoinj and thus there is virtually no added complexity to the API or UI to handle it. When chains are encrypted, they must all be encrypted under exactly the same key. This is also true of rotating chains. The API will not support the case of multiple keychains in the same wallet that have a different password, as this would make the API and wallet apps too confusing. For hardware/watching wallets, where the private key/seed material is not available, the correct approach is to create an entirely separate Wallet object with a single deterministic key chain that does not have access to the seed. Special support can be added to the wallet code later to handle "external signing" which would be a separate/new feature. External signing can be done today already of course by manually building a transaction and using hashForSignature, but it's less convenient than an integrated solution would be. Chain structure --------------- We follow the suggested chain structure outlined in BIP32 in which there is the notion of a top-level account, though in bitcoinj this will always be zero as it won't be exposed in the API for now. Under the account are two keys, one for receiving of funds (i.e. exposed to the use) and one which is used for "internal purposes", typically change keys though it may also be used for things like micropayment channels and, in future, de/refragmentation. In the most obvious implementation, a standard key chain would have all private or all pubkey only nodes. However, this will not be the case in bitcoinj. Instead private keys will largely be rederived on the fly, for a few reasons: 1) When a chain is encrypted, the private key/seed bytes are not available, yet the chain still needs to be extended on demand. In this sense an encrypted wallet is somewhat like a watching wallet. 2) Private keys can be derived from their parent extremely fast, as it merely involves a single bigint modular addition. 3) We would like to hold as many keys in RAM as possible, and so throwing away and rederiving private key bytes on the fly seems to be a good way to make some savings. This is important because RAM on Android devices can be extremely tight and there is no swap file on those platforms. When synchronizing, the system may need to extend the key hierarchy without access to the seed or master private keys, due to the need to calculate public keys in the gap. Because it would be complicated and confusing to have some user-exposed keys that have an encrypted private part and others that are missing it entirely, and because private key derivation is fast, we use the following arrangement for an encrypted deterministic key chain: - The root seed is encrypted. We keep it around even after the master private key is derived from it, because wallet apps may wish to show it to users so they can opt to write it down later. - The private keys at the top of the hierarchy M, M/0, M/0/0, M/0/1 are precalculated when the hierarchy is initialized and stored in the wallet encrypted. - Keys that hang off the external/internal parent keys only have their public keys stored. The private key is always derived on the fly. The ECKey API already allows an AES key/password to be specified when signing, and this operation will be extended to decrypting not the current key but the parent key and then rederiving. Bloom filtering --------------- Each key chain is responsible for implementing PeerFilterProvider, the wallet multiplexes all implementors together. The code that currently does this multiplexing (between Wallets) would be extracted and reused, resulting in what is internally a hierarchy of filter providers whose outputs are combined to result in a single Bloom filter and earliest key time, calculated by the PeerGroup. When a wallet is informed about a transaction, it compiles a list of what pubkeys were seen and sends those lists to each key chain. The basic key chain ignores this and does nothing. The deterministic key chain examines each key to see if it's within the "gap", which is defined as a set of keys that have been pre-generated but not yet used by the wallet for change or returned to the user. The purpose of the gap is to ensure that if a wallet is synchronized from an old backup, keys that were vended by the wallet "in the future" are recognized and added to the Bloom filters. The gap must be sized so that there's no chance that more than that number of keys will be consumed in a single block (or more accurately, in a single getdata run). For version one of this system, the gap will be manually sized to be appropriate for desktop wallets in typical use cases: web servers that are tracking very high traffic addresses might be at risk of exhausting the gap, and that would be treated as a fatal error. If the gap is exhausted before the key chain is asked to recalculate a new Bloom filter, an exception is sent to the user-provided exception handler, which wallets should be treated as fatal and cause a crash (the user would not be able to sync beyond that point and would need help). In future, the gap should be resized on the fly and blocks re-downloaded if traffic seems to be higher than expected, but that's more complex and can be handled by future work. When the deterministic key chain is informed that a key has been observed, it finds its offset in the gap list, and then extends the gap by that amount of keys. Thus if a key is seen that is 10 keys in the future, the gap would be extended by 10 keys, where "extended" means the BIP32 algorithm is run to derive more keys and they would become persisted to disk/held in memory. Extension does not automatically result in recalculation of the bloom filter (see above), rather the PeerGroup will be changed to re-request filters from all wallets every time a getdata request is made. Efficiency considerations ------------------------- Elliptic curve maths can be slow, especially on cheap phones that run Android Gingerbread, which has only a basic tracing JIT compiler. Thus, we try to avoid it as much as possible. Public keys that are derived from the root seed are stored in memory and on disk. They are not recalculated at any point. Extending the tree takes place only when the gap gets too small or the API user requests it. It may or may not be useful to use a thread pool for this, experimentation would be required to determine this. To reduce memory consumption and disk usage, and because private key derivation only requires hashing, we don't store the private keys in memory except for the top levels. Instead leaf private keys are recalculated on demand. BIP70 allows for requests to expire. We should use this feature to keep memory in check for HD wallets when generating new keys. Unfortunately, for non BIP70 payments it's harder to do: we can't know when an address will stop being reused so we must keep it in RAM forever. Encryption ---------- bitcoinj allows private keys to be encrypted under an AES key derived from a password using scrypt. We need to keep this function working for deterministic wallets. Also, to achieve the goal of silent/automatic upgrade, we need to perform the upgrade once the AES key is provided by the user and keep it synchronizable in the previous state until then. BasicKeyChain will also support encrypted keys (it's not really basic but rather "the functionality that was supported before"). Deterministic wallets must also support encryption, which requires encrypting both the seed value and any private keys derived from it. Because there are no seeds or private keys, watching/hardware wallets do not support encryption. An attempt to encrypt a wallet that contains both a basic key chain and a key hierarchy without a seed/private keys will throw an exception to avoid the case where a wallet is "half encrypted". Serialization ------------- The serialization format will be extended in the following manner: two new key types will be introduced, DETERMINISTIC_SEED and DETERMINISTIC_KEY. The wallet major version will be incremented so old apps will refuse to read wallets that use the new key types. A DETERMINISTIC_SEED entry is always followed by one or more DETERMINISTIC_KEY entries. However the inverse does not hold, in the case where a wallet is watching a key hierarchy. ORIGINAL/ENCRYPTED type keys (basic) always come first. A DETERMINISTIC_SEED is represented as a key with that type, with private key bytes set to the seed and the public key bytes missing. If the seed entry is encrypted, then private key bytes are also missing and the encrypted_private_key field is set, as for an encrypted regular/basic private key. The creation time is set appropriately. A DETERMINISTIC_KEY is represented in the same way as a regular/basic key, including encryption, but with a different type code. Additionally, a new DeterministicKey structure is added that contains the "chain code" and path. The chain code is opaque bytes that are used as part of the BIP32 derivation algorithm. Without the chain code, you cannot derive new keys. The path indicates a path through the key tree, rooted at the seed. It consists of a series of numbers that encode the index of each child as the tree is traversed downwards, with the high bit set to indicate public or private derivation (see the BIP32 spec for more information on this). Upgrade ------- HD wallets are strictly superior to old random wallets, thus by default all new wallets will be HD. The deterministic key chain will be created on demand by the KeyChainGroup, which allows the default parameters like lookahead size to be configured after construction of the wallet but before the DeterministicKeyChain is constructed. For old wallets that contain random keys, attempts to use any methods that rely on an HD chain being present will either automatically upgrade the wallet to HD, or if encrypted, throw an unchecked exception until the API user invokes an upgrade method that takes the users encryption key. The upgrade will select the oldest non-rotating private key, truncate it to 128 bits and use that as the seed for the new HD chain. We truncate and thus lose entropy because 128 bits is more than enough, and people like to write down their seeds on paper. 128 bit seeds using the BIP 39 mnemonic code specification yields 12 words, which is a convenient size. As part of migrating to deterministic wallets, if the wallet is encrypted the wallet author is expected to test after load whether the wallet needs an upgrade, and call the upgrade method explicitly with the password. Note that attempting to create a spend will fail if the wallet is not upgraded, because it will attempt to retrieve a change key which is done deterministically in the new version of the code: thus an non-upgraded wallet is not very useful for more than viewing (unless the API caller explicitly overrides the change address behaviour using the relevant field in SendRequest of course). Test plan --------- Basic: [ ] Create an empty wallet with wallet-tool. Dump: check is empty. [ ] Add a couple of keys. Dump: check keys were pre-genned and the correct addresses were returned. [ ] Send some money to the wallet. Check that it's found in the mempool correctly. [ ] Generate a block. Check that it's found in the block correctly. [ ] Send half back. Check that the change address is properly generated and the first key can be spent from. [ ] Send another half back. Check that the change output is being properly signed for and can be spent from. [ ] Encrypt the wallet. [ ] Attempt to decrypt with the wrong password. [ ] Send another half back. Check that we can send from an encrypted wallet. [ ] Receive another coin. Check we can receive to an encrypted wallet. [ ] Decrypt the wallet. Dump: check it's the same as the first wallet, modulo a few pregenned public keys. [ ] Send another half coin back. Check we can send from a decrypted wallet. [ ] Send another half coin back. Check we can send from the change in a decrypted wallet.