Saltearse al contenido

Calculando Gas de Transacciones

Las transacciones de Aptos por defecto cobran una tarifa de gas base, independientemente de las condiciones del mercado. Para cada transacción, esta cantidad de “gas base” está basada en tres condiciones:

  1. Instrucciones.
  2. Almacenamiento.
  3. Payload.

Mientras más llamadas de función, declaraciones condicionales de bifurcación, etc. requiera una transacción, más gas de instrucción costará. De igual manera, mientras más lecturas de y escrituras al almacenamiento global requiera una transacción, más gas de almacenamiento costará. Finalmente, mientras más bytes tenga el payload de una transacción, más costará.

Como se explica en la sección principios de optimización, el gas de almacenamiento tiene por mucho el mayor efecto en el gas base. Para información de fondo sobre el modelo de gas de Aptos, ver The Making of the Aptos Gas Schedule.

Los parámetros básicos de gas de instrucción están definidos en [instr.rs] e incluyen los siguientes tipos de instrucción:

ParámetroSignificado
nopUna sin-operación
ParámetroSignificado
retRetorno
abortAbortar
br_trueEjecutar rama condicional verdadera
br_falseEjecutar rama condicional falsa
branchBifurcación
ParámetroSignificado
popExtraer de la pila
ld_u8Cargar un u8
ld_u16Cargar un u16
ld_u32Cargar un u32
ld_u64Cargar un u64
ld_u128Cargar un u128
ld_256Cargar un u256
ld_trueCargar un true
ld_falseCargar un false
ld_const_baseCosto base para cargar una constante
ld_const_per_byteCosto por byte para cargar una constante
ParámetroSignificado
imm_borrow_locTomar prestado inmutablemente
mut_borrow_locTomar prestado mutablemente
imm_borrow_fieldTomar prestado un campo inmutablemente
mut_borrow_fieldTomar prestado un campo mutablemente
imm_borrow_field_genericPréstamo inmutable de campo genérico
mut_borrow_field_genericPréstamo mutable de campo genérico
copy_loc_baseCosto base para copiar
copy_loc_per_abs_val_unitCosto por unidad de valor absoluto
move_loc_baseMover
st_loc_baseAlmacenar local base
ParámetroSignificado
call_baseCosto base para una llamada de función
call_per_argCosto por argumento de función
call_per_localCosto por argumento local
call_generic_baseCosto base para llamada genérica
call_generic_per_ty_argCosto por argumento de tipo
call_generic_per_argCosto genérico por argumento
call_generic_per_localCosto genérico por argumento local
ParámetroSignificado
pack_baseCosto base para empaquetar un struct
pack_per_fieldCosto para empaquetar un struct, por campo
pack_generic_baseCosto base para empaquetar struct genérico
pack_generic_per_fieldCosto por campo para struct genérico
unpack_baseCosto base para desempaquetar un struct
unpack_per_fieldCosto para desempaquetar un struct, por campo
unpack_generic_baseCosto base para desempaquetar struct genérico
unpack_generic_per_fieldCosto por campo para desempaquetar genérico
ParámetroSignificado
read_ref_baseCosto base para leer desde una referencia
read_ref_per_abs_val_unitCosto por unidad de valor absoluto de lectura
write_ref_baseCosto base para escribir a una referencia
freeze_refCongelar una referencia
ParámetroSignificado
cast_u8Cast a un u8
cast_u16Cast a un u16
cast_u32Cast a un u32
cast_u64Cast a un u64
cast_u128Cast a un u128
cast_u256Cast a un u256
ParámetroSignificado
addSumar
subRestar
mulMultiplicar
divDividir
modMódulo

El gas de almacenamiento es significativamente más complejo que el gas de instrucción y payload, con dos tipos principales:

Las tarifas de IO de almacenamiento están diseñadas para reflejar el costo computacional de procesar transacciones:

ParámetroSignificado
storage_io_per_state_slot_writecobrado por operación de escritura de estado en la salida de transacción
storage_io_per_state_byte_writecobrado por byte en todas las ops de escritura de estado en la salida de transacción
storage_io_per_event_byte_writecobrado por byte en todos los eventos en la salida de transacción
storage_io_per_transaction_byte_writecobrado por byte en la transacción misma que también es parte del historial del ledger

Los siguientes parámetros de tarifa de almacenamiento se aplican (es decir, se cobran en valores APT absolutos) para representar el espacio en disco y costos estructurales asociados con usar la estructura de datos autenticada de Aptos para almacenar elementos en la blockchain.

ParámetroSignificado
storage_fee_per_state_slot_createasignar un slot de estado, por move_to(), table::add(), etc
storage_fee_per_state_byteNota que esto se cobra cada vez que el slot crece en tamaño, no solo en tiempo de asignación. (Sin embargo, para simplicidad, el reembolso es solo en tiempo de eliminación. Ver AIP-65)

