跳到主要内容

18 篇博文 含有标签「Solana」

查看所有标签

Solana区块链有几个不同的验证者组,称为集群

每个集群在整个生态系统中都有不同的作用,并包含专用的API节点来处理其对应集群的JSON-RPC请求。

集群内的各个节点由第三方拥有和运营,每个节点都有一个公共端点。

Solana公共RPC端点

Solana Labs组织为每个集群运营一个公共RPC端点。 这些公共端点有速率限制,但用户和开发者可以通过它们与Solana区块链进行交互。

信息

公共端点的速率限制可能会改变。本文档中列出的具体速率限制不保证是最新的。

使用不同集群的区块链浏览器

许多流行的Solana区块链浏览器支持选择任意集群,并且通常允许高级用户添加自定义/私有的RPC端点。

一些Solana区块链浏览器示例如下:

开发网 (Devnet)

开发网为任何希望试用Solana的人提供了一个测试平台,无论是用户、代币持有者、应用开发者还是验证者。

  1. 应用开发者应以开发网为目标。
  2. 潜在的验证者应首先以开发网为目标。
  3. 开发网与主网测试版的主要区别:
    • 开发网代币不是真实的
    • 开发网包含一个代币水龙头用于应用测试
    • 开发网可能会重置账本
    • 开发网通常运行与主网测试版相同的软件发布分支版本,但可能运行比主网测试版更新的小版本。
  4. 开发网的Gossip入口:
    • entrypoint.devnet.solana.com:8001

开发网端点

  • https://api.devnet.solana.com - 单一Solana Labs托管的API节点,有速率限制

示例solana命令行配置

使用Solana CLI连接到开发网络Cluster

solana config set --url https://api.devnet.solana.com

开发网速率限制:

  • 10秒每IP的最大请求数:100
  • 10秒每IP的单个RPC最大请求数:40
  • 每IP的最大并发连接数:40
  • 10秒每IP的最大连接速率:40
  • 30秒的最大数据量:100 MB

测试网 (Testnet)

测试网是Solana核心贡献者在实时集群上压力测试最新发布功能的地方,特别关注网络性能、稳定性和验证者行为。

  • 测试网代币不是真实的
  • 测试网可能会重置账本
  • 测试网包含一个代币水龙头用于应用测试
  • 测试网通常运行比开发网和主网测试版更新的软件发布分支
  • 测试网的Gossip入口:entrypoint.testnet.solana.com:8001

测试网端点

  • https://api.testnet.solana.com - 单一Solana Labs托管的API节点,有速率限制

示例solana命令行配置:

solana config set --url https://api.testnet.solana.com

测试网速率限制

  • 10秒每IP的最大请求数:100
  • 10秒每IP的单个RPC最大请求数:40
  • 每IP的最大并发连接数:40
  • 10秒每IP的最大连接速率:40
  • 30秒的最大数据量:100 MB

主网测试版 (Mainnet Beta)

主网测试版是一个无许可、持久的集群,供Solana用户、开发者、验证者和代币持有者使用。

  • 在主网测试版上发行的代币是真实的SOL
  • 主网测试版的Gossip入口:entrypoint.mainnet-beta.solana.com:8001

主网测试版端点

https://api.mainnet-beta.solana.com - 由Solana Labs托管的API节点集群,由负载均衡器支持,有速率限制

示例Solana命令行配置

要使用 Solana CLI 连接到 mainnet-beta Cluster:

solana config set --url https://api.mainnet-beta.solana.com

主网测试版速率限制:

  • 10秒每IP的最大请求数:100
  • 10秒每IP的单个RPC最大请求数:40
  • 每IP的最大并发连接数:40
  • 10秒每IP的最大连接速率:40
  • 30秒的最大数据量:100 MB
信息

公共RPC端点不适用于生产应用。请在启动应用、发行NFT等时使用专用/私有RPC服务器。 公共服务可能会受到滥用,并且速率限制可能会在没有事先通知的情况下更改。 同样,高流量网站可能会在没有事先通知的情况下被封锁。

常见HTTP错误代码

  • 403 - 您的IP地址或网站已被封锁。是时候运行自己的RPC服务器或寻找私人服务了。
  • 429 - 您的IP地址超出了速率限制。请减慢速度!使用Retry-After HTTP响应头来确定在再次请求之前需要等待的时间。
鱼雪

跨程序调用(CPI)指的是一个程序调用另一个程序的指令。这种机制使得Solana程序具有组合性。

你可以把指令看作是一个程序对网络公开的API端点,而CPI则是一个API内部调用另一个API。

CPI

当程序启动对另一个程序的跨程序调用(CPI)时:

  • 签名者的特权从调用程序(A)的初始事务传递到被调用方(B)程序
  • 被调用方(B)程序可以进一步调用其他程序,最多达到4层深度(例如,B->CC->D
  • 这些程序可以代表其程序ID派生的PDAs进行"签名"
信息

Solana 程序运行时定义了一个名为 max_invoke_stack_height 的常量,设置为 5。 这代表程序指令调用堆栈的最大高度。 堆栈高度从事务指令的 1 开始, 每当一个程序调用另一个指令时增加 1。 此设置有效地限制了 CPI 的调用深度为 4

关键点

  • CPI使Solana程序指令可以直接调用另一个程序的指令。
  • 调用程序的签名权限会延伸到被调用的程序。
  • 在进行CPI时,程序可以代表从其程序ID派生的PDA进行签名。
  • 被调用的程序可以进一步调用其他程序,最大调用深度为4

编写CPI

编写CPI指令的模式与构建一个交易指令类似。每个CPI指令必须指定以下信息:

  • 程序地址:指定被调用的程序。
  • 账户:列出指令读取或写入的所有账户,包括其他程序。
  • 指令数据:指定要调用的程序指令及所需的其他数据(函数参数)。

取决于要调用的程序,可能有一些crate提供了构建指令的辅助函数。 程序通过solana_program crate中的以下函数执行CPI:

  • invoke:用于没有PDA签名者的情况。
  • invoke_signed:用于需要调用程序用其程序ID派生的PDA进行签名的情况。

基本CPI

invoke函数用于不需要PDA签名者的CPI。 在进行CPI时,提供给调用程序的签名者权限会自动延伸到被调用程序。

pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>]
) -> Result<(), ProgramError>

