分类 Solana-新手教程 下的文章

传统 Solana 程序与 Anchor 程序的区别


在 Anchor 框架创建的 Solana 程序里,你看不到 process_instruction 函数,这是因为 Anchor 框架做了抽象和封装,把底层的 Solana 程序处理逻辑简化了。下面详细解释:

1. 传统 Solana 程序与 Anchor 程序的区别

  • 传统 Solana 程序: 在传统的 Solana 程序开发里,你得手动实现 process_instruction 函数。这个函数是程序的入口点,负责接收和处理交易指令。示例如下:
use solana_program::{
    account_info::AccountInfo, entrypoint, 
    entrypoint::ProgramResult, 
    pubkey::Pubkey,
};

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    // 处理指令逻辑
    Ok(())
}
  • Anchor 程序: Anchor 框架是一个高级的 Solana 程序开发框架,它隐藏了很多底层细节。Anchor 借助宏和自定义语法来定义程序逻辑,而不是直接实现 process_instruction 函数。

2. Anchor 程序的结构

在 Anchor 里,你通常会定义 #[program] 模块,在这个模块中定义指令处理函数。示例如下:

// lib.rs

use anchor_lang::prelude::*;

declare_id!
("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsL
nS");

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

    pub fn initialize(ctx: 
    Context<Initialize>) -> Result<()> {
        // 初始化逻辑
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    // 账户声明
}

在这个示例中,initialize 函数就是一个指令处理函数,它会在接收到相应的交易指令时被调用。Anchor 会自动处理 process_instruction 函数的实现,把交易指令路由到对应的处理函数。

3. 查看 Anchor 生成的 process_instruction

如果你想查看 Anchor 生成的 process_instruction 函数,可以编译程序,然后查看生成的 Rust 代码。执行以下命令编译程序:

anchor build

编译完成后,在 target/idl 目录下可以找到生成的 IDL(Interface Definition Language)文件,在 target/deploy 目录下可以找到生成的 Rust 代码。这些生成的代码包含了 process_instruction 函数的实现。

总结来说,Anchor 框架通过抽象和封装,让开发者不用直接处理 process_instruction 函数,从而提高了开发效率。

其它

Solana 传统程序: https://solana.com/zh/developers/guides/getstarted/local-rust-hello-world


Solana system程序功能分析


代码分析

版本:v2.0.14
核心代码:https://github.com/anza-xyz/agave/blob/v2.0.14/sdk/program/src/system_instruction.rs

SystemError

system程序调用中,常见的错误

错误类型 错误解释
AccountAlreadyInUse 具有相同地址的帐户已存在
ResultWithNegativeLamports 帐户没有足够的 SOL 来执行操作
InvalidProgramId 无法将帐户分配给此程序 ID
InvalidAccountDataLength 无法分配此长度的帐户数据
MaxSeedLengthExceeded 请求的Seed长度太长
AddressWithSeedMismatch 提供的地址与Seed得出的地址不匹配
NonceNoRecentBlockhashes 推进存储的随机数需要填充的RecentBlockhashes sysvar
NonceBlockhashNotExpired 存储的 nonce 仍在 recent_blockhashes 中
NonceUnexpectedBlockhashValue 指定的 nonce 与存储的 nonce 不匹配

固定变量

变量名 解释 默认值
MAX_PERMITTED_DATA_LENGTH 允许的最大帐户数据大小 10 MiB
MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION 每笔交易允许的新分配的最大大小,以字节为单位 MAX_PERMITTED_DATA_LENGTH x 2

数据结构

系统程序的指令和构造函数。
系统程序负责创建帐户和随机数帐户。它负责从系统程序拥有的帐户(包括典型的用户钱包帐户)转移 Lamport。
帐户创建通常涉及三个步骤:分配空间、转移 Lamport 以进行出租、分配给其拥有程序。create_account 函数一次完成所有三个步骤。所有新帐户必须包含足够的 Lamport 才能免除租金,否则创建指令将失败。
系统程序创建的帐户可以是用户控制的,其中密钥保存在区块链之外,也可以是程序派生的地址,其中拥有程序授予对帐户的写访问权限。
系统程序 ID 在 system_program 中定义。
此模块中的大多数函数都构造一个指令,必须将其提交给运行时执行,可以通过 RPC(通常使用 RpcClient)或通过跨程序调用。
通过 CPI 调用时,invoke 或invoke_signed 指令要求所有帐户引用都明确提供为 AccountInfo 值。所需的帐户引用在每个系统程序指令的 SystemInstruction 变体的文档中指定,并且这些变体与其构造函数的文档链接在一起。

pub enum SystemInstruction {
    /// 创建新账户
    /// # 账户引用
    /// 0. `[WRITE, SIGNER]` 资金账户
    /// 1. `[WRITE, SIGNER]` 新账户
    CreateAccount {
        /// 要转移到新帐户的lamports数量
        lamports: u64,

        /// 要分配的内存字节数
        space: u64,

        /// 所有者程序帐户地址
        owner: Pubkey,
    },

    /// 将账户分配给程序
    /// # 账户引用
    /// 0. `[WRITE, SIGNER]` 分配账户公钥
    Assign {
        /// 程序拥有者账户地址
        owner: Pubkey,
    },

    /// 转账 lampors
    ///
    /// # 账户引用
    /// 0. `[WRITE, SIGNER]` 资金账户
    /// 1. `[WRITE]` 收款账户
    Transfer { lamports: u64 },

    /// 在从基本公钥和种子派生的地址上创建新帐户
    ///
    /// # 帐户引用
    /// 0. `[WRITE, SIGNER]` 资金帐户
    /// 1. `[WRITE]` 创建帐户
    /// 2. `[SIGNER]` (可选) 基本帐户;必须将与以下基本公钥匹配的帐户
    /// 作为签名者提供,但可以与资金帐户相同
    /// 并作为帐户 0 提供
    CreateAccountWithSeed {
        /// 基本公钥
        base: Pubkey,

        /// ASCII 字符的字符串,不超过“Pubkey::MAX_SEED_LEN”
        seed: String,

        /// 要转移到新帐户的lamports数量
        lamports: u64,

        /// 要分配的内存字节数
        space: u64,

        /// 所有者程序帐户地址
        owner: Pubkey,
    },

    /// 使用存储的随机数,用后继者替换它
    ///
    /// # 帐户引用
    /// 0. `[WRITE]` 随机数帐户
    /// 1. `[]` 最近区块哈希系统变量
    /// 2. `[SIGNER]` 随机数权限
    AdvanceNonceAccount,

    /// 从 nonce 账户中提取资金
    ///
    /// # 账户引用
    /// 0. `[WRITE]` Nonce 账户
    /// 1. `[WRITE]` 接收者账户
    /// 2. `[]` RecentBlockhashes 系统变量
    /// 3. `[]` Rent 系统变量
    /// 4. `[SIGNER]` Nonce 权限
    ///
    /// `u64` 参数是要提取的 lampor,它必须使
    /// 账户余额高于免租储备或为零。
    WithdrawNonceAccount(u64),

    /// 将 Uninitialized nonce 账户的状态驱动为 Initialized,设置 nonce 值
    ///
    /// # 账户引用
    /// 0. `[WRITE]` Nonce 账户
    /// 1. `[]` RecentBlockhashes 系统变量
    /// 2. `[]` Rent 系统变量
    ///
    /// `Pubkey` 参数指定有权在账户上执行 nonce 指令的实体
    ///
    /// 执行此指令不需要签名,从而可以派生
    /// nonce 账户地址
    InitializeNonceAccount(Pubkey),

    /// 更改授权在账户上执行 nonce 指令的实体
    ///
    /// # 账户引用
    /// 0. `[WRITE]` Nonce 账户
    /// 1. `[SIGNER]` Nonce 授权
    ///
    /// `Pubkey` 参数标识要授权的实体
    AuthorizeNonceAccount(Pubkey),

    /// 在(可能是新的)账户中分配空间,无需资金
    ///
    /// # 账户引用
    /// 0. `[WRITE, SIGNER]` 新账户
    Allocate {
        /// 要分配的内存字节数
        space: u64,
    },

    /// 为某个地址分配空间并指定一个账户
    /// 源自一个基本公钥和一个种子
    ///
    /// # 账户引用
    /// 0. `[WRITE]` 已分配账户
    /// 1. `[SIGNER]` 基本账户
    AllocateWithSeed {
        /// 基本公钥
        base: Pubkey,

        /// ASCII 字符的字符串,不超过“pubkey::MAX_SEED_LEN”
        seed: String,

        /// 要分配的内存字节数
        space: u64,

        /// 所有者程序帐户地址
        owner: Pubkey,
    },

    /// 根据种子将账户分配给程序
    ///
    /// # 账户引用
    /// 0. `[WRITE]` 分配账户
    /// 1. `[SIGNER]` 基本账户
    AssignWithSeed {
        /// 基本公钥
        base: Pubkey,

        /// ASCII 字符的字符串,不超过“pubkey::MAX_SEED_LEN”
        seed: String,

        /// 所有者程序帐户地址
        owner: Pubkey,
    },

    /// 从派生地址转移 lamport
    ///
    /// # 账户引用
    /// 0. `[WRITE]` 资金账户
    /// 1. `[SIGNER]` 资金账户基础
    /// 2. `[WRITE]` 接收账户
    TransferWithSeed {
        /// 转账金额
        lamports: u64,

        /// 用于获取资金账户地址的Seed
        from_seed: String,

        /// 所有者用来获取资金账户地址
        from_owner: Pubkey,
    },

    /// 对旧版 nonce 版本进行一次性幂等升级,以便将它们从链块哈希域中剔除。
    ///
    /// # 帐户引用
    /// 0. `[WRITE]` Nonce 帐户
    UpgradeNonceAccount,
}

Create an account

此函数生成一个指令,该指令必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::CreateAccount。
帐户创建通常涉及三个步骤:分配空间、转移 Lamport 以进行租赁、分配给其拥有程序。create_account 函数一次完成所有三个步骤。

必需的签名者

from_pubkey 和 to_pubkey 签名者必须签署交易。

示例

这些示例使用 SystemInstruction::CreateAccount 的单次调用来创建新帐户、分配一些空间、向其转移最低 Lamport 以进行租赁豁免,并将其分配给系统程序,

示例:客户端 RPC

此示例从 RPC 客户端提交指令。付款人和 new_account 是签名者。

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    system_program,
    transaction::Transaction,
};
use anyhow::Result;

