Solana 60 天课程

2025年02月27日更新 89 人订阅
原价: ¥ 66 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 指令自省 Solana 中的 Ed25519 签名验证 Solana - Switchboard 预言机使用 原生Solana:程序入口与执行 原生 Solana :读取账户数据 原生 Solana :Borsh 序列化 原生 Solana:使用 invoke 和 invoke signed 进行跨程序调用 原生 Solana :创建存储账户 (一) 原生 Solana:创建存储账户 二 原生 Solana: 函数分发 原生 Solana:关键安全检查 Rust 程序到 SBF 编译 sBPF 虚拟机和指令集介绍 跟踪 sBPF 指令执行和计算成本 Solana 程序执行与输入序列化 指令处理器和运行时设置 sBPF 内存布局和寄存器约定 使用 sBPF 汇编读取 Solana 指令输入 Solana 系统调用:sBPF 汇编中的日志记录

Solana Token-2022 标准规范

  • RareSkills
  • 发布于 2025-10-17 08:49
  • 阅读 2783

本文深入探讨了 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 的架构

首先,我们将研究 Token-2022 在底层是如何扩展原始 SPL Token 程序的,账户布局如何保持兼容,以及指令集如何在不破坏现有程序的情况下增长。

超集设计和向后兼容性

Token-2022 是 SPL Token 程序的一个直接替代品。你之前可以执行的每个操作——铸造、转移、销毁、冻结、更改权限——仍然完全一样地工作。这是因为 Token-2022 保留了原始 SPL mint 账户的前 82 个字节和 token 账户的前 165 个字节,其中包含诸如供应量、小数位数、所有者、冻结权限和金额等字段。 下面是原始 SPL mint 账户的布局:

一个展示数据如何在 SPL 账户中序列化的图表

一个展示 SPL mint 账户前 82 个字节的内存布局的图表

原始程序 (SPL) 仅反序列化和处理该数据;Token-2022 反序列化相同的区域,然后继续读取其后的扩展。

扩展是内置于 Token-2022 程序中的可选特性,你可以在 mint 账户或 token 账户上启用它们。下图显示了 Token-2022 mint TLV 布局:原始 SPL mint 的固定 82 字节,后跟存储扩展数据的可变大小 TLV 区域。

一个展示 Token-2022 中的数据布局如何与 SPL 不冲突的图表。

Token-2022 账户布局和 TLV 扩展

原始 SPL Token 程序具有固定的二进制布局,其中每个字段的大小、类型和值都是预定的。Token-2022 在保留的前 82 到 165 字节区域中使用相同的固定二进制布局,但除此之外,它使用可变的类型-长度-值 (TLV) 编码方案来存储扩展。

类型–长度–值 (TLV) 是一种数据序列化方案。数据中的每个对象包含三个部分:

  • 类型:一个标识符,指定数据代表什么。
  • 长度:数据的字节大小。
  • :实际的数据字节。

因为每个条目都说明了它的类型和长度,所以程序可以读取类型来理解它正在解析的内容,读取精确到该数量的字节作为值,然后使用长度直接跳到下一个条目。

即使类型未知,程序仍然可以使用长度来跳过它。

例如,在下图中,类型为 12 的条目的长度为 2 字节,因此它的值字段包含两个字节。如果程序无法识别类型 12,它可以跳过这 2 个字节并直接移动到下一个条目,类型 20

一个说明类型长度值的图表

一个说明类型长度值的图表

Token-2022 使用这种 TLV 编码方案实现。每个扩展都表示为一个 TLV 条目,并且具有以下上下文:

  • 类型 是扩展的唯一 ID。
  • 长度 是序列化的扩展数据的大小(以字节为单位)。
  • 是序列化的扩展数据本身。

这种结构允许多个扩展按顺序打包到同一个账户中,而不会发生冲突。

一个展示 TLV 如何端到端布局的图表

为了说明 TLV 编码在实践中是如何工作的,让我们来看两个扩展:ImmutableOwnerMetadataPointer

ImmutableOwner TLV 布局

ImmutableOwner 扩展是一个 Token-2022 扩展,它可以防止 token 账户的所有者在创建后被更改。 我们现在来看看 TLV 布局,并在本文后面讨论更多关于它的用法。

ImmutableOwner 具有以下 TLV 属性:

  • 类型 (T): 0x0a (ImmutableOwner 的唯一 ID)
  • 长度 (L): 0x01 0x00 0x00 0x00 (数字 1 编码为 4 字节小端整数)
  • 值 (V): 0x01 (该扩展存储一个字节的数据值,可以是 01)

Rust 定义是一个空结构体:

pub struct ImmutableOwner {}

