变更日志

  • 2021 年 12 月 06 日:初始草案
  • 2022 年 02 月 07 日:Ledger 团队已阅读草案并在概念层面确认。
  • 2022 年 12 月 01 日:移除 Any 标题屏上的 Object: 前缀。
  • 2022 年 12 月 13 日:当字节长度 > 32 时,对字节哈希进行签名。
  • 2023 年 03 月 27 日:更新 Any 值渲染器,省略消息头屏。

状态

已接受。实现已开始。小型值渲染器的细节仍需打磨。

摘要

本附录描述了值渲染器,它们用于借助字符串数组以便于人类理解的方式显示 Protobuf 值。

值渲染器

值渲染器描述了不同 Protobuf 类型的值应如何编码为字符串数组。值渲染器可以形式化为一组双射函数 func renderT(value T) []string,其中 T 是下文本规范定义的某一种 Protobuf 类型。

Protobuf number

  • 适用于:
    • protobuf 数值整数类型(int{32,64}、uint{32,64}、sint{32,64}、fixed{32,64}、sfixed{32,64})
    • customtype 为 github.com/cosmos/cosmos-sdk/types.Int 或 github.com/cosmos/cosmos-sdk/types.Dec 的字符串
    • customtype 为 github.com/cosmos/cosmos-sdk/types.Int 或 github.com/cosmos/cosmos-sdk/types.Dec 的字节串
  • 末尾的小数零始终会被移除
  • 每三位整数数字之间使用 ' 进行格式化。
  • 使用 . 作为小数分隔符。

示例

  • 1000 (uint64) -> 1'000
  • "1000000.00"(表示 Dec 的字符串)-> 1'000'000
  • "1000000.10"(表示 Dec 的字符串)-> 1'000'000.1

coin

  • 适用于 cosmos.base.v1beta1.Coin。
  • 面额会使用 Metadata 转换为 display 面额(如果可用)。这需要进行状态查询。Metadata 的定义可见于 bank protobuf definition。如果 display 字段为空或为 nil,则不执行任何面额转换。
  • 数量会转换为 display 面额对应的数量,并按上文的 number 方式渲染
    • 我们不会更改面额的大小写。实际中,状态里的 display 面额通常以小写存储(例如 10 atom),但在日常使用中常以大写展示(例如 10 ATOM)。值渲染器会保留状态中使用的大小写,但我们可能会建议链将面额元数据改为大写,以获得更好的用户显示效果。
  • 面额与数量之间使用一个空格(例如 10 atom)。
  • 将来,IBC 面额或许可以转换为 DID/IID,前提是我们能找到一种稳健的实现方式(例如 cosmos:cosmos:hub:bank:denom:atom)

示例

  • 1000000000uatom -> [”1’000 atom”],因为 atom 是 metadata 的 display 面额。

coins

  • coin 数组会显示为将每个 coin 按上述规范编码后的结果,再使用分隔符 ", "(逗号加空格,不包含引号)连接起来。
  • coin 列表按 display 面额的 Unicode 码点排序:A-Z < a-z。例如,字符串 aAbBcC 排序后为 ABCabc。
    • 如果 coins 列表包含 0 个元素,则会渲染为 zero

示例

  • [”3cosm”, “2000000uatom”] -> 2 atom, 3 COSM(假设 display 面额分别为 atom 和 COSM)
  • [”10atom”, “20Acoin”] -> 20 Acoin, 10 atom(假设 display 面额分别为 atom 和 Acoin)
  • [] -> zero

repeated

  • 适用于所有 repeated 字段,但 cosmos.tx.v1beta1.TxBody#Messages 除外,它有专门的编码方式(见 ADR-050)。
  • repeated 类型使用以下模板:
