比特币的私钥、公钥和地址生成方法源代码分析

本文所有源代码均来自Bitcoin Core 0.11

1.比特币的私钥

以下是《精通比特币》一书中关于私钥生成部分的描述:

生成密钥的第一步也是最重要的一步,是要找到足够安全的熵源,即随机性来源。生成一个比特币私钥在本质上与“在1到2^256之间选一个数字”无异。只要选取的结果是不可预测或不可重复的,那么选取数字的具体方法并不重要。比特币软件使用操作系统底层的随机数生成器来产生256位的熵(随机性)。通常情况下,操作系统随机数生成器由人工的随机源进行初始化,也可能需要通过几秒钟内不停晃动鼠标等方式进行初始化。对于真正的偏执狂,可以使用掷骰子的方法,并用铅笔和纸记录。
更准确地说,私钥可以是1和n-1之间的任何数字,其中n是一个常数(n=1.158 * 10^77 ,略小于2^256),并由比特币所使用的椭圆曲线的阶所定义(见4.1.5 椭圆曲线密码学解释)。要生成这样的一个私钥,我们随机选择一个256位的数字,并检查它是否小于n-1。从编程的角度来看,一般是通过在一个密码学安全的随机源中取出一长串随机字节,对其使用SHA256哈希算法进行运算,这样就可以方便地产生一个256位的数字。如果运算结果小于n-1,我们就有了一个合适的私钥。否则,我们就用另一个随机数再重复一次。

根据描述,我们找到比特币关于私钥生成部分的源代码:

void CKey::MakeNewKey(bool fCompressedIn) {
    RandAddSeedPerfmon();
    do {
        GetRandBytes(vch, sizeof(vch));
    } while (!Check(vch));
    fValid = true;
    fCompressed = fCompressedIn;
}

函数首先调用了RandAddSeedPerfmon()函数,让我们看一下这个函数:

void RandAddSeed()
{
    // Seed with CPU performance counter
    int64_t nCounter = GetPerformanceCounter();
    RAND_add(&nCounter, sizeof(nCounter), 1.5);
    memory_cleanse((void*)&nCounter, sizeof(nCounter));
}
void RandAddSeedPerfmon()
{
    RandAddSeed();
#ifdef WIN32
    // Don't need this on Linux, OpenSSL automatically uses /dev/urandom
    // Seed with the entire set of perfmon data

    // This can take up to 2 seconds, so only do it every 10 minutes
    static int64_t nLastPerfmon;
    if (GetTime() < nLastPerfmon + 10 * 60)
        return;
    nLastPerfmon = GetTime();

    std::vector vData(250000, 0);
    long ret = 0;
    unsigned long nSize = 0;
    const size_t nMaxSize = 10000000; // Bail out at more than 10MB of performance data
    while (true) {
        nSize = vData.size();
        ret = RegQueryValueExA(HKEY_PERFORMANCE_DATA, "Global", NULL, NULL, begin_ptr(vData), &nSize);
        if (ret != ERROR_MORE_DATA || vData.size() >= nMaxSize)
            break;
        vData.resize(std::max((vData.size() * 3) / 2, nMaxSize)); // Grow size of buffer exponentially
    }
    RegCloseKey(HKEY_PERFORMANCE_DATA);
    if (ret == ERROR_SUCCESS) {
        RAND_add(begin_ptr(vData), nSize, nSize / 100.0);
        memory_cleanse(begin_ptr(vData), nSize);
        LogPrint("rand", "%s: %lu bytes\n", __func__, nSize);
    } else {
        static bool warned = false; // Warn only once
        if (!warned) {
            LogPrintf("%s: Warning: RegQueryValueExA(HKEY_PERFORMANCE_DATA) failed with code %i\n", __func__, ret);
            warned = true;
        }
    }
#endif
}

在非windows平台直接调用RandAddSeed()函数,如上图。这里使用了GetPerformanceCounter()函数,其源代码:

static inline int64_t GetPerformanceCounter()
{
    int64_t nCounter = 0;
#ifdef WIN32
    QueryPerformanceCounter((LARGE_INTEGER*)&nCounter);
#else
    timeval t;
    gettimeofday(&t, NULL);
    nCounter = (int64_t)(t.tv_sec * 1000000 + t.tv_usec);
#endif
    return nCounter;
}  

QueryPerformanceCounter()查询性能计数器,此函数用于获取精确的性能计数器数值,如果存在则返回当前计数器值的地址 。gettimeofday()为Linux下获取精确时间的函数。可以看到这里均为了从系统中获取足够安全的熵源,即随机性来源,并将值赋给nCounter。随后使用RAND_add函数。RAND_add(&nCounter, sizeof(nCounter), 1.5);关于这个函数,详细解释如下:

void RAND_add(const void *buf,int num,double entropy)
增加随机数生成的不可预知性,将buf数组中num个数据加入PRNG中,entropy是对buf中数据的随机性估计值,如果entropy 和num相等,那么RAND_add函数与Rand_seed函数相同;
buf中的数据,一般采用系统中随机性的事件,比如一些交互性数据,用户敲击的键盘值,鼠标滑过的位置等等。

