八.账号生成规则 哈希 密钥

账号格式

在之前我们用拼音名代替账号,现在我们来认识比特币中真正的账号格式:

1KY9b81NiCDYzYPTH7yLAR3ZAD7jKejdLG

这是1个典型的比特币账号(地址),以1开头,总共34个字符,区分大小写。

这种格式称为Pay-to-Public-Key-Hash (P2PKH),意思是支付给公钥的的哈希,是比特币的公钥经过哈希再进行base58编码而得到的账号,顾名思义,这种格式不是公钥,而是还要哈希一下。即对公钥还要经过哈希。

当然还有其它的格式,比如Pay-to-Public-Key (P2PK),直接就是公钥做账号,这是最原始的账号形式,还有P2WPKH / P2WSH 等格式(bc1开头),这里我们就拿主流的P2PKH格式来做说明。

关于P2PKH用base58编码而非base64这里也有个有意思的原因,稍后做说明。

生成来源

可以看到,p2pkh账号是通过公钥编码得来的,那么公钥怎么来的呢?公钥是通过私钥生成的,

私钥是什么,私钥是一串32字节,256位二进制的数字,这个是随机的。比如我说的小一点。

一个5位数的数字,12345,28723,99999,都可以用来当作你的私钥,然后通过算法(椭圆算法)计算出公钥,注意这种算法是单向的,无法从公钥反推出私钥,当然这里的5位数字太小了,这样极不安全,很容易被破解。我这里只是举个易懂的例子。

所以这是一个64位十六进制,也即256位二进制的数字,这个数字约等于无穷大,从安全性来讲,你找不到别人的那个数字,也就是私钥,包括暴力计算。从0开始不停的生成公钥找到匹配你的那个公钥。

所以你自己现在可以从里面随机找一个,然后再通过算法生成公钥,你就拥有了属于自己的账号了,注意保护好你的私钥哦,也就是你的那个64位的十六进制的数字。

公私钥关系及说明

私钥经过椭圆算法计算出来公钥,然后呢,仅此而已吗?不,这个算法有个非常有用的特性。

即,这样生成的公私钥,可以用私钥对一串数据进行签名(用特定算法),然后得到签名(其实也是一串数据),可以用公钥来验证是否一致。

这样有什么用呢,大家可能不太理解,我用加解密来说明吧,事实上这不是加解密行为,只是为了便于理解。

比如一个私钥,23345,加密生成了一个公钥22号,现在22号账号有50btc,22号想转账给1号10个btc。

那么怎么说明呢?

那么转账行为即 "22 to 1 50btc",我用私钥把这串数字加密签名,得到签名"xxjjwwww"。

然后你可以只用公钥22,来验证22 to 1 50btc,是否被22的私钥签过名(签名数据xxjjwwww),如果一致。则证明确实是22号想转钱给别人,在不用私钥的情况下能确认账号的所属者。上面这三者,你改了一个字一个标点符号,验证都会出错。

当然实际这个签名验证,不是加解密行为,而是一种签名算法:ECDSA(基于 secp256k1 椭圆曲线),它的工作过程是这样的:

ECDSA(基于 secp256k1 椭圆曲线)

1.签名:用私钥基于数据z生成r, s(两个 256 位整数)

2.验证:根据验证公式,公钥依赖于数据z,和r,s,会计算出一个点P出来。

3.然后验证P的x坐标是否等于签名里的r值,如果相等,那么就证明这个数据确实被你签名过。

4.公钥也是椭圆算法算出来的,所以它们同属一个私钥的话,它们就会有上述的关系。

公钥格式

这个通过椭圆算法从私钥算出来的公钥,长度为65个字节,最前面一个字节规定是04,表明是未压缩的。实际有效数据可以看成64个字节,512位二进制,当然还有另一种长度,是压缩的,前缀不是04,而是02,03这个是表明压缩格式的。

比如在源码中,创世块的账户直接是65个字节的公钥做账户名,如下:

cpp 复制代码
 txNew.vout[0].scriptPubKey = CScript() << 
