BCSkill (Block chain skill )
区块链中文技术社区

只讨论区块链底层技术
遵守一切相关法律政策!

Solana 核心概念-程序

在 Solana 生态系统中,“智能合约”被称为程序。 每 个程序都是一个链上账户,存储可执行逻 辑,组织成特定的函数,称 为指令(instructions) 。

关键点

  • 程序是包含可执行代码的链上账户。 此代码组织成不同函数,称为指令。
  • 程序是无状态的,但可以包含创建新账户的指令,用于存储和管理程序状态。
  • 程序可以由升级权限更新。 当升级权限设置为 null 时,程序变为不可变。
  • 可验证的构建使用户能够验证链上程序是否与公开的源代码相匹配。

编写 Solana 程序

Solana 程序主要用 Rust 编程语言编写,开发有两 种常见方法:

  • Anchor:专为 Solana 程序开发设计的框架。它提供了一种更快、更简单的编写程序的方式,使用 Rust 宏来显著减少样板代码。对于初学者,建议从 Anchor 框架开始。
  • Native Rust:这种方法涉 及在不使用任何框架的情况下用 Rust 编写 Solana 程序。 它提供了更多的灵活性,但 也增加了复杂性。

更新 Solana 程序

链上程序可以由指定为“升级权限”的账 户直接修改 ,通常是最初部署程序的账户。
如果升级权限被 撤销并设置为None,程序将变为不可变,无法再更新。

可验证的程序

确保链上代码的完整性和可验证性至关重要。 可验证的构建确保部署在链上的可执行代码 可以由任何第三方独立验证,以匹配其公开的源代码。 此过程增强了透明度和信任,使得 能够检测源代码与部署程序之间的差异。

Solana 开发者社区推出了支持可验证构建的工具,使开发者和用户能够验证链上程序是否准确反映其公开共享的源代码。

  • 搜索已验证的程序:要快速检查已验证的程序,用户可以在 SolanaFM 浏览器上搜索程序地址并导航到“Verification”选项 卡。 在这里查 看一个已验证程序的示例。
  • 验证工具: Ellipsis Labs 的Solana 可验证构建 CLI 使用户能够根据已发布的源代码独立验证链上程序。
  • Anchor 中对可验证构建的支持:Anchor 提供了对可验证构建的内置支持。 详情请 参阅 Anchor 文档

伯克利包过滤器 (BPF)

Solana 利用 LLVM 编译器基础设施将程序编译 成可执行和链接格式 (ELF) 文件。 这些文件包括一个修改版 的伯克利包过滤器 (eBPF) 字节码,用于Solana 程序,称为“Solana 字节码格式” (sBPF)。

使用 LLVM 使 Solana 能够潜在支持任何可以编译到 LLVM 的 BPF 后端的编程语言。 这显 著增强了 Solana 作为开发平台的灵活性。

Solana 核心概念-费用

Solana 区块链有几种不同类型的费用和成本,这些费用和成本是使用无许可网络所需的。 它们可以分为几种特定类型:

  • 交易费用 - 验证者处理交易/指令的费用
  • 优先费用 - 提高交易处理顺序的可选费用
  • 租金 - 保持链上数据存储的保留余额

交易费用

在 Solana 区块链上的链上程序中处理逻辑(指令)所支付的小额费用称为“交易费用”。
当每个交易 (包含一个或多 个指令 )通过网络发送时,它会由当前的 验证者领导者处理。一旦确认为全局状态交易,这个 交易费用 将支付给网络,以帮助支 持 Solana 区块链的经济设计。

交易费用不同于账户数据存储押金费租金 。虽然交易费用是支付给 Solana 网 络处理指令的费用,但租金押金是保存在账户中以在区块链上存储其数据并可回收的费 用。 如上所述,每笔交易费用的固定比例被 燃烧(销毁)。这是为了巩固 SOL 的经 济价值,从而维持网络的安全性。与完全销毁交易费用的方案不同,领导者仍然有动力在 其时隙(创建区块的机会)中包含尽可能多的交易。

目前,Solana 的基本交易费用设定为每个签名 5k lamports 的固定值。 在这个基本费用 之上,可以添加任何额外的优先费用 。

为什么要支付交易费用?

交易费用在 Solana 的经济设计中提供了许多好处,主要包 括:

  • 为验证者网络提供补偿,以支付处理交易所需的 CPU/GPU 计算资源
  • 通过引入交易的实际成本来减少网络垃圾邮件
  • 通过每笔交易的协议捕获最低费用金额,为网络提供长期经济稳定性

基本经济设计

许多区块链网络(包括比特币和以太坊)依赖于通胀性 基于协议的奖励 来在短期内保护 网络。从长期来看,这些网络将越来越依赖 交易费用 来维持安全性。
在 Solana 上也是如此。具体来说: 具体而言:

  • 每笔交易费用的固定比例(最初为 50%)被 燃烧(销毁),其余部分归当前处理交易 的领导者所有。
  • 预定的全球通胀率 为奖励提 供了一个来源,分配给 Solana 验证者 。

