本文深入探讨了 Solana 的 Token-2022 标准,它是 SPL Token 程序的一个向后兼容的新版本,支持通过扩展实现额外的功能。
Token-2022 是 SPL Token 程序的一个新的向后兼容版本,它通过 扩展 的形式支持额外的特性。这些扩展的字节码是 Token-2022 程序本身的一部分,没有部署单独的程序。为特定 token 激活的字节码由 mint 或 token 账户中启用的扩展决定。
例如,在最初的 SPL Token 程序中,添加诸如 token 名称、符号或 logo URL 等元数据需要像 Metaplex 这样的外部程序。但在 Token-2022 中,你可以通过在 token mint 上启用元数据扩展来添加元数据。
在本文中,我们将学习 Token-2022 在底层是如何工作的,以及为了支持扩展而做了哪些更改。我们将演练一个使用 Token-2022 创建带有扩展的 Solana token 的实际例子。
首先,我们将研究 Token-2022 在底层是如何扩展原始 SPL Token 程序的,账户布局如何保持兼容,以及指令集如何在不破坏现有程序的情况下增长。
Token-2022 是 SPL Token 程序的一个直接替代品。你之前可以执行的每个操作——铸造、转移、销毁、冻结、更改权限——仍然完全一样地工作。这是因为 Token-2022 保留了原始 SPL mint 账户的前 82 个字节和 token 账户的前 165 个字节,其中包含诸如供应量、小数位数、所有者、冻结权限和金额等字段。 下面是原始 SPL mint 账户的布局:

一个展示 SPL mint 账户前 82 个字节的内存布局的图表
原始程序 (SPL) 仅反序列化和处理该数据;Token-2022 反序列化相同的区域,然后继续读取其后的扩展。
扩展是内置于 Token-2022 程序中的可选特性,你可以在 mint 账户或 token 账户上启用它们。下图显示了 Token-2022 mint TLV 布局:原始 SPL mint 的固定 82 字节,后跟存储扩展数据的可变大小 TLV 区域。

原始 SPL Token 程序具有固定的二进制布局,其中每个字段的大小、类型和值都是预定的。Token-2022 在保留的前 82 到 165 字节区域中使用相同的固定二进制布局,但除此之外,它使用可变的类型-长度-值 (TLV) 编码方案来存储扩展。
类型–长度–值 (TLV) 是一种数据序列化方案。数据中的每个对象包含三个部分:
因为每个条目都说明了它的类型和长度,所以程序可以读取类型来理解它正在解析的内容,读取精确到该数量的字节作为值,然后使用长度直接跳到下一个条目。
即使类型未知,程序仍然可以使用长度来跳过它。
例如,在下图中,类型为 12 的条目的长度为 2 字节,因此它的值字段包含两个字节。如果程序无法识别类型 12,它可以跳过这 2 个字节并直接移动到下一个条目,类型 20。

一个说明类型长度值的图表
Token-2022 使用这种 TLV 编码方案实现。每个扩展都表示为一个 TLV 条目,并且具有以下上下文:
这种结构允许多个扩展按顺序打包到同一个账户中,而不会发生冲突。

为了说明 TLV 编码在实践中是如何工作的,让我们来看两个扩展:ImmutableOwner 和 MetadataPointer。
ImmutableOwner 扩展是一个 Token-2022 扩展,它可以防止 token 账户的所有者在创建后被更改。 我们现在来看看 TLV 布局,并在本文后面讨论更多关于它的用法。
ImmutableOwner 具有以下 TLV 属性:
0x0a (ImmutableOwner 的唯一 ID)0x01 0x00 0x00 0x00 (数字 1 编码为 4 字节小端整数)0x01 (该扩展存储一个字节的数据值,可以是 0 或 1)Rust 定义是一个空结构体:
pub struct ImmutableOwner {}
虽然该结构体没有字段,但 TLV 编码仍然为 V 保留一个字节,以表示该标志是否已启用 (1 为启用,0 为禁用)。 这就是为什么该条目具有非零长度的原因。
因此,它的 TLV 布局如下所示:

MetadataPointer TLV 布局MetadataPointer 扩展定义了 token 的链下元数据的位置以及谁可以更新它。
与 ImmutableOwner 扩展不同,MetadataPointer 包含值:两个公钥值(authority 和 metadata_address)。
以下是 Rust 结构体的样子:
struct MetadataPointer {
authority: PubKey;
metadata_address: PubKey;
}
在 TLV 中,布局将包含以下信息:
0x1a: MetadataPointer 的类型 ID (T)0x40 0x00 0x00 0x00: 长度 (L) = 64 字节 (小端序)<64 bytes>: 值 (V),序列化的 authority 和 metadata_address 指向元数据的公共地址。因为 V 包含一个 authority 和 metadata_address 公钥,所以让我们使用示例 32 字节公钥来表示它们,以便我们更好地了解布局的样子。
假设账户的 authority 公钥是:
7c4YH58z6Yd1H5pa9vHqPqN8P3f9DuzGcbj2duq5Vn6a
metadata_address 公钥是:
9A4q8Xzj8cQ6w6sKuS27rrR2i1cC6VnV4c7pg1Zg1Vgk
在 Solana 上,公钥是 Base58 编码的 32 字节值。值 (V) 是 authority 和 metadata_address 的串联。 当两个公钥都从 Base58 解码为其原始字节,然后以十六进制形式写入时,生成的 64 字节序列将为:
// authority (32 bytes)
0x62 0x21 0x73 0xa4 0x94 0x0c 0x4c 0x3c 0x29 0x7a 0x7f 0x3c 0x4f 0xc1 0x12 0x3f
0x3b 0x34 0xc6 0x51 0x3f 0x3e 0x24 0x23 0xf3 0x1c 0xaa 0x88 0x83 0x44 0xa3 0x37
// metadata_address (32 bytes)
0x79 0x30 0x0d 0x97 0x56 0x47 0xc2 0x18 0x79 0x35 0x0d 0xe6 0x18 0x9f 0x80 0xec
0xd6 0xca 0x36 0xa5 0xb1 0x77 0x5a 0xa8 0xe4 0x45 0x66 0x7b 0x85 0xf3 0x32 0xe1
假设 Token-2022 账户同时初始化了 ImmutableOwner 和 MetadataPointer 扩展,以下是 TLV 布局的样子:

