-
Notifications
You must be signed in to change notification settings - Fork 36.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Can create External HD wallet with -externalhd #9728
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -95,79 +95,116 @@ CPubKey CWallet::GenerateNewKey(CWalletDB &walletdb, bool internal) | |
bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY); // default to compressed public keys if we want 0.6.0 wallets | ||
|
||
CKey secret; | ||
|
||
CPubKey pubkey; | ||
// Create new metadata | ||
int64_t nCreationTime = GetTime(); | ||
CKeyMetadata metadata(nCreationTime); | ||
|
||
// use HD key derivation if HD was enabled during wallet creation | ||
if (IsHDEnabled()) { | ||
DeriveNewChildKey(walletdb, metadata, secret, (CanSupportFeature(FEATURE_HD_SPLIT) ? internal : false)); | ||
DeriveNewChildKey(walletdb, metadata, secret, pubkey, (CanSupportFeature(FEATURE_HD_SPLIT) ? internal : false)); | ||
} else { | ||
secret.MakeNewKey(fCompressed); | ||
pubkey = secret.GetPubKey(); | ||
assert(secret.VerifyPubKey(pubkey)); | ||
} | ||
|
||
// Compressed public keys were introduced in version 0.6.0 | ||
if (fCompressed) { | ||
SetMinVersion(FEATURE_COMPRPUBKEY); | ||
} | ||
|
||
CPubKey pubkey = secret.GetPubKey(); | ||
assert(secret.VerifyPubKey(pubkey)); | ||
|
||
mapKeyMetadata[pubkey.GetID()] = metadata; | ||
UpdateTimeFirstKey(nCreationTime); | ||
|
||
if (!AddKeyPubKeyWithDB(walletdb, secret, pubkey)) { | ||
throw std::runtime_error(std::string(__func__) + ": AddKey failed"); | ||
if(!IsExternalHD()) { | ||
if (!AddKeyPubKeyWithDB(walletdb, secret, pubkey)) { | ||
throw std::runtime_error(std::string(__func__) + ": AddKey failed"); | ||
} | ||
} | ||
else { | ||
if (!AddWatchOnly(pubkey, metadata, nCreationTime)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: brackets |
||
throw std::runtime_error(std::string(__func__) + ": AddWatchOnly failed"); | ||
} | ||
return pubkey; | ||
} | ||
|
||
void CWallet::DeriveNewChildKey(CWalletDB &walletdb, CKeyMetadata& metadata, CKey& secret, bool internal) | ||
|
||
void CWallet::DeriveNewChildKey(CWalletDB &walletdb, CKeyMetadata& metadata, CKey& secret, CPubKey& pubkey, bool internal) | ||
{ | ||
// for now we use a fixed keypath scheme of m/0'/0'/k | ||
CKey key; //master key seed (256bit) | ||
CExtKey masterKey; //hd master key | ||
CExtKey accountKey; //key at m/0' | ||
CExtKey chainChildKey; //key at m/0'/0' (external) or m/0'/1' (internal) | ||
CExtKey childKey; //key at m/0'/0'/<n>' | ||
|
||
// try to get the master key | ||
if (!GetKey(hdChain.masterKeyID, key)) | ||
if (GetKey(hdChain.masterKeyID, key)) | ||
{ | ||
// for now we use a fixed keypath scheme of m/0'/0'/k | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems like you are repeating yourself a little bit here. Perhaps a common function can be extracted to avoid some code duplication between this if and the next one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to think about this, but this was not as easy. I preferred keeping this way as it makes review easier: I did not touch what was already working. |
||
CExtKey masterKey; //hd master key | ||
CExtKey accountKey; //key at m/0' | ||
CExtKey chainChildKey; //key at m/0'/0' (external) or m/0'/1' (internal) | ||
CExtKey childKey; //key at m/0'/0'/<n>' | ||
|
||
masterKey.SetMaster(key.begin(), key.size()); | ||
|
||
// derive m/0' | ||
// use hardened derivation (child keys >= 0x80000000 are hardened after bip32) | ||
masterKey.Derive(accountKey, BIP32_HARDENED_KEY_LIMIT); | ||
|
||
// derive m/0'/0' (external chain) OR m/0'/1' (internal chain) | ||
assert(internal ? CanSupportFeature(FEATURE_HD_SPLIT) : true); | ||
accountKey.Derive(chainChildKey, BIP32_HARDENED_KEY_LIMIT + (internal ? 1 : 0)); | ||
|
||
// derive child key at next index, skip keys already known to the wallet | ||
do { | ||
// always derive hardened keys | ||
// childIndex | BIP32_HARDENED_KEY_LIMIT = derive childIndex in hardened child-index-range | ||
// example: 1 | BIP32_HARDENED_KEY_LIMIT == 0x80000001 == 2147483649 | ||
if (internal) { | ||
chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT); | ||
metadata.hdKeypath = "m/0'/1'/" + std::to_string(hdChain.nInternalChainCounter) + "'"; | ||
hdChain.nInternalChainCounter++; | ||
} | ||
else { | ||
chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT); | ||
metadata.hdKeypath = "m/0'/0'/" + std::to_string(hdChain.nExternalChainCounter) + "'"; | ||
hdChain.nExternalChainCounter++; | ||
} | ||
} while (HaveKey(childKey.key.GetPubKey().GetID())); | ||
secret = childKey.key; | ||
pubkey = childKey.key.GetPubKey(); | ||
metadata.hdMasterKeyID = hdChain.masterKeyID; | ||
// update the chain model in the database | ||
if (!walletdb.WriteHDChain(hdChain)) | ||
throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed"); | ||
} | ||
else if (IsExternalHD()) { | ||
CExtPubKey& masterKey = hdChain.externalHD; //hd master key | ||
CExtPubKey chainChildKey; //key at m/0 (external) or m/1 (internal) | ||
CExtPubKey childKey; //key at m/0/<n> | ||
|
||
// derive m/x | ||
masterKey.Derive(chainChildKey, internal ? 1 : 0); | ||
|
||
// derive child key at next index, skip keys already known to the wallet | ||
do { | ||
if (internal) { | ||
chainChildKey.Derive(childKey, hdChain.nInternalChainCounter); | ||
metadata.hdKeypath = "m/1/" + std::to_string(hdChain.nInternalChainCounter); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for other reviewers: the account key level is dropped here because it's hardened in BIP44. Maybe a source code comment would be good. |
||
hdChain.nInternalChainCounter++; | ||
} | ||
else { | ||
chainChildKey.Derive(childKey, hdChain.nExternalChainCounter); | ||
metadata.hdKeypath = "m/0/" + std::to_string(hdChain.nExternalChainCounter); | ||
hdChain.nExternalChainCounter++; | ||
} | ||
metadata.hdMasterKeyID = hdChain.masterKeyID; | ||
} while (HaveWatchOnly(childKey.pubkey.GetID())); | ||
pubkey = childKey.pubkey; | ||
// update the chain model in the database | ||
if (!walletdb.WriteHDChain(hdChain)) | ||
throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed"); | ||
} | ||
else { | ||
throw std::runtime_error(std::string(__func__) + ": Master key not found"); | ||
|
||
masterKey.SetMaster(key.begin(), key.size()); | ||
|
||
// derive m/0' | ||
// use hardened derivation (child keys >= 0x80000000 are hardened after bip32) | ||
masterKey.Derive(accountKey, BIP32_HARDENED_KEY_LIMIT); | ||
|
||
// derive m/0'/0' (external chain) OR m/0'/1' (internal chain) | ||
assert(internal ? CanSupportFeature(FEATURE_HD_SPLIT) : true); | ||
accountKey.Derive(chainChildKey, BIP32_HARDENED_KEY_LIMIT+(internal ? 1 : 0)); | ||
|
||
// derive child key at next index, skip keys already known to the wallet | ||
do { | ||
// always derive hardened keys | ||
// childIndex | BIP32_HARDENED_KEY_LIMIT = derive childIndex in hardened child-index-range | ||
// example: 1 | BIP32_HARDENED_KEY_LIMIT == 0x80000001 == 2147483649 | ||
if (internal) { | ||
chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT); | ||
metadata.hdKeypath = "m/0'/1'/" + std::to_string(hdChain.nInternalChainCounter) + "'"; | ||
hdChain.nInternalChainCounter++; | ||
} | ||
else { | ||
chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT); | ||
metadata.hdKeypath = "m/0'/0'/" + std::to_string(hdChain.nExternalChainCounter) + "'"; | ||
hdChain.nExternalChainCounter++; | ||
} | ||
} while (HaveKey(childKey.key.GetPubKey().GetID())); | ||
secret = childKey.key; | ||
metadata.hdMasterKeyID = hdChain.masterKeyID; | ||
// update the chain model in the database | ||
if (!walletdb.WriteHDChain(hdChain)) | ||
throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed"); | ||
} | ||
} | ||
|
||
bool CWallet::AddKeyPubKeyWithDB(CWalletDB &walletdb, const CKey& secret, const CPubKey &pubkey) | ||
|
@@ -298,6 +335,29 @@ bool CWallet::AddWatchOnly(const CScript& dest, int64_t nCreateTime) | |
return AddWatchOnly(dest); | ||
} | ||
|
||
bool CWallet::AddWatchOnly(const CPubKey &pubkey, const CKeyMetadata& meta, int64_t nCreateTime) | ||
{ | ||
auto script = GetScriptForDestination(pubkey.GetID()); | ||
if (!HaveWatchOnly(script)) | ||
{ | ||
mapKeyMetadata[CScriptID(script)] = meta; | ||
if (!AddWatchOnly(script, nCreateTime)) | ||
return false; | ||
} | ||
|
||
if (!CWalletDB(*dbw).WriteKeyMeta(pubkey, meta)) | ||
return false; | ||
|
||
script = GetScriptForRawPubKey(pubkey); | ||
if (!HaveWatchOnly(script)) | ||
{ | ||
mapKeyMetadata[CScriptID(script)] = meta; | ||
if (!AddWatchOnly(script, nCreateTime)) | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
bool CWallet::RemoveWatchOnly(const CScript &dest) | ||
{ | ||
AssertLockHeld(cs_wallet); | ||
|
@@ -1393,6 +1453,25 @@ bool CWallet::SetHDMasterKey(const CPubKey& pubkey) | |
return true; | ||
} | ||
|
||
bool CWallet::SetExternalHD(const CExtPubKey& extPubKey) | ||
{ | ||
LOCK(cs_wallet); | ||
|
||
// ensure this wallet.dat can only be opened by clients supporting HD | ||
SetMinVersion(FEATURE_EXTERNAL_HD); | ||
|
||
// store the keyid (hash160) together with | ||
// the child index counter in the database | ||
// as a hdchain object | ||
CHDChain newHdChain; | ||
newHdChain.masterKeyID = extPubKey.pubkey.GetID(); | ||
newHdChain.isExternalHD = true; | ||
newHdChain.externalHD = extPubKey; | ||
SetHDChain(newHdChain, false); | ||
|
||
return true; | ||
} | ||
|
||
bool CWallet::SetHDChain(const CHDChain& chain, bool memonly) | ||
{ | ||
LOCK(cs_wallet); | ||
|
@@ -1408,6 +1487,11 @@ bool CWallet::IsHDEnabled() const | |
return !hdChain.masterKeyID.IsNull(); | ||
} | ||
|
||
bool CWallet::IsExternalHD() const | ||
{ | ||
return IsHDEnabled() && hdChain.isExternalHD; | ||
} | ||
|
||
int64_t CWalletTx::GetTxTime() const | ||
{ | ||
int64_t n = nTimeSmart; | ||
|
@@ -1848,8 +1932,18 @@ bool CWalletTx::IsTrusted() const | |
if (parent == NULL) | ||
return false; | ||
const CTxOut& parentOut = parent->tx->vout[txin.prevout.n]; | ||
if (pwallet->IsMine(parentOut) != ISMINE_SPENDABLE) | ||
return false; | ||
const auto& isMine = pwallet->IsMine(parentOut); | ||
if (isMine != ISMINE_SPENDABLE) | ||
{ | ||
// If the wallet is external HD, check if it is a key we generated | ||
if (!pwallet->IsExternalHD() || isMine != ISMINE_WATCH_SOLVABLE) | ||
return false; | ||
const auto& meta = pwallet->mapKeyMetadata; | ||
auto it = meta.find(CScriptID(parentOut.scriptPubKey)); | ||
if (it == meta.end() || | ||
it->second.hdKeypath.empty()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we may need to generalize this check a bit more. If the script here is a multisig in which the watch-only wallet owns all the keys, this will return false since there is no hdKeypath. Practically this means that spending 0-conf p2sh funds is a dicey experience that results in "Insufficient funds" responses sometimes. Perhaps something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're not even required to be backwards compatible, I think adding a new piece of key metadata There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you have the private keys, yes, because of check a few lines above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So your goal is that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we completely those addresses via hdwatchonly keys and/or privkeys, yes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently in this mode you can import a privkey and spend unconfirmed outputs. Likewise you can spend unconfirmed hdwatchonly outputs. You cannot however spend a mixture of the two in a multisig, or pure hdwatchonly p2sh. |
||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
@@ -3290,9 +3384,14 @@ void CWallet::ReserveKeyFromKeyPool(int64_t& nIndex, CKeyPool& keypool, bool fRe | |
if (!walletdb.ReadPool(nIndex, keypool)) { | ||
throw std::runtime_error(std::string(__func__) + ": read failed"); | ||
} | ||
if (!HaveKey(keypool.vchPubKey.GetID())) { | ||
if (!IsExternalHD() && !HaveKey(keypool.vchPubKey.GetID())) { | ||
throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); | ||
} | ||
if (IsExternalHD()) { | ||
if (!HaveWatchOnly(keypool.vchPubKey.GetID())) { | ||
throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); | ||
} | ||
} | ||
if (keypool.fInternal != fReturningInternal) { | ||
throw std::runtime_error(std::string(__func__) + ": keypool entry misclassified"); | ||
} | ||
|
@@ -3855,6 +3954,7 @@ std::string CWallet::GetWalletHelpString(bool showDebug) | |
strUsage += HelpMessageOpt("-spendzeroconfchange", strprintf(_("Spend unconfirmed change when sending transactions (default: %u)"), DEFAULT_SPEND_ZEROCONF_CHANGE)); | ||
strUsage += HelpMessageOpt("-txconfirmtarget=<n>", strprintf(_("If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: %u)"), DEFAULT_TX_CONFIRM_TARGET)); | ||
strUsage += HelpMessageOpt("-usehd", _("Use hierarchical deterministic key generation (HD) after BIP32. Only has effect during wallet creation/first start") + " " + strprintf(_("(default: %u)"), DEFAULT_USE_HD_WALLET)); | ||
strUsage += HelpMessageOpt("-externalhd", _("Create a new external-HD wallet from a BIP32 HD public key")); | ||
strUsage += HelpMessageOpt("-walletrbf", strprintf(_("Send transactions with full-RBF opt-in enabled (default: %u)"), DEFAULT_WALLET_RBF)); | ||
strUsage += HelpMessageOpt("-upgradewallet", _("Upgrade wallet to latest format on startup")); | ||
strUsage += HelpMessageOpt("-wallet=<file>", _("Specify wallet file (within data directory)") + " " + strprintf(_("(default: %s)"), DEFAULT_WALLET_DAT)); | ||
|
@@ -3949,6 +4049,26 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile) | |
walletInstance->SetMaxVersion(nMaxVersion); | ||
} | ||
|
||
|
||
std::string externalHd = GetArg("-externalhd", ""); | ||
CExtPubKey extPubKey; | ||
if (!externalHd.empty()) { | ||
CBitcoinExtPubKey bitcoinExtPubKey(externalHd); | ||
extPubKey = bitcoinExtPubKey.GetKey(); | ||
if (!extPubKey.pubkey.IsFullyValid()) { | ||
InitError(_("Invalid ExtPubKey format")); | ||
return NULL; | ||
} | ||
|
||
if (!fFirstRun) { | ||
if (!walletInstance->IsExternalHD() || | ||
walletInstance->GetHDChain().masterKeyID != extPubKey.pubkey.GetID()) { | ||
InitError(_("Cannot specify new external hd on an already existing wallet")); | ||
return NULL; | ||
} | ||
} | ||
} | ||
|
||
if (fFirstRun) | ||
{ | ||
// Create new keyUser and set as default key | ||
|
@@ -3958,9 +4078,15 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile) | |
walletInstance->SetMinVersion(FEATURE_HD_SPLIT); | ||
|
||
// generate a new master key | ||
CPubKey masterPubKey = walletInstance->GenerateNewHDMasterKey(); | ||
if (!walletInstance->SetHDMasterKey(masterPubKey)) | ||
throw std::runtime_error(std::string(__func__) + ": Storing master key failed"); | ||
if(externalHd.empty()) { | ||
CPubKey masterPubKey = walletInstance->GenerateNewHDMasterKey(); | ||
if (!walletInstance->SetHDMasterKey(masterPubKey)) | ||
throw std::runtime_error(std::string(__func__) + ": Storing master key failed"); | ||
} | ||
else { | ||
if (!walletInstance->SetExternalHD(extPubKey)) | ||
throw std::runtime_error(std::string(__func__) + ": Storing master key failed"); | ||
} | ||
} | ||
CPubKey newDefaultKey; | ||
if (walletInstance->GetKeyFromPool(newDefaultKey, false)) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing two whitespaces before
(string)
.