比特币源码学习(2)-- 数据类型uint256

uint256类型

开场白

从今天开始,将开始记录我的比特币源码学习之旅!
我的学习路经可能不是由整体到局部,而是由局部到整体的过程,先从比特币中的底层数据结构开始,从底到顶逐个理解。我们开始吧!!!go go go …

uint256和arith_uint256

这两种类型本质上来说,都可以存储256位的数据,但主要的区别在于内部保存数据的粒度不同,如下代码可见:

//uint256基类
template
class base_blob
{
protected:
    static constexpr int WIDTH = BITS / 8;
    uint8_t data[WIDTH];
public:
    ...
}

//arith_uint256基类
template
class base_uint
{
protected:
    static constexpr int WIDTH = BITS / 32;
    uint32_t pn[WIDTH];
public:
    ...
}

uint256

uint256相对于arith_uint256较为简单,常用的方法如下

template
class base_blob
{
protected:
    static constexpr int WIDTH = BITS / 8;
    uint8_t data[WIDTH];
public:
    std::string GetHex() const;
    void SetHex(const char* psz);
    void SetHex(const std::string& str);
    
    template
    void Serialize(Stream& s) const
    {
        s.write((char*)data, sizeof(data));
    }
    template
    void Unserialize(Stream& s)
    {
        s.read((char*)data, sizeof(data));
    }
        friend inline bool operator==(const base_blob& a, const base_blob& b) { return a.Compare(b) == 0; }
    friend inline bool operator!=(const base_blob& a, const base_blob& b) { return a.Compare(b) != 0; }
    friend inline bool operator<(const base_blob& a, const base_blob& b) { return a.Compare(b) < 0; }

    inline int Compare(const base_blob& other) const { return memcmp(data, other.data, sizeof(data)); }
    ...
}

class uin256 : public base_blob<256> {...}

其中的主要方法也比较简单,这里不多阐述,但在SetHex中有部分代码刷新了我的认知,我们常常不会这样做的(完整代码请看uint256.cpp)

template 
void base_blob::SetHex(const char* psz)
{
    ...
    // hex string to uint
    const char* pbegin = psz;
    while (::HexDigit(*psz) != -1)
        psz++;

    // Ooooooh
    psz--;
    ...
}

上述的代码中,psz竟然可以减减,psz代表的是字符串,也表示当前所在的地址,地址减法我还真没遇见过,于是我写了个简单的测试代码,发现真的可以,只是在指针到达字符串的尾部后,我试图使用p–的方式重新找到字符串,此时数据已经丢失了,因此使用psz–的方式还是需要注意的,防止内存越界,甚至破坏其它数据!这里可能是将指针作为迭代器来使用!

int main()
{
    const char* str = "Hello World!";
    while(*str != -1) str++;
    str--;
    cout << str << endl;

    return 0;
}

base_uint

这个格式的实现要比uint256的实现复杂的多,它不仅包含了uint256中的方法,更和我们常用的数值类型相关的操作方式保持了一致,比如int类型的+,-,*,/以及位运算,因此这个类相对比较复杂,其中还涉及到了大数的加减乘除,接下来主要分析这部分。

template
class base_uint
{
protected:
    static constexpr int WIDTH = BITS / 32;
    uint32_t pn[WIDTH];
public:
    const base_uint operator~() const;
    const base_uint operator-() const;
    base_uint& operator=(uint64_t b);
    base_uint& operator^=(const base_uint& b);
    base_uint& operator&=(const base_uint& b);
    base_uint& operator|=(const base_uint& b);
    base_uint& operator^=(uint64_t b);
    base_uint& operator|=(uint64_t b);
    base_uint& operator<<=(unsigned int shift);
    base_uint& operator>>=(unsigned int shift);
    base_uint& operator+=(const base_uint& b);
    base_uint& operator-=(const base_uint& b);
    base_uint& operator+=(uint64_t b64);
    base_uint& operator-=(uint64_t b64);
    base_uint& operator*=(uint32_t b32);
    base_uint& operator*=(const base_uint& b);
    base_uint& operator/=(const base_uint& b);
    base_uint& operator++();
    base_uint& operator++(int);
    base_uint& operator--();
    base_uint& operator--(int);

    ...
};

以上列出了base_uint的相关算术接口,中规中举,接下来研究几个比较难以理解的接口源码(我觉得有些难,但对于吃透算法的人来说,应该只是小问题)。

先来看几个简单的接口实现

  1. 求负数
