十一.Berkeley DB数据库 序列化和钱包管理(3)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bczheng1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值