BCSkill (Block chain skill )
区块链中文技术社区

只讨论区块链底层技术
遵守一切相关法律政策!

Solana基础 - 在 Anchor 中:不同类型的账户

本文详细介绍了 Solana Anchor 框架中的 [derive(Accounts)] 宏,解释了 Solana 并行交易处理机制及其账户访问控制的重要性,并深入探讨了 Account、UncheckedAccount、Signer 和 Program 四种账户类型的使用场景和实现细节。

Anchor中的#[derive(Accounts)]:不同类型的账户

#[derive(Accounts)]在Solana Anchor中是一个类似属性的宏,用于包含所有在执行期间将被函数访问的账户的引用。

在Solana中,交易将要访问的每个账户必须提前指定

Solana之所以如此快速的一个原因是它以并行的方式执行交易。也就是说,如果Alice和Bob都想进行一笔交易,Solana将尝试同时处理他们的交易。然而,如果他们的交易因访问同一存储而发生冲突,就会出现问题。例如,假设Alice和Bob都在尝试写入同一账户。显然,他们的交易不能并行执行。

为了让Solana知道Alice和Bob的交易不能并行处理,Alice和Bob都必须提前指定所有他们的交易将更新的账户。

由于Alice和Bob都指定了一个(存储)账户,Solana运行时可以推断出两个交易之间存在冲突。必须选择一个(推测是支付了更高优先级费用的那个),另一个最终将失败。

这就是为什么每个函数都有自己单独的#[derive(Accounts)]结构体。结构体中的每个字段都是程序在执行期间打算(但并不要求)访问的账户。

一些以太坊开发者可能会注意到这一要求与EIP 2930访问列表交易的相似性。

账户的类型向Anchor信号你打算如何与该账户交互。

最常用的账户类型:Account、Unchecked Account、System Program和Signer

在我们初始化存储的代码中,我们看到了三种不同的“类型”账户:

  • Account
  • Signer
  • Program

这里是代码片段:

账户类型代码片段

当我们读取账户余额时,我们看到了第四种类型:

  • UncheckedAccount

这里是我们使用的代码:未检查账户的代码

我们用绿色框标出的每个项目都是通过文件顶部的anchor_lang::prelude::*;引入的。

AccountUncheckedAccountSignerProgram的目的是在继续之前对传入的账户进行某种检查,并且还提供与这些账户交互的函数。

我们将在接下来的部分进一步解释这四种类型。

Account

Account类型会检查被加载的账户的所有者是否确实被程序拥有。如果所有者不匹配,则不会加载。这作为一种重要的安全措施,以防止意外读取程序未创建的数据。

在以下示例中,我们创建了一个密钥对账户,并尝试将其传递给foo。因为该账户不属于程序,所以交易失败。

Rust:

use anchor_lang::prelude::*;

declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");

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

    pub fn foo(ctx: Context<Foo>) -> Result<()> {        
        // 我们不对账户SomeAccount做任何操作        
        Ok(())    
    }
}

##[derive(Accounts)]
pub struct Foo<'info> {    
    some_account: Account<'info, SomeAccount>,
}

##[account]
pub struct SomeAccount {}

Typescript:

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

describe("account_types", () => {
    async function airdropSol(publicKey, amount) {    
        let airdropTx = await anchor
            .getProvider()
            .connection.requestAirdrop(
                publicKey, 
                amount * anchor.web3.LAMPORTS_PER_SOL
            );  

        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,    
        });  
    }  

    // 配置客户端以使用本地区块链  
    anchor.setProvider(anchor.AnchorProvider.env());  

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

    it("账户拥有者错误", async () => {    
        const newKeypair = anchor.web3.Keypair.generate();    
        await airdropSol(newKeypair.publicKey, 10);    

        await program.methods
        .foo()
        .accounts({someAccount: newKeypair
        .publicKey}).rpc();  
    });
});

这是执行测试后的输出:

测试执行输出

如果我们向Account添加一个init宏,那么它将尝试将所有权从系统程序转移到该程序。然而,上面的代码没有init宏。

有关Account类型的更多信息可以在文档中找到:https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html

UncheckedAccount或AccountInfo

UncheckedAccountAccountInfo的别名。它不检查所有权,因此必须小心,因为它将接受任意账户。

这是使用UncheckedAccount读取一个不属于它的账户数据的示例。

use anchor_lang::prelude::*;

declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");

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

    pub fn foo(ctx: Context<Foo>) -> Result<()> {        
        let data = &ctx.accounts.some_account.try_borrow_data()?;        
        msg!("{:?}", data);        
        Ok(())    
    }
}

##[derive(Accounts)]
pub struct Foo<'info> {    
    /// 检查:我们只是打印数据    
    some_account: AccountInfo<'info>,
}

这是我们的Typescript代码。请注意,我们直接调用系统程序来创建密钥对账户,以便我们可以分配16字节的数据。

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

describe("account_types", () => {  
    const wallet = anchor.workspace.AccountTypes.provider.wallet;  

    // 配置客户端以使用本地区块链  
    anchor.setProvider(anchor.AnchorProvider.env());  

    const program = anchor.workspace.AccountTypes as Program<AccountTypes>;  
    it("使用账户信息加载账户", async () => {    
        // 创建一个不属于程序的账户    
        const newKeypair = anchor.web3.Keypair.generate();    
        const tx = new anchor.web3.Transaction().add(      
            anchor.web3.SystemProgram.createAccount({        
                fromPubkey: wallet.publicKey,        
                newAccountPubkey: newKeypair.publicKey,        
                space: 16,        
                lamports: await anchor          
                    .getProvider()                          
                    .connection
                    .getMinimumBalanceForRentExemption(32),             
                programId: program.programId,      
            })    
    );    

    await anchor.web3.sendAndConfirmTransaction(      
            anchor.getProvider().connection,      
            tx,      
            [wallet.payer, newKeypair]    
    );    

    // 读取账户中的数据    
    await program.methods      
            .foo()      
            .accounts({ someAccount: newKeypair.publicKey })      
            .rpc();  
    });
});

程序运行后,我们可以看到它打印了账户中的数据,该数据包含16个零字节:

在槽14298中执行的交易:
  签名:64fv6NqYB4tji9UfLpH8PgFDY1QV4vbMovrnnpw3271vStg7J5g1z1bm9YbE8Lobzozkc6y2YzLdgMjGdftCGKqv
  状态:成功
  日志消息:
    程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs调用[1]
    程序日志:指令:Foo
    程序日志:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs消耗5334的200000计算单位
    程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs成功

当我们传入任意地址时,我们需要使用这种账户类型,但在使用数据时要非常小心,因为黑客可能能够在账户中构造恶意数据,然后将其传递给Solana程序。

Signer

该类型将检查Signer账户是否签署了交易;它检查签名是否与账户的公钥匹配。

由于签名者也是一个账户,你可以读取签名者的余额或存储在账户中的数据(如果有的话),尽管它的主要目的是验证签名。

根据文档<https://docs.rs/anchor-lang/latest/anchor_lang/accounts/signer/struct.Signer.html%3E,`Signer`是一种验证账户签署了交易的类型。不会进行其他所有权或类型检查。如果使用了这个,便不应尝试访问底层账户数据

Rust示例:

use anchor_lang::prelude::*;

declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");

##[program]
pub mod account_types {    
    use super::*;    
    pub fn hello(ctx: Context&lt;Hello>) -> Result&lt;()> {        
        let lamports = ctx.accounts.signer.lamports();        
        let address = &ctx.accounts
            .signer
            .signer_key().unwrap();        
        msg!(
            "你好 {:?} 你有 {} lamports", 
            address, 
            lamports
        );        
        Ok(())    
    }
}

##[derive(Accounts)]
pub struct Hello&lt;'info> {    
    pub signer: Signer&lt;'info>,
}

Typescript:

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

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

    const program = anchor.workspace.AccountTypes as Program&lt;AccountTypes>;  

    it("账户拥有者错误", async () => {    
        await program.methods.hello().rpc();  
    });
});

这里是程序的输出:

在槽11184中执行的交易:
  签名:4xipobKHHp7a3N4durXN4YPGUesDAJNg7wsatBemdJAm7U1dXYG3gveLwnuY39iCTEZvaj6nnAViVJwDS8124uJJ
  状态:成功
  日志消息:
    程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs调用[1]
    程序日志:指令:Hello
    程序日志:你好5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj你有499999994602666000 lamports
    程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs消耗13096的200000计算单位
    程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs成功

Program

这个应该是不言自明的。它向Anchor信号该账户是一个可执行账户,即一个程序,你可以向其发出跨程序调用。我们一直在使用的就是系统程序,稍后我们将使用我们自己的程序。

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

Solana基础 - 在Solana中删除和关闭账户与程序

文章详细介绍了在Solana的Anchor框架中使用close指令关闭账户的操作,包括其原理、实现代码及背后的工作机制,并提供了Rust和Typescript的示例代码。

在 Solana 的 Anchor 框架中,closeinit 的反面 (在 Anchor 中初始化账户) — 它将 lamport 余额减少至零,将 lamports 发送到目标地址,并将账户的拥有者更改为系统程序。

以下是使用 Rust 中的 close 指令的示例:

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

declare_id!("8gaSDFr5cVy2BkLrWfSX9MCtPX9N4gmXDvTVm7RS6DYK");

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

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

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

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

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

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

#[derive(Accounts)]
pub struct Delete<'info> {
    #[account(mut, close = signer, )]
    pub the_pda: Account<'info, ThePda>,

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

#[account]
pub struct ThePda {
    pub x: u32,
}

Solana 返回关闭账户的租金

close = signer 宏指定事务中的签名者将收到为存储预留的租金 (当然,也可以指定其他地址)。这类似于以太坊中 selfdestruct 的工作方式 (在 Decun 升级之前) 为用户清理空间退款。从关闭一个账户中获得的 SOL 数量与该账户大小成正比。

以下是调用 initialize 然后调用 delete 的 TypeScript 代码:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { CloseProgram } from "../target/types/close_program";
import { assert } from "chai";

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

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

  it("Is initialized!", async () => {
    let [thePda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
    await program.methods.initialize().accounts({thePda: thePda}).rpc();
    await program.methods.delete().accounts({thePda: thePda}).rpc();

    let account = await program.account.thePda.fetchNullable(thePda);
    console.log(account)
  });
});

