本文详细介绍了 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