理解程序

本页旨在提供Solana程序工作原理的简要概述,并为希望深入了解特定主题的人提供额外的阅读材料。

简介

与大多数区块链不同,Solana将逻辑和数据分离为两个独立的组件。这些分别是程序账户。这意味着,程序不是在内部变量中存储数据,而是与存储在账户中的外部数据交互,并有能力修改它们。

这种架构非常适合使程序更加模块化,因为程序交互的数据不绑定到程序本身,可以扩展到新的规模维度。它还非常适合使程序更高效,因为区块链可以在不同账户上并行执行同一程序。

要与程序交互,我们需要使用该程序定义的指令。指令可以被视为程序公开的API端点。每个指令都包含一组必须满足才能执行的参数和约束。

总结:在Solana中,程序定义可用于与称为账户的外部数据存储交互的指令

请注意,从技术上讲,程序是标记为executable的特殊类型账户,其唯一目的是存储程序的编译代码。但是,为简化起见,我们将区分这些定义,并使用"账户"一词来指代非可执行账户。

在本指南的其余部分,我们将更详细地讨论账户和指令,然后解释在整个文档的图表中使用的视觉表示。

📚 额外阅读材料:

账户

在Solana中,账户用于存储数据。本质上,它们是存储在特定地址的简单字节数组。账户的地址是加密密钥对的公钥

任何有权访问该密钥对私钥的人都可以代表该账户签名,根据程序的不同,这可能赋予他们修改该账户存储数据的能力。

当创建账户时,它通常会立即被程序初始化,该程序将被标记为所有者并定义分配给字节数组的数据结构。拥有账户的程序负责提供可用于与其交互的指令。

📚 额外阅读材料:

程序派生地址 (PDA)

存在另一种类型的账户,称为程序派生账户。它的地址不是加密密钥对的公钥,而是从拥有该账户的程序的公钥算法派生的。该地址称为程序派生地址或简称PDA

由于地址总是从程序的公钥派生,因此其他程序无法通过算法派生相同的地址。此外,可以向算法提供额外的种子以向地址添加更多上下文。

这有多种用例,例如允许程序对跨程序调用进行签名或在确定性派生的地址内创建账户。

请注意,按设计,程序派生地址永远不会与加密生成的公钥冲突。所有加密公钥都是所谓的椭圆曲线的一部分。当生成PDA时,如果算法生成了落在该曲线上的密钥,则会向地址添加bump并逐一递增,直到生成的地址不再落在曲线上。

📚 额外阅读材料:

账户数据

无论我们处理的是常规账户还是程序派生账户,账户都将数据存储为序列化的字节数组。因此,程序有责任为其管理的每个账户定义数据结构,并提供区分这些账户的方法,以便我们知道应该应用哪个数据结构。

鉴别器

鉴别器用于区分程序内不同类型的账户。它们可以通过多种方式实现,但最常见的三种如下:

  • 使用所有账户的第一个字节作为共享枚举。通过在所有账户前添加共享枚举,我们可以使用序列化数据的第一个字节来识别账户。这是一种简单高效的实现鉴别器的方式。Metaplex维护的大多数程序都使用这种方法。
  • 使用所有账户的第一个字节作为确定性哈希。这与前一点非常相似,但使用哈希代替枚举。使用Anchor框架创建的程序将隐式使用此方法,因为Anchor将根据账户的名称自动生成该哈希。
  • 无鉴别器,使用账户大小。如果程序管理的所有账户具有不同的大小,则可以查看其字节数组的长度来确定我们正在处理哪个账户。这是一种性能良好的方法,因为它不需要向数据添加额外的字节,但它限制了程序对账户的灵活性。Solana的SPL Token程序使用这种方法,因为它只维护两个不同的固定大小账户。

字段类型、大小和偏移

每个账户使用不同类型的字段定义自己的数据结构。这些类型影响存储字段所需的字节数。例如,i8是8位整数需要1字节,而i64是64位整数需要8字节。