close = signer 指令表明将租金 lamports 发送给签名者,但可以指定你 prefer 的任何地址。

上述结构允许任何人关闭账户,你可能希望在实际应用程序中添加某种访问控制!

账户在关闭后可以被初始化

如果你在关闭账户后调用 initialize,它将再次被初始化。当然,之前兑换的租金必须再次支付。

练习:在单元测试中添加另一个 initialize 调用,以查看其通过。请注意,在测试结束时账户不再为 null。

关闭到底做了什么?

如果我们查看 Anchor 中的 close 命令源代码,我们可以看到它执行了我们上面描述的操作:

关闭:lamports

许多 Anchorlang 示例已过时

在 Anchor 的 0.25 版本中,关闭序列是不同的。

与当前实现类似,它首先将所有 lamports 发送到目标地址。

然而,它不会擦除数据并将其转移到系统程序,而是 close 会写入一个特殊的 8 字节序列,称为 CLOSE_ACCOUNT_DISCRIMINATOR。 (原始代码):

/// 发夹用于标记账户为关闭的鉴别符。
pub const CLOSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 255];

最终,运行时会擦除该账户,因为它的 lamports 为零。

Anchor 中的账户鉴别符是什么?

当 Anchor 初始化一个账户时,它计算鉴别符并将其存储在账户的头 8 字节中。账户鉴别符是该结构的 Rust 标识符的 SHA256 的前 8 字节。

当用户请求程序通过 pub the_pda: Account<'info, ThePda> 加载一个账户时,程序将计算 ThePda 标识符的 SHA256 的前 8 字节。然后它将加载 ThePda 数据并将存储的鉴别符与计算的进行比较。如果它们不匹配,那么 Anchor 将不会反序列化该账户。

这里的意图是防止攻击者构造一个恶意账户,该账户在通过“错误的结构”解析时会反序列化为意外的结果。

为什么 Anchor 过去将账户鉴别符设置为 [255, ..., 255]

通过将账户鉴别符设置为全 1,Anchor 将始终拒绝对账户的反序列化,因为它不会与任何账户鉴别符匹配。

设置账户鉴别符为全 1 的原因是防止攻击者在运行时擦除账户之前向账户直接发送 SOL。在这种情况下,程序“认为”它关闭了程序,但攻击者“复活”了它。如果旧的账户鉴别符仍然存在,那么被认为已删除的数据将会被重新读取。

为什么设置账户鉴别符为 [255, …, 255] 不再需要

通过将所有权改为系统程序,复活账户不会导致程序突然“重新拥有”该账户,系统程序拥有复活的账户,而攻击者浪费了 SOL。

要将所有权更改回程序,需要显式再次初始化,不能通过发送 SOL 来通过侧面渠道复活,以防止运行时将其擦除。

通过 CLI 关闭程序

要关闭一个程序,而不是由它拥有的账户,我们可以使用命令行:

solana program close <address> --bypass warning

警告是,一旦程序关闭,无法重新创建同一地址的程序。以下是关闭账户的 shell 命令序列:

solona 程序关闭

以下是上面屏幕截图中的命令序列:

  1. 首先,我们部署程序
  2. 我们在没有 --bypass-warning 标志的情况下关闭程序,工具提醒我们该程序无法重新部署
  3. 我们在带有标志的情况下关闭程序,程序被关闭,我们收到了 2.918 SOL 作为关闭账户的退款
  4. 我们尝试再次部署并失败,因为关闭的程序无法重新部署

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

Solana基础 - Solana 中的所有者与权限

文章详细解释了Solana中'owner'和'authority'的区别,'owner'是程序,'authority'是钱包,程序只能修改其拥有的账户数据,而'authority'通过发送签名交易来请求程序修改数据。
新来者在 Solana 中常常对“owner”和“authority”之间的区别感到困惑。本文力图尽可能简洁地澄清这种混淆。

新来者在 Solana 中常常对“owner”和“authority”之间的区别感到困惑。本文力图尽可能简洁地澄清这种混淆。

Owner 和 Authority

只有程序可以向账户写入数据——具体而言,只有向它们拥有的账户写入。程序不能随意向任意账户写入数据。

当然,程序不能自发地向账户写入数据。它们需要从一个钱包接收指令。然而,程序通常只会从特权钱包接受特定账户的写入指令:即 authority

账户的拥有者是一个程序。Authority 是一个钱包。Authority 发送一个交易到程序,该程序可以向账户写入数据。

Solana 中所有账户都有以下字段,这些字段大多不言自明:

  • 公钥
  • lamport 余额
  • owner
  • 可执行(布尔标志)
  • 租金周期(可忽略租金豁免账户)
  • 数据

我们可以通过在终端运行 solana account <我们的钱包地址> 来查看这些(在背景中运行 Solana 验证器):

solana account 命令

注意一些有趣的事情:我们不是我们钱包的拥有者! 地址 111...111system program

为什么系统程序拥有钱包,而不是钱包自己拥有自己?

只有账户的拥有者可以修改其中的数据。

这意味着我们无法直接修改我们的余额。只有系统程序可以做到这一点。要将 SOL 从我们的账户转出,我们发送一个签名交易到系统程序。系统程序验证我们拥有该账户的私钥,然后代表我们修改余额。

这是你在 Solana 中经常会看到的模式:只有账户的拥有者可以修改该账户中的数据。如果程序看到来自一个预定地址的有效签名,它就会修改账户中的数据:即 authority

Authority 是一个地址,程序将在看到有效签名时接受该地址的指令。Authority 不能直接修改账户。它需要通过拥有它正在尝试修改的账户的程序来进行操作。

Authority -> Owner -> Account

然而,拥有者始终是一个程序,而该程序将在交易的签名有效时代表其他人修改账户。

例如,在我们关于 使用不同签名者修改账户 的教程中,我们看到了这一点。

练习:创建一个程序来初始化存储账户。你将需要方便地记录程序和存储账户的地址。考虑将以下代码添加到测试中:

console.log(`program: ${program.programId.toBase58()}`);
console.log(`storage account: ${myStorage.toBase58()}`);

然后在被初始化的账户上运行 solana account <存储账户>。你应该看到拥有者是程序。

这里是运行练习的截图:

通过 : 已初始化

当我们查看存储账户的元数据时,我们看到程序是拥有者。

因为程序拥有存储账户,所以它能够写入数据。用户无法直接写入存储账户,他们需要签署交易并请求程序写入数据。

Solana 中的 owner 和 Solidity 中的 owner 非常不同

在 Solidity 中,我们通常将拥有者称为对智能合约拥有管理权限的特殊地址。“拥有者”并不是以太坊运行级别存在的概念,而是应用于 Solidity 合约的一种设计模式。Solana 中的拥有者则更为根本。在以太坊中,智能合约只能写入自己的存储插槽。想象一下我们有一种机制,可以让以太坊智能合约能够写入其他存储插槽。在 Solana 术语中,它将成为这些存储插槽的 owner

Authority 可以表示谁部署了一个合约以及谁可以发送特定账户的写入交易

Authority 可以是程序级别的一个构造。在我们关于 Anchor 签名者 的教程中,我们制作了一个程序,允许Alice从她的账户中扣除积分并转给其他人。为了知道只有Alice可以发送该账户的扣除交易,我们在账户中存储了她的地址:

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

Solana 使用类似的机制来记住谁部署了一个程序。在我们关于 Anchor 部署 的教程中,我们提到部署程序的钱包也能够升级它。

“升级”一个程序与向其写入新的数据——即新的字节码是一样的。只有程序的拥有者可以向其写入(该程序是 BPFLoaderUpgradeable ,我们将很快看到)。

那么,Solana 怎样知道如何将升级权限赋予部署某个程序的钱包呢?

从命令行查看程序的 authority

在我们部署程序之前,让我们通过在终端运行 solana address 来查看 anchor 正在使用哪个钱包:

solana address

请注意我们的地址是 5jmi...rrTj。现在让我们创建一个程序。

确保 solana-test-validatorsolana logs 在后台运行,然后部署 Solana 程序:

anchor init owner_authority
cd owner_authority
anchor build
anchor test --skip-local-validator

当我们查看日志时,我们看到刚刚部署的程序的地址:

结果:已部署程序

记住,在 Solana 中,一切都是账户,包括程序。现在让我们使用 solana account 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg 来检查这个账户。我们得到以下结果:

solana account 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg

注意 authority 字段缺失,因为“authority” 并不是 Solana 账户所拥有的字段。如果你滚动到本文顶部,你会看到控制台中的键与我们在文章顶部列出的字段匹配。

在这里,“owner” 是 BPFLoaderUpgradeable111...111,这是所有 Solana 程序的拥有者。

现在让我们运行 solana program show 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg,其中 6Ye7...y3TG 是我们的程序地址:

solana program show 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg

在上述绿色框中,我们看到我们的钱包地址——即用于部署程序的地址,以及我们之前用 solana address 打印的地址:

solana address

但这引出了一个重要问题…

Solana 将程序的“authority”存储在哪里,这个 authority 目前是我们的钱包?

它并不是账户中的一个字段,所以它一定是在某个 Solana 账户的 data 字段中。“authority” 存储在 ProgramData Address 中,该位置存储着程序的字节码:

solana program show

我们钱包(authority)的十六进制编码

在继续之前,将 ProgramData Address 的 base58 编码转换为十六进制表示将是有帮助的。完成此操作的代码在文章末尾提供,但是现在我们请读者接受下面这句话,即我们的 Solana 钱包地址 5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj 的十六进制表示为:

4663b48dfe92ac464658e512f74a8ee0ffa99fffe89fb90e8d0101a0c3c7767a

查看存储可执行程序的 ProgramData Address 账户中的数据

我们可以使用 solana account 查看 ProgramData 地址账户,但我们也将其发送到临时文件以避免在终端转储太多数据。

solana account FkYygT7X7qjifdxfBVWXTHpj87THJGmtmKUyU4SamfQm > tempfile

head -n 10 tempfile

上面命令的输出显示我们钱包(十六进制)嵌入到 data 中。请注意,黄色下划线的十六进制代码与我们钱包(authority)的十六进制编码匹配:

结果 : ProgramData

程序的字节码存储在单独的账户中,而不是程序的地址

这一点从上面的命令序列中应能隐含得出,但重申是值得的。尽管程序是一个标记为可执行的账户,但字节码并不存储在它自己的数据字段中,而是在另一个账户中(这个账户有点令人困惑地不是可执行的,它仅仅存储字节码)。

练习:你能找到程序将存储字节码的账户地址吗?本文的附录中包含可能有用的代码。

总结

只有程序的拥有者可以更改其数据。Solana 程序的拥有者是 BPFLoaderUpgradeable 系统程序,因此按照默认设置,部署程序的钱包无法更改存储在账户中的数据(字节码)。

为了启用程序的升级,Solana 运行时将部署者的钱包嵌入到程序的字节码中。它将这个字段称为“authority”。

当部署钱包尝试升级字节码时,Solana 运行时将检查事务签名者是否为 authority。如果事务签名者与 authority 匹配,则 BPFLoaderUpgradeable 将代表 authority 更新程序的字节码。

附录:将 base58 转换为十六进制

以下 Python 代码将完成转换。此代码由一个聊天机器人生成,因此仅供参考:

def decode_base58(bc, length):
    base58_digits = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
    n = 0
    for char in bc:
        n = n * 58 + base58_digits.index(char)
    return n.to_bytes(length, 'big')

def find_correct_length_for_decoding(base58_string):
    for length in range(25, 50):  # 尝试从 25 到 50 的长度
        try:
            decoded_bytes = decode_base58(base58_string, length)
            return decoded_bytes.hex()
        except OverflowError:
            continue
    return None

## 要转换的 Base58 字符串
base58_string = "5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj"

## 转换并获取十六进制字符串
hex_string = find_correct_length_for_decoding(base58_string)
print(hex_string)

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

Solana基础 - Solana 中的多重调用:批量交易与交易大小限制

本文介绍了Solana区块链中内置的多调用(multicall)功能,以及如何使用Anchor框架在Solana上进行批量交易。文章还详细解释了Solana交易大小限制,并展示了如何使用Rust和TypeScript代码实现原子性批量交易。

Solana 内置了多重调用(multicall)

在以太坊中,如果我们想要原子地批量处理多个交易,我们使用多重调用模式。如果其中一个失败,其余的也会失败。

Solana 在运行时内置了这一功能,因此我们不需要实现多重调用。在下面的示例中,我们在一次交易中初始化一个账户并写入它——无需使用 init_if_needed

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

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

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

  it("Is initialized!", async () => {
    const wallet = anchor.workspace.Batch.provider.wallet.payer;
    const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);

    const initTx = await program.methods.initialize().accounts({pda: pda}).transaction();

    // 对于 u32,我们不需要使用大数
    const setTx = await program.methods.set(5).accounts({pda: pda}).transaction();

    let transaction = new anchor.web3.Transaction();
    transaction.add(initTx);
    transaction.add(setTx);

    await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);

    const pdaAcc = await program.account.pda.fetch(pda);
    console.log(pdaAcc.value); // 输出 5
  });
});

以下是对应的 Rust 代码:

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

declare_id!("Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE");

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

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

    pub fn set(ctx: Context<Set>, new_val: u32) -> Result<()> {
        ctx.accounts.pda.value = new_val;
        Ok(())
    }
}

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

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

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

##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut)]
    pub pda: Account<'info, PDA>,
}

##[account]
pub struct PDA {
    pub value: u32,
}

关于上面代码的一些评论:

  • 在将 u32 值或更小值传递给 Rust 时,我们不需要使用 Javascript 大数。
  • 我们不是使用 await program.methods.initialize().accounts({pda: pda}).rpc(),而是使用 await program.methods.initialize().accounts({pda: pda}).transaction() 来创建一个交易。

Solana 交易大小限制

Solana 交易的总大小不能超过 1232 字节

这意味着你无法批量处理“无限”数量的交易并支付更多 gas,就像在以太坊中那样。

演示批量交易的原子性

让我们修改 Rust 中的 set 函数以始终失败。这将帮助我们看到,如果其中一个后续批处理交易失败,initialize 交易会被回滚。

以下 Rust 程序在调用 set 时总是返回错误:

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

declare_id!("Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE");

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

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

    pub fn set(ctx: Context<Set>, new_val: u32) -> Result<()> {
        ctx.accounts.pda.value = new_val;
        return err!(Error::AlwaysFails);
    }
}

##[error_code]
pub enum Error {
    #[msg(always fails)]
    AlwaysFails,
}

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

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

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

##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut)]
    pub pda: Account<'info, PDA>,
}

##[account]
pub struct PDA {
    pub value: u32,
}

以下 Typescript 代码发送初始化和设置的批处理交易:

import * as anchor from "@coral-xyz/anchor";
import { Program, SystemProgram } from "@coral-xyz/anchor";
import { Batch } from "../target/types/batch";

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

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

  it("Set the number to 5, initializing if necessary", async () => {
    const wallet = anchor.workspace.Batch.provider.wallet.payer;
    const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);

    // 输出 pda 的地址
    console.log(pda.toBase58());

    let transaction = new anchor.web3.Transaction();
    transaction.add(await program.methods.initialize().accounts({pda: pda}).transaction());
    transaction.add(await program.methods.set(5).accounts({pda: pda}).transaction());

    await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);
  });
});

当我们运行测试,然后查询本地验证器以获取 pda 账户时,我们发现它不存在。即使初始化交易在前,随后的设置交易的回滚导致整个交易被取消,因此没有账户被初始化。

