solanaNotes

账户

账户有几种:

  1. program:存储可执行代码,info无状态
  2. data:由program创建,用英语存储和管理程序状态
  3. native program: solana内置的程序
  4. Sysvar: 存储network cluster state的特殊账户

每个账户有一个唯一的地址,以Ed25519格式表示为32字节

1.accountInfo

账户最大为10mb,存储的数据结构称为accountInfo

account data executable lamports owner
type Bytes Boolean Number Program Address
usage 存储账户状态 标志着该账户是否是程序 账户余额的数字形式 拥有该账户的程序的公钥

image-20240801185312765

2.native program

常见的有the System Program和BPF loader

3.System program

所有新建账户默认归system program所有

系统程序执行以下关键任务:

  1. New Account Creation
  2. Space Allocation
  3. Assign Program Ownership

钱包只是系统程序拥有的账户。只有系统程序拥有的账户才可以作为交易费用的支付者

4.BPFLoader

BPF 加载程序是指定为网络上所有其他程序(不包括本机程序)的“所有者”的程序。它负责部署、升级和执行自定义程序。

5.Sysvar Accounts

Sysvar 帐户是位于预定义地址的特殊帐户,可提供对集群状态数据的访问。这些帐户会使用有关网络集群的数据动态更新。

6.Custom Programs

智能合约被称为程序Program,指包含可执行代码的账户,且拥有executable=true

创建program account时会创建三个独立账户:

  1. program account:代表链上程序的主账户。该帐户存储可执行数据帐户(2)的地址(存储已编译的程序代码)和程序的更新权限(被授权对程序进行更改的地址)
  2. program executable data account:包含程序的可执行二进制代码的账户
  3. buffer account:在主动部署或升级程序时存储字节代码的临时帐户。该过程完成后,数据将传输到程序可执行数据帐户(2),并且缓冲区帐户将关闭。

可近似将program account当作程序自身

7.data account

因为程序账户无法靠自身存储和修改数据,所以它会创建新账户存储修改数据,这些账户称为data account

image-20240802142547016

创建data account有两个步骤:

  1. 调用系统程序创建账户,并转移所有权给程序账户
  2. 调用已拥有新账户的程序账户,按照程序代码中的定义初始化账户数据

以下是一个程序部署的示例:

image-20240802202447241

和正常交易类似的info

image-20240802202501458

play ground默认的账户输入

image-20240802202518359

一点部署时的流程detail

image-20240802202530124

挺小一示例程序花了我挺多token,但多数都是后面能退回给我的rent。花的最多的是Program Executable Data Account,存储这个地址的program account花的反而很少。这两个都有一个相同的Upgrade Authority,指向fee payer和signer也就是我的账户。

在尝试close了program account后,两个account 剩下的token都退回到我原有账户。此时的token差值才是实际上的损耗

来点anchor

以下是一个anchor框架的案例程序的实践,目的在于使用程序派生地址PDA作为账户地址存储用户信息。这里也有playground的同款链接https://beta.solpg.io/66734b7bcffcf4b13384d1ad

0.PDA?

PDA 是使用用户定义的种子、bump种子和程序 ID 的组合确定性导出的地址。导出地址对应的账户存在与否,由之前是否显式地创建决定。在没有创建时访问PDA所在的账户是无效的。PDA可以被solana程序通过ID签名,从而不需要私钥。当调用了api产生的第一个有效的bump种子称为”canonical bump“(规范凹凸),而官方文档建议在使用PDA时使用规范bump,否则可能访问到空账户或者不属于运行程序的无效账户

PDA的bump值范围为0~255,每次调用api时的bump值创建账户时,给定相同的可选种子和programId ,具有不同值的bump种子仍然可以派生出有效的PDA(可能无效而且不推荐使用)。在anchor框架中,bump值可以由anchor自己推导出有效值,不用自己指定,从而避免了会遇到的问题。

1.Starter Code

项目结构如下

src/lib.rs

tests/anchor.test.ts

// src/lib.rs
use anchor_lang::prelude::*;

declare_id!("8KPzbM2Cwn4Yjak7QYAEH9wyoQh86NcBicaLuzPaejdw");

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

pub fn create(_ctx: Context<Create>) -> Result<()> {
Ok(())
}

pub fn update(_ctx: Context<Update>) -> Result<()> {
Ok(())
}

pub fn delete(_ctx: Context<Delete>) -> Result<()> {
Ok(())
}
}


