变更记录
- 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:
SignDoc 尽可能包含更多 Tx 内容,SignerInfo 与签名本身被分离,这样被签名内容之外只保留原始签名字节。
由于我们的目标是实现一种灵活、可扩展的跨链交易格式,因此一旦发现新的交易处理使用场景,即使暂时还无法实现,也应尽快将新的交易处理选项加入 TxBody。
考虑到这会带来一定的协调开销,TxBody 包含了一个 extension_options 字段,可用于承载尚未覆盖的交易处理选项。尽管如此,应用开发者仍应尽量将重要的 Tx 改进上游化。
签名
下列所有签名模式都旨在提供以下保证:- 无可塑性:一旦交易签名完成,
TxBody和AuthInfo就不能再发生变化 - Gas 可预测:如果我签署的是一笔由我支付手续费的交易,那么最终 Gas 完全取决于我签署的内容
Tx 的操作不会导致任何有意义的变化。
SIGN_MODE_DIRECT
“direct” 签名行为是直接对通过网络广播的原始 TxBody 字节进行签名。这样做的优势包括:
- 除标准 protocol buffers 实现外,对客户端额外能力的要求最低
- 实际上几乎不给交易可塑性留下任何空间(即签名格式与编码格式之间不存在可能被攻击者利用的微妙差异)
SignDoc 结构,它复用了 TxBody 和 AuthInfo 的序列化结果,并且只增加签名所需的字段:
- 使用任意有效的 protobuf 实现序列化
TxBody和AuthInfo。 - 创建一个
SignDoc,并使用 ADR 027 对其进行序列化。 - 对编码后的
SignDoc字节进行签名。 - 构建一个
TxRaw,并将其序列化后用于广播。
TxRaw 中编码的原始 TxBody 和 AuthInfo 字节,而不是依赖任何 “规范化” 算法;后者除了给客户端带来额外复杂度外,还会阻碍某些升级能力(本文后续会讨论)。
签名验证器会执行以下操作:
- 反序列化一个
TxRaw,并提取出body和auth_info。 - 从消息中创建必需签名者地址列表。
- 对每个必需签名者:
- 从状态中获取 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)中添加随机的未解释数据来膨胀交易大小
1024-2047 范围)视为非关键字段;如果这些字段未知,可以安全地忽略。
为此,我们需要一个未知字段过滤器,其行为应当如下:
- 始终拒绝未签名内容中的未知字段(即顶层
Tx,以及如果基于签名模式存在的话,AuthInfo中未签名的部分) - 在所有消息中拒绝未知字段(包括嵌套的
Any),但第 11 位被设置的字段除外
FileDescriptor,并返回一个布尔结果。
公钥编码
Cosmos SDK 中的公钥实现了cryptotypes.PubKey 接口。
我们建议像处理其他接口一样使用 Any 进行 protobuf 编码(例如 BaseAccount.PubKey 和 SignerInfo.PublicKey)。
当前已实现的公钥包括:secp256k1、secp256r1、ed25519 和 legacy-multisignature。
示例:
multisig.LegacyAminoPubKey 具有一个由 Any 组成的成员数组,用于支持任意 protobuf 公钥类型。
应用只应尝试处理自己已经测试过的一组已注册公钥。提供的签名校验 ante handler decorator 会强制执行这一点。
CLI 与 REST
目前,REST 和 CLI 处理器使用具体的 Amino codec,通过 Amino JSON 编码来对类型和交易进行编解码。由于客户端处理的某些类型可能是接口,正如我们在 ADR 019 中所描述的那样,客户端逻辑现在需要接收一个 codec 接口,这个接口不仅知道如何处理所有类型,还知道如何生成交易、签名和消息。Context,加入新字段:Codec、TxGenerator 和 AccountRetriever,并更新 AppModuleBasic.GetTxCmd,使其接收一个 Context,该 Context 应当预先填充好所有这些字段。
随后,每个客户端方法都应使用某个 Init 方法重新初始化这个预填充的 Context。tx.GenerateOrBroadcastTx 可用于生成或广播交易。例如:
未来改进
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 中生成签名,需要执行以下步骤:
-
对
SignDocAux进行编码(同样要求字段必须按顺序序列化): -
对编码后的
SignDocAux字节进行签名 -
将他们的签名和
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
oneofhandling - 2020 April 30: Switch to
Any - 2020 May 14: Describe public key encoding
- 2020 June 08: Store
TxBodyandAuthInfoas bytes inSignDoc; DocumentTxRawas broadcast and storage type. - 2020 August 07: Use ADR 027 for serializing
SignDoc. - 2020 August 19: Move sequence field from
SignDoctoSignerInfo, as discussed in #6966. - 2020 September 25: Remove
PublicKeytype in favor ofsecp256k1.PubKey,ed25519.PubKeyandmultisig.LegacyAminoPubKey. - 2020 October 15: Add
GetAccountandGetAccountWithHeightmethods to theAccountRetrieverinterface. - 2021 Feb 24: The Cosmos SDK does not use Tendermint’s
PubKeyinterface anymore, but its owncryptotypes.PubKey. Updates to reflect this. - 2021 May 3: Rename
clientCtx.JSONMarshalertoclientCtx.JSONCodec. - 2021 June 10: Add
clientCtx.Codec: codec.Codec.
Status
AcceptedContext
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 anoneof /JSON-signing
approach to the approach described below.
Decision
Transactions
Since interface values are encoded withgoogle.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:
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:
TxBodyandAuthInfocannot 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
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)
SignDoc below which reuses the serialization of
TxBody and AuthInfo and only adds the fields which are needed for signatures:
- Serialize
TxBodyandAuthInfousing any valid protobuf implementation. - Create a
SignDocand serialize it using ADR 027. - Sign the encoded
SignDocbytes. - Build a
TxRawand serialize it for broadcasting.
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:
- Deserialize a
TxRawand pull outbodyandauth_info. - Create a list of required signer addresses from the messages.
- For each required signer:
- Pull account number and sequence from the state.
- Obtain the public key either from state or
AuthInfo’ssigner_infos. - Create a
SignDocand 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, notTxBody)
- always rejects unknown fields in unsigned content (i.e. top-level
Txand unsigned parts ofAuthInfoif present based on the signing mode) - rejects unknown fields in all messages (including nested
Anys) other than fields with bit 11 set
FileDescriptors and returns a boolean result.
Public Key Encoding
Public keys in the Cosmos SDK implement thecryptotypes.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:
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.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:
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:
-
Encode
SignDocAux(with the same requirement that fields must be serialized in order): -
Sign the encoded
SignDocAuxbytes -
Send their signature and
SignerInfoto primary signer who will then sign and broadcast the final transaction (withSIGN_MODE_DIRECTandAuthInfoadded) 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.Anytype URLs increase transaction size although the effect may be negligible or compression may be able to mitigate it.