Error : Initialize will get rolled back because set will fail

前端的“需要初始化”功能

你可以使用前端代码模拟 init_if_needed 的行为,同时拥有一个单独的 initialize 函数。然而,从用户的角度来看,他们在第一次使用账户时无需发出多个交易。

要确定一个账户是否需要初始化,我们检查它是否有零 lamports 或被系统程序拥有。以下是如何在 Typescript 中实现此功能:

import * as anchor from "@coral-xyz/anchor";
import { Program, SystemProgram } from "@coral-xyz/anchor";
import { Batch } from "../target/types/batch";

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

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

  it("Set the number to 5, initializing if necessary", async () => {
    const wallet = anchor.workspace.Batch.provider.wallet.payer;
    const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);

    let accountInfo = await anchor.getProvider().connection.getAccountInfo(pda);

    let transaction = new anchor.web3.Transaction();
    if (accountInfo == null || accountInfo.lamports == 0 || accountInfo.owner == anchor.web3.SystemProgram.programId) {
      console.log("需要初始化");
      const initTx = await program.methods.initialize().accounts({pda: pda}).transaction();
      transaction.add(initTx);
    }
    else {
      console.log("无需初始化");
    }

    // 我们无论如何要设置数字
    const setTx = await program.methods.set(5).accounts({pda: pda}).transaction();
    transaction.add(setTx);

    await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);

    const pdaAcc = await program.account.pda.fetch(pda);
    console.log(pdaAcc.value);
  });
});

我们还需要修改我们的 Rust 代码,以 不\set 操作上强制失败。

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

declare_id!("Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE");

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

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

    pub fn set(ctx: Context<Set>, new_val: u32) -> Result<()> {
        ctx.accounts.pda.value = new_val;
        Ok(()) // 移除了错误
    }
}

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

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

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

##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut)]
    pub pda: Account<'info, PDA>,
}

##[account]
pub struct PDA {
    pub value: u32,
}

如果我们对同一个本地验证器实例运行两次测试,我们将获得以下输出:

第一次测试运行:

pass : first test run

第二次测试运行:

pass : second test run

Solana 如何部署超过 1232 字节的程序?

如果你创建一个新的 Solana 程序并 run anchor deploy(或 anchor test),你将在日志中看到多个对 BFPLoaderUpgradeable 的交易:

Transaction executed in slot 65695:
  Signature: 62Zu3NPyjjaEoH4XSc7kULtuoszLPctM1PTrLiC7A3CiaGJEzYscQ5c9SKbN3UUoqctyrdzW2upDXnSC4VnMjyfZ
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: 3cD19SGmdfd991NjcGHpYcnjhZ3FYqEWnHMJALQ95X5fvwHVhB3Cw9PwqSDwziiCMQHcZ8iuxXqg3UDJmp7gJHd3
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: 5apuTjqCMKGdyYGRZ9sCLDapPCKqjyJMyqWMC24EsW4pLzHhM3YUgnf5Q2sqXSLVTxjKaSgZ3fcCkZrAah32uzh2
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: HJ8XaErydn8ojxaEknZsg43pGA9mC8TBqV4zwSrZgXFvi5UqgZjNU65TQKqb6DyEZFtHecytt1k7U4N9Vw52rur
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: 3uY9beX23VdRXeEqUSP4cpAuTevdcjHDZ8K3pwKVpw51mwX1jLGQ7LYB7d68dWSe571TeAoxq33eoUU7c8gTDgic
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: 666r5LcQaH1ZcZWhrHFUFEqjHXEE1QUyh27HFRkWsDQihM7FYtyz3v4eJgVkQwhJuMDSYHJZHDRrSsNVbCFrEkV9
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
  Signature: 2QmPZFkDN9WsKiNjHFdaNLuaYbQFXtN8yRgHTDC3Ce2z28483LNVyuE1AnwgsRisiKeiKe5Wu9WTbkTbAwmodPTC
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
  Signature: EsTiuCn6PGA158Xi43XwGtYf2tDJTbgxRJehHS9AQ9AcW4qraxWuNPzdD7Wk4yeL65oaaa1G8WMqkjYbJcGzhv1V
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
  Signature: 3PZSv4dnggW52C3FL9E1JPvwueBp7E342o9aM29mH2CnfGsGLDBRJcN64EQeJEkc57hgGyZsiz8J1fSV1Qquz8zx
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
  Signature: 4ynMY9ioELf4xxtBpHeM1q2fuWM5usa1w8dXQhLhjstR8U6LmpYHTJs7Gc82XkVyMXywPrsbu3EDCAcpoFj7qwkJ
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65698:
  Signature: 5rs38HHbWF2ZrsgDCux1X9FRvkrhTdrEimdhidd2EYbaeezAmy9Tv5AFULgsarPtJCft8uZmsvhpYKwHGxnLf2sG
  Status: Ok
  Log Messages:
    Program 11111111111111111111111111111111 invoke [1]
    Program 11111111111111111111111111111111 success
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program 11111111111111111111111111111111 invoke [2]
    Program 11111111111111111111111111111111 success
    Deployed program Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE
    Program BFPLoaderUpgradeab1e11111111111111111111111 success

