Solana编程模型:Solana开发入门

Solana编程模型:Solana开发入门

本文内容

Solana的去中心化计算方法根植于一个简单的原则:一切都存储在自己的内存区域中,称为账户。Solana作为一个全局键/值存储,公钥用作其对应账户的唯一标识符。账户是Solana的支柱,因为它们存储状态;它们持有所有内容,不管是程序还是代币余额。交易用于更新账户并反映状态的变化。

在本文中,我们将探讨Solana架构的复杂性。我们从集群的概述和状态的概念开始,然后讨论账户和程序作为Solana基础组件的作用。接着,我们将研究交易如何实现账户和程序之间的动态交互。

通过本文,你将全面了解Solana的编程模型。你将熟悉集群的架构、账户在数据存储中的关键作用,以及交易如何更新账户数据的过程。此外,你还将探索Solana独有的功能,如其租金系统和版本化交易。

什么是Solana集群?

img

Solana架构的核心是集群 - 一组验证者共同处理交易并维护单个分类账(ledger)。Solana有几个不同的集群,每个集群都有特定的用途:

每个集群都独立运行,完全不知道其他集群的存在。发送到错误集群的交易将被拒绝,以确保每个运行环境的完整性。

想象集群就像一个巨大的数据堆。在计算机科学中,堆指的是可以动态存储和修改数据的内存区域。然而,需要注意的是,集群并不是字面上使用堆数据结构。这个类比是一个概念工具,有助于理解集群由各种可以在需要时分配和释放的内存区域组成。将集群视为动态堆对于理解数据在网络中的管理、访问和安全至关重要。

你也可以将这个巨大的数据堆想象成一种数字仓库。在这里,数据就像货架上的箱子,每个箱子都有独特的标签和移动以及更改其内容的特定规则。这确保了一个安全、有序的系统,只有授权的移动或更改才被允许。

称为程序(program)的智能合约在Solana上被分配了自己的部分仓库,或者说堆,它们可以管理。虽然程序可以从仓库中的任何部分读取,但它们需要特定的权限才能更改它们不拥有的空间的内容。唯一普遍允许的操作是将lamports(Solana的原生加密货币)转移到仓库内的任何空间。

所有状态都存在于这个堆中,甚至包括程序(program)。每个区域都有一个拥有它并相应管理它的程序。例如,程序由BPFLoader拥有,这是一个负责加载、部署和升级链上程序的程序。我们将这些内存区域,我们数字仓库的箱子,称为账户。

什么是账户?

在Solana上,一切都是账户。将账户视为持久保存数据的容器,就像计算机上的文件一样。它们是Solana程序模型的基本构件,用于存储状态(即账户余额、所有权信息、账户是否持有程序以及租金信息)。

在Solana上有三种类型的账户:

  • 存储数据的账户
  • 存储可执行程序的账户
  • 存储原生程序的账户

根据其功能,这些类型的账户可以进一步区分为:

  • 可执行账户 - 能够运行代码的账户
  • 不可执行账户 - 用于存储数据而无法执行代码的账户(因为它们不包含任何代码!)

img

在上图中,我们有一些可执行和不可执行账户的示例。对于可执行账户,Bubblegum是一个程序账户的示例。这是Metaplex使用的程序,用于创建和管理压缩的NFT。投票程序是原生程序账户的示例。它用于创建和管理跟踪验证者投票状态和奖励的账户。我们将在什么是程序?部分介绍程序账户和原生程序账户之间的区别。目前,重要的是要知道Solana上有不同类型的可执行账户。

此外,每个不可执行账户都可以归类为数据账户。数据账户的示例包括:

  • 关联 Token 账户 - 一个包含特定代币信息、其余额和所有者信息的账户(例如,Alice拥有10个USDC)
  • 系统账户 - 由系统程序创建和拥有的账户
  • 质押账户 - 用于将代币委托给验证者以潜在获得奖励的账户

