Verificación de Firmas Ed25519 en Programas Anchor de Solana
Este tutorial muestra cómo verificar una firma Ed25519 off-chain en un programa de Solana.
En Solana, los programas personalizados generalmente no implementan primitivas criptográficas como la verificación de firmas Ed25519 o Secp256k1 por sí mismos porque dichas operaciones son intensivas en cómputo y consumirían excesivas unidades de cómputo en la SVM.
En su lugar, Solana proporciona Ed25519Program y Secp256k1Program como programas nativos que están optimizados para la verificación de firmas. Esto es similar a cómo Ethereum usa un precompile para validar firmas ECDSA porque implementar esa lógica directamente en el bytecode de la EVM consumiría demasiado gas.
Aunque las transacciones de las wallets también se firman con Ed25519, esas firmas son verificadas por el propio entorno de ejecución (runtime) de Solana, no por el Ed25519Program. El Ed25519Program se utiliza cuando se necesita verificar firmas incluidas dentro de los datos de instrucción de la transacción, como la firma de un distribuidor para reclamar un airdrop.
En este artículo, mostraremos cómo funciona la verificación de firmas en Solana usando Ed25519Program y la introspección de instrucciones. Nuestro ejemplo principal será un flujo de airdrop, donde un distribuidor firma reclamos off-chain y los destinatarios envían esos reclamos firmados on-chain para su verificación, de modo que puedan reclamar el airdrop.
Ed25519Program no tiene estado (stateless)
El Ed25519Program de Solana solo realiza la verificación de firmas criptográficas basándose en los parámetros de entrada proporcionados. No mantiene ningún dato persistente entre llamadas, por lo tanto, no posee cuentas. Como resultado, no almacena el resultado de la verificación. Si la verificación de la firma falla, la transacción completa es rechazada; si tiene éxito, la ejecución continúa y la siguiente instrucción puede asumir de forma segura que la firma era válida.
Nuestro ejemplo principal: Airdrop
En un airdrop, necesitamos una forma de saber quién es elegible para reclamar tokens. Un enfoque es almacenar todas las direcciones elegibles on-chain, pero esto es costoso.
En lugar de almacenar todas las direcciones de los destinatarios on-chain, un airdrop basado en firmas utiliza un distribuidor de confianza (por ejemplo, el equipo del proyecto) para firmar mensajes off-chain que contienen la dirección de la wallet de cada destinatario y la cantidad de tokens (recipient, amount). El programa on-chain responsable de distribuir el airdrop verifica estas firmas para autorizar los reclamos de tokens y transferir el amount al recipient.
Cómo funciona el proceso de verificación
El proceso de verificación de firmas utiliza la introspección de instrucciones, donde un programa puede leer otras instrucciones en la misma transacción. Discutimos la introspección de instrucciones anteriormente, y ahora nos centraremos en cómo se aplica a la verificación de firmas.
Primero, el destinatario de nuestro airdrop envía una sola transacción con dos instrucciones, a las que nos referiremos en este artículo como Ed25519 Instruction para la instrucción 1 y AirdropClaim Instruction para la instrucción 2:
Recordemos que una instrucción contiene un program ID, una lista de cuentas y datos arbitrarios que el programa interpreta. Haremos referencia a este struct de instrucción a lo largo de este artículo:
pub struct Instruction {
/// Pubkey of the program that executes this instruction.
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program.
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation.
pub data: Vec<u8>,
}
Instrucción 1: Ed25519 Instruction para la verificación de firmas
La Ed25519 Instruction es una instrucción de Solana cuyo program_id es el verificador nativo Ed25519Program (Ed25519SigVerify111111111111111111111111111). Es la primera instrucción en nuestra transacción de airdrop.
Dado que el Ed25519Program es stateless, no se necesitan cuentas para esta instrucción, por lo que todas las entradas se codifican en la data de la instrucción.
Cómo se formatea la data de la instrucción para Ed25519Program
La data en la instrucción de Ed25519program comienza con un encabezado (header) de 16 bytes que contiene el número de firmas en la instrucción y los offsets (desplazamientos). En nuestro caso, solo tendremos el recuento de firmas del distribuidor y los offsets. Estos offsets apuntan al resto de la data para localizar la clave pública, el mensaje y la firma que se verificaron. El resto de los datos continuará desde el byte 16 hasta el byte 151.
| Ed25519 Instruction | |||
|---|---|---|---|
| [bytes 0…15] Encabezado (16 bytes) |
[bytes 16…47] Clave pública del distribuidor (32 bytes) |
[bytes 48…111] Firma del distribuidor (64 bytes) |
[bytes 112…151] Mensaje - Pubkey del destinatario (0…31) - Cantidad de tokens del airdrop (32…39, little-endian) |
Este es el struct en Rust del encabezado:
struct Ed25519InstructionHeader {
num_signatures: u8, // 1 byte
padding: u8, // 1 byte
offsets: Ed25519SignatureOffsets, // 14 bytes
}
struct Ed25519SignatureOffsets {
signature_offset: u16, // 2 bytes
signature_instruction_index: u16, // 2 bytes
public_key_offset: u16, // 2 bytes
public_key_instruction_index: u16, // 2 bytes
message_data_offset: u16, // 2 bytes
message_data_size: u16, // 2 bytes
message_instruction_index: u16, // 2 bytes
}
Nota que el struct Ed25519SignatureOffsets tiene los siguientes índices: signature_instruction_index, public_key_instruction_index y message_instruction_index. Estos índices se utilizan para determinar si la data de la instrucción se encuentra en la instrucción actual que se está ejecutando. Los índices en la data de la instrucción actual se establecen en u16::MAX en el código fuente de Ed25519 de Solana:
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,
};
Cualquier otro valor apuntaría a otra instrucción en la transacción.
El diseño (layout) de la data para la Ed25519 Instruction se verá así en nuestro ejemplo principal de airdrop.
| Ed25519 Instruction | |||
|---|---|---|---|
| 0…15 Encabezado (16 bytes) |
16…47 Clave pública del distribuidor |
48…111 Firma del distribuidor |
112…151 Mensaje - Pubkey del destinatario (0…31) - Cantidad de tokens del airdrop (32…39, little-endian) |
En la práctica, utilizarás helpers off-chain como Web3.js o el crate solana-ed25519-program para construir una instrucción válida. A continuación se muestra un fragmento del código fuente del crate ed25519 que muestra los parámetros de entrada para construir la instrucción y luego devolver una instrucción válida off-chain. (La versión en TypeScript se mostrará más adelante)
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
Conceptualmente, la versión deserializada de la Ed25519 Instruction se ve así:
| Ed25519 Instruction | |
|---|---|
| Program ID | Ed25519SigVerify111111111111111111111111111 |
| Accounts | [] |
| Instruction Data | - Encabezado (Recuento de firmas + Offsets) - Clave pública del distribuidor - Mensaje (recipient, amount) - Firma del distribuidor |
Cuando la transacción se ejecuta, la Ed25519 Instruction es procesada por el Ed25519Program. Si la firma es válida, la ejecución de la instrucción tiene éxito. Sin embargo, si la firma es inválida, aborta la transacción y registra un código de error, lo que significa que las instrucciones subsiguientes (como la AirdropClaim Instruction) no se ejecutan.
Demostraremos cómo funciona esta verificación de manera práctica más adelante en este artículo.
Instrucción 2: AirdropClaim Instruction
La AirdropClaim Instruction es una instrucción estándar de transacción de Solana enviada al programa del airdrop para reclamar el token del airdrop. La instrucción contiene el program ID del airdrop, la cuenta del destinatario y la cuenta sysvar de instrucciones para la introspección.
| AirdropClaim Instruction | |
|---|---|
| Program ID | Program ID del airdrop |
| Accounts | [recipient, cuenta sysvar de instrucciones] |
| Instruction Data | Sin datos personalizados |
El programa del airdrop primero hará una introspección de la Ed25519 Verification Instruction: Instruction 1 utilizando el sysvar de instrucciones para validar que:
- El program ID de la
Ed25519 Verification Instruction: Instruction 1coincida con elEd25519Program(Ed25519SigVerify111111111111111111111111111). - La
Ed25519 Verification Instruction: Instruction 1no tenga cuentas, como se espera para elEd25519Programstateless. - La data de la instrucción contenga la clave pública del distribuidor, la firma y el mensaje correctos, coincidiendo con los valores esperados.
Si la introspección muestra que la Ed25519 Verification Instruction: Instruction 1 es válida, el usuario puede reclamar su token del airdrop.
Flujo de ejecución de la Ed25519 Verification Instruction y la AirdropClaim Instruction
El siguiente diagrama muestra un flujo de ejecución de alto nivel de la Ed25519 Verification Instruction y la AirdropClaim Instruction en nuestro programa antes de que se pueda reclamar un airdrop.
El usuario envía una transacción con dos instrucciones: Ed25519 Verification Instruction y AirdropClaim Instruction.
- La
Ed25519 Verification Instructionva alEd25519Programpara verificar la firma del distribuidor. - Si la verificación de la firma falla, la transacción completa falla. Si tiene éxito, el flujo de ejecución continúa.
- La
AirdropClaim Instructionse envía entonces al Airdrop program. - El Airdrop program hace una introspección a la
Ed25519 Verification Instruction, verificando su program ID, cuentas y data para confirmar que fue una verificaciónEd25519válida. - Si la introspección confirma la
Ed25519 Verification Instruction, el usuario puede reclamar su token del airdrop.

Programa de verificación de firmas para la distribución de airdrop
Escribamos código real que demuestre cómo usar la introspección de instrucciones para verificar firmas Ed25519 siguiendo nuestro flujo de distribución de airdrop. Esta aplicación tiene dos fases:
- El lado del cliente (client side) construye la transacción agregando la
Ed25519 Verification Instruction: Instruction 1y laAirdropClaim Instruction: Instruction 2, y luego envía la transacción a la red. - La lógica del programa valida la
Ed25519 Verification Instruction: Instruction 1mediante la introspección y permite al usuario reclamar su token del airdrop.
Implementaremos la lógica del lado del cliente en el conjunto de pruebas (test suite), así que comencemos creando la lógica del programa primero.
La lógica del programa: la verificación del reclamo
Para seguir esta sección, asegúrate de tener configurado el entorno de desarrollo de Solana en tu máquina. De lo contrario, lee el primer artículo de la serie para configurarlo.
Inicializa una aplicación Anchor ejecutando el comando de anchor:
anchor init airdrop-distribution
Actualiza las importaciones en el archivo programs/airdrop-distribution/lib.rs con estas importaciones de Anchor. Necesitamos:
- la importación
ed25519_programpara nuestra verificación, - la clave pública para las diferentes instancias donde la necesitamos,
- y luego usaremos las importaciones
sysvarpara la introspección.
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
ed25519_program,
pubkey::Pubkey,
sysvar::instructions as ix_sysvar,
sysvar::SysvarId
};
Conserva tu declare_id generado
declare_id!("Gh2JoycvxfreSgjzhCHuRDK7sZDAbxeo7Pd8GKCoSLmS");
A continuación, incluiremos el resto de la lógica del programa y la repasaremos paso a paso.
El programa contiene una función claim donde reside toda la lógica. Aquí hay un desglose de lo que sucede en la función:
- Carga el
sysvarde instrucciones para leer las instrucciones completas de la transacción. - Encuentra el índice de la instrucción actual y carga la inmediatamente anterior a ella.
- Requiere que la instrucción anterior se haya enviado al programa nativo
Ed25519y no tenga cuentas. - Analiza (parses) la data de la
Ed25519 Verification Instruction: Instruction 1, luego verifica el encabezado, valida el número de firmas y extrae los offsets. - Verifica que todos los offsets en el encabezado apunten a los datos dentro de la misma instrucción y apunten específicamente a la firma, la clave pública y el mensaje.
- Reconstruye la clave pública del distribuidor a partir de los datos y verifica que coincida con la cuenta del distribuidor esperada.
- Reconstruye el mensaje firmado
[recipient pubkey (32)][amount (u64 little-endian)]y verifica que el destinatario del mensaje firmado coincida con la cuenta del destinatario en laAirdropClaim Instruction: Instruction 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<Claim>) -> Result<()> {
// --- 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<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 ---
let pk_slice = &data[public_key_offset..public_key_offset + PUBKEY_LEN];
let mut pk_arr = [0u8; 32];
pk_arr.copy_from_slice(pk_slice);
let distributor_pubkey = Pubkey::new_from_array(pk_arr);
if distributor_pubkey != ctx.accounts.expected_distributor.key() {
return err!(AirdropError::DistributorMismatch);
}
// --- reconstruct and validate the signed message ---
// Format: [recipient pubkey (32 bytes)][amount (u64 little-endian)]
let msg = &data[message_offset..message_offset + message_size];
let mut rec_arr = [0u8; 32];
rec_arr.copy_from_slice(&msg[0..32]);
let recipient_from_msg = Pubkey::new_from_array(rec_arr);
if recipient_from_msg != ctx.accounts.recipient.key() {
return err!(AirdropError::RecipientMismatch);
}
let mut amount_bytes = [0u8; 8];
amount_bytes.copy_from_slice(&msg[32..40]);
let amount = u64::from_le_bytes(amount_bytes);
// User can now claim the airdrop token.
// The airdrop transfer can now be implemented here.
Ok(())
}
}
#[derive(Accounts)]
pub struct Claim<'info> {
/// The recipient of the airdrop (must match the recipient in the signed message)
#[account(mut)]
pub recipient: Signer<'info>,
/// Expected distributor pubkey (checked against signed message, not Anchor)
/// CHECK: Validated manually against the parsed message
pub expected_distributor: UncheckedAccount<'info>,
/// The sysvar containing the full transaction's instructions
/// CHECK: Validated by requiring its well-known address
#[account(address = ix_sysvar::Instructions::id())]
pub instruction_sysvar: AccountInfo<'info>,
/// System program used for the transfer
pub system_program: Program<'info, System>,
}
#[error_code]
pub enum AirdropError {
#[msg("Invalid instruction sysvar")]
InvalidInstructionSysvar,
#[msg("Expected Ed25519 program id")]
BadEd25519Program,
#[msg("Bad Ed25519 accounts")]
BadEd25519Accounts,
#[msg("Distributor public key mismatch")]
DistributorMismatch,
#[msg("Recipient mismatch in message")]
RecipientMismatch,
}
Expliquemos las partes clave del código anterior. Cubriremos:
- Cómo el código anterior carga la
Ed25519 Verification Instruction: Instruction 1desde la cuenta sysvar utilizando las funciones auxiliares (helper functions) de indexación relativa de instrucciones proporcionadas por el SDK de Rust de Solana. - El acceso y verificación de la data de la
Ed25519 Verification Instruction: Instruction 1. - La recuperación del recuento de firmas y los offsets en la región del encabezado.
- La validación para garantizar que estamos accediendo a la firma, la clave pública y el mensaje precisos en la transacción actual.
- El acceso a la firma del distribuidor, la clave pública y el mensaje en la data de la instrucción.
Compartiremos capturas de pantalla de cada parte clave del código del programa anterior y las discutiremos en las siguientes secciones.
1. Introspección: Carga y validación de la Ed25519 Verification Instruction: Instruction 1
La siguiente captura de pantalla de nuestro código de programa muestra cómo usamos la introspección de instrucciones a través del sysvar de instrucciones para verificar la Ed25519 Verification Instruction: Instruction 1.
- Llamamos a
load_current_index_checked()para obtener el índice de la instrucción actual y aload_instruction_at_checked()para cargar la instrucción inmediatamente anterior. - Una vez que tenemos la instrucción anterior (
Ed25519 Verification Instruction: Instruction 1), nosotros:- verificamos que su program ID coincida con el
Ed25519Program. Esto asegura que la instrucción es, de hecho, una verificación de firma Ed25519. - y confirmamos que la lista de cuentas de la instrucción está vacía.
- verificamos que su program ID coincida con el
- Una vez que estas comprobaciones tienen éxito, extraemos la data de la instrucción, que es un vector, y la vinculamos (bind) a la variable
data.