在区块链上,账户只是字节数组,因此了解每个字段的大小以及它们从该数组的哪个位置开始(即其偏移量)非常重要。这在使用memcmp过滤器从指定程序获取多个账户时很有用。

请注意,并非所有字段都有固定大小。例如,Vec<i8>是8位整数的向量,可能包含零个、一个或多个项目。因此,基于位于第一个可变大小字段之后的字段过滤账户变得更加复杂。

可选字段

字段也可以定义为可选,这意味着存在该字段将为空的情况。

此字段使用一个额外的字节作为前缀来指示字段是否为空。

具体来说,它将被赋值为None,程序在使用该字段时将相应地表现。

指示性字段

这不是数据结构中明确定义的内容,但在文档中,我们将某些字段标记为指示性

指示性字段意味着该字段提供的信息不会被程序本身使用。相反,它向第三方_指示_一条信息。程序仍将强制数据完整性,但不会在内部使用该信息。

以Metadata账户为例。

Creators数组中每个创作者的Share属性是指示性的。Token Metadata程序将确保所有创作者的Share值加起来为100%,但它不会对该信息做任何操作。相反,它期望NFT市场在分配版税时使用此信息。

另一方面,Is Mutable属性不是指示性的,因为Token Metadata程序在内部使用该信息来防止对不可变Metadata账户的更新。

指令

我们可以使用程序提供的指令与程序交互。多个指令可以打包成一个交易发送到区块链。每个交易都是原子的,意味着如果其中任何指令失败,整个交易将回滚。

与账户类似,指令在发送到网络之前需要序列化为字节数组。序列化的数据必须包含以下信息,以便程序执行它。

  • 鉴别器:与账户类似,指令通常带有鉴别器前缀,以便程序可以识别正在执行的指令。
  • 账户:受此指令影响的账户地址数组。这是因为账户正在被读取、修改或两者兼有。该数组的顺序很重要,因为程序根据其位置识别所提供账户的类型。
  • 参数:指令所需的数据字段数组。由于指令可以直接从账户获取大部分信息,因此该数组为空并不罕见。这些参数与账户的字段相当,因此可以具有与上述相同的属性,如"可选"和"指示性"。
  • 签名者:所提供账户子集的签名数组。只有需要对指令进行签名的账户才需要这些签名。我们将在下一节中更详细地讨论这一点。

📚 额外阅读材料:

签名者和/或可写账户

程序可能要求在指令中提供的账户是签名者和/或可写的。

  • 签名者:签名者账户需要对交易进行签名才能使指令成功。通过附加签名,用户可以证明他们是账户的所有者。
  • 可写:可写账户将被指令修改。此信息对于区块链了解哪些交易可以并行执行以及哪些不可以很重要。

因此,有了这两个布尔值,我们有以下四种可能的情况:

  • 非签名者且非可写:此账户仅用于读取数据。它不能被修改,我们也无法对其所有权做出假设。
  • 签名者且非可写:此账户也不能被修改,但我们知道发送交易的用户拥有其私钥。这允许程序允许或拒绝对某些操作的访问。
  • 签名者且可写:此账户对交易进行签名_并且_可能会被指令修改。这种组合相当常见,因为程序通常在修改账户之前要求证明谁是账户的所有者。否则,任何人都可以修改任何他们不拥有的账户。
  • 非签名者且可写:此账户可能会被修改,但我们无法对其所有权做出假设。通常,这意味着程序使用其他签名者账户来证明它可以修改该账户。PDA账户也是如此,因为它们由程序拥有,因此,我们需要跟踪程序允许修改它们的权限。还要注意,某些操作,如给账户充值lamports,不需要账户对交易进行签名。

跨程序调用 (CPI)

跨程序调用允许程序在其自己的指令中执行嵌套指令。它可以使用来自自己程序和/或其他程序的指令。

📚 额外阅读材料: