Transacciones e instrucciones

En Solana, los usuarios envían transacciones para interactuar con la red. Las transacciones contienen una o más instrucciones que especifican operaciones a procesar. La lógica de ejecución para las instrucciones se almacena en programas desplegados en la red de Solana, donde cada programa define su propio conjunto de instrucciones.

A continuación se presentan detalles clave sobre el procesamiento de transacciones en Solana:

  • Si una transacción incluye múltiples instrucciones, estas se ejecutan en el orden en que fueron añadidas a la transacción.
  • Las transacciones son "atómicas" - todas las instrucciones deben procesarse correctamente, o la transacción completa falla y no se produce ningún cambio.

Una transacción es esencialmente una solicitud para procesar una o más instrucciones. Puedes pensar en una transacción como un sobre que contiene formularios. Cada formulario es una instrucción que le dice a la red qué hacer. Enviar la transacción es como enviar el sobre por correo para que se procesen los formularios.

Transacción simplificadaTransacción simplificada

Puntos clave

  • Las transacciones de Solana incluyen instrucciones que invocan programas en la red.
  • Las transacciones son atómicas - si alguna instrucción falla, toda la transacción falla y no se produce ningún cambio.
  • Las instrucciones en una transacción se ejecutan en orden secuencial.
  • El límite de tamaño de la transacción es 1232 bytes.
  • Cada instrucción requiere tres piezas de información:
    1. La dirección del programa a invocar
    2. Las cuentas de las que la instrucción lee o en las que escribe
    3. Cualquier dato adicional requerido por la instrucción (p. ej., argumentos de función)

Ejemplo de transferencia de SOL

El diagrama a continuación representa una transacción con una sola instrucción para transferir SOL de un remitente a un receptor.

En Solana, las "carteras" son cuentas que pertenecen al System Program. Solo el propietario del programa puede cambiar los datos de una cuenta, por lo que transferir SOL requiere enviar una transacción para invocar el System Program.

Transferencia de SOLTransferencia de SOL

La cuenta del remitente debe firmar (is_signer) la transacción para permitir que el System Program deduzca su saldo de lamports. Las cuentas del remitente y del destinatario deben ser modificables (is_writable) ya que sus saldos de lamports cambian.

Después de enviar la transacción, el System Program procesa la instrucción de transferencia. El System Program luego actualiza los saldos de lamports de ambas cuentas, la del remitente y la del destinatario.

Proceso de transferencia de SOLProceso de transferencia de SOL

