- 区块链
- 共识算法
【免费下载链接】tendermint
⟁ Tendermint Core (BFT Consensus) in Go
本文基于 crypto/README.md 展开,深入 Tendermint Core 的密码学模块
crypto,系统讲解其抽象接口设计、三种签名算法(Ed25519、Secp256k1、Sr25519)的实现细节、二进制(Protobuf)与 JSON 两种编码规范,并结合仓库源码给出可运行的实战用法。读完本文,你将能正确导入并使用 Tendermint 的密钥接口完成密钥生成、签名、验签与地址计算,并理解为什么 Tendermint 的 JSON 里会同时出现type与value两个字段。
一、crypto 包是什么:为 Tendermint 量身裁剪的密码学工具集
crypto是 Tendermint 为自身使用场景适配、封装的密码学包。它在 Go 标准库与第三方密码学库(如golang.org/x/crypto、btcec、go-schnorrkel)之上,抽象出一套统一、稳定的密钥与密码原语接口,使得共识、P2P、ABCI 等上层模块可以不关心具体算法,只依赖统一的PubKey/PrivKey接口完成签名与验证。
从源码结构看,crypto包(crypto/)由以下几部分组成:
| 子包 / 文件 | 职责 |
|---|---|
| crypto.go | 核心接口定义:PubKey、PrivKey、Symmetric、Address |
| ed25519/ | Ed25519 签名算法实现(Tendermint 默认验证人密钥算法) |
| secp256k1/ | Secp256k1 ECDSA 实现,兼容 Bitcoin 地址风格 |
| sr25519/ | Sr25519 Schnorr 签名实现(基于 Ristretto,与 Polkadot 生态兼容) |
| encoding/ | 密钥与 ProtobufPublicKey消息的双向转换 |
| armor/ | ASCII Armor 编解码(类似 PGP 的-----BEGIN ...-----包装) |
| xsalsa20symmetric/ | 基于 NaClsecretbox的对称加解密 |
| tmhash/ | SHA256 及截断到 20 字节的哈希工具 |
| merkle/ | 确定性最小高度 Merkle 树哈希(RFC 6962 风格) |
此外,crypto/version.go 声明了当前包版本Version = "0.9.0-dev",crypto/doc.go 给出了包的整体设计意图:它是对 Go 标准库等价功能的包装,便于 Tendermint 统一使用。
二、导入方式:接口包与算法子包的分层设计
crypto/README.md明确给出了两种导入路径,区分了「接口」与「具体算法」两个抽象层次:
// 1. 只需要接口(PubKey / PrivKey / Symmetric / Address)时,导入根包: import "github.com/tendermint/tendermint/crypto" // 2. 需要具体算法实现时,导入对应子包: import "github.com/tendermint/tendermint/crypto/ed25519" import "github.com/tendermint/tendermint/crypto/secp256k1" import "github.com/tendermint/tendermint/crypto/sr25519"这种分层设计的好处是:上层业务代码(如验证人管理、签名服务)可以只依赖crypto根包的接口写逻辑,把具体算法作为运行时注入的依赖;而需要做密钥生成、类型断言或算法切换时,才引入具体子包。
三、核心接口:PubKey、PrivKey 与 Address
crypto包的核心抽象定义在 crypto/crypto.go 中。三种接口分别对应非对称公钥、非对称私钥与对称密码原语:
type PubKey interface { Address() Address Bytes() []byte VerifySignature(msg []byte, sig []byte) bool Equals(PubKey) bool Type() string } type PrivKey interface { Bytes() []byte Sign(msg []byte) ([]byte, error) PubKey() PubKey Equals(PrivKey) bool Type() string } type Symmetric interface { Keygen() []byte Encrypt(plaintext []byte, secret []byte) (ciphertext []byte) Decrypt(ciphertext []byte, secret []byte) (plaintext []byte, err error) }理解这几个接口,是掌握整个crypto包的钥匙:
PubKey.Bytes()/PrivKey.Bytes():返回密钥的原始字节序列,供序列化、落盘与网络传输使用;VerifySignature(msg, sig)/Sign(msg):签名与验签的核心入口,消息直接以字节数组形式传入;Type():返回算法类型字符串(ed25519、secp256k1、sr25519),用于在 JSON/Protobuf 编码时区分具体实现;Equals:密钥相等性比较。值得注意的实现细节是,各算法子包的Equals都使用crypto/subtle.ConstantTimeCompare(如 ed25519.go)或bytes.Equal进行常数时间比较,以避免因比较耗时差异引发时序侧信道攻击。
Address:更紧凑的标识符
Address是公钥的紧凑标识,在 Tendermint 中被广泛用作验证人地址。其定义位于 crypto/crypto.go:
const ( // AddressSize is the size of a pubkey address. AddressSize = tmhash.TruncatedSize // 20 字节 ) type Address = bytes.HexBytes func AddressHash(bz []byte) Address { return Address(tmhash.SumTruncated(bz)) }关键点:
AddressSize固定为 20 字节(tmhash.TruncatedSize,见 crypto/tmhash/hash.go);Address是bytes.HexBytes的类型别名,即使在 JSON 中也以十六进制字符串形式编码,这是 Tendermint 对地址类字节数据的一贯约定;AddressHash是通用的「SHA256 取前 20 字节」地址计算函数,具体算法的Address()方法会在此基础上叠加各自的差异(见下文)。
四、三种签名算法:实现细节与差异对照
4.1 Ed25519:Tendermint 默认的验证人签名算法
实现位于 crypto/ed25519/ed25519.go,基于标准库crypto/ed25519(RFC 8032)。其类型常量一目了然:
const ( PrivKeyName = "tendermint/PrivKeyEd25519" PubKeyName = "tendermint/PubKeyEd25519" PubKeySize = 32 PrivateKeySize = 64 SignatureSize = 64 SeedSize = 32 KeyType = "ed25519" )- 私钥 64 字节:前 32 字节为随机种子,后 32 字节为拼接的压缩公钥(
Sign与PubKey()均依赖这一布局,见 ed25519.go); - 公钥 32 字节:压缩 Edwards25519 点;
- 签名 64 字节:压缩点 + 域元素各 32 字节。
地址计算规则为SHA256(pubkey) 截取前 20 字节,对应 ed25519.go:
func (pubKey PubKey) Address() crypto.Address { if len(pubKey) != PubKeySize { panic("pubkey is incorrect size") } return crypto.Address(tmhash.SumTruncated(pubKey)) }密钥生成有两种方式(ed25519.go):
GenPrivKey():从操作系统随机源(crypto.CReader(),即crypto/rand.Reader)读取 32 字节种子生成;GenPrivKeyFromSecret(secret):对秘密做crypto.Sha256得到 32 字节种子后生成。源码注释特别提醒:若该 secret 来自用户口令,应先经过 KDF(如 bcrypt)处理,避免直接用弱口令。
4.2 Secp256k1:兼容 Bitcoin 生态的 ECDSA
实现位于 crypto/secp256k1/secp256k1.go,底层基于github.com/btcsuite/btcd/btcec。它与 Ed25519 在多个维度上存在显著差异:
- 公钥采用压缩格式,固定 33 字节(
PubKeySize = 33:1 字节前缀 0x02/0x03 标识 y 坐标奇偶性 + 32 字节 x 坐标,见 secp256k1.go); - 私钥 32 字节(
PrivKeySize = 32); - 地址采用 Bitcoin 风格:
RIPEMD160(SHA256(pubkey)),见 secp256k1.go; - 签名是 R || S 的 64 字节定长序列:先对消息做
crypto.Sha256,再做 ECDSA;签名序列化时 R、S 各自左补零到 32 字节(见 secp256k1.go 与serializeSig)。
该实现还内建了防签名延展性(malleability)保护:验签时若S > secp256k1halfN(即 S 不在 lower-S 形式)直接拒绝,这与 libsecp256k1 的行为保持一致(见 secp256k1.go)。
密钥生成同样提供随机生成GenPrivKey()与确定性生成GenPrivKeySecp256k1(secret)。确定性版本严格遵循 FIPS 186-3 附录 A.2.1 的方法保证私钥是合法的域元素:
// c = sha256(secret) // k = (c mod (n − 1)) + 1,其中 n 为曲线阶4.3 Sr25519:Schnorr 签名(Ristretto 群)
实现位于 crypto/sr25519/,底层基于github.com/ChainSafe/go-schnorrkel,与 Polkadot/Substrate 生态使用的签名算法同源。其常量定义在 crypto/sr25519/privkey.go 与 crypto/sr25519/pubkey.go:
PrivKeySize = 32,PubKeySize = 32;SignatureSize = 64(crypto/sr25519/encoding.go);- 类型标识为
sr25519,JSON 类型名为tendermint/PrivKeySr25519、tendermint/PubKeySr25519。
签名流程(privkey.go):私钥经NewMiniSecretKeyFromRaw→ExpandEd25519()扩展为正式密钥,再在签名上下文(NewSigningContext,带空上下文)下对消息签名。地址计算与 Ed25519 相同:SHA256(pubkey)[:20](pubkey.go)。
4.4 三种算法速查表
| 维度 | Ed25519 | Secp256k1 | Sr25519 |
|---|---|---|---|
| 算法类型 | Edwards25519 曲线签名 | ECDSA(椭圆曲线 secp256k1) | Schnorr(Ristretto 群) |
| 公钥长度 | 32 字节 | 33 字节(压缩) | 32 字节 |
| 私钥长度 | 64 字节(seed 32 + pubkey 32) | 32 字节 | 32 字节 |
| 签名长度 | 64 字节 | 64 字节(R||S) | 64 字节 |
| 地址算法 | SHA256(pubkey)[:20] | RIPEMD160(SHA256(pubkey)) | SHA256(pubkey)[:20] |
| 防延展性 | 天然具备 | 要求 lower-S | 天然具备 |
| 底层依赖 | 标准库crypto/ed25519 | btcd/btcec | ChainSafe/go-schnorrkel |
说明:地址/签名的十六进制与 base64 形态,请对照 crypto/README.md 中的 JSON 示例(见下文第六节);更完整的地址与签名规范描述见 spec/core/encoding.md。
五、二进制编码:Protobuf 与定长字节约定
crypto/README.md指出:二进制编码遵循Tendermint 编码规范,即仓库内的 spec/core/encoding.md。该规范的核心约定包括:
- 所有数据结构使用 Protocol Buffers(proto3)序列化(spec/core/encoding.md);
- 字节数组 = UVarint 长度前缀 + 原始字节:例如字节数组
[0xA, 0xB]编码为0x020A0B(spec/core/encoding.md); - 哈希统一使用 SHA256,且「先序列化、后哈希」:
SHA256(obj)等价于SHA256(ProtoEncoding(obj))(spec/core/encoding.md)。
针对公钥,Protobuf 中通过Oneof 区分不同算法类型。仓库内的 proto/tendermint/crypto/keys.proto 定义如下:
// PublicKey defines the keys available for use with Tendermint Validators message PublicKey { option (gogoproto.compare) = true; option (gogoproto.equal) = true; oneof sum { bytes ed25519 = 1; bytes secp256k1 = 2; } }对应的 Go 转换实现位于 crypto/encoding/codec.go:
PubKeyToProto(k crypto.PubKey) (pc.PublicKey, error):把crypto.PubKey按具体类型包装进PublicKey的 oneof 字段(codec.go);PubKeyFromProto(k pc.PublicKey) (crypto.PubKey, error):反向转换,并严格校验长度——Ed25519 公钥必须是 32 字节、Secp256k1 必须是 33 字节,否则返回错误(codec.go);init()中通过json.RegisterType注册了tendermint.crypto.PublicKey等 Protobuf 类型的 JSON 类型名(codec.go)。
六、JSON 编码:Amino 兼容的 type/value 包装
6.1 为什么要用内部 JSON 编码器
crypto/README.md明确指出:JSON 编码使用 Tendermint 内部的 JSON 编码器,其设计文档即 libs/json/doc.go。该包的目标是提供与 Amino JSON 编码向后兼容的序列化行为,与标准库encoding/json主要有两点差异:
64 位整数编码为字符串而非数字(避免 JavaScript 等 64 位浮点只有 53 位精度的问题):
int32(32) // Output: 32 int64(64) // Output: "64" uint64(64) // Output: "64"对应实现见 libs/json/encoder.go;
接口类型使用
{"type":"<类型名>","value":<值>}包装,即所谓的类型信封(type envelope)。
6.2 类型注册机制
类型信封的名称来自全局类型注册表。json.RegisterType(libs/json/types.go)把 Go 类型与字符串名绑定,并要求只能在init()中调用(重名或重复注册会 panic)。各算法子包正是通过init()完成注册,例如:
- crypto/ed25519/ed25519.go 注册
tendermint/PrivKeyEd25519、tendermint/PubKeyEd25519; - crypto/secp256k1/secp256k1.go 注册
tendermint/PrivKeySecp256k1、tendermint/PubKeySecp256k1; - crypto/sr25519/encoding.go 注册
tendermint/PrivKeySr25519、tendermint/PubKeySr25519。
编码时,encodeReflectInterface会查找具体类型的注册名并输出信封(libs/json/encoder.go);字节切片则按 base64 编码(libs/json/encoder.go),这就是密钥 JSON 中value字段是 base64 字符串的原因。
6.3 README 中的完整 JSON 编码示例
以下六条示例来自 crypto/README.md 原文,全部为仓库内真实可验证的编码形态:
ed25519.PrivKey - {"type":"tendermint/PrivKeyEd25519","value":"EVkqJO/jIXp3rkASXfh9YnyToYXRXhBr6g9cQVxPFnQBP/5povV4HTjvsy530kybxKHwEi85iU8YL0qQhSYVoQ=="} ed25519.PubKey - {"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="} sr25519.PrivKeySr25519 - {"type":"tendermint/PrivKeySr25519","value":"xtYVH8UCIqfrY8FIFc0QEpAEBShSG4NT0zlEOVSZ2w4="} sr25519.PubKeySr25519 - {"type":"tendermint/PubKeySr25519","value":"8sKBLKQ/OoXMcAJVxBqz1U7TyxRFQ5cmliuHy4MrF0s="} crypto.PrivKeySecp256k1 - {"type":"tendermint/PrivKeySecp256k1","value":"zx4Pnh67N+g2V+5vZbQzEyRerX9c4ccNZOVzM9RvJ0Y="} crypto.PubKeySecp256k1 - {"type":"tendermint/PubKeySecp256k1","value":"A8lPKJXcNl5VHt1FK8a244K9EJuS4WX1hFBnwisi0IJx"}可以验证的规律:
- 每个对象的
type字段就是init()中注册的类型名,value字段是密钥原始字节的 base64 编码; - Ed25519 与 Secp256k1 公钥的
value长度分别对应 32 与 33 字节展开后的 base64(前者的 base64 以=结尾提示原始长度恰为 32 字节的倍数,后者 33 字节则无填充符号); - 同一算法体系的私钥
value比公钥长(Ed25519 私钥 64 字节、Secp256k1 私钥 32 字节但示例对应公钥 33 字节编码形态不同),与上文第四节的密钥长度表完全吻合。
关于信封格式的通用定义,spec/core/encoding.md 也给出了权威说明:注册类型统一编码为{"type":"<type name>","value":<JSON>},value即公钥原始字节的 base64。
七、实战用法:生成、签名、验签与哈希
7.1 密钥生成与派生
import ( "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" ) // 1. 随机生成(使用操作系统熵源 crypto/rand.Reader) privKey := ed25519.GenPrivKey() pubKey := privKey.PubKey() // 2. 从秘密确定性派生(秘密应来自 KDF,如 bcrypt 输出) privKey2 := ed25519.GenPrivKeyFromSecret([]byte("some-kdf-output")) // 3. 拿到公钥地址(20 字节,hex 编码) addr := pubKey.Address()三种算法子包都提供GenPrivKey()与GenPrivKeyFromSecret(),签名接口完全一致,可无缝替换。
7.2 签名与验签
msg := []byte("This is Tendermint") sig, err := privKey.Sign(msg) // PrivKey.Sign ok := pubKey.VerifySignature(msg, sig) // PubKey.VerifySignature注意算法差异对调用方的影响:Ed25519/Sr25519 直接对消息字节签名验签;Secp256k1 内部会先对消息做 SHA256 再签名(见 secp256k1.go),并强制要求签名长度为 64 字节、S 为 lower-S 形式,否则验签直接返回false。
7.3 哈希工具
根包提供 SHA256 便捷封装(crypto/hash.go):
sum := crypto.Sha256([]byte("This is Tendermint")) fmt.Printf("%x\n", sum) // f91afb642f3d1c87c17eb01aae5cb65c242dfdbe7cf1066cc260f4ce5d33b94e该输出正是仓库内 crypto/example_test.go 的测试断言结果,可直接作为自测基准。需要 20 字节截断哈希时,使用tmhash.SumTruncated(crypto/tmhash/hash.go),它是所有公钥地址计算的基础。
7.4 对称加密与 ASCII Armor
对于需要对称加密的场景,crypto提供基于 NaClsecretbox的 XSalsa20-Poly1305 实现(crypto/xsalsa20symmetric/symmetric.go):
EncryptSymmetric(plaintext, secret):要求 secret 恰好 32 字节;输出 = 24 字节随机 nonce + 密文(总长比明文多secretbox.Overhead + 24字节);DecryptSymmetric(ciphertext, secret):反向解密,密文过短或认证失败均返回错误;- 源码注释建议 secret 由
Sha256(Bcrypt(passphrase))之类的方式从口令派生。
而 crypto/armor/armor.go 提供了EncodeArmor/DecodeArmor,用于把密钥字节包装为可读的 ASCII 块(可携带 headers 元数据),适合密钥导出、备份与文本传输场景。
7.5 随机数工具
crypto/random.go 提供仅依赖 OS 熵源的随机数接口:
CRandBytes(numBytes):返回加密安全随机字节;CRandHex(numDigits):返回十六进制字符串,文档注释指出CRandHex(24)提供 96 位随机性,通常足够大多数用途;CReader():返回crypto/rand.Reader,即各GenPrivKey的默认熵源。
八、小结
Tendermint 的crypto包是一个「接口统一、算法可插拔、编码规范严谨」的密码学基础设施:
- 接口层(
crypto根包)定义了PubKey/PrivKey/Symmetric与 20 字节Address,让上层代码与具体算法解耦; - 算法层(
ed25519、secp256k1、sr25519子包)分别提供 64/33/32 字节公钥、不同地址算法(SHA256 截断 vs Bitcoin RIPEMD160)、不同签名语义,并通过Type()与注册名区分; - 编码层:二进制走 Protobuf(proto/tendermint/crypto/keys.proto + crypto/encoding/codec.go),JSON 走 Amino 兼容的内部编码器(libs/json/),密钥统一以
{"type":"tendermint/...","value":"<base64>"}形态出现在日志、RPC 与配置中。
掌握这些约定,无论是解析节点日志中的公钥字段、编写验证人密钥管理工具,还是为 Tendermint 上层应用接入签名服务,都能做到心中有数、手到擒来。更多编码细节可继续研读 spec/core/encoding.md 与各子包的测试文件(如 crypto/ed25519/ed25519_test.go、crypto/secp256k1/secp256k1_test.go)。
- 区块链
- 共识算法
【免费下载链接】tendermint
⟁ Tendermint Core (BFT Consensus) in Go
相关推荐
Crypto++公钥密码学:RSA加密与签名实践终极指南
Crypto++公钥密码学:RSA加密与签名实践终极指南 Crypto++是一个强大的免费C++加密库,提供了完整的RSA公钥密码学实现。RSA加密是现代安全通
密码学后端cryptography 椭圆曲线密码学(EC)完整指南:密钥生成、ECDSA 签名与 ECDH 密钥交换
cryptography 椭圆曲线密码学(EC)完整指南:密钥生成、ECDSA 签名与 ECDH 密钥交换 本文基于 cryptography 官方文档 ec.
密码学cryptography 库 RSA 公钥密码完全指南:密钥生成、序列化、签名与加密实战
cryptography 库 RSA 公钥密码完全指南:密钥生成、序列化、签名与加密实战 RSA 是 cryptography 库中历史最悠久、使用最广泛的非对
密码学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考