Solana 60 天课程

2025年02月27日更新 89 人订阅
原价: ¥ 66 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 指令自省 Solana 中的 Ed25519 签名验证 Solana - Switchboard 预言机使用 原生Solana:程序入口与执行 原生 Solana :读取账户数据 原生 Solana :Borsh 序列化 原生 Solana:使用 invoke 和 invoke signed 进行跨程序调用 原生 Solana :创建存储账户 (一) 原生 Solana:创建存储账户 二 原生 Solana: 函数分发 原生 Solana:关键安全检查 Rust 程序到 SBF 编译 sBPF 虚拟机和指令集介绍 跟踪 sBPF 指令执行和计算成本 Solana 程序执行与输入序列化 指令处理器和运行时设置 sBPF 内存布局和寄存器约定 使用 sBPF 汇编读取 Solana 指令输入 Solana 系统调用:sBPF 汇编中的日志记录

Solana 中的 Ed25519 签名验证

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

本文详细介绍了如何在 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 条评论

请先 登录 后评论