從零開始學習比特幣開發:建立錢包重要方法之填充金鑰池
寫在前面
建立錢包和交易是比特幣最重要的兩方面,涉及到很多很多的內容,遠非一篇文章能概括的完。前面兩篇分別從整體上講解了錢包的建立流程和建立私鑰/公鑰的流程,本篇講解填充金鑰池生成 HD 錢包的過程。嘔血之作,吐血推薦,對錢包感興趣的朋友一定不要錯誤。TopUpKeyPool 填充金鑰池
本方法在第一次建立時會執行,在升級錢包到 HD 時,也會執行。它被用來填充金鑰池 keypool。下面我們來看下方法的執行。-
如果標誌禁止私鑰,則返回假。
if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { return false; }
-
如果錢包被鎖定,則返回假。
if (IsLocked()) return false;
-
如果引數
kpSize
大於0,則設定變數nTargetSize
為kpSize
;否則,設定為使用者指定的值,或預設的 1000。unsigned int nTargetSize; if (kpSize > 0) nTargetSize = kpSize; else nTargetSize = std::max(gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t) 0);
-
計算內部、外部可用的金鑰數量。
int64_t missingExternal = std::max(std::max((int64_t) nTargetSize, (int64_t) 1) - (int64_t)setExternalKeyPool.size(), (int64_t) 0); int64_t missingInternal = std::max(std::max((int64_t) nTargetSize, (int64_t) 1) - (int64_t)setInternalKeyPool.size(), (int64_t) 0);
在使用者不指定的情況下,並且是第一次,因為setExternalKeyPool
、setInternalKeyPool
這兩個集合都為空,所以missingExternal
、missingInternal
兩個變數的值都為 1000。 -
如果不支援 HD 錢包,或者不支援 HD 分割,那麼設定變數
missingInternal
為0。if (!IsHDEnabled() || !CanSupportFeature(FEATURE_HD_SPLIT)) { missingInternal = 0; }
-
生成訪問資料庫的物件。
bool internal = false; WalletBatch batch(*database);
-
進行
for (int64_t i = missingInternal + missingExternal; i--;)
迴圈。-
如果
i
小於missingInternal
,則設定變數internal
為真。if (i < missingInternal) { internal = true; }
-
設定當前索引。
int64_t index = ++m_max_keypool_index;
-
生成公鑰物件。
CPubKey pubkey(GenerateNewKey(batch, internal));
GenerateNewKey
方法用來生成公鑰。我們來看下這個方法的執行流程。-
生成表示建立的公鑰是否為壓縮的變數
fCompressed
。在 0.6 版本之後的公鑰預設都是壓縮的。bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY);
-
建立私鑰物件和金鑰元資料物件。這時候私鑰和金鑰元資料物件都沒有經過設定,還不能成為真正的私鑰和金鑰元資料,只有經過下面兩個方法中的某一個處理之後,才變成真正可用的物件。
CKey secret; int64_t nCreationTime = GetTime(); CKeyMetadata metadata(nCreationTime);
-
如果錢包支援 HD,那麼呼叫
DeriveNewChildKey
方法來衍生子私鑰,否則,呼叫MakeNewKey
方法來生成私鑰。if (IsHDEnabled()) { DeriveNewChildKey(batch, metadata, secret, (CanSupportFeature(FEATURE_HD_SPLIT) ? internal : false)); } else { secret.MakeNewKey(fCompressed); }
IsHDEnabled
是通過私鑰物件的hdChain.seed_id
物件是否為空來判斷的,因為在前面生成錢包私鑰之後,就設定了這個方法,所以這個方法一定返回真。MakeNewKey
這個方法,我們在前面建立錢包的私鑰時已經看到過,DeriveNewChildKey
這個方法我們在下面進行重點講解,這裡暫且略過。 -
如果變數
fCompressed
為真,則呼叫SetMinVersion
方法,設定最小版本為FEATURE_COMPRPUBKEY
,支援壓縮公鑰的版本。if (fCompressed) { SetMinVersion(FEATURE_COMPRPUBKEY); }
-
呼叫私鑰的
GetPubKey
方法,返回對應的公鑰。方法內部使用橢圓曲線演算法求得公鑰。具體不細講,讀者自行看程式碼。CPubKey CKey::GetPubKey() const { assert(fValid); secp256k1_pubkey pubkey; size_t clen = CPubKey::PUBLIC_KEY_SIZE; CPubKey result; int ret = secp256k1_ec_pubkey_create(secp256k1_context_sign, &pubkey, begin()); assert(ret); secp256k1_ec_pubkey_serialize(secp256k1_context_sign, (unsigned char*)result.begin(), &clen, &pubkey, fCompressed ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED); assert(result.size() == clen); assert(result.IsValid()); return result; }
-
把金鑰元資料加入
mapKeyMetadata
集合中。mapKeyMetadata[pubkey.GetID()] = metadata;
-
呼叫
UpdateTimeFirstKey
方法,更新nTimeFirstKey
屬性。UpdateTimeFirstKey(nCreationTime);
-
呼叫
AddKeyPubKeyWithDB
方法,把私鑰和公鑰儲存到資料庫中。這個方法在前面已經講過,這裡再講了。if (!AddKeyPubKeyWithDB(batch, secret, pubkey)) { throw std::runtime_error(std::string(__func__) + ": AddKey failed"); }
- 返回公鑰。
-
生成表示建立的公鑰是否為壓縮的變數
-
用公鑰生成一個金鑰池實體
CKeyPool
物件,並呼叫訪問錢包資料庫物件的WritePool
方法,以pool
為鍵把金鑰池實體物件寫入資料庫。if (!batch.WritePool(index, CKeyPool(pubkey, internal))) { throw std::runtime_error(std::string(__func__) + ": writing generated key failed"); }
-
如果變數
internal
為真,則把索引儲存到setInternalKeyPool
集合中,否則,儲存到setExternalKeyPool
集合中。if (internal) { setInternalKeyPool.insert(index); } else { setExternalKeyPool.insert(index); }
-
把索引儲存到
m_pool_key_to_index
集合中。m_pool_key_to_index[pubkey.GetID()] = index;
-
如果
- 返回真。
DeriveNewChildKey 衍生新的子私鑰
這個方法用來衍生子金鑰。方法的邏輯如下:-
生成相關的變數。
CKey seed;//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>'
-
呼叫
GetKey
方法,根據HD 鏈物件中儲存的公鑰物件來得到對應的私鑰物件。這個私鑰是根私鑰,也被稱為種子私鑰。如果獲得不到,則丟擲異常。if (!GetKey(hdChain.seed_id, seed)) throw std::runtime_error(std::string(__func__) + ": seed not found");
GetKey
方法執行流程如下:-
如果錢包沒有加密,則呼叫
CBasicKeyStore::GetKey
方法,返回公鑰物件的私鑰。if (!IsCrypted()) { return CBasicKeyStore::GetKey(address, keyOut); }
CBasicKeyStore::GetKey
方法直接從mapKeys
集合中取出對應的私鑰。mapKeys
集合是我們前面分析DeriveNewSeed
這個方法的第 6 步AddKeyPubKey
這個方法中把私鑰/公鑰及元資料儲存到資料庫過程中設定的。 -
否則,直接從加密集合
mapCryptedKeys
中取得對應的私鑰。CryptedKeyMap::const_iterator mi = mapCryptedKeys.find(address); if (mi != mapCryptedKeys.end()) { const CPubKey &vchPubKey = (*mi).second.first; const std::vector<unsigned char> &vchCryptedSecret = (*mi).second.second; return DecryptKey(vMasterKey, vchCryptedSecret, vchPubKey, keyOut); }
- 如果以上兩種情況都不能返回,那麼只能返回假了。
-
如果錢包沒有加密,則呼叫
-
呼叫主私鑰(擴充套件私鑰)的
SetSeed
方法,設定種子。此時的主私鑰只是一個物件,而不能當作真正的私鑰來使用,因為內部資料不存在。只有在本方法呼叫之後,主私鑰才能真正用來衍生金鑰。我們現在來看下SetSeed
方法的邏輯:-
首先,生成需要的變數。
static const unsigned char hashkey[] = {'B','i','t','c','o','i','n',' ','s','e','e','d'}; std::vector<unsigned char, secure_allocator<unsigned char>> vout(64);
-
其次,呼叫
CHMAC_SHA512
方法,使用HMAC_SHA512
演算法,根據種子私鑰生成長度為 512 位的字串。CHMAC_SHA512(hashkey, sizeof(hashkey)).Write(seed, nSeedLen).Finalize(vout.data());
-
然後,呼叫私鑰的
Set
方法,用 512 位的字串的左邊 256 位來初始化私鑰的keydata
屬性,並且設定私鑰的有效標誌fValid
屬性為真,壓縮標誌fCompressed
為引數指定的值,預設為真。key.Set(vout.data(), vout.data() + 32, true);
-
呼叫
memcpy
方法,把 512 位的字串的右邊 256 位儲存為鏈碼。memcpy(chaincode.begin(), vout.data() + 32, 32);
-
設定主私鑰的
nDepth
、nChild
為 0。nDepth = 0; nChild = 0;
-
把
vchFingerprint
重置為 0。memset(vchFingerprint, 0, sizeof(vchFingerprint));
-
首先,生成需要的變數。
-
呼叫主私鑰(擴充套件私鑰)的
Derive
方法,開始衍生子金鑰accountKey
。masterKey.Derive(accountKey, BIP32_HARDENED_KEY_LIMIT);
注意,在 bip32 之後,採用硬化衍生子金鑰。我們來看下
Derive
這個方法的執行流程:-
設定擴充套件私鑰
out
引數的nDepth
為nDepth
加 1。主私鑰的nDepth
為 0,參見上面所講。 -
獲取主公鑰的 CKeyID。
CKeyID id = key.GetPubKey().GetID();
-
設定置擴充套件私鑰
out
引數的nChild
為引數_nChild
的值,這裡為0x80000000
,10進製為 2147483648。 -
呼叫根私鑰的
Derive
方法,開始衍生私鑰。return key.Derive(out.key, out.chaincode, _nChild, chaincode);
這個方法內部執行流程如下:-
生成一個向量。
std::vector<unsigned char, secure_allocator<unsigned char>> vout(64);
-
把變數
nChild
向右移動 31位,如果所得等於 0,那麼呼叫GetPubKey
方法,獲得私鑰對應的公鑰,然後呼叫BIP32Hash
方法,填充變數vout
。在BIP32Hash
方法中使用CHMAC_SHA512
方法,根據鏈碼引數和子金鑰數量來填充vout
變數。如果右移所得不等於0,同樣呼叫BIP32Hash
方法,填充變數vout
。if ((nChild >> 31) == 0) { CPubKey pubkey = GetPubKey(); assert(pubkey.size() == CPubKey::COMPRESSED_PUBLIC_KEY_SIZE); BIP32Hash(cc, nChild, *pubkey.begin(), pubkey.begin()+1, vout.data()); } else { assert(size() == 32); BIP32Hash(cc, nChild, 0, begin(), vout.data()); }
-
把變數
vout
的內容拷貝到子鏈碼中。memcpy(ccChild.begin(), vout.data()+32, 32);
-
把私鑰的內容拷貝到子私鑰中。
memcpy((unsigned char*)keyChild.begin(), begin(), 32);
-
使用橢圓曲線演算法真正初始化子私鑰。
bool ret = secp256k1_ec_privkey_tweak_add(secp256k1_context_sign, (unsigned char*)keyChild.begin(), vout.data());
-
設定子私鑰為壓縮的,是否是有效的,並返回。
keyChild.fCompressed = true; keyChild.fValid = ret; return ret;
-
生成一個向量。
-
設定擴充套件私鑰
-
accountKey
私鑰(擴充套件私鑰)的Derive
方法,開始衍生子金鑰chainChildKey
。方法前面剛講過,此處略過。accountKey.Derive(chainChildKey, BIP32_HARDENED_KEY_LIMIT+(internal ? 1 : 0));
-
接下來衍生子私鑰
childKey
,與上面大致相同,可自行閱讀。do { 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
。secret = childKey.key;
-
設定變數
metadata
的 HD 種子為當前 HD 鏈的的種子。metadata.hd_seed_id = hdChain.seed_id;
-
更新 HD 鏈到資料庫中。
if (!batch.WriteHDChain(hdChain)) throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed");