从零手搓一个SHA-256:用C语言一步步实现比特币的‘心脏’算法

发布时间:2026/6/11 22:07:04
从零手搓一个SHA-256:用C语言一步步实现比特币的‘心脏’算法 从零手搓一个SHA-256用C语言一步步实现比特币的‘心脏’算法当你用比特币转账时是否好奇过那串看似随机的交易ID是如何生成的或者当矿工们谈论算力时他们究竟在计算什么这一切的核心都离不开一个名为SHA-256的密码学算法。今天我们将抛开现成的库函数用C语言从零开始实现这个支撑整个区块链网络的数字指纹生成器。1. 为什么选择SHA-256在开始编码之前我们需要理解SHA-256为何能成为区块链技术的基石。想象你要给朋友发送一封电子邮件如何确保内容在传输过程中没有被篡改哈希算法就是解决这个问题的数字指纹生成器。SHA-256的三个关键特性确定性相同输入永远产生相同输出雪崩效应1比特的输入变化会导致约50%的输出比特改变不可逆性无法从哈希值反推出原始数据在比特币网络中每个区块都包含前一个区块的哈希值形成不可篡改的链条。矿工们竞争的正是找到一个满足特定条件的SHA-256哈希值这个过程就是我们常说的挖矿。2. 搭建SHA-256的基础结构2.1 定义基本宏操作SHA-256的核心是一系列位操作我们先定义关键的宏#define ROTR(x, n) (((x) (n)) | ((x) (32 - (n)))) #define CH(x, y, z) (((x) (y)) ^ (~(x) (z))) #define MAJ(x, y, z) (((x) (y)) ^ ((x) (z)) ^ ((y) (z))) #define SIG0(x) (ROTR(x, 7) ^ ROTR(x, 18) ^ ((x) 3)) #define SIG1(x) (ROTR(x, 17) ^ ROTR(x, 19) ^ ((x) 10))这些看似复杂的操作实际上是SHA-256实现混淆和扩散的关键。例如ROTR实现32位字的循环右移这是密码学中常用的操作。2.2 初始化哈希常量SHA-256使用两组预设的常量值// 初始哈希值前8个质数的平方根小数部分前32位 uint32_t H[8] { 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 }; // 64个常量前64个质数的立方根小数部分前32位 uint32_t K[64] { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, // ... 完整列表见标准文档 };这些魔法数字并非随意选择而是来自数学常数的特定部分确保了算法的随机性和安全性。3. 实现消息预处理3.1 填充消息SHA-256要求输入长度必须是512位的整数倍。我们的填充规则是在消息末尾添加一个1位添加足够多的0位最后64位表示原始消息的位长度void pad_message(uint8_t *message, uint64_t length) { uint64_t bit_length length * 8; uint64_t new_length ((length 8) / 64 1) * 64; // 添加1位 message[length] 0x80; // 添加0位 for (uint64_t i length 1; i new_length - 8; i) { message[i] 0; } // 添加原始位长度大端序 for (int i 0; i 8; i) { message[new_length - 8 i] (bit_length (56 - i * 8)) 0xFF; } }3.2 消息分块处理填充后的消息被分割成512位的块每个块又分为16个32位字void process_block(const uint8_t *block, uint32_t *hash) { uint32_t W[64]; uint32_t a, b, c, d, e, f, g, h; // 1. 准备消息调度表W for (int t 0; t 16; t) { W[t] (block[t*4] 24) | (block[t*41] 16) | (block[t*42] 8) | block[t*43]; } for (int t 16; t 64; t) { W[t] SIG1(W[t-2]) W[t-7] SIG0(W[t-15]) W[t-16]; } // 2. 初始化工作变量 a hash[0]; b hash[1]; c hash[2]; d hash[3]; e hash[4]; f hash[5]; g hash[6]; h hash[7]; // 3. 执行64轮压缩 for (int t 0; t 64; t) { uint32_t T1 h EP1(e) CH(e,f,g) K[t] W[t]; uint32_t T2 EP0(a) MAJ(a,b,c); h g; g f; f e; e d T1; d c; c b; b a; a T1 T2; } // 4. 更新哈希值 hash[0] a; hash[1] b; hash[2] c; hash[3] d; hash[4] e; hash[5] f; hash[6] g; hash[7] h; }4. 完整SHA-256实现现在我们将各个部分组合成完整的实现#include stdio.h #include stdint.h #include string.h typedef struct { uint8_t data[64]; uint32_t datalen; uint64_t bitlen; uint32_t state[8]; } SHA256_CTX; void sha256_init(SHA256_CTX *ctx) { ctx-datalen 0; ctx-bitlen 0; memcpy(ctx-state, H, 8 * sizeof(uint32_t)); } void sha256_update(SHA256_CTX *ctx, const uint8_t *data, size_t len) { for (size_t i 0; i len; i) { ctx-data[ctx-datalen] data[i]; ctx-datalen; if (ctx-datalen 64) { process_block(ctx-data, ctx-state); ctx-bitlen 512; ctx-datalen 0; } } } void sha256_final(SHA256_CTX *ctx, uint8_t *hash) { uint32_t i ctx-datalen; // 填充消息 if (ctx-datalen 56) { ctx-data[i] 0x80; while (i 56) ctx-data[i] 0x00; } else { ctx-data[i] 0x80; while (i 64) ctx-data[i] 0x00; process_block(ctx-data, ctx-state); memset(ctx-data, 0, 56); } // 添加位长度 ctx-bitlen ctx-datalen * 8; for (i 0; i 8; i) { ctx-data[56 i] (ctx-bitlen (56 - i * 8)) 0xFF; } process_block(ctx-data, ctx-state); // 生成最终哈希值 for (i 0; i 8; i) { hash[i*4] (ctx-state[i] 24) 0xFF; hash[i*41] (ctx-state[i] 16) 0xFF; hash[i*42] (ctx-state[i] 8) 0xFF; hash[i*43] ctx-state[i] 0xFF; } }5. 测试我们的实现让我们用经典的abc字符串测试我们的实现int main() { SHA256_CTX ctx; uint8_t hash[32]; char *msg abc; sha256_init(ctx); sha256_update(ctx, (uint8_t*)msg, strlen(msg)); sha256_final(ctx, hash); printf(SHA-256 hash of %s:\n, msg); for (int i 0; i 32; i) { printf(%02x, hash[i]); } printf(\n); return 0; }正确输出应该是ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad6. SHA-256与比特币挖矿现在你理解了SHA-256的工作原理就能真正理解比特币挖矿的本质。矿工们实际上是在寻找一个随机数nonce使得SHA-256(SHA-256(区块头)) 目标难度值这个过程的计算复杂度正是比特币安全性的基础。我们实现的SHA-256虽然功能完整但为了实际挖矿你还需要优化实现如使用SIMD指令添加矿工逻辑改变nonce并重复计算连接比特币网络验证区块在开发过程中我遇到的一个有趣现象是即使使用相同的代码在不同平台上可能会因为字节序问题得到不同的结果。这提醒我们密码学实现中细节的重要性——一个微小的错误就可能完全破坏安全性。

周新闻

月新闻