费用收取

交易需要至少有一个账户签署交易并且是可写的。 这些 可写签名账户 首先在账户列表 中序列化,并且第一个总是用作“费用支付者”。

在处理任何交易指令之前,费用支付者账户 的余额将被扣除以 支付交易费用。 如果费用支付者余额不足以支付交易费用,交易处理将停止并导致交易失 败。

如果余额足够,费用将被扣除,交易的指令将开始执行。 如果任何指令导致错误,交易处 理将停止,并最终在 Solana 账本中记录为失败交易。 对于这些失败的交易,费用仍然由 运行时收取。

如果任何指令返回错误或违反运行时限制,所有账户更改除了交易费用扣除将被回 滚。 这是因为验证者网络已经花费了计算资源来收集交易并开始初步处理。

和以太坊基本一致

费用分配

交易费用 被部分销毁 ,剩余费用由生成包含相应交易的区块的验证者收取。 具体来 说,50%被销毁 ,50%分配给 生成区块的验证者。

为什么要销毁部分费用?

如上所述,每笔交易费用的固定比例被 燃烧(销毁)。 这是为了巩固 SOL 的经济价 值,从而维持网络的安全性。 与完全销毁交易费用的方案不同,领导者仍然有动力在其时 隙(创建区块的机会)中包含尽可能多的交易。
销毁的费用还可以帮助防止恶意验证者审查交易,通过 在分叉考虑实施。

攻击示例

在具有恶意或审查领导者 的历史证明(PoH) 分叉的情况下

  • 由于审查导致的费用损失,我们预计总销毁费用将 少于 可比的诚实分叉
  • 如果审查领导者要补偿这些丢失的协议费用,他们将不得不自己替换其分叉上的销毁费用
  • 从而可能减少最初的审查动机

计算交易费用

给定交易的完整费用基于两个主要部分计算

  • 每个签名的静态设定基本费用,以及
  • 交易期间使用的计算资源,以“计算单元”为 单位衡量

由于每个交易可能需要不同数量的计算资源,每个交易作为 计算预算 的一部分被分配了 每个交易的最大 计算单元 数量。

计算预算

在一个交易中链上执行的所有操作都需要消耗不同数量的计算资源(计算成本),这些资源 由验证者在处理时消耗。消耗这些资源的最小单位称为 "计算单元"。 为了防止滥用计算 资源,每个交易被分配一个“计算预算”。该预算指定了有 关计算单元的详细信息,包括:

  • 与交易可能执行的不同类型操作相关的计算成本(每个操作消耗的计算单元),
  • 交易可以消耗的最大计算单元数量(计算单元限制),
  • 以及交易必须遵守的操作界限(如账户数据大小限制)

当交易消耗其整个计算预算(计算预算耗尽)或超过某个界限(如尝试超 过最大调用堆栈深度或最大加载账户数 据大小限制)时,运行时将停止交易处理并返回错误。导致交易失败且没有状态更改(除了 交易费用被收取 )。 导致交易失败且没有状态更改(除了交易费用 被收取 )。

账户数据大小限制

一个交易可以通过包含一个 SetLoadedAccountsDataSizeLimit 指令来指定它允许加载的 账户数据的最大字节数(不超过运行时的绝对最大值)。 如果没有提供 SetLoadedAccountsDataSizeLimit,交易将默认使用运行时的 MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES值。

可以使用 ComputeBudgetInstruction::set_loaded_accounts_data_size_limit 函数来 创建这个指令:

let instruction = ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(100_000);

计算单元

在一个交易中链上执行的所有操作都需要消耗不同数量的计算资源(计算成本),这些资源 由验证者在处理时消耗。消耗这些资源的最小单位称为 "计算单元"。 消耗这些资源的最 小单位称为 "计算单元"。

当一个交易被处理时,每个在链上执行的指令都会逐步消耗计算单元(消耗预算)。 由于 每个指令执行的逻辑不同(写入账户、cpi、执行系统调用等),每个指令可能会消 耗不同数量的 计算单元。

每个交易都有一个 计算单元限制,可以是运行时设置的默认限 制,也可以是显式请求的更高限制。一旦交易超过其计算单元限制,其处理将停止,导致交 易失败。 一旦交易超过其计算单元限制,其处理将停止,导致交易失败。

以下是一些常见的会产生计算成本的操作:

  • 执行指令
  • 在程序之间传递数据
  • 执行系统调用
  • using sysvars
  • 使用 msg! 宏记录日志
  • 记录公钥
  • 创建程序地址(PDA)
  • 跨程序调用(CPI)
  • 加密操作

对于 跨程序调用,被调用的指令继承其父级的计算预算和限制。 如果一个被调用的指令消耗了交易的剩余预算,或超过了限制,整个调用链和顶层交易处 理将停止。

对于 跨程序调用,被调用的指令继承其父级的计算预算和限制。 如果一个被调用的指令消耗了交易的剩余预算,或超过了限制,整个调用链和顶层交易处 理将停止。