账户结构

账户的结构遵循AccountInfo结构:

pub struct AccountInfo<'a> {
    pub key: &'a Pubkey,
    pub lamports: Rc>,
    pub data: Rc>,
    pub owner: &'a Pubkey,
    pub rent_epoch: Epoch,
    pub is_signer: bool,
    pub is_writable: bool,
    pub executable: bool,
}

账户通过其地址(key)进行标识,这是一个唯一的32字节公钥。

lamports字段保存着该账户拥有的lamports数量。一个lamport等于Solana的原生代币 SOL的十亿分之一。

data 指的是由该账户存储的原始数据字节数组。它可以存储从数字资产的元数据到代币余额等任何内容,并可由程序进行修改。

owner 字段包含了此账户的所有者,由程序账户的地址表示。关于账户所有者有一些规则:

  • 只有账户的所有者才能更改其数据并提取lamports
  • 任何人都可以向账户存入lamports
  • 账户的所有者可以将所有权转移给新所有者,前提是账户的数据被重置为零

is_signer 字段是一个布尔值,指示交易是否已由涉及账户的所有者签名。换句话说,它告诉交易中涉及的程序账户,账户是否是签名者。作为签名者意味着账户持有公钥对应的私钥,并有权批准提议的交易。

is_writable 字段是一个布尔值,指示账户的数据是否可以修改。Solana允许交易将账户指定为只读,以促进并行处理。虽然运行时允许不同程序同时访问只读账户,但它使用交易处理顺序处理潜在的可写账户写入冲突。这确保只有非冲突的交易可以并行处理。

executable 字段是一个布尔值,指示账户是否可以处理指令。是的,这意味着程序存储在账户中,我们将在下一节中深入探讨这一点。首先,我们需要介绍租金的概念。

rent_epoch 字段指示此账户将在下一个epoch 时期欠租金。一个epoch是 leader 调度的插槽数。与操作系统中的传统文件不同,Solana上的账户具有以lamports表示的寿命。账户的持续存在取决于其lamport余额,这让我们引入了租金的概念。

租金

租金是在Solana上保持账户活动并确保账户保存在验证器内存中而产生的存储成本。租金收取是根据epoch进行评估的,epoch是由时间段定义的时间单位,在此期间leader调度是有效的。。以下是租金的运作方式:

  • 租金收取 - 租金每个epoch收取一次。当账户被交易引用时,也可以收取租金
  • 租金分配 - 收取的一部分租金被销毁,意味着它被永久性地从流通中移除。其余部分在每个插槽后分配给投票账户
  • 租金支付 - 如果一个账户没有足够的lamports来支付租金,那么它的数据将被移除,并且账户将在一个被称为垃圾回收的过程中被取消分配
  • 租金豁免 - 如果账户保持等于两年租金支付的最低余额,则账户可以成为租金豁免。所有新账户必须满足此租金豁免门槛,这取决于账户的大小
  • 租金检索 - 用户可以关闭一个账户以取回其剩余的lamports。这允许用户检索存储在账户中的租金

可以使用getMinimumBalanceForRentExemption RPC端点来估算特定账户大小的租金。Test Drive通过接受usize中的账户数据长度来简化此过程。Solana rent CLI子命令也可以用于估算账户成为租金豁免所需的最低SOL金额。例如,在撰写本文时,运行命令solana rent 20000将返回租金豁免最低值:0.14009088 SOL

Solana上的地址

实际上,Solana上有两种“类型”地址。Solana使用ed25519,一种使用SHA-512(SHA-2)Curve22519椭圆曲线的EdDSA签名方案来创建地址。 生成 32 字节的公钥,它们作为主要地址格式可以直接使用,因为它们没有被哈希。