Los ejemplos a continuación muestran cómo enviar una transacción que transfiere SOL de una cuenta a otra. Consulta el código fuente de la instrucción de transferencia del System Program aquí.

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// Create a connection to cluster
const rpc = createSolanaRpc("http://localhost:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Fund sender with airdrop
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: sender.address,
lamports: lamports(LAMPORTS_PER_SOL), // 1 SOL
commitment: "confirmed"
});
// Check balance before transfer
const { value: preBalance1 } = await rpc.getBalance(sender.address).send();
const { value: preBalance2 } = await rpc.getBalance(recipient.address).send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
// Add the transfer instruction to a new transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
// Send the transaction to the network
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
const transactionSignature = getSignatureFromTransaction(signedTransaction);
// Check balance after transfer
const { value: postBalance1 } = await rpc.getBalance(sender.address).send();
const { value: postBalance2 } = await rpc.getBalance(recipient.address).send();
console.log(
"Sender prebalance:",
Number(preBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient prebalance:",
Number(preBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Sender postbalance:",
Number(postBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient postbalance:",
Number(postBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log("Transaction Signature:", transactionSignature);
Console
Click to execute the code.

Las bibliotecas cliente a menudo abstraen los detalles para construir instrucciones de programa. Si no hay una biblioteca disponible, puedes construir la instrucción manualmente. Esto requiere que conozcas los detalles de implementación de la instrucción.

Los ejemplos a continuación muestran cómo construir manualmente la instrucción de transferencia. La pestaña Expanded Instruction es funcionalmente equivalente a la pestaña Instruction.

const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount * LAMPORTS_PER_SOL
});

En las secciones siguientes, explicaremos los detalles de las transacciones y las instrucciones.

Instrucciones

Una instrucción en un programa de Solana puede considerarse como una función pública que cualquiera puede llamar utilizando la red Solana.

Puedes pensar en un programa de Solana como un servidor web alojado en la red de Solana, donde cada instrucción es como un punto de acceso API público que los usuarios pueden llamar para realizar acciones específicas. Invocar una instrucción es similar a enviar una solicitud POST a un punto de acceso API, permitiendo a los usuarios ejecutar la lógica de negocio del programa.

Para llamar a una instrucción de un programa en Solana, necesitas construir una Instruction con tres piezas de información:

  • ID del programa: La dirección del programa con la lógica de negocio para la instrucción que se está invocando.
  • Cuentas: La lista de todas las cuentas que la instrucción lee o en las que escribe.
  • Instruction Data: Un array de bytes que especifica qué instrucción invocar en el programa y cualquier argumento requerido por la instrucción.
Instruction
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 de transacciónInstrucción de transacción

AccountMeta

Al crear una Instruction, debes proporcionar cada cuenta requerida como un AccountMeta. El AccountMeta especifica lo siguiente:

  • pubkey: La dirección de la cuenta
  • is_signer: Si la cuenta debe firmar la transacción
  • is_writable: Si la instrucción modifica los datos de la cuenta
AccountMeta
pub struct AccountMeta {
/// An account's public key.
pub pubkey: Pubkey,
/// True if an `Instruction` requires a `Transaction` signature matching `pubkey`.
pub is_signer: bool,
/// True if the account data or metadata may be mutated during program execution.
pub is_writable: bool,
}

Al especificar de antemano qué cuentas lee o escribe una instrucción, las transacciones que no modifican las mismas cuentas pueden ejecutarse en paralelo.

Para saber qué cuentas requiere una instrucción, incluyendo cuáles deben ser modificables, de solo lectura, o firmar la transacción, debes consultar la implementación de la instrucción según la define el programa.

En la práctica, normalmente no tienes que construir una Instruction manualmente. La mayoría de los desarrolladores de programas proporcionan bibliotecas cliente con funciones auxiliares que crean las instrucciones por ti.

AccountMetaAccountMeta

Estructura de instrucción de ejemplo

Ejecuta los ejemplos a continuación para ver la estructura de una instrucción de transferencia de SOL.

import { generateKeyPairSigner, lamports } from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount
});
console.log(JSON.stringify(transferInstruction, null, 2));
Console
Click to execute the code.

Los siguientes ejemplos muestran la salida de los fragmentos de código anteriores. El formato exacto difiere dependiendo del SDK, pero cada instrucción de Solana requiere la siguiente información:

  • ID del programa: La dirección del programa que ejecutará la instrucción.
  • Cuentas: Una lista de cuentas requeridas por la instrucción. Para cada cuenta, la instrucción debe especificar su dirección, si debe firmar la transacción y si se escribirá en ella.
  • Datos: Un búfer de bytes que indica al programa qué instrucción ejecutar e incluye cualquier argumento requerido por la instrucción.
{
"accounts": [
{
"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC",
"role": 3,
"signer": {
"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC",
"keyPair": {
"privateKey": {},
"publicKey": {}
}
}
},
{
"address": "2mBY6CTgeyJNJDzo6d2Umipw2aGUquUA7hLdFttNEj7p",
"role": 1
}
],
"programAddress": "11111111111111111111111111111111",
"data": {
"0": 2,
"1": 0,
"2": 0,
"3": 0,
"4": 128,
"5": 150,
"6": 152,
"7": 0,
"8": 0,
"9": 0,
"10": 0,
"11": 0
}
}

Transacciones

Después de haber creado las instrucciones que deseas invocar, el siguiente paso es crear una Transaction y añadir las instrucciones a la transacción. Una transacción de Solana está compuesta por:

  1. Firmas: Un array de firmas de todas las cuentas requeridas como firmantes para las instrucciones en la transacción. Una firma se crea firmando el Message de la transacción con la clave privada de la cuenta.
  2. Mensaje: El mensaje de la transacción incluye la lista de instrucciones que se procesarán de forma atómica.
Transaction
pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}