你可以在 Solana 运行时的 ComputeBudget 中找到更多关于所有消耗计算单元的操作的详细信息。

计算单元限制

每个交易都有一个最大计算单元数(CU)称为 "计算单元限制"。 每个交易,Solana 运 行时有一个绝对最大计算单元限制为 140 万 CU 并设置了一个默认请求的最大限制为 每个指令 20 万 CU。

一个交易可以通过包含一个 SetComputeUnitLimit 指令来请求一个更具体和优化的计算 单元限制。 可以是更高或更低的限制。 但它永远不能请求超过每个交易的绝对最大限制。

虽然交易的默认计算单元限制在大多数情况下适用于简单交易,但它们通常不够优化(对于 运行时和用户)。对于更复杂的交易,如调用执行多个 CPI 的程序,你可能需要为交易请 求更高的计算单元限制。 对于更复杂的交易,如调用执行多个 CPI 的程序,你可能需要为 交易请求更高的计算单元限制。

为你的交易请求最佳计算单元限制对于帮助你支付更少的交易费用和更好地在网络上调度你 的交易至关重要。 钱包、dApps 和其他服务应确保其计算单元请求是最佳的,以提供尽可 能好的用户体验。

计算单元价格

当一个交易希望支付更高的费用以提升其处理优先级时,它可以设置一个 "计算单元价 格"。 这个价格与 计算单元限制 结合使用,将用于确定交易的 优先级费用。

默认情况下,没 有设置计算单元价格因 此没有额外的优先级费用。

优先级费用

作为计算预算的一部分,运行时支持交易支付一个可选费用,称 为 "优先级费用"。支付这个额外费用有助于提升交易在处理时的优先级,从而实现更快 的执行时间。

如何计算优先级费用

一个交易的优先级费用通过将其计算单元限制乘以计算单元价格(以 微 lamports 为单位)来计算。这些值可以通过包含以下计算预算指令在每个交易中设置一 次:这些值可以通过包含以下计算预算指令在每个交易中设置一次:

  • SetComputeUnitLimit - 设置交易可以消耗的最大计算单元数
  • SetComputeUnitPrice - 设置交易愿意支付的额外费用以提升其优先级

如果没有提供 SetComputeUnitLimit 指令,将使用 默认计算单元限制。
如果没有提供 SetComputeUnitPrice 指令,交易将默认没有额外的提升费用和最低优先 级(即没有优先级费用)。

如何设置优先级费用

一个交易的优先级费用通过包含一个 SetComputeUnitPrice 指令来设置,并可选地包含 一个 SetComputeUnitLimit 指令。 运行时将使用这些值来计算优先级费用,这将用于在 区块内优先处理给定的交易。

你可以通过其 Rust 或 @solana/web3.js 函数来创建这些指令。 然后可以将每个指令包含 在交易中并像正常一样发送到集群。 另请参阅下面 的最佳实践。

与 Solana 交易中的其他指令不同,计算预算指令不需要任何账户。包含多个相同类型 指令的交易将失败。 包含多个相同类型指令的交易将失败。

Rust

Rust 的 solana-sdk crate 包含在 ComputeBudgetInstruction 中的函数,用于设置 compute unit limit 和 compute unit price 的指令

let instruction = ComputeBudgetInstruction::set_compute_unit_limit(300_000);
let instruction = ComputeBudgetInstruction::set_compute_unit_price(1);

Javascript

The @solana/web3.js library includes functions within the ComputeBudgetProgram class to craft instructions for setting the compute unit limit and compute unit price:

const instruction = ComputeBudgetProgram.setComputeUnitLimit({
  units: 300_000,
});
const instruction = ComputeBudgetProgram.setComputeUnitPrice({
  microLamports: 1,
});

优先费用最佳实践

以下是关于优先费用最佳实践的一般信息。 你还可以在本指南中找到更多关 于如何请求最佳计算的 详细信息,包括如何模拟交易以确定其大致计算使用量。

请求最少的计算单元

交易应请求执行所需的最少计算单元以最小化费用。 还要注意,当请求的计算单元数量超 过实际消耗的计算单元数量时,费用不会调整。

获取最近的优先费用

在将交易发送到集群之前,你可以使用 getRecentPrioritizationFees RPC 方法获取节点处理的最近区块中支付的优先费用列表。
然后你可以使用这些数据来估算适当的优先费用,以便 (a) 更好地确保交易被集群处理, 并且 (b) 最小化支付的费用。

租金

存入每个 Solana 账户 以保持其关联数据在链上可用的费用 称为“租金”。 此费用在每个账户的正常 lamport 余额中扣留,并在账户关闭时可回收。

租金不同于交易费用 。 租金是“支付”(在账户中扣留)以保持 数据存储在 Solana 区块链上,并且可以回收。 而交易费用是支付给网络上处 理指令 的费用。

