☰
Tendermint crypto 密码学包完全指南:密钥接口、签名算法与 Amino 兼容 JSON 编码
2026/10/12 1:30:07 网站建设 项目流程
  • 区块链
  • 共识算法

【免费下载链接】tendermint

⟁ Tendermint Core (BFT Consensus) in Go

项目地址:https://gitcode.com/gh_mirrors/te/tendermint
点击查看免费下载

本文基于 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 三种算法速查表

维度Ed25519Secp256k1Sr25519
算法类型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/ed25519btcd/btcecChainSafe/go-schnorrkel

说明:地址/签名的十六进制与 base64 形态,请对照 crypto/README.md 中的 JSON 示例(见下文第六节);更完整的地址与签名规范描述见 spec/core/encoding.md。

五、二进制编码:Protobuf 与定长字节约定

crypto/README.md指出:二进制编码遵循Tendermint 编码规范,即仓库内的 spec/core/encoding.md。该规范的核心约定包括:

  1. 所有数据结构使用 Protocol Buffers(proto3)序列化(spec/core/encoding.md);
  2. 字节数组 = UVarint 长度前缀 + 原始字节:例如字节数组[0xA, 0xB]编码为0x020A0B(spec/core/encoding.md);
  3. 哈希统一使用 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主要有两点差异:

  1. 64 位整数编码为字符串而非数字(避免 JavaScript 等 64 位浮点只有 53 位精度的问题):

    int32(32) // Output: 32 int64(64) // Output: "64" uint64(64) // Output: "64"

    对应实现见 libs/json/encoder.go;

  2. 接口类型使用{"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

项目地址:https://gitcode.com/gh_mirrors/te/tendermint
点击查看免费下载

相关推荐

上一篇:终极指南:PHPExcel多工作表操作的完整技巧(创建、复制与删除)
下一篇:Linux Wallpaper Engine JSON配置文件详解:从场景对象到相机设置

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询