Solana 编程模型:Solana 开发简介

这篇文章是关于什么的?

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

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

读完本文后,您将彻底了解 Solana 的编程模型。您将熟悉集群的架构、账户在数据存储中的关键作用以及交易更新账户数据的过程。此外,您还将探索 Solana 独有的功能,例如租赁系统和版本化交易。

什么是 Solana 集群?

Solana 架构的核心是集群——一组协同工作的验证器来处理交易并维护单个分类账。 Solana 有几个不同的集群,每个集群都有特定的用途:

  • Localhost:在默认端口 8899 处找到的本地开发集群。Solana命令行界面 (CLI)附带内置测试验证器,可以根据单个开发人员的需求进行自定义,无需任何空投或遇到速率限制
  • Devnet:用于在 Solana 上进行测试和实验的无后果沙箱环境
  • 测试网:Solana 核心贡献者的测试场,用于在新更新和功能到达主网之前对其进行试用。它还用作想要运行性能测试的开发人员的测试环境
  • 主网测试版:实时、无需许可的集群,现实世界中的交易发生在其中。这是“真正的”Solana,用户、开发者、代币持有者和验证者每天都在其中互动

每个集群独立运行,彼此完全不知情。发送到错误集群的事务将被拒绝,以确保每个操作环境的完整性。

将集群想象成一个整体的数据堆。在计算机科学中,堆是指可以动态存储和修改数据的内存区域。然而,值得注意的是,集群实际上并不使用堆数据结构。这个类比可以作为一个概念工具,帮助理解集群由各种内存区域组成,这些区域可以在需要时分配和释放。将集群理解为动态堆是理解如何在网络中管理、访问和保护数据的关键。

您还可以将这个庞大的数据堆视为某种数字仓库。在这里,数据就像架子上的盒子,每个盒子都有独特的标签和移动它们和更改其内容的特定规则。这确保了一个安全、有序的系统,仅允许经过授权的移动或更改。

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

所有状态都存在于这个堆中,甚至是程序。每个地区都有一个拥有该地区并进行相应管理的计划。例如,程序由BPFLoader拥有,该程序负责加载、部署和升级链上程序。我们将这些存储区域(我们的数字仓库的盒子)称为帐户。

什么是账户?

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

Solana 上的账户分为三种类型:

  • 存储数据的帐户
  • 存储可执行程序的帐户
  • 存储本机程序的帐户

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

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

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

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

账户结构

帐户根据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 是 SOL 的十亿分之一,SOL是 Solana 的原生代币

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

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

  • 只有帐户所有者才能更改其数据并撤回许可证
  • 任何人都可以将 Lamps 存入帐户
  • 帐户所有者可以将所有权转让给新所有者,前提是该帐户的数据重置为零

is_signer字段是一个布尔值,指示相关帐户的所有者是否已签署交易。换句话说,它告诉参与交易的程序该帐户是否是签名者。成为签名者意味着该账户持有公钥对应的私钥,并有权批准拟议的交易。

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

可执行字段是一个布尔值指示帐户是否可以处理指令。是的,这确实意味着程序存储在帐户中,我们将在下一节中深入讨论。首先,我们需要了解租金的概念。

rent_epoch字段指示该帐户将欠租金的下一个纪元。 epoch领导调度有效的时隙数。与操作系统中的传统文件不同,Solana 上的帐户的生命周期以 lamport 数量表示。帐户的持续存在取决于其 lamport 余额,这一想法给我们带来了租金的概念。

租金是为了使帐户在 Solana 上保持活动状态并确保帐户保存在验证器内存中而产生的存储成本。租金收取是根据时期来评估的,时期是由领导者时间表有效的时间段定义的时间单位。租金的运作方式如下:

  • 租金收集- 一个时期收集一次租金。也可以在交易引用账户时收集
  • 租金分配- 一些收取的租金被烧毁,这意味着它被永久地从流通中移除。剩余的在每个时段后​​分配到投票账户
  • 租金支付- 如果帐户没有足够的 lamport 来支付租金,则其数据将被删除,并且该帐户将在称为垃圾收集的过程中取消分配
  • 免租- 如果账户保持相当于两年租金的最低余额,则可以免租。所有新帐户都必须满足此租金豁免阈值,这取决于帐户的规模
  • 租金回收- 用户可以关闭帐户以收回其剩余的许可证。这允许用户检索存储在帐户中的租金

可以使用特定账户规模的getMinimumBalanceForRentExemption RPC 端点来估算租金。测试驱动通过在usize中接受帐户的数据长度来简化此操作。Solanarent CLI 子命令还可用于估计帐户免租金的最低 SOL 金额。例如,在撰写本文时,运行命令solanarent 20000将返回租金豁免最小值:0.14009088SOL

Solana 上的地址

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

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

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

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

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

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

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

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

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

什么是程序?

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

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

Solana 上的程序有能力:

  • 拥有额外账户
  • 从其他帐户读取或记入其他帐户
  • 修改数据或借记他们拥有的帐户

