40、C++项目实战:比特币地址生成器
说实话,比特币地址生成器这个项目,是我当年刚接触区块链时写的第一个练手项目。那时候我还在想,这玩意儿到底是怎么从一串乱码变成我们看到的那个以"1"或"bc1"开头的地址的?后来自己动手撸了一遍代码,才真正搞明白——说白了,就是哈希、编码、校验这三板斧。
今天我就带你从零开始,用C++手写一个比特币地址生成器。别怕,代码量不大,但每一步都有讲究。
比特币地址的生成流程
先看整体流程,我画了张图,你一看就明白:
嗯,流程不复杂,但每一步都有坑。我当初第一次写的时候,就在Base58编码上栽了跟头——编码表顺序搞反了,生成的地址死活不对。后来对着比特币源码一行行比对才找到问题。
核心数据结构
先定义几个基础类型,方便后面操作:
#include <iostream>
#include <vector>
#include <string>
#include <cryptopp/sha.h>
#include <cryptopp/ripemd.h>
#include <cryptopp/eccrypto.h>
#include <cryptopp/oids.h>
#include <cryptopp/hex.h>
using namespace CryptoPP;
using std::vector;
using std::string;
// 字节数组类型别名
using ByteArray = vector<uint8_t>;
// 比特币地址结构
struct BitcoinAddress {
string privateKeyHex; // 私钥(十六进制)
string publicKeyHex; // 公钥(十六进制)
string address; // Base58地址
};
这里我用的是Crypto++库,业界公认的密码学库,靠谱。我个人习惯用vector<uint8_t>来存字节数据,比裸指针安全得多。
第一步:生成私钥
私钥就是一个256位的随机数。比特币的安全性全押在这上面了——随机数质量不行,地址就可能被碰撞到。
ByteArray GeneratePrivateKey() {
AutoSeededRandomPool rng;
ByteArray privKey(32);
rng.GenerateBlock(privKey.data(), privKey.size());
return privKey;
}
我曾经见过有人用rand()生成私钥,吓得我赶紧给他科普了一顿——rand()的周期才2^31,暴力破解分分钟的事。一定要用密码学安全的随机数生成器。
第二步:从私钥推导公钥
比特币用的是secp256k1椭圆曲线。私钥乘以生成点,得到公钥。这个乘法是单向的——从公钥反推私钥,目前没人能做到。
ByteArray GetPublicKey(const ByteArray& privateKey) {
ECDSA<ECP, SHA256>::PrivateKey privKey;
privKey.Initialize(
ASN1::secp256k1(),
privateKey.data(), privateKey.size()
);
ECDSA<ECP, SHA256>::PublicKey pubKey;
privKey.MakePublicKey(pubKey);
// 获取未压缩公钥(65字节:0x04 + x坐标 + y坐标)
ByteArray pubKeyBytes(65);
pubKey.GetPublicKeyByteArray(pubKeyBytes.data(), pubKeyBytes.size());
return pubKeyBytes;
}
注意这里返回的是未压缩公钥,以0x04开头。压缩公钥能省一半空间,但生成地址的流程是一样的。
第三步:哈希运算
公钥先过SHA-256,再过RIPEMD-160,得到20字节的公钥哈希。这一步叫"双重哈希",比特币里到处都用这个套路。
ByteArray HashPublicKey(const ByteArray& publicKey) {
// SHA-256
SHA256 sha;
ByteArray sha256Hash(SHA256::DIGESTSIZE);
sha.CalculateDigest(sha256Hash.data(), publicKey.data(), publicKey.size());
// RIPEMD-160
RIPEMD160 ripemd;
ByteArray ripemdHash(RIPEMD160::DIGESTSIZE);
ripemd.CalculateDigest(ripemdHash.data(), sha256Hash.data(), sha256Hash.size());
return ripemdHash;
}
你想想看,为什么不用一次哈希?因为SHA-256虽然强,但万一哪天被攻破了呢?双重哈希相当于上了双保险。比特币的地址设计,处处体现着"保守"二字。
第四步:添加版本字节和校验和
主网地址的版本字节是0x00。校验和是取版本字节+公钥哈希的SHA-256再SHA-256的前4个字节。
ByteArray AddChecksum(const ByteArray& payload) {
// 计算双重SHA-256
SHA256 sha;
ByteArray hash1(SHA256::DIGESTSIZE);
sha.CalculateDigest(hash1.data(), payload.data(), payload.size());
ByteArray hash2(SHA256::DIGESTSIZE);
sha.CalculateDigest(hash2.data(), hash1.data(), hash1.size());
// 取前4字节作为校验和
ByteArray result = payload;
result.insert(result.end(), hash2.begin(), hash2.begin() + 4);
return result;
}
ByteArray PreparePayload(const ByteArray& hash160) {
ByteArray payload = {0x00}; // 主网版本字节
payload.insert(payload.end(), hash160.begin(), hash160.end());
return AddChecksum(payload);
}
这里有个细节:校验和是双重SHA-256。比特币的区块头哈希也是双重SHA-256。我猜中本聪当时就是图省事,一个函数到处复用。
第五步:Base58编码
Base58去掉了容易混淆的字符:0/O/I/l。编码过程就是不断除以58,取余数映射到字母表。
const string BASE58_ALPHABET =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
string Base58Encode(const ByteArray& data) {
// 统计前导零
int leadingZeros = 0;
for (auto b : data) {
if (b == 0) leadingZeros++;
else break;
}
// 大数除法
ByteArray digits(data.begin(), data.end());
string result;
while (!digits.empty()) {
int remainder = 0;
ByteArray next;
for (auto b : digits) {
int value = (remainder << 8) + b;
int quotient = value / 58;
remainder = value % 58;
if (!next.empty() || quotient != 0) {
next.push_back(quotient);
}
}
result.insert(result.begin(), BASE58_ALPHABET[remainder]);
digits = next;
}
// 前导零映射为'1'
for (int i = 0; i < leadingZeros; i++) {
result.insert(result.begin(), '1');
}
return result;
}
我曾经在Base58编码上栽过跟头——前导零的处理。比特币地址开头的"1"其实代表前导零字节。如果你忘了处理这个,生成的地址会少一个字符,校验永远通不过。
组装起来
把上面所有步骤串起来,就是完整的地址生成器:
BitcoinAddress GenerateBitcoinAddress() {
BitcoinAddress addr;
// 1. 生成私钥
auto privKey = GeneratePrivateKey();
HexEncoder encoder;
encoder.Attach(new StringSink(addr.privateKeyHex));
encoder.Put(privKey.data(), privKey.size());
encoder.MessageEnd();
// 2. 生成公钥
auto pubKey = GetPublicKey(privKey);
encoder.Attach(new StringSink(addr.publicKeyHex));
encoder.Put(pubKey.data(), pubKey.size());
encoder.MessageEnd();
// 3. 哈希公钥
auto hash160 = HashPublicKey(pubKey);
// 4. 添加版本和校验
auto payload = PreparePayload(hash160);
// 5. Base58编码
addr.address = Base58Encode(payload);
return addr;
}
int main() {
auto addr = GenerateBitcoinAddress();
std::cout << "私钥: " << addr.privateKeyHex << std::endl;
std::cout << "公钥: " << addr.publicKeyHex << std::endl;
std::cout << "地址: " << addr.address << std::endl;
return 0;
}
验证地址有效性
生成地址后,怎么确认它是对的?你可以用在线区块链浏览器查一下,或者自己写个验证函数:
bool ValidateAddress(const string& address) {
// 解码Base58
auto decoded = Base58Decode(address);
if (decoded.size() != 25) return false; // 1字节版本 + 20字节哈希 + 4字节校验
// 分离校验和
ByteArray payload(decoded.begin(), decoded.begin() + 21);
ByteArray checksum(decoded.begin() + 21, decoded.end());
// 重新计算校验和
auto expected = AddChecksum(payload);
ByteArray expectedChecksum(expected.end() - 4, expected.end());
return checksum == expectedChecksum;
}
调试的时候,你可以用已知的私钥来验证——比如比特币创世区块的私钥(虽然没人知道)。更实际的做法是:生成一个地址,转几个聪的测试币进去,再花掉。能花出去,说明地址没问题。
性能与安全考量
| 环节 | 性能瓶颈 | 安全风险 | 我的建议 |
|---|---|---|---|
| 私钥生成 | 几乎无 | 随机数质量 | 用硬件随机源 |
| 椭圆曲线乘法 | CPU密集 | 侧信道攻击 | 用恒定时间实现 |
| 哈希运算 | 中等 | 碰撞风险极低 | 双重哈希保底 |
| Base58编码 | 大数除法慢 | 无 | 可预计算优化 |
说实话,单次生成地址的性能不是问题——毫秒级就搞定了。但如果你要批量生成(比如搞HD钱包),椭圆曲线乘法就成了瓶颈。我有个项目需要每秒生成上万个地址,最后用OpenMP并行化才搞定。
写在最后
比特币地址生成器,麻雀虽小五脏俱全。它涵盖了密码学、编码、校验这些基础但核心的知识点。你把这个项目吃透了,再去理解区块链的其他概念——交易签名、Merkle树、区块头——就会轻松很多。
嗯,代码我已经跑通了,你可以直接拿去用。但记住:这只是教学示例,生产环境一定要用经过审计的库,比如libsecp256k1。自己写的密码学代码,说白了就是给自己挖坑——我当年就吃过这个亏。
公众号:蓝海资料掘金营,微信deep3321