fn create_account(
    client: &RpcClient,
    payer: &Keypair,
    new_account: &Keypair,
    space: u64,
) -> Result<()> {
    let rent = client. get_minimum_balance_for_rent_exemption(space. try_into()?)?;
    let instr = system_instruction::create_account(
        &payer. pubkey(),
        &new_account. pubkey(),
        rent,
        space,
        &system_program::ID,
    );

    let blockhash = client. get_latest_blockhash()?;
    let tx = Transaction::new_signed_with_payer(
        &[instr],
        Some(&payer. pubkey()),
        &[payer, new_account],
        blockhash,
    );

    let _sig = client. send_and_confirm_transaction(&tx)?;

    Ok(())
}
示例:链上程序

此示例从链上 Solana 程序提交指令。创建的帐户是程序派生的地址。付款人和 new_account_pda 是签名者,其中 new_account_pda 由程序本身通过invoke_signed 虚拟签名,付款人由提交交易的客户端签名。

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke_signed,
    pubkey::Pubkey,
    system_instruction,
    system_program,
    sysvar::rent::Rent,
    sysvar::Sysvar,
};

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CreateAccountInstruction {
    /// The PDA seed used to distinguish the new account from other PDAs
    pub new_account_seed: [u8; 16],
    /// The PDA bump seed
    pub new_account_bump_seed: u8,
    /// The amount of space to allocate for `new_account_pda`
    pub space: u64,
}

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instr = CreateAccountInstruction::deserialize(&mut &instruction_data[..])?;

    let account_info_iter = &mut accounts. iter();

    let payer = next_account_info(account_info_iter)?;
    let new_account_pda = next_account_info(account_info_iter)?;
    let system_account = next_account_info(account_info_iter)?;

    assert!(payer. is_signer);
    assert!(payer. is_writable);
    // Note that `new_account_pda` is not a signer yet.
    // This program will sign for it via `invoke_signed`.
    assert!(!new_account_pda. is_signer);
    assert!(new_account_pda. is_writable);
    assert!(system_program::check_id(system_account. key));

    let new_account_seed = &instr. new_account_seed;
    let new_account_bump_seed = instr. new_account_bump_seed;

    let rent = Rent::get()?
        .minimum_balance(instr. space. try_into().expect("overflow"));

    invoke_signed(
        &system_instruction::create_account(
            payer. key,
            new_account_pda. key,
            rent,
            instr. space,
            &system_program::ID
        ),
        &[payer. clone(), new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    Ok(())
}
程序代码
pub fn create_account(
    from_pubkey: &Pubkey,
    to_pubkey: &Pubkey,
    lamports: u64,
    space: u64,
    owner: &Pubkey,
) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*from_pubkey, true),
        AccountMeta::new(*to_pubkey, true),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::CreateAccount {
            lamports,
            space,
            owner: *owner,
        },
        account_metas,
    )
}

// we accept `to` as a parameter so that callers do their own error handling when
//   calling create_with_seed()
pub fn create_account_with_seed(
    from_pubkey: &Pubkey,
    to_pubkey: &Pubkey, // must match create_with_seed(base, seed, owner)
    base: &Pubkey,
    seed: &str,
    lamports: u64,
    space: u64,
    owner: &Pubkey,
) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*from_pubkey, true),
        AccountMeta::new(*to_pubkey, false),
        AccountMeta::new_readonly(*base, true),
    ];

    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::CreateAccountWithSeed {
            base: *base,
            seed: seed.to_string(),
            lamports,
            space,
            owner: *owner,
        },
        account_metas,
    )
}

从系统程序分配帐户所有权。

此函数生成一个指令,该指令必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::Assign。
必需的签名者公钥签名者必须签署交易。

示例

这些示例为帐户分配空间,向其转移最低余额以免除租金,并将帐户分配给程序。