所有账户都需要保持足够高的 lamport 余额(相对于其分配的空间)以成 为租金豁免并留在 Solana 区块链上。任何试图将账户余额减少到其相应 的租金豁免最低余额以下的交易将失败(除非余额减少到正好为零)。 Solana 上的新账户 (和程序)必须初始化足够的 lamports 以成为 租金豁免。情况并非总是如此。以 前,运行时会定期自动从低于其 租金豁免的最低余额 的每个账户中收取费用。最终将这 些账户减少到零余额并从全局状态中垃圾回收它们(除非手动补充)。

当账户的所有者不再希望将此数据保留在链上并在全局状态中可用时,所有者可以关闭账户 并回收租金存款。

这是通过将账户的全部 lamport 余额提取(转移)到另一个账户(即你的钱包)来完成 的。 通过将账户余额减少到正好为 0,运行时将在“ 垃圾回收 ”过程中从网络中删除账户及其关联数据。

租金费率

Solana 的租金费率在整个网络范围内设置,主要基于运行时设置的“ 每字节每年 lamports”。 目前,租金费率是一个静态金额,并存储 在Rent sysvar 中。

此租金费率用于计算在账户内扣留的租金确切金额,以分配给账户的空间(即可以存储在账 户中的数据量)。账户分配的空间越多,扣留的租金存款就越高。 账户分配的空间越多, 扣留的租金存款就越高。

租金豁免

账户必须维持高于链上存储其各自数据所需的最低金额的 Lamport 余额。这被称为“免租”,该余额被称为“免租最低余额”。
Solana 上的新账户(和程序)必须初始化足够的 lamports 以成为 租金豁免。情 况并非总是如此。以前,运行时会定期自动从低于其 租金豁免的最低余额 的每个账户 中收取费用。 最终将这些账户减少到零余额并从全局状态中垃圾回收它们(除非手动补 充)。

在创建新账户的过程中,你必须确保存入足够的 lamports 以超过此最低余额。 任何低于 此最低阈值的金额将导致交易失败。

每次账户余额减少时,运行时都会检查账户是否仍高于此租金豁免的最低余额。由于审查导 致的费用损失,我们预计总销毁费用将少于可比的诚实分叉

账户成为租金豁免的具体最低余额取决于区块链的当前租金费率和账户希望 分配的存储空间量(账户大小)。 因此,建议使 用getMinimumBalanceForRentExemption RPC 端点来计算给定账户大小的具体余额。

所需的租金存款金额也可以通过 solana rent CLI 子命令估 算:

solana rent 15000

# output
Rent per byte-year: 0.00000348 SOL
Rent per epoch: 0.000288276 SOL
Rent-exempt minimum: 0.10529088 SOL

垃圾回收

未保持大于零的 lamport 余额的账户将在称为垃圾回收的过程中从网络中删除。 此过程有 助于减少网络范围内不再使用/维护的数据存储。

在交易成功将账户余额减少到正好为 0 后,垃圾回收会由运行时自动进行。 任何试图将 账户余额减少到其租金豁免最低余额以下(不正好为零)的交易将失败。

重要的是要注意垃圾回收在交易执行完成之后发生。 如果有一个指令通过将账户余额 减少到零来“关闭”账户,则可以在同一交易中通过后续指令“重新打开”账户。 如果账户状 态在“关闭”指令中未被清除,则后续的“重新打开”指令将具有相同的账户状态。 这是一个 安全问题,因此了解垃圾回收生效的确切时间非常重要。

即使账户已从网络中删除(通过垃圾回收),它仍可能有与其地址相关的交易(无论是过去 的历史还是未来的交易)。 即使 Solana 区块浏览器可能显示“找不到账户”类型的消息, 你仍然可以查看与该账户相关的交易历史。

你可以阅读验证 器已实现的提案了 解更多关于垃圾回收的信息。

Solana 核心概念-交易和指令

交易和指令

在 Solana上,我们发送 交易与网络交互。 交 易包括一个或多个指令, 每个交易代表一个待 处理的特定操作。 指令的执行逻辑是存储在部署到 Solana 网络的 programs 上。每个程序存储自己的一组指令。
以下是关于交易执行方式的关键细节:

  • 执行顺序:如果一个交易包括多个指令,这些指令将按照它们添加到交易中的顺序进行处 理。
  • 原子性:交易是原子的,意味着它要么完全完成并且所有指令都成功处理,要么完全失 败。 如果交易中的任何指令失败,则不会执行任何指令。

为简单起见,可以将交易视为请求处理一个或多个指令。

你可以将交易想象成一个信封,其中每个指令是您填写并放入信封中的文件。 然后我们发 出信封来处理文档,就像在网络上发送一个交易来处理我们的指令一样。

关键要点

  • Solana 交易由与网络上各种程序进行交互的指令组成,其中每个指令代表一个特定操 作。
  • 每个指令指定执行指令的程序、指令所需的账户以及指令执行所需的数据。
  • 交易中的指令按照它们列出的顺序进行处理。
  • 交易是原子的,意味着要么所有指令都成功处理,要么整个交易失败。
  • 交易的最大大小为1232字节。

基本示例