<field_name>: <int> <field_kind>
<field_name> (<index>/<int>): <value rendered 1st line>
<optional value rendered in the next lines>
<field_name> (<index>/<int>): <value rendered 1st line>
<optional value rendered in the next lines>
End of <field_name>.
其中:
  • field_name 是 repeated 字段的 Protobuf 字段名
  • field_kind:
    • 如果 repeated 字段的类型是消息,field_kind 为消息名
    • 如果 repeated 字段的类型是枚举,field_kind 为枚举名
    • 其他情况中,field_kind 为 protobuf 原始类型(例如 "string" 或 "bytes")
  • int 是数组长度
  • index 是 repeated 字段从 1 开始的索引

示例

给定如下 proto 定义:
message AllowedMsgAllowance {
  repeated string allowed_messages = 1;
}
并初始化为:
x := []AllowedMsgAllowance{"cosmos.bank.v1beta1.MsgSend", "cosmos.gov.v1.MsgVote"
}
则其值渲染编码如下:
Allowed messages: 2 strings
Allowed messages (1/2): cosmos.bank.v1beta1.MsgSend
Allowed messages (2/2): cosmos.gov.v1.MsgVote
End of Allowed messages

message

  • 适用于所有没有自定义编码的 Protobuf 消息。
  • 字段名遵循 句式大小写
    • 将每个 _ 替换为空格
    • 将句子的首字母大写
  • 字段名按其 Protobuf 字段编号排序
  • 屏幕标题是字段名,屏幕内容是值。
  • 嵌套:
    • 如果某个字段包含嵌套消息,我们会使用以下模板对底层消息进行值渲染:
    <field_name>: <1st line of value-rendered message>
    > <lines 2-n of value-rendered message>             // 注意 `>` 前缀。
    
    • > 字符用于表示嵌套。每增加一层嵌套,就再增加一个 >。

示例

给定以下 Protobuf 消息:
enum VoteOption {
  VOTE_OPTION_UNSPECIFIED = 0;
  VOTE_OPTION_YES = 1;
  VOTE_OPTION_ABSTAIN = 2;
  VOTE_OPTION_NO = 3;
  VOTE_OPTION_NO_WITH_VETO = 4;
}

message WeightedVoteOption {
  VoteOption option = 1;
  string     weight = 2 [(cosmos_proto.scalar) = "cosmos.Dec"];
}

message Vote {
  uint64 proposal_id = 1;
  string voter       = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
  reserved 3;
  repeated WeightedVoteOption options = 4;
}
我们会得到 Vote 消息的如下编码:
Vote object
> Proposal id: 4
> Voter: cosmos1abc...def
> Options: 2 WeightedVoteOptions
> Options (1/2): WeightedVoteOption object
>> Option: VOTE_OPTION_YES
>> Weight: 0.7
> Options (2/2): WeightedVoteOption object
>> Option: VOTE_OPTION_NO
>> Weight: 0.3
> End of Options

枚举

  • 将枚举变体名显示为字符串。

示例

参见上文 message Vote{} 的示例。

google.protobuf.Any

  • 适用于 google.protobuf.Any
  • 渲染为:
<type_url>
> <value rendered underlying message>
不过有一个例外:当底层消息是没有自定义编码的 Protobuf 消息时,会省略消息头屏,并减少一级缩进。 具有自定义编码的消息,包括 google.protobuf.Timestamp、google.protobuf.Duration、google.protobuf.Any、cosmos.base.v1beta1.Coin,以及具有应用自定义编码的消息,将保留其头部和缩进级别。

示例

消息头屏被去除,并减少一级缩进:
/cosmos.gov.v1.Vote
> Proposal id: 4
> Vote: cosmos1abc...def
> Options: 2 WeightedVoteOptions
> Options (1/2): WeightedVoteOption object
>> Option: Yes
>> Weight: 0.7
> Options (2/2): WeightedVoteOption object
>> Option: No
>> Weight: 0.3
> End of Options
具有自定义编码的消息:
/cosmos.base.v1beta1.Coin
> 10uatom

google.protobuf.Timestamp