这是在 Solana Playground 上的一个示例程序,它使用 invoke 函数进行 CPI, 调用 System Program 上的转账指令。您还可以参考基础 CPI 指南获取更多详细信息。

带PDA签名者的CPI

invoke_signed函数用于需要PDA签名者的CPI。

用于派生签名者PDA的种子通过signer_seeds参数传递给invoke_signed函数。

您可以查阅程序派生地址页面,了解关于如何派生 PDAs 的详细信息。

pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>

运行时使用授予调用程序的权限来确定可以扩展到被调用程序的权限。 这些权限包括签名者和可写账户。 例如,如果调用程序处理的指令包含一个签名者或可写账户, 那么调用程序可以调用包含该签名者和/或可写账户的指令。

尽管PDA没有私钥,但它们仍可以通过CPI在指令中充当签名者。 为了验证PDA是否由调用程序派生,必须在signers_seeds中包含用于生成PDA的种子。

在处理CPI时,Solana运行时内部调用create_program_address, 使用signers_seeds和调用程序的program_id。 如果找到有效的PDA,该地址会被添加为有效的签名者。

这是在 Solana Playground 上的一个示例程序,使用 invoke_signed 函数进行 CPI, 调用 System Program 上的 transferinstruction,并使用 PDA 签名者。 您可以参考使用 PDA 签名者指南了解更多详细信息。

总结

跨程序调用(CPI)是Solana程序的重要组成部分,通过允许程序调用其他程序的指令,

实现了程序的高度组合性和灵活性。

通过合理使用CPI,开发者可以创建更加复杂和功能丰富的去中心化应用。

鱼雪

Solana区块链有几种不同类型的费用和成本,这些费用和成本是在使用无许可网络时产生的。

这些费用可以分为几种特定类型

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

交易费用

在Solana区块链上的链上程序中处理逻辑(指令)所支付的小额费用被称为交易费用

当每笔交易(包含一个或多个指令)通过网络发送时,它会由当前的验证者领导处理。

一旦被确认为全局状态交易,这笔交易费用将支付给网络,以支持Solana区块链的经济设计

信息

交易费用不同于账户数据存储押金(租金)交易费用用于在Solana网络上处理指令,而租金押金是保存在账户中的余额, 用于存储其数据在区块链上,并且可以被取回。

目前,Solana的基本交易费用设定为每个签名5000 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、执行系统调用等), 每个指令可能消耗不同数量的计算单元。

信息

一个程序可以记录其计算使用的细节,包括剩余的计算预算。 请参阅程序调试以获取更多信息。 您还可以在此指南中找到更多关于优化计算使用的信息。

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

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

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

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

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

计算单元限制

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

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

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

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

信息

有关更多详细信息和最佳实践,请阅读此指南以请求最佳计算限制。

计算单元价格

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

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

优先费用

作为计算预算的一部分,运行时支持交易支付一种可选费用,称为“优先费用”。 支付此额外费用有助于提升交易相对于其他交易的优先级,从而实现更快的执行时间。

如何计算优先费用

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

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

如果未提供SetComputeUnitLimit指令,将使用默认的计算单元限制。

如果未提供SetComputeUnitPrice指令, 交易将默认没有额外的提升费用和最低优先级(即没有优先费用)。

如何设置优先费用

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

您可以通过Rust或@solana/web3.js函数来制作这些指令。 每个指令可以像正常一样包含在交易中并发送到集群。另见下面的最佳实践。

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

信息

交易只能包含每种类型的一个计算预算指令。 重复的指令类型将导致TransactionError::DuplicateInstruction错误,并最终导致交易失败。

Rust

Rust solana-sdk crate包括ComputeBudgetInstruction内的函数, 以制作设置计算单元限制和计算单元价格的指令:

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

Javascript

@solana/web3.js库包括ComputeBudgetProgram类内的函数, 以制作设置计算单元限制和计算单元价格的指令:

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

const instruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 1,
});

优先费用最佳实践

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

请求最少计算单元

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

获取最近的优先费用

在将交易发送到集群之前,您可以使用getRecentPrioritizationFees RPC方法获取节点处理的最近区块内支付的优先费用列表。

然后,您可以使用这些数据估计交易的适当优先费用,以便(a)更好地确保其被集群处理,以及(b)最小化支付的费用。

租金

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

信息

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

所有账户都必须保持足够高的lamport余额(相对于其分配的空间)以免除租金并保持在Solana区块链上。

任何试图将账户余额减少到低于其相应的免租金最低余额的交易将失败(除非余额正好减少到零)。

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

这可以通过将账户的全部lamport余额提现(转移)到另一个账户(即您的钱包)来实现。 通过将账户余额正好减少到0,运行时将在垃圾收集中从网络中移除该账户及其关联数据。

租金率

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

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

免租金

账户必须保持高于其所需数据存储在链上的最低余额。这称为“免租金”,其余额称为“免租金最低余额”。

信息

Solana上的新账户(和程序)必须初始化足够的lamports以免除租金。 这并非一直如此。以前,运行时会定期自动从低于其免租金最低余额的账户中收取费用, 最终将这些账户减少到零余额并从全局状态中垃圾收集(除非手动补充)。

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

每次减少账户余额时,运行时都会检查账户是否仍然高于此免租金最低余额。 除非将最终余额减少到正好为零(关闭账户),否则会导致账户余额低于免租金阈值的交易将失败。

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

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

solana rent 15000

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

垃圾收集

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

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

注意

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

即使在账户已从网络中移除(通过垃圾收集)后, 它可能仍然有与其地址相关的交易(过去的历史或未来的交易)。 尽管Solana区块浏览器可能会显示“找不到账户”类型的信息, 您仍然可以查看与该账户相关的交易历史。

您可以阅读验证者实施的垃圾收集提案以了解更多信息。

总结

Solana上的费用体系包括以下几种主要费用类型:

  1. 交易费用
  • 用途:支付给验证者处理交易和指令的费用。
  • 计算:每个签名的固定费用(目前为5000 lamports+ 计算单元费用(基于交易的计算资源消耗)。
  • 功能:减少垃圾交易,补偿验证者的计算资源消耗,确保网络的经济稳定性。
  1. 优先费用
  • 用途:提高交易处理的优先级,以实现更快的执行时间。
  • 计算:计算单元限制 × 计算单元价格(以微lamports计)。
  • 功能:在网络繁忙时,支付更高的费用可提升交易处理优先级。
  1. 租金
  • 用途:存储账户数据在链上保留的费用。
  • 计算:根据账户存储数据的大小计算租金押金。
  • 功能:保证账户数据在链上存储,同时当账户余额减少到零时,账户数据会被垃圾收集移除,释放存储空间。

具体细节

  1. 交易费用

    • 支付给验证者网络,部分费用被烧毁(目前为50%)。
    • 如果交易失败,费用仍会被扣除,但账户状态会回滚。
  2. 优先费用

    • 可选费用,通过设置计算单元限制和价格来实现。
    • 帮助提升交易在区块内的处理优先级,确保在网络高负载时仍能及时处理。
  3. 租金

    • 所有账户需保持足够的余额以免除租金。
    • 未达到免租金最低余额的账户会在垃圾收集中被移除。
    • 可以通过计算租金押金的最低余额来确保账户数据长期保留。

Solana的费用体系旨在确保网络的高效运行和经济可持续性,同时为用户提供灵活的费用选项以满足不同的交易需求。

鱼雪

在Solana生态系统中,智能合约被称为程序

每个程序都是一个存储可执行逻辑的链上账户这些逻辑被组织成特定的函数,称为指令

关于Solana程序的更多内容,请参阅本文档的部署程序部分。

关键点

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

编写Solana程序

Solana程序主要用Rust编程语言编写。

有两种常见的开发方法

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

更新Solana程序

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

可验证程序

确保链上代码的完整性和可验证性是至关重要的。

可验证构建确保任何第三方可以独立验证部署在链上的可执行代码与其公开的源代码匹配。

此过程增强了透明度和信任度,使检测源代码与部署程序之间的差异成为可能。

  • 搜索已验证程序:用户可以在SolanaFM Explorer上搜索程序地址并导航到“Verification”标签,快速检查已验证程序。
  • 验证工具:Ellipsis Labs的Solana Verifiable Build CLI使用户能够独立验证链上程序与发布的源代码。
  • Anchor对可验证构建的支持:Anchor提供了对可验证构建的内置支持。详细信息可以在Anchor文档中找到。

伯克利包过滤器 (BPF)

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

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

程序总结

在Solana生态系统中,"智能合约"称为程序。

程序是链上账户,存储可执行逻辑,分为具体的指令。

主要特点如下:

  • 无状态设计:程序本身无状态,但可以创建新账户来管理状态。
  • 升级和不可变性:程序可以通过升级权限账户更新。一旦设置升级权限为null,程序将变得不可变。
  • 可验证构建:可验证构建确保链上程序与其公开的源代码匹配,提高透明度和信任度。

编写和更新程序

  • 编写:Solana程序主要用Rust编写,有两种方法:
    • Anchor框架:简化开发,适合初学者。
    • 原生Rust:提供更大灵活性,但更复杂。
  • 更新:通过升级权限账户进行。如果升级权限被撤销,程序将无法更新。

可验证性

  • 验证工具:使用工具如Solana Verifiable Build CLI进行验证。
  • Anchor支持:Anchor内置支持可验证构建。

技术细节

  • 伯克利包过滤器 (BPF):Solana使用LLVM编译程序,生成Solana字节码格式 (sBPF),增强了平台的灵活性。
鱼雪

在Solana中,程序派生地址(PDA有两个主要用途

  1. 确定性账户地址:通过组合可选“种子”(预定义输入)和特定程序ID来确定性地派生地址。
  2. 启用程序签名:Solana运行时允许程序对派生自其程序ID的PDA进行“签名”。

可以将PDA看作是通过预定义输入(例如字符串、数字和其他账户地址)在链上创建类似哈希表结构的一种方式。

这种方法的好处在于,它消除了对准确地址的跟踪需求。 相反,您只需记住用于其派生的特定输入。

PDA

重要的是要明白,简单地派生程序派生地址(PDA)不会自动在该地址上创建链上账户。 以 PDA 作为链上地址的账户必须通过用于派生地址的程序明确创建。 你可以将派生 PDA 理解为在地图上找到地址。仅有地址并不意味着该位置上有任何建筑物。

信息

本节将涵盖有关推导 PDAs 的详细信息。 有关程序如何使用 PDAs 进行签名的细节将在跨程序调用 (CPIs) 部分中进行讨论, 因为这需要两个概念的上下文。

关键点

  • 通过使用用户定义的种子、bump seed 和程序的 ID 的组合,确定性地推导出 PDAs 地址。
  • PDAs 是指偏离 Ed25519 曲线且没有相应私钥的地址。
  • Solana 程序可以通过其程序 ID 为使用该程序 ID 推导出的 PDAs 进行程序化“签名”。
  • 推导 PDA 不会自动创建链上账户。
  • 使用 PDA 作为地址的账户必须通过 Solana 程序内的专用指令显式创建。

什么是PDA

PDA是确定性派生的地址,类似标准公钥,但没有关联的私钥。

外部用户无法为该地址生成有效签名。

然而,Solana运行时允许程序无需私钥即可程序化地对PDA进行“签名”。

在上下文中,Solana Keypairs 是 Ed25519 曲线上的点(椭圆曲线加密),具有公钥和对应的私钥。

我们经常使用公钥作为新的链上账户的唯一标识符,私钥用于签名

在曲线上的地址

PDA 是使用预定义的一组输入故意导出到 Ed25519 曲线之外的点。

不在 Ed25519 曲线上的点没有有效的对应私钥,无法用于加密操作(签名)。

PDA 然后可以用作链上账户的地址(唯一标识符), 提供一种便捷的存储、映射和获取程序状态的方法。

弧线以外的地址

如何派生PDA

派生PDA需要三个输入:

  1. 可选种子:用于派生PDA的预定义输入(如字符串、数字、其他账户地址), 这些输入转换为字节缓冲区。
  2. 递增种子:保证生成有效PDA的附加输入(值在2550之间)。 递增种子附加到可选种子后用于生成PDA,将点“推离”Ed25519曲线。
  3. 程序ID:用于派生PDA的程序地址,也是可以代表PDA“签名”的程序。

使用findProgramAddressSync方法可以派生PDA,输入可选种子的字节缓冲区和程序ID。 找到有效PDA后,该方法返回地址(PDA)和递增种子。

PDA推导

以下示例包含指向Solana Playground的链接,在那里您可以在基于浏览器的编辑器中运行示例。

示例代码

要衍生一个PDA,我们可以使用 @solana/web3.js 中的 findProgramAddressSync 方法。 在其他编程语言(例如 Rust)中也有这个函数的等价物,但本节中我们将通过Javascript的示例进行讲解。

使用 findProgramAddressSync 方法时,我们传入:

  • 预定义的可选seeds转换为字节缓冲区,
  • 以及用于衍生PDA的程序ID(地址)

找到有效的PDA后,findProgramAddressSync 方法将返回PDA的地址和用于衍生PDA的bump seed

以下示例衍生一个PDA,而不提供任何可选seeds

import { PublicKey } from "@solana/web3.js";

const programId = new PublicKey("11111111111111111111111111111111");

const [PDA, bump] = PublicKey.findProgramAddressSync([], programId);

console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);

您可以在 Solana Playground 上运行此示例。PDAbump seed 输出将始终相同。

PDA: Cu7NwqCXSmsR5vgGA3Vw9uYVViPi3kQvkbKByVQ8nPY9
Bump: 255

增加可选种子“helloWorld”:

import { PublicKey } from "@solana/web3.js";

const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";

const [PDA, bump] = PublicKey.findProgramAddressSync(
[Buffer.from(string)],
programId,
);

console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);

你也可以在 Solana Playground 上运行此示例。PDA 和 增量种子 的输出将始终相同:

PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
Bump: 254

请注意,增量种子为254。这意味着255导出了Ed25519曲线上的一个点,并不是有效的PDA

findProgramAddressSync 返回的增量种子是在给定的可选种子和程序 ID 组合中导出有效 PDA 的第一个值(介于255-0之间)。

信息

这个第一个有效的提升种子被称为“规范提升”。 为了程序安全起见,在处理 PDAs 时建议只使用规范提升。

生成PDA账户

在底层,findProgramAddressSync 会迭代地将一个额外的bump seed (nonce) 附加到种子缓冲区并调用 createProgramAddressSync 方法。 bump seed 从值为 255 开始,每次减 1,直到找到有效的 PDA (脱轨)。

您可以通过使用 createProgramAddressSync 并显式传递 254 的增量种子来复制先前的示例。

import { PublicKey } from "@solana/web3.js";

const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const bump = 254;

const PDA = PublicKey.createProgramAddressSync(
[Buffer.from(string), Buffer.from([bump])],
programId,
);

console.log(`PDA: ${PDA}`);

在Solana Playground上运行上述示例。在使用相同的seeds和程序ID的情况下,PDA输出将与先前的匹配:

PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X

Canonical Bump

“规范碰撞” 指的是从 255 开始递减到 1 的第一个碰撞 seed,从而派生出一个有效的 PDA。 出于程序安全考虑,建议仅使用从规范碰撞派生的 PDA。

使用上面的例子作为参考,下面的示例尝试使用从 255-0 的每个碰撞 seed 来派生一个 PDA。

import { PublicKey } from "@solana/web3.js";

const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";

// Loop through all bump seeds for demonstration
for (let bump = 255; bump >= 0; bump--) {
try {
const PDA = PublicKey.createProgramAddressSync(
[Buffer.from(string), Buffer.from([bump])],
programId,
);
console.log("bump " + bump + ": " + PDA);
} catch (error) {
console.log("bump " + bump + ": " + error);
}
}

在 Solana Playground 上运行示例,您应该会看到以下输出:

bump 255: Error: Invalid seeds, address must fall off the curve
bump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
bump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4y
bump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHH
bump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdP
bump 250: Error: Invalid seeds, address must fall off the curve
...
// remaining bump outputs

正如预期的那样,bump 种子 255 会引发错误,第一个用于导出有效 PDA 的 bump 种子是 254

但请注意,bump 种子 253-251 都会导出具有不同地址的有效 PDA。 这意味着,在给定相同的可选种子和programId的情况下, 具有不同值的 bump 种子仍然可以导出有效的 PDA。

注意

在构建Solana程序时,建议包含安全检查,验证传递给程序的PDA是否使用规范的增量派生。 如果未这样做,可能会引入漏洞,允许向程序提供意外的帐户。

创建PDA账户

在 Solana Playground 上的这个示例程序演示了如何使用 PDA 作为 newaccount 的地址来创建帐户。 该示例程序是使用 Anchor 框架编写的。

lib.rs 文件中,您将找到以下程序,其中包含一个指令,用于使用 PDA 作为账户地址创建新账户。

新账户存储用户地址和用于派生 PDA 的递增种子。

lib.rs
use anchor_lang::prelude::*;

declare_id!("75GJVCJNhaukaa2vCCqhreY31gaphv7XTScBChmr1ueR");

#[program]
pub mod pda_account {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let account_data = &mut ctx.accounts.pda_account;
// store the address of the `user`
account_data.user = *ctx.accounts.user.key;
// store the canonical bump
account_data.bump = ctx.bumps.pda_account;
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub user: Signer<'info>,

#[account(
init,
// set the seeds to derive the PDA
seeds = [b"data", user.key().as_ref()],
// use the canonical bump
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
pub system_program: Program<'info, System>,
}

#[account]

#[derive(InitSpace)]
pub struct DataAccount {
pub user: Pubkey,
pub bump: u8,
}

用于派生 PDA 的种子包括指令中提供的user帐户的硬编码字符串data和地址。 Anchor 框架会自动派生规范化的bump种子。

#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,

init约束指示Anchor调用系统程序,使用PDA作为地址创建新帐户。

在幕后,这是通过CPI完成的。

#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,

在上面提供的 Solana Playground 链接中的测试文件(pda-account.test.ts)中, 您将找到派生 PDA 的 JavaScript 等效代码。

const [PDA] = PublicKey.findProgramAddressSync(
[Buffer.from("data"), user.publicKey.toBuffer()],
program.programId,
);

然后发送交易以调用initialize指令,使用PDA作为地址创建新的链上账户。 一旦发送交易,PDA用于获取在该地址创建的链上账户。

it("Is initialized!", async () => {
const transactionSignature = await program.methods
.initialize()
.accounts({
user: user.publicKey,
pdaAccount: PDA,
})
.rpc();

console.log("Transaction Signature:", transactionSignature);
});

it("Fetch Account", async () => {
const pdaAccount = await program.account.dataAccount.fetch(PDA);
console.log(JSON.stringify(pdaAccount, null, 2));
});

请注意,如果您多次使用相同的user地址作为种子调用initialize指令,则交易将失败。

这是因为在派生地址处已经存在一个帐户。

总结

程序派生地址 (PDA) 概述

在Solana中,程序派生地址(PDA)有两个主要用途:

  1. 确定性账户地址:通过组合可选的“种子”和程序ID来确定性地生成地址。
  2. 程序签名:Solana运行时允许程序对PDA进行签名。

关键特点

  • 确定性派生:PDA通过用户定义的种子、递增种子(bump seed)和程序ID组合确定性地派生。
  • 无私钥:PDA没有对应的私钥,因此没有用户可以对其生成有效签名,但程序可以进行签名。
  • 显式创建账户:派生PDA不会自动创建链上账户,必须通过程序显式创建。

PDA的派生过程

派生PDA需要三个输入:

  1. 可选种子:预定义的输入(如字符串、数字、其他账户地址)。
  2. 递增种子:用于保证生成有效PDA的附加输入,值在255到0之间。
  3. 程序ID:用于派生PDA的程序地址。

使用这些输入,程序可以确定性地生成唯一的PDA。

示例代码

以下示例展示了如何使用findProgramAddressSync方法派生PDA:

import { PublicKey } from "@solana/web3.js";

const programId = new PublicKey("11111111111111111111111111111111");

const [PDA, bump] = PublicKey.findProgramAddressSync([], programId);

console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);

增加可选种子“helloWorld”:

import { PublicKey } from "@solana/web3.js";

const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";

const [PDA, bump] = PublicKey.findProgramAddressSync(
[Buffer.from(string)],
programId,
);

console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);

创建PDA账户

以下示例展示了如何使用PDA作为新账户的地址创建账户(使用Anchor框架编写):

use anchor_lang::prelude::*;

declare_id!("75GJVCJNhaukaa2vCCqhreY31gaphv7XTScBChmr1ueR");

#[program]
pub mod pda_account {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let account_data = &mut ctx.accounts.pda_account;
account_data.user = *ctx.accounts.user.key;
account_data.bump = ctx.bumps.pda_account;
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub user: Signer<'info>,

#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
pub system_program: Program<'info, System>,
}

#[account]
#[derive(InitSpace)]
pub struct DataAccount {
pub user: Pubkey,
pub bump: u8,
}

注意事项

  • 规范递增种子:建议只使用规范递增种子派生的PDA,以确保程序安全。
  • 账户创建:使用相同用户地址作为种子多次调用initialize指令会失败,因为在派生地址处已经存在账户。
鱼雪

在 Solana 上,所有数据都存储在称为账户的结构中

Solana 上的数据组织方式类似于键值存储,其中数据库中的每个条目称为账户

Solana Accounts

关键点

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

账户

每个账户通过其唯一地址进行标识,该地址由 32 字节组成,格式为 Ed25519 公钥。

可以将地址视为账户的唯一标识符

Account Address

账户与其地址之间的关系可以看作是键值对,其中地址作为键,用于定位账户对应的链上数据。

账户信息

账户的最大大小为 10MB10 兆字节),每个 Solana 上存储的数据账户都具有以下结构, 称为账户信息AccountInfo)。

AccountInfo

每个账户的账户信息包括以下字段:

  • data:存储账户状态的字节数组。如果账户是一个程序(智能合约),则存储可执行程序代码。该字段通常称为“账户数据”。
  • executable:一个布尔标志,指示该账户是否是一个程序。
  • lamports:账户余额的数字表示,单位lamports,这是 SOL 的最小单位(1 SOL = 10 亿 lamports)。
  • owner:指定拥有账户的程序的公钥(程序 ID)。

作为 Solana 账户模型的一个关键部分每个 Solana 上的账户都有一个指定的“所有者”,即一个程序

只有被指定为账户所有者的程序才能修改账户上存储的数据或扣除 lamport 余额。

需要注意的是,虽然只有所有者可以扣除余额,但任何人都可以增加余额。

信息

要在链上存储数据,需要将一定数量的 SOL 转入账户。 转入的数量与账户上存储的数据量成比例。 这个概念通常称为租金。不过,您可以将租金视为押金, 因为当账户关闭时,分配给账户的 SOL 可以全额退还。

原生程序

Solana 包含少量原生程序,这些程序是验证器实现的一部分,并为网络提供各种核心功能。 您可以在此处找到完整的原生程序列表。

在 Solana 上开发自定义程序时,您通常会与两个原生程序进行交互,即系统程序和 BPF 加载器

系统程序

默认情况下,所有新账户都由系统程序拥有。

系统程序执行以下几个关键任务:

  • 新账户创建:只有系统程序可以创建新账户。
  • 空间分配:设置每个账户数据字段的字节容量。
  • 分配程序所有权:系统程序创建账户后,可以将指定的程序所有者重新分配给其他程序账户。 这就是自定义程序如何获得系统程序创建的新账户的所有权。

在 Solana 上,“钱包”只是由系统程序拥有的一个账户。钱包的 lamport 余额是账户拥有的 SOL 数量。

System Account

信息

只有由系统程序拥有的账户才能用作交易费用支付者。

BPF 加载器程序

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

Sysvar 账户

Sysvar 账户是位于预定义地址的特殊账户,提供访问集群状态数据的功能。

这些账户会动态更新网络集群的数据。您可以在此处找到 Sysvar 账户的完整列表。

自定义程序

在 Solana 上,智能合约称为程序

程序包含可执行代码的账户,通过设置为 true 的“可执行”标志来表示。

有关程序部署过程的详细说明,请参阅本文档的部署程序页面。

程序账户

当在 Solana 上部署新程序时,实际上会创建三个独立的账户:

  • 程序账户:表示链上程序的主账户。该账户存储可执行数据账户的地址(存储已编译的程序代码)和程序的更新权限(授权更改程序的地址)。
  • 程序可执行数据账户:包含程序可执行字节代码的账户。
  • 缓冲账户:在程序正在部署或升级时存储字节代码的临时账户。完成后,数据将转移到程序可执行数据账户,缓冲账户将关闭。

例如,这里是指向 Solana Explorer 的 Token 扩展程序账户及其相应的程序可执行数据账户的链接。

Program and Executable Data Accounts

为简单起见,您可以将“程序账户”视为程序本身。

Program Account

信息

程序账户的地址通常称为程序 ID用于调用程序

数据账户

Solana 程序是“无状态”的,这意味着程序账户仅包含程序的可执行字节代码

为了存储和修改额外的数据,必须创建新账户。

这些账户通常称为数据账户

数据账户可以存储在所有者程序代码中定义的任何任意数据。

Data Account

请注意,只有系统程序可以创建新账户。

系统程序创建账户后,可以将新账户的所有权转移给另一个程序。

换句话说,为自定义程序创建数据账户需要两个步骤

  1. 调用系统程序创建账户,然后将所有权转移给自定义程序。
  2. 调用现在拥有该账户的自定义程序,然后根据程序代码初始化账户数据。

这个数据账户创建过程通常被抽象为一步,但了解底层过程是有帮助的。

Solana 账户模型总结

在 Solana 区块链上,所有数据都存储在称为“账户”的结构中

以下是 Solana 上账户模型的主要内容和账户类型:

