您正在查看: Solana 分类下的文章

Solana基础 - 使用不同签名者修改账户

文章详细介绍了在Solana区块链上如何使用不同的签名者初始化账户并进行更新操作,通过Rust代码和客户端代码示例,展示了如何实现账户管理和权限控制。

在我们迄今为止的 Solana 教程中,我们只初始化并写入一个账户。

在实际操作中,这非常有限制。举个例子,如果用户 Alice 正在将积分转移给 Bob,Alice 必须能够写入由用户 Bob 初始化的账户。

在本教程中,我们将演示如何使用一个钱包初始化一个账户,并使用另一个钱包更新它。

初始化步骤

我们用于初始化账户的 Rust 代码没有改变:

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = signer,
              space=size_of::<MyStorage>() + 8,
              seeds = [],
              bump)]
    pub my_storage: Account<'info, MyStorage>,

    #[account(mut)]
    pub signer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[account]
pub struct MyStorage {
    x: u64,
}

使用备用钱包进行初始化事务

然而,客户端代码中有一个重要的变化:– 为了测试,我们创建了一个名为 newKeypair 的新钱包。与 Anchor 默认提供的一个不同。– 我们向新钱包空投 1 SOL,这样它可以支付交易费用。– 请注意注释 // THIS MUST BE EXPLICITLY SPECIFIED。我们正在将该钱包的 publicKey 传递给 Signer 字段。当我们使用内置 Anchor 的默认签名者时,Anchor 在后台为我们传递这个。但是,当我们使用不同的钱包时,我们需要显式提供这个。– 我们通过 .signers([newKeypair]) 配置设置签名者为 newKeypair

在这个代码片段之后我们将解释为何我们看起来是在两次指定签名者:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherWrite } from "../target/types/other_write";

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  await confirmTransaction(airdropTx);
}

async function confirmTransaction(tx) {
  const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
  await anchor.getProvider().connection.confirmTransaction({
    blockhash: latestBlockHash.blockhash,
    lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
    signature: tx,
  });
}

describe("other_write", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    const newKeypair = anchor.web3.Keypair.generate();
    await airdropSol(newKeypair.publicKey, 1e9); // 1 SOL

    let seeds = [];
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    await program.methods.initialize().accounts({
      myStorage: myStorage,
      signer: newKeypair.publicKey // ** THIS MUST BE EXPLICITLY SPECIFIED **
    }).signers([newKeypair]).rpc();
  });
});

signer 这个键并不一定要叫 signer

练习:在 Rust 代码中,将 payer = signer 改为 payer = fren,并将 pub signer: Signer<'info> 改为 pub fren: Signer<'info>,然后在测试中将 signer: newKeypair.publicKey 改为 fren: newKeypair.publicKey。初始化应该会成功,测试应该通过。

为什么 Anchor 需要指定 Signer 和 publicKey?

乍一看,似乎我们在两次指定签名者是多余的,但让我们仔细看看:

公共密钥在此传递

在红色框中,我们看到 fren 字段被指定为一个 Signer 账户。Signer 类型意味着 Anchor 将查看交易的签名,并确保签名与此处传递的地址匹配。

稍后我们将看到如何使用它来验证签名者是否被授权进行某些交易。

Anchor 一直在幕后执行这一操作,但由于我们传入了一个与 Anchor 默认使用的签名者不同的 Signer,所以我们必须明确指定 Signer 是哪个账户。

错误:在 Solana Anchor 中未知的签名者

unknown signer 错误发生在交易的签名者与传递给 Signer 的 public key 不匹配时。

假设我们修改了测试,去除 .signers([newKeypair]) 规范。Anchor 将使用默认签名者,而默认签名者不会匹配我们的 newKeypair 钱包的 publicKey

去掉 .signers([newKeypair])

我们将得到以下错误:

错误:签名验证失败

类似地,如果我们没有明确传入 publicKey,Anchor 将默默地使用默认签名者:

未传入 publicKey

我们将得到以下 错误:未知的签名者

错误:未知的签名者

有些误导性地,Anchor 并不是因为没有指定签名者而说签名者是未知的。Anchor 能够推断出如果没有指定签名者,则将使用默认签名者。如果我们同时去除 .signers([newKeypair]) 代码和 fren: newKeypair.publicKey 代码,那么 Anchor 将为验证公共密钥进行签名,并且验证签名者的签名是否与公共密钥匹配。

以下代码将导致成功初始化,因为 Signer 的公共密钥和签署交易的账户都是 Anchor 默认签名者。

await program.methods.initialize().accounts({
  myStorage: myStorage
}).rpc();

other_write 初始化

Bob 可以写入 Alice 初始化的账户

下面我们展示一个带有初始化账户和写入功能的 Anchor 程序。

