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

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

Hero image showing Solona transaction batch and transaction size limit

Solana 内置了多重调用

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

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 字节限制。

了解更多

查看我们的 Solana 开发课程 以获取更多 Solana 教程。

最初发表于 2024 年 3 月 10 日

  • 原文链接: rareskills.io/post/solan...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/