您正在查看: Solana-优秀转载 分类下的文章

Solana基础 - 在链上读取另一个锚点程序账户数据

本文详细介绍了在Solana链上程序中如何读取不属于自己的账户数据,通过创建data_holder和data_reader两个程序,展示了如何初始化并读取PDA中的数据,并探讨了Anchor框架下的数据反序列化机制及其限制。

在 Solidity 中,读取另一个合约的存储需要调用 view 函数或者存储变量是公共的。在 Solana 中,离线客户端可以直接读取存储账户。这个教程展示了一个链上 Solana 程序如何读取它不拥有的账户中的数据。

我们将设置两个程序: data_holderdata_readerdata_holder 将初始化并拥有一个 PDA,其数据将被 data_reader 读取。

设置存储数据的 data_holder 程序: Shell 1

以下代码是一个基本的 Solana 程序,它初始化账户 Storage,其中包含 u64 字段 x,并在初始化时将值 9 存储在其中:

Typescript 代码:

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

describe("data-holder", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    const seeds = [];
    const [storage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(
      seeds,
      program.programId
    );

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

    let storageStruct = await program.account.storage.fetch(
      storage
    );

    console.log("The value of x is: ",storageStruct.x.toString());

    console.log("Storage account address: ", storage.toBase58());
  });
});

测试将打印出 PDA 的地址,我们稍后会提到这个地址:

PDA 输出

读取器

为了让 data_reader 读取另一个账户,必须通过 Context 结构将该账户的公共密钥作为交易的一部分传递。这与传递任何其他类型的账户没有区别。

账户中的数据以序列化字节的形式存储。 为了反序列化账户,data_reader 程序需要读取它的 Rust 结构定义。我们需要以下账户定义,并且它与 data_holder 中的 Storage 结构完全相同:

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

这个结构与 data_reader 中的结构完全相同——连名称也必须相同(稍后我们会介绍为什么)。读取账户的代码在以下两行中:

let mut data_slice: &[u8] = &data_account.data.borrow();

let data_struct: Storage = 
    AccountDeserialize::try_deserialize(
        &mut data_slice,
    )?;

data_slice 是账户中数据的原始字节。如果你运行 solana account <pda address>(使用我们在部署 data_holder 时生成的 PDA 地址),你可以在那里看到数据,包括我们储存在红框中的数字 9:

终端输出 solana \\&lt;pda address\\> 包含数字 9

黄框中前 8 个字节是账户鉴别符,稍后我们将对此进行描述。

反序列化发生在此步骤:

let data_struct: Storage =
    AccountDeserialize::try_deserialize(
        &mut data_slice,
    )?;

在这里传递类型 Storage(我们上面定义的结构),告诉 Solana 如何(尝试)反序列化数据。

现在让我们在新文件夹中创建一个单独的 anchor 项目 anchor new data_reader

完整的 Rust 代码如下:

use anchor_lang::prelude::*;

declare_id!("HjJ1Rqsth5uxA6HKNGy8VVRvwK4W7aFgmQsss7UxePBw");

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

    pub fn read_other_data(
        ctx: Context&lt;ReadOtherData>,
    ) -> Result&lt;()> {

        let data_account = &ctx.accounts.other_data;

        if data_account.data_is_empty() {
            return err!(MyError::NoData);
        }

        let mut data_slice: &[u8] = &data_account.data.borrow();

        let data_struct: Storage =
            AccountDeserialize::try_deserialize(
                &mut data_slice,
            )?;

        msg!("The value of x is: {}", data_struct.x);

        Ok(())
    }
}

#[error_code]
pub enum MyError {
    #[msg("No data")]
    NoData,
}

#[derive(Accounts)]
pub struct ReadOtherData&lt;'info> {
    /// CHECK: We do not own this account so
    // we must be very cautious with how we
    // use the data
    other_data: UncheckedAccount&lt;'info>,
}

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

以下是要运行的测试代码。确保在下面的代码中更改 PDA 的地址:

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

describe("data-reader", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    // CHANGE THIS TO THE ADDRESS OF THE PDA OF
    // DATA ACCOUNT HOLDER
    const otherStorageAddress ="HRGqGCLXxLryZav2SeKJKqBWYs8Ne7ppJxf3MLM3Y71E";

    const pub_key_other_storage = new anchor.web3.PublicKey(
      otherStorageAddress
    );

    const tx = await program.methods
      .readOtherData()
      .accounts({ otherData: pub_key_other_storage })
      .rpc();
  });
});

要测试读取另一个账户的数据:

  1. 在后台运行 solana-test-validator 启动 data_holder 测试。
  2. 复制并粘贴 Storage 账户打印的公共密钥。
  3. 将该公共密钥放入 data_reader 测试的 otherStorageAddress 中。
  4. 在另一个 shell 中运行 Solana 日志。
  5. 运行 data_reader 的测试以读取数据。

以下内容应在 Solana 日志中可见:

程序日志 : x 的值是: 9

如果我们不给结构体相同的名称,会发生什么?

如果你将 data_reader 中的 Storage 结构改为其他名称,如 Storage2 并尝试读取该账户,则会发生以下错误:

错误: 由于账户而引起的 AnchorError: Storage2

Anchor 计算的账户鉴别符是结构名称的 sha256 的前八个字节。账户鉴别符不依赖于结构中的变量

当 Anchor 读取账户时,它检查前八个字节(账户鉴别符)以查看它是否与它在本地用于反序列化数据的结构定义的账户鉴别符匹配。如果它们不匹配,Anchor 将不会反序列化数据。

检查账户鉴别符是防止客户端错误地传入错误账户或数据格式与 Anchor 预期不符的账户数据的一种保障。

反序列化不会因解析更大结构而回退

Anchor 检查账户鉴别符是否匹配——它不会验证被读取的账户内部的字段。

案例 1: Anchor 不检查结构字段名称是否匹配

让我们将 data_reader 中的 Storage 结构中的 x 字段改为 y,保持 data_holder 中的 Storage 结构不变:

// data_reader

#[account]
pub struct Storage {
    y: u64,
}

我们还需要将日志行更改如下:

msg!("The value of y is: {}", data_struct.y);

当我们重新运行测试时,它成功读取了数据:

Program log: Instruction: ReadOtherData
Program log: The value of y is: 9

案例 2: Anchor 不检查数据类型

现在让我们将 ydata_reader 中的 Storage 结构的数据类型更改为 u32,尽管原始结构是 u64

// data_reader

#[account]
pub struct Storage {
    y: u32,
}

当我们运行测试时,Anchor 仍然成功解析账户数据。

Program log: Instruction: ReadOtherData
Program log: The value of y using u32 is: 9

之所以“成功”,是因为数据的布局如下:

终端输出 solana \\&lt;pda address\\> 包含数字 9

7 里的 9 位于前几个字节中——u32 将在前 4 个字节中查找数据,因此它能够“看到”9

当然,如果我们要存储 u32 无法容纳的值,例如 $2^{32}$,那么我们的读取程序将打印错误的数字。

练习 : 重置验证器并重新部署 data_holder,值设置为 $2^{32}$。在 Rust 中幂运算的方式是 let result = u64::pow(base, exponent)。例如,let result = u64::pow(2, 32); 查看 data_reader 记录了什么值。

案例 3: 解析的字段数据超出存储

存储账户的大小为 16 字节。它存储 8 字节给账户鉴别符,以及 8 字节给 u64 变量。如果我们尝试读取比实际大更多的数据,例如通过定义一个需要超过 16 字节来存储的结构,反序列化将失败:

#[account]
pub struct Storage {
    y: u64,
    z: u64,
}

上述结构需要 16 字节来存储 y 和 z,但还需要额外的 8 字节来保存账户鉴别符,使得账户大小达到 24 字节。

错误: AnchorError 发生. 错误代码: AccountDidNotDeserialize

解析 Anchor 账户数据总结

在从外部账户读取数据时,Anchor 将检查账户鉴别符是否匹配,以及账户中是否有足够的数据可反序列化为用作 try_deserialize 类型的结构:

let data_struct: Storage =
    AccountDeserialize::try_deserialize(
        &mut data_slice,
    )?;

Anchor 不检查变量的名称或长度。

在底层,Anchor 不存储任何如何解释账户中数据的元数据。它只是存储变量的字节,按顺序存储。

并非所有数据账户都遵循 Anchor 的约定

Solana 不要求使用账户鉴别符。用原始 Rust 编写的 Solana 程序——没有 Anchor 框架——可能以与 Anchor 的序列化方法(即 AccountDeserialize::try_deserialize)不直接兼容的方式存储数据。要反序列化非 Anchor 数据,开发者必须提前知道使用的序列化方法——在 Solana 生态系统中并没有强制的通用约定。

读取任意账户数据时要小心

Solana 程序默认是可升级的。它们如何在帐户中存储数据的方式可能随时改变,这可能会破坏正在读取它们的程序。

接受来自任意账户的数据是危险的——在读取其数据之前,通常应检查该账户是否由受信任的程序拥有。

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

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&lt;Foo>) -> Result&lt;()> {        
        // 我们不对账户SomeAccount做任何操作        
        Ok(())    
    }
}

##[derive(Accounts)]
pub struct Foo&lt;'info> {    
    some_account: Account&lt;'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&lt;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&lt;Foo>) -> Result&lt;()> {        
        let data = &ctx.accounts.some_account.try_borrow_data()?;        
        msg!("{:?}", data);        
        Ok(())    
    }
}

##[derive(Accounts)]
pub struct Foo&lt;'info> {    
    /// 检查:我们只是打印数据    
    some_account: AccountInfo&lt;'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&lt;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

搜索