CBigNum("0x5F1DF16B2B704C8A578D0BBAF74D385CDE12C11EE50455F3C438EF4C3FBCF649B6DE611FEAE06279A60939E028A8D65C10B73071A6F16719274855FEB0FD8A6704") 
<< OP_CHECKSIG;

当然后面的版本为了节省空间,是用压缩的方法。

这里我们可以看到一个奇怪的点,开头并不是04,而5f,还是由于大小端序的问题,我们需要反着看,末尾是04.

实际上以太坊的公钥也是同样的算法生成的,只不过是去掉了前缀04,是64个字节,然后不同的是比特币最终采用base58编码,以太坊是哈希公钥后,得到32字节的哈希值,然后直接取后20个字节,前面加上0x,这就是以太坊的账户生成规则。

我们后面将以未压缩的公钥格式来举例。

base58编码

说到base58编码,就不得不说到base64编码,什么是base64编码,就是大小写26个字母,加上0-9的数字,再加上"+" "/"这两个字符。总共有64个。

这种编码用来表达一串数字,其实你可以把它看作是64进制的数字。

那么base58就是58进制,只不它比base64少了,0(数字0),O(o的大写字母),l(L的小写字母),I(i的大小字母),+和/,总共6个字符,为什么要去掉这些,因为这个字符做账号时容易引起混淆,那么64-6=58,就变成了58进制,这就是base58编码的由来,也是比特币为什么采用base58编码的原因。

比特币账号生成规则

接一下来说一下账号详细的生成过程。

1.先对公钥进行sha256哈希,得到32字节的数据。

2.然后再次这个哈希值进行RIPEMD-160哈希,得到20字节的数据。

RIPEMD-160也是一种哈希算法,只不过跟sha256不同的是,看名称后面,就可以明白,256代表32个字节,那160肯定就对应着20个字节了。

3.给这个20字节前面加上版本号"0x00"

4.对这21个字节,进行双重sha256哈希,然后得到一个值,取前4个字节作为校验和

5.将这4个字节加到第3步结果的后面。

6.然后对这个结果进行base58编码。

其实,核心地址,在第2步就已经生成了,就是对公钥进行两次哈希,一次sha256,一次RIPEMD-160。

后面的加版本号0x00,在进行58编码后,对应着58编码里的数字1,这就是为什么账号地址是1开头。

然后又为了,防止输入和传输错误,又加上了校验和,即对这个地址进行两次哈希,得到一个哈希值后,取前四个字节加在账号后面,这就形成了最终的账号。

这个校验和让你可以识别一个账号是不是合法的地址,通过去掉后四个字节,然后对数据进行两次哈希,看这个值的前4个字节是否一致。如果你是胡乱打一个账号,那肯定是不一致的,这不是一个合法的账号。

CKey类说明

在源码中,是使用Ckey类来生成公私钥的,可随机生成。

这个类在key.h文件中,我们新建一个key.h头文件,然后将下面的代码复制进去:

cpp 复制代码
// Copyright (c) 2009 Satoshi Nakamoto
// Distributed under the MIT/X11 software license, see the accompanying
// file license.txt or http://www.opensource.org/licenses/mit-license.php.


// secp160k1
// const unsigned int PRIVATE_KEY_SIZE = 192;
// const unsigned int PUBLIC_KEY_SIZE  = 41;
// const unsigned int SIGNATURE_SIZE   = 48;
//
// secp192k1
// const unsigned int PRIVATE_KEY_SIZE = 222;
// const unsigned int PUBLIC_KEY_SIZE  = 49;
// const unsigned int SIGNATURE_SIZE   = 57;
//
// secp224k1
// const unsigned int PRIVATE_KEY_SIZE = 250;
// const unsigned int PUBLIC_KEY_SIZE  = 57;
// const unsigned int SIGNATURE_SIZE   = 66;
//
// secp256k1:
// const unsigned int PRIVATE_KEY_SIZE = 279;
// const unsigned int PUBLIC_KEY_SIZE  = 65;
// const unsigned int SIGNATURE_SIZE   = 72;
//
// see www.keylength.com
// script supports up to 75 for single byte push