这里,Anchor 将部署字节码的过程分解为多个交易,因为一次性部署整个字节码将无法在单个交易中适应。通过将日志重定向到文件,我们可以计算发生了多少次交易:

solana logs > logs.txt
## 在另一个 shell 中运行 `anchor deploy`
grep "Transaction executed" logs.txt | wc -l

这将大致匹配在 anchor testanchor deploy 命令后暂时显示的情况:

Result : 193/194 transactions

有关交易如何批处理的确切过程描述,可以参见 Solana 文档:如何部署 Solana 程序

交易列表是单独的交易,而不是批量交易。如果是批量交易,它将超过 1232 字节限制。

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

Solana基础 - Anchor 中的 Init if needed 与重初始化攻击

本篇文章详细介绍了Anchor框架的init_if_needed宏,提供了一种在一次事务中初始化账户并写入数据的方法。文中阐述了该宏的便利性与可能引发的重初始化攻击风险,特别是在账户状态和lamport余额的处理上。同时,通过示例代码和测试用例,深入分析了如何安全地使用这些功能,以避免潜在的错误和安全隐患。

在之前的教程中,我们必须在单独的交易中初始化一个账户,然后才能对其写入数据。我们可能希望能够在一个交易中初始化一个账户并对其写入数据,以简化用户的操作。

Anchor 提供了一个方便的宏 init_if_needed,顾名思义,如果账户不存在,则会初始化该账户。

下面的示例计数器不需要单独的初始化交易,它会立即开始将“1”添加到 counter 存储中。

Rust:

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

declare_id!("9DbiqCqtqgP3NYufxBakbeRd7JyNpNYbsm6Jqrn8Z2Hn");

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

    pub fn increment(ctx: Context<Initialize>) -> Result<()> {
        let current_counter = ctx.accounts.my_pda.counter;
        ctx.accounts.my_pda.counter = current_counter + 1;
        Ok(())
    }
}

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

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

#[account]
pub struct MyPDA {
    pub counter: u64,
}

Typescript:

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

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

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

  it("已初始化!", async () => {
    const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
    await program.methods.increment().accounts({myPda: myPda}).rpc();
    await program.methods.increment().accounts({myPda: myPda}).rpc();
    await program.methods.increment().accounts({myPda: myPda}).rpc();

    let result = await program.account.myPda.fetch(myPda);
    console.log(`counter是 ${result.counter}`);
  });
});

当我们尝试使用 anchor build 构建这个程序时,将会遇到以下错误:

错误: init_if_needed

为了消除错误 init_if_needed requires that anchor-lang be imported with the init-if-needed cargo feature enabled,我们可以打开 programs/<anchor_project_name> 中的 Cargo.toml 文件并添加以下行:

[dependencies]
anchor-lang = { version = "0.29.0", features = ["init-if-needed"] }

但在我们只是移除错误之前,我们应该了解什么是重新初始化攻击及其如何发生。

在 Anchor 程序中,账户不能被重初始化(默认)

如果我们尝试初始化一个已经初始化的账户,交易将会失败。

Anchor 如何知道一个账户已经初始化?

从 Anchor 的角度来看,如果该账户的 lamport 余额为零 或者该账户由系统程序拥有,那么它就是未初始化的。

由系统程序拥有或具有零 lamport 余额的账户可以再次初始化。

为了说明这一点,我们有一个具有典型 initialize 函数的 Solana 程序(它使用的是 init,而不是 init_if_needed)。它还具有 drain_lamports 函数和 give_to_system_program 函数,这两个函数的作用如其名所示:

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

declare_id!("FC467mPCCKXG97ut1WdLLi55vuAcyCW8AD1vid27bZfn");

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

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

    pub fn drain_lamports(ctx: Context<DrainLamports>) -> Result<()> {
        let lamports = ctx.accounts.my_pda.to_account_info().lamports();
        ctx.accounts.my_pda.sub_lamports(lamports)?;
        ctx.accounts.signer.add_lamports(lamports)?;
        Ok(())
    }

    pub fn give_to_system_program(ctx: Context<GiveToSystemProgram>) -> Result<()> {
        let account_info = &mut ctx.accounts.my_pda.to_account_info();
        // assign 方法更改拥有者
        account_info.assign(&system_program::ID);
        account_info.realloc(0, false)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct DrainLamports<'info> {
    #[account(mut)]
    pub my_pda: Account<'info, MyPDA>,
    #[account(mut)]
    pub signer: Signer<'info>,
}

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

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

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

#[derive(Accounts)]
pub struct GiveToSystemProgram<'info> {
    #[account(mut)]
    pub my_pda: Account<'info, MyPDA>,
}

#[account]
pub struct MyPDA {}

现在考虑以下单元测试:

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

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

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

  it("在转交给系统程序或排空 lamports 后初始化", async () => {
    const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);

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

    await program.methods.giveToSystemProgram().accounts({myPda: myPda}).rpc();

    await program.methods.initialize().accounts({myPda: myPda}).rpc();
    console.log("账户在转交给系统程序后已初始化!")

    await program.methods.drainLamports().accounts({myPda: myPda}).rpc();

    await program.methods.initialize().accounts({myPda: myPda}).rpc();
    console.log("账户在排空 lamports 后已初始化!")
  });
});