为了使地址有效,它必须是ed25519曲线上的一个点。然而,并非所有地址都需要从此曲线派生。程序派生地址(PDA)是在曲线之外生成的,这意味着它们没有对应的私钥,也不能用于签名。PDAs是通过系统程序创建的,当程序需要管理账户时使用。这只是为了让你,读者,了解Solana上不同类型的地址而提出的。我们将在未来的文章中介绍PDA。

Solana上的账户与以太坊上的账户有何不同?

以太坊有两种主要的账户类型:外部拥有账户(EOA)和合约账户。EOA由私钥控制,而合约账户由其合约代码管理,并且不能自行发起交易。

img

EOA和合约账户都遵循相同的账户结构:

  • 余额 - 每个账户都有以太币计量的余额
  • Nonce - 对于EOA,这是从账户发送的交易计数。对于合约,这是账户创建的合约数量
  • 存储根 - Merkle Patricia Trie的根节点的256位哈希,这是账户存储内容的编码
  • CodeHash - 合约的以太坊虚拟机(EVM)代码的哈希。这是不可变的,意味着一旦创建,其代码就不会改变,尽管其状态可以。需要注意的是,在以太坊上升级合约存在例外情况,例如使用代理模式,但这超出了本文的范围。对于EOA,这是一个空字符串的哈希,因为EOA不包含代码

Solana采用了更统一的账户模型,其中任何账户都有可能成为程序。代码和数据的分离促进了更高效和灵活的环境。Solana程序是无状态的,与各种数据账户进行交互,而无需冗余部署。这对去中心化金融(DeFi)应用特别有利,用户可以在不同程序之间交互而无需移动资产。相比之下,以太坊的编程模型将代码和状态合并为一个实体。这使得交互更加复杂,并且由于状态更改的gas要求,可能导致更高的成本。

Solana账户用于支付租金,要求它们保持最低余额以保持活动状态。这确保未使用或资金不足的账户最终会被网络回收,从而减少状态膨胀。最近的更新使得主网上不再有任何支付租金的账户 - 账户必须是租金豁免的。相比之下,以太坊使用gas来管理资源分配。在这种模型下,合约存储会持续存在,除非明确清除。Solana的方法为状态存储提供了更可预测的成本结构,而以太坊的成本可能会有所变化,并且在网络拥堵期间可能变得禁止。

在接下来的部分中,我们将研究Solana如何将其程序逻辑与状态分离。与以太坊的编程模型相比,你将看到这种模块化方法如何促进更高效的链上操作,同时为开发人员提供透明和可预测的成本结构。

什么是程序?

程序是由BPF Loader拥有的可执行账户。它们由Solana Runtime执行,该运行时旨在处理交易和程序逻辑。

img

Solana编程模型的一个显著特点是代码和数据的分离。程序是无状态的,这意味着它们不会在内部存储任何状态。相反,它们需要操作的所有数据都存储在单独的账户中,这些账户通过交易以引用的方式传递给程序。这种设计允许将程序的单个通用部署与不同的账户进行交互。

Solana上的程序具有以下能力:

  • 拥有额外的账户
  • 从其他账户中读取或存入资金
  • 修改数据或扣除它们拥有的账户

有两种类型的程序:

  • 链上程序 - 这些是部署在Solana上的用户编写的程序。它们可以由其升级权限进行升级,升级权限通常是部署程序的账户
  • 原生程序 - 这些程序集成到Solana 核心(Core)中。它们提供验证者运行所需的基本功能。原生程序只能通过网络范围的软件更新进行升级。常见的示例包括系统程序BPF Loader程序投票程序

链上和原生程序都可以被用户和其他程序调用。主要区别在于它们的升级机制:链上程序可以由其升级权限进行升级,而原生程序只能作为集群更新的一部分进行升级。

Solana Labs 策划了一组称为Solana程序库的链上程序。该库促进了各种链上操作,包括代币借贷和权益池创建。例如,关联代币账户程序设定了将用户的钱包与其相应的代币账户链接的标准和机制。此外,SPL是动态的。诸如Token-2022之类的程序构建并扩展了代币程序提供的功能。