#include <openssl/ec.h>         // EC_KEY_new_by_curve_name, EC_KEY_generate_key 等
#include <openssl/ecdsa.h>      // ECDSA_sign, ECDSA_verify

#include <openssl/obj_mac.h>    //支持NID_secp256k1定义
class key_error : public std::runtime_error
{
public:
    explicit key_error(const std::string& str) : std::runtime_error(str) {}
};


// secure_allocator is defined is serialize.h
//typedef vector<unsigned char, secure_allocator<unsigned char> > CPrivKey;
//不采用上面的secure_allocator,会有版本兼容性问题,少了私钥安全擦除问题
//就是变量释放后,还得将数据擦除,否则内存块有残留数据之类的。
// 少了这个功能对程序运行无影响,只是这样不太安全。
typedef vector<unsigned char> CPrivKey;


class CKey
{
protected:
    EC_KEY* pkey;

public:
    CKey()
    {
        pkey = EC_KEY_new_by_curve_name(NID_secp256k1);
        if (pkey == NULL)
            throw key_error("CKey::CKey() : EC_KEY_new_by_curve_name failed");
    }

    CKey(const CKey& b)
    {
        pkey = EC_KEY_dup(b.pkey);
        if (pkey == NULL)
            throw key_error("CKey::CKey(const CKey&) : EC_KEY_dup failed");
    }

    CKey& operator=(const CKey& b)
    {
        if (!EC_KEY_copy(pkey, b.pkey))
            throw key_error("CKey::operator=(const CKey&) : EC_KEY_copy failed");
        return (*this);
    }

    ~CKey()
    {
        EC_KEY_free(pkey);
    }

    void MakeNewKey()
    {
        if (!EC_KEY_generate_key(pkey))
            throw key_error("CKey::MakeNewKey() : EC_KEY_generate_key failed");
    }

    bool SetPrivKey(const CPrivKey& vchPrivKey)
    {
        const unsigned char* pbegin = &vchPrivKey[0];
        if (!d2i_ECPrivateKey(&pkey, &pbegin, vchPrivKey.size()))
            return false;
        return true;
    }

    CPrivKey GetPrivKey() const
    {
        unsigned int nSize = i2d_ECPrivateKey(pkey, NULL);
        if (!nSize)
            throw key_error("CKey::GetPrivKey() : i2d_ECPrivateKey failed");
        CPrivKey vchPrivKey(nSize, 0);
        unsigned char* pbegin = &vchPrivKey[0];
        if (i2d_ECPrivateKey(pkey, &pbegin) != nSize)
            throw key_error("CKey::GetPrivKey() : i2d_ECPrivateKey returned unexpected size");
        return vchPrivKey;
    }

    bool SetPubKey(const vector<unsigned char>& vchPubKey)
    {
        const unsigned char* pbegin = &vchPubKey[0];
        if (!o2i_ECPublicKey(&pkey, &pbegin, vchPubKey.size()))
            return false;
        return true;
    }

    vector<unsigned char> GetPubKey() const
    {
        unsigned int nSize = i2o_ECPublicKey(pkey, NULL);
        if (!nSize)
            throw key_error("CKey::GetPubKey() : i2o_ECPublicKey failed");
        vector<unsigned char> vchPubKey(nSize, 0);
        unsigned char* pbegin = &vchPubKey[0];
        if (i2o_ECPublicKey(pkey, &pbegin) != nSize)
            throw key_error("CKey::GetPubKey() : i2o_ECPublicKey returned unexpected size");
        return vchPubKey;
    }

    bool Sign(uint256 hash, vector<unsigned char>& vchSig)
    {
        vchSig.clear();
        unsigned char pchSig[10000];
        unsigned int nSize = 0;
        if (!ECDSA_sign(0, (unsigned char*)&hash, sizeof(hash), pchSig, &nSize, pkey))
            return false;
        vchSig.resize(nSize);
        memcpy(&vchSig[0], pchSig, nSize);
        return true;
    }