Las tarifas por byte se evalúan similarmente en vectores, que consumen _i=0n1e_i+b(n)\sum\_{i = 0}^{n - 1} e\_i + b(n) bytes, donde:

  • nn es el número de elementos en el vector
  • e_ie\_i es el tamaño del elemento ii
  • b(n)b(n) es un “tamaño base” que es una función de nn

Ver la [especificación de secuencia BCS] para más información sobre el tamaño base de vector (técnicamente un ULEB128), que típicamente ocupa solo un byte en la práctica, de manera que un vector de 100 elementos u8 cuenta por 100+1=101100 + 1 = 101 bytes. Por lo tanto, por la metodología de lectura por elemento descrita arriba, leer el último elemento de tal vector se trata como una lectura de 101 bytes.

El gas de payload está definido en transaction.rs, que incorpora gas de almacenamiento con varios parámetros asociados con payload y precios:

ParámetroSignificado
min_transaction_gas_unitsUnidades mínimas de gas interno para una transacción, cobradas al inicio de ejecución
large_transaction_cutoffTamaño, en bytes, arriba del cual las transacciones serán cobradas una cantidad adicional por byte
intrinsic_gas_per_byteUnidades de gas interno cobradas por byte para payloads arriba de large_transaction_cutoff
maximum_number_of_gas_unitsLímite superior en unidades de gas externo para una transacción
min_price_per_gas_unitPrecio mínimo de gas permitido para una transacción
max_price_per_gas_unitPrecio máximo de gas permitido para una transacción
max_transaction_size_in_bytesTamaño máximo de payload de transacción en bytes
gas_unit_scaling_factorFactor de conversión entre unidades de gas interno y externo

Aquí, “unidades de gas interno” se definen como constantes en archivos fuente como [instr.rs] y [storage_gas.move], que son más granulares que “unidades de gas externo” por un factor de gas_unit_scaling_factor: para convertir de unidades de gas interno a externo, dividir por gas_unit_scaling_factor. Luego, para convertir de unidades de gas externo a octas, multiplicar por el “precio de gas”, que denota el número de octas por unidad de gas externo.

Al momento de escribir esto, min_price_per_gas_unit en transaction.rs se define como [aptos_global_constants]::GAS_UNIT_PRICE (que está definido como 100), con otras constantes notables de transaction.rs como sigue:

ConstanteValor
min_price_per_gas_unit100
max_price_per_gas_unit10,000,000,000
gas_unit_scaling_factor1,000,000

Ver Gas de payload para el significado de estas constantes.

Cuando la carga de la red es baja, se espera que el precio de unidad de gas sea bajo, haciendo que la mayoría de aspectos del costo de transacción sean más asequibles. Sin embargo, la tarifa de almacenamiento es una excepción, ya que se cobra en términos de valor APT absoluto. En la mayoría de instancias, la tarifa de transacción es el componente predominante del costo total de transacción. Esto es especialmente cierto cuando una transacción asigna slots de estado, escribe a elementos de estado de tamaño considerable, emite numerosos o grandes eventos, o cuando la transacción misma es grande. Todos estos factores consumen espacio en disco en nodos Aptos y se cobran acordemente.

Por otro lado, el reembolso de almacenamiento incentiva liberar slots de estado eliminando elementos de estado. La tarifa de slot de estado se reembolsa completamente al desasignar el slot, mientras que la tarifa excesiva de byte de estado no es reembolsable. Esto cambiará pronto diferenciando entre bytes permanentes (aquellos en el estado global) y bytes efímeros relativos (aquellos que atraviesan el historial del ledger).

Algunas estrategias de optimización de costo concernientes a la tarifa de almacenamiento:

  1. Minimizar creación de elementos de estado.
  2. Minimizar emisiones de eventos.
  3. Evitar elementos de estado grandes, eventos y transacciones.
  4. Limpiar elementos de estado que ya no están en uso.
  5. Si dos campos se actualizan consistentemente juntos, agruparlos en el mismo resource o grupo de resource.
  6. Si un struct es grande y solo unos pocos campos se actualizan frecuentemente, mover esos campos a un resource separado o grupo de resource.

Al momento de escribir esto, todas las operaciones de gas de instrucción se multiplican por el EXECUTION_GAS_MULTIPLIER definido en [meter.rs], que está establecido en 20. Por lo tanto, las siguientes operaciones representativas asumen costos de gas como sigue (dividir gas interno por factor de escalamiento, luego multiplicar por precio mínimo de gas):

OperaciónOctas mínimas
Table add/borrow/remove box240
Llamada de función200
Cargar constante130
Tomar prestado globalmente100
Leer/escribir referencia40
Cargar u128 en pila16
Operación table box por byte2

(Nota que el gas de instrucción de operación table box por byte no cuenta para gas de almacenamiento, que se evalúa por separado).