这将让人熟悉我们的 Solana 计数器程序教程,但注意底部标记的 // THIS FIELD MUST BE INCLUDED 注释中的小更改:

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }

    pub fn update_value(ctx: Context<UpdateValue>, new_value: u64) -> Result<()> {
        ctx.accounts.my_storage.x = new_value;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = fren,
              space=size_of::<MyStorage>() + 8,
              seeds = [],
              bump)]
    pub my_storage: Account<'info, MyStorage>,

    #[account(mut)]
    pub fren: Signer<'info>, // 此处传递了一个公共密钥

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateValue<'info> {
    #[account(mut, seeds = [], bump)]
    pub my_storage: Account<'info, MyStorage>,

    // THIS FIELD MUST BE INCLUDED
    #[account(mut)]
    pub fren: Signer<'info>,
}

#[account]
pub struct MyStorage {
    x: u64,
}

以下客户端代码将为 Alice 和 Bob 创建钱包,并向他们分别空投 1 SOL。Alice 将初始化 MyStorage 账户,Bob 将写入它:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherWrite } from "../target/types/other_write";

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  await confirmTransaction(airdropTx);
}

async function confirmTransaction(tx) {
  const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
  await anchor.getProvider().connection.confirmTransaction({
    blockhash: latestBlockHash.blockhash,
    lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
    signature: tx,
  });
}

describe("other_write", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    const alice = anchor.web3.Keypair.generate();
    const bob = anchor.web3.Keypair.generate();

    const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_tx);

    const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_bob);

    let seeds = [];
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    // ALICE 初始化账户
    await program.methods.initialize().accounts({
      myStorage: myStorage,
      fren: alice.publicKey
    }).signers([alice]).rpc();

    // BOB 写入账户
    await program.methods.updateValue(new anchor.BN(3)).accounts({
      myStorage: myStorage,
      fren: bob.publicKey
    }).signers([bob]).rpc();

    let value = await program.account.myStorage.fetch(myStorage);
    console.log(`存储的值为 ${value.x}`);
  });
});

限制对 Solana 账户的写入

在实际应用中,我们不希望 Bob 随意向任意账户写入数据。让我们创建一个基本示例,用户可以用 10 积分初始化一个账户,并将这些积分转移到另一个账户。(显而易见的问题是黑客可以使用不同钱包创建任意数量的账户,但这超出了我们示例的范围)。

构建一个原型 ERC20 程序

Alice 应该能够修改她的账户和 Bob 的账户。也就是说,她应该能够扣除她的积分并转到 Bob 的积分。她不应该扣除 Bob 的积分——只有 Bob 才能这样做。

根据惯例,我们称一个能够对账户进行特权更改的地址为 Solana 中的“authority”。在账户结构中存储“authority”字段是一种常见模式,以表示只有该账户才能对该账户进行敏感操作(如在我们的示例中扣除积分)。

这在一定程度上类似于 Solidity 中的 onlyOwner 模式,不过它适用于单个账户,而不是整个合约:

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("HFmGQX4wPgPYVMFe4WrBi925NKvGySrEG2LGyRXsXJ4Z");

const STARTING_POINTS: u32 = 10;

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.player.points = STARTING_POINTS;
        ctx.accounts.player.authority = ctx.accounts.signer.key();
        Ok(())
    }

    pub fn transfer_points(ctx: Context<TransferPoints>,
                           amount: u32) -> Result<()> {
        require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority);
        require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints);

        ctx.accounts.from.points -= amount;
        ctx.accounts.to.points += amount;
        Ok(())
    }
}

#[error_code]
pub enum Errors {
    #[msg("SignerIsNotAuthority")]
    SignerIsNotAuthority,
    #[msg("InsufficientPoints")]
    InsufficientPoints
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = signer,
              space = size_of::<Player>() + 8,
              seeds = [&(signer.as_ref().key().to_bytes())],
              bump)]
    player: Account<'info, Player>,
    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct TransferPoints<'info> {
    #[account(mut)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    #[account(mut)]
    signer: Signer<'info>,
}

#[account]
pub struct Player {
    points: u32,
    authority: Pubkey
}

请注意,我们使用签名者的地址(&(signer.as_ref().key().to_bytes()))来推导存储其积分的账户的地址。这类似于 Solidity 中的 mapping in Solana,其中 Solana “msg.sender / tx.origin” 是键。

initialize 函数中,程序将初始积分设置为 10,并将 authority 设置为 signer。用户无法控制这些初始值。

transfer_points 函数使用 Solana Anchor require 宏和错误代码宏 来确保:1)交易的签名者是正在扣减余额的账户的管理员;2)该账户有足够的积分余额进行转移。

测试代码库应该易于理解。Alice 和 Bob 初始化他们的账户,然后 Alice 把 5 分转移给 Bob:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Points } from "../target/types/points";

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  await confirmTransaction(airdropTx);
}

async function confirmTransaction(tx) {
  const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
  await anchor.getProvider().connection.confirmTransaction({
    blockhash: latestBlockHash.blockhash,
    lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
    signature: tx,
  });
}

describe("points", () => {
  anchor.setProvider(anchor.AnchorProvider.env());
  const program = anchor.workspace.Points as Program<Points>;


  it("Alice transfers points to Bob", async () => {
    const alice = anchor.web3.Keypair.generate();
    const bob = anchor.web3.Keypair.generate();

    const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_tx);

    const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_bob);

    let seeds_alice = [alice.publicKey.toBytes()];
    const [playerAlice, _bumpA] = anchor.web3.PublicKey.findProgramAddressSync(seeds_alice, program.programId);

    let seeds_bob = [bob.publicKey.toBytes()];
    const [playerBob, _bumpB] = anchor.web3.PublicKey.findProgramAddressSync(seeds_bob, program.programId);

    // Alice 和 Bob 初始化他们的账户
    await program.methods.initialize().accounts({
      player: playerAlice,
      signer: alice.publicKey,
    }).signers([alice]).rpc();

    await program.methods.initialize().accounts({
      player: playerBob,
      signer: bob.publicKey,
    }).signers([bob]).rpc();

    // Alice 将 5 积分转移给 Bob。请注意,这是一个 u32
    // 所以我们不需要一个 BigNum
    await program.methods.transferPoints(5).accounts({
      from: playerAlice,
      to: playerBob,
      signer: alice.publicKey,
    }).signers([alice]).rpc();

    console.log(`Alice 现在有 ${(await program.account.player.fetch(playerAlice)).points} 积分`);
    console.log(`Bob 现在有 ${(await program.account.player.fetch(playerBob)).points} 积分`);
  });
});

练习:创建一个 keypair mallory,尝试通过在 .signers([mallory]) 中使用 mallory 来窃取 Alice 或 Bob 的积分。你的攻击应该会失败,但你还是应该尝试。

使用 Anchor 约束替代 require! 宏

另一种替代写法 require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority); 的方法是使用 Anchor 约束。Anchor 账户文档提供了一系列可用的约束。

Anchor has_one 约束

has_one 约束假设在 #[derive(Accounts)]#[account] 之间存在“共享键”,并检查这两个键的值是否相等。最好的展示方式是用图片:

从属结构 TransferPoints

在幕后,如果作为交易的一部分传递的 authority 账户(作为 Signer)不等于账户中存储的 authority,则 Anchor 将阻止该交易。

在我们上面的实现中,我们在账户中使用了字符 authority,而在 #[derive(Accounts)] 中用 signer。这种键名不匹配将阻止这个宏的工作,因此,上面的代码将 signer 的键更改为 authorityauthority 不是一个特殊的关键字,只是一种约定。作为练习,你可以将所有 authority 实例改为 fren,代码将会以同样的方式工作。

Anchor constraint 约束

我们还可以将宏 require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints); 替换为 Anchor 约束。

constraint 宏允许我们对传递给交易的账户和账户中的数据施加任意约束。在我们的情况下,我们希望确保发送者有足够的积分:

#[derive(Accounts)]
#[instruction(amount: u32)] // amount 必须做为指令传递
pub struct TransferPoints<'info> {
    #[account(mut,
              has_one = authority,
              constraint = from.points >= amount)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    authority: Signer<'info>,
}

#[account]
pub struct Player {
    points: u32,
    authority: Pubkey
}

宏足够聪明,能识别出 from 是基于传递进来的 from 键的账户,并且该账户有 points 字段。来自 transfer_points 函数参数的 amount 必须通过 instruction 宏进行传递,以便 constraint 宏将 amount 与账户中的积分余额进行比较。

向 Anchor 约束添加自定义错误消息

我们可以通过添加自定义错误来提高约束违规时错误消息的可读性,这些自定义错误与我们通过 @ 符号传递给 require! 宏的错误相同:

#[derive(Accounts)]
#[instruction(amount: u32)]
pub struct TransferPoints<'info> {
    #[account(mut,
              has_one = authority @ Errors::SignerIsNotAuthority,
              constraint = from.points >= amount @ Errors::InsufficientPoints)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    authority: Signer<'info>,
}

#[account]
pub struct Player {
    points: u32,
    authority: Pubkey
}

之前在 Rust 代码中定义的 Errors 枚举使用了这些错误,并在 require! 宏中用到了它们。

练习:修改测试以违反 has_oneconstraint 宏,并观察错误消息。

转载:https://learnblockchain.cn/article/11408

Solana基础 - 在 Solana 上实现 SOL 转账及构建支付分配器

在 Solana 上实现 SOL 转账及构建支付分配器
本教程将介绍 Solana Anchor 程序如何在交易中转移 SOL 的机制。

与以太坊不同,在以太坊中,钱包通过 msg.value 指定交易的一部分并“推送” ETH 到合约,而 Solana 程序则是从钱包“拉取” Solana。

因此,没有“可支付”函数或“msg.value”这样的概念。

下面我们创建了一个新的 anchor 项目,名为 sol_splitter,并放置了将 SOL 从发送者转移到接收者的 Rust 代码。