    bool Verify(uint256 hash, const vector<unsigned char>& vchSig)
    {
        // -1 = error, 0 = bad sig, 1 = good
        if (ECDSA_verify(0, (unsigned char*)&hash, sizeof(hash), &vchSig[0], vchSig.size(), pkey) != 1)
            return false;
        return true;
    }

    static bool Sign(const CPrivKey& vchPrivKey, uint256 hash, vector<unsigned char>& vchSig)
    {
        CKey key;
        if (!key.SetPrivKey(vchPrivKey))
            return false;
        return key.Sign(hash, vchSig);
    }

    static bool Verify(const vector<unsigned char>& vchPubKey, uint256 hash, const vector<unsigned char>& vchSig)
    {
        CKey key;
        if (!key.SetPubKey(vchPubKey))
            return false;
        return key.Verify(hash, vchSig);
    }
};

对比源码,我们还需要改一些东西,保证兼容性,需要包含一些头文件,因为CKey类涉及的算法,也是使用openssl库来完成的。也就是为了支持里面的那些EC_KEY之类的函数。

我们需要包含:

cpp 复制代码
#include <openssl/ec.h>         // EC_KEY_new_by_curve_name, EC_KEY_generate_key 等
#include <openssl/ecdsa.h>      // ECDSA_sign, ECDSA_verify

#include <openssl/obj_mac.h>    //支持NID_secp256k1定义

只要引入头文件就行了,我附加库在之间的章节已经操作过了。所以这里就不再讲了。

然后是secure_allocator又有兼容性问题,我直接去掉了。

公私钥生成

然后我们只要调用CKey类的MakeNewKey,就能生成一对公私钥:

cpp 复制代码
int main() {

        CKey key;
        //生成新钥
        key.MakeNewKey();
        //获取私钥输出
        CPrivKey der_priv = key.GetPrivKey();
        std::cout << "DER 私钥 (hex):\n";
        for (unsigned char c : der_priv) {
            std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)c;
        }
        std::cout << "\n\n";

     
        // 获取公钥输出
        CPrivKey pub = key.GetPubKey();
        std::cout << "公钥 (hex):\n";
        for (unsigned char c : pub) {
            std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)c;
        }
        std::cout << std::endl;
}

注意包含key.h头文件,以及

复制代码
std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)c;

setw setfill所需的头文件#include <iomanip>。

这个是用来设置输出格式的,表明一个字节占两位,空位补0。否则一个字节,如果正好是9,那么只会输出0x9,而不是0x09。

运行结果:

(注意,这里每次生成的数据都不一样,因为是随机的)

DER格式私钥

我们可以看到,公钥是没问题的,130个字符(130占位),65个字节,但是私钥明显不对啊,应该是32个字节,按照之前的说法。但这里的私钥足足558位,279个字节,这是为什么呢,这是程序出错了,乱码吗?不是的,请看下面这个,key.h里的注释:

cpp 复制代码
// Copyright (c) 2009 Satoshi Nakamoto
// Distributed under the MIT/X11 software license, see the accompanying
// file license.txt or http://www.opensource.org/licenses/mit-license.php.


// secp160k1
// const unsigned int PRIVATE_KEY_SIZE = 192;
// const unsigned int PUBLIC_KEY_SIZE  = 41;
// const unsigned int SIGNATURE_SIZE   = 48;
//
// secp192k1
// const unsigned int PRIVATE_KEY_SIZE = 222;
// const unsigned int PUBLIC_KEY_SIZE  = 49;
// const unsigned int SIGNATURE_SIZE   = 57;
//
// secp224k1
// const unsigned int PRIVATE_KEY_SIZE = 250;
// const unsigned int PUBLIC_KEY_SIZE  = 57;
// const unsigned int SIGNATURE_SIZE   = 66;
//
// secp256k1:
// const unsigned int PRIVATE_KEY_SIZE = 279;
// const unsigned int PUBLIC_KEY_SIZE  = 65;
// const unsigned int SIGNATURE_SIZE   = 72;
//
// see www.keylength.com
// script supports up to 75 for single byte push

请看secp256

private_key_size=279这里,256的椭圆算法,它这里的私钥就是279个字节,这是一种DER格式的私钥,是一种结构,其中包含了32字节的私钥和椭圆算法的一些参数数据等。