示例:客户端 RPC

此示例从 RPC 客户端提交指令。它将帐户分配给提供的程序帐户。付款人和新帐户是签名者。

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use anyhow::Result;

fn create_account(
    client: &RpcClient,
    payer: &Keypair,
    new_account: &Keypair,
    owning_program: &Pubkey,
    space: u64,
) -> Result<()> {
    let rent = client. get_minimum_balance_for_rent_exemption(space. try_into()?)?;

    let transfer_instr = system_instruction::transfer(
        &payer. pubkey(),
        &new_account. pubkey(),
        rent,
    );

    let allocate_instr = system_instruction::allocate(
        &new_account. pubkey(),
        space,
    );

    let assign_instr = system_instruction::assign(
        &new_account. pubkey(),
        owning_program,
    );

    let blockhash = client. get_latest_blockhash()?;
    let tx = Transaction::new_signed_with_payer(
        &[transfer_instr, allocate_instr, assign_instr],
        Some(&payer. pubkey()),
        &[payer, new_account],
        blockhash,
    );

    let _sig = client. send_and_confirm_transaction(&tx)?;

    Ok(())
}
示例:链上程序

此示例提交来自链上 Solana 程序的指令。创建的帐户是程序派生的地址,由付款人提供资金,并分配给正在运行的程序。付款人和 new_account_pda 是签名者,其中 new_account_pda 由程序本身通过invoke_signed 虚拟签名,付款人由提交交易的客户端签名。

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke_signed,
    pubkey::Pubkey,
    system_instruction,
    system_program,
    sysvar::rent::Rent,
    sysvar::Sysvar,
};

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CreateAccountInstruction {
    /// The PDA seed used to distinguish the new account from other PDAs
    pub new_account_seed: [u8; 16],
    /// The PDA bump seed
    pub new_account_bump_seed: u8,
    /// The amount of space to allocate for `new_account_pda`
    pub space: u64,
}

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instr = CreateAccountInstruction::deserialize(&mut &instruction_data[..])?;

    let account_info_iter = &mut accounts. iter();

    let payer = next_account_info(account_info_iter)?;
    let new_account_pda = next_account_info(account_info_iter)?;
    let system_account = next_account_info(account_info_iter)?;

    assert!(payer. is_signer);
    assert!(payer. is_writable);
    // Note that `new_account_pda` is not a signer yet.
    // This program will sign for it via `invoke_signed`.
    assert!(!new_account_pda. is_signer);
    assert!(new_account_pda. is_writable);
    assert!(system_program::check_id(system_account. key));

    let new_account_seed = &instr. new_account_seed;
    let new_account_bump_seed = instr. new_account_bump_seed;

    let rent = Rent::get()?
        .minimum_balance(instr. space. try_into().expect("overflow"));

    invoke_signed(
        &system_instruction::transfer(
            payer. key,
            new_account_pda. key,
            rent,
        ),
        &[payer. clone(), new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    invoke_signed(
        &system_instruction::allocate(
            new_account_pda. key,
            instr. space,
        ),
        &[new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    invoke_signed(
        &system_instruction::assign(
            new_account_pda. key,
            &program_id,
        ),
        &[new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    Ok(())
}

程序代码

pub fn assign(pubkey: &Pubkey, owner: &Pubkey) -> Instruction {
    let account_metas = vec![AccountMeta::new(*pubkey, true)];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::Assign { owner: *owner },
        account_metas,
    )
}

pub fn assign_with_seed(
    address: &Pubkey, // must match create_with_seed(base, seed, owner)
    base: &Pubkey,
    seed: &str,
    owner: &Pubkey,
) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*address, false),
        AccountMeta::new_readonly(*base, true),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::AssignWithSeed {
            base: *base,
            seed: seed.to_string(),
            owner: *owner,
        },
        account_metas,
    )
}

从系统程序拥有的帐户转移 lamport。

此函数生成一个指令,该指令必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::Transfer。
必需的签名者from_pubkey 签名者必须签署交易。

示例

这些示例为帐户分配空间,向其转移最低余额以免除租金,并将帐户分配给程序。

示例:客户端 RPC

此示例从 RPC 客户端提交指令。它将帐户分配给提供的程序帐户。付款人和 new_account 是签名者。

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use anyhow::Result;

fn create_account(
    client: &RpcClient,
    payer: &Keypair,
    new_account: &Keypair,
    owning_program: &Pubkey,
    space: u64,
) -> Result<()> {
    let rent = client.get_minimum_balance_for_rent_exemption(space. try_into()?)?;

    let transfer_instr = system_instruction::transfer(
        &payer. pubkey(),
        &new_account. pubkey(),
        rent,
    );

    let allocate_instr = system_instruction::allocate(
        &new_account. pubkey(),
        space,
    );

    let assign_instr = system_instruction::assign(
        &new_account. pubkey(),
        owning_program,
    );

    let blockhash = client. get_latest_blockhash()?;
    let tx = Transaction::new_signed_with_payer(
        &[transfer_instr, allocate_instr, assign_instr],
        Some(&payer. pubkey()),
        &[payer, new_account],
        blockhash,
    );

    let _sig = client.send_and_confirm_transaction(&tx)?;

    Ok(())
}
示例:链上程序

此示例提交来自链上 Solana 程序的指令。创建的帐户是程序派生的地址,由付款人提供资金,并分配给正在运行的程序。付款人和 new_account_pda 是签名者,其中 new_account_pda 由程序本身通过invoke_signed 虚拟签名,付款人由提交交易的客户端签名。

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke_signed,
    pubkey::Pubkey,
    system_instruction,
    system_program,
    sysvar::rent::Rent,
    sysvar::Sysvar,
};

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CreateAccountInstruction {
    /// The PDA seed used to distinguish the new account from other PDAs
    pub new_account_seed: [u8; 16],
    /// The PDA bump seed
    pub new_account_bump_seed: u8,
    /// The amount of space to allocate for `new_account_pda`
    pub space: u64,
}

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instr = CreateAccountInstruction::deserialize(&mut &instruction_data[..])?;

    let account_info_iter = &mut accounts. iter();

    let payer = next_account_info(account_info_iter)?;
    let new_account_pda = next_account_info(account_info_iter)?;
    let system_account = next_account_info(account_info_iter)?;

    assert!(payer. is_signer);
    assert!(payer. is_writable);
    // Note that `new_account_pda` is not a signer yet.
    // This program will sign for it via `invoke_signed`.
    assert!(!new_account_pda. is_signer);
    assert!(new_account_pda. is_writable);
    assert!(system_program::check_id(system_account. key));

    let new_account_seed = &instr. new_account_seed;
    let new_account_bump_seed = instr. new_account_bump_seed;

    let rent = Rent::get()?
        .minimum_balance(instr. space. try_into().expect("overflow"));

    invoke_signed(
        &system_instruction::transfer(
            payer. key,
            new_account_pda. key,
            rent,
        ),
        &[payer. clone(), new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    invoke_signed(
        &system_instruction::allocate(
            new_account_pda. key,
            instr. space,
        ),
        &[new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    invoke_signed(
        &system_instruction::assign(
            new_account_pda. key,
            &program_id,
        ),
        &[new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    Ok(())
}

程序代码

pub fn transfer(from_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*from_pubkey, true),
        AccountMeta::new(*to_pubkey, false),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::Transfer { lamports },
        account_metas,
    )
}

pub fn transfer_with_seed(
    from_pubkey: &Pubkey, // must match create_with_seed(base, seed, owner)
    from_base: &Pubkey,
    from_seed: String,
    from_owner: &Pubkey,
    to_pubkey: &Pubkey,
    lamports: u64,
) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*from_pubkey, false),
        AccountMeta::new_readonly(*from_base, true),
        AccountMeta::new(*to_pubkey, false),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::TransferWithSeed {
            lamports,
            from_seed,
            from_owner: *from_owner,
        },
        account_metas,
    )
}

