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


EVM与SVM:帐户 看看在以太坊和索拉纳上构建时帐户有何不同


作为区块链网络,以太坊和索拉纳拥有独特的数据结构,作为全球公共世界计算机,在其网络上存储和共享数据。在本章中,我们旨在探索这些链如何构建其数据集。

以太坊中的帐户

在以太坊中,“帐户”是指拥有以太并可以发送交易的实体。它包括存款和取款所需的地址,分类如下:

  • EOA(外部拥有的帐户):外部拥有的帐户,拥有私钥。把它想象成个人钱包的帐户。
  • CA(合同账户):合同账户,持有智能合同代码。

以太坊中EOA和CA的一个关键区别是,EOA不是智能合约,通常没有自己的存储空间。因此,EOA的代码散列设置为“空”散列,表示帐户没有存储空间。

外部拥有帐户(EOA)是具有私钥的帐户,拥有私钥意味着控制对资金或合同的访问。私钥意味着对资金或合同的访问进行控制。EOA中包含以下数据:

合同帐户包含EOA根本无法持有的智能合同代码。此外,合同帐户没有私钥。相反,它由智能合约代码的逻辑控制。这个智能合同代码在创建合同帐户时记录在以太坊区块链上,是由EVM执行的软件程序。

与EOA一样,合同帐户有一个地址,可以发送和接收以太币。然而,当交易的目的地是合同帐户地址时,交易和交易数据将用作在EVM中执行合同的输入。除了以太外,该事务还可以包括指示要执行的合同特定功能的数据以及要传递给该函数的参数。因此,事务可以调用合同中的函数。如果EOA要求,合同也可以调用其他合同。然而,由于合同帐户没有私钥,它不能签署交易,也不能自行启动交易。这些关系总结如下:

  • EOA → EOA (OK)
  • EOA → CA (OK)
  • EOA → CA → CA (OK)
  • CA → CA (Impossible)

Solana的帐户

Solana帐户的概念比以太坊更广泛。在Solana中,所有数据都根据帐户进行存储和执行。这意味着,在每次需要在事务之间存储状态的情况下,都会使用帐户进行保存。与Linux等操作系统中的文件类似,帐户可以存储超出程序生命周期的任意数据。此外,与文件一样,帐户包含元数据,这些元数据告知运行时谁可以访问数据以及如何访问数据。

在Solana的Sealevel VM中,所有帐户都能够存储数据。那么,智能合约开发人员可以在哪里存储他们的数据呢?他们可以将数据存储在可执行帐户拥有的不可执行帐户(PDA)中。开发人员可以通过分配与其可执行帐户地址相同的所有者来创建新帐户来存储数据。

然而,存储数据的Solana网络上的“帐户”需要支付费用。这些帐户包括关于它们所含数据寿命的元数据,以名为“Lamports”的本机令牌表示。帐户存储在验证器的内存中,并支付“租金”以保留在那里。验证员定期扫描所有帐户并收取租金。Lamports降至零的帐户将自动删除,因为他们无法支付租金。如果一个帐户包含足够数量的Lamports,它将免收租金,并且不单独扣除租金。

Solana的帐户分为以下两种类型,类似于以太坊:

可执行帐户(程序帐户):这些是存储代码的智能合约,通常更简单地称为“程序”。

不可执行帐户(数据帐户):这些可以接收令牌或数据,但不能执行代码,因为可执行变量设置为“false”。

(*与以太坊不同,Solana使用“程序”而不是“合同”一词。)
比较每个链中的帐户结构揭示了以下差异。

那么,EOA和CA如何与Solana的帐户结构相对应?它们可以映射如下。

帐户抽象

以太坊长期以来一直在探索帐户抽象的概念。以太坊有两种类型的帐户:EOA和CA,每种帐户都具有明显不同的功能。值得注意的是,合同账户(CA)无法生成或签署交易,导致重大限制。交易必须通过EOA发起和签署,这意味着使用21,000天然气的基本费用,并增加了帐户管理的复杂性。帐户抽象旨在消除这些限制,允许单个帐户同时执行EOA和合同帐户的功能。
因此,可以对图表进行以下调整:

  • EOA → EOA(OK)
  • EOA → CA(OK)
  • EOA → CA → CA(OK)
  • EOA + CA(AA)→CA(现在,好的!)

例如,multisig钱包或智能合约钱包需要将少量以太坊存储在单独的EOA中以支付交易费用,导致随着时间的推移不得不补充它的不便。帐户抽象允许单个帐户执行合同和签发交易,从而改善了这种不便。通过ERC-4337,Vitalik向社区提出了这个概念,并于2021年被采纳,现在在以太坊网络中实施。

总之,帐户抽象提供了以下好处:

  • 其他人支付我的交易费用,或者我为其他人支付。
  • 使用ERC-20代币支付费用
  • 设置自定义安全规则。
  • 在密钥丢失的情况下恢复帐户。
  • 在受信任的设备或个人之间共享帐户安全。
  • 批量交易(例如,一次性授权和执行掉期)。
  • Dapp和钱包开发人员有更多机会创新用户体验。

帐户抽象是否在索拉纳中实施?

Solana自推出以来一直实施帐户抽象(AA)。如前所述,Solana将所有数据存储在称为“帐户”的单元中,分为可执行文件(程序帐户)和不可执行文件(数据帐户)。从一开始,Solana就支持程序创建和管理特定帐户(即直接发起交易)的能力。此功能扩展了Solana中的帐户抽象功能,被称为程序派生地址(PDA)。Solana程序与数据帐户不同,是包含可执行代码的可执行帐户。使用PDA,开发人员可以为交易签名设置规则和机制,允许代表Solana网络认可和批准的受控帐户(PDA)自主授权各种链上操作。因此,与以太坊不同,Solana允许直接控制另一个基于Solana程序的程序,而无需繁琐的分层。

总结

Solana的帐户概念构建了链上的所有数据,所有数据都基于帐户。
Solana原生支持AA,允许在程序之间进行自调用。

原文:https://learnblockchain.cn/article/8416