Ahora, hemos logrado verificar la información de nivel superior de la instrucción ed2559Program: el ID y las cuentas. También hemos obtenido la data de la Ed25519 Verification Instruction: Instruction 1, por lo que el siguiente paso es verificar el contenido de los datos. La data es un vector del tipo de datos u8.
2. Acceso y verificación de la data de la Ed25519 Verification Instruction: Instruction 1
Esperamos que la data de la instrucción codifique, en orden: un encabezado que especifique el recuento de firmas y los offsets para los campos siguientes; la clave pública del distribuidor; el mensaje; y la firma Ed25519 del distribuidor.

Ahora, revisaremos la siguiente parte de nuestro código para ver cómo el programa del airdrop accede y verifica la data de la Ed25519 Verification Instruction: Instruction 1.
3. Recuperación del recuento de firmas y los offsets en la región del encabezado
El código en la captura de pantalla a continuación extrae el recuento de firmas, los offsets y los índices que apuntan a dónde se encuentra cada elemento en el vector de datos de la Ed25519 Verification Instruction: Instruction 1.
En el encabezado, el recuento de firmas debería estar en el primer índice, obtenemos eso con data[0]. La expectativa es que el recuento sea 1 porque solo debería haber una firma del distribuidor. Imponemos esto con una declaración require.
Después de eso, el encabezado contiene valores de offset y de índice que nos dicen dónde encontrar la clave pública del distribuidor, la firma y el mensaje dentro de la data de la instrucción.
Para analizarlos, definimos un closure read_u16 que avanza a través del búfer de datos de dos en dos bytes, devolviendo cada offset como un u16. Esto facilita la reconstrucción de un diseño de datos de instrucción coherente.