Formato de transacciónFormato de transacción

La estructura de un mensaje de transacción consiste en:

  • Encabezado del mensaje: Especifica el número de firmantes y cuentas de solo lectura.
  • Direcciones de cuentas: Un array de direcciones de cuentas requeridas por las instrucciones en la transacción.
  • Hash de bloque reciente: Actúa como una marca de tiempo para la transacción.
  • Instrucciones: Un array de instrucciones a ejecutar.
Message
pub struct Message {
/// The message header, identifying signed and read-only `account_keys`.
pub header: MessageHeader,
/// All the account keys used by this transaction.
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
/// The id of a recent ledger entry.
pub recent_blockhash: Hash,
/// Programs that will be executed in sequence and committed in
/// one atomic transaction if all succeed.
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
}

Tamaño de transacción

Las transacciones de Solana tienen un límite de tamaño de 1232 bytes. Este límite proviene del tamaño de la Unidad Máxima de Transmisión (MTU) IPv6 de 1280 bytes, menos 48 bytes para encabezados de red (40 bytes IPv6 + 8 bytes de encabezado).

El tamaño total de una transacción (firmas y mensaje) debe mantenerse por debajo de este límite e incluye:

  • Firmas: 64 bytes cada una
  • Mensaje: Encabezado (3 bytes), claves de cuenta (32 bytes cada una), hash de bloque reciente (32 bytes) e instrucciones

Formato de transacciónFormato de transacción

Encabezado del mensaje

El encabezado del mensaje especifica los permisos para la cuenta en la transacción. Funciona en combinación con las direcciones de cuentas estrictamente ordenadas para determinar qué cuentas son firmantes y cuáles son escribibles.

  1. El número de firmas requeridas para todas las instrucciones en la transacción.
  2. El número de cuentas firmadas que son de solo lectura.
  3. El número de cuentas no firmadas que son de solo lectura.
MessageHeader
pub struct MessageHeader {
/// The number of signatures required for this message to be considered
/// valid. The signers of those signatures must match the first
/// `num_required_signatures` of [`Message::account_keys`].
pub num_required_signatures: u8,
/// The last `num_readonly_signed_accounts` of the signed keys are read-only
/// accounts.
pub num_readonly_signed_accounts: u8,
/// The last `num_readonly_unsigned_accounts` of the unsigned keys are
/// read-only accounts.
pub num_readonly_unsigned_accounts: u8,
}

Encabezado del mensajeEncabezado del mensaje

Formato de array compacto

Un array compacto en un mensaje de transacción es un array serializado en el siguiente formato:

  1. La longitud del array (codificada como compact-u16)
  2. Los elementos del array listados uno tras otro

Formato de array compactoFormato de array compacto

Este formato se utiliza para codificar las longitudes de los arrays de Direcciones de cuentas e Instrucciones en los mensajes de transacción.

Array de direcciones de cuentas

Un mensaje de transacción contiene una única lista de todas las direcciones de cuentas requeridas por sus instrucciones. El array comienza con un número compact-u16 que indica cuántas direcciones contiene.

Para ahorrar espacio, la transacción no almacena los permisos para cada cuenta individualmente. En su lugar, se basa en una combinación del MessageHeader y un orden estricto de las direcciones de cuentas para determinar los permisos.

Las direcciones siempre están ordenadas de la siguiente manera:

  1. Cuentas que son escribibles y firmantes
  2. Cuentas que son de solo lectura y firmantes
  3. Cuentas que son escribibles y no firmantes
  4. Cuentas que son de solo lectura y no firmantes

El MessageHeader proporciona los valores utilizados para determinar el número de cuentas para cada grupo de permisos.

Array compacto de direcciones de cuentasArray compacto de direcciones de cuentas

Blockhash reciente

Cada transacción requiere un blockhash reciente que sirve para dos propósitos:

  1. Actúa como una marca de tiempo para cuando se creó la transacción
  2. Previene transacciones duplicadas

Un blockhash expira después de 150 bloques (aproximadamente 1 minuto asumiendo tiempos de bloque de 400ms), después de lo cual la transacción se considera expirada y no puede ser procesada.

