区块链中文技术社区

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 客户端将完全变为非活动状态

Solana 支持的钱包

Solana 开发入门

Solana 的开发可以分为两个主要部分:

  1. 链上程序开发:在这里您可以创建自定义程序并将其直接部署到区块链。部署后,任何知道如何与它们通信的人都可以使用它们。您可以用 Rust、C 或 C++ 编写这些程序。Rust 目前对链上程序开发的支持最多。
  2. 客户端开发:在这里,您可以编写与链上程序通信的软件(称为去中心化应用程序或 dApp)。您的应用程序可以提交交易以在链上执行操作。客户端开发可以用任何编程语言编写。

客户端和链上端之间的“粘合剂”是 Solana JSON RPC API。客户端向 Solana 网络发送 RPC 请求以与链上程序交互。这与前端和后端之间的正常开发非常相似。使用 Solana 的主要区别在于后端是一条全球无权限区块链。这意味着任何人都可以与您的链上程序交互,而无需颁发 API 密钥或任何其他形式的权限。

客户端SDK

Language SDK
RUST solana_sdk
Typescript @solana/web3.js
Python solders
Java solanaj or solana4j
C++ solcpp
Go solana-go
Kotlin solanaKT or sol4k
Dart solana
C# solnet
GdScript godot

您还需要与 RPC 建立连接才能与网络交互。您可以与RPC 基础设施提供商合作,也 可以运行自己的 RPC 节点。

脚手架

为了快速开始使用应用程序的前端,您可以通过在 CLI 中输入以下内容来生成可定制的 Solana 脚手架:

npx create-solana-dapp <project-name>

这将创建一个新项目,其中包含开始在 Solana 上构建所需的所有文件和基本配置。脚手架将包含一个示例前端和一个链上程序模板(如果您选择了一个)。您可以阅读文档 create-solana-dapp以 了解更多信息。

测试框架

测试实例

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

  • Solana Cookbook:参考资料和代码片段的集合,可帮助您在 Solana 上进行构建。
  • Solana 程序示例:示例程序库,为程序上的不同操作提供构建块。
  • 指南:教程和指南引导您在 Solana 上进行构建。

获取支持

https://solana.stackexchange.com/