为帐户分配空间。

此函数生成一个指令,该指令必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::Allocate。
如果帐户的大小已大于 0,或者请求的大小大于 MAX_PERMITTED_DATA_LENGTH,则交易将失败。
必需的签名者公钥签名者必须签署交易。

示例

这些示例为帐户分配空间,向其转移租金豁免的最低余额,并将帐户分配给程序。

示例:客户端 RPC

此示例从 RPC 客户端提交指令。它将帐户分配给提供的程序帐户。付款人和新帐户是签名者。

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use anyhow::Result;

fn create_account(
    client: &RpcClient,
    payer: &Keypair,
    new_account: &Keypair,
    owning_program: &Pubkey,
    space: u64,
) -> Result<()> {
    let rent = client. get_minimum_balance_for_rent_exemption(space. try_into()?)?;

    let transfer_instr = system_instruction::transfer(
        &payer. pubkey(),
        &new_account. pubkey(),
        rent,
    );

    let allocate_instr = system_instruction::allocate(
        &new_account. pubkey(),
        space,
    );

    let assign_instr = system_instruction::assign(
        &new_account. pubkey(),
        owning_program,
    );

    let blockhash = client. get_latest_blockhash()?;
    let tx = Transaction::new_signed_with_payer(
        &[transfer_instr, allocate_instr, assign_instr],
        Some(&payer. pubkey()),
        &[payer, new_account],
        blockhash,
    );

    let _sig = client. send_and_confirm_transaction(&tx)?;

    Ok(())
}
示例:链上程序

此示例提交来自链上 Solana 程序的指令。创建的帐户是程序派生的地址,由付款人提供资金,并分配给正在运行的程序。付款人和 new_account_pda 是签名者,其中 new_account_pda 由程序本身通过invoke_signed 虚拟签名,付款人由提交交易的客户端签名。

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke_signed,
    pubkey::Pubkey,
    system_instruction,
    system_program,
    sysvar::rent::Rent,
    sysvar::Sysvar,
};

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CreateAccountInstruction {
    /// The PDA seed used to distinguish the new account from other PDAs
    pub new_account_seed: [u8; 16],
    /// The PDA bump seed
    pub new_account_bump_seed: u8,
    /// The amount of space to allocate for `new_account_pda`
    pub space: u64,
}

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instr = CreateAccountInstruction::deserialize(&mut &instruction_data[..])?;

    let account_info_iter = &mut accounts. iter();

    let payer = next_account_info(account_info_iter)?;
    let new_account_pda = next_account_info(account_info_iter)?;
    let system_account = next_account_info(account_info_iter)?;

    assert!(payer. is_signer);
    assert!(payer. is_writable);
    // Note that `new_account_pda` is not a signer yet.
    // This program will sign for it via `invoke_signed`.
    assert!(!new_account_pda. is_signer);
    assert!(new_account_pda. is_writable);
    assert!(system_program::check_id(system_account. key));

    let new_account_seed = &instr. new_account_seed;
    let new_account_bump_seed = instr. new_account_bump_seed;

    let rent = Rent::get()?
        .minimum_balance(instr. space. try_into().expect("overflow"));

    invoke_signed(
        &system_instruction::transfer(
            payer. key,
            new_account_pda. key,
            rent,
        ),
        &[payer. clone(), new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    invoke_signed(
        &system_instruction::allocate(
            new_account_pda. key,
            instr. space,
        ),
        &[new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    invoke_signed(
        &system_instruction::assign(
            new_account_pda. key,
            &program_id,
        ),
        &[new_account_pda. clone()],
        &[&[payer. key. as_ref(), new_account_seed, &[new_account_bump_seed]]],
    )?;

    Ok(())
}

程序代码

pub fn allocate(pubkey: &Pubkey, space: u64) -> Instruction {
    let account_metas = vec![AccountMeta::new(*pubkey, true)];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::Allocate { space },
        account_metas,
    )
}

pub fn allocate_with_seed(
    address: &Pubkey, // must match create_with_seed(base, seed, owner)
    base: &Pubkey,
    seed: &str,
    space: u64,
    owner: &Pubkey,
) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*address, false),
        AccountMeta::new_readonly(*base, true),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::AllocateWithSeed {
            base: *base,
            seed: seed.to_string(),
            space,
            owner: *owner,
        },
        account_metas,
    )
}

将 lamport 从系统程序拥有的帐户转移到多个帐户。

此函数生成一个指令向量,该向量必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::Transfers。
所需签名者from_pubkey 签名者必须签署交易。

示例

示例:客户端 RPC

此示例在单个交易中执行多次转移。

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use anyhow::Result;

fn transfer_lamports_to_many(
    client: &RpcClient,
    from: &Keypair,
    to_and_amount: &[(Pubkey, u64)],
) -> Result<()> {
    let instrs = system_instruction::transfer_many(&from. pubkey(), to_and_amount);

    let blockhash = client. get_latest_blockhash()?;
    let tx = Transaction::new_signed_with_payer(
        &instrs,
        Some(&from. pubkey()),
        &[from],
        blockhash,
    );

    let _sig = client. send_and_confirm_transaction(&tx)?;

    Ok(())
}
示例:链上程序

此示例从“银行”账户(由调用程序拥有的程序派生地址)进行多次转账。此示例提交来自链上 Solana 程序的指令。创建的帐户是程序派生的地址,并分配给正在运行的程序。付款人和 new_account_pda 是签名者,其中 new_account_pda 由程序本身通过invoke_signed 虚拟签名,付款人由提交交易的客户端签名。

use solana_program::{
    account_info::{next_account_info, next_account_infos, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke_signed,
    pubkey::Pubkey,
    system_instruction,
    system_program,
};

/// # Accounts 
/// 
/// - 0: bank_pda - writable 
/// - 1: system_program - executable 
/// - *: to - writable
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct TransferLamportsToManyInstruction {
    pub bank_pda_bump_seed: u8,
    pub amount_list: Vec<u64>,
}

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instr = TransferLamportsToManyInstruction::deserialize(&mut &instruction_data[..])?;

    let account_info_iter = &mut accounts. iter();

    let bank_pda = next_account_info(account_info_iter)?;
    let bank_pda_bump_seed = instr. bank_pda_bump_seed;
    let system_account = next_account_info(account_info_iter)?;

    assert!(system_program::check_id(system_account. key));

    let to_accounts = next_account_infos(account_info_iter, account_info_iter. len())?;

    for to_account in to_accounts {
         assert!(to_account. is_writable);
         // ... do other verification ...
    }

    let to_and_amount = to_accounts
        .iter()
        .zip(instr. amount_list. iter())
        .map(|(to, amount)| (*to. key, *amount))
        .collect::<Vec<(Pubkey, u64)>>();

    let instrs = system_instruction::transfer_many(bank_pda. key, to_and_amount. as_ref());

    for instr in instrs {
        invoke_signed(&instr, accounts, &[&[b"bank", &[bank_pda_bump_seed]]])?;
    }

    Ok(())
}