由此我们便利用系统生成了一个随机数种子,并将其加入PRNG(伪随机数生成器)中。
然后循环调用GetRandBytes()

void GetRandBytes(unsigned char* buf, int num)
{
    if (RAND_bytes(buf, num) != 1) {
        LogPrintf("%s: OpenSSL RAND_bytes() failed with error: %s\n", __func__, ERR_error_string(ERR_get_error(), NULL));
        assert(false);
    }
}

即计算RAND_bytes()

int RAND_bytes(unsigned char *buf,int num);
根据加密算法生成随机数,其实也是一个伪随机数,但是,如果在调用此函数之前,设定好随机种子,那么生成的随机数是不能被预先计算出来的。
buf:输出,生产的随机数存储的数组;
num: 输入,生产的随机数个数;
返回值:1成功,0失败;

直到满足eccrypto::Check(vch)函数要求停止,同时将fValid置为true。至此便生成了一个私钥。

2.比特币的公钥

通过椭圆曲线算法可以从私钥计算得到公钥,这是不可逆转的过程:K = k * G 。其中k是私钥,G是被称为生成点的常数点,而K是所得公钥。其反向运算,被称为“寻找离散对数”——已知公钥K来求出私钥k——是非常困难的,就像去试验所有可能的k值,即暴力搜索。

关于生成公钥的函数如下:

CPubKey CKey::GetPubKey() const {
    assert(fValid);
    CPubKey result;
    int clen = 65;
    int ret = secp256k1_ec_pubkey_create(secp256k1_context, (unsigned char*)result.begin(), &clen, begin(), fCompressed);
    assert((int)result.size() == clen);
    assert(ret);
    assert(result.IsValid());
    return result;
}

核心便是利用secp256k1_ec_pubkey_create()函数来根据之前生成的私钥产生对应的公钥。

3.比特币的地址

在交易中,比特币地址通常以收款方出现。如果把比特币交易比作一张支票,比特币地址就是收款人,也就是我们要写入收款人一栏的内容。一张支票的收款人可能是某个银行账户,也可能是某个公司、机构,甚至是现金支票。支票不需要指定一个特定的账户,而是用一个普通的名字作为收款人,这使它成为一种相当灵活的支付工具。与此类似,比特币地址的使用也使比特币交易变得很灵活。比特币地址可以代表一对公钥和私钥的所有者,也可以代表其它东西,比如脚本。现在,让我们来看一个简单的例子,由公钥生成比特币地址。
比特币地址可由公钥经过单向的加密哈希算法得到。哈希算法是一种单向函数,接收任意长度的输入产生指纹摘要。加密哈希函数在比特币中被广泛使用:比特币地址、脚本地址以及在挖矿中的工作量证明算法。由公钥生成比特币地址时使用的算法是Secure Hash Algorithm (SHA)和the RACE Integrity Primitives Evaluation Message Digest (RIPEMD),特别是SHA256和RIPEMD160。
以公钥 K 为输入,计算其SHA256哈希值,并以此结果计算RIPEMD160 哈希值,得到一个长度为160比特(20字节)的数字:
A = RIPEMD160(SHA256(K))

不考虑隔离见证,比特币有两种主要的交易类型:

  • P2PKH(pay to public key hash)
    该类型的脚本可将bitcoin锁定到某一公钥hash上。
  • P2SH(pay to script hash)
    该类型的脚本可将bitcoin锁定到某一脚本的hash值上。

分别对应两种不同的地址:

  • 公钥hash
    锁定到该种地址上的bitcoin,可用密钥签名即可解锁使用。
  • 脚本hash
    锁定到该种地址上的bitcoin,可用密钥签名+补偿脚本解锁使用。

3.1 对于P2PKH类型交易,地址生成函数如下:

//! Get the KeyID of this public key (hash of its serialization)
    CKeyID GetID() const
    {
        return CKeyID(Hash160(vch, vch + size()));
    }

其中Hash160分为SHA256和RIPEMD160两步

/** Compute the 160-bit hash an object. */
template
inline uint160 Hash160(const T1 pbegin, const T1 pend)
{
    static unsigned char pblank[1] = {};
    uint160 result;
    CHash160().Write(pbegin == pend ? pblank : (const unsigned char*)&pbegin[0], (pend - pbegin) * sizeof(pbegin[0]))
              .Finalize((unsigned char*)&result);
    return result;
}

3.2 对于P2SH类型交易,地址生成函数如下:

/** A reference to a CScript: the Hash160 of its serialization (see script.h) */
class CScriptID : public uint160
{
public:
    CScriptID() : uint160() {}
    CScriptID(const CScript& in);
    CScriptID(const uint160& in) : uint160(in) {}
};

此时便根据公钥生成了160比特(20字节)的地址。

4.总结

比特币由公钥以及地址的生成流程大致如下图:


比特币的私钥、公钥和地址生成方法源代码分析_第1张图片
来自《精通比特币》

你可能感兴趣的:(比特币的私钥、公钥和地址生成方法源代码分析)