当然,如果发送者直接发送 SOL,而不是通过程序来完成,这样会更高效,但我们想要说明的是如何做到这一点:

    use anchor_lang::prelude::*;
    use anchor_lang::system_program;

    declare_id!("9qnGx9FgLensJQy1hSB4b8TaRae6oWuNDveUrxoYatr7");

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

        pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> {

            let cpi_context = CpiContext::new(
                ctx.accounts.system_program.to_account_info(), 

                system_program::Transfer {
                    from: ctx.accounts.signer.to_account_info(),
                    to: ctx.accounts.recipient.to_account_info(),
                }
            );

            let res = system_program::transfer(cpi_context, amount);

            if res.is_ok() {
                return Ok(());
            } else {
                return err!(Errors::TransferFailed);
            }
        }
    }

    #[error_code]
    pub enum Errors {
        #[msg("transfer failed")]
        TransferFailed,
    }

    #[derive(Accounts)]
    pub struct SendSol<'info> {
        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipient: UncheckedAccount<'info>,

        system_program: Program<'info, System>,

        #[account(mut)]
        signer: Signer<'info>,
    }

这里有很多内容需要解释。

引入 CPI:跨程序调用

在以太坊中,转移 ETH 只需在 msg.value 字段中指定一个值。在 Solana 中,一个名为 system program 的内置程序将 SOL 从一个账户转移到另一个账户。这就是为什么在我们初始化账户时,它不断出现并且需要支付费用来初始化它们。

你可以大致将系统程序视为以太坊中的预编译。想象一下,它的行为有点像内置于协议中的 ERC-20 代币,用作原生货币。它有一个名为 transfer 的公共函数。

CPI 交易的上下文

每当调用 Solana 程序函数时,必须提供一个 Context。该 Context 包含程序将要交互的所有账户。

调用系统程序没有什么不同。系统程序需要一个 Context,其中包含 fromto 账户。转移的 amount 作为“常规”参数传递——它不是 Context 的一部分(因为“amount”不是一个账户,它只是一个值)。

现在我们可以解释下面的代码片段:

cpi_context system_program transfer

我们正在构建一个新的 CpiContext,它将我们要调用的程序作为第一个参数(绿色框),以及将作为该交易一部分的账户(黄色框)。参数 amount 在这里没有提供,因为 amount 不是一个账户。

现在我们已经构建了 cpi_context,可以在指定金额的同时对系统程序进行跨程序调用(橙色框)。

这返回一个 Result<()> 类型,就像我们 Anchor 程序上的公共函数一样。

不要忽视跨程序调用的返回值。

要检查跨程序调用是否成功,我们只需检查返回值是否为 Ok。Rust 通过 is_ok() 方法使这变得简单:

            let res = system_program::transfer(cpi_context, amount);

            if res.is_ok() {
                return Ok(());
            } else {
                return err!(Errors::TransferFailed);
            }
        }
    }

    #[error_code]
    pub enum Errors {
        #[msg("transfer failed")]
        TransferFailed,
    }

只有签名者可以是“from”

如果你调用系统程序时 from 是一个不是 Signer 的账户,那么系统程序将拒绝该调用。没有签名,系统程序无法知道你是否授权了该调用。

TypeScript 代码:

    import * as anchor from "@coral-xyz/anchor";
    import { Program } from "@coral-xyz/anchor";
    import { SolSplitter } from "../target/types/sol_splitter";

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

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

      async function printAccountBalance(account) {
        const balance = await anchor.getProvider().connection.getBalance(account);
        console.log(`${account} has ${balance / anchor.web3.LAMPORTS_PER_SOL} SOL`);
      }

      it("Transmit SOL", async () => {
        // generate a new wallet
        const recipient = anchor.web3.Keypair.generate();

        await printAccountBalance(recipient.publicKey);

        // send the account 1 SOL via the program
        let amount = new anchor.BN(1 * anchor.web3.LAMPORTS_PER_SOL);
        await program.methods.sendSol(amount)
          .accounts({recipient: recipient.publicKey})
          .rpc();

        await printAccountBalance(recipient.publicKey);
      });
    });

一些需要注意的事项:

  • 我们创建了一个辅助函数 printAccountBalance 来显示余额的前后
  • 我们使用 anchor.web3.Keypair.generate() 生成了接收者钱包
  • 我们将 1 SOL 转移到新账户

当我们运行代码时,预期结果如下。打印语句是接收者地址的前后余额:

result sol_sprinter

练习:构建一个 Solana 程序,将传入的 SOL 平均分配给两个接收者。你将无法通过函数参数来完成此操作,账户需要在 Context 结构中。

构建支付分割器:使用 remaining_accounts 处理任意数量的账户。

我们可以看到,如果我们想将 SOL 分配给多个账户,必须指定一个 Context 结构会显得相当笨拙:

    #[derive(Accounts)]
    pub struct SendSol<'info> {
        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipient1: UncheckedAccount<'info>,

        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipient2: UncheckedAccount<'info>,

        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipient3: UncheckedAccount<'info>,

        // ...

        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipientn: UncheckedAccount<'info>,

        system_program: Program<'info, System>,

        #[account(mut)]
        signer: Signer<'info>,
    }

