变更记录

  • 2020 年 2 月 15 日:初稿
  • 2020 年 2 月 24 日:更新以处理包含接口字段的消息
  • 2020 年 4 月 27 日:将接口中 oneof 的用法改为 Any
  • 2020 年 5 月 15 日:说明 cosmos_proto 扩展以及与 amino 的兼容性
  • 2020 年 12 月 4 日:将 MarshalAny 和 UnmarshalAny 移动并重命名到 codec.Codec 接口中。
  • 2021 年 2 月 24 日:移除对 HybridCodec 的提及,该方案已在 #6843 中被放弃。

状态

已接受

背景

目前,Cosmos SDK 使用 go-amino 进行二进制和 JSON 对象的线上传输编码,以实现逻辑对象与持久化对象之间的一致性。 摘自 Amino 文档:
Amino 是一种对象编码规范。它是 Proto3 的一个子集,并扩展了对接口的支持。 关于 Proto3 的更多信息,请参见 Proto3 规范。Amino 在很大程度上与其兼容(但不兼容 Proto2)。 Amino 编码协议的目标,是让逻辑对象与持久化对象保持一致。
Amino 还希望达到以下目标(并非完整列表):
  • 二进制字节必须能够结合模式进行解码。
  • 模式必须可升级。
  • 编码器和解码器逻辑必须足够简单。
然而,我们认为 Amino 并没有完全实现这些目标,也无法充分满足 Cosmos SDK 对真正灵活、跨语言、多客户端兼容编码协议的需求。尤其是,Amino 在支持不同语言编写的客户端之间进行对象序列化方面,已经被证明是一个很大的痛点,同时在真正的向后兼容性和可升级性方面几乎没有提供多少帮助。此外,通过性能分析和各种基准测试,Amino 已被证明是 Cosmos SDK 中极大的性能瓶颈 1。这一点在模拟执行性能和应用交易吞吐量上体现得尤为明显。 因此,我们需要采用一种满足以下标准的状态序列化编码协议:
  • 与语言无关
  • 与平台无关
  • 客户端支持丰富且生态繁荣
  • 高性能
  • 编码后消息体积尽可能小
  • 基于代码生成而非基于反射
  • 支持向后兼容和向前兼容
需要注意的是,迁移 away from Amino 应被视为双管齐下的方案,即状态编码和客户端编码。本文 ADR 聚焦于 Cosmos SDK 状态机中的状态序列化。后续还会有相应的 ADR 专门处理客户端侧编码。

决策

我们将采用 Protocol Buffers 对 Cosmos SDK 中持久化的结构化数据进行序列化,同时为希望继续使用 Amino 的应用提供一种清晰的机制和良好的开发者体验。我们将通过更新模块来提供这一机制:模块接收一个编解码器接口 Marshaler,而不是具体的 Amino codec。此外,Cosmos SDK 将提供 Marshaler 接口的两个具体实现:AminoCodec 和 ProtoCodec。
  • AminoCodec:二进制和 JSON 编码都使用 Amino。
  • ProtoCodec:二进制和 JSON 编码都使用 Protobuf。
模块将使用应用中实例化的那个 codec。默认情况下,Cosmos SDK 的 simapp 会在 MakeTestEncodingConfig 函数中实例化 ProtoCodec 作为 Marshaler 的具体实现。如果应用开发者有需要,也可以很容易地覆盖这一默认行为。 最终目标是用 Protobuf 编码替代 Amino JSON 编码,从而让模块接收并/或扩展 ProtoCodec。在此之前,仍会保留 Amino JSON 以支持遗留场景。Cosmos SDK 中仍有少数位置将 Amino JSON 写死,例如旧版 API REST 端点以及 x/params 存储。计划会逐步将它们转换为 Protobuf。

模块编解码器

