Solana 60 天课程

2025年02月27日更新 92 人订阅
原价: ¥ 66 限时优惠
专栏简介

Solana 中的 Ed25519 签名验证

  • RareSkills
  • 发布于 2025-10-22 09:00
  • 阅读 2543

本文详细介绍了如何在 Solana Anchor 程序中验证链下 Ed25519 签名。通过使用 Solana 提供的 Ed25519Program 原生程序和指令内省技术,实现了一个空投场景的签名验证流程,包括构建带有 Ed25519 验证指令和空投申领指令的交易,并在链上程序中验证签名的有效性,以授权代币申领。

在 Solana Anchor 程序中验证 Ed25519 签名

本教程展示了如何在 Solana 程序中验证链下 Ed25519 签名。

在 Solana 中,自定义程序通常不自己实现诸如 Ed25519Secp256k1 签名验证之类的密码学原语,因为此类操作计算密集型,并且会在 SVM 中消耗过多的 计算单元

相反,Solana 提供了 Ed25519ProgramSecp256k1Program 作为针对签名验证进行优化的原生程序。这类似于 Ethereum 如何使用 预编译 来验证 ECDSA 签名,因为直接在 EVM 字节码中实现该逻辑会消耗过多的 gas。

尽管钱包交易也使用 Ed25519 进行签名,但这些签名由 Solana 运行时本身验证,而不是由 Ed25519Program 验证。 当你需要验证包含在交易指令数据中的签名时,例如空投索赔的分发者签名时,会使用 Ed25519Program

在本文中,我们将展示如何在 Solana 中使用 Ed25519Program指令内省 来实现签名验证。 我们的运行示例将是一个空投流程,其中分发者在链下签署包含每个接收者的钱包地址和代币数量 (recipient, amount) 的消息。链上程序负责分发空投,它验证这些签名以授权代币索赔并将 amount 转移给 recipient

Ed25519Program 是无状态的

Solana Ed25519Program 仅根据提供的输入参数执行密码签名验证。 它不维护调用之间的任何持久数据,因此,它不拥有任何帐户。 因此,它不存储验证的结果。 如果签名验证失败,则整个交易将被拒绝; 如果成功,则执行继续,并且下一个指令可以安全地假定签名有效。

我们的运行示例:空投

在空投中,我们需要一种方法来知道谁有资格申领代币。 一种方法是将所有符合条件的地址存储在链上,但这成本很高。

基于签名的空投不将所有接收者地址存储在链上,而是使用受信任的分发者(例如,项目团队)来签署包含每个接收者的钱包地址和代币数量 (recipient, amount) 的链下消息。 负责分发空投的链上程序会验证这些签名,以授权代币申领并将 amount 转移给 recipient

验证过程如何工作

签名验证过程使用指令内省,程序可以在同一交易中读取其他指令。 我们之前讨论过指令内省,现在我们将重点关注它如何应用于签名验证。

首先,我们的空投接收者提交一个包含两个指令的单一交易,在本文中,我们将它们称为Ed25519 指令(指令 1)和AirdropClaim 指令(指令 2):

回想一下,一个指令包含一个程序 ID、一个帐户列表以及程序解释的任意数据。 我们将在本文中引用这个指令结构:

pub struct Instruction {
    /// 执行此指令的程序的公钥。
    pub program_id: Pubkey,
    /// 描述应传递给程序的帐户的元数据。
    pub accounts: Vec<AccountMeta>,
    /// 传递给程序供其自行解释的不透明数据。
    pub data: Vec<u8>,
}

指令 1:用于签名验证的 Ed25519 指令

Ed25519 指令 是一个 Solana 指令,其 program_id 是原生 Ed25519Program 验证器 (Ed25519SigVerify111111111111111111111111111)。 它是我们空投交易中的第一个指令。

由于 Ed25519Program 是无状态的,因此此指令不需要任何帐户,因此所有输入都编码在指令 data 中。

Ed25519Program 的指令数据如何格式化

Ed25519program 指令中的 data 以一个 16 字节的标头开始,该标头包含指令中的签名数量和偏移量。在我们的例子中,我们只有分发者的签名计数和偏移量。这些偏移量指向 data 的其余部分,以定位已验证的公钥、消息和签名。数据的其余部分将从第 16 个字节持续到第 151 个字节。

Ed25519 指令
[字节 0..15]<br>标头(16 字节) [字节 16..47]<br>分发者的公钥(32 字节) [字节 48..111]<br>分发者的签名(64 字节) [字节 112..151]<br>消息<br>– 接收者的公钥 (0..31)<br>– 空投代币数量(32..39,小端)

这是标头的 Rust 结构:

struct Ed25519InstructionHeader {
    num_signatures: u8,   // 1 字节
    padding: u8,          // 1 字节
    offsets: Ed25519SignatureOffsets, // 14 字节
}

struct Ed25519SignatureOffsets {
    signature_offset: u16,             // 2 字节
    signature_instruction_index: u16,  // 2 字节
    public_key_offset: u16,            // 2 字节
    public_key_instruction_index: u16, // 2 字节
    message_data_offset: u16,          // 2 字节
    message_data_size: u16,            // 2 字节
    message_instruction_index: u16,    // 2 字节
}

请注意,Ed25519SignatureOffsets 结构具有以下索引:signature_instruction_indexpublic_key_instruction_indexmessage_instruction_index。 这些索引用于确定指令数据是否在当前正在执行的指令中。 当前指令数据中的索引在 Solana Ed25519 源代码 中设置为 u16::MAX

    let offsets = Ed25519SignatureOffsets {
        signature_offset: signature_offset as u16,
        signature_instruction_index: u16::MAX,
        public_key_offset: public_key_offset as u16,
        public_key_instruction_index: u16::MAX,
        message_data_offset: message_data_offset as u16,
        message_data_size: message.len() as u16,
        message_instruction_index: u16::MAX,
    };

任何其他值都将指向交易中的另一个指令。

在我们的运行空投示例中,Ed25519 指令 数据的布局如下所示。

Ed25519 指令
0..15<br>标头(16 字节) 16..47<br>分发者的公钥 48..111<br>分发者的签名 112..151<br>消息<br>– 接收者的公钥 (0..31)<br>– 空投代币数量(32..39,小端)

在实践中,你将使用 Web3.js 或 solana-ed25519-program crate 之类的链下助手来构建有效的指令。 下面是 ed25519 crate 源代码的一个片段,显示了构建指令的输入参数,然后返回一个有效的链下指令。 (Typescript 版本将在稍后显示)

use solana_ed25519_program::new_ed25519_instruction_with_signature;

pub fn new_ed25519_instruction_with_signature(
    message: &[u8],
    signature: &[u8; 64],
    pubkey: &[u8; 32],
) -> Instruction

从概念上讲,反序列化的 Ed25519 指令 如下所示:

Ed25519 指令
程序 ID Ed25519SigVerify111111111111111111111111111
帐户 []
指令数据 – 标头(签名计数 + 偏移量)<br>– 分发者的公钥<br>– 消息(接收者、数量)<br>– 分发者的签名

当交易执行时,Ed25519 指令Ed25519Program 处理。 如果签名有效,则指令执行成功。 但是,如果签名无效,它会中止交易并记录错误代码,这意味着后续指令(如 AirdropClaim 指令)不会执行。

我们将在本文后面演示此验证的实际工作方式。

指令 2:AirdropClaim 指令

AirdropClaim 指令 是发送给空投程序的用于申领空投代币的标准 Solana 交易指令。 该指令包含空投程序 ID、接收者帐户和用于内省的指令 sysvar 帐户。

AirdropClaim 指令
程序 ID 空投程序 ID
帐户 [接收者,指令 sysvar 帐户]
指令数据 无自定义数据

空投程序将首先使用指令 sysvar 内省 **** Ed25519 验证指令:指令 1 以验证:

  • Ed25519 验证指令:指令 1 程序 ID 与 Ed25519Program (Ed25519SigVerify111111111111111111111111111) 匹配。
  • Ed25519 验证指令:指令 1 没有帐户,正如无状态 Ed25519Program 所期望的那样。
  • 该指令的数据包含正确的分发者公钥、签名和消息,与预期值匹配。

如果内省显示 Ed25519 验证指令:指令 1 有效,则用户可以申领他们的空投代币。

Ed25519 验证指令AirdropClaim 指令 的执行流程

下图显示了在可以领取空投之前,我们的程序中 Ed25519 验证指令AirdropClaim 指令 的高级执行流程。

用户发送一个包含两个指令的交易:Ed25519 验证指令AirdropClaim 指令

  1. Ed25519 验证指令 转到 Ed25519Program 以验证分发者的签名。
  2. 如果签名验证失败,则整个交易失败。 如果成功,则执行流程继续。
  3. 然后将 AirdropClaim 指令 发送到 空投程序
  4. 空投程序Ed25519 验证指令 进行内省,检查其程序 ID、帐户和数据,以确认它是有效的 Ed25519 验证。
  5. 如果内省确认了 Ed25519 验证指令,则用户可以申领他们的空投代币。

一张图表,说明了 Ed25519 验证指令和 AirdropClaim 指令的执行流程。

用于空投分发的签名验证程序

让我们编写实际代码,演示如何按照我们的空投分发流程使用指令内省来验证 Ed25519 签名。 此应用程序有两个阶段:

  1. 客户端 构建交易,添加 Ed25519 验证指令:指令 1AirdropClaim 指令:指令 2,然后将交易发送到网络。
  2. 程序逻辑 通过内省验证 Ed25519 验证指令:指令 1,并允许用户申领他们的空投代币。

我们将在测试套件中实现客户端逻辑,因此让我们首先创建程序逻辑。

程序逻辑:申领验证

要跟上本节的进度,请确保你的机器上已设置 Solana 开发环境。 否则,请阅读 本系列的第一篇文章 进行设置。

通过运行 anchor 命令初始化一个 Anchor 应用程序:

 anchor init airdrop-distribution

使用这些 Anchor 导入更新 programs/airdrop-distribution/lib.rs 文件中的导入。 我们需要:

  • 用于验证的 ed25519_program 导入,
  • 以及我们在不同实例中需要它的公钥,
  • 然后我们将使用 sysvar 导入进行内省。
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
    ed25519_program,
    pubkey::Pubkey,
    sysvar::instructions as ix_sysvar,
    sysvar::SysvarId
};

保留你生成的 declare_id

declare_id!("Gh2JoycvxfreSgjzhCHuRDK7sZDAbxeo7Pd8GKCoSLmS");

接下来,我们将包含程序的其余逻辑,并逐步讲解。

该程序包含一个 claim 函数,其中包含所有逻辑。 以下是函数中发生的事情的细分:

  1. 它加载指令 sysvar 以读取完整的交易指令。
  2. 查找当前指令的索引并加载其紧邻的前一个指令。
  3. 要求前面的指令已发送到原生的 Ed25519 程序并且没有帐户。
  4. 解析 Ed25519 验证指令:指令 1 数据,然后检查标头,验证签名数量并提取偏移量。
  5. 验证标头中的所有偏移量是否指向同一指令内的数据,并专门指向签名、公钥和消息。
  6. 从数据中重建分发者的公钥,并检查它是否与预期的分发者帐户匹配。
  7. 重建签名的消息 [recipient pubkey (32)][amount (u64 little-endian)] 并检查签名消息中的接收者是否与 AirdropClaim 指令:指令 2 中的接收者帐户匹配。

use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
    ed25519_program,
    pubkey::Pubkey,
    sysvar::instructions as ix_sysvar,
    sysvar::SysvarId
};

declare_id!("Gh2JoycvxfreSgjzhCHuRDK7sZDAbxeo7Pd8GKCoSLmS");

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

    pub fn claim(ctx: Context&lt;Claim>) -> Result&lt;()> {

        // --- constants for parsing Ed25519 instruction data ---
        const HEADER_LEN: usize = 16;  // fixed-size instruction header
        const PUBKEY_LEN: usize = 32;  // size of an Ed25519 public key
        const SIG_LEN: usize = 64;     // size of an Ed25519 signature
        const MSG_LEN: usize = 40;     // expected message length: [recipient(32) + amount(8)]

        // Load the instruction sysvar account (holds all tx instructions)
        let ix_sysvar_account = ctx.accounts.instruction_sysvar.to_account_info();

        // Index of the current instruction in the transaction
        let current_ix_index = ix_sysvar::load_current_index_checked(&ix_sysvar_account)
            .map_err(|_| error!(AirdropError::InvalidInstructionSysvar))?;

        // The Ed25519 verification must have run just before this instruction
        require!(current_ix_index > 0, AirdropError::InvalidInstructionSysvar);

        // Load the immediately preceding instruction (the Ed25519 ix)
        let ed_ix = ix_sysvar::load_instruction_at_checked(
            (current_ix_index - 1) as usize,
            &ix_sysvar_account,
        )
        .map_err(|_| error!(AirdropError::InvalidInstructionSysvar))?;

        // Ensure it is the Ed25519 program and uses no accounts (stateless check)
        require!(ed_ix.program_id == ed25519_program::id(), AirdropError::BadEd25519Program);
        require!(ed_ix.accounts.is_empty(), AirdropError::BadEd25519Accounts);

        // Ed25519 Verification Instruction data
        let data = &ed_ix.data;

        // --- parse Ed25519 instruction format ---
        // First byte: number of signatures (must be 1)
        // Rest of header: offsets describing where signature, pubkey, and message are
        require!(data.len() >= HEADER_LEN, AirdropError::InvalidInstructionSysvar);
        let sig_count = data[0] as usize;
        require!(sig_count == 1, AirdropError::InvalidInstructionSysvar);

        // helper to read u16 offsets from the header (little-endian)
        let read_u16 = |i: usize| -> Result&lt;u16> {
            let start = 2 + 2 * i;
            let end = start + 2;
            let src = data
                .get(start..end)
                .ok_or(error!(AirdropError::InvalidInstructionSysvar))?;
            let mut arr = [0u8; 2];
            arr.copy_from_slice(src);
            Ok(u16::from_le_bytes(arr))
        };

        // Extract the offsets for signature, pubkey, and message
        let signature_offset = read_u16(0)? as usize;
        let signature_ix_idx = read_u16(1)? as usize;
        let public_key_offset = read_u16(2)? as usize;
        let public_key_ix_idx = read_u16(3)? as usize;
        let message_offset = read_u16(4)? as usize;
        let message_size = read_u16(5)? as usize;
        let message_ix_idx = read_u16(6)? as usize;

        // Enforce that all offsets point to the current instruction's data.
        // The Ed25519 program uses u16::MAX as a sentinel value for "current instruction".
        // This prevents the program from accidentally reading signature, public key,
        // or message bytes from some other instruction in the transaction.
        let this_ix = u16::MAX as usize;
        require!(
            signature_ix_idx == this_ix
                && public_key_ix_idx == this_ix
                && message_ix_idx == this_ix,
            AirdropError::InvalidInstructionSysvar
        );

        // Ensure all offsets point beyond the 16-byte header,
        // i.e. into the region containing the signature, public key, and message
        require!(
            signature_offset >= HEADER_LEN
                 && public_key_offset >= HEADER_LEN
                 && message_offset >= HEADER_LEN,
            AirdropError::InvalidInstructionSysvar
        );

        // Bounds checks for signature, pubkey, and message slices
        require!(data.len() >= signature_offset + SIG_LEN, AirdropError::InvalidInstructionSysvar);
        require!(data.len() >= public_key_offset + PUBKEY_LEN, AirdropError::InvalidInstructionSysvar);
        require!(data.len() >= message_offset + message_size, AirdropError::InvalidInstructionSysvar);
        require!(message_size == MSG_LEN, AirdropError::InvalidInstructionSysvar);

        // --- reconstruct and validate the distributor's pubkey ---
        l...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论