读取账户时,程序可以迭代 TLV 部分,仅选择性地解码它识别的扩展。 未知扩展类型通过使用它们声明的长度跳过。
让我们考虑一下程序将如何处理未知扩展 UnknownExtension:
程序读取 UnknownExtension 的 TLV 条目:
0x0b (一个未知类型)0x14 0x00 0x00 0x00 (20,小端序)0x4…0xed (20 字节长,如 L 所指定)现在假设未知扩展具有 64 字节的值,程序将从 L 读取值 64,然后向前跳过 64 字节(在 V 上方)以找到下一个 T。 这种方法使 Token-2022 具有向前兼容性;未来的扩展不会破坏现有程序。
Solana 指令是对链上程序的调用,它由三个字段组成:
原始 SPL Token 程序有 25 个唯一指令。 Token-2022 支持所有这些指令,并在第 25 个指令之后添加新指令以启用新的扩展功能。
换句话说,Token-2022 中的现有 token 指令(如 MintTo,Transfer 或 Burn)的行为与 SPL 中的完全相同。
这是一个示例指令布局,它告诉 token 程序将 100 个 token 从 mint 账户铸造到目标 token 账户:

应用程序可以通过简单地更改其交易中的程序 ID 来采用 Token-2022。
以下是超出 Token 程序的原始 25 个指令的 Token-2022 指令列表。 命名为匹配它们初始化或管理的扩展名的 token 指令:
25: InitializeMintCloseAuthority
26: TransferFeeExtension
27: ConfidentialTransferExtension
28: DefaultAccountStateExtension
29: Reallocate
30: MemoTransferExtension
31: CreateNativeMint
32: InitializeNonTransferableMint
33: InterestBearingMintExtension
34: CpiGuardExtension
35: InitializePermanentDelegate
36: TransferHookExtension
37: ConfidentialTransferFeeExtension
38: WithdrawExcessLamports
39: MetadataPointerExtension
40: GroupPointerExtension
41: GroupMemberPointerExtension
42: ConfidentialMintBurnExtension
43: ScaledUiAmountExtension
44: PausableExtension
在 Token-2022 中,所有扩展都必须在初始化 mint 或 token 账户之前指定,以便可以为它们的数据分配足够的空间。 一旦初始化,就无法添加其他扩展。
你可以使用 spl-token CLI 创建带有扩展的 Token-2022 token。 它将计算所需的帐户大小,将每个扩展的 TLV 条目写入 mint 账户,并最终在幕后使用 InitializeMint2 指令初始化 mint。
这是 CLI 模板的样子:
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
create-token <扩展标识>
假设我们想向一个 mint 添加两个扩展:一个定义 token 的固定利率 (InterestBearingConfig),另一个使 mint 能够引用链下元数据 (MetadataPointer)。 我们可以运行以下命令,附加 —-interest-rate 和 —-enable-metadata 标志。 下面命令中的 5 是利率:
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
create-token --interest-rate 5 --enable-metadata
此命令创建一个同时启用了 InterestBearingConfig 和 MetadataPointer 扩展的 mint 账户。 在内部,它使用 ExtensionType::try_calculate_account_len::<Mint>(&[InterestBearingConfig, MetadataPointer])?; 分配完整的账户大小,立即写入利率参数,隐式地初始化 InterestBearingConfig 扩展,并为元数据保留一个 TLV 插槽。 现在,我们必须手动初始化元数据扩展。
try_calculate_account_len (来自 token-2022 库) 函数根据所选扩展计算所需的总空间,并且每个扩展的初始化指令在最终 mint 初始化锁定账户结构之前配置其特定参数。
如果你运行我们之前提到的 spl-token 命令来启用扩展,你将看到如下输出:

在此阶段:
5。你会注意到以下消息:“要初始化 mint 中的元数据,请运行 spl-token initialize-metadata 5bL18vT46c7SkdN37F3pb1GdxsN8kTcZCPoRcYj6cS5w <YOUR_TOKEN_NAME> <YOUR_TOKEN_SYMBOL> <YOUR_TOKEN_URI>,并使用 mint 授权签署。" 在响应中。
我们将运行该命令来最终确定元数据初始化并填充已分配的元数据 TLV 块。
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
initialize-metadata 5bL18vT46c7SkdN37F3pb1GdxsN8kTcZCPoRcYj6cS5w \
"MyToken" "MTKN" "https://example.com/mytoken.json"
结果将如下所示:
![一个 spl-token cli...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!