五.serialize.h中的CDataStream类

在区块链中,有大量序列化的需求,比如进行哈希运算时,要将类对象序列化再进行哈希。

存储到磁盘里也需要序列化和反序列化,网络传输也是。

而serialize.h这个文件里的代码就是充当这样的作用,用来实现序列化的功能。

1.参数说明

可以看到这个文件里有一些类,比如CDataStream,CAutoFile(磁盘存储序列化相关)

而CDataStream是负责,哈希,网络相关的序列化。可以看到

cpp 复制代码
uint256 SerializeHash(const T& obj, int nType = SER_GETHASH, int nVersion = VERSION)
{
    // Most of the time is spent allocating and deallocating CDataStream's
    // buffer.  If this ever needs to be optimized further, make a CStaticStream
    // class with its buffer on the stack.
    CDataStream ss(nType, nVersion);
    ss.reserve(10000);
    ss << obj;
    return Hash(ss.begin(), ss.end());
}

在定义CDataStream ,有这样一个参数,nType,为SER_GETHASH,表明是用于哈希序列化。

如果为SER_NETWORK表明是网络。

2.VERSION

在上一章中,单独测试CDataStream使用,则没有问题,将它添进到我的项目中,之后。又报错,

在serialize.h中报常数错误,static const int VERSION = 101;这句,为什么会报错呢?应该还是编译器版本的原因,VERSION这个词在后来的编译器中可能被占用了,比如这样:#define VERSION 999;那么这句:static const int VERSION = 101; 可能就会变成这样

static const int 999 = 101;

具体是哪个(系统)文件不清楚,我们现在只能这样解决:

cpp 复制代码
#undef VERSION

static const int VERSION = 101;

加上#undef VERSION 取消这个宏定义,当然这样可能会有冲突(影响系统默认值),当下并不重要。

或者你为了不冲突,你将static const int VERSION = 101; 改为static const int MY_VERSION = 101;不使用VERSION这个命名,但是你自己的所有代码都要跟着修改。这里暂时不采用这种方法。

3.CDataStream序列化逻辑

来看一下我的测试代码:

cpp 复制代码
//处理和存储交易数据的类
class CTransaction
{
public:
    string vin;
    string vout;

    uint256 GetHash() const
    {
       return SerializeHash(*this);
    }
};

int main() {

    CTransaction tx;
    tx.vin = "a:50";
    tx.vout = "b:50";
    uint256 txhash = tx.GetHash();
    cout << txhash.ToString() << endl;
    //BitcoinMiner();
  
}

这里还是会报错,为什么呢?因为你通过CDataStream来序列化(发生于GetHash函数中),而CDataStream自带的,只支持默认类型的,比如int ,string这样的。

你要序列化CTransaction这个你自己定义的类,它是通过调用这个对象的Serialize来实现的。

cpp 复制代码
template<typename Stream, typename T>
inline void Serialize(Stream& os, const T& a, long nType, int nVersion = VERSION)
{
    a.Serialize(os, (int)nType, nVersion);
}

可以看到CDataStream里的重载函数Serialize的第二个参数,如果不是默认类型,比如int,string,

它就会调用这个对象自身的Serialize方法来实现序列化,即a.Serialize;

而你的CTransaction类中并没有实现这个方法,所以在a.Serialize这句会报错,提示a对象并没有Serialize这个成员。

那么在bitcoin源码中,它是怎样实现的呢,如下:

4.IMPLEMENT_SERIALIZE宏定义
cpp 复制代码
class CTransaction
{
public:
    int nVersion;
    vector<CTxIn> vin;
    vector<CTxOut> vout;
    int nLockTime;


    CTransaction()
    {
        SetNull();
    }