为了解决这个问题,Anchor 在 Context 结构中添加了一个 remaining_accounts 字段。

下面的代码说明了如何使用这个特性:

    use anchor_lang::prelude::*;
    use anchor_lang::system_program;

    declare_id!("9qnGx9FgLensJQy1hSB4b8TaRae6oWuNDveUrxoYatr7");

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

        // 'a, 'b, 'c 是 Rust 生命周期,暂时忽略它们
        pub fn split_sol<'a, 'b, 'c, 'info>(
            ctx: Context<'a, 'b, 'c, 'info, SplitSol<'info>>,
            amount: u64,
        ) -> Result<()> {

            let amount_each_gets = amount / ctx.remaining_accounts.len() as u64;
            let system_program = &ctx.accounts.system_program;

            // 注意关键字 `remaining_accounts`
            for recipient in ctx.remaining_accounts {
                let cpi_accounts = system_program::Transfer {
                    from: ctx.accounts.signer.to_account_info(),
                    to: recipient.to_account_info(),
                };
                let cpi_program = system_program.to_account_info();
                let cpi_context = CpiContext::new(cpi_program, cpi_accounts);

                let res = system_program::transfer(cpi_context, amount_each_gets);
                if !res.is_ok() {
                    return err!(Errors::TransferFailed);
                }
            }

            Ok(())
        }
    }

    #[error_code]
    pub enum Errors {
        #[msg("transfer failed")]
        TransferFailed,
    }

    #[derive(Accounts)]
    pub struct SplitSol<'info> {
        #[account(mut)]
        signer: Signer<'info>,
        system_program: Program<'info, System>,
    }

这是 TypeScript 代码:

    import * as anchor from "@coral-xyz/anchor";
    import { Program } from "@coral-xyz/anchor";
    import { SolSplitter } from "../target/types/sol_splitter";

    describe("sol_splitter", () => {
      // 配置客户端以使用本地集群。
      anchor.setProvider(anchor.AnchorProvider.env());

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

      async function printAccountBalance(account) {
        const balance = await anchor.getProvider().connection.getBalance(account);
        console.log(`${account} has ${balance / anchor.web3.LAMPORTS_PER_SOL} SOL`);
      }

      it("Split SOL", async () => {
        const recipient1 = anchor.web3.Keypair.generate();
        const recipient2 = anchor.web3.Keypair.generate();
        const recipient3 = anchor.web3.Keypair.generate();

        await printAccountBalance(recipient1.publicKey);
        await printAccountBalance(recipient2.publicKey);
        await printAccountBalance(recipient3.publicKey);

        const accountMeta1 = {pubkey: recipient1.publicKey, isWritable: true, isSigner: false};
        const accountMeta2 = {pubkey: recipient2.publicKey, isWritable: true, isSigner: false};
        const accountMeta3 = {pubkey: recipient3.publicKey, isWritable: true, isSigner: false};

        let amount = new anchor.BN(1 * anchor.web3.LAMPORTS_PER_SOL);
        await program.methods.splitSol(amount)
          .remainingAccounts([accountMeta1, accountMeta2, accountMeta3])
          .rpc();

        await printAccountBalance(recipient1.publicKey);
        await printAccountBalance(recipient2.publicKey);
        await printAccountBalance(recipient3.publicKey);
      });
    });

运行测试显示了之前和之后的余额:

test result Split SOL

这里是对 Rust 代码的一些评论:

Rust 生命周期

split_sol 的函数声明有一些奇怪的语法:

    pub fn split_sol<'a, 'b, 'c, 'info>(
        ctx: Context<'a, 'b, 'c, 'info, SplitSol<'info>>,
        amount: u64,
    ) -> Result<()>

'a'b'c 是 Rust 生命周期。Rust 生命周期是一个复杂的话题,我们暂时不想涉及。但简单来说,Rust 代码需要确保在循环 for recipient in ctx.remaining_accounts 中传入的资源在整个循环期间始终存在。

ctx.remaining_accounts

循环通过 for recipient in ctx.remaining_accounts 遍历。关键字 remaining_accounts 是 Anchor 机制,用于传递任意数量的账户,而不必在 Context 结构中创建一堆键。

在 TypeScript 测试中,我们可以像这样将 remaining_accounts 添加到事务中:

    await program.methods.splitSol(amount)
      .remainingAccounts([accountMeta1, accountMeta2, accountMeta3])
      .rpc();

转载:https://learnblockchain.cn/article/10310

Solana基础 - 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因

文章详细介绍了 Solana 区块链中缺少的一些功能,如 fallback 和 receive 函数、view 和 pure 函数,以及 Solidity 中的自定义修饰符和单位在 Rust 中的缺失。此外,文章还讨论了 Solana 中读取账户数据的方式和防止重入攻击的策略。

Solana 没有 fallback 或 receive 函数