虽然该结构体没有字段,但 TLV 编码仍然为 V 保留一个字节,以表示该标志是否已启用 (1 为启用,0 为禁用)。 这就是为什么该条目具有非零长度的原因。

因此,它的 TLV 布局如下所示:

ImmutableOwner 字节布局

MetadataPointer TLV 布局

MetadataPointer 扩展定义了 token 的链下元数据的位置以及谁可以更新它。

ImmutableOwner 扩展不同,MetadataPointer 包含值:两个公钥值(authoritymetadata_address)。

以下是 Rust 结构体的样子:

struct MetadataPointer {
    authority: PubKey;
    metadata_address: PubKey;
}

在 TLV 中,布局将包含以下信息:

  • 0x1a: MetadataPointer 的类型 ID (T)
  • 0x40 0x00 0x00 0x00: 长度 (L) = 64 字节 (小端序)
  • <64 bytes>: 值 (V),序列化的 authoritymetadata_address 指向元数据的公共地址。

因为 V 包含一个 authoritymetadata_address 公钥,所以让我们使用示例 32 字节公钥来表示它们,以便我们更好地了解布局的样子。

假设账户的 authority 公钥是:

7c4YH58z6Yd1H5pa9vHqPqN8P3f9DuzGcbj2duq5Vn6a

metadata_address 公钥是:

9A4q8Xzj8cQ6w6sKuS27rrR2i1cC6VnV4c7pg1Zg1Vgk

在 Solana 上,公钥是 Base58 编码的 32 字节值。值 (V) 是 authoritymetadata_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 账户同时初始化了 ImmutableOwnerMetadataPointer 扩展,以下是 TLV 布局的样子:

Token-2022 TLV 布局,展示了端到端布局的 ImmutableOwner 和 MetadataPointer

读取账户时,程序可以迭代 TLV 部分,仅选择性地解码它识别的扩展。 未知扩展类型通过使用它们声明的长度跳过。

让我们考虑一下程序将如何处理未知扩展 UnknownExtension

  • 程序读取 UnknownExtension 的 TLV 条目:

    • T = 0x0b (一个未知类型)
    • L = 0x14 0x00 0x00 0x00 (20,小端序)
    • V = 0x4…0xed (20 字节长,如 L 所指定)
  • 由于该类型未被识别,程序不会尝试解释该值
  • 但它仍然读取长度并按该字节数向前跳过(在本例中为 20)
  • 然后,它继续执行下一个 TLV 条目(如果有)

现在假设未知扩展具有 64 字节的值,程序将从 L 读取值 64,然后向前跳过 64 字节(在 V 上方)以找到下一个 T。 这种方法使 Token-2022 具有向前兼容性;未来的扩展不会破坏现有程序。

Token-2022 指令兼容性和新功能

Solana 指令是对链上程序的调用,它由三个字段组成:

  • 程序 ID,要调用的链上程序的公钥,
  • 程序将读取或写入的帐户列表
  • 指令数据 - 由程序定义的任意字节序列。

原始 SPL Token 程序有 25 个唯一指令。 Token-2022 支持所有这些指令,并在第 25 个指令之后添加新指令以启用新的扩展功能。

换句话说,Token-2022 中的现有 token 指令(如 MintToTransferBurn)的行为与 SPL 中的完全相同。

这是一个示例指令布局,它告诉 token 程序将 100 个 token 从 mint 账户铸造到目标 token 账户:

Token-2022 中一个 mint 账户中的数据

应用程序可以通过简单地更改其交易中的程序 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 token

在 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

此命令创建一个同时启用了 InterestBearingConfigMetadataPointer 扩展的 mint 账户。 在内部,它使用 ExtensionType::try_calculate_account_len::<Mint>(&[InterestBearingConfig, MetadataPointer])?; 分配完整的账户大小,立即写入利率参数,隐式地初始化 InterestBearingConfig 扩展,并为元数据保留一个 TLV 插槽。 现在,我们必须手动初始化元数据扩展。

try_calculate_account_len (来自 token-2022 库) 函数根据所选扩展计算所需的总空间,并且每个扩展的初始化指令在最终 mint 初始化锁定账户结构之前配置其特定参数。

如果你运行我们之前提到的 spl-token 命令来启用扩展,你将看到如下输出:

一个截图,展示了 spl-token cli 在终端中运行,以创建一个使用利率扩展和元数据扩展的 Token-2022。

在此阶段:

  • 利率扩展已完全初始化,利率为 5
  • 元数据扩展具有保留的 TLV 插槽,但尚未写入任何元数据内容。

你会注意到以下消息:“要初始化 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...

剩余50%的内容订阅专栏后可查看

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论