什么是amino编码

一冠王 · · 892 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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?

  1. 需要Amino的编码库:https://github.com/tendermint/go-amino

  2. 如果不需要编码interface的实现类对象,使用方法跟JSON一样;

  3. 如果需要编码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
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:一冠王

查看原文:什么是amino编码

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

892 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传