4. Validación para garantizar que estamos accediendo a la firma, la clave pública y el mensaje precisos en la instrucción actual
En este punto, tenemos el recuento de firmas y los offsets, pero necesitamos asegurarnos de que:
- Estamos interactuando con la instrucción que cargamos desde el sysvar como la instrucción actual. Recuerda que el índice de la firma (
signature_ix_idx), la clave pública (public_key_ix_idx) y el mensaje (message_ix_idx) en la data de la instrucción actual se establecen enu16::MAXen el código fuente de Ed25519. Cualquier otro valor apuntaría a otra instrucción en la transacción. - Los offsets apuntan más allá del encabezado de 16 bytes y hacia la parte del vector que contiene la firma, la clave pública y el mensaje.

5. Acceso a la firma del distribuidor, la clave pública y el mensaje en el vector de data de la instrucción
La siguiente captura de pantalla muestra cómo utilizamos los offsets analizados desde el encabezado de data de la Ed25519 Verification Instruction: Instruction 1 para localizar la clave pública del distribuidor y el contenido del mensaje (destinatario y cantidad) dentro de los datos de la instrucción, validándolos con la versión proporcionada por el usuario en la AirdropClaim Instruction: Instruction 2.
- La primera región marcada muestra cómo extraemos la clave pública del distribuidor a partir de la data de la
Ed25519 Instruction, la reconstruimos como unPubkeyde 32 bytes y la comparamos con la clave públicaexpected_distributorde la cuenta del distribuidor en laAirdropClaim Instruction: Instruction 2. - La segunda región marcada muestra cómo extraemos el mensaje firmado (destinatario + cantidad), reconstruimos el pubkey del destinatario y verificamos que coincida con la cuenta del
recipienten laAirdropClaim Instruction: Instruction 2.
Si ambas comprobaciones tienen éxito, la verificación de la firma está completa. En este punto, podrías implementar la transferencia del token al destinatario. Dado que este artículo se centra en la verificación, no hemos implementado la transferencia.