有两种类型的程序:

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

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

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

Solana 上的程序开发通常是在Anchor的帮助下在Rust中进行的,Anchor 是一个固执己见的框架,通过减少样板文件并简化序列化和反序列化来简化程序的创建。虽然 Rust 是首选,但开发人员并不限于它 - C、C++ 以及任何针对 LLVM 的 BPF 后端的语言(即允许将程序编译为BPF 字节码的LLVM组件)都可以使用。SolangNeon Labs的最新进展允许开发人员在程序开发中使用Solidity 。

程序通常在部署到 Testnet 或 Mainnet Beta 之前针对 Localhost 和 Devnet 进行开发和测试。开发人员可以通过 Solana CLI 使用命令solana program deploy <程序路径> 来部署他们的程序。该程序一旦编译成包含 BPF 字节码的ELF 共享对象,就会上传到指定的 Solana 集群。已部署的程序位于标记为executable的帐户中,帐户的地址用作program_id

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

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

什么是交易?

交易是链上活动的支柱。它们充当调用程序和执行状态更改的机制。 Solana 上的交易是一组指令,告诉验证者应该执行哪些操作、在哪些帐户上执行以及他们是否拥有执行此操作所需的权限。

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

  • 可供读取或写入的帐户数组
  • 一条或多条指令
  • 一个或多个签名

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


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

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

消息本身是Message类型的结构体。它定义为:


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

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

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

centre_blockhash是最近的块哈希,包含 32 字节 SHA-256 哈希。这需要指示客户端最后一次观察分类账的时间,并充当最近交易的生命周期。验证器将拒绝使用旧区块哈希的交易。此外,包含最近的区块哈希有助于防止重复交易,因为任何与先前交易完全相同的交易都会被拒绝。如果出于某种原因,需要在交易提交到网络之前很久对其进行签名,则可以使用持久交易随机数来代替最近的区块哈希,以确保它是唯一的交易。

指令字段包含一个或多个CompiledInstruction结构,每个结构都规定网络验证器要采取的特定操作。

指示

指令是用于 Solana 程序的单次调用的指令。它是程序中最小的执行逻辑单元,也是Solana上最基本的操作单元。程序解释从指令传递的数据并对指定的帐户进行操作。指令结构体定义为:


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

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

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

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 的网络堆栈使用保守的 1280 字节 MTU 大小。为报头留出空间后,1232 字节可用于数据包数据。因此,Solana 交易仅限于此规模。

这种大小限制促进了一系列网络增强,但也限制了单个事务中可以执行的操作的复杂性。鉴于每个账户地址占用32字节存储,一笔交易最多可以存储35个账户,无需任何指令。这种限制对于在单笔交易中需要超过 35 个免签名帐户的用例提出了挑战。

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

  • 遗留- 原始交易格式
  • 0(版本 0)- 最新的事务格式,包括对地址查找表的支持

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

该图改编自 Solana Cookbook 的版本化事务部分

术语“版本化事务”是指 Solana 支持旧版和版本 0 事务格式的方式。这种方法确保了可组合性,同时支持运行时增强。

版本化事务的结构

VersionedTransaction定义为:


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

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


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

消息版本由序列化过程中的第一位确定。如果设置了第一位,则其余 7 位用于确定从版本0开始序列化哪个消息版本。如果第一位未设置,则所有字节都将用于对旧消息格式进行编码。这是因为有两个同名的 Message 结构,但是它们被分成不同的模块 - Legacyv0

消息代表事务的压缩内部格式这用于运行时的网络传输和操作。它包含交易指令使用的所有帐户的线性列表、详细说明帐户数组结构的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,
}

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

将编程模型与 Solana 的事务流程集成

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

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

Solana 的编程模型不仅仅是编写代码,而是了解代码如何在更广泛的生态系统中交互。帐户对于该模型至关重要,是在网络上存储和修改数据的主要手段。交易通过告诉验证者需要创建、更新或删除哪些数据来启用链上活动。彻底了解这些方面对于开发人员构建针对 Solana 生态系统内的性能和协同作用进行优化的应用程序至关重要。

结论

恭喜!在本文中,我们探讨了 Solana 系统架构的复杂性,深入研究了集群作为整体数据堆的概念。我们发现了如何将堆组织成不同的内存区域(称为帐户),从而形成 Solana 编程模型的支柱。帐户存储从用户令牌到定义网络行为的程序的所有内容,所有这些都通过交易进行修改。

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

对于投资者和临时用户而言,了解 Solana 的设计如何创建强大、灵活且高效的生态系统对于了解该平台的可行性及其培育只有在 Solana 上才能实现的创新应用程序的能力至关重要。

💡
原文链接:https://www.helius.dev/blog

本文由SlerfTools翻译,转载请注明出处。

SlerfTools专为Solana设计的工具箱,致力于简化区块链操作,提供无编程全可视化界面,使发币管理流动性无代码创建Dapp等复杂过程变得安全简单。