程序代码

pub fn transfer_many(from_pubkey: &Pubkey, to_lamports: &[(Pubkey, u64)]) -> Vec<Instruction> {
    to_lamports
        .iter()
        .map(|(to_pubkey, lamports)| transfer(from_pubkey, to_pubkey, *lamports))
        .collect()
}

创建一个包含持久交易 nonce 的帐户。

此函数生成一个指令向量,该向量必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::CreateAccount 和 SystemInstruction::InitializeNonceAccount。
持久交易 nonce 是一个特殊帐户,可以执行过去已签名的交易。
标准 Solana 交易包括最近的区块哈希(有时称为 nonce)。在执行过程中,Solana 运行时会验证最近的区块哈希大约不到两分钟,并且在这两分钟内没有执行具有相同区块哈希的其他相同交易。这些检查可防止意外重放交易。因此,不可能签署交易,等待两分钟以上,然后成功执行该交易。
持久交易 nonce 是标准最近区块哈希 nonce 的替代方案。它们存储在链上的帐户中,每次使用时,它们的值都会更改为新值以供下次使用。运行时会验证每个持久 nonce 值是否仅使用一次,并且对 nonce 的“旧”程度没有任何限制。由于它们存储在链上并需要额外的指令才能使用,因此使用持久交易 nonce 进行交易比使用标准交易更昂贵。
持久 nonce 的值本身就是一个区块哈希,可通过 nonce::state::Data 的区块哈希字段访问,该字段从 nonce 帐户数据反序列化而来。
基本的持久交易 nonce 生命周期是

  1. 使用 create_nonce_account 指令创建 nonce 帐户。
  2. 提交包含 advance_nonce_account 指令的特殊格式的交易。
  3. 通过使用 withdraw_nonce_account 指令撤回其 lamports 来销毁 nonce 帐户。

Nonce 帐户有一个关联的授权帐户,该帐户存储在其帐户数据中,可以使用 authorize_nonce_account 指令进行更改。授权机构必须签署包含 advance_nonce_account、authorize_nonce_account 和 withdraw_nonce_account 指令的交易。
Nonce 帐户归系统程序所有。
此构造函数创建 SystemInstruction::CreateAccount 指令和 SystemInstruction::InitializeNonceAccount 指令。
必需的签名者
from_pubkey 和 nonce_pubkey 签名者必须签署交易。

示例

从链下客户端创建 nonce 帐户:

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
    nonce::State,
};
use anyhow::Result;

fn submit_create_nonce_account_tx(
    client: &RpcClient,
    payer: &Keypair,
) -> Result<()> {

    let nonce_account = Keypair::new();

    let nonce_rent = client. get_minimum_balance_for_rent_exemption(State::size())?;
    let instr = system_instruction::create_nonce_account(
        &payer. pubkey(),
        &nonce_account. pubkey(),
        &payer. pubkey(), // Make the fee payer the nonce account authority
        nonce_rent,
    );

    let mut tx = Transaction::new_with_payer(&instr, Some(&payer. pubkey()));

    let blockhash = client. get_latest_blockhash()?;
    tx. try_sign(&[&nonce_account, payer], blockhash)?;

    client. send_and_confirm_transaction(&tx)?;

    Ok(())
}

程序代码

pub fn create_nonce_account(
    from_pubkey: &Pubkey,
    nonce_pubkey: &Pubkey,
    authority: &Pubkey,
    lamports: u64,
) -> Vec<Instruction> {
    vec![
        create_account(
            from_pubkey,
            nonce_pubkey,
            lamports,
            nonce::State::size() as u64,
            &system_program::id(),
        ),
        Instruction::new_with_bincode(
            system_program::id(),
            &SystemInstruction::InitializeNonceAccount(*authority),
            vec![
                AccountMeta::new(*nonce_pubkey, false),
                #[allow(deprecated)]
                AccountMeta::new_readonly(recent_blockhashes::id(), false),
                AccountMeta::new_readonly(rent::id(), false),
            ],
        ),
    ]
}

pub fn create_nonce_account_with_seed(
    from_pubkey: &Pubkey,
    nonce_pubkey: &Pubkey,
    base: &Pubkey,
    seed: &str,
    authority: &Pubkey,
    lamports: u64,
) -> Vec<Instruction> {
    vec![
        create_account_with_seed(
            from_pubkey,
            nonce_pubkey,
            base,
            seed,
            lamports,
            nonce::State::size() as u64,
            &system_program::id(),
        ),
        Instruction::new_with_bincode(
            system_program::id(),
            &SystemInstruction::InitializeNonceAccount(*authority),
            vec![
                AccountMeta::new(*nonce_pubkey, false),
                #[allow(deprecated)]
                AccountMeta::new_readonly(recent_blockhashes::id(), false),
                AccountMeta::new_readonly(rent::id(), false),
            ],
        ),
    ]
}

提高持久交易 nonce 的值。

此函数生成一个指令,该指令必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::AdvanceNonceAccount。
每个依赖于持久交易 nonce 的交易都必须包含一个 SystemInstruction::AdvanceNonceAccount 指令作为消息中的第一个指令,由此函数创建。当包含在第一个位置时,Solana 运行时会将该交易识别为依赖于持久交易 nonce 的交易并对其进行相应处理。Message::new_with_nonce 函数可用于构造正确格式的消息,而无需直接调用 advance_nonce_account。
在构建包含 AdvanceNonceInstruction 的交易时,必须以不同的方式处理 recent_blockhash — 不是将其设置为最近的区块哈希,而是必须从 nonce 帐户中检索并反序列化 nonce 的值,并将该值指定为“最近的区块哈希”。可以使用 solana_rpc_client_nonce_utils::data_from_account 函数反序列化 nonce 帐户。
有关持久事务 nonce 的进一步描述,请参阅 create_nonce_account。
所需签名者
authorized_pubkey 签名者必须签署交易。

示例

使用持久 nonce 创建和签署交易:

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    message::Message,
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use std::path::Path;
use anyhow::Result;

