1._mkdir
接上一章:
1.由于db.cpp里也会用到 _mkdir函数,就是main.cpp里GetAppDir函数return .的时候,由db.cpp根据当前目录创建数据库目录,而不是由main.cpp里GetAppDir函数创建,db.cpp也要包含#include <direct.h>头文件,在这里我们就开始新建一个headers.h头文件吧,以后将公用的头文件都放在这里面,先加入#include <direct.h>进去支持_mkdir函数:
#include <direct.h>
然后在db.cpp里包含一下headers.h,main.cpp也相应的改一下。
2. RandAddSeed();
报错:
db.cpp有调用 RandAddSeed();函数,这个函数的代码在util.cpp文件中,我们在这个文件中定义 RandAddSeed();,代码如下:
void RandAddSeed(bool fPerfmon)
{
// 1. 用高精度 CPU 性能计数器作为熵源(最快、最常用的一次性补充)
LARGE_INTEGER PerformanceCount;
QueryPerformanceCounter(&PerformanceCount); // 获取当前高精度计时器值(通常纳秒级,CPU 相关)
RAND_add(&PerformanceCount, sizeof(PerformanceCount), 1.5); // 喂给 OpenSSL,估算熵值为 1.5 字节(保守估计)
memset(&PerformanceCount, 0, sizeof(PerformanceCount)); // 清零,防止敏感数据残留内存
// 2. 控制频率:每 5 分钟最多执行一次“重型”熵收集(或当 fPerfmon=true 时强制执行)
static int64 nLastPerfmon;
if (fPerfmon || GetTime() > nLastPerfmon + 5 * 60)
{
nLastPerfmon = GetTime();
// 3. 读取 Windows 性能计数器全局数据(非常大的熵池,包含系统负载、网络、磁盘等统计)
unsigned char pdata[250000]; // 分配 250KB 缓冲区
memset(pdata, 0, sizeof(pdata));
unsigned long nSize = sizeof(pdata);
long ret = RegQueryValueEx(HKEY_PERFORMANCE_DATA, "Global", NULL, NULL, pdata, &nSize);
RegCloseKey(HKEY_PERFORMANCE_DATA); // 关闭句柄
if (ret == ERROR_SUCCESS) // 如果读取成功
{
uint256 hash;
SHA256(pdata, nSize, (unsigned char*)&hash); // 对这 250KB 数据做 SHA256 哈希(压缩 + 混淆)
RAND_add(&hash, sizeof(hash), min(nSize/500.0, (double)sizeof(hash))); // 喂给 OpenSSL,熵估计 = min(原始大小/500, 32)
hash = 0; // 清零 hash
memset(pdata, 0, nSize); // 清零大缓冲区
printf("RandAddSeed() got %d bytes of performance data\n", nSize); // 调试输出
}
}
}
说明:
这个函数的作用,比如生成随机数吧,程序会大量用到这个功能,打个比方,生成私钥。但是呢,系统的生成随机数的方式,是一种简单的方法,基于简单的规则,所有有可能产生伪随机数,这个随机并不一定安全。
所以我们自己写了一个RandAddSeed函数,用一定的方法产生随数种子,每隔一段时间。这样安全性就提高了,而不会说,几万个人重复用这套程序,然后生成了相同的随机数。
这个种子函数对应的生成随机数函数是openssl里的Rand_bytes函数,这个可以生成指定字节的随机数。比如私钥的生成类似于这样:
#include <openssl/rand.h>
#include <openssl/err.h>
// ...
RandAddSeed()
unsigned char privkey[32];
if (RAND_bytes(privkey, 32) != 1) {
RandAddSeed()
fprintf(stderr, "RAND_bytes failed: %s\n", ERR_error_string(ERR_get_error(), NULL));
return false;
}
// 然后检查是否在 1 ~ n-1 范围内(极少需要重来)
if (/* privkey == 0 或 >= curve order */) {
// 重来
}
生成成功函数会返回1。
2.1解决:将RandAddSeed函数代码复制到util.cpp里后,自然里面的RAND_add函数,需要rand.h头文件支持,我们在util.cpp包含:
#include "openssl/rand.h"
2.2然后是 sha256函数,这是openssl/sha.h里的函数
我们将这个sha.h放进头文件里headers.h里吧,然后再包含这个头文件就行。
2.3接着是GetTime函数,这个函数的代码也在util.cpp里,代码如下,复制进去:
int64 GetTime()
{
return time(NULL);
}
调用了time函数,这是个系统函数,头文件time.h
#include<time.h>
2.4然后我们右击bitcoin源码中的GetTime,右击--转到声明,看它声明在哪里,在util.h里:
int64 GetTime();
我们在同样的地方声明一下就可以了,确保其它的cpp能认到这个函数,只要#include "util.h"就行。
至此RandAddSeed函数的问题解决。
3.头文件说明
接下来,解决另外的错误:
报util.h 里SHA256:找不到标识符,SHA256_ctx未声明的标识符,这个函数以及相关类型,是需要openssl/sha.h头文件支持的,但是现在的问题是,这个util.h我没动过,之前也都正常。
现在为什么反而报错了呢,在添加了db.cpp,util.cpp等文件后。
这是因为,我没有在util.h里包含openssl/sha.h,我之前在main.cpp里使用,包含了opensll/sha.h,然后再#include util.h,通过这种间接的方式实现支持包含。
这样之前在单独的编译main.cpp里,util.h是没问题的。但添加的另一个cpp,db.cpp,util.cpp编译时,却没有间接包含,只包含了util.h,因为每个cpp,是编译器单独编译的,互不关联,(小知识点:编译好后会链接obj,这就是为什么虽然单独编译,但变量同名还是会冲突,报obj时冲突),所以单独编译db.cpp的时候,会报util.h里未定义SHA256等编译错误,它根本找不到openssl/sha.h。
解决方法,直接在util.h #include "openssl/sha.h",或每一个使用util.h的cpp都间接包含一下。
或放在headers.h头文件里,然后都包含headers.h一下,这里我就把它放在headers.h里,从这里开始越来越贴近源码写法。
注意:但是,还是报错,这是因为这里的话,编译器不是实时动态刷新的,需要编译一下,才能清除掉错误。
改完后,又出现了util.h里未声明的uint256标识符,这个头文件,是在uint256.h,这肯定是哪个cpp用了util.h,而没间接包含uint256.h导致,但是之前不报这个错误,最终发现main.cpp里:
#include "headers.h"
#include "uint256.h"
把uint256.h放在headers.h前面解决该问题,为什么会出现这种情况,这是因为我把util.h打包进了headers.h文件里,所以我去掉了mian.cpp里的util.h,而原来的util.h 是在uint256.h下面的。所以当时没问题。这里却在上面,这种间接的包含必须遵循前后顺序。
总之,这种头文件相关的错误我们一定要细心,明白原理,报未定义错误,看当前cpp有没有直接包含,没有的话,就是间接包含导致的错误,看哪个cpp包含了util.h却没有uint256.h,或者更进一层次的间接,就是这里我包含了headers.h,却把uint256.h写在它下面。这是特殊时期的问题,不建议这样写。但我们得有解决这种问题的能力。
找到问题后,我们还是一劳永逸的解决,将uint256.h放进headers.h里,以后想使用util.h,包含headers.h即可,不会再有其它的错误。然后去掉其它cpp里的uint256.h,留headers.h即可,否则会报uint256.h里的重定义错误。如果只有uint256.h则改成headers.h.
这样改起来,会有些麻烦,导致牵一发而动全身,这就是为什么我一直没改,然后越拖到后面要改的地方就越多,今天就花点时间解决一下这个问题,比如,在db.cpp里:
// Copyright (c) 2009 Satoshi Nakamoto
// Distributed under the MIT/X11 software license, see the accompanying
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
#include<map>
#include<vector>
#include<Windows.h>
#include"uint256.h"
#include "serialize.h"
#include "key.h"
#include "db.h"
#include "main.h"
#include "headers.h"
using namespace std;
就需要去掉uint256.h,而只留headers.h,但这们我可想而知可能会产生一个新的问题,那就是,如果在serialize.h里有用到uint256.h呢,你的serialize.h在这里,很明显是通过间接包含的方式,实现支持uint256.h的,那么我将headers.h直接放在最前面就能解决吗?
但是headers.h里如果有头文件,又需要下面的头文件支持呢?(仅举例)
这些都是问题,需要我们花时间去排查,去验证,将它们都打包进headers.h,安排好。
我们慢慢来一个一个解决问题,过程繁琐,这里就不再贴具体步骤了,原理知道了就行。
所幸现在文件不多,这是我改好后的headers.h文件,你们去cpp和.h里对比查找,有headers.h所包含的头文件,一律用headers.h代替,并去掉跟headers.h相冲突的头文件,暂时就改了这些:
#include<windows.h>
#include <direct.h>
#include<map>
#include<vector>
#include<string>
#include "openssl/rand.h"
#include "openssl/sha.h"
#include "serialize.h"
#include "uint256.h"
#include "util.h"
#include "key.h"
#include "db.h"
#include "main.h"
注意bitcoin源码中这样的使用方式会有一个问题,如果头文件出现互相依赖,就会有问题,所以现在能用,那都是安排好,没错,正好都没有互相依赖,不是使用了什么神秘的魔法,没有#pragma once之类的写法。而是一切都是最好的安排,手工排序,即使有的话,只是在cpp里单独声明下,而不使用头文件,须知头文件,也只是为了方便声明等少写代码,我直接声明,则能避免掉一些问题。
有需要,以后可下载我改好的完整项目。
我们来看db.cpp里的代码,继承了CDB类的有4个。分别:
CTxDB
存储交易数据
CAddrDB
地址数据库(已知节点地址)
CReviewDB
评论/反馈数据库(早期实验性功能,几乎没用)
CWalletDB
钱包数据库(密钥、交易、地址簿、设置等)
因为其它的类其下涉及的类型比较多,全部修复起来比较麻烦,现在也没必要。
我们这里把另外三个类相关全注释掉,只留CWalletDB类,然后解决它的报错,其它的以后用到再添加。
CWalletDB修复
4.类里的LoadWallet
将其它类去掉后,只保留CWalletDB相关代码,我们可以看到有这部分代码:
bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
{
vchDefaultKeyRet.clear();
//// todo: shouldn't we catch exceptions and try to recover and continue?
CRITICAL_BLOCK(cs_mapKeys)
CRITICAL_BLOCK(cs_mapWallet)
{
// Get cursor
Dbc* pcursor = GetCursor();
if (!pcursor)
return false;
loop
{
// Read next record
CDataStream ssKey;
CDataStream ssValue;
int ret = ReadAtCursor(pcursor, ssKey, ssValue);
if (ret == DB_NOTFOUND)
break;
else if (ret != 0)
return false;
// Unserialize
// Taking advantage of the fact that pair serialization
// is just the two items serialized one after the other
string strType;
ssKey >> strType;
if (strType == "name")
{
string strAddress;
ssKey >> strAddress;
ssValue >> mapAddressBook[strAddress];
}
else if (strType == "tx")
{
uint256 hash;
ssKey >> hash;
CWalletTx& wtx = mapWallet[hash];
ssValue >> wtx;
if (wtx.GetHash() != hash)
printf("Error in wallet.dat, hash mismatch\n");
//// debug print
//printf("LoadWallet %s\n", wtx.GetHash().ToString().c_str());
//printf(" %12I64d %s %s %s\n",
// wtx.vout[0].nValue,
// DateTimeStr(wtx.nTime).c_str(),
// wtx.hashBlock.ToString().substr(0,14).c_str(),
// wtx.mapValue["message"].c_str());
}
else if (strType == "key")
{
vector<unsigned char> vchPubKey;
ssKey >> vchPubKey;
CPrivKey vchPrivKey;
ssValue >> vchPrivKey;
mapKeys[vchPubKey] = vchPrivKey;
mapPubKeys[Hash160(vchPubKey)] = vchPubKey;
}
else if (strType == "defaultkey")
{
ssValue >> vchDefaultKeyRet;
}
else if (strType == "setting") /// or settings or option or options or config?
{
string strKey;
ssKey >> strKey;
if (strKey == "fGenerateBitcoins") ssValue >> fGenerateBitcoins;
if (strKey == "nTransactionFee") ssValue >> nTransactionFee;
if (strKey == "addrIncoming") ssValue >> addrIncoming;
}
}
}
printf("fGenerateBitcoins = %d\n", fGenerateBitcoins);
printf("nTransactionFee = %I64d\n", nTransactionFee);
printf("addrIncoming = %s\n", addrIncoming.ToString().c_str());
return true;
}
bool LoadWallet()
{
vector<unsigned char> vchDefaultKey;
if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
return false;
if (mapKeys.count(vchDefaultKey))
{
// Set keyUser
keyUser.SetPubKey(vchDefaultKey);
keyUser.SetPrivKey(mapKeys[vchDefaultKey]);
}
else
{
// Create new keyUser and set as default key
keyUser.MakeNewKey();
if (!AddKey(keyUser))
return false;
if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address"))
return false;
CWalletDB().WriteDefaultKey(keyUser.GetPubKey());
}
return true;
}
两个LoadWallet函数,一个是属于CWalletDB类的函数,一个是全局函数,它们的关系是,全局函数LoadWallet最终调用了类里的LoadWallet函数来实现功能。
Dbc* pcursor = GetCursor();
先获得数据库当前游标,然后loop循环里,循环读出来数据:
int ret = ReadAtCursor(pcursor, ssKey, ssValue);
读取到键值对后,但是我们可以看到,有这样的判断:
else if (strType == "tx")
else if (strType == "key")
说明钱包这个数据库不仅仅是存储有公钥对,还有别的数据,我们来看一下,这里的tx和key是什么意思,为什么以此来判断不同的数据类型:
在上一级添加公私钥到钱包的时候:
bool AddKey(const CKey& key)
{
CRITICAL_BLOCK(cs_mapKeys)
{
mapKeys[key.GetPubKey()] = key.GetPrivKey();
mapPubKeys[Hash160(key.GetPubKey())] = key.GetPubKey();
}
return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey());
}
是调用CWalletDB类的WriteKey来实现的,但是WriteKey是怎么来写键值对的呢?
bool WriteKey(const vector<unsigned char>& vchPubKey, const CPrivKey& vchPrivKey)
{
return Write(make_pair(string("key"), vchPubKey), vchPrivKey, false);
}
5.make_pair
我们可以看到,它不是直接写键的,还有给它加一个"属性"名:key,表示这是公私钥数据,为什么要这样,因为公钥用来当做键,可能会跟其它的数据有冲突(如果存储多种数据的话),比如交易数据呢,如果也要用公钥来当键,那不是有冲突了吗?所以要分类,标志"key",表示这是用户的公私钥数据。
这里是通过make_pair这个方法,把键和属性名合成一个键。
make_pair是系统函数,头文件在<utility>,作用是将两个值打包成一个std::pair对象。
然后write将这个pair对象当作键写进数据库里去,可以看到父类CDB里的Write函数,底层将这个对象给序列化了,部分代码如下:
bool Write(const K& key, const T& value, bool fOverwrite=true)
{
if (!pdb)
return false;
// Key
CDataStream ssKey(SER_DISK);
ssKey.reserve(1000);
ssKey << key;
Dbt datKey(&ssKey[0], ssKey.size());
好,这就是为什么在读数据的时候,需要判断一下为"key"还是"tx"之类的,区分数据类型。
6.数据库文件名
接着我们回到开始,LoadWallet打开的是什么文件,先来看:
if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
return false;
可以看到有个参数"cr"这个是文件打开类型,对应如下代码解码:
1.
class CWalletDB : public CDB
{
public:
CWalletDB(const char* pszMode="r+", bool fTxn=false) : CDB("wallet.dat", pszMode, fTxn) { }
2.
CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL)
{
int ret;
if (pszFile == NULL)
return;
bool fCreate = strchr(pszMode, 'c');
bool fReadOnly = (!strchr(pszMode, '+') && !strchr(pszMode, 'w'));
unsigned int nFlags = DB_THREAD;
if (fCreate)
nFlags |= DB_CREATE;
else if (fReadOnly)
nFlags |= DB_RDONLY;
if (!fReadOnly || fTxn)
nFlags |= DB_AUTO_COMMIT;
可以看到cr的意思,就是就是nFlags|=DB_CREATE,只执行了这一个。
r只是为了便于阅读,实际没用上。最终的意思就是文件不存在则创建,如果存在则默认属性打开,当然是可读写的。
7.mapKeys和mapWallet
然后我们来总结一下,公私钥,读到了就存在内存中里:
mapKeys[vchPubKey] = vchPrivKey;
mapPubKeys[Hash160(vchPubKey)] = vchPubKey;
mapKeys是用来映射公钥和私钥的,而mapPubKeys是用来映射hash160"格式"的账号对应的公钥。有了这个映射我们看到一个hash160账号,就可以通过mapPubKeys查它的公钥,得到公钥后,再根据mapKeys查它的私钥。
tx
如果是tx(交易数据):
else if (strType == "tx")
{
uint256 hash;
ssKey >> hash;
CWalletTx& wtx = mapWallet[hash];
ssValue >> wtx;
这里的hash值,就是一个tx(CTransaction)的哈希值,然后用hash做mapWallet的下标名,没有则创建,把这个引用到wtx,然后把值写进wtx,相当于写进mapWallet这个"数组"里。
//mapWallet定义
map<uint256, CWalletTx> mapWallet;
这里的值(ssValue)是一个CTransaction对象的序列化吗?并不是,它是一个CWalletTx对象的序列化,CWalletTx继承自CMerkleTx,而CMerkleTx继承自CTransaction。
通过这样一些继承,多了一些功能,比如能知道tx所在区块,默克尔树相关,已经是否为自己的交易, 是否已花费等等相关信息。
接下来我们可以解决一些报错了,首先第一个:
8.keyUser
keyUser未声明的标识符。
可以看到bool LoadWallet()时,会传一个引用参数:
vector<unsigned char> vchDefaultKey;
if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
return false;
这个vchDefaultKey,表示你的钱包默认账户(key)。
它在读数据库的时候,如果读到defaultkey这个标识,则能获取到:
else if (strType == "defaultkey")
{
ssValue >> vchDefaultKeyRet;
}
那它是怎么创建的呢?也是在LoadWallet里啊,后面有一句,如果没有则创建,也就是钱包第一次加载的时候。写进去后,后面就能读取到了:
bool LoadWallet()
{
vector<unsigned char> vchDefaultKey;
if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
return false;
if (mapKeys.count(vchDefaultKey))
{
// Set keyUser
keyUser.SetPubKey(vchDefaultKey);
keyUser.SetPrivKey(mapKeys[vchDefaultKey]);
}
else
{
// Create new keyUser and set as default key
keyUser.MakeNewKey();
if (!AddKey(keyUser))
return false;
if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address"))
return false;
CWalletDB().WriteDefaultKey(keyUser.GetPubKey());
}
return true;
}
所以这个keyUser的作用是什么,看上面的代码现在就明白了,
它是一个Ckey类型,定义在main.cpp里:
CKey keyUser;
用来存储你钱包默认账户的key,如果钱包里有,则从钱包里获取,如果没有则自己创建一个。
所以解决这个报错,我们在main.cpp定义一下,然后在main.h声明一下就行,声明:
extern CKey keyUser;
头文件则不用包含了,因为都在headers.h里面。已经包含了headers.h

402

被折叠的 条评论
为什么被折叠?