使用 RFC 3339 进行渲染(它是 ISO 8601 的一种简化形式),这是当前针对可移植时间值的推荐格式。渲染结果始终使用 "Z"(UTC)作为时区。它只使用必要的秒小数位;如果时间戳没有小数秒,则完全省略小数部分。(得到的时间戳不会自动按标准字典序排序,但我们更倾向于更短字符串带来的可读性。)

示例

具有 1136214245 秒和 700000000 纳秒的时间戳会被渲染为 2006-01-02T15:04:05.7Z。 具有 1136214245 秒和零纳秒的时间戳会被渲染为 2006-01-02T15:04:05Z。

google.protobuf.Duration

duration proto 表示原始的秒数和纳秒数。 它会依次渲染为更长的时间单位:天、小时和分钟,外加剩余的秒数。 前导和尾随的零数量单位会被省略,但位于非零单位之间的所有单位都会显示,例如 3 days, 0 hours, 0 minutes, 5 seconds。 更长的时间单位,例如月或年,并不精确。 周虽然是精确的,但并不常用,91 days 比 13 weeks 更容易立即理解。尽管 days 也可能带来问题,例如在夏令时切换时,相邻两天的中午到中午可能是 23 或 25 小时,但相比只使用小时(例如 91 days 对比 2184 hours),使用严格的 24 小时天数仍有明显优势。 当纳秒非零时,会显示为小数秒,并且只保留最少位数,例如 0.5 seconds。 恰好为零的 duration 显示为 0 seconds。 当数量恰好为一时,单位使用单数形式(不带结尾 s);其他情况使用复数形式。 负的 duration 会以前导负号(-)表示。 示例:
  • 1 day
  • 30 days
  • -1 day, 12 hours
  • 3 hours, 0 minutes, 53.025 seconds

bytes

  • 长度小于或等于 35 的字节串会渲染为十六进制,全部使用大写字母,不带 0x 前缀。
  • 长度大于 35 的字节串会使用 SHA256 哈希。渲染文本为 SHA-256=,后接 32 字节哈希值,使用十六进制、全部大写字母、不带 0x 前缀。
  • 最终还会将十六进制字符串按每 4 位分组,使用空格 ' ' 作为分隔符。如果字节长度为奇数,则剩余的 2 个十六进制字符位于末尾。
之所以选择 35,是因为在应用上述 3 条规则后,这是“哈希后加前缀”的表示长度仍然长于“直接格式化原始数据”的最长长度。更具体地说:
  • 35 字节数组会有 70 个十六进制字符,加上 17 个空格字符,总计 87 个字符。
  • 从长度 36 开始的字节数组会被哈希为 32 字节,即 64 个十六进制字符加 15 个空格,再加上 SHA-256= 前缀,总长度为 87 个字符。 此外,secp256k1 公钥长度为 33,因此它们的 Textual 表示不会是其哈希值,这正是我们希望避免的。
注意:长度超过 35 字节的数据渲染方式不可逆。关于这一点的讨论,请参见 ADR-050 的可逆性章节。

示例

输入以字节数组形式展示。
  • [0]: 00
  • [0,1,2]: 0001 02
  • [0,1,2,..,34]: 0001 0203 0405 0607 0809 0A0B 0C0D 0E0F 1011 1213 1415 1617 1819 1A1B 1C1D 1E1F 2021 22
  • [0,1,2,..,35]: SHA-256=5D7E 2D9B 1DCB C85E 7C89 0036 A2CF 2F9F E7B6 6554 F2DF 08CE C6AA 9C0A 25C9 9C21

address bytes

我们目前在 protobuf 中使用 string 类型表示地址,因此这可能并不需要;但如果在 sign mode textual 中使用任何地址字节,它们应当使用 bech32 格式进行渲染

strings

字符串按原样渲染。

默认值

  • 每个字段的 Protobuf 默认值都会被跳过。

示例