以下是代表从发送方向接收方转移 SOL 的单个指令的交易的图示。
Solana 上的个人“钱包”是由系统程序 拥有的账 户。 作为 Solana 账户模型 的一部分,只有拥有帐户的程序才允 许修改帐户上的数据。

因此,从“钱包”账户转移 SOL 需要发送一个交易来调用 System Program 上的转移指令。

发送者账户必须包含在交易上作为签名者(is_signner),以批准扣除他们的 lamport 余 额。 发送者和接收方的账户必须是可变的(is_wrable),因为指令修改了两个账户的 lamport 余额。

交易一旦发送,系统程序将被调用来处理传输的指令。 然后,系统程序相应更新的发送者 和接受者账户的 lamport 余额。

简单 SOL 转移

这是一个使用 SystemProgram.transfer 方法构建SOL转移指令的 Solana Playground示例

// 定义转账金额
const transferAmount = 0.01; // 0.01 SOL

// 创建一个转账指令,从 wallet_1 到 wallet_2
const transferInstruction = SystemProgram.transfer({
  fromPubkey: sender.publicKey,
  toPubkey: receiver.publicKey,
  lamports: transferAmount * LAMPORTS_PER_SOL, // Convert transferAmount to lamports
});

//  添加转账指令到新交易
const transaction = new Transaction().add(transferInstruction);

运行脚本并检查记录到控制台的交易详细信息。在下面的部分中,我们将详细介绍发生的情 况。 在下面的部分,我们过一遍运行时发生什么的细节。

  client.ts:
$     sender prebalance: 14.32573752
    receiver prebalance: 0


    sender postbalance: 14.31573252
    receiver postbalance: 0.01


    Transaction Signature: https://explorer.solana.com/tx/2qRf4gscQamiiAUmXqoqqU1u1BVnGRBNvSetgjEHVhBDVeRdbR92KNMobFn4WQuuVjq5Np4uLKLp1wGLNY5eusuF?cluster=devnet

交易

Solana 的交易包 括:

pub struct Transaction {
    /// A set of signatures of a serialized [`Message`], signed by the first
    /// keys of the `Message`'s [`account_keys`], where the number of signatures
    /// is equal to [`num_required_signatures`] of the `Message`'s
    /// [`MessageHeader`].
    ///
    /// [`account_keys`]: Message::account_keys
    /// [`MessageHeader`]: crate::message::MessageHeader
    /// [`num_required_signatures`]: crate::message::MessageHeader::num_required_signatures
    // NOTE: Serialization-related changes must be paired with the direct read at sigverify.
    #[wasm_bindgen(skip)]
    #[serde(with = "short_vec")]
    pub signatures: Vec<Signature>, // 签名

    /// The message to sign.
    #[wasm_bindgen(skip)]
    pub message: Message, // 消息
}
  1. 签名: 包含在交易中的签名数组。
  2. 消息: 要原子处理的指令列表。

交易消息的结构包括:

  • 消息头:指定签名者和只读账户的数量。
  • 账户地址:指令在交易中所 需的账户地址数组。
  • 最新的 Blockhash:作为交易的时间 戳。
  • 指令:要执行的指令数组。

交易大小

Solana网络遵循最大传输单元(MTU)大小为1280字节,与 IPv6 MTU大小约束一致,以确保快速可 靠地通过 UDP 传输集群信息。 在计算必要的标头后(IPv6的40字节和8字节的片段 头),1232 字节仍可用于数据包, 例如序列化交易。

这意味着 Solana 交易的总大小限制为 1232 字节。签名和消息的组合不能超过此限制。

  • 签名:每个签名需要64字节。签名的数量可以根据交易的要求而变化。 签名数量可以不 同,取决于交易的要求。
  • 消息:消息包括指令、账户和附加元数据,每个账户需要32字节。 账户加上元数据的组 合大小可以根据交易中包含的指令而变化。

消息头

消息头具 体规定了交易账户地址数组中包含的账户的权限。 它由三个字节组成,每个字节含有一个 u8 整数,它们共同规定:

pub struct MessageHeader {
    /// The number of signatures required for this message to be considered
    /// valid. The signers of those signatures must match the first
    /// `num_required_signatures` of [`Message::account_keys`].
    // NOTE: Serialization-related changes must be paired with the direct read at sigverify.
    pub num_required_signatures: u8,

    /// The last `num_readonly_signed_accounts` of the signed keys are read-only
    /// accounts.
    pub num_readonly_signed_accounts: u8,

    /// The last `num_readonly_unsigned_accounts` of the unsigned keys are
    /// read-only accounts.
    pub num_readonly_unsigned_accounts: u8,
}
  1. 交易所需的签名数量。
  2. 需要签名的只读账户地址的数量。
  3. 不需要签名的只读账户地址的数量。

紧凑数组格式

在交易消息的上下文中,紧凑数组指的是以以下格式序列化的数组:

  1. 数组的长度,编码为 compact-u16。
  2. 编码长度后按顺序列出数组的各个项。

这种编码方法用于指定交易消息中 的账户地址和指令数 组的长度。

