9. 动态数据更新与租金管理

动态数据更新与租金管理

问题场景

在上一章我们成功创建了账户并写入了初始数据。但如果用户想要修改数据怎么办? 新数据可能比旧数据更长(例如从 "Hello" 变成 "Hello World"),也可能更短。这带来了两个必须解决的问题:

  1. 空间问题:账户的 data 数组长度是固定的。如果新数据更长,写入会越界报错。
  2. 租金问题
    • 数据变长:不仅空间不够,原有的租金余额也不足以维持新的租赁豁免门槛(Rent Exempt Minimum)。
    • 数据变短:空间浪费,且用户多付了租金,应该退还。

我们需要在更新数据前,动态调整账户大小和余额。


1. 重新分配空间 (Realloc)

Solana 在较新版本中引入了 realloc 方法,允许程序动态修改所拥有账户的大小。

rust
// 调整账户数据长度 // 参数2 (false): 是否将新增的字节初始化为 0。 // 因为我们马上会覆盖写入新数据,所以这里填 false 以节省计算资源。 account_data.realloc(new_data.len(), false)?;

注意:单个交易中 realloc 增加的大小有限制(通常单次不超过 10KB),且总大小不能超过 10MB。


2. 租金补足 (Top-up)

场景:数据变长。 逻辑:账户现在的余额 < 新长度所需的最低租金。我们需要让用户补差价。

因为我们要从用户钱包扣钱,这属于转移用户资产,所以必须通过 System Program 并要求用户签名。

rust
let rent_exemption = Rent::get()?.minimum_balance(new_data.len()); if rent_exemption > account_data.lamports() { let diff = rent_exemption - account_data.lamports(); // 调用系统程序转账 (CPI) invoke( &system_instruction::transfer( account_user.key, // 从用户 account_data.key, // 到 PDA diff, ), accounts, )?; }

思考:为什么这里用 invoke 而不是 invoke_signed? 因为付款方是用户钱包,用户已经对这笔交易进行了签名授权,所以程序只需要转发指令即可。


3. 租金退款 (Refund)

场景:数据变短。 逻辑:账户现在的余额 > 新长度所需的最低租金。多余的钱应该退给用户。

因为我们要从 PDA 账户 扣钱,而该 PDA 的所有者正是当前程序。程序有权直接修改自己拥有的账户余额,不需要调用 System Program。

rust
if rent_exemption < account_data.lamports() { let diff = account_data.lamports() - rent_exemption; // 直接修改 Lamports (这是允许的,因为 PDA 归本程序所有) **account_data.try_borrow_mut_lamports()? -= diff; **account_user.try_borrow_mut_lamports()? += diff; }

关键区别

  • 用户 -> PDA:必须走 System Program Transfer (需要用户签名)。
  • PDA -> 用户:可以直接修改 Lamports (程序是 PDA 的主人)。

4. 写入新数据

处理完空间和租金后,就可以安全地写入数据了:

rust
account_data.data.borrow_mut().copy_from_slice(&new_data);

这一套逻辑(Realloc + Rent Logic)是 Solana 原生开发中的标准范式,虽然写起来略显繁琐,但它精确控制了每一分钱的流向。

RUSTPlayground
EDITOR ACTIVE
Initializing RUST Environment...

动态扩容模拟器 (Realloc)

Size Diff
0 bytes
Wallet
10000
Payer (Signer)
PDA Storage
"Hello"
5bRent: 0
Owned by Program
Realloc 机制解析

尝试修改上方的数据长度。Solana 允许程序动态调整 \`account_info.data_len\`,但必须同时保证账户余额满足新的 \`Rent Exempt\` 门槛。