message TestData {
  string signer = 1;
  string metadata = 2;
}
myTestData := TestData{
    Signer: "cosmos1abc"
}
我们会得到 TestData 消息的如下编码:
TestData object
> Signer: cosmos1abc

bool

布尔值会渲染为 True 或 False。

[已废弃] 使用自定义 msg_title 替代 Msg type_url

本段位于附录中,仅用于提供信息,将在 ADR 的下一次更新中移除。

Changelog

  • Dec 06, 2021: Initial Draft
  • Feb 07, 2022: Draft read and concept-ACKed by the Ledger team.
  • Dec 01, 2022: Remove Object: prefix on Any header screen.
  • Dec 13, 2022: Sign over bytes hash when bytes length > 32.
  • Mar 27, 2023: Update Any value renderer to omit message header screen.

Status

Accepted. Implementation started. Small value renderers details still need to be polished.

Abstract

This Annex describes value renderers, which are used for displaying Protobuf values in a human-friendly way using a string array.

Value Renderers

Value Renderers describe how values of different Protobuf types should be encoded as a string array. Value renderers can be formalized as a set of bijective functions func renderT(value T) []string, where T is one of the below Protobuf types for which this spec is defined.

Protobuf number

  • Applies to:
    • protobuf numeric integer types (int{32,64}, uint{32,64}, sint{32,64}, fixed{32,64}, sfixed{32,64})
    • strings whose customtype is github.com/cosmos/cosmos-sdk/types.Int or github.com/cosmos/cosmos-sdk/types.Dec
    • bytes whose customtype is github.com/cosmos/cosmos-sdk/types.Int or github.com/cosmos/cosmos-sdk/types.Dec
  • Trailing decimal zeroes are always removed
  • Formatting with 's for every three integral digits.
  • Usage of . to denote the decimal delimiter.

Examples

  • 1000 (uint64) -> 1'000
  • "1000000.00" (string representing a Dec) -> 1'000'000
  • "1000000.10" (string representing a Dec) -> 1'000'000.1

coin

  • Applies to cosmos.base.v1beta1.Coin.
  • Denoms are converted to display denoms using Metadata (if available). This requires a state query. The definition of Metadata can be found in the bank protobuf definition. If the display field is empty or nil, then we do not perform any denom conversion.
  • Amounts are converted to display denom amounts and rendered as numbers above
    • We do not change the capitalization of the denom. In practice, display denoms are stored in lowercase in state (e.g. 10 atom), however they are often showed in UPPERCASE in everyday life (e.g. 10 ATOM). Value renderers keep the case used in state, but we may recommend chains changing the denom metadata to be uppercase for better user display.
  • One space between the denom and amount (e.g. 10 atom).
  • In the future, IBC denoms could maybe be converted to DID/IIDs, if we can find a robust way for doing this (ex. cosmos:cosmos:hub:bank:denom:atom)

Examples

  • 1000000000uatom -> ["1'000 atom"], because atom is the metadata’s display denom.

coins

  • an array of coin is display as the concatenation of each coin encoded as the specification above, the joined together with the delimiter ", " (a comma and a space, no quotes around).
  • the list of coins is ordered by unicode code point of the display denom: A-Z < a-z. For example, the string aAbBcC would be sorted ABCabc.
    • if the coins list had 0 items in it then it’ll be rendered as zero

Example

  • ["3cosm", "2000000uatom"] -> 2 atom, 3 COSM (assuming the display denoms are atom and COSM)
  • ["10atom", "20Acoin"] -> 20 Acoin, 10 atom (assuming the display denoms are atom and Acoin)
  • [] -> zero

repeated

  • Applies to all repeated fields, except cosmos.tx.v1beta1.TxBody#Messages, which has a particular encoding (see ADR-050).
  • A repeated type has the following template:
<field_name>: <int> <field_kind>
<field_name> (<index>/<int>): <value rendered 1st line>
<optional value rendered in the next lines>
<field_name> (<index>/<int>): <value rendered 1st line>
<optional value rendered in the next lines>
End of <field_name>.
where:
  • field_name is the Protobuf field name of the repeated field
  • field_kind:
    • if the type of the repeated field is a message, field_kind is the message name
    • if the type of the repeated field is an enum, field_kind is the enum name
    • in any other case, field_kind is the protobuf primitive type (e.g. “string” or “bytes”)
  • int is the length of the array
  • index is one based index of the repeated field

Examples

Given the proto definition:
message AllowedMsgAllowance {
  repeated string allowed_messages = 1;
}
and initializing with:
x := []AllowedMsgAllowance{"cosmos.bank.v1beta1.MsgSend", "cosmos.gov.v1.MsgVote"
}
we have the following value-rendered encoding:
Allowed messages: 2 strings
Allowed messages (1/2): cosmos.bank.v1beta1.MsgSend
Allowed messages (2/2): cosmos.gov.v1.MsgVote
End of Allowed messages

message

  • Applies to all Protobuf messages that do not have a custom encoding.
  • Field names follow sentence case
    • replace each _ with a space
    • capitalize first letter of the sentence
  • Field names are ordered by their Protobuf field number
  • Screen title is the field name, and screen content is the value.
  • Nesting:
    • if a field contains a nested message, we value-render the underlying message using the template:
    <field_name>: <1st line of value-rendered message>
    > <lines 2-n of value-rendered message>             // Notice the `>` prefix.
    
    • > character is used to denote nesting. For each additional level of nesting, add >.

Examples

Given the following Protobuf messages:
enum VoteOption {
  VOTE_OPTION_UNSPECIFIED = 0;
  VOTE_OPTION_YES = 1;
  VOTE_OPTION_ABSTAIN = 2;
  VOTE_OPTION_NO = 3;
  VOTE_OPTION_NO_WITH_VETO = 4;
}

message WeightedVoteOption {
  VoteOption option = 1;
  string     weight = 2 [(cosmos_proto.scalar) = "cosmos.Dec"];
}

message Vote {
  uint64 proposal_id = 1;
  string voter       = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
  reserved 3;
  repeated WeightedVoteOption options = 4;
}
we get the following encoding for the Vote message:
Vote object
> Proposal id: 4
> Voter: cosmos1abc...def
> Options: 2 WeightedVoteOptions
> Options (1/2): WeightedVoteOption object
>> Option: VOTE_OPTION_YES
>> Weight: 0.7
> Options (2/2): WeightedVoteOption object
>> Option: VOTE_OPTION_NO
>> Weight: 0.3
> End of Options

Enums

  • Show the enum variant name as string.

Examples

See example above with message Vote{}.

google.protobuf.Any

  • Applies to google.protobuf.Any
  • Rendered as:
<type_url>
> <value rendered underlying message>
There is however one exception: when the underlying message is a Protobuf message that does not have a custom encoding, then the message header screen is omitted, and one level of indentation is removed. Messages that have a custom encoding, including google.protobuf.Timestamp, google.protobuf.Duration, google.protobuf.Any, cosmos.base.v1beta1.Coin, and messages that have an app-defined custom encoding, will preserve their header and indentation level.

Examples

Message header screen is stripped, one-level of indentation removed:
/cosmos.gov.v1.Vote
> Proposal id: 4
> Vote: cosmos1abc...def
> Options: 2 WeightedVoteOptions
> Options (1/2): WeightedVoteOption object
>> Option: Yes
>> Weight: 0.7
> Options (2/2): WeightedVoteOption object
>> Option: No
>> Weight: 0.3
> End of Options
Message with custom encoding:
/cosmos.base.v1beta1.Coin
> 10uatom

google.protobuf.Timestamp

Rendered using RFC 3339 (a simplification of ISO 8601), which is the current recommendation for portable time values. The rendering always uses “Z” (UTC) as the timezone. It uses only the necessary fractional digits of a second, omitting the fractional part entirely if the timestamp has no fractional seconds. (The resulting timestamps are not automatically sortable by standard lexicographic order, but we favor the legibility of the shorter string.)

