什么是amino编码
1. Reflect反射
1.1 关于go的reflect
现代通用编程语言中,有的语言支持反射,有的不支持。并且每个支持反射的语言的反射模型都不同。
Go官方自带的reflect包就是与反射相关,只要import这个包就可以使用。
Go语言实现了反射,其反射机制就是在运行时动态地调用对象的方法和属性。
ps:golang的gRPC也是通过反射来实现的。
1.2 为什么go中 nil != nil ?
从宏观上讲,go中任意一个变量在计算底层都包含(type,value)两个部分。
type分为static type和concrete type。其中,static type可以认为是你在编码时看得见的类型(比如int、string),concrete type可以认为是runtime系统能识别且认识的类型。
go中的类西蒙断言能否断言成功,就是取决于变量的concrete type,而不是static type。所以,如果go中一个reader变量的concrete type也实现了write方法,它也可以被类型断言为writer。
写代码的时候,在创建golang指定类型的变量(如int,string)时,它的static type就已经确定下来了。
注意:往往interface类型最是需要反射。
每个接口都对应一个(value,type)对,value可以理解为该变量值的相关信息,type可以理解为该变量实际变量类型的相关信息。
go中任何一个变量都可以转换成空接口类型interface{}。一个interface{}都包含2个指针,分别指向concrete type和对应的value。
这样说来,反射就是用来检测存储在接口变量内部(value,concrete type)对的一种机制。
如何直接获取接口内部变量信息——TestReflectInfo:
func TestReflectInfo(t *testing.T) {
var a float32 = 1.234
fmt.Println(reflect.TypeOf(a))
fmt.Println(reflect.ValueOf(a))
fmt.Println("===========================")
b := Stu{1024, "michael.w"}
fmt.Println(reflect.TypeOf(b))
fmt.Println(reflect.ValueOf(b))
fmt.Println("===========================")
c := &Stu{1024, "michael.w"}
fmt.Println(reflect.TypeOf(c))
fmt.Println(reflect.ValueOf(c))
}
打印结果:
float32
1.234
===========================
amino_test.Stu
{1024 michael.w}
===========================
*amino_test.Stu
&{1024 michael.w}
1.3 go reflect的使用技巧
1.3.1 通过类型判断,转换为原有真实类型
1.3.1.1 已知原有类型
TestTypeConvert1:
func TestTypeConvert1(t *testing.T) {
var a float32 = 3.1415926
p := reflect.ValueOf(&a)
v := reflect.ValueOf(a)
convertPointer := p.Interface().(*float32)
convertValue := v.Interface().(float32)
// strictly:float32 -> *float32 -> panic
// 向reflect.Valueof()中传入的参数是指针,那么在类型断言时候也要断言指针,否则会panic
fmt.Println(convertPointer, convertValue)
}
打印结果:
0xc0000906b8 3.1415925
1.3.1.2 未知原有类型
TestTypeConvert2:
type S1 struct {
age int
name string
Salary float64
}
func (s S1) S1Method() {
fmt.Println("s1Method")
}
func TestTypeConvert2(t *testing.T) {
s1 := S1{1024, "michael,w", 3.1415926}
DoFiledAndMethod(s1)
}
func DoFiledAndMethod(input interface{}) {
getType := reflect.TypeOf(input)
fmt.Println(getType)
getValue := reflect.ValueOf(input)
for i := 0; i < getType.NumField(); i++ {
field := getType.Field(i)
value := getValue.Field(i)
fmt.Println(field, value)
}
fmt.Println(getType.NumMethod())
for i := 0; i < getType.NumMethod(); i++ {
m := getType.Method(i)
fmt.Println(m)
}
}
打印结果:
amino_test.S1
{age github.com/tendermint/go-amino_test int 0 [0] false} 1024
{name github.com/tendermint/go-amino_test string 8 [1] false} michael,w
{Salary float64 24 [2] false} 3.1415926
1
{S1Method func(amino_test.S1) <func(amino_test.S1) Value> 0}
可见S1中的成员变量的type和value信息都已经被反射出来。
1.3.2 通过反射设置实际变量的值
TestSetFactValue:
func TestSetFactValue(t *testing.T) {
num := 3.1415926
p := reflect.ValueOf(&num)
newValue := p.Elem()
fmt.Println(newValue.Type())
fmt.Println(newValue.CanSet())
newValue.SetFloat(5.21)
fmt.Println(num)
}
打印结果:
float64
true
5.21
在没有借助指针或num变量本身,通过反射就修改了变量的值。
注:要修改反射类型的对象就一定要保证其值是“addressable”的,即需要传入指针。
1.3.3 通过反射进行类方法的调用
TestStructMethod:
type S2 struct {
Age int
Sex byte
Salary float64
}
func (s S2) S2Method(age int, sex byte, salary float64) {
fmt.Println("============ get into the func S2Method =============")
fmt.Println(age, sex, salary)
}
func TestStructMethod(t *testing.T) {
s2 := S2{18, 'M', 999.999}
getValue := reflect.ValueOf(s2)
// register the method by its original name
methodValue := getValue.MethodByName("S2Method")
// construct the argument set by reflected-type
args := []reflect.Value{
reflect.ValueOf(17),
reflect.ValueOf(uint8('F')),
reflect.ValueOf(9.999)}
// invoke the method
methodValue.Call(args)
}
打印结果:
17 70 9.999
可见通过反射,已经调用了到了S2类型变量s2的类方法。需要注意的是,通过反射调用类方法时传入的参数是普通参数的Value对象。
反射的高级用法:当做框架工程时,需要可以随意拓展方法/让用户可以自定义方法。
2. Amino详解与使用
2.1 什么是Amino
具体内容找谷哥或度娘。
个人认为Amino是protobuf3的一个升级版。Amino直接可以支持interface的编码,即可以把序列化好的数据unmarshal到一个接口对象中。
2.2 Amino与其他编码格式的对比
2.2.1 Amino vs JSON
// define my struct
type Exchange struct {
volumeOfTrade int32
Website string
ShortName byte
}
type Whole struct {
Ranking int32
Memo string
Exchange Exchange
}
func TestAminoAndJSON(t *testing.T) {
s3 := Whole{
1,
"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks",
Exchange{30, "https://www.okex.me", 'O'},
}
// json...
jsonBytes, err := json.Marshal(s3)
if err != nil {
fmt.Println(err)
}
showEncodeInfo(jsonBytes, "JSON")
// amino...
var cdc = amino.NewCodec()
aminoBytes, err := cdc.MarshalBinaryLengthPrefixed(s3)
if err != nil {
fmt.Println(err)
}
showEncodeInfo(aminoBytes, "amino")
// amino is compatible with JSON...
fmt.Println("================== decode JSON with Amino =======================")
var object Whole
if err := cdc.UnmarshalJSON(jsonBytes, &object); err != nil {
fmt.Println(err)
}
fmt.Println(object)
}
func showEncodeInfo(bz []byte, encodeName string) {
fmt.Println(encodeName)
fmt.Printf("content: %s\nlen : %d\n", bz, len(bz))
}
打印结果:
JSON
content: {"Ranking":1,"Memo":"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks","Exchange":{"Website":"https://www.okex.me","ShortName":79}}
len : 152
amino
content: ��EThe Times 03/Jan/2009 Chancellor on brink of second bailout for banks��
�https://www.okex.me�O
len : 99
================== decode JSON with Amino =======================
{1 The Times 03/Jan/2009 Chancellor on brink of second bailout for banks {0 https://www.okex.me 79}}
通过编码后的字节切片长度可以看出Amino比JSON要高效得多,但是牺牲了可阅读性。
同时,Amino完全兼容JSON。
2.2.2 Amino vs Protobuf3
Protobuf3的具体细节介绍:https://developers.google.com/protocol-buffers/docs/proto3
Protobuf3不支持interface,但是Amino支持。Amino江湖人称“Protobuf4”,兼容Protobuf3,但是不兼容Protobuf2。
2.3 如何使用Amino?
-
需要Amino的编码库:https://github.com/tendermint/go-amino
-
如果不需要编码interface的实现类对象,使用方法跟JSON一样;
-
如果需要编码interface的实现类对象,需要事先注册定义的interface和interface的实现类。
TestInterface:
type MyInterface interface { ShowInfo() Print(string) string } type MyStruct struct { Name string } func (ms MyStruct) ShowInfo() { fmt.Println("MyStruct:func->ShowInfo") } func (ms MyStruct) Print(str string) string { fmt.Println("MyStruct:func->Print") return str } func TestInterface(t *testing.T) { ms := MyStruct{ "michael.w", } fmt.Println(reflect.TypeOf(ms)) cdc := amino.NewCodec() // register interface cdc.RegisterInterface((*MyInterface)(nil), nil) //register concrete implementor cdc.RegisterConcrete(&MyStruct{}, "amino_test/MyStruct", nil) aminoBytes, err := cdc.MarshalBinaryLengthPrefixed(ms) if err != nil { fmt.Println(err) } fmt.Println(aminoBytes) var ms2 MyStruct if err = cdc.UnmarshalBinaryLengthPrefixed(aminoBytes, &ms2); err != nil { fmt.Println(err) } fmt.Println("================ Test result ==============") ms2.ShowInfo() ret := ms2.Print("CaiXukun") fmt.Println(ret) fmt.Println(ms2)
}
打印结果:
amino_test.MyStruct
[15 16 22 209 136 10 9 109 105 99 104 97 101 108 46 119]
================ Test result ==============
MyStruct:func->ShowInfo
MyStruct:func->Print
CaiXukun
{michael.w}
可见解码出的对象ms2依然是接口实现类,可以调用接口设定的方法。
注意:
- Amino不支持对枚举型,浮点型和Maps的编码;
- Amino在注册接口时要注册接口的指针(用nil强转接口类型即可);
- Amino在注册接口实现类时注意:实现方法的接收对象(接受者)是指针还是对象本身。如果接受者为指针,那么注册也应该是指针;
- 注册接口实现类时需要提供一个名字。这个名字需要全局唯一。
#### 2.4 关于前缀prefix bytes
注册接口实现类时需要提供一个全局唯一的名字。Amino其实就是通过前缀机制来将每个接口实现类与对应接口联系在一起的。
Prefix bytes一共有四个字节,且第一个字节不允许是0。所以一共有2^(8x4)-2^(8x3)
= 4,278,190,080种前缀的可能性。
为了保证无碰撞,Amino又引入三字节的 Disambiguation bytes(消歧)位于 Prefix bytes前面。
Disambiguation bytes的第一个字节也不允许是0。
#### 2.5 如何计算Prefix bytes和Disambiguation bytes
假设我们在注册接口实现类对象时传入的名字为"com.tendermint.consensus/MyConcreteName”
```go
// 伪代码
hash := sha256("com.tendermint.consensus/MyConcreteName")
hex.EncodeBytes(hash) // 0x{00 00 A8 FC 54 00 00 00 BB 9C 83 DD ...}
由于哈希值hex编码后前两个字节是0x00,需要全部删去。
rest = dropLeadingZeroBytes(hash) // 0x{A8 FC 54 00 00 00 BB 9C 83 DD ...}
disamb = rest[0:3]
rest = dropLeadingZeroBytes(rest[3:])
// prefix也需要去掉开头的0x00
prefix = rest[0:4]
之后向后取三个字节作为Disambiguation bytes。
最后再去掉开头的零,向后取四字节作为Preifx bytes。
3. Amino类方法走读
目前Amino只有go语言版的。并且是由tendermint的开发团队自行研发,有自己的版本号迭代。
go-amino编码库中常用的编解码的类方法如下:
// Amino带前缀字节的编码与解码(实际上在MarshalBinaryLengthPrefixed中包含了MarshalBinaryBare)
func (cdc *Codec) MarshalBinaryLengthPrefixed(o interface{}) ([]byte, error)
func (cdc *Codec) UnmarshalBinaryLengthPrefixed(bz []byte, ptr interface{}) error
// Amino不带前缀字节的编码与解码
func (cdc *Codec) MarshalBinaryBare(o interface{}) ([]byte, error)
func (cdc *Codec) UnmarshalBinaryBare(bz []byte, ptr interface{}) error
// Amino兼容JSON,所以提供JSON的编码与解码方法
func (cdc *Codec) MarshalJSON(o interface{}) ([]byte, error)
func (cdc *Codec) UnmarshalJSON(bz []byte, ptr interface{}) error
amino.go中还有有MustMarshalxxx或MustUnmarshalxxx的方法:表示只要在amino编解码中有一处地方出问题,直接panic掉。
4. Amino编码过程分析
整个编码过程(MarshalBinaryLengthPrefixed)完全按照标题的层级先后顺序
4.1 对对象o进行二进制编码(不带prefixBytes),得到字节切片bz
4.1.1 derefPointers(reflect.ValueOf(o))
利用反射,检查对象o是否为空指针类型。如果是,panic。
注:amino不能直接编码指针对象。请用一个自定义一个struct包裹。
4.1.2 info, err := cdc.getTypeInfo_wlock(rt)
上锁 -> 判断o是否为指针类型 (如果是转成go底层可识别的指针标记)->在typeInfos中找o对应的*TypeInfo(如果没有在本地添加;如果没有还是interface类型,提示未注册)->解锁离开
让我们看一下amino编码器的结构:
type Codec struct {
mtx sync.RWMutex // 读写锁
sealed bool
typeInfos map[reflect.Type]*TypeInfo // 重点
interfaceInfos []*TypeInfo
concreteInfos []*TypeInfo
disfixToTypeInfo map[DisfixBytes]*TypeInfo
nameToTypeInfo map[string]*TypeInfo
}
// 各对象的详细类型信息
type TypeInfo struct {
Type reflect.Type // go本地自带的那些类型
PtrToType reflect.Type // 如果是指针,指向啥玩意
ZeroValue reflect.Value // 空值
ZeroProto interface{}
InterfaceInfo // 如果是interface,那么有关该interface的信息
ConcreteInfo // 如果是interface实现类对象,那么有关该interface实现类的信息
StructInfo // 如果是结构体,那么有关该结构体的信息
}
// 接口信息结构
type InterfaceInfo struct {
Priority []DisfixBytes // Disfix priority.
Implementers map[PrefixBytes][]*TypeInfo // Mutated over time.
InterfaceOptions
}
// 接口实现类信息结构
type ConcreteInfo struct {
// These fields are only set when registered (as implementing an interface).
Registered bool // Registered with RegisterConcrete().
PointerPreferred bool // Deserialize to pointer type if possible.
// NilPreferred bool // Deserialize to nil for empty structs if PointerPreferred.
Name string // Registered name.
Disamb DisambBytes // Disambiguation bytes derived from name.
Prefix PrefixBytes // Prefix bytes derived from name.
ConcreteOptions // Registration options.
// These fields get set for all concrete types,
// even those not manually registered (e.g. are never interface values).
IsAminoMarshaler bool // Implements MarshalAmino() (<ReprObject>, error).
AminoMarshalReprType reflect.Type // <ReprType>
IsAminoUnmarshaler bool // Implements UnmarshalAmino(<ReprObject>) (error).
AminoUnmarshalReprType reflect.Type // <ReprType>
}
// 结构体信息结构
type StructInfo struct {
Fields []FieldInfo // If a struct.
}
关于amino前缀的一些硬规范在tendermint/go-amino/codec.go中
// Lengths
const (
PrefixBytesLen = 4
DisambBytesLen = 3
DisfixBytesLen = PrefixBytesLen + DisambBytesLen
)
// Prefix types
type (
PrefixBytes [PrefixBytesLen]byte
DisambBytes [DisambBytesLen]byte
DisfixBytes [DisfixBytesLen]byte // Disamb+Prefix
)
注:
- 参数rt可以理解为reflect.TypeOf(o)返回值。
- 如果出现了interface未注册的情况,整个解码过程并不会panic掉,而是继续正常进行。这也是为什么会有Mustxxx方法的原因。
4.1.3 开始二进制编码
开始二进制编码(这里的逻辑很复杂)。如果是编码已注册的接口实现类对象,会在编码结果中加入Prefix bytes。
4.2 对字节切片bz的长度进行Uvarint编码
Uvarint是默认用来编码golang中的int,int32和int64——兼容Protobuf。它也是Protobuf编码简短的精髓!
先看一下核心编码代码:
func PutUvarint(buf []byte, x uint64) int {
i := 0
for x >= 0x80 {
buf[i] = byte(x) | 0x80 // 从数值低位开始处理
x >>= 7 // 每7位成一组
i++
}
buf[i] = byte(x)
return i + 1 // 返回编码后一共多少个字节
}
整个过程我用文字解释一下:
十进制:12306 int(int64) 8字节
十六进制:0x00 00 00 00 00 00 30 12
二进制:b(6*8=48个0),0011 0000,0001 0010
谷哥工程师这样做,从低位开始每七位分一组:
b(6*8=48个0),00 | 11 0000,0 | 001 0010
然后依次与b1000 0000取或,并放进buf中。最后buf中:
buf中: 1001 0010, 111 00000 ,00000000
整个函数return 3,即编码后变成了3字节(不可阅读)
4.3 字节切片间的合并
bz长度Uvarint编码+对象二进制编码bz组成的新字符切片为最终的amino编码结果。
5. Amino使用的小细节
5.1 Tendermint中的哈希函数
Tendermint使用SHA256作为自己的哈希函数。
go自带的sha256:
func (d *digest) Sum(in []byte) []byte
可见需要程序员自己对对象做序列化,然后才能通过sha256曲哈希值。
而Tendermint的SHA256(obj)其实就是SHA256(AminiEncode(obj))的封装形式。
5.2 Tendermint使用Amino来区分不同类型的私钥、公钥和签名
位于:tendermint/tendermint/crypto/encoding/amino/amino.go
// RegisterAmino registers all crypto related types in the given (amino) codec.
func RegisterAmino(cdc *amino.Codec) {
// These are all written here instead of
cdc.RegisterInterface((*crypto.PubKey)(nil), nil)
cdc.RegisterConcrete(ed25519.PubKeyEd25519{},
ed25519.PubKeyAminoName, nil)
cdc.RegisterConcrete(secp256k1.PubKeySecp256k1{},
secp256k1.PubKeyAminoName, nil)
cdc.RegisterConcrete(multisig.PubKeyMultisigThreshold{},
multisig.PubKeyMultisigThresholdAminoRoute, nil)
cdc.RegisterInterface((*crypto.PrivKey)(nil), nil)
cdc.RegisterConcrete(ed25519.PrivKeyEd25519{},
ed25519.PrivKeyAminoName, nil)
cdc.RegisterConcrete(secp256k1.PrivKeySecp256k1{},
secp256k1.PrivKeyAminoName, nil)
}
5.3 Tendermint中有个数据结构Part
Tendermint在block扩散问题上,采用分part的机制。
// MakePartSet returns a PartSet containing parts of a serialized block.
// This is the form in which the block is gossipped to peers.
// CONTRACT: partSize is greater than zero.
func (b *Block) MakePartSet(partSize int) *PartSet {
if b == nil {
return nil
}
b.mtx.Lock()
defer b.mtx.Unlock()
// We prefix the byte length, so that unmarshaling
// can easily happen via a reader.
bz, err := cdc.MarshalBinaryLengthPrefixed(b) // 对block进行amino编码
if err != nil {
panic(err)
}
return NewPartSetFromData(bz, partSize) // 将编码后的信息分别放进不同的Part中
}
看一下Part和PartSet的结构:
type Part struct {
Index int `json:"index"` // 索引
Bytes cmn.HexBytes `json:"bytes"` // block被分割的编码数据
Proof merkle.SimpleProof `json:"proof"` // merkle证明
}
// Part的集合
type PartSet struct {
total int
hash []byte
mtx sync.Mutex
parts []*Part // Part集合
partsBitArray *cmn.BitArray
count int
}