fn create_transfer_tx_with_nonce(
    client: &RpcClient,
    nonce_account_pubkey: &Pubkey,
    payer: &Keypair,
    receiver: &Pubkey,
    amount: u64,
    tx_path: &Path,
) -> Result<()> {

    let instr_transfer = system_instruction::transfer(
        &payer. pubkey(),
        receiver,
        amount,
    );

    // In this example, `payer` is `nonce_account_pubkey`'s authority
    let instr_advance_nonce_account = system_instruction::advance_nonce_account(
        nonce_account_pubkey,
        &payer. pubkey(),
    );

    // The `advance_nonce_account` instruction must be the first issued in
    // the transaction.
    let message = Message::new(
        &[
            instr_advance_nonce_account,
            instr_transfer
        ],
        Some(&payer. pubkey()),
    );

    let mut tx = Transaction::new_unsigned(message);

    // Sign the tx with nonce_account's `blockhash` instead of the
    // network's latest blockhash.
    let nonce_account = client. get_account(nonce_account_pubkey)?;
    let nonce_data = solana_rpc_client_nonce_utils::data_from_account(&nonce_account)?;
    let blockhash = nonce_data. blockhash();

    tx. try_sign(&[payer], blockhash)?;

    // Save the signed transaction locally for later submission.
    save_tx_to_file(&tx_path, &tx)?;

    Ok(())
}

程序代码

pub fn advance_nonce_account(nonce_pubkey: &Pubkey, authorized_pubkey: &Pubkey) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*nonce_pubkey, false),
        #[allow(deprecated)]
        AccountMeta::new_readonly(recent_blockhashes::id(), false),
        AccountMeta::new_readonly(*authorized_pubkey, true),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::AdvanceNonceAccount,
        account_metas,
    )
}

从持久交易 nonce 账户中提取 lamport。

此函数生成一个指令,该指令必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::WithdrawNonceAccount。
提取 nonce 账户的全部余额将导致运行时在成功完成交易后销毁它。
否则,nonce 账户必须保持大于或等于租金豁免所需的最低余额。如果此指令的结果使 nonce 账户的余额低于租金豁免所需的余额,但也大于零,则交易将失败。
此构造函数创建一个 SystemInstruction::WithdrawNonceAccount 指令。
必需的签名者
authorized_pubkey 签名者必须签署交易。

示例

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use anyhow::Result;

fn submit_withdraw_nonce_account_tx(
    client: &RpcClient,
    nonce_account_pubkey: &Pubkey,
    authorized_account: &Keypair,
) -> Result<()> {

    let nonce_balance = client. get_balance(nonce_account_pubkey)?;

    let instr = system_instruction::withdraw_nonce_account(
        &nonce_account_pubkey,
        &authorized_account. pubkey(),
        &authorized_account. pubkey(),
        nonce_balance,
    );

    let mut tx = Transaction::new_with_payer(&[instr], Some(&authorized_account. pubkey()));

    let blockhash = client. get_latest_blockhash()?;
    tx. try_sign(&[authorized_account], blockhash)?;

    client. send_and_confirm_transaction(&tx)?;

    Ok(())
}

程序代码

pub fn withdraw_nonce_account(
    nonce_pubkey: &Pubkey,
    authorized_pubkey: &Pubkey,
    to_pubkey: &Pubkey,
    lamports: u64,
) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*nonce_pubkey, false),
        AccountMeta::new(*to_pubkey, false),
        #[allow(deprecated)]
        AccountMeta::new_readonly(recent_blockhashes::id(), false),
        AccountMeta::new_readonly(rent::id(), false),
        AccountMeta::new_readonly(*authorized_pubkey, true),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::WithdrawNonceAccount(lamports),
        account_metas,
    )
}

更改持久交易 nonce 帐户的权限。

此函数生成一个指令,该指令必须在交易中提交或调用才能生效,其中包含序列化的 SystemInstruction::AuthorizeNonceAccount。
此构造函数创建一个 SystemInstruction::AuthorizeNonceAccount 指令。
必需的签名者
authorized_pubkey 签名者必须签署交易。

示例

use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use anyhow::Result;

fn authorize_nonce_account_tx(
    client: &RpcClient,
    nonce_account_pubkey: &Pubkey,
    authorized_account: &Keypair,
    new_authority_pubkey: &Pubkey,
) -> Result<()> {

    let instr = system_instruction::authorize_nonce_account(
        &nonce_account_pubkey,
        &authorized_account. pubkey(),
        &new_authority_pubkey,
    );

    let mut tx = Transaction::new_with_payer(&[instr], Some(&authorized_account. pubkey()));

    let blockhash = client. get_latest_blockhash()?;
    tx. try_sign(&[authorized_account], blockhash)?;

    client. send_and_confirm_transaction(&tx)?;

    Ok(())
}

程序代码

pub fn authorize_nonce_account(
    nonce_pubkey: &Pubkey,
    authorized_pubkey: &Pubkey,
    new_authority: &Pubkey,
) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*nonce_pubkey, false),
        AccountMeta::new_readonly(*authorized_pubkey, true),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::AuthorizeNonceAccount(*new_authority),
        account_metas,
    )
}

对旧版 nonce 版本进行一次性幂等升级,以将其排除在链块哈希域之外。

pub fn upgrade_nonce_account(nonce_pubkey: Pubkey) -> Instruction {
    let account_metas = vec![AccountMeta::new(nonce_pubkey, /*is_signer:*/ false)];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::UpgradeNonceAccount,
        account_metas,
    )
}

Solana 计算账户租金与燃烧逻辑


分析文档

https://docs.anza.xyz/implemented-proposals/rent/

收租原因:Solana 上的账户可能具有与账户余额(Account::lamports)分开的所有者控制状态(Account::data)。由于网络上的验证者需要在内存中维护此状态的工作副本,因此网络会针对此资源消耗收取基于时间和空间的费用,也称为租金。

费用收取间隔:每个纪元,genesis时固定
免租下限:余额不小于当前账户所需的2年租金
为什么设置2年:根据硬件成本每2年下降 50% 的事实得出
下次收租时间:Account::rent_epoch
交易不可见:租金的收取是根据协议级账户更新进行的,例如向验证者分配租金,这意味着没有相应的租金扣除交易。因此,租金的收取是相当不可见的,只能通过最近的交易或给定其账户地址前缀的预定时间隐式观察到。

分析代码

版本:v2.0.25
核心代码:https://github.com/anza-xyz/agave/blob/v2.0.25/sdk/program/src/rent.rs

核心默认参数

参数 解释 默认值 genesis初始化参数
lamports_per_byte_year 租赁费率(单位:lampports/byte-year) 1000000000 / 100 x 365 / (1024 x 1024) lamports_per_byte_year
exemption_threshold 账户余额必须包含租金的时间(以年为单位)才能免除租金 2.0 rent_exemption_threshold
burn_percent 所收取租金中被销毁的百分比。有效值在 [0, 100] 范围内。剩余百分比分配给验证者。 50 rent_burn_percentage
  • 计算基本租金的账户存储开销
    ACCOUNT_STORAGE_OVERHEAD:128,这是存储没有数据的账户所需的字节数。在计算 [Rent::minimum_balance] 时,它会被添加到账户数据长度中。

核心方法

 /// 给定帐户数据大小的免租应付最低余额。