Para comparación, leer un elemento de 100 bytes cuesta r_i+100\*r_b=3000+100\*3=3300r\_i + 100 \* r\_b = 3000 + 100 \* 3 = 3300 octas como mínimo, unas 16.5 veces tanto como una llamada de función, y en general, los costos de gas de instrucción son ampliamente dominados por costos de gas de almacenamiento.

Notablemente, sin embargo, todavía técnicamente hay un incentivo para reducir el número de llamadas de función en un programa, pero los esfuerzos de ingeniería son más efectivamente dedicados a escribir código modular y descompuesto que está orientado hacia reducir costos de gas de almacenamiento, en lugar de intentar escribir bloques de código repetitivos con menos funciones anidadas (en casi todos los casos).

En casos extremos es posible que el gas de instrucción supere con creces el gas de almacenamiento, por ejemplo si una función matemática de bucle toma 10,000 iteraciones para converger; pero nuevamente este es un caso extremo y para la mayoría de aplicaciones el gas de almacenamiento tiene un impacto mayor en el gas base que el gas de instrucción.

Al momento de escribir esto, transaction/mod.rs define la cantidad mínima de gas interno por transacción como 1,500,000 unidades internas (15,000 octas como mínimo), una cantidad que aumenta por 2,000 unidades de gas interno (20 octas mínimo) por byte para payloads mayores que 600 bytes, con el número máximo de bytes permitidos en una transacción establecido en 65536. Por lo tanto, en la práctica, el gas de payload es improbable que sea una preocupación.

// ✗ Ineficiente: múltiples resources pequeños
struct UserName has key { name: vector<u8> }
struct UserAge has key { age: u8 }
struct UserEmail has key { email: vector<u8> }
// ✓ Eficiente: un resource consolidado
struct UserProfile has key {
name: vector<u8>,
age: u8,
email: vector<u8>
}
// ✗ Ineficiente: eventos frecuentes y grandes
public fun process_items(items: vector<LargeItem>) {
let i = 0;
while (i < vector::length(&items)) {
let item = vector::borrow(&items, i);
// Emite evento por cada item (costoso)
emit_detailed_processing_event(item);
i = i + 1;
};
}
// ✓ Eficiente: evento sumario único
public fun process_items(items: vector<LargeItem>) {
let processed_count = 0;
let i = 0;
while (i < vector::length(&items)) {
// Procesar sin eventos individuales
process_item_internal(vector::borrow(&items, i));
processed_count = processed_count + 1;
i = i + 1;
};
// Un solo evento sumario
emit_batch_processing_event(processed_count);
}
// ✓ Eficiente: actualizar campos relacionados juntos
public fun update_user_info(
user: &signer,
new_name: vector<u8>,
new_email: vector<u8>
) acquires UserProfile {
let profile = borrow_global_mut<UserProfile>(signer::address_of(user));
// Una sola operación de almacenamiento para múltiples campos
profile.name = new_name;
profile.email = new_email;
}
public fun cleanup_expired_items(registry: &mut ItemRegistry, current_time: u64) {
let i = 0;
while (i < vector::length(&registry.items)) {
let item = vector::borrow(&registry.items, i);
if (item.expires_at < current_time) {
// Remover items expirados para liberar espacio (reembolso)
vector::remove(&mut registry.items, i);
} else {
i = i + 1;
}
};
}
import { AptosClient } from "aptos";
const client = new AptosClient("https://fullnode.mainnet.aptoslabs.com");
// Simular transacción para estimar gas
const simulation = await client.simulateTransaction(senderAccount, transaction, {
estimateGasUnitPrice: true,
estimateMaxGasAmount: true,
});
console.log(`Gas estimado: ${simulation[0].gas_used}`);
console.log(`Precio de gas: ${simulation[0].gas_unit_price}`);
#[test]
fun test_gas_usage() {
let scenario = test_scenario::begin(@admin);
// Medir gas antes
let gas_before = test_scenario::gas_used(&scenario);
// Ejecutar operación
expensive_operation(&mut scenario);
// Medir gas después
let gas_after = test_scenario::gas_used(&scenario);
let gas_consumed = gas_after - gas_before;
assert!(gas_consumed < MAX_ACCEPTABLE_GAS, 1);
test_scenario::end(scenario);
}
  • Storage Fee > Storage IO > Instruction Gas > Payload Gas
  • Enfócate primero en optimizar almacenamiento
  • Agrupa datos relacionados en un solo resource
  • Limpia estado innecesario regularmente
  • Usa eventos con moderación y eficiencia
  • Prefiere operaciones por lotes sobre individuales
  • Simula transacciones antes de enviar
  • Monitorea costos de gas en producción
  • Establece límites de gas apropiados
  • Optimiza basado en métricas reales
  • Diseña estructuras de datos planas cuando sea posible
  • Evita anidamiento profundo de recursos
  • Usa tablas para almacenamiento a gran escala
  • Implementa patrones de limpieza automática

La comprensión del modelo de gas de Aptos es esencial para desarrollar aplicaciones eficientes y rentables en la plataforma.