对于不需要处理和序列化接口的模块,迁移到 Protobuf 的路径相当直接。这类模块只需将任何当前通过其具体 Amino codec 编码并持久化的类型迁移到 Protobuf,并让其 keeper 接收一个会是 ProtoCodec 的 Marshaler。这种迁移较为简单,因为现有行为基本可以直接保持不变。 注意,任何需要编码 bool 或 int64 之类原始类型的业务逻辑,都应使用 gogoprotobuf 的 Value 类型。 示例:
ts, err := gogotypes.TimestampProto(completionTime)
    if err != nil {
    // ...
}
    bz := cdc.MustMarshal(ts)
不过,不同模块在用途和设计上可能差异很大,因此我们必须支持模块编码和处理接口的能力(例如 Account 或 Content)。对于这些模块,它们必须定义自己的 codec 接口,并扩展 Marshaler。这些专用接口是模块特有的,并会包含能够序列化所需接口的方法约定。 示例:
// x/auth/types/codec.go

type Codec interface {
    codec.Codec

  MarshalAccount(acc exported.Account) ([]byte, error)

UnmarshalAccount(bz []byte) (exported.Account, error)

MarshalAccountJSON(acc exported.Account) ([]byte, error)

UnmarshalAccountJSON(bz []byte) (exported.Account, error)
}

使用 Any 编码接口

一般来说,模块级的 .proto 文件应定义使用 google.protobuf.Any 编码接口的消息。 在经过 扩展讨论 之后, 相较于我们最初 protobuf 设计中的应用级 oneof,这被选为更优先的替代方案。 支持 Any 的理由可以概括如下:
  • 相比需要在不同应用之间更谨慎协调的应用级 oneof,Any 为处理接口提供了更简单、更一致的客户端体验。使用 oneof 创建通用的交易签名库可能会比较繁琐,而且关键逻辑可能需要为每条链重复实现。
  • 相比 oneof,Any 更能抵抗人为错误。
  • 对模块和应用而言,Any 通常都更易实现。
反对使用 Any 的主要理由在于它会带来额外的空间开销,以及可能的性能开销。空间开销未来可以通过持久化层压缩来处理,而性能影响很可能较小。因此,不使用 Any 被视为一种过早优化,因为相比之下,用户体验才是更高优先级的考量。 需要注意的是,鉴于 Cosmos SDK 已决定采用上文描述的 Codec 接口,应用仍然可以选择使用 oneof 来编码状态和交易,但这并不是推荐方式。如果应用选择使用 oneof 而不是 Any,它们很可能会失去与支持多链的客户端应用之间的兼容性。因此,开发者应认真权衡,自己更在意的究竟是一个可能属于过早优化的问题,还是终端用户与客户端开发者的体验。

Any 的安全使用

默认情况下,gogo protobuf 对 Any 的实现 使用全局类型注册 将打包在 Any 中的值解码为具体的 Go 类型。这会引入一个漏洞:依赖树中的任何恶意模块 都可以向全局 protobuf 注册表注册一个类型, 并让引用了该类型 type_url 字段的交易在反序列化时 加载并解码这个类型。 为防止这一问题,我们引入一种类型注册机制,通过 InterfaceRegistry 接口将 Any 值解码为具体类型,这与 Amino 的类型注册机制有一定相似之处:
type InterfaceRegistry interface {
    // RegisterInterface associates protoName as the public name for the
    // interface passed in as iface
    // Ex:
    //   registry.RegisterInterface("cosmos_sdk.Msg", (*sdk.Msg)(nil))

RegisterInterface(protoName string, iface interface{
})

    // RegisterImplementations registers impls as a concrete implementations of
    // the interface iface
    // Ex:
    //  registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSend{
}, &MsgMultiSend{
})

RegisterImplementations(iface interface{
}, impls ...proto.Message)
}
除了充当白名单之外,InterfaceRegistry 还可以用于向客户端传达满足某个接口的具体类型列表。 在 .proto 文件中:
  • 接受接口的字段应使用 cosmos_proto.accepts_interface 进行注解,所用名称应与传给 InterfaceRegistry.RegisterInterface 的 protoName 完全限定名一致。
  • 接口实现应使用 cosmos_proto.implements_interface 进行注解,所用名称应与传给 InterfaceRegistry.RegisterInterface 的 protoName 完全限定名一致。