const base_uint base_uint::operator-() const
{
    base_uint ret;
    for (int i = 0;i < WIDTH; i++)
        ret.pn[i] = ~pn[i];
    ++ret;
    return ret;
}

从简单的开始,计算机中负数的存储可表达为(-a=~a+1)

  1. 求和以及数值乘法
base_uint& base_uint::operator+=(const base_uint& b)
{
    uint64_t carry = 0;                         //[1]
    for(int i = 0;i < WIDTH;i++)
    {
        uint64_t n = carry + pn[i] + b.pn[i];   //[2]
        pn[i] = n & 0xffffffff;                 
        carry = n >> 32;                        //[3]
    }
    return *this;
}
template
base_uint& base_uint::operator*=(uint32_t b32)
{
    uint64_t carry = 0;
    for (int i = 0; i < WIDTH; i++) {
        uint64_t n = carry + (uint64_t)b32 * pn[i];
        pn[i] = n & 0xffffffff;
        carry = n >> 32;
    }
    return *this;
}

[1]carry用来存放进位,值为0或者1,[2]注意使用uint64_t来保存结果,防止uint32_t相加溢出
[3]右移获取进位值,当pn[i]+b.pn[i]>=0x0000000100000000时,carry取1,否则取0
乘法接口和加法类似

现在看看难些的接口

  1. 乘法
template 
base_uint& base_uint::operator*=(const base_uint& b)
{
    base_uint a;
    for (int j = 0; j < WIDTH; j++) {
        uint64_t carry = 0;
        for (int i = 0; i + j < WIDTH; i++) {
            uint64_t n = carry + a.pn[i + j] + (uint64_t)pn[j] * b.pn[i]; //[1]
            a.pn[i + j] = n & 0xffffffff;                                 //[2]
            carry = n >> 32;
        }
    }
    *this = a;
    return *this;
}

乍一看,这里面有些地方的实现还是比较难以理解的,特别时在[1]处,很难一眼看出公式的意思,其实我们想想我们使用竖式计算算术乘法的时候是怎么做的,就不难理解这个公式了含义了!

  1. 左移和右移
template 
base_uint& base_uint::operator<<=(unsigned int shift)
{
    base_uint a(*this);
    for (int i = 0; i < WIDTH; i++)
        pn[i] = 0;
    int k = shift / 32;                                     //[1]
    shift = shift % 32;                                     //[2]
    for (int i = 0; i < WIDTH; i++) {
        if (i + k + 1 < WIDTH && shift != 0)                //[3]
            pn[i + k + 1] |= (a.pn[i] >> (32 - shift));     //[4]
        if (i + k < WIDTH)                                  //[5]
            pn[i + k] |= (a.pn[i] << shift);                //[6]
    }
    return *this;
}

template 
base_uint& base_uint::operator>>=(unsigned int shift)
{
    base_uint a(*this);
    for (int i = 0; i < WIDTH; i++)
        pn[i] = 0;
    int k = shift / 32;
    shift = shift % 32;
    for (int i = 0; i < WIDTH; i++) {
        if (i - k - 1 >= 0 && shift != 0)
            pn[i - k - 1] |= (a.pn[i] << (32 - shift));
        if (i - k >= 0)
            pn[i - k] |= (a.pn[i] >> shift);
    }
    return *this;
}

主要看左移代码(右移和左移是对称的),由于base_uint内部使用了uint32类型的数组来保存数据,因此[1]和[2]分别表示该数组中最小单元移动的次数以及最小单元内部移动的位数,如下图所示:比特币源码学习(2)-- 数据类型uint256_第1张图片。[3][4]处当左移的部分未超过pn数组的大小时,需要将左移丢弃的部分放置到更高为的低位部分,[5][6]好理解,即为正常的左移,只不过移位后存储的位置会有变化。

下面来看看我认为最难以理解的部分

  1. 除法
    template 
base_uint& base_uint::operator/=(const base_uint& b)
{
    base_uint div = b;     // make a copy, so we can shift.
    base_uint num = *this; // make a copy, so we can subtract.
    *this = 0;                   // the quotient.
    int num_bits = num.bits();   // 获取分母二进制的有效位数
    int div_bits = div.bits();   // 获取分子二进制的有效位数
    if (div_bits == 0)
        throw uint_error("Division by zero");
    if (div_bits > num_bits) // the result is certainly 0.
        return *this;
    int shift = num_bits - div_bits;
    div <<= shift; // shift so that div and num align.                          //[1]
    while (shift >= 0) {
        if (num >= div) {                                                       //[2]
            num -= div;
            pn[shift / 32] |= (1 << (shift & 31)); // set a bit of the result.  //[3]
        }
        div >>= 1; // shift back.                                               //[4]
        shift--;
    }
    // num now contains the remainder of the division.
    return *this;
}