pub fn minimum_balance(&self, data_len: usize) -> u64 {
     let bytes = data_len as u64;
     (((ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte_year) as f64
      * self.exemption_threshold) as u64
 }

/// 给定的余额和数据长度是否可以免除。
pub fn is_exempt(&self, balance: u64, data_len: usize) -> bool {
    balance >= self.minimum_balance(data_len)
}

minimum_balance 中 字节的长度由基础账户字节数(128)+存储数据字节长度累加,然后乘以租赁费率(费用/每年字节),再乘以免除阈值(2)
对于普通没有数据的地址,最小的免租计算为 ACCOUNT_STORAGE_OVERHEAD x lamports_per_byte_year x exemption_threshold
默认参数情况下,免租下限计算为 0.00089 SOL

目前租金逻辑将被禁用
已生效:Devnet, Testnet,可能主网也快支持了
https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0084-disable-rent-fees-collection.md

对于genesis中cluster_type等于ClusterType::Development默认会开启所有feature,包含该禁用

if genesis_config.cluster_type == ClusterType::Development {
    solana_runtime::genesis_utils::activate_all_features(&mut genesis_config);
}

如果想在ClusterType::Development类型下禁用feature disable_rent_fees_collection,如果首次部署,简单粗暴直接注释掉激活行

// (disable_rent_fees_collection::id(), "Disable rent fees collection #33945"),

总结

  1. 收租配置在genesis中固定配置
  2. 每个纪元收取一次
  3. 余额不小于当前账户所需的2年租金时免租
  4. 下次收租时间:Account::rent_epoch
  5. 租金计算方式:基础账户字节数(128)+存储数据字节长度累加,然后乘以租赁费率(费用/每年字节),再乘以免除阈值(2)
  6. 普通没有数据的地址,默认参数情况下,账户余额不小于 0.00089 SOL 免租
  7. 收租逻辑在cluster_type Development类型下默认禁用,如需使用,需要手动调整,并且此功能主网可能也将被禁用

Solana CLI 中的持久事务 Nonces


背景

离线交易

常规Solana交易都是典型短生命周期(大约1分钟),依赖recent_blockhash。如果想提前离线生成交易,等待较长时间再发送,则会过期,交易失败。
持久交易 nonce 是一种绕过交易的典型短生命周期的机制 recent_blockhash

顺序执行

持久交易 nonce可以让交易按照顺序执行

创建Nonce账户

账户地址可以通过create-nonce-account创建nonce账户,nonce账户用于存放下一个nonce值,nonce账户必须是免租的,所以需要持有最低的余额。

solana-keygen new -o nonce-keypair.json
solana create-nonce-account nonce-keypair.json 1

查询nonce

solana nonce nonce-keypair.json

输出

8GRipryfxcsxN8mAGjy8zbFo9ezaUsh47TsPzmZbuytU

提高存储的 Nonce

solana new-nonce nonce-keypair.json

显示nonce账户

solana nonce-account nonce-keypair.json

输出

balance: 0.5 SOL
minimum balance required: 0.00136416 SOL
nonce: DZar6t2EaCFQTbUP4DHKwZ1wT8gCPW2aRfkVWhydkBvS

从nonce账户提取资金

solana withdraw-from-nonce-account nonce-keypair.json ~/.config/solana/id.json 0.5

通过提取全部余额来关闭 nonce 账户

创建 nonce 账户后重新分配权限

solana authorize-nonce-account nonce-keypair.json nonce-authority.json

CLI 实测

这里我们演示了 Alice 使用持久随机数向 Bob 支付 1 个 SOL。对于所有支持持久随机数的子命令,该过程都是相同的

创建

首先,我们需要一些 Alice、Alice 的随机数和 Bob 的账户

solana-keygen new -o alice.json
solana-keygen new -o nonce.json
solana-keygen new -o bob.json

为Alice领取测试代币

Alice 需要一些资金来创建一个 nonce 账户并发送给 Bob。空投一些 SOL

solana airdrop -k alice.json 1
1 SOL

创建 Alice 的 nonce

现在 Alice 需要一个 nonce 账户。创建一个
这里没有使用 单独的nonce 权限alice.json,因此对 nonce 账户拥有完全的权限

solana create-nonce-account -k alice.json nonce.json 0.1
3KPZr96BTsL3hqera9up82KAU462Gz31xjqJ6eHUAjF935Yf8i1kmfEbo6SVbNaACKE5z6gySrNjVRvmS8DcPuwV

第一次向Bob转账

Alice 尝试向 Bob 付款,但签名时间过长。指定的区块哈希过期,交易失败

$ solana transfer -k alice.json --blockhash expiredDTaxfagttWjQweib42b6ZHADSx94Tw8gHx11 bob.json 0.01
[2025-03-06T18:48:28.462911000Z ERROR solana_cli::cli] Io(Custom { kind: Other, error: "Transaction \"33gQQaoPc9jWePMvDAeyJpcnSPiGUAdtVg8zREWv4GiKjkcGNufgpcbFyRKRrA25NkgjZySEeKue5rawyeH5TzsV\" failed: None" })
Error: Io(Custom { kind: Other, error: "Transaction \"33gQQaoPc9jWePMvDAeyJpcnSPiGUAdtVg8zREWv4GiKjkcGNufgpcbFyRKRrA25NkgjZySEeKue5rawyeH5TzsV\" failed: None" })

Nonce 来救援

Alice 重试交易,这次指定她的 nonce 账户和存储在那里的区块哈希
记住,在这个例子中,alice.json是nonce 权限

solana nonce-account nonce.json
balance: 0.1 SOL
minimum balance required: 0.00136416 SOL
nonce: F7vmkY3DTaxfagttWjQweib42b6ZHADSx94Tw8gHx3W7
$ solana transfer -k alice.json --blockhash F7vmkY3DTaxfagttWjQweib42b6ZHADSx94Tw8gHx3W7 --nonce nonce.json bob.json 0.01
HR1368UKHVZyenmH7yVz5sBAijV6XAPeWbEiXEGVYQorRMcoijeNAbzZqEZiH8cDB8tk65ckqeegFjK8dHwNFgQ

成功

交易成功!Bob 从 Alice 处收到 0.01 SOL,Alice 存储的 nonce 值增加到新值

solana balance -k bob.json
0.01 SOL
solana nonce-account nonce.json
balance: 0.1 SOL
minimum balance required: 0.00136416 SOL
nonce: 6bjroqDcZgTv6Vavhqf81oBHTv3aMnX19UTB51YhAZnN

测试代码

const {
  Connection,
  Keypair,
  PublicKey,
  LAMPORTS_PER_SOL,
  SystemProgram,
  Transaction,
  sendAndConfirmTransaction,
  NonceAccount
} = require('@solana/web3.js');

async function main() {
  // 连接到 Solana 网络
  const connection = new Connection('https://api.devnet.solana.com', 'confirmed');

  // 创建一个新的 Keypair 作为 Nonce 账户的所有者
  const owner = Keypair.generate();

  // 创建一个新的 Keypair 作为 Nonce 账户
  const nonceAccount = Keypair.generate();

  // 计算 Nonce 账户所需的租金
  const nonceRent = await connection.getMinimumBalanceForRentExemption(NonceAccount.span);

  // 创建一个创建 Nonce 账户的交易
  const createNonceAccountTransaction = new Transaction().add(
    SystemProgram.createNonceAccount({
      fromPubkey: owner.publicKey,
      noncePubkey: nonceAccount.publicKey,
      lamports: nonceRent
    })
  );

  // 发送并确认创建 Nonce 账户的交易
  await sendAndConfirmTransaction(connection, createNonceAccountTransaction, [owner, nonceAccount]);

  // 获取 Nonce 账户的状态
  const nonceAccountState = await connection.getNonce(nonceAccount.publicKey);
  let nonce = nonceAccountState.nonce;

  // 离线签署 10 笔交易
  for (let i = 0; i < 10; i++) {
    // 创建一个新的交易
    const transaction = new Transaction().add(
      SystemProgram.transfer({
        fromPubkey: owner.publicKey,
        toPubkey: new PublicKey('your_recipient_address'),
        lamports: LAMPORTS_PER_SOL * 0.01 // 示例金额
      })
    );

    // 设置 Nonce 信息
    transaction.recentBlockhash = nonce;
    transaction.feePayer = owner.publicKey;

    // 签署交易
    transaction.sign(owner);

    // 在这里你可以将交易序列化并保存,以便离线发送
    const serializedTransaction = transaction.serialize();

    console.log(`第 ${i + 1} 笔交易的 Nonce:`, nonce);

    // 推进 Nonce
    const advanceNonceTransaction = new Transaction().add(
      SystemProgram.advanceNonceAccount({
        noncePubkey: nonceAccount.publicKey,
        authorizedPubkey: owner.publicKey
      })
    );
    await sendAndConfirmTransaction(connection, advanceNonceTransaction, [owner]);

    // 获取新的 Nonce
    const newNonceAccountState = await connection.getNonce(nonceAccount.publicKey);
    nonce = newNonceAccountState.nonce;
  }
}

main().catch(console.error);

总结

  1. 如果想离线长期保存交易或者期望交易按顺序执行,可以使用nonce账户
  2. 实现过程,查询对应的nonce账户的nonce值,代替recent_blockhash
  3. CLI中每次交易后nonce账户的nonce值会自动推进更新,对于代码调用,需要发起SystemProgram.advanceNonceAccount交易主动推进
  4. nonce值无法本地离线计算推进,每次都需要链上发起更新

https://docs.anza.xyz/cli/examples/durable-nonce


使用Anchor编写测试程序


使用Anchor编写测试程序

安装依赖

sudo apt-get update
sudo apt-get install -y \
    build-essential \
    pkg-config \
    libudev-dev llvm libclang-dev \
    protobuf-compiler libssl-dev

安装Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. "$HOME/.cargo/env"
rustc --version

安装Solana CLI

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

添加环境变量

export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"
sudo vi /etc/profile
export PATH=$PATH:/usr/local/go/bin
source /etc/profile

版本更新

agave-install update

安装 Anchor CLI

cargo install --git https://github.com/coral-xyz/anchor avm --force
avm --version

版本更新

avm install latest
avm use latest

指定版本

avm install 0.30.1
avm use 0.30.1

确认版本

anchor --version

Solana CLI Basics

solana config set --url devnet

Create Wallet

solana-keygen new
Wrote new keypair to /home/surou/.config/solana/id.json
pubkey: BAGNJGtBngKZKdcZRNjebnS6mwPg5prDgj6JyR74D4ad

查看当前地址

solana address

获取测试币

solana config set -ud
solana airdrop 5

注:当前devnet最大每次5 SOL

查看余额

solana balance

Anchor CLI Basics

初始化项目名

anchor init anchor init my-project
cd my-project
anchor build
anchor deploy
anchor test

测试交易

anchor init 自动创建的测试程序

use anchor_lang::prelude::*;

declare_id!("9X2BqKSgKDrEhRsemfmkrAjpYVB78HpoWRBKTMPBPup9");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!("Greetings from: {:?}", ctx.program_id);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

交易发起地址:https://explorer.solana.com/address/BAGNJGtBngKZKdcZRNjebnS6mwPg5prDgj6JyR74D4ad?cluster=devnet

部署交易

https://explorer.solana.com/tx/47xSqiSDduatTW2xXgQeDNvkN12aboAaThzFqXVHQTqsZGXhrbQRTkRiREkLk3skLfCRxJVmbHi2da5t6BkGxL9d?cluster=devnet

  • 程序账户(新建):9X2BqKSgKDrEhRsemfmkrAjpYVB78HpoWRBKTMPBPup9
  • 缓冲区账户:HHYvtbHvaUQgX6zG2BvPui8PwQ23zS5Bneqm1eELj8V6 {缓冲区账户是 Solana 生态系统中一种用于存储和管理程序数据的特殊账户类型,它为开发人员提供了灵活的数据管理和更新机制}
  • 程序存储账户:https://explorer.solana.com/address/9ZHpouNY6xrEVWjzgtFp4zsJGrViVGkS7JS4xEn9qo5f?cluster=devnet
    • Upgradeable:Yes
    • Upgrade Authority: BAGNJGtBngKZKdcZRNjebnS6mwPg5prDgj6JyR74D4ad

查看交易地址,发现很多笔与BPFLoaderUpgradeab1e11111111111111111111111的交易被打包
例如:https://explorer.solana.com/tx/2FB1tRfGn2NSXF7hHFwTeE9WAAa4eHTwmh82Ly1M4EUBcRZfTiCo2RRyjWPK6w4vD9vNNSH6RjWRudhHmtmBpWWn?cluster=devnet

Solana 上每笔交易能够携带的数据量存在限制。要是程序的字节码过大,就需要把字节码分割成多个部分,然后通过多笔交易依次上传。这就会产生很多笔与 BPFLoaderUpgradeable 相关的交易。

发起交易

执行 anchor test,自动执行程序中的initialize

describe("my-project", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.MyProject as Program<MyProject>;

  it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().rpc();
    console.log("Your transaction signature", tx);
  });
});