Solana 交易必须预先指定它将修改或读取的账户作为交易的一部分。如果“fallback”函数访问了一个不确定的账户,那么整个交易将失败。这将使用户不得不预测 fallback 函数将访问的账户。因此,直接不允许这种类型的函数更为简单。

Solana 没有“view”或“pure”函数的概念

Solidity 中的“view”函数通过两种机制保证状态不会改变:

  • view 函数中的所有外部调用都是 静态调用(如果发生状态更改,调用将回滚)
  • 如果编译器检测到状态更改的操作码,它将抛出错误

纯函数更进一步,编译器会检查是否存在查看状态的操作码。

这些函数限制主要发生在编译器层面,而 Anchor 没有实现任何这些编译器检查。Anchor 并不是构建 Solana 程序的唯一框架。Seahorse 是另一个框架。也许其他框架会带有明确说明函数可以做什么和不能做什么的函数注解,但目前我们可以依赖以下保证:如果某个账户未包含在 Context 结构体定义中,该函数将不会访问该账户。

并不意味着该账户完全无法被访问。例如,我们可以编写一个单独的程序来读取账户,并以某种方式将该数据转发给相关函数。

最后,Solana 虚拟机或运行时中没有 staticcall 这样的东西。

无论如何,view 函数在 Solana 中并不必要

因为 Solana 程序可以读取传递给它的任何账户,所以它可以读取另一个程序拥有的账户。

拥有账户的程序不需要实现 view 函数来允许另一个程序查看该账户。web3 js 客户端或其他程序可以直接查看 Solana 账户数据

这有一个非常重要的含义:

无法使用重入锁来直接防御 Solana 中的只读重入。程序必须为读者暴露一个标志,以知道数据是否可靠。

只读重入发生在受害合约访问显示被操纵数据的 view 函数时。在 Solidity 中,可以通过在 view 函数中添加 nonReentrant 修饰符来防御。然而,在 Solana 中,无法阻止另一个程序查看账户中的数据。

然而,Solana 程序仍然可以实现重入防护使用的标志。消费另一个程序账户的程序可以检查这些标志,以查看账户是否可能处于重入状态,从而不应被信任。

Rust 中没有自定义修饰符

onlyOwnernonReentrant 这样的自定义修饰符是 Solidity 的构造,而不是 Rust 中可用的功能。

Rust 或 Anchor 中不可用自定义单位

由于 Solidity 与以太坊紧密相关,它有方便的像 etherswei 这样的关键字来衡量以太币。不出所料,LAMPORTS_PER_SOL 在 Rust 中没有定义,但让人有些惊讶的是,它在 Anchor Rust 框架中也没有定义。不过,它在 Solana web3 js 库中是可用的。

同样,Solidity 中有 days 作为 84,600 秒的方便别名,但在 Rust/Anchor 中没有这样的等价物。

Solana 中没有所谓的“payable”函数。程序从用户那里转移 SOL,用户不将 SOL 转移给程序

这是下一个教程的主题。

转载:https://learnblockchain.cn/article/11413

Solana基础 - 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance

文章介绍了如何在Solana程序中使用Anchor框架读取账户余额,并详细解释了UncheckedAccount的使用及其安全性考虑。

在Anchor Rust中读取账户余额

要在Solana程序中读取地址的Solana余额,请使用以下代码:

use anchor_lang::prelude::*;

declare_id!("Gnf6u7S7fGJbqEGH9PuDE5Prq6f6ZrDxHY3jNJ4SYySQ");

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

    pub fn read_balance(ctx: Context<ReadBalance>) -> Result<()> {
        let balance = ctx.accounts.acct.to_account_info().lamports();

        msg!("余额以Lamports表示为 {}", balance);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct ReadBalance<'info> {
    /// CHECK: 尽管我们读取这个账户的余额,但我们并没有用到这个信息
    pub acct: UncheckedAccount<'info>,
}

下面是触发它的web3 js代码:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Balance } from "../target/types/balance";

describe("balance", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

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

  // 以下是我们使用的Solana钱包
  let pubkey = new anchor.web3.PublicKey("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj");


  it("测试余额", async () => {
    const tx = await program.methods.readBalance().accounts({ acct: pubkey }).rpc();
  });
});

本示例中的某些项与以前的教程不同,特别是使用UncheckedAccount

在Solana Anchor中什么是UncheckedAccount

UncheckedAccount类型通知Anchor不检查正在读取的账户是否被该程序拥有。

注意,我们通过Context结构传递的账户并不是该程序初始化的账户,因此该程序并不拥有它。

当Anchor读取#[derive(Accounts)]中的Account类型账户时,它会检查(在后台)该账户是否被该程序拥有。如果不拥有,执行将会停止。

这作为一个重要的安全检查。

如果恶意用户构造了一个该程序未创建的账户,并将其传递给Solana程序,而Solana程序盲目信任该账户中的数据,就可能发生严重错误。

例如,如果该程序是一个银行,而该账户存储了用户的余额,那么黑客可以提供一个不同的账户,其人工作高于实际余额。

