Transacciones e instrucciones

En Solana, enviamos transacciones para interactuar con la red. Las transacciones incluyen una o más instrucciones, cada una representando una operación que debe ser procesada. La lógica de ejecución de las instrucciones es almacenada en programas desplegados en la red Solana, donde cada programa guarda su propio conjunto de instrucciones.

A continuación se listan los detalles clave sobre cómo se ejecutan las transacciones:

  • Orden de ejecución: Si una transacción incluye varias instrucciones, las instrucciones son procesadas en el orden que se agregan a la transacción.
  • Atomicidad: una transacción es atómica, lo que significa que se completan todas las instrucciones con éxito, o falla por completo. Si alguna instrucción dentro de la transacción falla, ninguna de las instrucciones es ejecutada.

Por simplicidad, una transacción puede ser pensada como una solicitud para procesar una o múltiples instrucciones.

Transacción simplificadaTransacción simplificada

Puedes imaginar una transacción como un sobre, donde cada instrucción es un documento que rellenas y colocas dentro del sobre. Luego enviamos el sobre para procesar los documentos, al igual que enviar una transacción en la red procesa nuestras instrucciones.

Puntos clave #

  • Las transacciones en Solana consisten en instrucciones que interactúan con varios programas en la red, donde cada instrucción representa una operación específica.

  • Cada instrucción especifica el programa para ejecutar la instrucción, las cuentas requeridas por la instrucción, y los datos necesarios para la ejecución de la instrucción.

  • Las instrucciones dentro de una transacción se procesan en el orden en el que aparecen en la lista.

  • Las transacciones son atómicas, lo que significa que todas las instrucciones se procesan con éxito, o toda la transacción falla.

  • El tamaño máximo de una transacción es de 1232 bytes.

Ejemplo básico #

A continuación se muestra un diagrama que representa una transacción con una sola instrucción para transferir SOL de un remitente a un receptor.

Las "billeteras" individuales en Solana son cuentas propiedad del programa del sistema. Como parte del modelo de cuentas de Solana, solo el programa designado como owner de una cuenta puede modificar los datos de la cuenta.

Por lo tanto, transferir SOL desde una cuenta "billetera" requiere enviar una transacción para invocar la instrucción de transferencia en el programa del sistema.

Transferencia de SOLTransferencia de SOL

La cuenta del remitente debe ser incluida como un firmante (is_signer) en la transacción para aprobar la deducción de su saldo de lamports. Tanto las cuentas de remitente como el destinatario deben ser mutables (is_writable) porque la instrucción modifica el balance de lamports para ambas cuentas.

Una vez enviada la transacción, se invoca el programa del sistema para procesar la instrucción de transferencia. A continuación, el programa del sistema actualiza los saldos en lamports tanto de la cuenta del remitente como la del destinatario.

Proceso de transferir SOLProceso de transferir SOL

Transferencia simple de SOL #

Aquí tienes el link a un ejemplo de Solana Playground de cómo construir una instrucción de transferencia SOL usando el método SystemProgram.transfer:

// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL
 
// Create a transfer instruction for transferring SOL from wallet_1 to wallet_2
const transferInstruction = SystemProgram.transfer({
  fromPubkey: sender.publicKey,
  toPubkey: receiver.publicKey,
  lamports: transferAmount * LAMPORTS_PER_SOL, // Convert transferAmount to lamports
});
 
// Add the transfer instruction to a new transaction
const transaction = new Transaction().add(transferInstruction);

Ejecute el script e inspeccione los detalles de la transacción registrados en la consola. En las secciones de abajo, pasaremos por los detalles de lo que está sucediendo.

Transacción #

Una transacción de Solana consiste en:

  1. Firmas: Un arreglo de firmas incluidas en la transacción.
  2. Mensaje: Lista de instrucciones a procesar atómicamente.

Formato de la transacciónFormato de la transacción

La estructura de un mensaje de transacción consta de:

Mensaje de transacciónMensaje de transacción

Tamaño de la transacción #

La red de Solana se adiere la unidad máxima de transmisión (MTU) de tamaño 1280 bytes, consistente con las limitaciones de tamaño de la MTU de IPv6, para asegurar una transmisión de información por UDP rápida y confiable. Después de contabilizar las cabeceras necesarias (40 bytes para IPv6 y 8 bytes para la cabecera del fragmento), 1232 bytes siguen disponibles para datos de paquetes, tales como transacciones serializadas.

Esto significa que el tamaño total de una transacción en Solana está limitado a 1232 bytes. La combinación de las firmas y el mensaje no puede exceder este límite.

  • Firmas: Cada firma requiere 64 bytes. El número de firmas puede variar, dependiendo de los requisitos de la transacción.
  • Mensaje: El mensaje incluye instrucciones, cuentas y metadatos adicionales, donde cada cuenta que requiere 32 bytes. El tamaño combinado de las cuentas y los metadatos puede variar, dependiendo de las instrucciones incluidas en la transacción.