Examples

The timestamp with 1136214245 seconds and 700000000 nanoseconds is rendered as 2006-01-02T15:04:05.7Z. The timestamp with 1136214245 seconds and zero nanoseconds is rendered as 2006-01-02T15:04:05Z.

google.protobuf.Duration

The duration proto expresses a raw number of seconds and nanoseconds. This will be rendered as longer time units of days, hours, and minutes, plus any remaining seconds, in that order. Leading and trailing zero-quantity units will be omitted, but all units in between nonzero units will be shown, e.g. 3 days, 0 hours, 0 minutes, 5 seconds. Even longer time units such as months or years are imprecise. Weeks are precise, but not commonly used - 91 days is more immediately legible than 13 weeks. Although days can be problematic, e.g. noon to noon on subsequent days can be 23 or 25 hours depending on daylight savings transitions, there is significant advantage in using strict 24-hour days over using only hours (e.g. 91 days vs 2184 hours). When nanoseconds are nonzero, they will be shown as fractional seconds, with only the minimum number of digits, e.g 0.5 seconds. A duration of exactly zero is shown as 0 seconds. Units will be given as singular (no trailing s) when the quantity is exactly one, and will be shown in plural otherwise. Negative durations will be indicated with a leading minus sign (-). Examples:
  • 1 day
  • 30 days
  • -1 day, 12 hours
  • 3 hours, 0 minutes, 53.025 seconds

bytes

  • Bytes of length shorter or equal to 35 are rendered in hexadecimal, all capital letters, without the 0x prefix.
  • Bytes of length greater than 35 are hashed using SHA256. The rendered text is SHA-256=, followed by the 32-byte hash, in hexadecimal, all capital letters, without the 0x prefix.
  • The hexadecimal string is finally separated into groups of 4 digits, with a space ' ' as separator. If the bytes length is odd, the 2 remaining hexadecimal characters are at the end.
The number 35 was chosen because it is the longest length where the hashed-and-prefixed representation is longer than the original data directly formatted, using the 3 rules above. More specifically:
  • a 35-byte array will have 70 hex characters, plus 17 space characters, resulting in 87 characters.
  • byte arrays starting from length 36 will be be hashed to 32 bytes, which is 64 hex characters plus 15 spaces, and with the SHA-256= prefix, it takes 87 characters. Also, secp256k1 public keys have length 33, so their Textual representation is not their hashed value, which we would like to avoid.
Note: Data longer than 35 bytes are not rendered in a way that can be inverted. See ADR-050’s section about invertability for a discussion.

Examples

Inputs are displayed as byte arrays.
  • [0]: 00
  • [0,1,2]: 0001 02
  • [0,1,2,..,34]: 0001 0203 0405 0607 0809 0A0B 0C0D 0E0F 1011 1213 1415 1617 1819 1A1B 1C1D 1E1F 2021 22
  • [0,1,2,..,35]: SHA-256=5D7E 2D9B 1DCB C85E 7C89 0036 A2CF 2F9F E7B6 6554 F2DF 08CE C6AA 9C0A 25C9 9C21

address bytes

We currently use string types in protobuf for addresses so this may not be needed, but if any address bytes are used in sign mode textual they should be rendered with bech32 formatting

strings

Strings are rendered as-is.

Default Values

  • Default Protobuf values for each field are skipped.

Example

message TestData {
  string signer = 1;
  string metadata = 2;
}
myTestData := TestData{
    Signer: "cosmos1abc"
}
We get the following encoding for the TestData message:
TestData object
> Signer: cosmos1abc

bool

Boolean values are rendered as True or False.

[ABANDONED] Custom msg_title instead of Msg type_url

This paragraph is in the Annex for informational purposes only, and will be removed in a next update of the ADR.