这是整个类中代码最多的部分,但并不表示所有的代码都难以理解(当然这是我认为比较不易弄懂的地方,大牛请忽略),主要是难点标示在[1][2][3][4]位置!该除法使用减法来计算,但一次一次的减明显会很慢,因此这里考虑一次会减多个整数。[1]处将除数和被除数的位数对齐,即将除数放大了2的shift次方,来假设除数和被除数在同一量级的情况下的计算结果!下面先举个例子来说明这个逻辑。

假设两个数a=18745,b=15,计算m = a / b;
1. a:18065      b:15        t=b*1000=15000           a=t*1+3065          ans=1000
2. a:3065       b:15        t=b*100=1500             a=t*2+65            ans=1000+100*2
3. a:65         b:15        t=b*10=150               a=t*0+65            ans=1000+100*2
4. a:65         b:15        t=b*1=15                 a=t*4+5             ans=1000+100*2+4=1204
5. a:5  不需要再计算
最终m=1249

上述例子中直接使用十进制来计算,因此每次降低一个数量级,即减少10倍,在代码中使用二进制也是类似的道理。

arith_uint256

class arith_uint256 : public base_uint<256>
{
public:
    arith_uint256& SetCompact(uint32_t nCompact, bool *pfNegative = nullptr, bool *pfOverflow = nullptr);
    uint32_t GetCompact(bool fNegative = false) const;
    ...
}

以上两个类方法还是比较重要的,一个用于可压缩负数的32位数据,另一个用于恢复压缩的数据。这两个方法替换了openssl中的BN_bn2mpi和BN_mpi2bn两个函数。
压缩的标准是这样,前8位构成指数项,后24位是尾数,其中第24位(0x800000)表示当前数据的符号位,用公式表示就是:N=(-1^sign) * mantissa * 256 ^ (exponent - 3)。例如0x05123456压缩后为0x1234560000,0x0600c0de压缩后为0xc0de000000。在比特币中,所有的有符号位数据可以转化为以256为底的格式存储。
具体的代码实现如下

// This implementation directly uses shifts instead of going
// through an intermediate MPI representation.
arith_uint256& arith_uint256::SetCompact(uint32_t nCompact, bool* pfNegative, bool* pfOverflow)
{
    int nSize = nCompact >> 24;
    uint32_t nWord = nCompact & 0x007fffff;
    if (nSize <= 3) {
        nWord >>= 8 * (3 - nSize);
        *this = nWord;
    } else {
        *this = nWord;
        *this <<= 8 * (nSize - 3);
    }
    if (pfNegative)
        *pfNegative = nWord != 0 && (nCompact & 0x00800000) != 0;
    if (pfOverflow)
        *pfOverflow = nWord != 0 && ((nSize > 34) ||
                                     (nWord > 0xff && nSize > 33) ||
                                     (nWord > 0xffff && nSize > 32));
    return *this;
}

uint32_t arith_uint256::GetCompact(bool fNegative) const
{
    int nSize = (bits() + 7) / 8;
    uint32_t nCompact = 0;
    if (nSize <= 3) {
        nCompact = GetLow64() << 8 * (3 - nSize);
    } else {
        arith_uint256 bn = *this >> 8 * (nSize - 3);
        nCompact = bn.GetLow64();
    }
    // The 0x00800000 bit denotes the sign.
    // Thus, if it is already set, divide the mantissa by 256 and increase the exponent.
    if (nCompact & 0x00800000) {
        nCompact >>= 8;
        nSize++;
    }
    assert((nCompact & ~0x007fffff) == 0);
    assert(nSize < 256);
    nCompact |= nSize << 24;
    nCompact |= (fNegative && (nCompact & 0x007fffff) ? 0x00800000 : 0);
    return nCompact;
}

总结

平时在阅读代码的时候没有注意太多,当仔细静下心来阅读和理解这些开源代码时,收获还是挺多的,虽然作为文字记录下来的时候表达还是不够清晰,
希望在后面不断的操练中能改善!

你可能感兴趣的:(区块链,比特币,编程)