Solana上的程序开发通常使用Rust,并借助Anchor进行开发。Anchor是一个开发的框架,通过减少样板代码和简化序列化和反序列化来简化程序的创建。虽然Rust是首选语言,但开发人员并不受其限制 - 可以使用C、C++和任何针对LLVM的BPF后端的语言(即LLVM的组件,允许将程序编译为BPF字节码)。最近来自SolangNeon Labs的发展使开发人员能够在程序开发中使用Solidity

Solana上的程序通常在本地主机和开发网络上进行开发和测试,然后部署到测试网络或主网Beta。开发人员可以使用Solana CLI通过命令solana program deploy <程序路径>部署其程序。一旦编译成包含BPF字节码的ELF共享对象,程序将上传到指定的Solana集群。部署的程序存储在标记为可执行的账户中,账户地址作为program_id

最初,Solana上的程序是使用比程序两倍大小的账户部署的。Solana的1.16更新引入了可调整大小的账户支持,以为开发人员提供更灵活的资源分配。现在,开发人员可以使用较小大小的账户部署其程序,并在以后扩展其大小。

如上所述,程序被认为是无状态的,因为它们与之交互的任何数据都存储在作为引用传递的单独账户中。所有程序都有一个单一的入口点,其中指令处理发生,该入口点接收program_id、账户数组和指令数据作为字节数组。一旦被交易调用,程序将由Solana运行时执行。

什么是交易?

交易是链上活动的支柱。它们作为调用程序和实施状态更改的机制。Solana上的交易是一系列指令的捆绑,告诉验证者应执行哪些操作,涉及哪些账户以及它们是否具有必要的权限来执行这些操作。

img

交易由三个主要部分组成:

  • 一个要读取或写入的账户数组
  • 一个或多个指令
  • 一个或多个签名

Solana上的交易遵循交易结构。这提供了网络处理和验证操作所需的信息。它定义如下:

pub struct Transaction {
    pub signatures: Vec,
    pub message: Message,
}

signatures字段包含与序列化Message对应的一组签名。每个签名与Messageaccount_keys列表中的一个账户密钥相关联,从 fee payer 开始。 fee payer 是在处理交易时负责支付交易费用的账户。这通常是发起交易的账户。所需签名的数量等于消息的MessageHeader中定义的num_required_signatures

message本身是类型为Message的结构。它定义如下:

pub struct Message {
    pub header: MessageHeader,
    pub account_keys: Vec,
    pub recent_blockhash: Hash,
    pub instructions: Vec,
}

消息的header包含三个无符号8位整数:所需签名的数量(即num_required_signatures)、只读签名者的数量和只读非签名者的数量。

account_keys字段列出了交易中涉及的所有账户地址。请求读写访问权限的账户首先出现,然后是只读账户。

recent_blockhash是一个最近的区块哈希,包含一个32字节的SHA-256哈希。这是为了指示客户端上次观察到账本的时间,并作为最近交易的生命周期。验证者将拒绝具有旧区块哈希的交易。此外,最近区块哈希的包含有助于防止重复交易,因为任何与先前完全相同的交易都将被拒绝。如果出于任何原因,交易需要在提交到网络之前很长时间签名,可以使用持久交易nonce来代替最近的区块哈希,以确保它是唯一的交易。

instructions字段包含一个或多个CompiledInstruction结构,每个结构都指示网络验证者执行特定操作。

指令

指令是对单个Solana程序调用的指令。它是程序中执行逻辑的最小单位,也是Solana上最基本的操作单元。程序解释从指令传递的数据,并对指定的账户进行操作。Instruction结构定义如下:

pub struct Instruction {
    pub program_id: Pubkey,
    pub accounts: Vec,
    pub data: Vec,
}