    IMPLEMENT_SERIALIZE
    (
        READWRITE(this->nVersion);
        nVersion = this->nVersion;
        READWRITE(vin);
        READWRITE(vout);
        READWRITE(nLockTime);
    )

注意源码,多了nVersion ,nLockTime,版本和时间这些参数,并且vin和vout是数组形式的,这些我们先不管,我们按之前简单的来,这里的关键是IMPLEMENT_SERIALIZE 和READWRITE这两个实现了类里的Serialize方法,怎么实现的呢,这两个其实是宏定义,看如下代码:

cpp 复制代码
#define IMPLEMENT_SERIALIZE(statements)    \
    unsigned int GetSerializeSize(int nType=0, int nVersion=VERSION) const  \
    {                                           \
        CSerActionGetSerializeSize ser_action;  \
        const bool fGetSize = true;             \
        const bool fWrite = false;              \
        const bool fRead = false;               \
        unsigned int nSerSize = 0;              \
        ser_streamplaceholder s;                \
        s.nType = nType;                        \
        s.nVersion = nVersion;                  \
        {statements}                            \
        return nSerSize;                        \
    }                                           \
    template<typename Stream>                   \
    void Serialize(Stream& s, int nType=0, int nVersion=VERSION) const  \
    {                                           \
        CSerActionSerialize ser_action;         \
        const bool fGetSize = false;            \
        const bool fWrite = true;               \
        const bool fRead = false;               \
        unsigned int nSerSize = 0;              \
        {statements}                            \
    }                                           \
    template<typename Stream>                   \
    void Unserialize(Stream& s, int nType=0, int nVersion=VERSION)  \
    {                                           \
        CSerActionUnserialize ser_action;       \
        const bool fGetSize = false;            \
        const bool fWrite = false;              \
        const bool fRead = true;                \
        unsigned int nSerSize = 0;              \
        {statements}                            \
    }

#define READWRITE(obj)      (nSerSize += ::SerReadWrite(s, (obj), nType, nVersion, ser_action))

可以看到, IMPLEMENT_SERIALIZE就是把这段语句传进去:

READWRITE(this->nVersion);

nVersion = this->nVersion;

READWRITE(vin);

READWRITE(vout);

READWRITE(nLockTime); 注意这里并不是五个参数,用;隔开的,这只是一整句代码。

所以将 {statements} 部分用上述代码代替,就得到了完整的函数代码,我们就可以来看看它干了什么事。写了三个函数。

分别是GetSerializeSize获取,类对象序列化数据的大小,Serialize序列化类对象,Unserialize反序列化。

这里底层都是调用READWRITE(obj),单独对每个成员实现序列化,然后再拼接,是这样的方法。

那么READWRITE(obj)展开就是:

(nSerSize += ::SerReadWrite(s, (obj), nType, nVersion, ser_action))

最终关键方法浮出水面,调用了SerReadWrite这个函数,从前面大概可以分析出,这个函数可以实现获取序列化对象的大小,它的返回值就是,在这里,用nSerSize来接收了,那么序列化后的数据,通过s来接收,而反序列化,这个s又变成传数据进去,(obj),就是类成员对象。

我们来看一下SerReadWrite函数,它在源码中有三个重载:

cpp 复制代码
template<typename Stream, typename T>
inline unsigned int SerReadWrite(Stream& s, const T& obj, int nType, int nVersion, CSerActionGetSerializeSize ser_action)
{
    return ::GetSerializeSize(obj, nType, nVersion);
}

template<typename Stream, typename T>
inline unsigned int SerReadWrite(Stream& s, const T& obj, int nType, int nVersion, CSerActionSerialize ser_action)
{
    ::Serialize(s, obj, nType, nVersion);
    return 0;
}

template<typename Stream, typename T>
inline unsigned int SerReadWrite(Stream& s, T& obj, int nType, int nVersion, CSerActionUnserialize ser_action)
{
    ::Unserialize(s, obj, nType, nVersion);
    return 0;
}

分别对应了,获取序列化大小,序列化,和反序列化,这三个功能这个函数都实现了,通过重载的方法。但是怎么知道该调用哪个函数呢?看这个函数的最后一个参数:

CSerActionGetSerializeSize ser_action

CSerActionSerialize ser_action

CSerActionUnserialize ser_action

你传什么类型,就会执行对应的函数,我们回到IMPLEMENT_SERIALIZE这个宏定义的三个函数里。每一个开头第一句就定义了这个类型,指明调用哪个重载函数。

好,明白了原理后,我们最终发现,此序列化最终还是调用的serialize.h里的通用全局序列函数,就是默认的CDataStream序列化int ,string 类型所使用的函数,即:

::Serialize(s, obj, nType, nVersion);

所以最终还是一样,因为你不管定义了什么自定义类,你的类成员最终还是由基本类型组成的,比如int ,string之类的。

所以这个方法就是,先将你的类成员拆分成基本类型,序列化,然后再拼在一起。

5.编写支持序列化的类

那么现在我们可以自己写一个类,然后通过上面的方法让它支持序列化:

cpp 复制代码
class CTransaction
{
public:
    string vin;
    string vout;

