变更记录

  • 2020 年 3 月 06 日:初稿
  • 2020 年 3 月 12 日:API 更新
  • 2020 年 4 月 13 日:补充了接口 oneof 处理的细节
  • 2020 年 4 月 30 日:切换为 Any
  • 2020 年 5 月 14 日:说明公钥编码
  • 2020 年 6 月 08 日:在 SignDoc 中将 TxBody 和 AuthInfo 存储为字节;记录 TxRaw 作为广播和存储类型。
  • 2020 年 8 月 07 日:使用 ADR 027 对 SignDoc 进行序列化。
  • 2020 年 8 月 19 日:如 #6966 中讨论,将 sequence 字段从 SignDoc 移动到 SignerInfo。
  • 2020 年 9 月 25 日:移除 PublicKey 类型,改用 secp256k1.PubKey、ed25519.PubKey 和 multisig.LegacyAminoPubKey。
  • 2020 年 10 月 15 日:向 AccountRetriever 接口添加 GetAccount 和 GetAccountWithHeight 方法。
  • 2021 年 2 月 24 日:Cosmos SDK 不再使用 Tendermint 的 PubKey 接口,而是使用自己的 cryptotypes.PubKey。已据此更新。
  • 2021 年 5 月 3 日:将 clientCtx.JSONMarshaler 重命名为 clientCtx.JSONCodec。
  • 2021 年 6 月 10 日:添加 clientCtx.Codec: codec.Codec。

状态

已接受

背景