program_id字段指定要执行的程序的公钥。这是将处理指令的程序的地址。由该公钥指示的程序帐户的所有者指定了负责初始化和执行程序的加载器。加载器一旦部署,就会将链上Solana字节码格式(SBF)程序标记为可执行。Solana的运行时将拒绝任何试图调用未标记为可执行的帐户的交易。

accounts字段列出了指令可能从中读取或写入的账户。这些账户必须作为AccountMeta值提供。任何可能被指令改变数据的账户必须被指定为可写,否则交易将失败。这是因为程序不能向它们不拥有或没有必要权限的账户写入。这也适用于改变账户的lamports:从程序不拥有的账户中减去lamports将导致交易失败,而向任何账户添加lamports是允许的。accounts字段还可以指定程序不会读取或写入的账户。这是为了通过运行时影响程序执行的调度,但是这些账户将被忽略。

data是一个包含8位无符号整数的通用向量,用作传递给程序的输入。该字段至关重要,因为它包含程序将执行的编码指令。

Solana对指令数据的格式是不可知的。但是,它内置了对bincodeborsh(用于哈希的二进制对象表示序列化器)的支持。序列化是将复杂数据结构转换为一系列可以传输或存储的字节的过程。数据的编码方式选择应考虑解码的开销,因为所有这些都发生在链上。通常更倾向于使用Borsh序列化,而不是bincode,因为它具有稳定的规范,JavaScript实现,并且通常更有效。

程序使用辅助函数来简化支持指令的构建。例如,系统程序提供了一个辅助函数来构建SystemInstruction::Assign指令:

pub fn assign(pubkey: &Pubkey, owner: &Pubkey) -> Instruction {
    let account_metas = vec![AccountMeta::new(*pubkey, true)];
    Instruction::new(
        system_program::id(),
        &SystemInstruction::Assign { owner: *owner },
        account_metas,
    )
}

该函数构造一个指令,当处理时,将把指定账户的所有者更改为提供的新所有者。

单个交易可以包含多个指令,这些指令按顺序依次执行并具有原子性。这意味着要么所有指令成功,要么都不成功。这也意味着指令的顺序可能至关重要。程序必须经过加固,以安全地处理任何可能的指令序列,以防止任何潜在的利用。

例如,在去初始化期间,程序可能会尝试通过将其lamport余额设置为零来去初始化一个账户。这假设Solana运行时将删除该账户。这个假设在交易之间是有效的,但在指令之间或跨程序调用(我们将在以后的文章中介绍跨程序调用)是无效的。程序应明确将账户的数据清零,以加固去初始化过程中的潜在缺陷。否则,攻击者可以发出后续指令来利用假定的删除,例如在交易完成之前重新使用该账户。

什么是版本化交易?

Solana上的交易使用IPv6 最大传输单元(MTU)标准,以保证数据在集群中的快速可靠传输。Solana的网络堆栈使用保守的MTU大小为1280字节。在留出空间用于头部之后,可用于数据包的字节数为1232字节。因此,Solana交易受到此大小的限制。

这种大小约束促进了一系列网络增强,但也限制了可以在单个交易中执行的操作的复杂性。鉴于每个账户地址占用32字节的存储空间,一个交易可以在没有任何指令的情况下存储多达35个账户。这种限制对于需要在单个交易中使用超过35个无需签名的账户的用例构成挑战。

为了解决这个问题,引入了一种新的交易格式,可以支持多个版本的交易格式。Solana运行时目前支持两个交易版本:

  • legacy - 原始交易格式
  • 0(版本0)- 包括对地址查找表(ALTs)支持的最新交易格式

版本0发布以支持地址查找表(ALT)。它们实质上在链上以表格形式存储账户地址。这些表格是单独的账户,存储账户地址并允许使用1字节的u8索引在交易中引用它们。这显著减少了交易的大小,因为每个包含的账户只需要使用1字节而不是32字节。ALT对于涉及许多账户的复杂操作特别有用,例如DeFi应用程序中常见的操作。

img