不过,要实施这个黑客,用户必须在单独的交易中创建虚假账户,然后将其传递给Solana程序。然而,Anchor框架会在后台检查该账户是否不被程序拥有,并拒绝读取该账户。

UncheckedAccount绕过了这个安全检查。

重要AccountInfoUncheckedAccount是彼此的别名,并且AccountInfo具有相同的安全性考虑。

在我们的案例中,我们传递的账户显然不被程序拥有——我们想检查一个任意账户的余额。因此,我们必须确保移除这个安全检查后没有关键逻辑可以被篡改。

在我们的案例中,我们只是将余额记录到控制台,但大多数现实案例会有更复杂的逻辑。

什么是/// CHECK:

由于使用UncheckedAccount的危险性,Anchor强制你包括此注释,以鼓励你不要忽视安全考虑。

练习:删除/// CHECK:注释并运行anchor build,你应该看到构建停止,并要求你添加回注释并解释为什么未经检查的账户是安全的。也就是说,读取不可信的账户可能是危险的,Anchor希望确保你不对账户中的数据执行任何关键操作。

为什么程序中没有#[account]结构?

#[account]结构告诉Anchor如何反序列化持有数据的账户。例如,以下看似的账户结构将通知Anchor将存储在账户中的数据反序列化为一个单一的u64

#[account]
pub struct Counter {
    counter: u64
}

然而,在我们的案例中,我们并不想从账户中读取数据——我们只想读取余额。这类似于我们可以读取以太坊地址的余额,而无法读取其代码。由于我们想反序列化数据,因此我们不提供#[account]结构。

并非所有账户中的SOL都是可支配的

回忆一下我们关于Solana账户租金的讨论,账户必须保持一定的SOL余额以便“免租”,否则运行时将删除该账户。仅仅因为账户中有“1 SOL”并不一定意味着该账户可以支配全部1 SOL。

例如,如果你正在构建一个质押或银行应用程序,其中用户存入的SOL存放在单独的账户中,那么简单地测量这些账户的SOL余额并不准确,因为租金将包含在余额中。

转载:https://learnblockchain.cn/article/11411

Solana基础 - Solana中的存储成本、最大存储容量和账户调整

文章详细介绍了Solana区块链中账户存储空间租金的计算方式及其相关概念,包括租金豁免、存储成本比较、账户大小限制和调整、以及部署程序的成本预估。

在分配存储空间时,付款方必须支付每字节分配一定数量的SOL。

Solana称之为“租赁”。这个名称有点误导,因为它暗示需要每月充值,但情况并非总是如此。一旦租金支付,就不再需要支付,即使两年过去了。当支付了两年的租金后,账户被认为是“免租”的。

这个名称来源于Solana最初按字节每年的形式向账户收费。如果你只支付了半年的租金,那么你的账户将在六个月后被删除。如果你提前支付了两年的租金,账户就会“免租”。该账户将不再需要支付租金。如今,所有账户都必须是免租的;你不能支付少于两年的租金。

虽然租金是按照“每字节”计算的,但零数据的账户不是免费的;Solana仍需要对其进行索引并存储与之相关的元数据。

当账户初始化时,所需租金的数量会在后台计算;你无需明确计算租金。

然而,你确实想要能够预测存储的成本,以便能够恰当地设计你的应用程序。

如果你想要一个快速的估算,运行 solana rent <字节数> 在命令行中会给出快速的答案:solana rent 32

如前所述,分配零字节是非免费的:solana rent 0

让我们看看这个费用是如何计算的。

Anchor Rent Module给我们提供了一些与租金相关的常量:

ACCOUNT_STORAGE_OVERHEAD:该常量的值为128(字节),顾名思义,一个空账户有128字节的开销。
DEFAULT_EXEMPTION_THRESHOLD:该常量的值为2.0(浮点数64),指的是提前支付两年的租金使得账户免于支付进一步的租金。
DEFAULT_LAMPORTS_PER_BYTE_YEAR:该常量的值为3,480,意味着每字节每年需要3,480 lamports。由于我们需要支付两年, 每字节将花费6,960 lamports。
以下Rust程序打印出一个空账户将花费的金额。请注意,结果与上面的 solana rent 0 截图相符:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::rent as rent_module;

declare_id!("BfMny1VwizQh89rZtikEVSXbNCVYRmi6ah8kzvze5j1S");

[program]

pub mod rent {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let cost_of_empty_acc = rent_module:: ACCOUNT_STORAGE_OVERHEAD as f64 * 
                            rent_module::DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 *
                            rent_module::DEFAULT_EXEMPTION_THRESHOLD; 

    msg!("创建空账户的成本: {}", cost_of_empty_acc);
    // 890880

    Ok(())
}

}

[derive(Accounts)]

pub struct Initialize {}
如果我们想计算一个非空账户的费用,那么我们只需将字节数加入到空账户的费用中,如下所示:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::rent as rent_module;

declare_id!("BfMny1VwizQh89rZtikEVSXbNCVYRmi6ah8kzvze5j1S");