#[derive(Accounts)]
#[instruction(message: String)]
pub struct Create<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init,
seeds = [b"message".user.key().as_ref()],
bump,
payer = user,
space = 8 + 32 + 4 + message.len() + 1
)]
pub message_account: Account<'info,MessageAccount>,
pub system_program: Program<'info,System>,
};

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

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

#[account]
pub struct MessageAccount {
pub user: PubKey,
pub message: String,
pub bump: u8,
}

这段代码定义了一个message account和创建此message account需要的数据结构

grammer explanation:

  1. #[derive(Accounts)]用于注释表示指令所需账户列表的结构,这个结构的每一个字段都是一个账户。
  2. 结构中的每个帐户(字段)都用帐户类型进行注释(例如Signer<'info> ),并且可以使用约束进一步注释(例如#[account(mut)] )。账户类型和账户限制用于对传递给指令的账户执行安全检查。
  3. 每个字段命名对账户验证没有影响,但是推荐描述性账户名称

对Create struct功能的描述:

  1. 定义了create指令所需的账户,比如user: Signer<'info>代表创建message account,并通过#[account(mut)]标记为可变的,因为它为新账户付费,而且只能是签名者批准交易,因为lamport将从账户扣除
  2. message_account: Account<'info,MessageAccount>这个创建的新账户用于存储用户的消息,init约束表示将在指令中创建账户,seeds和bump表明账户的地址是PDA(program derived Address),payer=user指定”给创建新用户”付费的账户,space指定分配给新账户数据字段的字节数
  3. system_program: Program<'info, System>这个是创建新账户时需要调用的系统账户,init调用系统程序来创建分配有指定space的新账户,并将所有权重新分配给当前程序
  4. #[instruction(message: String)]使Create能够从create指令访问message参数.
  5. seeds和bump一起使用来指定账户的地址是PDA.seeds的两个参数:b"message"是第一个种子的硬编码字符串,user.key().as_ref()引用user的公钥作为第二个种子,bump告诉anchor自动查找并使用正确的bump种子.最终anchor将使用seeds和bump导出PDA
  6. space的各参数:
    1. Anchor Account discriminator (identifier): 8 bytes
      anchor账户鉴别符(标识符):8字节
    2. User Address (Pubkey): 32 bytes
      用户地址(公钥):32字节
    3. User Message (String): 4 bytes for length + variable message length
      用户消息(字符串):4字节长度+可变消息长度
    4. PDA Bump seed (u8): 1 byte
      PDA bump种子 (u8):1 字节

再对对应的create函数修改:

pub fn create(ctx: Context<Create>, message: String) -> Result<()> {
msg!("Create Message: {}", message);
let account_data = &mut ctx.accounts.message_account;
account_data.user = ctx.accounts.user.key();
account_data.message = message;
account_data.bump = ctx.bumps.message_account;
Ok(())
}

功能描述:create通过ctx: Context<Create>获得对Create结构中指定账户的访问,通过message获得需要存储的用户信息

函数逻辑:

  1. msg!()宏打印消息到logs
  2. 初始化账户数据message,user为用户账户的地址,message为消息,bump用于后续派生PDA

类似的可仿照实现Update结构:

#[derive(Accounts)]
#[instruction(message: String)]
pub struct Update<'info> {
#[account(mut)]
pub user: Signer<'info>,

#[account(
mut,
seeds = [b"message", user.key().as_ref()],
bump = message_account.bump,
realloc = 8 + 32 + 4 + message.len() + 1,
realloc::payer = user,
realloc::zero = true,
)]
pub message_account: Account<'info, MessageAccount>,
pub system_program: Program<'info, System>,
}

Create有所区别的在于,使用了realloc调整账户的数据大小,比如realloc::payer可能与存储在message_account上的不同,而bump与存储在message_account上的相同.相同的seeds和bump保证了PDA的不变

接下来实现update函数:

pub fn update(ctx: Context<Update>, message: String) -> Result<()> {
msg!("Update Message: {}", message);
let account_data = &mut ctx.accounts.message_account;
account_data.message = message;
Ok(())
}

再来实现Delete结构

#[derive(Accounts)]
pub struct Delete<'info> {
#[account(mut)]
pub user: Signer<'info>,

#[account(
mut,
seeds = [b"message", user.key().as_ref()],
bump = message_account.bump,
close= user,
)]
pub message_account: Account<'info, MessageAccount>,
}

其中close= user指定账户将被关闭,其lamports将转移给user,对应的user标记为mut.奇怪的是此时没有系统程序的参与

最后实现delete函数:

pub fn delete(_ctx: Context<Delete>) -> Result<()> {
msg!("Delete Message");
Ok(())
}

功能被结构Delete之中的close实现,这个函数无事可干

此时如果你也是用Playground的在线ide,就可以直接build && deploy

2.test
import { PublicKey } from "@solana/web3.js";

describe("pda", () => {
> to be added firstly
it("Create Message Account", async () => {});

it("Update Message Account", async () => {});

it("Delete Message Account", async () => {});
});

这是test最开始的样子,向第一个it前添加以下code

const program = pg.program;
const wallet = pg.wallet;

const [messagePda, messageBump] = PublicKey.findProgramAddressSync(
[Buffer.from("message"), wallet.publicKey.toBuffer()],
program.programId,
);

program行允许访问客户端库,wallet行是我的plaground 钱包.其余的演示如何用程序中指定的种子导出pda

接下来写create的test部分:

it("Create Message Account", async () => {
const message = "Hello, World!";
const transactionSignature = await program.methods
.create(message)
.accounts({
messageAccount: messagePda,
})
.rpc({ commitment: "confirmed" });

const messageAccount = await program.account.messageAccount.fetch(
messagePda,
"confirmed",
);

console.log(JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,
);
});

签名,获取messageAccount.输出日志

image-20240805004511493

继续update部分:

it("Update Message Account", async () => {
const message = "Hello, Solana!";
const transactionSignature = await program.methods
.update(message)
.accounts({
messageAccount: messagePda,
})
.rpc({ commitment: "confirmed" });

const messageAccount = await program.account.messageAccount.fetch(
messagePda,
"confirmed",
);

console.log(JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,
);
});

还有delete:

it("Delete Message Account", async () => {
const transactionSignature = await program.methods
.delete()
.accounts({
messageAccount: messagePda,
})
.rpc({ commitment: "confirmed" });

const messageAccount = await program.account.messageAccount.fetchNullable(
messagePda,
"confirmed",
);

console.log("Expect Null:", JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,
);
});

写完了就test看一眼吧

image-20240805004811078

3.Cross Program Invocation(CPI)

前两节的程序将在这里开始得到修改,用CPI实现从这个程序与其他程序的交互。

CPI:一个程序调用另一个程序的指令,另一个程序的指令类似于向互联网公开的API。

image-20240811145514716

发起CPI时,在A上的签名者权限会扩展到被调用的B上。这样的迭代调用深度最高为4,程序可以代表从其 ID 派生的PDA进行“签名”。

在solana_program crate中的invokeinvoke_signed各自对应没有PDA签名和有PDA签名的调用。

用CPI实现transfer的案例:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::{program::invoke, system_instruction};

declare_id!("55xRZZnhSk1aN6seNTj75mThJEjZkBRYPQJ8qvKVh1eC");

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

pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {
let from_pubkey = ctx.accounts.sender.to_account_info();
let to_pubkey = ctx.accounts.recipient.to_account_info();
let program_id = ctx.accounts.system_program.to_account_info();

let instruction =
&system_instruction::transfer(&from_pubkey.key(), &to_pubkey.key(), amount);

invoke(instruction, &[from_pubkey, to_pubkey, program_id])?;
Ok(())
}
}

#[derive(Accounts)]
pub struct SolTransfer<'info> {
#[account(mut)]
sender: Signer<'info>,
#[account(mut)]
recipient: SystemAccount<'info>,
system_program: Program<'info, System>,
}

可以看到,invoke时需要传入的参数有instruction和一个切片

image-20240811152013245

实际上调用的是system的transfer,transfer需要from和to两个公钥,所以传的切片里有这三个地址

再看一个invoke_signed的例子:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::{program::invoke_signed, system_instruction};

declare_id!("EyxvVL2akUZZHx4DXzYzCroKLmigPrS2WgpSetKzM9wh");

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

pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {
let from_pubkey = ctx.accounts.pda_account.to_account_info();
let to_pubkey = ctx.accounts.recipient.to_account_info();
let program_id = ctx.accounts.system_program.to_account_info();

let seed = to_pubkey.key();
let bump_seed = ctx.bumps.pda_account;
let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];

let instruction =
&system_instruction::transfer(&from_pubkey.key(), &to_pubkey.key(), amount);

invoke_signed(
instruction,
&[from_pubkey, to_pubkey, program_id],
signer_seeds,
)?;
Ok(())
}
}

#[derive(Accounts)]
pub struct SolTransfer<'info> {
#[account(
mut,
seeds = [b"pda", recipient.key().as_ref()],
bump,
)]
pda_account: SystemAccount<'info>,
#[account(mut)]
recipient: SystemAccount<'info>,
system_program: Program<'info, System>,
}