Puedes usar el método RPC getLatestBlockhash para obtener el blockhash actual y la última altura de bloque en la que el blockhash será válido.

Array de instrucciones

Un mensaje de transacción contiene un array de instrucciones en el tipo CompiledInstruction. Las instrucciones se convierten a este tipo cuando se añaden a una transacción.

Al igual que el array de direcciones de cuentas en el mensaje, comienza con una longitud compact-u16 seguida por los datos de instrucción. Cada instrucción contiene:

  1. Índice del ID del programa: Un índice que apunta a la dirección del programa en el array de direcciones de cuentas. Esto especifica el programa que procesa la instrucción.
  2. Índices de cuentas: Un array de índices que apuntan a las direcciones de cuentas requeridas para esta instrucción.
  3. Datos de instrucción: Un array de bytes que especifica qué instrucción invocar en el programa y cualquier dato adicional requerido por la instrucción (por ejemplo, argumentos de función).
CompiledInstruction
pub struct CompiledInstruction {
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

Array compacto de instruccionesArray compacto de instrucciones

Estructura de transacción de ejemplo

Ejecuta los ejemplos a continuación para ver la estructura de una transacción con una sola instrucción de transferencia de SOL.

import {
createSolanaRpc,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
const rpc = createSolanaRpc("http://localhost:8899");
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount
});
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Decode the messageBytes
const compiledTransactionMessage =
getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);
console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Console
Click to execute the code.

Los siguientes ejemplos muestran la salida del mensaje de transacción de los fragmentos de código anteriores. El formato exacto difiere según el SDK, pero incluye la misma información.

{
"version": 0,
"header": {
"numSignerAccounts": 1,
"numReadonlySignerAccounts": 0,
"numReadonlyNonSignerAccounts": 1
},
"staticAccounts": [
"HoCy8p5xxDDYTYWEbQZasEjVNM5rxvidx8AfyqA4ywBa",
"5T388jBjovy7d8mQ3emHxMDTbUF8b7nWvAnSiP3EAdFL",
"11111111111111111111111111111111"
],
"lifetimeToken": "EGCWPUEXhqHJWYBfDirq3mHZb4qDpATmYqBZMBy9TBC1",
"instructions": [
{
"programAddressIndex": 2,
"accountIndices": [0, 1],
"data": {
"0": 2,
"1": 0,
"2": 0,
"3": 0,
"4": 128,
"5": 150,
"6": 152,
"7": 0,
"8": 0,
"9": 0,
"10": 0,
"11": 0
}
}
]
}

Después de enviar una transacción, puedes recuperar sus detalles usando el método RPC getTransaction. La respuesta tendrá una estructura similar al siguiente fragmento. Alternativamente, puedes inspeccionar la transacción usando Solana Explorer.

Una "firma de transacción" identifica de manera única una transacción en Solana. Usas esta firma para buscar los detalles de la transacción en la red. La firma de transacción es simplemente la primera firma en la transacción. Ten en cuenta que la primera firma también es la firma del pagador de la tarifa de transacción.

Transaction Data
{
"blockTime": 1745196488,
"meta": {
"computeUnitsConsumed": 150,
"err": null,
"fee": 5000,
"innerInstructions": [],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"postBalances": [989995000, 10000000, 1],
"postTokenBalances": [],
"preBalances": [1000000000, 0, 1],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 13049,
"transaction": {
"message": {
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"accountKeys": [
"8PLdpLxkuv9Nt8w3XcGXvNa663LXDjSrSNon4EK7QSjQ",
"7GLg7bqgLBv1HVWXKgWAm6YoPf1LoWnyWGABbgk487Ma",
"11111111111111111111111111111111"
],
"recentBlockhash": "7ZCxc2SDhzV2bYgEQqdxTpweYJkpwshVSDtXuY7uPtjf",
"instructions": [
{
"accounts": [0, 1],
"data": "3Bxs4NN8M2Yn4TLb",
"programIdIndex": 2,
"stackHeight": null
}
],
"indexToProgramIds": {}
},
"signatures": [
"3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX"
]
},
"version": "legacy"
}

Is this page helpful?