    uint256 GetHash() const
    {
       return SerializeHash(*this);
    }
    IMPLEMENT_SERIALIZE
    (
    READWRITE(vin);
    READWRITE(vout);
    )
};

基本就是调用IMPLEMENT_SERIALIZE宏,然后你有几个成员,就写几个READWRITE。

然后再如下代码测试:

cpp 复制代码
int main() {

    CTransaction tx;
    tx.vin = "a:50";
    tx.vout = "b:50";

    uint256 txhash = tx.GetHash();
    cout << txhash.ToString() << endl;

    //BitcoinMiner();
  
}

结果:

可以看到代码已经不报错了,可以顺利执行。但是这个结果,是否正确呢?

就是这个哈希值,以2a开头,9c结尾这个。

我们可以手动来分析计算一下。

a:50 分别是'a' ':' '5' '0' 这四个字符,对应的ASCII码是:

97 58 53 48(可用在线工具查询)

那么用十六进制表达就是:

61 3A 35 30

又因为,CDataStream的序列化,前面要加上描述数据大小的值,这里是4个字节。所以前面还得加上04,所以最终这个数据就是04 61 3A 35 30。

那么b:50按上面的计算就是 04 62 3a 35 30,所以这两个拼在一起,就是:

04613a353004623a3530

我们也可以用代码来输出一下,看序列化后的结果:

cpp 复制代码
int main() {

    CTransaction tx;
    tx.vin = "a:50";
    tx.vout = "b:50";

    CDataStream ss;
    ss << tx;
    for (auto it = ss.begin(); it != ss.end(); ++it) {
        printf("%02x ", (unsigned char)*it);
    }
    printf("\n");

    uint256 txhash = tx.GetHash();
    cout << txhash.ToString() << endl;

    //BitcoinMiner();
  
}

输出:

可以看到,再将tx序列化后,ss获得的值和我们计算出来的是一致的。

对象序列化后得到是这样一串数据,接着,就是将这串数据进行两次哈希。

6.手动验证结果

我们用在线工具来测试一下,注意要选择支持hex输入数据的,否则它会把0461...每个数字当成一一个字符,比如0,4,当成两个字符,但其实这是十六进制表达,04,只是一个ascii码,表达的是一个字符。如果你不选择hex模式,那么应该是a:10b:10之类的,当然前面还要加上04这个ascii对应的字符,这里仅举例,就不去查04实际对应那个字符了。

好,开始第一次哈希:

得到哈希值:

c5ff0c19e47839dafd3b9ea7c6a6967c09d10f8f7e6ad8c4fc53b97814fd5b73

接着将这个哈希值再哈希一次:

可以看到是9c开头?结果不对?结果是对的,哈希值已经正确储存,uint256的值也是一致的,只是大小端序的问题。需要反着展示,即从后往前看,不是a2141这样啊,两位代表一个字节。

所以是2a41c1.....9c,从后面两位一反转的看。所以这样结果就是对上了。

相关推荐
王老师青少年编程6 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【跳跃与过河问题】:跳跳!
c++·算法·贪心·csp·信奥赛·跳跃与过河问题·跳跳
MediaTea6 小时前
ML:决策树的基本原理与实现
人工智能·算法·决策树·机器学习·数据挖掘
王老师青少年编程6 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【跳跃与过河问题】:独木桥
c++·算法·贪心·csp·信奥赛·跳跃与过河问题·独木桥
忡黑梨6 小时前
eNSP_DHCP配置
c语言·网络·c++·python·算法·网络安全·智能路由器
陈壮实的搬砖日记6 小时前
白话生成式推荐二:MiniOneRec之RQ-VAE
算法
陈壮实的搬砖日记7 小时前
白话生成式推荐二:MiniOneRec之SFT
算法
她说彩礼65万7 小时前
C语言 动态内存管理
c语言·开发语言·算法
Irene19917 小时前
数据排序为什么默认升序
算法·排序
.5487 小时前
DFS + BFS(深度优先搜索 & 广度优先搜索)
算法·深度优先·宽度优先
一行代码一行诗++7 小时前
转义字符和语句
c语言·开发语言·算法