计算机程序中使用随机数是常见的,常用来做数据校验、密码或其他安全用途。比如很多短信验证码就是6位数的随机数字。之所以一般是6位因为要考虑用户的记忆和输入方便。太长了用户记不住输入也不方便,它可以结合验证码的时效和对应的手机号码来提高安全性。但在Bitcoin中,这种有限长度的数字作为随机数,在强大的算力面前分分钟被破解。作为Bitcoin的随机数生成器(RNG)使用了大量高强度的随机数来提高系统安全,随机数的核心的应用就是钱包(wallet)系统的私钥(有关钱包系统在以后的章节中会介绍)。私钥是一个长度为64的十六进制字符串(32个字节,256位)。为了保证安全,系统必须保证随机数不被猜中而且是唯一的。对于Bitcoin这种开放、几乎人人皆可随时访问的区块链数据库,其中钱包系统的安全等级最高,它要求在现有的运算条件下对私钥的破解概率降低到几乎是零。因为一旦私钥遭破解就可以操控钱包账户的一切资金。要做到这样的高安全就必须保证随机生成的私钥有最安全的熵源。
计算机中的常用的随机数我们通常称之为伪随机数(Pseudo-random),它不是真正的随机,而是按照某种统计学算法生成的。在C++语言中很多生成随机数的函数,比如:srand、rand等,但这些远不能满足Bitcoin安全的需求,这些函数几乎排除在Bitcoin能使用的函数之外。熵作为混乱程度的度量,熵源可以理解为计算随机数的数据来源。必须保证这些数据是高度混乱的、不规律的,以确保攻击者无法预测和破解。
关于熵
维基百科中关于熵是这样解释的:在信息论中,熵(entropy)是接收的每条消息中包含的信息的平均量,又被称为信息熵、信源熵、平均自信息量。这里,“消息”代表来自分布或数据流中的事件、样本或特征。(熵最好理解为不确定性的量度而不是确定性的量度,因为越随机的信源的熵越大。)来自信源的另一个特征是样本的概率分布。熵的概念最早起源于物理学,用于度量一个热力学系统的无序程度。在信息论里面,熵是对不确定性的测量。但是在信息世界,熵越高,则能传输越多的信息,熵越低,则意味着传输的信息越少。
比特币随机数生成器和熵源的设计
Bitcoin中随机数据生成的主要函数代码(注释有对应的文件名):
// https://github.com/bitcoin/bitcoin/blob/master/src/random.cpp
static void ProcRand(unsigned char* out, int num, RNGLevel level) noexcept
{
// Make sure the RNG is initialized first (as all Seed* function possibly need hwrand to be available).
RNGState& rng = GetRNGState();
assert(num <= 32);
CSHA512 hasher;
switch (level) {
case RNGLevel::FAST:
SeedFast(hasher);
break;
case RNGLevel::SLOW:
SeedSlow(hasher, rng);
break;
case RNGLevel::PERIODIC:
SeedPeriodic(hasher, rng);
break;
}
// Combine with and update state
if (!rng.MixExtract(out, num, std::move(hasher), false)) {
// On the first invocation, also seed with SeedStartup().
CSHA512 startup_hasher;
SeedStartup(startup_hasher, rng);
rng.MixExtract(out, num, std::move(startup_hasher), true);
}
}
下面分别说明三种级别的随机数以及熵源设计:
1.RNGLevel::FAST
顾名思义就是比较快速生成随机数,计算时间相对短,复杂程度相对低,构造流程如下图:
- Stack pointer: 使用了一个32字节长度的buffer在堆栈中的地址。该地址长度一般为8字节。注意是buffer的堆栈地址,而不是buffer本身的值。堆栈地址是高度随机且外部用户无法获取的安全地址。
- SeedHardwareFast:调用GetRdRand从芯片上的硬件随机数生成器中获取随机数。所用到的随机数生成器由芯片上的熵池初始化。在调用之前,首先在随机数生成器RNGState的初始化中调用InitHardwareRand来判断CPU处理器是否支持RDRAND和RDSEED指令。RDSEED和RDRAND提供了访问硬件熵池的高级方法,功能比较类似,它们都是计算机的硬件指令。其中RDSEED指令的熵来自时序线路,并且使用硅片上的热噪声来以3GHz的速度输出随机比特流。在Linux C++中,使用内联的汇编指令来完成这些指令调用。
- SeedTimestamp:调用GetPerformanceCounter来读取CPU时间计数器,返回一个无符号的64位整数。该数字同样具有高度随机性和不可预测性。
- MixExtract:混合熵并计算512位的哈希值。首先在熵中混合上一次计算的Hash值,同时混合一个64位的无符号计数器,然后计算Hash-512,并将计算结果保存在m_state中供下一次使用。
- SeedStartup: 继续构造随机数的熵源,上面步骤构造的熵源已经在暂存在m_state中,该函数继续执行SeedHardwareSlow,它与SeedHardwareFast类似,加入了RNDSEED随机数。熵源包括:SeedSlow(在下面介绍)、RandAddDynamicEnv动态的环境变量数据,RandAddStaticEnv静态的环境变量数据、SeedStrengthen执行10毫秒强度的哈希运算并混合熵源。其中SeedStartup只执行一次。
- MixExtract:再次混合熵源并计算哈希值。
通过上述复杂和多重混合计算,其目的是为了构造一个高度随机且无法预测的熵源种子随机数,最后通过计算熵源的512位的哈希值获得指定长度的随机值(最长64个字节)。且每一次计算都是根据上一次计算的结果再进行混合并不断累积,从而使信息熵越来越高,随机值也越趋安全。
2. RNGLevel::SLOW
计算时间较长,较复杂,混合的熵源比较多。如下图:
它在SeedFast的基础上增加了:
- GetOSRand:用于获取系统设设备的随机数
- SeedEvents:它是RNGState中根据随机事件来混合熵源,当系统连接到一个新的节点时,调用RandAddEvent函数器混合一个32位无符号的的节点id,其目的是系统在运行过程中动态混合一些新的随机种子,以提高信息源的熵强度,从而提高随机数的安全性。
3.RNGLevel::PERIODIC
系统会周期(每隔一分钟)调用RandAddPeriodic,从而不断进行哈希运算和累积熵,从而提高熵的不确定性和安全性。如下图(蓝色背景部分为代码):
它与SLOW和FAST区别是增加了SeedStrengthen,其用意是根据CPU的时钟来加熵的强度。这三种级别的算法不是独立的,而是相互关联的,并且不断累积熵源。
快速随机数(FastRandomContext)
相对于钱包私钥使用的随机数需要很高的密码安全特性,一般的随机数,如使用随机数来命名文件就不需要经过上述那么复杂的运算。因此Bitcoin中针对这些开发了快速随机数的算法,如下图所示:
快速随机数生成器有两个主要函数rand256和randbytes。rand256获取256位长度的随机数。randbytes获取指定字节长度的随机数(不超过64个字节数)。与上面介绍的通过熵源构造的随机数生成器RNGState不同的是,快速随机数使用了ChaCha20(一种新型的流密码算法)伪随机的密钥流(keystream)的算法。通过/src/random.cpp的代码得知,在FastRandomContext的构造函数中,通过对chacha20设定key来计算keystream。默认情况下,必须首先请求RandomSeed来获取一个key,该key通过RNGState来获取。如果用户在构造FastRandomContext时设定了key,则不需要通过定RandomSeed来获得key。Key值是ChaCha20算法计算keystream的基础(关于ChaCha20算法在后面的章节介绍)。
下面的代码是FastRandomContext的两个构造函数:
FastRandomContext::FastRandomContext(bool fDeterministic) noexcept :
requires_seed(!fDeterministic), bytebuf_size(0), bitbuf_size(0)
{
if (!fDeterministic) {
return;
}
uint256 seed;
rng.SetKey(seed.begin(), 32);
}
FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), bytebuf_size(0), bitbuf_size(0)
{
rng.SetKey(seed.begin(), 32);
}
第一个构造函数表示需要RandomSeed来设定key。第二个构造函数表明用户指定了key。这里的uint256是一个256位32字节长度的uint8_t类型的数组。
使用SHA-512获取随机值
Bitcoin系统中获取高安全随机数,其中钱包系统的256位私钥,都是通过复杂的计算和混合不同的随机数据来获得高度混乱和不可预测的熵源,并最终计算它的SHA-512来获得随机值。该随机值具有很高的密码安全性,使黑客攻击变得几乎不可能。有关SHA-512的算法在后面的章节再介绍,需注意的是快速随机数算法并不需要计算SHA-512,而是通过Chacha20算法实现的。
代码位置
关于本章随机数的相关代码主要在Bitcoin下面的文件中实现:
https://github.com/bitcoin/bitcoin/blob/master/src/random.cpp