所以这个是正常的,关于从里面提取32字节私钥的方法,这里就不做说明了,暂时还用不到,现在有个大概了解即可。如果实在想要,算了,我这里还是提供一下,网上找来的,如下代码函数,提取私钥的函数:

DER提取32字节私钥

cpp 复制代码
// 函数:从 DER 格式私钥提取 32 字节原始私钥(大端,补前导零)
std::vector<unsigned char> ExtractPrvKeyFromDer(const CPrivKey& der_priv) {
    // 1. 从 DER 重新解析出 EC_KEY
    const unsigned char* p = der_priv.data();
    EC_KEY* temp_key = d2i_ECPrivateKey(nullptr, &p, der_priv.size());
    if (!temp_key) {
        throw std::runtime_error("d2i_ECPrivateKey 解析失败");
    }

    // 2. 获取 BIGNUM 格式的私钥
    const BIGNUM* bn = EC_KEY_get0_private_key(temp_key);
    if (!bn) {
        EC_KEY_free(temp_key);
        throw std::runtime_error("DER 中没有私钥数据");
    }

    // 3. 计算实际字节长度(secp256k1 私钥最多 32 字节)
    int priv_len = BN_num_bytes(bn);
    if (priv_len > 32) {
        EC_KEY_free(temp_key);
        throw std::runtime_error("私钥长度超过 32 字节");
    }

    // 4. 创建 32 字节缓冲区,前导补零
    std::vector<unsigned char> raw_priv(32, 0);
    BN_bn2bin(bn, raw_priv.data() + (32 - priv_len));

    // 5. 释放临时 EC_KEY
    EC_KEY_free(temp_key);

    return raw_priv;
}

调用:

cpp 复制代码
int main() {

        CKey key;
        //生成新钥
        key.MakeNewKey();
        //获取DER私钥
        CPrivKey der_priv = key.GetPrivKey();
        // 提取32字节私钥
        auto extracted_prv = ExtractPrvKeyFromDer(der_priv);

        std::cout << "原始32字节私钥 (hex):\n";
        for (unsigned char c : extracted_prv) {
            std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)c;
        }
        std::cout << "\n";
     
        // 获取公钥输出
        CPrivKey pub = key.GetPubKey();
        std::cout << "公钥 (hex):\n";
        for (unsigned char c : pub) {
            std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)c;
        }
        std::cout << std::endl;
}

结果:

当然,我没验证过啊,不保证准确。验证方法,把私钥导入其他工具,看公钥是否一致。

应该是没问题的,目前就不验证了,等以后私钥转最终的账号时候,用到这个功能,再验证。

另:关于65字节公钥怎么转1开头的地址,可以用源码中的PubKeyToAddress函数实现。

这个函数实现原理将在后续章节说明。

相关推荐
黎阳之光2 小时前
视频孪生领航者,以中国技术定义全球数智化新高度
大数据·人工智能·算法·安全·数字孪生
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 39. 组合总和 | C++ 回溯算法与 startIndex 剪枝
c++·算法·leetcode
患得患失9492 小时前
【前端WebSocket】心跳功能,心跳重置策略、双向确认(Ping-Pong) 以及 指数退避算法(Exponential Backoff)
前端·websocket·算法
海砥装备HardAus2 小时前
飞控算法中双环串级PID深度解析:角度环与角速度环的协同机制
stm32·算法·无人机·飞控·串级pid
宵时待雨2 小时前
优选算法专题1:双指针
数据结构·c++·笔记·算法·leetcode
zsc_1182 小时前
pvz3解码小游戏求解算法
算法
汀、人工智能2 小时前
[特殊字符] 第107课:LRU缓存(最后一课[特殊字符])
数据结构·算法·链表·数据库架构·哈希表·lru缓存
数据知道2 小时前
claw-code 源码分析:结构化输出与重试——`structured_output` 一类开关如何改变「可解析性」与失败语义?
算法·ai·claude code·claw code
tankeven2 小时前
HJ172 小红的矩阵染色
c++·算法