和前一个对比:

  1. sender不同,这个是从pda_account开始send sol
  2. recipient对invoke_signed提供seed,pda提供bump seed并一起签名
4.CPI in anchor

继续更改之前的程序,从Update开始:(这里的修改和Delete的struct相同)

use anchor_lang::system_program::{transfer, Transfer};
//将这行添加到开头
//下面对struct Update修改
#[derive(Accounts)]
#[instruction(message: String)]
pub struct Update<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [b"vault", user.key().as_ref()],
bump,
)]
pub vault_account: SystemAccount<'info>,//新增
#[account(
mut,
seeds = [b"message", user.key().as_ref()],
bump = message_account.bump,
realloc = 8 + 32 + 4 + message.len() + 1,
realloc::payer = user,
realloc::zero = true,
)]
pub message_account: Account<'info, MessageAccount>,
pub system_program: Program<'info, System>,
}

新增的vault将在用户修改message时接受sol并存储这些token

在update的函数中补全这一功能的实现:

    pub fn update(ctx: Context<Update>, message: String) -> Result<()> {
msg!("Update Message: {}", message);
let account_data = &mut ctx.accounts.message_account;
account_data.message = message;

+ let transfer_accounts = Transfer {
+ from: ctx.accounts.user.to_account_info(),
+ to: ctx.accounts.vault_account.to_account_info(),
+ };
+ let cpi_context = CpiContext::new(
+ ctx.accounts.system_program.to_account_info(),
+ transfer_accounts,
+ );
+ transfer(cpi_context, 1_000_000)?;
Ok(())
}

可以看到anchor封装了cpi,简洁地实现了transfer

在delete函数里也加上sol的处理

+    pub fn delete(ctx: Context<Delete>) -> Result<()> {
msg!("Delete Message");

+ let user_key = ctx.accounts.user.key();
+ let signer_seeds: &[&[&[u8]]] =
+ &[&[b"vault", user_key.as_ref(), &[ctx.bumps.vault_account]]];
+
+ let transfer_accounts = Transfer {
+ from: ctx.accounts.vault_account.to_account_info(),
+ to: ctx.accounts.user.to_account_info(),
+ };
+ let cpi_context = CpiContext::new(
+ ctx.accounts.system_program.to_account_info(),
+ transfer_accounts,
+ ).with_signer(signer_seeds);
+ transfer(cpi_context, ctx.accounts.vault_account.lamports())?;
Ok(())
}

这里的cpi加上了with_signer,也是实现了功能

再rebuild程序并deploy。和etherum不一样的是,新程序可以直接在旧程序的地址上部署,共享相同的id

新的test如下:(不熟ts,直接给)

import { PublicKey } from "@solana/web3.js";

describe("pda", () => {

const program = pg.program;
const wallet = pg.wallet;

const [messagePda, messageBump] = PublicKey.findProgramAddressSync(
[Buffer.from("message"), wallet.publicKey.toBuffer()],
program.programId,
);
const [vaultPda, vaultBump] = PublicKey.findProgramAddressSync(
[Buffer.from("vault"),wallet.publicKey.toBuffer()],
program.programId,
);
it("Create Message Account", async () => {
const message = "Hello, World!";
const transactionSignature = await program.methods
.create(message)
.accounts({
messageAccount: messagePda,
})
.rpc({ commitment: "confirmed" });

const messageAccount = await program.account.messageAccount.fetch(
messagePda,
"confirmed",
);

console.log(JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,
);
});

it("Update Message Account", async () => {
const message = "Hello, Solana!";
const transactionSignature = await program.methods
.update(message)
.accounts({
messageAccount: messagePda,
vaultAccount: vaultPda
})
.rpc({ commitment: "confirmed" });

const messageAccount = await program.account.messageAccount.fetch(
messagePda,
"confirmed",
);

console.log(JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,
);
});

it("Delete Message Account", async () => {
const transactionSignature = await program.methods
.delete()
.accounts({
messageAccount: messagePda,
vaultAccount: vaultPda
})
.rpc({ commitment: "confirmed" });

const messageAccount = await program.account.messageAccount.fetchNullable(
messagePda,
"confirmed",
);

console.log("Expect Null:", JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,
);
});
});

就是不知道为啥我只有10sol,在前一个程序部署后剩下6.any,test并再次部署+test后我的sol增加到了11.6···有空一定得溯源看看怎么多出来的