未来,protoName、cosmos_proto.accepts_interface、cosmos_proto.implements_interface 可能会通过代码生成、反射和/或静态 lint 使用。 实现 InterfaceRegistry 的同一个结构体还将实现一个 InterfaceUnpacker 接口,用于解包 Any:
type InterfaceUnpacker interface {
    // UnpackAny unpacks the value in any to the interface pointer passed in as
    // iface. Note that the type in any must have been registered with
    // RegisterImplementations as a concrete type for that interface
    // Ex:
    //    var msg sdk.Msg
    //    err := ctx.UnpackAny(any, &msg)
    //    ...
    UnpackAny(any *Any, iface interface{
})

error
}
注意,InterfaceRegistry 的使用方式并未偏离 protobuf 对 Any 的标准用法,它只是为 Golang 的使用增加了一层安全和自省能力。 上文描述的 ProtoCodec 将包含 InterfaceRegistry。为了让模块注册接口类型,应用模块可以选择实现以下接口:
type InterfaceModule interface {
    RegisterInterfaceTypes(InterfaceRegistry)
}
模块管理器将包含一个方法,用于对每个实现该接口的模块调用 RegisterInterfaceTypes,以填充 InterfaceRegistry。

使用 Any 编码状态

Cosmos SDK 将提供辅助方法 MarshalInterface 和 UnmarshalInterface,以隐藏将接口类型包装进 Any 的复杂性,并简化序列化过程。
import "github.com/cosmos/cosmos-sdk/codec"

// note: eviexported.Evidence is an interface type
func MarshalEvidence(cdc codec.BinaryCodec, e eviexported.Evidence) ([]byte, error) {
    return cdc.MarshalInterface(e)
}

func UnmarshalEvidence(cdc codec.BinaryCodec, bz []byte) (eviexported.Evidence, error) {
    var evi eviexported.Evidence
    err := cdc.UnmarshalInterface(&evi, bz)

return err, nil
}

在 sdk.Msg 中使用 Any

类似的概念也将应用于包含接口字段的消息。 例如,当 Evidence 是一个接口时,我们可以如下定义 MsgSubmitEvidence:
// x/evidence/types/types.proto