对应交易为:https://explorer.solana.com/tx/3vzfjypR6AJVYvGgr8tiqpqTXrbYGsyMXwaidhv35haxCGFJJfDUT5oLra5dAoZpLx7tQsmTT7goZxTCuLNUz5qS?cluster=devnet

程序升级

anchor upgrade --program-id 9X2BqKSgKDrEhRsemfmkrAjpYVB78HpoWRBKTMPBPup9 ./target/deploy/my_project.so 

对应交易:https://explorer.solana.com/tx/5B8ZdZrZ3oKxMr5iF1PXdbPRPD1LZ75vEFVTmnQUUNrAhqm18kt8ydM4S6o4Z9LN3jMN69nvDXW5b4vc8PzzLWDM?cluster=devnet

对比首次部署时的交易:https://explorer.solana.com/tx/47xSqiSDduatTW2xXgQeDNvkN12aboAaThzFqXVHQTqsZGXhrbQRTkRiREkLk3skLfCRxJVmbHi2da5t6BkGxL9d?cluster=devnet

缓冲区账户由 HHYvtbHvaUQgX6zG2BvPui8PwQ23zS5Bneqm1eELj8V6 更新为 AxQ65Tm1VSptQWT22AsZFsdFovbYgo4i7dJHJLNwJjxa
类似于以太坊中的升级代理合约变更了逻辑地址

对比以太坊,Solana的程序升级简化很多

https://www.anchor-lang.com/docs/installation