El lado del cliente: construcción de la transacción off-chain
Hemos visto cómo funciona la verificación de firmas. Ahora, probémoslo creando una transacción que contendrá las dos instrucciones — Ed25519 Verification Instruction: Instruction 1 y la AirdropClaim Instruction: Instruction 2.
Dependencias
Usaremos la biblioteca criptográfica tweetnacl para crear la firma del distribuidor, así que instálala ejecutando el comando a continuación:
yarn add tweetnacl
Una vez hecho esto, agrega tweetnacl a tus importaciones en tests/airdrop-distribution.ts junto con las siguientes importaciones, como se muestra a continuación. Usaremos la dependencia Ed25519Program para crear la primera instrucción para la verificación, mientras que TransactionInstruction es el tipo de instrucción de transacción estándar esperado.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { expect } from "chai";
// Add the following
import { Airdrop } from "../target/types/airdrop"; // The IDL
import {
PublicKey,
Keypair,
SystemProgram,
Transaction,
**TransactionInstruction,
Ed25519Program**
} from "@solana/web3.js";
import * as nacl from "tweetnacl";
Tendremos cuatro escenarios de casos de prueba:
- Reclamo válido (Valid claim): el distribuidor firma el destinatario y la cantidad correctos, la instrucción del
Ed25519Programse ejecuta antes de la instrucción declaim, entonces la transacción tiene éxito. - Orden incorrecto (Wrong order): la instrucción de
claimviene antes que elEd25519Program, la transacción falla conInvalidInstructionSysvar. - Distribuidor incorrecto (Wrong distributor): la firma no coincide con la firma del
expectedDistributor, la transacción falla conDistributorMismatch. - Destinatario incorrecto (Wrong recipient): el destinatario firmado difiere del usuario que intenta reclamar la firma del airdrop, la transacción falla con
RecipientMismatch. - Múltiples reclamos (Multiple claims): un caso de prueba para mostrar que un intento de engañar al sistema mediante la construcción de múltiples
AirdropClaim Instructionfallará. Esto se debe a que la lógica de introspección del programa solo observa laEd25519 Verification Instruction: Instruction 1inmediatamente anterior, por lo que la segundaAirdropClaim Instructionfallará.
Comienza configurando primero la prueba para usar el clúster local y configura cuentas de prueba para el distribuidor, el destinatario y una cuenta de distribuidor inválida para los casos de prueba negativos.
// ...
describe("airdrop", () => {
// Configure the client to use the local cluster
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Airdrop as Program<Airdrop>;
const provider = anchor.getProvider();
// Test accounts
let distributorKeypair: Keypair;
let recipientKeypair: Keypair;
let invalidDistributorKeypair: Keypair;
before(async () => {
// Generate test keypairs
distributorKeypair = Keypair.generate();
recipientKeypair = Keypair.generate();
invalidDistributorKeypair = Keypair.generate();
});
A continuación, agregaremos una función auxiliar (helper) que construye la Ed25519 Verification Instruction: Instruction 1. Construye el mensaje a partir del destinatario y la cantidad, lo firma con la clave del distribuidor y luego usa Ed25519Program.createInstructionWithPublicKey para devolver un TransactionInstruction que el entorno de ejecución (runtime) pueda verificar.
function createEd25519Instruction(
distributorKeypair: Keypair,
recipientPubkey: PublicKey,
amount: number
): TransactionInstruction {
// Build the message: 32 bytes recipient pubkey + 8 bytes amount
const message = Buffer.alloc(40);
recipientPubkey.toBuffer().copy(message, 0);
message.writeBigUInt64LE(BigInt(amount), 32);
// Sign the message with distributor
const signature = nacl.sign.detached(message, distributorKeypair.secretKey);
// Use the helper to build the instruction
return Ed25519Program.createInstructionWithPublicKey({
publicKey: distributorKeypair.publicKey.toBytes(),
message,
signature,
});
}
Reutilizaremos la función anterior en nuestros casos de prueba para crear la Ed25519 Verification Instruction: Instruction 1. Comencemos con nuestro primer caso de prueba, que es un reclamo de airdrop válido que debería tener éxito.
Creamos dos instrucciones: Ed25519 Verification Instruction: Instruction 1 y la AirdropClaim Instruction: Instruction 2. Pasamos las cuentas del distribuidor, destinatario y sysvar de instrucciones a la función claim del programa, como se definió anteriormente. Luego enviamos la transacción y confirmamos que tuvo éxito. En caso de éxito, devuelve un ID de transacción; de lo contrario, obtenemos un error.
it("Successfully claims airdrop with valid signature", async () => {
const claimAmount = 1000000;
// Create Ed25519 Signature Verification Instruction: Instruction 1
const ed25519Ix = createEd25519Instruction(
distributorKeypair,
recipientKeypair.publicKey,
claimAmount
);
// Create the AirdropClaim Instruction: Instruction 2
const claimIx = await program.methods
.claim()
.accountsPartial({
recipient: recipientKeypair.publicKey,
expectedDistributor: distributorKeypair.publicKey,
instructionSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
const tx = new Transaction();
tx.add(ed25519Ix); // Add Instruction 1 to the transaction
tx.add(claimIx); // Add Instruction 2 to the transaction
// Just expect the transaction to succeed
expect(await provider.sendAndConfirm(tx, [recipientKeypair])).to.not.be.empty;
});
Los casos de falla implicarán el mismo proceso, solo necesitaremos agregar datos inválidos que harán que fallen. Así que aquí está el código de prueba completo con comentarios explicativos.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Airdrop } from "../target/types/airdrop";
import { PublicKey, Keypair, SystemProgram, Transaction, TransactionInstruction, Ed25519Program } from "@solana/web3.js";
import { expect } from "chai";
import * as nacl from "tweetnacl";
describe("airdrop", () => {
// Configure the client to use the local cluster
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Airdrop as Program<Airdrop>;
const provider = anchor.getProvider();
// Test accounts
let distributorKeypair: Keypair;
let recipientKeypair: Keypair;
let invalidDistributorKeypair: Keypair;
before(async () => {
// Generate test keypairs
distributorKeypair = Keypair.generate();
recipientKeypair = Keypair.generate();
invalidDistributorKeypair = Keypair.generate();
});
function createEd25519Instruction(
distributorKeypair: Keypair,
recipientPubkey: PublicKey,
amount: number
): TransactionInstruction {
// Build the message: 32 bytes recipient pubkey + 8 bytes amount
const message = Buffer.alloc(40);
recipientPubkey.toBuffer().copy(message, 0);
message.writeBigUInt64LE(BigInt(amount), 32);
// Sign the message with distributor
const signature = nacl.sign.detached(message, distributorKeypair.secretKey);
// Use the helper to build the instruction
return Ed25519Program.createInstructionWithPublicKey({
publicKey: distributorKeypair.publicKey.toBytes(),
message,
signature,
});
}
it("Successfully claims airdrop with valid signature", async () => {
const claimAmount = 1000000;
const ed25519Ix = createEd25519Instruction(
distributorKeypair,
recipientKeypair.publicKey,
claimAmount
);
const claimIx = await program.methods
.claim()
.accountsPartial({
recipient: recipientKeypair.publicKey,
expectedDistributor: distributorKeypair.publicKey,
instructionSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
const tx = new Transaction();
tx.add(ed25519Ix);
tx.add(claimIx); // AirdropClaim Instruction: Instruction 2
// Just expect the transaction to succeed
expect(await provider.sendAndConfirm(tx, [recipientKeypair])).to.not.be.empty;
});
it("Fails when Ed25519 instruction is not first", async () => {
const claimAmount = 1000000;
const claimIx = await program.methods
.claim()
.accountsPartial({
recipient: recipientKeypair.publicKey,
expectedDistributor: distributorKeypair.publicKey,
instructionSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
const ed25519Ix = createEd25519Instruction(
distributorKeypair,
recipientKeypair.publicKey,
claimAmount
);
// Create transaction with claim first, then Ed25519 (wrong order)
const tx = new Transaction();
tx.add(claimIx);
tx.add(ed25519Ix);
try {
await provider.sendAndConfirm(tx, [recipientKeypair]);
expect.fail("Should have failed with wrong instruction order");
} catch (error) {
expect(error.message).to.include("InvalidInstructionSysvar");
}
});
it("Fails with distributor mismatch", async () => {
const claimAmount = 1000000;
// Create Ed25519 instruction with wrong distributor
const ed25519Ix = createEd25519Instruction(
invalidDistributorKeypair, // Wrong distributor signs
recipientKeypair.publicKey,
claimAmount
);
const claimIx = await program.methods
.claim()
.accountsPartial({
recipient: recipientKeypair.publicKey,
expectedDistributor: distributorKeypair.publicKey, // But we expect the correct one
instructionSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
const tx = new Transaction();
tx.add(ed25519Ix);
tx.add(claimIx);
try {
await provider.sendAndConfirm(tx, [recipientKeypair]);
expect.fail("Should have failed with distributor mismatch");
} catch (error) {
expect(error.message).to.include("DistributorMismatch");
}
});
it("Fails with recipient mismatch", async () => {
const claimAmount = 1000000;
const wrongRecipient = Keypair.generate();
// Create Ed25519 instruction with wrong recipient in message
const ed25519Ix = createEd25519Instruction(
distributorKeypair,
wrongRecipient.publicKey, // Wrong recipient in signed message
claimAmount
);
const claimIx = await program.methods
.claim()
.accountsPartial({
recipient: recipientKeypair.publicKey,
expectedDistributor: distributorKeypair.publicKey,
instructionSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
const tx = new Transaction();
tx.add(ed25519Ix);
tx.add(claimIx);
try {
await provider.sendAndConfirm(tx, [recipientKeypair]);
expect.fail("Should have failed with recipient mismatch");
} catch (error) {
expect(error.message).to.include("RecipientMismatch");
}
});
it("Fails when multiple claim instructions try to reuse the same Ed25519 signature", async () => {
const claimAmount = 1000000;
// Create a single Ed25519 instruction
const ed25519Ix = createEd25519Instruction(
distributorKeypair,
recipientKeypair.publicKey,
claimAmount
);
// First claim instruction (valid)
const claimIx1 = await program.methods
.claim()
.accountsPartial({
recipient: recipientKeypair.publicKey,
expectedDistributor: distributorKeypair.publicKey,
instructionSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
// Second claim instruction (tries to reuse the same Ed25519)
const claimIx2 = await program.methods
.claim()
.accountsPartial({
recipient: recipientKeypair.publicKey,
expectedDistributor: distributorKeypair.publicKey,
instructionSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
const tx = new Transaction();
tx.add(ed25519Ix);
tx.add(claimIx1);
tx.add(claimIx2);
try {
await provider.sendAndConfirm(tx, [recipientKeypair]);
expect.fail("Should have failed because multiple claims tried to reuse the same signature");
} catch (error) {
// The second claim fails because its immediately preceding instruction
// is not the Ed25519 verification, so the program throws
expect(error.message).to.include("BadEd25519Program");
}
});
});
Ejecutemos la prueba con el comando a continuación:
anchor test
Y el resultado debería verse así:

Nuestra implementación hasta ahora se ha centrado en la verificación de firmas. Ten en cuenta que este ejemplo es con fines de aprendizaje; debes considerar las mejores prácticas estándar de seguridad de programas al crear y enviar transacciones reales.
Ha habido casos en los que una implementación incorrecta de offsets ha introducido vulnerabilidades. Un ejemplo de este tipo se cubre en el artículo “Wrong Offset: Bypassing Signature Verification.” Si bien lo que hemos aprendido en este artículo no se ve afectado por esa vulnerabilidad, vale la pena ser consciente del riesgo potencial.
Este artículo es parte de una serie de tutoriales sobre Solana.