账户地址数组

交易消息包括一个数组,其中包含所 有账户地址 ,这些地址是交易内指令所需的。

pub account_keys: Vec<Pubkey>,

该数组以一个 compact-u16 编码开 始,后跟按账户权限排序的地址。 消息头中的元数据用于确定每个部分中的账户数量。

  • 可写且签名者账户
  • 只读且签名者的账户
  • 可写且非签名者的账户
  • 只读且非签名者的账户

最近的块哈希

所有交易都包括一 个最近的区块哈希, 用作交易的时间戳。 区块哈希用于防止重复和消除过时的交易。

交易的区块哈希的最大年龄为150个区块(假设每个区块时间为400毫秒,约1分钟)。 如果 交易的区块哈希比最新的区块哈希旧150个区块,那么它被视为已过期。 这意味着在特定时 间范围内未处理的交易将永远不会被执行。

你可以使用getLatestBlockhash RPC方法来获 取当前的区块哈希以及区块哈希将有效的最后一个区块高度。 以下是一个 在Solana Playground 上的示例。

所以不能和以太坊一样,预先离线生成很多交易,因为以太坊只对比nonce没有过期

指令数组

交易消息包括一个包含所 有请求处理的指令的 数组。 交易消息中的指令采用以下格 式:CompiledInstruction

与账户地址数组类似,这个紧凑数组以一 个compact-u16编码开始,后跟一个 指令数组。数组中的每个指令指定以下信息: 对于每个指令所需的每个账户,必须指定以 下信息:

  1. 程序ID:标识将处理指令的链上程序。这表示为指向账户地址数组中的一个账户地 址的u8索引。 这是一个 u8 索引,指向帐户地址数组中的帐户地址。
  2. 账户地址索引的紧凑数组:指向每个指令所需的账户地址数组的u8索引数组。
  3. 不透明u8数据的紧凑数组:特定于被调用程序的u8字节数组。 此数据指定要在程序 上调用的指令,以及指令需要的任何附加数据(例如函数参数)。

示例交易结构

以下是包括单个 SOL转账 指令的交易结构示 例。 它显示了消息细节,包括头部、账户密钥、区块哈希和指令,以及交易的签名。

  • header:包括用于指定accountKeys数组中的读/写和签名者权限的数据。
  • accountKeys:包括交易中所有指令的账户地址。
  • recentBlockhash:交易创建时包含的区块哈希。
  • instructions:包括交易中所有指令。 每个指令中的account和programIdIndex通 过索引引用accountKeys数组。
  • signatures:包括交易中指令所需的所有签名。 通过使用相应账户的私钥对交易消息 进行签名来创建签名。
"transaction": {
    "message": {
      "header": {
        "numReadonlySignedAccounts": 0,
        "numReadonlyUnsignedAccounts": 1,
        "numRequiredSignatures": 1
      },
      "accountKeys": [
        "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
        "5snoUseZG8s8CDFHrXY2ZHaCrJYsW457piktDmhyb5Jd",
        "11111111111111111111111111111111"
      ],
      "recentBlockhash": "DzfXchZJoLMG3cNftcf2sw7qatkkuwQf4xH15N5wkKAb",
      "instructions": [
        {
          "accounts": [
            0,
            1
          ],
          "data": "3Bxs4NN8M2Yn4TLb",
          "programIdIndex": 2,
          "stackHeight": null
        }
      ],
      "indexToProgramIds": {}
    },
    "signatures": [
      "5LrcE2f6uvydKRquEJ8xp19heGxSvqsVbcqUeFoiWbXe8JNip7ftPQNTAVPyTK7ijVdpkzmKKaAQR7MWMmujAhXD"
    ]
  }

指令

一 个指令是 对链上执行特定操作的请求,也是程序中最小 的连续执行逻辑单元。

构建要添加到交易中的指令时,每个指令必须包括以下信息:

  • 程序地址:指定被调用的程序。
  • 账户:列出每个指令读取或写入的每个账户,包括其他程序,使用 AccountMeta 结构。
  • 指令数据:一个字节数组,指定要在程序上调用 的指令处理程序,以及指令处理程序所需 的任何附加数据(函数参数)。

账户元数据

对于每个指令所需的每个账户,必须指定以下信息:

  • pubkey:账户的链上地址
  • is_signer:指定账户是否在交易中作为签名者
  • is_writable:指定账户数据是否将被修改

这些信息被称 为账户元数据

pub struct AccountMeta {
    /// An account's public key.
    pub pubkey: Pubkey,
    /// True if an `Instruction` requires a `Transaction` signature matching `pubkey`.
    pub is_signer: bool,
    /// True if the account data or metadata may be mutated during program execution.
    pub is_writable: bool,
}

通过指定指令所需的所有账户,以及每个账户是否可写,可以并行处理交易。
例如,两个不包含写入相同状态的账户的交易可以同时执行。

示例指令结构