本 ADR 延续了 ADR 019 中建立的动机、设计和背景,也就是为 Cosmos SDK 客户端侧设计 Protocol Buffer 的迁移路径。 具体来说,客户端侧迁移路径主要包括交易生成与签名、消息构造与路由,以及 CLI 与 REST 处理器和业务逻辑(即 querier)。 基于这一点,我们将通过两个主要领域来处理迁移路径:交易和查询。不过,本 ADR 只关注交易。查询应在未来的 ADR 中讨论,但应基于这些提案继续推进。 基于详细讨论(#6030 和 #6078),交易的原始设计已从基于 oneof /JSON 签名的方法,显著调整为下文描述的方法。

决策

交易

由于接口值在状态中使用 google.protobuf.Any 编码(参见 ADR 019),因此交易中的 sdk.Msg 也使用 Any 编码。 使用 Any 编码接口值的一个主要目标,是提供一组可被应用复用的核心类型,从而使客户端能够尽可能安全地兼容更多链。 本规范的目标之一,是提供一种灵活的跨链交易格式,在不破坏客户端兼容性的前提下服务于广泛的使用场景。 为便于签名,交易被拆分为 TxBody(下文会在 SignDoc 中复用)和 signatures:
// types/types.proto
package cosmos_sdk.v1;

message Tx {
    TxBody body = 1;
    AuthInfo auth_info = 2;
    // A list of signatures that matches the length and order of AuthInfo's signer_infos to
    // allow connecting signature meta information like public key and signing mode by position.
    repeated bytes signatures = 3;
}

// A variant of Tx that pins the signer's exact binary represenation of body and
// auth_info. This is used for signing, broadcasting and verification. The binary
// `serialize(tx: TxRaw)` is stored in Tendermint and the hash `sha256(serialize(tx: TxRaw))`
// becomes the "txhash", commonly used as the transaction ID.
message TxRaw {
    // A protobuf serialization of a TxBody that matches the representation in SignDoc.
    bytes body = 1;
    // A protobuf serialization of an AuthInfo that matches the representation in SignDoc.
    bytes auth_info = 2;
    // A list of signatures that matches the length and order of AuthInfo's signer_infos to
    // allow connecting signature meta information like public key and signing mode by position.
    repeated bytes signatures = 3;
}

message TxBody {
    // A list of messages to be executed. The required signers of those messages define
    // the number and order of elements in AuthInfo's signer_infos and Tx's signatures.
    // Each required signer address is added to the list only the first time it occurs.
    //
    // By convention, the first required signer (usually from the first message) is referred
    // to as the primary signer and pays the fee for the whole transaction.
    repeated google.protobuf.Any messages = 1;
    string memo = 2;
    int64 timeout_height = 3;
    repeated google.protobuf.Any extension_options = 1023;
}

message AuthInfo {
    // This list defines the signing modes for the required signers. The number
    // and order of elements must match the required signers from TxBody's messages.
    // The first element is the primary signer and the one which pays the fee.
    repeated SignerInfo signer_infos = 1;
    // The fee can be calculated based on the cost of evaluating the body and doing signature verification of the signers. This can be estimated via simulation.
    Fee fee = 2;
}

message SignerInfo {
    // The public key is optional for accounts that already exist in state. If unset, the
    // verifier can use the required signer address for this position and lookup the public key.
    google.protobuf.Any public_key = 1;
    // ModeInfo describes the signing mode of the signer and is a nested
    // structure to support nested multisig pubkey's
    ModeInfo mode_info = 2;
    // sequence is the sequence of the account, which describes the
    // number of committed transactions signed by a given address. It is used to prevent
    // replay attacks.
    uint64 sequence = 3;
}

message ModeInfo {
    oneof sum {
        Single single = 1;
        Multi multi = 2;
    }

    // Single is the mode info for a single signer. It is structured as a message
    // to allow for additional fields such as locale for SIGN_MODE_TEXTUAL in the future
    message Single {
        SignMode mode = 1;
    }

    // Multi is the mode info for a multisig public key
    message Multi {
        // bitarray specifies which keys within the multisig are signing
        CompactBitArray bitarray = 1;
        // mode_infos is the corresponding modes of the signers of the multisig
        // which could include nested multisig public keys
        repeated ModeInfo mode_infos = 2;
    }
}

enum SignMode {
    SIGN_MODE_UNSPECIFIED = 0;

    SIGN_MODE_DIRECT = 1;

    SIGN_MODE_TEXTUAL = 2;

    SIGN_MODE_LEGACY_AMINO_JSON = 127;
}
如下文所述,为了让 SignDoc 尽可能包含更多 Tx 内容,SignerInfo 与签名本身被分离,这样被签名内容之外只保留原始签名字节。 由于我们的目标是实现一种灵活、可扩展的跨链交易格式,因此一旦发现新的交易处理使用场景,即使暂时还无法实现,也应尽快将新的交易处理选项加入 TxBody。 考虑到这会带来一定的协调开销,TxBody 包含了一个 extension_options 字段,可用于承载尚未覆盖的交易处理选项。尽管如此,应用开发者仍应尽量将重要的 Tx 改进上游化。

签名

下列所有签名模式都旨在提供以下保证:
  • 无可塑性:一旦交易签名完成,TxBody 和 AuthInfo 就不能再发生变化
  • Gas 可预测:如果我签署的是一笔由我支付手续费的交易,那么最终 Gas 完全取决于我签署的内容
这些保证让消息签名者能够最大程度地确信,中间方对 Tx 的操作不会导致任何有意义的变化。

SIGN_MODE_DIRECT

“direct” 签名行为是直接对通过网络广播的原始 TxBody 字节进行签名。这样做的优势包括:
  • 除标准 protocol buffers 实现外,对客户端额外能力的要求最低
  • 实际上几乎不给交易可塑性留下任何空间(即签名格式与编码格式之间不存在可能被攻击者利用的微妙差异)
签名使用下方的 SignDoc 结构,它复用了 TxBody 和 AuthInfo 的序列化结果,并且只增加签名所需的字段:
// types/types.proto
message SignDoc {
    // A protobuf serialization of a TxBody that matches the representation in TxRaw.
    bytes body = 1;
    // A protobuf serialization of an AuthInfo that matches the representation in TxRaw.
    bytes auth_info = 2;
    string chain_id = 3;
    uint64 account_number = 4;
}
为了使用默认模式进行签名,客户端需要执行以下步骤:
  1. 使用任意有效的 protobuf 实现序列化 TxBody 和 AuthInfo。
  2. 创建一个 SignDoc,并使用 ADR 027 对其进行序列化。
  3. 对编码后的 SignDoc 字节进行签名。
  4. 构建一个 TxRaw,并将其序列化后用于广播。
签名验证基于比较 TxRaw 中编码的原始 TxBody 和 AuthInfo 字节,而不是依赖任何 “规范化” 算法;后者除了给客户端带来额外复杂度外,还会阻碍某些升级能力(本文后续会讨论)。 签名验证器会执行以下操作:
  1. 反序列化一个 TxRaw,并提取出 body 和 auth_info。
  2. 从消息中创建必需签名者地址列表。
  3. 对每个必需签名者:
    • 从状态中获取 account number 和 sequence。
    • 从状态或 AuthInfo 的 signer_infos 中获取公钥。
    • 创建一个 SignDoc,并使用 ADR 027 对其进行序列化。
    • 使用同一列表位置上的签名,对序列化后的 SignDoc 进行验证。

SIGN_MODE_LEGACY_AMINO

为了支持旧版钱包和交易所,Amino JSON 交易签名将被临时保留支持。待钱包和交易所有机会升级到基于 protobuf 的签名后,这一选项将被禁用。在此之前,可以预见的是,禁用当前的 Amino 签名会造成过多破坏,因此并不可行。请注意,这主要是 Cosmos Hub 的需求,其他链可以选择立即禁用 Amino 签名。 旧版客户端仍可使用当前的 Amino JSON 格式对交易进行签名,并在广播前通过 REST /tx/encode 端点将其编码为 protobuf。

SIGN_MODE_TEXTUAL

正如 #6078 中已广泛讨论的那样,人们希望有一种人类可读的签名编码方式,尤其适用于像 Ledger 这样的硬件钱包,它们会在签名前向用户展示交易内容。JSON 曾是对此的一种尝试,但距离理想状态仍有差距。 SIGN_MODE_TEXTUAL 旨在作为一种人类可读编码的占位方案,用于替代 Amino JSON。这种新编码应比 JSON 更强调可读性,可能基于类似 MessageFormat 的格式化字符串。 为了确保这种新的人类可读格式不会遭受交易可塑性问题,SIGN_MODE_TEXTUAL 要求将人类可读字节与原始 SignDoc 拼接起来生成签名字节。 在 SIGN_MODE_TEXTUAL 实现后,可能会支持多种人类可读格式(甚至包括本地化消息)。

未知字段过滤

通常情况下,交易处理器应当拒绝 protobuf 消息中的未知字段,原因如下:
  • 未知字段中可能包含重要数据,如果忽略这些数据,可能会导致客户端出现意外行为
  • 它们会带来一种可塑性漏洞,攻击者可以通过向未签名内容(即顶层 Tx,而不是 TxBody)中添加随机的未解释数据来膨胀交易大小
在某些场景下,我们也可以选择安全地忽略未知字段(链接),以便为较新的客户端提供平滑的前向兼容性。 我们建议将第 11 位被设置的字段编号(对大多数用例来说,即 1024-2047 范围)视为非关键字段;如果这些字段未知,可以安全地忽略。 为此,我们需要一个未知字段过滤器,其行为应当如下:
  • 始终拒绝未签名内容中的未知字段(即顶层 Tx,以及如果基于签名模式存在的话,AuthInfo 中未签名的部分)
  • 在所有消息中拒绝未知字段(包括嵌套的 Any),但第 11 位被设置的字段除外
这很可能需要一个自定义的 protobuf 解析过程,该过程接收消息字节和 FileDescriptor,并返回一个布尔结果。

公钥编码

Cosmos SDK 中的公钥实现了 cryptotypes.PubKey 接口。 我们建议像处理其他接口一样使用 Any 进行 protobuf 编码(例如 BaseAccount.PubKey 和 SignerInfo.PublicKey)。 当前已实现的公钥包括:secp256k1、secp256r1、ed25519 和 legacy-multisignature。 示例:
message PubKey {
    bytes key = 1;
}
multisig.LegacyAminoPubKey 具有一个由 Any 组成的成员数组,用于支持任意 protobuf 公钥类型。 应用只应尝试处理自己已经测试过的一组已注册公钥。提供的签名校验 ante handler decorator 会强制执行这一点。

CLI 与 REST

目前,REST 和 CLI 处理器使用具体的 Amino codec,通过 Amino JSON 编码来对类型和交易进行编解码。由于客户端处理的某些类型可能是接口,正如我们在 ADR 019 中所描述的那样,客户端逻辑现在需要接收一个 codec 接口,这个接口不仅知道如何处理所有类型,还知道如何生成交易、签名和消息。
type AccountRetriever interface {
    GetAccount(clientCtx Context, addr sdk.AccAddress) (client.Account, error)

GetAccountWithHeight(clientCtx Context, addr sdk.AccAddress) (client.Account, int64, error)

EnsureExists(clientCtx client.Context, addr sdk.AccAddress)

error
  GetAccountNumberSequence(clientCtx client.Context, addr sdk.AccAddress) (uint64, uint64, error)
}

type Generator interface {
    NewTx()

TxBuilder
  NewFee()

ClientFee
  NewSignature()

ClientSignature
  MarshalTx(tx types.Tx) ([]byte, error)
}

type TxBuilder interface {
    GetTx()

sdk.Tx

  SetMsgs(...sdk.Msg)

error
  GetSignatures() []sdk.Signature
  SetSignatures(...sdk.Signature)

GetFee()

sdk.Fee
  SetFee(sdk.Fee)

GetMemo()

string
  SetMemo(string)
}
然后,我们更新 Context,加入新字段:Codec、TxGenerator 和 AccountRetriever,并更新 AppModuleBasic.GetTxCmd,使其接收一个 Context,该 Context 应当预先填充好所有这些字段。 随后,每个客户端方法都应使用某个 Init 方法重新初始化这个预填充的 Context。tx.GenerateOrBroadcastTx 可用于生成或广播交易。例如:
import "github.com/spf13/cobra"
import "github.com/cosmos/cosmos-sdk/client"
import "github.com/cosmos/cosmos-sdk/client/tx"

func NewCmdDoSomething(clientCtx client.Context) *cobra.Command {
    return &cobra.Command{
    RunE: func(cmd *cobra.Command, args []string)

error {
    clientCtx := ctx.InitWithInput(cmd.InOrStdin())
    msg := NewSomeMsg{...
}

tx.GenerateOrBroadcastTx(clientCtx, msg)
},
}
}

未来改进

SIGN_MODE_TEXTUAL 规范

我们计划在近期未来给出 SIGN_MODE_TEXTUAL 的具体规范和实现,以便 ledger 应用及其他钱包能够平滑地从 Amino JSON 迁移出去。

SIGN_MODE_DIRECT_AUX

(在链接中记录为选项 (3)) 我们可以增加一种模式 SIGN_MODE_DIRECT_AUX,以支持这样一种场景:需要将多个签名收集到单笔交易中,但消息组合者尚不知道最终交易会包含哪些签名。例如,我可能有一个 3/5 的多签钱包,并希望将一个 TxBody 发送给全部 5 个签名者,看看谁会先签名。一旦我收集到 3 个签名,就可以继续构建完整交易。 在 SIGN_MODE_DIRECT 下,每个签名者都需要对完整的 AuthInfo 进行签名,而其中包含所有签名者及其签名模式的完整列表,这会使上述场景变得非常困难。 SIGN_MODE_DIRECT_AUX 将允许“辅助”签名者仅使用 TxBody 和他们自己的 PublicKey 来创建签名。这样就可以将 AuthInfo 中完整签名者列表的确定推迟到签名收集完成之后。 “辅助”签名者是指除支付手续费的主签名者之外的任何签名者。对于主签名者来说,实际上需要完整的 AuthInfo 来计算 gas 和手续费,因为这取决于签名者数量、所使用的密钥类型以及签名模式。而辅助签名者无需关心手续费或 gas,因此只需对 TxBody 签名即可。 要在 SIGN_MODE_DIRECT_AUX 中生成签名,需要执行以下步骤:
  1. 对 SignDocAux 进行编码(同样要求字段必须按顺序序列化):
    // types/types.proto
    message SignDocAux {
        bytes body_bytes = 1;
        // PublicKey is included in SignDocAux :
        // 1. as a special case for multisig public keys. For multisig public keys,
        // the signer should use the top-level multisig public key they are signing
        // against, not their own public key. This is to prevent against a form
        // of malleability where a signature could be taken out of context of the
        // multisig key that was intended to be signed for
        // 2. to guard against scenario where configuration information is encoded
        // in public keys (it has been proposed) such that two keys can generate
        // the same signature but have different security properties
        //
        // By including it here, the composer of AuthInfo cannot reference the
        // a public key variant the signer did not intend to use
        PublicKey public_key = 2;
        string chain_id = 3;
        uint64 account_number = 4;
    }
    
  2. 对编码后的 SignDocAux 字节进行签名
  3. 将他们的签名和 SignerInfo 发送给主签名者;主签名者会在收集到足够签名后,补充 SIGN_MODE_DIRECT 和 AuthInfo,然后签名并广播最终交易

SIGN_MODE_DIRECT_RELAXED

(在链接中记录为选项 (1)(a)) 这是 SIGN_MODE_DIRECT 的一种变体,在这种模式下,多个签名者无需提前协调公钥和签名模式。它会涉及一种带有手续费信息、类似于上文 SignDocAux 的替代 SignDoc。如果客户端开发者发现提前收集公钥和模式的负担过重,未来可以加入这一模式。

影响

正面

  • 显著的性能提升。
  • 支持向后和向前的类型兼容性。
  • 更好地支持跨语言客户端。
  • 多种签名模式为协议演进提供了更大的空间

负面

  • google.protobuf.Any 类型 URL 会增加交易大小,不过这种影响可能可以忽略,或者可以通过压缩来缓解。

中性

参考资料


Changelog

  • 2020 March 06: Initial Draft
  • 2020 March 12: API Updates
  • 2020 April 13: Added details on interface oneof handling
  • 2020 April 30: Switch to Any
  • 2020 May 14: Describe public key encoding
  • 2020 June 08: Store TxBody and AuthInfo as bytes in SignDoc; Document TxRaw as broadcast and storage type.
  • 2020 August 07: Use ADR 027 for serializing SignDoc.
  • 2020 August 19: Move sequence field from SignDoc to SignerInfo, as discussed in #6966.
  • 2020 September 25: Remove PublicKey type in favor of secp256k1.PubKey, ed25519.PubKey and multisig.LegacyAminoPubKey.
  • 2020 October 15: Add GetAccount and GetAccountWithHeight methods to the AccountRetriever interface.
  • 2021 Feb 24: The Cosmos SDK does not use Tendermint’s PubKey interface anymore, but its own cryptotypes.PubKey. Updates to reflect this.
  • 2021 May 3: Rename clientCtx.JSONMarshaler to clientCtx.JSONCodec.
  • 2021 June 10: Add clientCtx.Codec: codec.Codec.

Status

Accepted

Context

This ADR is a continuation of the motivation, design, and context established in ADR 019, namely, we aim to design the Protocol Buffer migration path for the client-side of the Cosmos SDK. Specifically, the client-side migration path primarily includes tx generation and signing, message construction and routing, in addition to CLI & REST handlers and business logic (i.e. queriers). With this in mind, we will tackle the migration path via two main areas, txs and querying. However, this ADR solely focuses on transactions. Querying should be addressed in a future ADR, but it should build off of these proposals. Based on detailed discussions (#6030 and #6078), the original design for transactions was changed substantially from an oneof /JSON-signing approach to the approach described below.

Decision

Transactions

Since interface values are encoded with google.protobuf.Any in state (see ADR 019), sdk.Msgs are encoding with Any in transactions. One of the main goals of using Any to encode interface values is to have a core set of types which is reused by apps so that clients can safely be compatible with as many chains as possible. It is one of the goals of this specification to provide a flexible cross-chain transaction format that can serve a wide variety of use cases without breaking client compatibility. In order to facilitate signing, transactions are separated into TxBody, which will be re-used by SignDoc below, and signatures:
// types/types.proto
package cosmos_sdk.v1;

message Tx {
    TxBody body = 1;
    AuthInfo auth_info = 2;
    // A list of signatures that matches the length and order of AuthInfo's signer_infos to
    // allow connecting signature meta information like public key and signing mode by position.
    repeated bytes signatures = 3;
}

// A variant of Tx that pins the signer's exact binary represenation of body and
// auth_info. This is used for signing, broadcasting and verification. The binary
// `serialize(tx: TxRaw)` is stored in Tendermint and the hash `sha256(serialize(tx: TxRaw))`
// becomes the "txhash", commonly used as the transaction ID.
message TxRaw {
    // A protobuf serialization of a TxBody that matches the representation in SignDoc.
    bytes body = 1;
    // A protobuf serialization of an AuthInfo that matches the representation in SignDoc.
    bytes auth_info = 2;
    // A list of signatures that matches the length and order of AuthInfo's signer_infos to
    // allow connecting signature meta information like public key and signing mode by position.
    repeated bytes signatures = 3;
}

message TxBody {
    // A list of messages to be executed. The required signers of those messages define
    // the number and order of elements in AuthInfo's signer_infos and Tx's signatures.
    // Each required signer address is added to the list only the first time it occurs.
    //
    // By convention, the first required signer (usually from the first message) is referred
    // to as the primary signer and pays the fee for the whole transaction.
    repeated google.protobuf.Any messages = 1;
    string memo = 2;
    int64 timeout_height = 3;
    repeated google.protobuf.Any extension_options = 1023;
}

message AuthInfo {
    // This list defines the signing modes for the required signers. The number
    // and order of elements must match the required signers from TxBody's messages.
    // The first element is the primary signer and the one which pays the fee.
    repeated SignerInfo signer_infos = 1;
    // The fee can be calculated based on the cost of evaluating the body and doing signature verification of the signers. This can be estimated via simulation.
    Fee fee = 2;
}

message SignerInfo {
    // The public key is optional for accounts that already exist in state. If unset, the
    // verifier can use the required signer address for this position and lookup the public key.
    google.protobuf.Any public_key = 1;
    // ModeInfo describes the signing mode of the signer and is a nested
    // structure to support nested multisig pubkey's
    ModeInfo mode_info = 2;
    // sequence is the sequence of the account, which describes the
    // number of committed transactions signed by a given address. It is used to prevent
    // replay attacks.
    uint64 sequence = 3;
}

message ModeInfo {
    oneof sum {
        Single single = 1;
        Multi multi = 2;
    }

    // Single is the mode info for a single signer. It is structured as a message
    // to allow for additional fields such as locale for SIGN_MODE_TEXTUAL in the future
    message Single {
        SignMode mode = 1;
    }

    // Multi is the mode info for a multisig public key
    message Multi {
        // bitarray specifies which keys within the multisig are signing
        CompactBitArray bitarray = 1;
        // mode_infos is the corresponding modes of the signers of the multisig
        // which could include nested multisig public keys
        repeated ModeInfo mode_infos = 2;
    }
}

enum SignMode {
    SIGN_MODE_UNSPECIFIED = 0;

    SIGN_MODE_DIRECT = 1;

    SIGN_MODE_TEXTUAL = 2;

    SIGN_MODE_LEGACY_AMINO_JSON = 127;
}
As will be discussed below, in order to include as much of the Tx as possible in the SignDoc, SignerInfo is separated from signatures so that only the raw signatures themselves live outside of what is signed over. Because we are aiming for a flexible, extensible cross-chain transaction format, new transaction processing options should be added to TxBody as soon those use cases are discovered, even if they can’t be implemented yet. Because there is coordination overhead in this, TxBody includes an extension_options field which can be used for any transaction processing options that are not already covered. App developers should, nevertheless, attempt to upstream important improvements to Tx.

Signing

All of the signing modes below aim to provide the following guarantees:
  • No Malleability: TxBody and AuthInfo cannot change once the transaction is signed
  • Predictable Gas: if I am signing a transaction where I am paying a fee, the final gas is fully dependent on what I am signing
These guarantees give the maximum amount confidence to message signers that manipulation of Txs by intermediaries can’t result in any meaningful changes.

SIGN_MODE_DIRECT

The “direct” signing behavior is to sign the raw TxBody bytes as broadcast over the wire. This has the advantages of:
  • requiring the minimum additional client capabilities beyond a standard protocol buffers implementation
  • leaving effectively zero holes for transaction malleability (i.e. there are no subtle differences between the signing and encoding formats which could potentially be exploited by an attacker)
Signatures are structured using the SignDoc below which reuses the serialization of TxBody and AuthInfo and only adds the fields which are needed for signatures:
// types/types.proto
message SignDoc {
    // A protobuf serialization of a TxBody that matches the representation in TxRaw.
    bytes body = 1;
    // A protobuf serialization of an AuthInfo that matches the representation in TxRaw.
    bytes auth_info = 2;
    string chain_id = 3;
    uint64 account_number = 4;
}
In order to sign in the default mode, clients take the following steps:
  1. Serialize TxBody and AuthInfo using any valid protobuf implementation.
  2. Create a SignDoc and serialize it using ADR 027.
  3. Sign the encoded SignDoc bytes.
  4. Build a TxRaw and serialize it for broadcasting.
Signature verification is based on comparing the raw TxBody and AuthInfo bytes encoded in TxRaw not based on any “canonicalization” algorithm which creates added complexity for clients in addition to preventing some forms of upgradeability (to be addressed later in this document). Signature verifiers do:
  1. Deserialize a TxRaw and pull out body and auth_info.
  2. Create a list of required signer addresses from the messages.
  3. For each required signer:
    • Pull account number and sequence from the state.
    • Obtain the public key either from state or AuthInfo’s signer_infos.
    • Create a SignDoc and serialize it using ADR 027.
    • Verify the signature at the same list position against the serialized SignDoc.

SIGN_MODE_LEGACY_AMINO

In order to support legacy wallets and exchanges, Amino JSON will be temporarily supported transaction signing. Once wallets and exchanges have had a chance to upgrade to protobuf based signing, this option will be disabled. In the meantime, it is foreseen that disabling the current Amino signing would cause too much breakage to be feasible. Note that this is mainly a requirement of the Cosmos Hub and other chains may choose to disable Amino signing immediately. Legacy clients will be able to sign a transaction using the current Amino JSON format and have it encoded to protobuf using the REST /tx/encode endpoint before broadcasting.

SIGN_MODE_TEXTUAL

As was discussed extensively in #6078, there is a desire for a human-readable signing encoding, especially for hardware wallets like the Ledger which display transaction contents to users before signing. JSON was an attempt at this but falls short of the ideal. SIGN_MODE_TEXTUAL is intended as a placeholder for a human-readable encoding which will replace Amino JSON. This new encoding should be even more focused on readability than JSON, possibly based on formatting strings like MessageFormat. In order to ensure that the new human-readable format does not suffer from transaction malleability issues, SIGN_MODE_TEXTUAL requires that the human-readable bytes are concatenated with the raw SignDoc to generate sign bytes. Multiple human-readable formats (maybe even localized messages) may be supported by SIGN_MODE_TEXTUAL when it is implemented.

Unknown Field Filtering

Unknown fields in protobuf messages should generally be rejected by transaction processors because:
  • important data may be present in the unknown fields, that if ignored, will cause unexpected behavior for clients
  • they present a malleability vulnerability where attackers can bloat tx size by adding random uninterpreted data to unsigned content (i.e. the master Tx, not TxBody)
There are also scenarios where we may choose to safely ignore unknown fields (Link) to provide graceful forwards compatibility with newer clients. We propose that field numbers with bit 11 set (for most use cases this is the range of 1024-2047) be considered non-critical fields that can safely be ignored if unknown. To handle this we will need an unknown field filter that:
  • always rejects unknown fields in unsigned content (i.e. top-level Tx and unsigned parts of AuthInfo if present based on the signing mode)
  • rejects unknown fields in all messages (including nested Anys) other than fields with bit 11 set
This will likely need to be a custom protobuf parser pass that takes message bytes and FileDescriptors and returns a boolean result.

Public Key Encoding

Public keys in the Cosmos SDK implement the cryptotypes.PubKey interface. We propose to use Any for protobuf encoding as we are doing with other interfaces (for example, in BaseAccount.PubKey and SignerInfo.PublicKey). The following public keys are implemented: secp256k1, secp256r1, ed25519 and legacy-multisignature. Ex:
message PubKey {
    bytes key = 1;
}
multisig.LegacyAminoPubKey has an array of Any’s member to support any protobuf public key type. Apps should only attempt to handle a registered set of public keys that they have tested. The provided signature verification ante handler decorators will enforce this.

CLI & REST

Currently, the REST and CLI handlers encode and decode types and txs via Amino JSON encoding using a concrete Amino codec. Being that some of the types dealt with in the client can be interfaces, similar to how we described in ADR 019, the client logic will now need to take a codec interface that knows not only how to handle all the types, but also knows how to generate transactions, signatures, and messages.
type AccountRetriever interface {
    GetAccount(clientCtx Context, addr sdk.AccAddress) (client.Account, error)

GetAccountWithHeight(clientCtx Context, addr sdk.AccAddress) (client.Account, int64, error)

EnsureExists(clientCtx client.Context, addr sdk.AccAddress)

error
  GetAccountNumberSequence(clientCtx client.Context, addr sdk.AccAddress) (uint64, uint64, error)
}

type Generator interface {
    NewTx()

TxBuilder
  NewFee()

ClientFee
  NewSignature()

ClientSignature
  MarshalTx(tx types.Tx) ([]byte, error)
}

type TxBuilder interface {
    GetTx()

sdk.Tx

  SetMsgs(...sdk.Msg)

error
  GetSignatures() []sdk.Signature
  SetSignatures(...sdk.Signature)

GetFee()

sdk.Fee
  SetFee(sdk.Fee)

GetMemo()

string
  SetMemo(string)
}
We then update Context to have new fields: Codec, TxGenerator, and AccountRetriever, and we update AppModuleBasic.GetTxCmd to take a Context which should have all of these fields pre-populated. Each client method should then use one of the Init methods to re-initialize the pre-populated Context. tx.GenerateOrBroadcastTx can be used to generate or broadcast a transaction. For example:
import "github.com/spf13/cobra"
import "github.com/cosmos/cosmos-sdk/client"
import "github.com/cosmos/cosmos-sdk/client/tx"

func NewCmdDoSomething(clientCtx client.Context) *cobra.Command {
    return &cobra.Command{
    RunE: func(cmd *cobra.Command, args []string)

error {
    clientCtx := ctx.InitWithInput(cmd.InOrStdin())
    msg := NewSomeMsg{...
}

tx.GenerateOrBroadcastTx(clientCtx, msg)
},
}
}

Future Improvements

SIGN_MODE_TEXTUAL specification

A concrete specification and implementation of SIGN_MODE_TEXTUAL is intended as a near-term future improvement so that the ledger app and other wallets can gracefully transition away from Amino JSON.

SIGN_MODE_DIRECT_AUX

(*Documented as option (3) in Link) We could add a mode SIGN_MODE_DIRECT_AUX to support scenarios where multiple signatures are being gathered into a single transaction but the message composer does not yet know which signatures will be included in the final transaction. For instance, I may have a 3/5 multisig wallet and want to send a TxBody to all 5 signers to see who signs first. As soon as I have 3 signatures then I will go ahead and build the full transaction. With SIGN_MODE_DIRECT, each signer needs to sign the full AuthInfo which includes the full list of all signers and their signing modes, making the above scenario very hard. SIGN_MODE_DIRECT_AUX would allow “auxiliary” signers to create their signature using only TxBody and their own PublicKey. This allows the full list of signers in AuthInfo to be delayed until signatures have been collected. An “auxiliary” signer is any signer besides the primary signer who is paying the fee. For the primary signer, the full AuthInfo is actually needed to calculate gas and fees because that is dependent on how many signers and which key types and signing modes they are using. Auxiliary signers, however, do not need to worry about fees or gas and thus can just sign TxBody. To generate a signature in SIGN_MODE_DIRECT_AUX these steps would be followed:
  1. Encode SignDocAux (with the same requirement that fields must be serialized in order):
    // types/types.proto
    message SignDocAux {
        bytes body_bytes = 1;
        // PublicKey is included in SignDocAux :
        // 1. as a special case for multisig public keys. For multisig public keys,
        // the signer should use the top-level multisig public key they are signing
        // against, not their own public key. This is to prevent against a form
        // of malleability where a signature could be taken out of context of the
        // multisig key that was intended to be signed for
        // 2. to guard against scenario where configuration information is encoded
        // in public keys (it has been proposed) such that two keys can generate
        // the same signature but have different security properties
        //
        // By including it here, the composer of AuthInfo cannot reference the
        // a public key variant the signer did not intend to use
        PublicKey public_key = 2;
        string chain_id = 3;
        uint64 account_number = 4;
    }
    
  2. Sign the encoded SignDocAux bytes
  3. Send their signature and SignerInfo to primary signer who will then sign and broadcast the final transaction (with SIGN_MODE_DIRECT and AuthInfo added) once enough signatures have been collected

SIGN_MODE_DIRECT_RELAXED

(Documented as option (1)(a) in Link) This is a variation of SIGN_MODE_DIRECT where multiple signers wouldn’t need to coordinate public keys and signing modes in advance. It would involve an alternate SignDoc similar to SignDocAux above with fee. This could be added in the future if client developers found the burden of collecting public keys and modes in advance too burdensome.

Consequences

Positive

  • Significant performance gains.
  • Supports backward and forward type compatibility.
  • Better support for cross-language clients.
  • Multiple signing modes allow for greater protocol evolution

Negative

  • google.protobuf.Any type URLs increase transaction size although the effect may be negligible or compression may be able to mitigate it.

Neutral

References