此图表改编自Solana Cookbook关于版本化交易部分

术语“版本化交易”指的是Solana支持legacy和版本0交易格式的方式。这种方法确保了组合性,同时拥抱运行时改进。

版本化交易的结构

VersionedTransaction定义如下:

pub struct VersionedTransaction {
    pub signatures: Vec,
    pub message: VersionedMessage,
}

signatures字段是交易签署者的签名列表。它们用于验证和维护交易的完整性。message是交易的实际内容。这由VersionedMessage类型封装,它是一个枚举包装器,处理 legacy 和版本0消息:

pub enum VersionedMessage {
    Legacy(Message),
    V0(Message),
}

消息版本是由序列化过程中的第一个位确定的。如果第一个位被设置,剩余的7位将用于确定从版本0开始序列化的哪个Message版本。如果第一个位未设置,则所有字节将用于编码遗留Message格式。这是因为有两个同名的Message结构,但它们分别分布在不同的模块中 - legacyv0

Message 表示交易的简化内部格式。这用于网络传输和运行时操作。它包括交易指令使用的所有账户的线性列表,详细说明账户数组结构的 MessageHeader(消息头),最近的区块哈希和消息指令的紧凑编码。这是 v0 Message 结构:

pub struct Message {
    pub header: MessageHeader,
    pub account_keys: Vec,
    pub recent_blockhash: Hash,
    pub instructions: Vec,
    pub address_table_lookups: Vec,
}

legacy 消息和 v0 消息之间的区别在于包含了 address_table_lookups 字段。

将编程模型与 Solana 的交易流程集成

Solana 的编程模型与其账户和交易系统深度集成。以下是这些概念如何联系在一起的:

  • 账户作为状态 - Solana 上的账户充当程序的状态容器。编程模型围绕修改这些容器中存储的数据以响应指令而展开
  • 指令 - 程序定义了处理交易中包含的指令的逻辑。这些指令是与账户数据交互的可执行组件
  • 序列化和处理 - 当交易被序列化时,程序的指令决定了账户状态的变化。序列化过程尊重程序的设计,无论它使用传统还是版本 0 的交易格式
  • 原子性 - Solana 的编程模型确保原子指令处理。程序必须设计成能够安全高效地处理并发交易
  • 可扩展性 - Solana 的编程模型通过地址查找表(ALT)等功能支持可扩展性。这些表减少了交易的大小,并增加了交易可以引用的账户数量

Solana 的编程模型不仅仅是编写代码,它还涉及理解代码在更广泛生态系统中的交互方式。账户对于这个模型至关重要,它们作为数据在网络上存储和修改的主要手段。交易通过告知验证者需要创建、更新或删除哪些数据,实现了链上活动。开发人员对这些方面的深入理解对于构建能够在 Solana 生态系统内优化性能和协同作用的应用程序至关重要。

结论

恭喜!在本文中,我们已经深入了解了 Solana 的系统架构的复杂性,深入探讨了集群作为数据的单体堆的概念。我们发现了这个堆是如何组织成称为账户的不同内存区域的,构成了 Solana 编程模型的支柱。账户存储了从用户代币到定义网络行为的程序的一切,所有这些都是通过交易进行修改的。

对于开发人员来说,理解 Solana 对去中心化计算的方法至关重要。理解账户、程序和交易的复杂性对于构建能够充分利用 Solana 能力的应用程序是必要的。这是关于理解一个代码与状态分离的系统。这导致了无状态程序,以前所未有的组合性和可升级性规模与账户上的数据进行交互。

对于投资者和普通用户来说,理解 Solana 的设计如何创建一个强大、灵活和高效的生态系统对于欣赏该平台的可行性以及促进创新应用程序的能力至关重要,这些应用程序只有在 Solana 上才有可能。

附加资源 / 进一步阅读


本翻译由 DeCert.me 协助支持, 在 DeCert 构建可信履历,为自己码一个未来。

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

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO