4_Solana程序

Solana程序(智能合约)

https://solana.com/docs/core/programs

  • 在solana中“智能合约”被称为“程序”(program)
  • 每个程序是一个链上的账户, 该账户存储了可执行的代码(指令)

关键点:

  • Solana程序是一个包含了可执行代码的链上账户, 代码中包含了不同的函数, 即指令

  • 程序是无状态的,但是可以包含创建新账户指令,这个新账户可以用来存储和管理程序状态(即数据账户)

  • 程序可以被升级,仅限拥有可升级权限的账户可以升级程序。如果一个程序的升级权限设置为null, 那么这个程序就不能再升级了。

  • Verifiable builds enable users to verify that onchain programs match the publicly available source code.

编写Solana程序

更新Solana程序

https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/programs/bpf_loader/src/lib.rs#L675

3_Solana的手续费

Solana手续费

https://solana.com/docs/core/fees#compute-unit-limit

  • Transaction Fees(base fee) : 交易费
  • Prioritization Fees : 可选的,加速交易的手续费
    • 给矿工
  • Rent: 账户租金(充值)
  • 计算单元(CU)价格: 如果需要加速交易,需要设置 compute unit price, 并且设置compute unit limit, 这2个参数用来决定交易的 priority fee

  • Prioritization fee 的计算取决于:

    • SetComputeUnitLimit : 设置交易最大能够消耗的CU
    • SetComputeUnitPrice : CU价格,来加速
      • 如果不提供此值, 则交易无priority fee
  • 如何设置 prioritization fee

2_Solana交易和指令

Solana的交易和指令

https://solana.com/docs/core/transactions

关键细节:

  • 执行顺序: 如果交易包含多个指令,按照顺序执行(指令添加到交易中的顺序)
  • 原子性: 交易是原子性,只有当全部指令都执行成功,交易才成功,否则交易执行失败

关键点:

  • 交易由不同指令组成,这些指令用来与链上不同的程序进行交互, 不同的指令代表不同的操作
  • 每个指令指定3个要素, 见下文的CompiledInstruction结构体:
    • 程序id索引
    • 账户列表, 即指令所涉及的账户
    • 输入数据
  • 交易中的指令,按照顺序执行
  • 交易是原子性的
  • 一笔交易最大为1232 bytes

Transaction

  • recent_blockhash用作交易的时间戳, 交易最大的age是 150 区块 (约1分钟),超过150区块就视为过期, 过期交易将不能执行
    • 可以通过getLatestBlockHash获取最新区块hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/sdk/src/transaction/mod.rs#L173
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,
}

Message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
pub struct Message {
/// The message header, identifying signed and read-only `account_keys`.
/// Header values only describe static `account_keys`, they do not describe
/// any additional account keys loaded via address table lookups.
pub header: MessageHeader,

// 所有的需要使用到的账户数组
/// List of accounts loaded by this transaction.
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,

// 用做交易的时间戳,也用于防止重复交易和过期交易
// 交易最大的age是 150 区块 (约1分钟)
/// The blockhash of a recent block.
pub recent_blockhash: Hash,


// 指令合集
/// Instructions that invoke a designated program, are executed in sequence,
/// and committed in one atomic transaction if all succeed.
///
/// # Notes
///
/// Program indexes must index into the list of message `account_keys` because
/// program id's cannot be dynamically loaded from a lookup table.
///
/// Account indexes must index into the list of addresses
/// constructed from the concatenation of three key lists:
/// 1) message `account_keys`
/// 2) ordered list of keys loaded from `writable` lookup table indexes
/// 3) ordered list of keys loaded from `readable` lookup table indexes
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,

/// List of address table lookups used to load additional accounts
/// for this transaction.
#[serde(with = "short_vec")]
pub address_table_lookups: Vec<MessageAddressTableLookup>,
}

pub enum VersionedMessage {
Legacy(LegacyMessage),
V0(v0::Message),
}

pub struct VersionedTransaction {
/// List of signatures
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
/// Message to sign.
pub message: VersionedMessage,
}

// 消息头
// https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/sdk/program/src/message/mod.rs#L96
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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 交易中的结构是 CompiledInstruction
pub struct CompiledInstruction {
// 索引
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,

// 需要和合约交互账户
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,

// 输入数据
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

// Instruction 是底层的数据结构
// https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/sdk/program/src/instruction.rs#L329
pub struct Instruction {
/// Pubkey of the program that executes this instruction.
#[wasm_bindgen(skip)]
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program.
#[wasm_bindgen(skip)]
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation.
#[wasm_bindgen(skip)]
pub data: Vec<u8>,
}

AccountMeta

1
2
3
4
5
6
7
8
9
// https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/sdk/program/src/instruction.rs#L539
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的交易示例图:

  • 结构图

  • SOL转账交易执行流程:


交易确认&过期

https://solana.com/docs/advanced/confirmation

  • 过期时间 151个区块, 每个区块400ms, 即 60s
  • 交易中的recentBlockHash必须是151区块内, 否则将是过期交易

一些建议

  • 建议1: 调用getLatestBlockhash时, 推荐使用 confired

    • proceeded: 已处理, 很激进,速度最快,但有可能被跳过
      • 用这种级别的交易中,有约5%交易会被验证节点丢弃
    • confirmed: 已被多个验证节点确认, 折衷方案
      • 这个级别,比较稳妥,因为被多个节点确认,之后被丢弃的几率很小
    • finalized: 最终确认, 太保守, 交易不会被丢弃
      • 32个slot确认, 需要12.8s
  • 建议2:在sendTransactionsimulateTransaction时使用, 要设置相同的 preflightCommitment, 即都设置 confirmed

  • 建议3:使用可靠的RPC节点,不要用落后的RPC节点

  • 建议4:不要用过期的blockhash, 而是在签名前实时获取最新的blockHash

  • 前端应用要一直轮询最新的区块hash, 确保用户在触发交易时,获取的区块是最新的

  • 钱包要一直轮询最新的区块hash, 并刷新交易中的区块hash,确保用户签名时用的是最新的区块hash

  • 建议5:使用健康的RPC节点获取区块hash

  • 其他建议


Solana交易重试

https://solana.com/docs/advanced/retry

  • RPC节点会尝试重新广播
  • 开发者可以实现自定义的重新广播逻辑
  • 开发者可以利用 sendTransactionmaxRetries参数
  • 开发者在提交交易前,应该执行预检(preflight), 如: simulattionTransaction
  • 在对重试交易进行签名交易前,必须确保之前那笔交易中的区块hash已经过期,
    • 否则存在发起2笔交易的风险

交易的流程

Solana没有交易池(mempool, txpool), 所有的交易都会转发给leaders节点执行

Transaction Processing Unit (TPU) 处理交易的阶段:

  • Fetch Stage
  • SigVerify Stage
  • Banking Stage
  • Proof of History Service
  • Broadcast Stage
![](https://raw.githubusercontent.com/youngqqcn/repo4picgo/master/img/rt-tpu-jito-labs.png)

交易被丢弃的几种情况

  • 第1种: 开发者引用过期区块hash, 提交交易时被RPC pool丢弃 ,这种是最常见的
  • 第2种: 临时分叉, 引用了被丢弃的分叉区块hash
  • 第3种: 临时分叉, 引用了被丢弃的分叉区块hash

https://solana.com/docs/advanced/lookup-tables

Versioned Transaction

Solana有2种不同的交易类型:

  • legacy: older transaction format with no additional benefit
  • 0: added support for Address Lookup Tables

具体例子:https://www.solanazh.com/course/7-1

Address Lookup Tables

https://solana.com/docs/advanced/lookup-tables

  • 每个交易最大1232字节,因此,每笔普通交易(legacy)最多包含32个地址(每个地址32字节)

  • 使用 Versioned Transaction 和 Address Lookup Tables, 可以将每笔交易能包含的地址提升到 256个地址

  • 地址压缩: 在所有地址都存在链上之后, 每个地址(32字节)只需用一个索引(1字节)进行地址定位即可

    • 先把地址存在链上,获得一个lookupTableAccount
    • 然后通过索引来获取lookupTableAccount中的地址

Solana共识

https://solana.com/developers/evm-to-svm/consensus

  • Solana的共识: 基于Tower BFT + PoHPoS
    • PoS是solana的上层出块的共识协议
    • PoS之下是 Tower BFT
      • Tower BFT = PBFT + PoH
      • PoH(Proof of History) : 作为全局网络时钟,以决定区块/交易/数据的顺序, 因此Solana可以快速决定区块/交易/数据的先后顺序,并且验证节点可以快速解决分叉
      • 每个交易包含了recentBlockhash, 即最近的 150个区块内的区块hash, 这个hash用来决定交易执行先后顺序

1_Solana账户模型

深入理解 Solana 账户模型

  • 官方文档(推荐):https://solana.com/docs/core/accounts

  • 账户模型:

  • 不同于以太坊中只有智能合约可以存储状态, solana中所有账户都可以存储状态(数据)

  • solana的智能合约(可执行账户)仅存储程序代码(不可变), 不存储状态

  • solana中的智能合约(可执行账户)的状态存储在其他账户(不可执行,但可变)中

    • 这些存储状态的账户(数据账户),其owner是程序(可执行账户)
  • solana中每个账户有一个owner,仅owner可以修改账户状态

  • solana提供了很多有用的系统程序(合约), 属于runtime运行时

    • https://docs.solanalabs.com/runtime/programs
    • System Program:
      • 功能:
        • 创建新账户, 只有 System Program 可以创建新账户
        • 分配新账户的权限,一旦创建新账户,就可以转移账户权限给其他程序

          为自定义程序创建一个数据账户(Data Account),可以分为2步:

          • 1, 调用 System Program 创建一个账户,然后将权限转移给自定义程序
          • 2, 调用自定义程序(此时是账户的owner)初始化该账户的数据
        • 分配数据空间
        • 转移普通账户(owner是 System Program)的余额
        • 仅owner是 System Program 可以支付手续费
      • Program id: 11111111111111111111111111111111
      • Instructions: SystemInstruction
    • BPF Loader Program
      • 功能:
        • 是所有自定义程序的owner
        • Deploys, upgrades, and executes programs on the chain.
      • Program id: BPFLoaderUpgradeab1e11111111111111111111111
      • Instructions: LoaderInstruction
    • SPL Token
      • TODO :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pub struct Account {
/// 账户余额
/// lamports in the account
pub lamports: u64,


// 合约数据
/// data held in this account
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,

// 所有者:
// on-chain program
// 可以写入
// 可花费lanport
/// the program that owns this account. If executable, the program that loads this account.
// This field stores the address of an on-chain program and represents which on-chain program is allowed to write to the account’s data and subtract from its lamport balance.
pub owner: Pubkey,

// 是否可执行
/// this account's data contains a loaded program (and is now read-only)
pub executable: bool,


/// the epoch at which this account will next owe rent
pub rent_epoch: Epoch,
}

程序账户(Program Account)

简化版如下:

数据账户(Data Account)

为自定义程序创建一个数据账户(Data Account),可以分为2步:

  • 1, 调用 System Program 创建一个账户,然后将权限转移给自定义程序
  • 2, 调用自定义程序(此时是账户的owner)初始化该账户的数据

Solana账户规则:

https://solana.wiki/docs/solidity-guide/accounts/#solana-runtime-account-rules

  • 不可变性:

    • 可执行账户完全不可变
  • 数据分配

    • System Program 可以更改账户数据大小
    • 新分配的账户数据总是归零的
    • 账户数据大小不可缩小

      在写入期间,程序不能增加其拥有的账户数据大小, 如果需要更多数据,必须将数据拷贝到更大账户中,因此,程序不会在账户中存储动态大小的maps和数组,而是,将数据存储在多个账户中

  • 数据

    • 每个账户最多 10MB 数据(代码 或 状态)
    • 只有账户的owner才可以修改数据
    • 账户只有处于数据归零状态下才可以分配新的owner
  • 余额

    • 只有账户的owner可以减少余额
    • 任何程序账户都可以账户增加余额(转移)

      如果一个账户的owner是程序,那么,不能通过私钥操作该账户的余额,因为,私钥账户(普通账户)的owner是System Program, 而System Program 不是该账户的owner, 因此就不能操作该账户的余额

  • 所有权

    • 只有账户owner可以制定新的账户owner
  • 租金

    • 租金每2天(1个epoch)更新一次,由账户大小决定
    • 如果账户的余额大于2年的租金(预存), 那么,该账户可以免除租金(不用交房租)
  • 余额为0的账户

    • 余额为0的账户,在交易执行后会被系统删除
    • 一个交易中可以创建临时余额为0的账户
  • 新的执行账户

    • 只有制定的loader program可以修改账户的可执行状态

0_Solana开发资源

Solana开发资源

https://www.notion.so/Solana-fca856aad4e5441f80f28cc4e015ca98
https://github.com/CreatorsDAO/solana-co-learn/blob/main/docs/awesome-solana-zh/README.mdx

开源 Solana DEX

Solana节点搭建

RPC节点最低配置:

  • CPU: 32核+

  • RAM: 512GB+

  • Disk: 2TB+

  • Network: 10GBit/s+

  • 预估服务器费用(年): 13万RMB左右

使用 solana-keygen 生成与 Phantom一致的地址

1
solana-keygen new --word-count 12 --no-bip39-passphrase --derivation-path "m/44'/501'/0'/0'" --outfile ./mynew-address.json
  • Copyrights © 2021-2024 youngqqcn

请我喝杯咖啡吧~