科普

比特币用户很喜欢讨论“非对称加密”、“椭圆曲线”、“量子计算机”这类高深莫测的话题,然后再以一种非常莫名其妙的方式把币弄丢,比如说:“随机”。

历史上多起各品牌的钱包用户丢币事件,都是因为随机函数存在问题。

随机很重要,对于比特币这种密码学电子货币来说,尤其重要。可惜社区内对于随机的讨论并不多,导致很多人缺乏正确的认识,因此,我们今天就和大家聊聊随机。

说到随机,有两个必须要搞清楚的概念:“真随机数生成器”(TRNG)和伪随机数生成器(PRNG)。

大部分计算机程序和语言中的随机函数,都是伪随机数生成器,它们都是由确定的算法,通过一个“种子”(比如“时间”),来产生“看起来随机”的结果。

毫无疑问,任何人只要知道算法和种子,或者之前已经产生了的随机数,都可能获得接下来随机数序列的信息。因为它们的可预测性,在密码学上并不安全,所以我们称其为“伪随机”。这种随机数,用来让游戏里的小人跑跑路没多大问题,如果用来生成比特币私钥,那可就太不安全了。

再说说真随机数生成器,中文维基中,将“硬件随机数生成器”(HRNG)等同于真随机数生成器,这其实并不十分准确,严格意义上的真随机可能仅存在于量子力学之中,我们当前所想要的(或者所能要的),并不是这种随机。

我们其实想要一种不可预测的、统计意义上的、密码学安全的随机数,只要能做到这一点的随机数生成器,都可以称其为真随机数生成器。这种真随机,并不一定非得是特殊设计的硬件,Linux操作系统内核中的随机数生成器(/dev/random),维护了一个熵池(搜集硬件噪声,如:键盘、鼠标操作、网络信号强度变化等),使得它能够提供最大可能的随机数据熵,因此同样是高品质的真随机数生成器。

不过/dev/random是阻塞的,也就是说,如果熵池空了,对于/dev/random的读操作将被挂起,直到收集到足够的环境噪声为止。

因此,在开发程序时,我们应使用/dev/urandom,作为/dev/random的一个副本,它不会阻塞,但其输出的熵可能会小于/dev/random

好了,在说了这么多之后,在我们开发比特币应用时,应该使用何种随机数生成器来生成私钥呢?

答案很简单:urandom。永远只用urandom

不要使用任何第三方的随机数解决方案,哪怕是一些高级的安全库,所提供的声称“非常安全”的随机函数。因为它们都是用户态的密码学随机数生成器,而urandom是内核态的随机数生成器,内核有权访问裸设备的熵,内核可以确保,不在应用程序间,共享相同的状态。

历史上,无数次随机数失败案例,大多出现在用户态的随机数生成器,而且,用户态的随机数生成器几乎总是要依赖于,内核态的随机数生成器(如果不依赖,那风险则更大),除了没准儿能简化您的某些开发工作,丝毫看不出任何额外的好处,反而增加了因引入第三方代码,所可能导致的潜在安全风险。

因此,开发者在需要密码学安全的随机数时,应使用urandom

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import secrets  # 导入secrets模块,用于生成安全随机数
import colorama # 导入colorama模块,用于在控制台中添加颜色
import os
from colorama import Fore, Style # 导入colorama中的前景色和样式
import ecdsa # 导入ecdsa模块,用于椭圆曲线数字签名算法
import base58 # 导入base58模块,用于Base58编码和解码
from Crypto.Hash import keccak # 从Crypto模块中的Hash子模块中导入keccak算法,用于计算Keccak256哈希

import argparse

def generate_secure_private_key():
# 生成32字节的安全随机原始私钥
raw = os.urandom(32)
return raw

def keccak256(data): # 定义函数keccak256,接受一个数据作为参数
hasher = keccak.new(digest_bits=256) # 创建一个Keccak256哈希对象
hasher.update(data) # 更新哈希对象的数据
return hasher.digest() # 返回哈希结果

def get_signing_key(raw_priv): # 定义函数get_signing_key,接受一个原始私钥作为参数
return ecdsa.SigningKey.from_string(raw_priv, curve=ecdsa.SECP256k1) # 使用原始私钥生成签名密钥对象,使用SECP256k1曲线

def verifying_key_to_addr(key): # 定义函数verifying_key_to_addr,接受一个公钥作为参数
pub_key = key.to_string() # 获取公钥的字节串表示
primitive_adder = b"\x41" + keccak256(pub_key)[-20:] # 生成TRON地址的原始字节串
Addr = base58.b58encode_check(primitive_adder) # 对原始字节串进行Base58编码得到TRON地址
return Addr # 返回TRON地址