顺序如下:

  1. 我们初始化了 PDA
  2. 我们将 PDA 的所有权转移给系统程序
  3. 我们再次调用初始化,它成功了
  4. 我们从 my_pda 账户中排空 lamports
  5. 由于零 lamport 余额,Solana 运行时将该账户视为不存在,因为它将被安排删除,因为它不再符合租金豁免条件。
  6. 我们再次调用初始化,它成功了。我们在遵循此顺序后成功重新初始化了账户。

再说一遍,Solana 没有“初始化”标记或其他东西。如果所有者是系统程序或者 lamport 余额为零,Anchor 将允许初始化交易成功。

为什么在我们的例子中重新初始化可能是一个问题

将所有权转移给系统程序需要擦除账户中的数据。移除所有 lamports “表明” 你不希望账户继续存在。

你的意图是通过其中任何一种方式重启计数器或结束计数器的生命周期吗?如果你的应用程序从不期望计数器被重置,这可能会导致错误。

Anchor 希望你认真思考你的意图,这就是为什么它使你在 Cargo.toml 中启用功能标志额外增加了步骤。

如果你接受计数器在某个时刻被重置并向上计数,重新初始化就不是问题。但如果在任何情况下计数器都不应该重置为零,那么你最好单独实现 initialization 函数,并添加一个保护措施,以确保它一生中只能调用一次(例如,在一个单独的账户中存储一个布尔标记)。

当然,你的程序不一定具有将账户转移给系统程序或从账户中提取 lamports 的机制。但 Anchor 无法知道这一点,因此它总是会发出关于 init_if_needed 的警告,因为它无法确定该账户是否能够返回到可初始化状态。

拥有两条初始化路径可能导致越界错误或其他意外行为

在我们的计数器示例中,使用 init_if_needed,计数器从未等于零,因为第一次初始化交易也将值从零递增到一。

如果我们_也_有一个普通的初始化函数,它不会递增计数器,那么计数器将被初始化,并且值为零。如果一些业务逻辑永远不希望看到计数器的值为零,那么可能会发生意外行为。

在以太坊中,从未“触摸”的变量的存储值默认值为零。在 Solana 中,未初始化的账户不存在,无法读取。

“初始化”在 Anchor 中并不总是意味着“init”

有点令人困惑,有些人使用“initialize”一词在一般层面上表示“第一次将数据写入账户”,而不仅仅是 Anchor 的 init 宏。

如果我们查看 Soldev 的示例程序,我们看到没有使用 init 宏:

use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
        user.authority = ctx.accounts.authority.key();
        user.serialize(&mut *ctx.accounts.user.data.borrow_mut())?;
        Ok(())
    }
}

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

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
    authority: Pubkey,
}

代码在第11行直接读取账户,然后设置字段。程序随意覆盖数据,无论是第一次写入还是第二次(或第三次)写入。

相反,“initialize”在这里的命名法是“第一次写入账户”。

这里的“重新初始化攻击”与 Anchor 框架所警告的攻击不同。具体而言,“initialize”可以多次调用。Anchor 的 init 宏检查 lamport 余额是否非零以及程序是否已经拥有该账户,这将防止多次调用 initialize。init 宏可以看到账户已经有 lamports 或已由程序拥有。然而,上述代码没有此类检查。

值得阅读他们的教程,以了解这种重新初始化攻击的变种。

请注意,这里使用的是较旧版本的 Anchor。AccountInfoUncheckedAccount 的另一种说法,因此你需要在其上方添加 /// Check: 注释。

擦除账户鉴别符不会让账户重新初始化

一个账户是否已初始化与其内部数据(或缺失数据)无关。

要在不转移的情况下擦除账户中的数据:

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

declare_id!("FC467mPCCKXG97ut1WdLLi55vuAcyCW8AD1vid27bZfn");

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

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

    pub fn erase(ctx: Context<Erase>) -> Result<()> {
        ctx.accounts.my_pda.realloc(0, false)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Erase<'info> {
    /// CHECK: 我们将要擦除账户
    #[account(mut)]
    pub my_pda: UncheckedAccount<'info>,
}

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

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

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

#[account]
pub struct MyPDA {}

重要的是,我们使用 UncheckedAccount 擦除数据,而 .realloc(0, false) 不是常规 Account 中可用的方法。

该操作将擦除账户鉴别符,因此不再通过 Account 可读取。

练习:初始化账户,调用 erase 然后尝试再次初始化账户。它将失败,因为即使账户没有数据,仍然由程序拥有且 lamport 余额非零。

摘要

init_if_needed 宏可以方便地避免与新存储账户交互时需要两个交易。Anchor 框架默认阻止它,以迫使我们思考以下可能的不良情况:

  • 如果有方法将 lamport 余额减少至零或将所有权转移给系统程序,则可以重新初始化账户。根据业务需求,这可能是一个问题,也可能不是。
  • 如果程序同时具有 init 宏和 init_if_needed 宏,则开发人员必须确保拥有两个代码路径不会导致状态意外。
  • 即使账户中的数据完全被擦除,账户仍然处于初始化状态。
  • 如果程序具有“盲目”写入账户的函数,则该账户中的数据可能被覆盖。这通常需要通过 AccountInfo 或其别名 UncheckedAccount 加载账户。

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