账户类型

  1. 程序账户(Program Accounts

    • 描述:存储可执行程序代码的无状态账户。
    • 用途:代表链上的智能合约,包含可执行的字节码。
    • 关键点:程序账户的地址被称为程序 ID,用于调用该程序。
  2. 数据账户(Data Accounts

    • 描述:存储程序状态的账户。
    • 用途:用于存储和管理与程序相关的任意数据。
    • 关键点:只有系统程序可以创建新账户,创建后可以将所有权转移给自定义程序。
  3. 原生程序账户(Native Program Accounts

    • 描述:内置于 Solana 运行时的程序账户。
    • 用途:提供网络的核心功能。
    • 关键点:包括系统程序和 BPF 加载器等,系统程序默认拥有新账户。
  4. Sysvar 账户(Sysvar Accounts

    • 描述:存储网络集群状态的特殊账户。
    • 用途:提供动态更新的网络状态数据。
    • 关键点:包括诸如时钟、租金等集群状态信息。

账户功能

  • 租金(Rent:存储数据的账户需要存入与数据量成比例的 SOL 作为租金,这些 SOL 可以在账户关闭时全额退还。
  • 账户地址:每个账户都有唯一的地址,格式为 Ed25519 公钥,作为账户的唯一标识符。
  • 账户信息(AccountInfo
    • data:存储账户状态的字节数组。
    • executable:标识账户是否为程序的布尔标志。
    • lamports:账户余额,单位为 lamports1 SOL = 10 亿 lamports)。
    • owner:账户的所有者程序的公钥(程序 ID)。

系统程序(System Program

  • 新账户创建:只有系统程序可以创建新账户。
  • 空间分配:设置账户数据字段的字节容量。
  • 分配程序所有权:系统程序创建账户后,可以将所有权转移给其他程序。

BPF 加载器(BPF Loader

  • 描述:负责部署、升级和执行自定义程序的程序。
  • 用途:管理非原生程序的部署和执行。

总结

Solana 的账户模型包括多种账户类型,各自具有不同的功能和用途。

  • 程序账户:用于存储和执行智能合约
  • 数据账户:用于存储程序状态
  • 原生程序账户:提供核心网络功能
  • Sysvar 账户:存储网络状态数据
  • 系统程序:负责新账户的创建和管理
  • BPF 加载器:管理自定义程序的部署和执行。

理解这些账户类型及其作用,有助于更好地进行 Solana 区块链的开发和使用。

鱼雪

欢迎来到 Solana 开发文档!

此页面包含开始 Solana 开发所需的所有信息, 包括基本要求、Solana 开发的工作原理以及您需要的工具。

高级开发概述

在 Solana 上进行开发可以分为两个主要部分

链上程序开发:这是指直接在区块链上创建和部署自定义程序。 部署后,任何知道如何与它们通信的人都可以使用它们。 您可以用 RustCC++ 编写这些程序。 目前,Rust 在链上程序开发方面获得了最多的支持。 客户端开发:这是指编写与链上程序通信的软件(称为去中心化应用程序,或 dApps)。 您的应用程序可以提交交易以在链上执行操作。客户端开发可以用任何编程语言编写。

客户端和链上之间的粘合剂是 Solana JSON RPC API

客户端通过发送 RPC 请求与链上程序交互。这与前端和后端开发非常相似

不同之处在于,Solana 的后端是一个全球无许可的区块链,这意味着任何人都可以与您的链上程序交互, 而无需发放 API 密钥或任何其他形式的权限。

客户端如何与 Solana 区块链工作

Solana 开发与其他区块链有所不同,因为它具有高度可组合的链上程序。

这意味着您可以在任何已部署的程序之上进行构建,通常无需进行任何自定义链上程序开发。

例如,如果您想处理代币,可以使用网络上已经部署的 Token Program。

所有开发工作都将在您选择的客户端语言中进行。

开发人员会发现,Solana 的开发栈与其他开发栈非常相似。

主要区别在于您将与区块链一起工作,并且需要考虑用户如何在链上而不仅仅是在前端与您的应用程序交互。

Solana 开发仍然包括 CI/CD 管道、测试、调试工具、前端和后端,以及任何正常开发流程中的内容

开始所需的工具

开始 Solana 开发所需的工具根据您是在开发客户端、链上程序还是两者都有所不同。

客户端开发

如果您正在开发链上应用程序,您应该了解 Rust。

如果您正在开发客户端,您可以使用任何您熟悉的编程语言。

Solana 有社区贡献的 SDK 帮助开发人员使用大多数流行语言与 Solana 网络交互:

语言SDK
RUSTsolana_sdk
Typescript@solana/web3.js
Pythonsolders
Javasolanaj
C++solcpp
Gosolana-go
KotlinsolanaKT
Dartsolana

您还需要一个与 RPC 的连接来与网络交互。

您可以使用 RPC 基础设施提供商,或者运行自己的 RPC 节点。

要快速开始为您的应用程序创建前端, 可以在 CLI 中输入以下命令生成一个可定制的 Solana scaffold

npx create-solana-dapp <project-name>

这将创建一个包含所有必要文件和基本配置的新项目,以便开始在 Solana 上构建。

scaffold 将包括一个示例前端和一个链上程序模板(如果您选择了一个)。

您可以阅读 create-solana-dapp 文档以了解更多信息。

链上程序开发

链上程序开发包括用 RustCC++ 编写程序。

首先,您需要确保在您的机器上安装了 Rust

您可以使用以下命令安装:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

然后,您需要安装 Solana CLI 以编译和部署您的程序。 您可以通过运行以下命令安装 Solana CLI

sh -c "$(curl -sSfL https://release.solana.com/stable/install)"

使用 Solana CLI,建议运行本地验证器以测试您的程序。

安装 Solana CLI 后,运行以下命令启动本地验证器:

solana-test-validator

这将在您的机器上启动一个本地验证器,您可以用它来测试您的程序。 您可以在本指南中阅读更多关于本地开发的信息。

在构建链上程序时,您可以选择使用原生 Rust(即不使用框架)或使用 Anchor 框架。

Anchor 是一个框架,通过为开发人员提供更高级别的 API,使在 Solana 上构建变得更容易。

可以将 Anchor 类比为用 React 构建网站,而不是直接使用 JavaScript 和 HTML。

虽然 JavaScript 和 HTML 为您提供了对网站的更多控制, 但 React 加速了您的开发过程并使开发变得容易。 您可以在 Anchor 的网站上阅读更多信息。

您需要一种方法来测试您的程序。根据您的语言偏好,有几种不同的方法来测试您的程序:

  • solana-program-test - 用 Rust 构建的测试框架
  • solana-bankrun - 用于编写 TypeScript 测试的测试框架
  • bankrun - 用于编写 Python 测试的测试框架

如果您不想在本地开发您的程序,还有在线 IDE Solana Playground。 Solana Playground 允许您编写、测试和部署程序。 您可以按照我们的指南开始使用 Solana Playground。

开发环境

选择正确的环境非常重要。 在 Solana 上,有几个不同的网络环境(称为集群)来促进成熟的测试和 CI/CD 实践:

  • Mainnet Beta:生产网络,所有活动都在这里发生。交易在这里需要真实的资金。
  • Devnet:质量保证网络,您可以在部署到生产环境之前在这里测试您的程序。相当于“预生产环境”。
  • Local:您在机器上运行的本地网络,使用 solana-test-validator 测试您的程序。 这应该是您开发程序时的首选。

示例构建

在您开始在 Solana 上构建时,还有一些资源可以帮助加速您的旅程:

  • Solana Cookbook:一个参考和代码片段集合,帮助您在 Solana 上构建。
  • Solana Program Examples:一个示例程序的仓库,提供不同操作的构建模块。
  • Guides:教程和指南,带您逐步在 Solana 上构建。

获取支持

您可以在 Solana StackExchange 上找到最好的支持。 首先在那搜索您的问题——很可能已经有人问过类似的问题,并且有答案。 如果没有,请添加一个新问题!记得在您的问题中包含尽可能多的细节, 并使用文本(而不是截图)显示错误消息,以便有相同问题的其他人可以找到您的问题!

鱼雪

在 Solana 上,我们通过发送交易来与网络交互

交易包含一个或多个指令,每个指令代表一个特定的操作

指令的执行逻辑存储在部署到 Solana 网络的程序中

以下是交易执行的关键细节:

交易执行顺序与原子性

  • 执行顺序:如果一个交易包含多个指令,这些指令会按照它们在交易中的顺序依次处理
  • 原子性:交易是原子的,要么所有指令全部成功执行,要么全部失败。 如果任何一个指令失败,整个交易都不会执行

可以简单地将交易看作是处理一个或多个指令的请求

交易中的关键点

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

基本示例

下面是一个包含单个指令的交易的示意图,该指令用于将 SOL 从发送方转移到接收方

在 Solana 上,钱包是由系统程序拥有的账户

根据 Solana 账户模型,只有拥有账户的程序才可以修改账户上的数据。

因此,从“钱包”账户转移 SOL 需要发送一个交易,以调用系统程序上的转移指令

SOL转移

发送方账户必须包含在交易的签名者(is_signer)中,以批准扣除其lamport余额。

发送方和接收方账户都必须是可变的(is_writable), 因为该指令会修改两个账户的lamport余额。

一旦交易发送,系统程序将被调用来处理转账指令。 然后,系统程序将相应地更新发送方和接收方帐户的lamport余额。

简单的SOL转账

以下是一个 Solana Playground 示例, 演示如何使用 SystemProgram.transfer 方法构建 SOL 转账指令

// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL

// Create a transfer instruction for transferring SOL from wallet_1 to wallet_2
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: receiver.publicKey,
lamports: transferAmount * LAMPORTS_PER_SOL, // Convert transferAmount to lamports
});

// Add the transfer instruction to a new transaction
const transaction = new Transaction().add(transferInstruction);

运行脚本并检查记录在控制台的交易详细信息。在下面的部分中,我们将逐步讲解发生在幕后的细节。

交易

Solana 交易的组成

Solana 交易包括以下部分

  • 签名:交易中包含的一组签名。
  • 消息:要原子处理的指令列表。

交易格式

交易消息的结构包括:

  • 消息头指定签名者和只读账户的数量
  • 账户地址指令所需的账户地址数组
  • 最近区块哈希作为交易时间戳的区块哈希
  • 指令要执行的指令数组

交易消息

交易大小

Solana 网络遵循 1280 字节的最大传输单元(MTU)大小。 去掉必要的头(40 字节用于 IPv68 字节用于分段头),剩余 1232 字节用于数据包数据, 例如序列化的交易。

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

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

交易格式

消息头

消息头指定交易中账户地址数组中账户的权限,由三个 u8 整数组成,分别表示:

  • 交易所需的签名数量
  • 需要签名的只读账户地址数量
  • 不需要签名的只读账户地址数量

消息头

紧凑数组格式

紧凑数组是按以下格式序列化的数组:

  • 数组长度,以紧凑 u16 编码。
  • 数组的各个项目按顺序列出。

紧凑数组

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

账户地址数组

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

该数组以紧凑型的 u16 编码开始,表示账户地址的数量,然后是按账户权限排序的地址。 消息头中的元数据用于确定每个部分中的账户数量

  • 可写且有签名的帐户
  • 只读且有签名的帐户
  • 可写但无签名的帐户
  • 只读且无签名的帐户

紧凑的帐户地址数组

最近的区块哈希

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

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

您可以使用 getLatestBlockhash RPC 方法获取当前的区块哈希和该区块哈希将有效的最后一个区块高度。 这里是在 Solana Playground 上的一个示例。

指令数组

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

与账户地址数组类似,这个紧凑数组以指令数量的紧凑u16编码开头,后跟一组指令。 数组中的每个指令指定以下信息:

  • 程序 ID:标识将处理指令的链上程序。这表示为指向账户地址数组内帐户地址的 u8 索引。
  • 紧凑的帐户地址索引数组:指向指令所需的每个帐户的帐户地址数组的 u8 索引数组。
  • 稀疏的不透明 u8 数据数组:与调用的程序特定的 u8 字节数组。 这些数据指南程序要调用的指令,以及指令所需的任何其他数据(如函数参数)。

紧凑的指令数组

示例交易结构

下面是一个事务结构的示例,包括单个 SOL 转账指令。 它显示消息详细信息,包括头部、帐户密钥、块哈希和指令,以及事务的签名。

  • header:包括用于指定accountKeys数组中的读/写和签署者权限的数据。
  • accountKeys:数组,包括事务中所有指令的帐户地址。
  • recentBlockhash:事务创建时包含的块哈希。
  • instructions:数组,包括事务中的所有指令。 指令中的每个accountprogramIdIndex按索引引用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 结构。
  • 指令数据:一个字节数组,指定要调用程序上的哪个指令处理程序,以及指令处理程序需要的任何其他数据(函数参数)。

交易指令

AccountMeta

对于每个指令所需的帐户,必须指定以下信息: pubkey:帐户的链上地址 is_signer:指定帐户是否作为交易的签名者 is_writable:指定帐户数据是否将被修改

此信息称为AccountMeta。 AccountMeta

通过指定指令所需的所有账户,并指明每个账户是否可写,可以并行处理交易。

例如,两个不包含写入相同状态的任何账户的交易可以同时执行。

示例指令结构

以下是 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 转账指令:

// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL

// Instruction index for the SystemProgram transfer instruction
const transferInstructionIndex = 2;

// Create a buffer for the data to be passed to the transfer instruction
const instructionData = Buffer.alloc(4 + 8); // uint32 + uint64
// Write the instruction index to the buffer
instructionData.writeUInt32LE(transferInstructionIndex, 0);
// Write the transfer amount to the buffer
instructionData.writeBigUInt64LE(BigInt(transferAmount * LAMPORTS_PER_SOL), 4);

// Manually create a transfer instruction for transferring SOL from sender to receiver
const transferInstruction = new TransactionInstruction({
keys: [
{ pubkey: sender.publicKey, isSigner: true, isWritable: true },
{ pubkey: receiver.publicKey, isSigner: false, isWritable: true },
],
programId: SystemProgram.programId,
data: instructionData,
});

// Add the transfer instruction to a new transaction
const transaction = new Transaction().add(transferInstruction);

在幕后,使用SystemProgram.transfer方法的简单示例在功能上等同于上面更冗长的示例。 SystemProgram.transfer方法简单地隐藏了为每个操作所需的指令数据缓冲区和AccountMeta的详细信息。

鱼雪