Formato de la transacciónFormato de la transacción

Encabezado del mensaje #

El encabezado de mensajes especifica los privilegios de las cuentas incluidas en el arreglo de direcciones de cuenta de la transacción. Se compone de tres bytes, cada uno contiene un entero u8, que en conjunto especifica:

  1. El número de firmas requeridas para la transacción.
  2. El número de direcciones de cuenta de sólo lectura que requieren firmas.
  3. El número de direcciones de cuenta de sólo lectura que no requieren firmas.

Encabezado de mensajesEncabezado de mensajes

Formato de arreglo compacto #

Un arreglo compacto en el contexto de un mensaje de transacción se refiere a un arreglo serializado en el siguiente formato:

  1. La longitud del arreglo, codificado como compact-u16.
  2. Los elementos individuales del arreglo listados secuencialmente después de la longitud codificada.

Formato de arreglo compactoFormato de arreglo compacto

Este método de codificación se utiliza para especificar las longitudes de los arreglos de direcciones de cuentas e instrucciones dentro de un mensaje de transacción.

Arreglo de direcciones de cuentas #

El mensaje de una transacción incluye todas las direcciones de cuentas necesarias para las instrucciones dentro de la transacción.

Este arreglo comienza con una codificación compact-u16 del número de direcciones de cuentas, seguido de las direcciones ordenadas por los privilegios para las cuentas. Los metadatos en el encabezado del mensaje se utilizan para determinar el número de cuentas en cada sección.

  • Cuentas con permisos de escritura y firmantes
  • Cuentas de sólo lectura y firmantes
  • Cuentas con permisos de escritura y no firmantes
  • Cuentas de sólo lectura y no firmantes

Arreglo compacto de direcciones de cuentasArreglo compacto de direcciones de cuentas

Blockhash Reciente #

Todas las transacciones incluyen un blockhash reciente para actuar como una marca de tiempo para la transacción. El blockhash es utilizado para prevenir duplicaciones y eliminar transacciones abandonadas.

La edad máxima del blockhash de una transacción es de 150 bloques (~1 minuto asumiendo que los bloques toman 400ms). Si el blockhash de una transacción es 150 bloques menor que el último blockhash, se considera caducado. Esto significa que las transacciones no procesadas dentro de un plazo específico nunca se ejecutarán.

Puedes usar el método del RPC getLatestBlockhash para obtener el blockhash actual y la altura del último bloque a la que el blockhash será válido. Aquí hay un ejemplo en Solana Playground.

Arreglo de instrucciones #

El mensaje de una transacción incluye un arreglo de todas las instrucciones a procesar. Las instrucciones dentro del mensaje de una transacción están en el formato de instrucción compilada.

Al igual que el arreglo de direcciones de cuenta, este arreglo compacto comienza con una codificación compact-u16 del número de instrucciones, seguida de un arreglo de instrucciones. Cada instrucción en el arreglo especifica la siguiente información:

  1. ID del programa: Identifica el programa en la cadena de bloques que procesará la instrucción. Esto se representa como un índice u8 que apunta a la dirección de la cuenta dentro del arreglo de direcciones de cuentas.
  2. Arreglo compacto de índices de direcciones de cuenta: Arreglo de índices u8 apuntando a los arreglos de direcciones de cuentas para cada cuenta requerida por la instrucción.
  3. Arreglo compacto de datos opacos en u8: Un arreglo de bytes de u8 específico para el programa invocado. Estos datos especifican la instrucción a invocar del programa con cualquier dato adicional que la instrucción requiera (como argumentos de función).

Arreglo compacto de instruccionesArreglo compacto de instrucciones

Ejemplo de la estructura de una transacción #

A continuación se muestra un ejemplo de la estructura de una transacción que incluye una instrucción para transferir SOL. Muestra los detalles del mensaje, incluyendo el encabezado, las claves de las cuentas, el blockhash y las instrucciones, además de las firmas de la transacción.

  • header: Incluye datos usados para especificar los privilegios de lectura/escritura y firmante en el arreglo accountKeys.

  • accountKeys: Arreglo que incluye las direcciones de las cuentas usadas por las instrucciones de la transacción.

  • recentBlockhash: El blockhash incluido en la transacción cuando la transacción fue creada.

  • instructions: Arreglo que incluye todas las instrucciones de la transacción. Cada account y programIdIndex en una instrucción hace referencia al arreglo de accountKeys por índice.

  • signatures: Arreglo que incluye las firmas requeridas por las instrucciones de la transacción. Una firma se crea firmando el mensaje de transacción usando la clave privada correspondiente para una cuenta.