message MsgSubmitEvidence {
  bytes submitter = 1
    [
      (gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
    ];
  google.protobuf.Any evidence = 2;
}
需要注意的是,要从 Any 中解包 evidence,我们确实需要持有 InterfaceRegistry 的引用。为了能在像 ValidateBasic 这样本不应该了解 InterfaceRegistry 的方法中引用 evidence,我们在反序列化过程中引入了 UnpackInterfaces 阶段,在真正需要这些接口之前先将它们解包。

解包接口

为了实现反序列化中的 UnpackInterfaces 阶段,即在真正需要之前先解包封装在 Any 中的接口,我们创建了一个供 sdk.Msg 和其他类型实现的接口:
type UnpackInterfacesMessage interface {
    UnpackInterfaces(InterfaceUnpacker)

error
}
我们还在 Any 结构体本身中引入了一个私有的 cachedValue interface{} 字段,并提供了公共 getter GetCachedValue() interface{}。 UnpackInterfaces 方法应在消息反序列化期间、紧接 Unmarshal 之后调用,所有打包在 Any 中的接口值都会被解码并存储到 cachedValue 中,以便后续引用。 这样一来,解包后的接口值随后就可以在任何代码中安全使用,而无需了解 InterfaceRegistry;同时,消息还可以提供一个简单的 getter,将缓存值转换为正确的接口类型。 这样还有一个额外好处:Any 值的反序列化只会在初始反序列化期间发生一次,而不是每次读取该值时都执行。此外,当 Any 值首次被打包时(例如在调用 NewMsgSubmitEvidence 时),原始接口值会被缓存,因此再次读取时无需再进行反序列化。 MsgSubmitEvidence 可以实现 UnpackInterfaces,并额外提供一个便捷 getter GetEvidence,如下所示:
func (msg MsgSubmitEvidence)

UnpackInterfaces(ctx sdk.InterfaceRegistry)

error {
    var evi eviexported.Evidence
  return ctx.UnpackAny(msg.Evidence, *evi)
}

func (msg MsgSubmitEvidence)

GetEvidence()

eviexported.Evidence {
    return msg.Evidence.GetCachedValue().(eviexported.Evidence)
}

Amino 兼容性

如果配合合适的 codec 实例使用,我们自定义实现的 Any 可以与 Amino 透明协作。这意味着,打包在 Any 中的接口会像常规的 Amino 接口一样进行 amino 序列化(前提是它们已正确注册到 Amino 中)。 为了使这一功能生效:
  • 所有遗留代码都必须使用 *codec.LegacyAmino 而不是 *amino.Codec,后者现在是一个能够正确处理 Any 的包装器
  • 所有新代码都应使用同时兼容 amino 和 protobuf 的 Marshaler
  • 此外,在 v0.39 之前,codec.LegacyAmino 将被重命名为 codec.LegacyAmino。

为什么没有改选 X

若要查看与其他替代协议的更完整比较,请参见这里。

Cap’n Proto

Cap’n Proto 看起来确实是 Protobuf 的一个有利替代方案,因为它原生支持接口/泛型并且内建规范化能力,但与 Protobuf 相比,它缺少丰富的客户端生态,而且成熟度也稍低一些。

FlatBuffers

FlatBuffers 也是一个潜在可行的替代方案,主要区别在于 FlatBuffers 不需要先解析/解包到二级表示之后才能访问数据,这通常也伴随着按对象进行的内存分配。 然而,这需要投入大量精力进行研究,并完整理解迁移范围以及后续推进路径,而这些目前都还不够明确。此外,FlatBuffers 并不是为不受信任的输入而设计的。

未来改进与路线图

未来我们可能会考虑在持久化层之上增加一层压缩层,这不会改变交易或默克尔树哈希,但可以减少 Any 的存储开销。此外,我们也可能采用 protobuf 的命名约定,使类型 URL 在保持描述性的同时更加简洁。 围绕 Any 的使用增加更多代码生成功能,也是未来可以探索的方向,从而让 Go 开发者的使用体验更加顺畅。

影响

正面

  • 显著的性能提升。
  • 支持向后和向前的类型兼容性。
  • 为跨语言客户端提供更好的支持。

负面

  • 需要一定学习成本才能理解并实现 Protobuf 消息。
  • 由于使用了 Any,消息体积会略有增加,不过未来可以通过压缩层来抵消这一点

中性

参考资料

  1. 链接
  2. 链接

Changelog

  • 2020 Feb 15: Initial Draft
  • 2020 Feb 24: Updates to handle messages with interface fields
  • 2020 Apr 27: Convert usages of oneof for interfaces to Any
  • 2020 May 15: Describe cosmos_proto extensions and amino compatibility
  • 2020 Dec 4: Move and rename MarshalAny and UnmarshalAny into the codec.Codec interface.
  • 2021 Feb 24: Remove mentions of HybridCodec, which has been abandoned in #6843.

Status

Accepted

Context

Currently, the Cosmos SDK utilizes go-amino for binary and JSON object encoding over the wire bringing parity between logical objects and persistence objects. From the Amino docs:
Amino is an object encoding specification. It is a subset of Proto3 with an extension for interface support. See the Proto3 spec for more information on Proto3, which Amino is largely compatible with (but not with Proto2). The goal of the Amino encoding protocol is to bring parity into logic objects and persistence objects.
Amino also aims to have the following goals (not a complete list):
  • Binary bytes must be decode-able with a schema.
  • Schema must be upgradeable.
  • The encoder and decoder logic must be reasonably simple.
However, we believe that Amino does not fulfill these goals completely and does not fully meet the needs of a truly flexible cross-language and multi-client compatible encoding protocol in the Cosmos SDK. Namely, Amino has proven to be a big pain-point in regards to supporting object serialization across clients written in various languages while providing virtually little in the way of true backwards compatibility and upgradeability. Furthermore, through profiling and various benchmarks, Amino has been shown to be an extremely large performance bottleneck in the Cosmos SDK 1. This is largely reflected in the performance of simulations and application transaction throughput. Thus, we need to adopt an encoding protocol that meets the following criteria for state serialization:
  • Language agnostic
  • Platform agnostic
  • Rich client support and thriving ecosystem
  • High performance
  • Minimal encoded message size
  • Codegen-based over reflection-based
  • Supports backward and forward compatibility
Note, migrating away from Amino should be viewed as a two-pronged approach, state and client encoding. This ADR focuses on state serialization in the Cosmos SDK state machine. A corresponding ADR will be made to address client-side encoding.

Decision

We will adopt Protocol Buffers for serializing persisted structured data in the Cosmos SDK while providing a clean mechanism and developer UX for applications wishing to continue to use Amino. We will provide this mechanism by updating modules to accept a codec interface, Marshaler, instead of a concrete Amino codec. Furthermore, the Cosmos SDK will provide two concrete implementations of the Marshaler interface: AminoCodec and ProtoCodec.
  • AminoCodec: Uses Amino for both binary and JSON encoding.
  • ProtoCodec: Uses Protobuf for both binary and JSON encoding.
Modules will use whichever codec that is instantiated in the app. By default, the Cosmos SDK’s simapp instantiates a ProtoCodec as the concrete implementation of Marshaler, inside the MakeTestEncodingConfig function. This can be easily overwritten by app developers if they so desire. The ultimate goal will be to replace Amino JSON encoding with Protobuf encoding and thus have modules accept and/or extend ProtoCodec. Until then, Amino JSON is still provided for legacy use-cases. A handful of places in the Cosmos SDK still have Amino JSON hardcoded, such as the Legacy API REST endpoints and the x/params store. They are planned to be converted to Protobuf in a gradual manner.

Module Codecs

Modules that do not require the ability to work with and serialize interfaces, the path to Protobuf migration is pretty straightforward. These modules are to simply migrate any existing types that are encoded and persisted via their concrete Amino codec to Protobuf and have their keeper accept a Marshaler that will be a ProtoCodec. This migration is simple as things will just work as-is. Note, any business logic that needs to encode primitive types like bool or int64 should use gogoprotobuf Value types. Example:
ts, err := gogotypes.TimestampProto(completionTime)
    if err != nil {
    // ...
}
    bz := cdc.MustMarshal(ts)
However, modules can vary greatly in purpose and design and so we must support the ability for modules to be able to encode and work with interfaces (e.g. Account or Content). For these modules, they must define their own codec interface that extends Marshaler. These specific interfaces are unique to the module and will contain method contracts that know how to serialize the needed interfaces. Example:
// x/auth/types/codec.go

type Codec interface {
    codec.Codec

  MarshalAccount(acc exported.Account) ([]byte, error)

UnmarshalAccount(bz []byte) (exported.Account, error)

MarshalAccountJSON(acc exported.Account) ([]byte, error)

UnmarshalAccountJSON(bz []byte) (exported.Account, error)
}

Usage of Any to encode interfaces

In general, module-level .proto files should define messages which encode interfaces using google.protobuf.Any. After extension discussion, this was chosen as the preferred alternative to application-level oneofs as in our original protobuf design. The arguments in favor of Any can be summarized as follows:
  • Any provides a simpler, more consistent client UX for dealing with interfaces than app-level oneofs that will need to be coordinated more carefully across applications. Creating a generic transaction signing library using oneofs may be cumbersome and critical logic may need to be reimplemented for each chain
  • Any provides more resistance against human error than oneof
  • Any is generally simpler to implement for both modules and apps
The main counter-argument to using Any centers around its additional space and possibly performance overhead. The space overhead could be dealt with using compression at the persistence layer in the future and the performance impact is likely to be small. Thus, not using Any is seem as a pre-mature optimization, with user experience as the higher order concern. Note, that given the Cosmos SDK’s decision to adopt the Codec interfaces described above, apps can still choose to use oneof to encode state and transactions but it is not the recommended approach. If apps do choose to use oneofs instead of Any they will likely lose compatibility with client apps that support multiple chains. Thus developers should think carefully about whether they care more about what is possibly a pre-mature optimization or end-user and client developer UX.

Safe usage of Any

By default, the gogo protobuf implementation of Any uses global type registration to decode values packed in Any into concrete go types. This introduces a vulnerability where any malicious module in the dependency tree could register a type with the global protobuf registry and cause it to be loaded and unmarshaled by a transaction that referenced it in the type_url field. To prevent this, we introduce a type registration mechanism for decoding Any values into concrete types through the InterfaceRegistry interface which bears some similarity to type registration with Amino:
type InterfaceRegistry interface {
    // RegisterInterface associates protoName as the public name for the
    // interface passed in as iface
    // Ex:
    //   registry.RegisterInterface("cosmos_sdk.Msg", (*sdk.Msg)(nil))

RegisterInterface(protoName string, iface interface{
})

    // RegisterImplementations registers impls as a concrete implementations of
    // the interface iface
    // Ex:
    //  registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSend{
}, &MsgMultiSend{
})