def generate_wallet(bits, name, quiet):
count = 0 # 初始化计数器count为0
print(Fore.CYAN + "Please Wait..." + Style.RESET_ALL) # 输出提示信息,请等待...
while True: # 进入无限循环
raw = generate_secure_private_key()
#raw = secrets.token_bytes(32) # 生成32字节的安全随机原始私钥,和raw = os.urandom(32)等价
key = get_signing_key(raw) # 获取签名密钥对象
Wallet = verifying_key_to_addr(key.get_verifying_key()).decode() # 获取TRON钱包地址
# 判断钱包地址后缀是否为4位相同的字母或数字
if Wallet[-bits:-1] == Wallet[-1] * (bits - 1):
HexAdd = base58.b58encode_check(Wallet.encode()).hex() # 对TRON地址进行Base58编码并转换为十六进制字符串
publickey = key.get_verifying_key().to_string().hex() # 获取公钥的十六进制字符串表示
privatekey = raw.hex() # 获取原始私钥的十六进制字符串表示
count += 1 # 计数器加1
if not quiet:
print(str(count) + " | " + privatekey + " | " + Wallet) # 输出当前生成的私钥和TRON地址
with open(f"{name}Key.txt", "a") as f: # 打开文件trxKey.txt,以追加模式写入
f.write(privatekey + '\n') # 将私钥写入文件,并换行
with open(f"{name}Add.txt", "a") as f1: # 打开文件trxAdd.txt,以追加模式写入
f1.write(Wallet + '\n') # 将TRON地址写入文件,并换行



def main():
# 定义命令行解析器对象
parser = argparse.ArgumentParser(description='生成Tron钱包豹子尾号地址', add_help=False) # 添加 add_help=False 来禁用默认帮助信息
# 添加自定义帮助信息参数
parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='显示此帮助信息并退出程序,使用:-h or --help')
# 静默执行
parser.add_argument('-q', '--quiet', action='store_true', help='是否将生成的公私钥输出到控制台')
# 添加命令行参数
parser.add_argument('-b', '--bits', type=int, default=4, help='钱包地址相同后缀位数,使用:-b number or --bits number')
# 添加输出文件自定义名称
parser.add_argument('-n', '--name', type=str, default='tron', help='输出文件的名称,使用:-n name or --name name')
# 解析命令行参数
args = parser.parse_args()
# 调用生成钱包函数,并传入参数
generate_wallet(args.bits, args.name, args.quiet)

print('Number of bits:', args)


if __name__ == "__main__":
main()

执行

  1. 新建虚拟环境「可选」

    python3 -m venv myenv

    进入虚拟环境

    source myenv/bin/activate

  2. 安装依赖

    pip install colorama ecdsa base58 pycryptodome

  3. 运行

    python xx.py -h

    执行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ python ./XXX.py -h
    usage: XXX.py [-h] [-q] [-b BITS] [-n NAME]

    生成Tron钱包豹子尾号地址

    optional arguments:
    -h, --help 显示此帮助信息并退出程序,使用:-h or --help
    -q, --quiet 是否将生成的公私钥输出到控制台
    -b BITS, --bits BITS 钱包地址相同后缀位数,使用:-b number or --bits number
    -n NAME, --name NAME 输出文件的名称,使用:-n name or --name name

    $ python ./XXX.py -b 3
    Please Wait...
    1 | 9e533918217dea1c77e3aa32c509f5454ec2b895668f9aa2700e713b19fd5720 | TFotRzQVXspDtr1ebMZmL2LJFDkzU2Keee
    2 | 1fe5f63d4dad586e6e78259b792752fa68ce81e00b555a9bc7b88f85f78cf0a1 | TAzGmUqePT3VYo1wBHYHkmVyCa5oXGCSSS

    上面代码,os.urandom(32) 是一种非常安全的随机数生成方法,特别适合用于密码学用途。它通过操作系统提供的加密随机数生成器生成随机字节,满足高安全性的需求,os.urandom 调用的是操作系统底层的加密随机数生成器。

    • 在 Linux 、Mac上,它会使用 /dev/urandom
    • 在 Windows 上,它会调用 CryptGenRandom 或类似的 API。

    这些随机数生成器的设计目标是提供密码学安全级别的随机性,因此 os.urandom(32) 是足够安全的,特别是用于生成私钥。

替代方案

如果需要更高的控制或对随机性来源有更严格的要求,可以使用以下方法,但它们通常不必要:

  • Python 3.6+ 中的 secrets 模块:

    1
    2
    import secrets
    raw = secrets.token_bytes(32)

    secrets 是专为密码学设计的模块,与 os.urandom 底层依赖相同,但提供了更高级的接口。

参考