"transaction": {
    "message": {
      "header": {
        "numReadonlySignedAccounts": 0,
        "numReadonlyUnsignedAccounts": 1,
        "numRequiredSignatures": 1
      },
      "accountKeys": [
        "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
        "5snoUseZG8s8CDFHrXY2ZHaCrJYsW457piktDmhyb5Jd",
        "11111111111111111111111111111111"
      ],
      "recentBlockhash": "DzfXchZJoLMG3cNftcf2sw7qatkkuwQf4xH15N5wkKAb",
      "instructions": [
        {
          "accounts": [
            0,
            1
          ],
          "data": "3Bxs4NN8M2Yn4TLb",
          "programIdIndex": 2,
          "stackHeight": null
        }
      ],
      "indexToProgramIds": {}
    },
    "signatures": [
      "5LrcE2f6uvydKRquEJ8xp19heGxSvqsVbcqUeFoiWbXe8JNip7ftPQNTAVPyTK7ijVdpkzmKKaAQR7MWMmujAhXD"
    ]
  }

Instrucciones #

Una instrucción es una solicitud para procesar una acción específica en la cadena de bloques y la unidad mas pequeña de ejecución de lógica en un programa.

Al crear una instrucción para agregarla a una transacción, cada instrucción debe incluir la siguiente información:

  • Dirección del programa: Especifica el programa que está siendo invocado.
  • Cuentas: Muestra todas las cuentas a las que leen o escriben las instrucciones, incluyendo otros programas, usando la estructura AccountMeta.
  • Datos de instrucción: Un arreglo de bytes que especifica el manejador de instrucciones que invoca el programa, además de cualquier dato adicional requerido (argumentos de función).

Instrucción de una TransacciónInstrucción de una Transacción

AccountMeta #

Para cada cuenta requerida por una instrucción, la siguiente información debe ser especificada:

  • pubkey: La dirección en la cadena de bloques de una cuenta
  • is_signer: Especifica si la cuenta es requerida como un firmante en la transacción
  • is_writable: Especifica si los datos de la cuenta serán modificados

Esta información se conoce como AccountMeta.

AccountMetaAccountMeta

Al especificar todas las cuentas requeridas por una instrucción, y si cada cuenta es escribible, las transacciones se pueden procesar en paralelo.

Por ejemplo, dos transacciones que no incluyen ninguna cuenta que escriba en el mismo estado pueden ejecutarse al mismo tiempo.

Ejemplo de la estructura de una instrucción #

A continuación hay un ejemplo de la estructura de una instrucción para transferir SOL que detalla las llaves de la cuenta, identificador del programa, y datos requeridos por la instrucción.

  • keys: Incluye el AccountMeta para cada cuenta requerida por una instrucción.
  • programId: La dirección del programa que contiene la lógica de ejecución para la instrucción invocada.
  • data: Los datos para la instrucción como un buffer de bytes
{
  "keys": [
    {
      "pubkey": "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R",
      "isSigner": true,
      "isWritable": true
    },
    {
      "pubkey": "BpvxsLYKQZTH42jjtWHZpsVSa7s6JVwLKwBptPSHXuZc",
      "isSigner": false,
      "isWritable": true
    }
  ],
  "programId": "11111111111111111111111111111111",
  "data": [2,0,0,0,128,150,152,0,0,0,0,0]
}

Ejemplo ampliado #

Los detalles para construir las instrucciones del programa suelen ser abstraídos por las bibliotecas cliente. Sin embargo, si no hay una biblioteca disponible, puedes construir manualmente la instrucción.

Transferencia manual de SOL #

Aquí está un ejemplo en Solana Playground de cómo construir manualmente una instrucción de transferencia SOL:

// Define el monto a transferir
const transferAmount = 0.01; // 0.01 SOL
 
// El indice de la instrucción de transferencia del programa del sistema
const transferInstructionIndex = 2;
 
// Un buffer de data que se pasará a la instrucción de transferencia
const instructionData = Buffer.alloc(4 + 8); // uint32 + uint64
// Escribe el indice de la instrucción en el buffer
instructionData.writeUInt32LE(transferInstructionIndex, 0);
// Escribe el monto a transferir en el buffer
instructionData.writeBigUInt64LE(BigInt(transferAmount * LAMPORTS_PER_SOL), 4);
 
// Manualmente crea la instrucción de transferencia
const transferInstruction = new TransactionInstruction({
  keys: [
    { pubkey: sender.publicKey, isSigner: true, isWritable: true },
    { pubkey: receiver.publicKey, isSigner: false, isWritable: true },
  ],
  programId: SystemProgram.programId,
  data: instructionData,
});
 
// Agrega la instrucción a una transacción
const transaction = new Transaction().add(transferInstruction);

Bajo el capó, el ejemplo simple está usando el método SystemProgram.transfer que es funcionalmente equivalente al ejemplo ampliado anterior. El método SystemProgram.transfer simplemente abstrae los detalles de crear el buffer de datos de la instrucción y el AccountMeta para cada cuenta requerida por la instrucción.