sha256.Sum256不能直接字符串拼接做区块哈希,因其是32字节数组别名,字符串拼接引入歧义分隔符,破坏链式不可篡改性;正确做法是按固定顺序序列化字段为字节流。

为什么 sha256.Sum256 不能直接用字符串拼接做区块哈希
因为 Go 的 sha256.Sum256 是一个 32 字节数组的别名,不是字符串;直接 fmt.Sprintf("%x", block.Hash) 看似能输出十六进制,但若你用字符串拼接(比如 "prev:" + prevHash + "data:" + data)再哈希,会引入歧义分隔符问题——相同内容不同拼接方式产生不同哈希,破坏链式不可篡改性。
正确做法是把结构体字段按确定顺序序列化为字节流。最简方案:用 encoding/binary 写入定长字段,或用 bytes.Join 拼接字节切片并加明确分隔符(如 []byte{0})。
- 避免用
fmt.Sprintf拼接后再哈希:易受空格、换行、字段顺序变化影响 - 区块数据字段若含任意字符串,必须先统一编码(如 UTF-8),且禁止隐式类型转换(
int直接转string会变 rune) - 测试时可打印
fmt.Printf("%x", hash[:])验证字节一致性,而不是依赖hash.String()
如何让 Block 结构体支持稳定哈希计算
Go 中结构体直接传给 sha256.Sum256 会因内存对齐、字段 padding 导致哈希不稳定。必须显式控制序列化过程。
推荐用 bytes.Buffer 手动写入关键字段(不包含 Hash 自身),顺序固定:
立即学习“go语言免费学习笔记(深入)”;
func (b *Block) CalculateHash() [32]byte {
var h sha256.Hash
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, b.Index)
buf.Write([]byte{0}) // 分隔符
buf.Write([]byte(b.Data))
buf.Write([]byte{0})
buf.Write(b.PrevHash[:])
h.Sum(buf.Bytes())
return h.Sum([32]byte{})[:32].([32]byte)
}
- 不要在结构体里存未初始化的指针或 slice(如
*[]byte),它们的内存地址会变 -
PrevHash字段类型必须是[32]byte,不是[]byte或string,否则长度不固定 - 首次生成创世块时,
PrevHash应设为全零值:[32]byte{},而非nil
为什么 Blockchain 的 AddBlock 必须校验前序哈希
不校验就等于放弃链式验证核心逻辑。常见错误是只比对 block.PrevHash == lastBlock.Hash,但没检查 block.PrevHash 是否真实来自上一区块的 CalculateHash() 输出。
真正要做的有两件事:一是确认引用存在(非全零且在链中),二是确认该哈希确实能由上一区块内容算出。
- 添加新区块前,必须调用
lastBlock.CalculateHash()并与newBlock.PrevHash逐字节比较 - 如果允许“跳块”(如跳过第 3 块直接连第 4 块),需额外维护索引映射,但原型阶段应禁用
- 校验失败时返回具体错误,比如
"prev_hash mismatch at index %d",而不是泛泛的"invalid block"
模拟运行时容易忽略的边界:时间戳和索引一致性
很多人用 time.Now().Unix() 当时间戳,结果多个区块在同一秒生成,哈希却不同——表面看没问题,实则破坏了“同一输入必得同一输出”的可复现性,不利于测试和回放。
更隐蔽的问题是索引(Index)手动递增时漏加或重复,导致链断裂或覆盖。
- 创世块
Index必须为0,后续每个AddBlock必须基于len(chain.Blocks)-1计算,而非依赖传入参数 - 时间戳建议在测试中固定(如
1717027200),上线再换time.Now(),避免非确定性 - 如果用
json.Marshal调试输出,注意它会把[32]byte转成 base64 字符串,不是十六进制,容易误判哈希是否一致
[:] 切片操作,或多一个隐式类型转换,链就断在看不见的地方。