以下是一个 SOL 转账指令结构的示例,详 细说明了指令所需的账户密钥、程序 ID 和数据。

  • keys:包括每个指令所需的AccountMeta (账户元数据)。
  • programId:包含执行指令的程序地址。
  • data:指令数据,作为字节缓冲区
{
  "keys": [
    {
      "pubkey": "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
      "isSigner": true,
      "isWritable": true
    },
    {
      "pubkey": "BpvxsLYKQZTH42jjtWHZpsVSa7s6JVwLKwBptPSHXuZc",
      "isSigner": false,
      "isWritable": true
    }
  ],
  "programId": "11111111111111111111111111111111",
  "data": [2,0,0,0,128,150,152,0,0,0,0,0]
}

扩展示例

构建程序指令的详细信息通常由客户端库抽象掉。 但是,如果没有可用的库,你总是可以 手动构建指令。

手动SOL转账

这是一个 Solana Playground 示 例,展示了如何手动构建 SOL 转账指令:

// 定义转账金额
const transferAmount = 0.01; // 0.01 SOL

// 系统程序转移指令的指令索引
const transferInstructionIndex = 2;

// 为要传递给传输指令的数据创建一个缓冲区
const instructionData = Buffer.alloc(4 + 8); // uint32 + uint64
// 将指令索引写入缓冲区
instructionData.writeUInt32LE(transferInstructionIndex, 0);
// 将转帐金额写入缓冲区
instructionData.writeBigUInt64LE(BigInt(transferAmount * LAMPORTS_PER_SOL), 4);

// 手动创建一个传输指令,用于将SOL从发送方传输到接收方
const transferInstruction = new TransactionInstruction({
  keys: [
    { pubkey: sender.publicKey, isSigner: true, isWritable: true },
    { pubkey: receiver.publicKey, isSigner: false, isWritable: true },
  ],
  programId: SystemProgram.programId,
  data: instructionData,
});

// 将转账指令添加到新交易中
const transaction = new Transaction().add(transferInstruction);

在背后,使用 SystemProgram.transfer 方法 的简单例子在功能上等同于上面更详 细的示例。

// 定义转账金额
const transferAmount = 0.01; // 0.01 SOL

// 创建一个转账指令,从 wallet_1 到 wallet_2
const transferInstruction = SystemProgram.transfer({
  fromPubkey: sender.publicKey,
  toPubkey: receiver.publicKey,
  lamports: transferAmount * LAMPORTS_PER_SOL, // Convert transferAmount to lamports
});

//  添加转账指令到新交易
const transaction = new Transaction().add(transferInstruction);

SystemProgram.transfer方法简单地隐藏了为每个指令所需的账户创建指令 数据缓冲区和AccountMeta的细节。

Solana 核心概念-账户模型

1. Solana 账户模型

在 Solana 上,所有数据都存储在所谓的“帐户”中。 Solana 上的数据组织方式类似于键值存储,其中数据 库中的每个条目称为“帐户(Account)”。

1.1 要点

  • 账户最多可存储10MB 数据,这些数据可以由可执行的程序代码或程序状态组成。
  • 账户需要以SOL形式存入租金,与存储数据的数量成比例,当账户关闭时,该金额可以 全额退款。
  • 每个帐户都有一个程序“所有者”。 只有拥有帐户的程序可以修改其数据或扣除其lamport余额。 然而,任何人都可以增加的余额。
  • 程序 (智能合约) 是无状态账户,用于存储可执行代码。
  • 数据账户是由程序创建的,用于存储和管理程序状态。
  • 原生程序是内置程序,包括在 Solana 运行时内。
  • Sysvar 帐户是存储网络集群状态的特殊帐户。

1.2 账户

每个帐户都可以通过其唯一的地址来识别,用 Ed25519 PublicKey 格式。 你可以将地址的视为账户的唯一标识符。

此账户及其地址之间的关系可以被视为键值对, 而地址是定位相应的链上账户数据的键。

1.2.1 帐户信息

帐户有一个 最大大小为 10MB(10 Mega Bytes) ,

/// Maximum permitted size of account data (10 MiB).
pub const MAX_PERMITTED_DATA_LENGTH: u64 = 10 * 1024 * 1024;

并且 Solana 上每个帐户上存储的数据有以下结构称为 AccountInfo

/// Account information
#[derive(Clone)]
#[repr(C)]
pub struct AccountInfo<'a> {
    /// 账户公钥
    pub key: &'a Pubkey,
    /// 账户余额的数字表示方式为 lamports,这是SOL中最小的单位(1 SOL = 10亿 lamports)
    pub lamports: Rc<RefCell<&'a mut u64>>,
    /// 存储帐户状态的字节数组。 如果帐户是程序(智能合约),则存储可执行程序代 码。 此字段是,通常称为“帐户数据”。
    pub data: Rc<RefCell<&'a mut [u8]>>,
    /// 指定哪一个程序的公钥 (程序ID) 拥有该帐户。
    pub owner: &'a Pubkey,
    /// 此帐户下次欠租金的时期
    pub rent_epoch: Epoch,
    /// 该交易是否由该账户的公钥签名
    pub is_signer: bool,
    /// 该帐户是否可写?
    pub is_writable: bool,
    /// 表示帐户是否为程序的布尔标识符。
    pub executable: bool,
}