RegisterImplementations(iface interface{
}, impls ...proto.Message)
}
In addition to serving as a whitelist, InterfaceRegistry can also serve to communicate the list of concrete types that satisfy an interface to clients. In .proto files:
  • fields which accept interfaces should be annotated with cosmos_proto.accepts_interface using the same full-qualified name passed as protoName to InterfaceRegistry.RegisterInterface
  • interface implementations should be annotated with cosmos_proto.implements_interface using the same full-qualified name passed as protoName to InterfaceRegistry.RegisterInterface
In the future, protoName, cosmos_proto.accepts_interface, cosmos_proto.implements_interface may be used via code generation, reflection &/or static linting. The same struct that implements InterfaceRegistry will also implement an interface InterfaceUnpacker to be used for unpacking Anys:
type InterfaceUnpacker interface {
    // UnpackAny unpacks the value in any to the interface pointer passed in as
    // iface. Note that the type in any must have been registered with
    // RegisterImplementations as a concrete type for that interface
    // Ex:
    //    var msg sdk.Msg
    //    err := ctx.UnpackAny(any, &msg)
    //    ...
    UnpackAny(any *Any, iface interface{
})

error
}
Note that InterfaceRegistry usage does not deviate from standard protobuf usage of Any, it just introduces a security and introspection layer for golang usage. InterfaceRegistry will be a member of ProtoCodec described above. In order for modules to register interface types, app modules can optionally implement the following interface:
type InterfaceModule interface {
    RegisterInterfaceTypes(InterfaceRegistry)
}
The module manager will include a method to call RegisterInterfaceTypes on every module that implements it in order to populate the InterfaceRegistry.