[program]

pub mod rent {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let cost_of_empty_acc = rent_module:: ACCOUNT_STORAGE_OVERHEAD as f64 * 
                            rent_module::DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 *
                            rent_module::DEFAULT_EXEMPTION_THRESHOLD;

    msg!("创建空账户的成本: {}", cost_of_empty_acc);
    // 890,880 lamports

    let cost_for_32_bytes = cost_of_empty_acc + 
                            32 as f64 * 
                            rent_module::DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 *
                            rent_module::DEFAULT_EXEMPTION_THRESHOLD;

    msg!("创建32字节账户的成本: {}", cost_for_32_bytes);
    // 1,113,600 lamports
    Ok(())
}

}

[derive(Accounts)]

pub struct Initialize {}
同样,请注意该程序的输出与命令行上的输出相符。

比较存储成本与ETH
截至本文写成时,ETH的价值约为 $2,425。初始化一个新账户的费用为22,100 gas,因此我们可以计算32字节的gas成本为$0.80,假设gas费为15 gwei。

目前,Solana的价格为
90
/
S
O
L
,因此支付
1
,
113
,
600
l
a
m
p
o
r
t
s
以初始化
32
字节存储将花费
90/SOL,因此支付1,113,600lamports以初始化32字节存储将花费0.10。

然而,ETH的市场资本是SOL的7.5倍,因此如果SOL的市场资本与ETH相同,SOL的当前价格将为$675,而32字节的存储将花费 $0.75。

Solana有一个永久的通胀模型,最终将收敛到每年1.5%,这应有助于反映出存储随着时间的推移变得更便宜,根据摩尔定律,成本相同的晶体管密度每18个月翻倍。

请记住,从字节到加密的转换是协议中设置的常量,可能在任何时候由于硬分叉而改变。

余额低于2年免租阈值的账户会减少直到账户被删除
可以在这里阅读一个用户钱包账户逐渐被“耗尽”的趣味Reddit帖子:https://www.reddit.com/r/solana/comments/qwin1h/my_sol_balance_in_the_wallet_is_decreasing/

原因是该钱包的余额低于租赁豁免阈值,Solana运行时正在慢慢减少账户余额以支付租金。

如果由于余额低于免租阈值而导致钱包最终被删除,可以通过向其发送更多SOL进行“复活”,但如果数据存储在账户中,则该数据将会丢失。

大小限制
当我们初始化一个账户时,大小不得超过10,240字节。

练习:创建一个基本的存储初始化程序并设置space=10241。这比限制高出1字节。你应该会看到类似以下的错误:solana 账户无法初始化,因为超过了大小限制

更改账户大小
如果你需要增加账户的大小,可以使用realloc宏。这在账户存储一个向量并需要更多空间时可能会很方便。增加1000字节的代码在increase_account_size函数和IncreaseAccountSize上下文结构中可以找到(请查看以下代码中的大写注释):

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

[program]

pub mod basic_storage {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
}

pub fn increase_account_size(ctx: Context<IncreaseAccountSize>) -> Result<()> {
    Ok(())
}

}

[derive(Accounts)]

pub struct IncreaseAccountSize<'info> {

#[account(mut,
          // ***** 增加1,000 BYTE点在这里 *****
          realloc = size_of::<MyStorage>() + 8 + 1000,
          realloc::payer = signer,
          realloc::zero = false,
          seeds = [],
          bump)]
pub my_storage: Account<'info, MyStorage>,

#[account(mut)]
pub signer: Signer<'info>,

pub system_program: Program<'info, System>,

}

[derive(Accounts)]

pub struct Initialize<'info> {

#[account(init,
          payer = signer,
          space=size_of::<MyStorage>() + 8,
          seeds = [],
          bump)]
pub my_storage: Account<'info, MyStorage>,

#[account(mut)]
pub signer: Signer<'info>,

pub system_program: Program<'info, System>,

}

[account]

pub struct MyStorage {
x: u64,
}
在增加账户大小时,如果你不想删除账户数据,请确保设置realloc::zero = false(在上面的代码中)。如果你希望账户数据被设置为全零,请使用realloc::zero = true。你不需要更改测试。该宏会为你在后台处理这些。

练习:在测试中初始化一个账户,然后调用increase_account_size函数。通过在命令行中执行solana account 查看账户大小。你需要在本地验证器上执行此操作,以使账户持续存在。

最大Solana账户大小
每次重新分配的最大账户增加为10240。Solana中账户的最大大小为10MB。

预测程序部署的成本
部署Solana程序的主要费用来自支付存储字节码的租金。字节码存储在与anchor deploy返回的地址不同的单独账户中。

下面的屏幕截图展示了如何获取此信息:程序部署成本

目前,简单的Hello World程序的部署费用略高于2.47 SOL。通过直接编写原生Rust代码而不是使用Anchor框架,费用可以显著降低,但我们不建议在你完全理解Anchor默认消除的所有安全风险之前这样做。

转载:https://learnblockchain.cn/article/11420