作为Solana 账户模式的一个关键部分,Solana 上的每个账户都有一个指定为“所有者”,特 别是一个程序。 只有指定为帐户所有者的程序可以修改帐户中存储的数据或扣除lamport余 额。 重要的是要注意到,虽然只有所有者可以扣除余额,但任何人都可以增加余额。

若要在链上存储数据,必须将一定数量的SOL转账到一个帐户。 转账金额与账户上存储的 数据的大小成比例。 这一概念通常称为“租用”。 然而,仍然存在着这种情况。你可以认 为"出租"更像"保证金",因为分配给帐户的SOL可以在关闭帐户时完全恢复。

原生程序

Solana 包含少数原生程序,这些程序是验证器实现的一部分,并为网络提供了各种核心功 能。 你可以这里找到原生程序的完 整列表。
当在 Solana 上开发自定义程序时,你通常会与两个原生程序交互,即系统程序和 BPF Loader。

系统程序

默认情况下,所有新账户都属 于系统程序。 系统程序执行几项关键任务,例如:

  • 新账户创建: 只有系统程序可以创建新帐户。
  • 空间分配: 设置每个帐户数据字节的容量。
  • 分配程序所有者: 一旦系统程序创建了一个帐户,它可以将指定的程序所有者重新分配到不同的程序帐户。 这是自定义程序如何获取系统程序创建的新账户的所有权。

在Solana上,“钱包”只是系统程序拥有的帐户。 钱包的 lamport 余额是账户拥有的 SOL金 额。

只有系统程序拥有的账户才能用作交易费付款人。

BPFLoader 程序

BPF Loader 是指定为网络上所有其他程序的“所有者”的程序,不包括原生程序。 它负责部署、升级和 执行自定义程序。

Sysvar 帐户

Sysvar账户是位于预定义地址的特殊账户,可以访问集群状态数据。 这些帐户使用网络集 群的数据动态更新。 你可以在这里找 到 Sysvar 账户的完整列表。

自定义程序

在Solana,“智能合约”称为 programs。 程序是一个包含可执 行代码的帐户,由一个设置为真的 “executable” 标志表示。

程序账户

当新程序 为已部署时, 从技术上讲,Solana 上创建了三个独立的账户:

  • 程序账户: 代表链上程序的主账户。 此帐户存储可执行数据帐户的地址(存储编译的 程序代码)和程序的更新权限(授权地址更改程序)。
  • 程序可执行数据帐户:包含可执行文件的帐户程序的字节码。
  • 缓冲帐户:一个临时帐户,在程序正在部署或升级时存储字节代码。 处理完成后, 数据将转入程序可执行数据账户,并关闭缓冲账户。

例如,这里是 Solana 浏览器上的 Token 扩展 的程序账户链 接及其相应 的程序可执行数据帐户

为了简单起见,你可以将“程序帐户”视为程序本身。

“程序帐户”的地址通常称为“程序ID”,用于调用该程序。

数据帐户

Solana 程序是“无状态”的,这意味着程序帐户只包含程序可执行字节代码。 要存储和修改 额外数据,必须创建新的帐户。 这些账户通常称为“数据账户”。
数据账户可以存储在所有者程序的代码中定义的任意数据。

注意,只有 System Program 可以创建新帐 户。 一旦系统程序创建了一个帐户,它就可以将新帐户的所有权转移到另一个程序。

换言之,为自定义程序创建数据账户需要两个步骤:

  1. 调用系统程序来创建一个帐户,然后将所有权转到一个自定义程序
  2. 调用现在拥有账户的自定义程序,然后初始化程序代码中定义的账户数据
    这个数据账户创建过程常常是抽象为一步,但是了解基础过程是有用的。

Solana版本分布与选择

查看版本分布

https://www.validators.app/?locale=en&network=mainnet

目前主要版本在 2.0.15 - 2.0.18

solana 官方最新版本为v1.18.26
https://github.com/solana-labs/solana

agave 最新版本为 v2.0.18
https://github.com/anza-xyz/agave/

说明目前solana 目前网络主要运行的agave版本

从anza消息来看,核心的开发都转到了agave, 现有solana-labs/solana会逐步被废弃

https://medium.com/anza-xyz/anza-github-migration-faq-9ab94e03f8dc

如果我有新的 PR,我应该将其提交给 Agave 还是旧的 Solana Labs repo,为什么?新的 PR 应该提交给 anza-xyz/agave。solana-labs/solana repo 将很快存档。

旧的 Solana Labs 仓库会怎样? Solana Labs monorepo 最终将变为非活动状态。它不会立即归档,因为那里的问题更容易就地解决,但很快就会归档。这将是一个长达数月的过程,Agave 客户端会镜像 Solana Labs 客户端,然后两者会分道扬镳,Solana Labs 客户端将完全变为非活动状态