Using Any to encode state

The Cosmos SDK will provide support methods MarshalInterface and UnmarshalInterface to hide a complexity of wrapping interface types into Any and allow easy serialization.
import "github.com/cosmos/cosmos-sdk/codec"

// note: eviexported.Evidence is an interface type
func MarshalEvidence(cdc codec.BinaryCodec, e eviexported.Evidence) ([]byte, error) {
    return cdc.MarshalInterface(e)
}

func UnmarshalEvidence(cdc codec.BinaryCodec, bz []byte) (eviexported.Evidence, error) {
    var evi eviexported.Evidence
    err := cdc.UnmarshalInterface(&evi, bz)

return err, nil
}

Using Any in sdk.Msgs

A similar concept is to be applied for messages that contain interfaces fields. For example, we can define MsgSubmitEvidence as follows where Evidence is an interface:
// x/evidence/types/types.proto

message MsgSubmitEvidence {
  bytes submitter = 1
    [
      (gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
    ];
  google.protobuf.Any evidence = 2;
}
Note that in order to unpack the evidence from Any we do need a reference to InterfaceRegistry. In order to reference evidence in methods like ValidateBasic which shouldn’t have to know about the InterfaceRegistry, we introduce an UnpackInterfaces phase to deserialization which unpacks interfaces before they’re needed.

Unpacking Interfaces

To implement the UnpackInterfaces phase of deserialization which unpacks interfaces wrapped in Any before they’re needed, we create an interface that sdk.Msgs and other types can implement:
type UnpackInterfacesMessage interface {
    UnpackInterfaces(InterfaceUnpacker)

error
}
We also introduce a private cachedValue interface{} field onto the Any struct itself with a public getter GetCachedValue() interface{}. The UnpackInterfaces method is to be invoked during message deserialization right after Unmarshal and any interface values packed in Anys will be decoded and stored in cachedValue for reference later. Then unpacked interface values can safely be used in any code afterwards without knowledge of the InterfaceRegistry and messages can introduce a simple getter to cast the cached value to the correct interface type. This has the added benefit that unmarshaling of Any values only happens once during initial deserialization rather than every time the value is read. Also, when Any values are first packed (for instance in a call to NewMsgSubmitEvidence), the original interface value is cached so that unmarshaling isn’t needed to read it again. MsgSubmitEvidence could implement UnpackInterfaces, plus a convenience getter GetEvidence as follows:
func (msg MsgSubmitEvidence)

UnpackInterfaces(ctx sdk.InterfaceRegistry)

error {
    var evi eviexported.Evidence
  return ctx.UnpackAny(msg.Evidence, *evi)
}

func (msg MsgSubmitEvidence)

GetEvidence()

eviexported.Evidence {
    return msg.Evidence.GetCachedValue().(eviexported.Evidence)
}

Amino Compatibility

Our custom implementation of Any can be used transparently with Amino if used with the proper codec instance. What this means is that interfaces packed within Anys will be amino marshaled like regular Amino interfaces (assuming they have been registered properly with Amino). In order for this functionality to work:
  • all legacy code must use *codec.LegacyAmino instead of *amino.Codec which is now a wrapper which properly handles Any
  • all new code should use Marshaler which is compatible with both amino and protobuf
  • Also, before v0.39, codec.LegacyAmino will be renamed to codec.LegacyAmino.

Why Wasn’t X Chosen Instead

For a more complete comparison to alternative protocols, see here.

Cap’n Proto

While Cap’n Proto does seem like an advantageous alternative to Protobuf due to it’s native support for interfaces/generics and built in canonicalization, it does lack the rich client ecosystem compared to Protobuf and is a bit less mature.

FlatBuffers

FlatBuffers is also a potentially viable alternative, with the primary difference being that FlatBuffers does not need a parsing/unpacking step to a secondary representation before you can access data, often coupled with per-object memory allocation. However, it would require great efforts into research and full understanding the scope of the migration and path forward — which isn’t immediately clear. In addition, FlatBuffers aren’t designed for untrusted inputs.

Future Improvements & Roadmap

In the future we may consider a compression layer right above the persistence layer which doesn’t change tx or merkle tree hashes, but reduces the storage overhead of Any. In addition, we may adopt protobuf naming conventions which make type URLs a bit more concise while remaining descriptive. Additional code generation support around the usage of Any is something that could also be explored in the future to make the UX for go developers more seamless.

Consequences

Positive

  • Significant performance gains.
  • Supports backward and forward type compatibility.
  • Better support for cross-language clients.

Negative

  • Learning curve required to understand and implement Protobuf messages.
  • Slightly larger message size due to use of Any, although this could be offset by a compression layer in the future

Neutral

References

  1. Link
  2. Link