Saltar al contenido principal

Archivo estatal

Los datos del contrato están compuestos por tres tipos diferentes: Persistent, Temporary e Instance. En un contrato, se accede a estos mediante env.storage().persistent(), env.storage().temporary() y env.storage().instance() respectivamente; consulta la documentación de storage().

Aprende a elegir el almacenamiento adecuado para tu caso de uso en esta Guía Práctica y en otras guías relacionadas con el archivo estatal aquí.

Todos los datos del contrato tienen un Tiempo de Vida (TTL) que debe extenderse periódicamente. Si el TTL de una entrada no se extiende periódicamente, el TTL de la entrada finalmente llegará a 0 y se convertirá en "archivada" o será eliminada permanentemente, dependiendo del tipo de almacenamiento. Cada tipo de almacenamiento funciona de manera similar, pero tiene diferentes tarifas y comportamientos de archivo:

  • Cuando el TTL de una entrada Temporary es 0, se elimina del ledger y es permanentemente inaccesible.
  • Cuando el TTL de una entrada Persistent o Instance es 0, se vuelve inaccesible y se "archiva", pero se puede "restaurar" y usar nuevamente a través de la RestoreFootprintOp.

Descripciones de Tipos de Datos de Contrato

El uso general y la interfaz son idénticos para todos los tipos de almacenamiento. Solo difieren en tarifas y comportamientos de archivo de la siguiente manera:

Temporary

  • Las tarifas más económicas.
  • Eliminado permanentemente cuando el TTL llega a 0, no se puede restaurar.
  • Suitable for time-bounded data (i.e. price oracles, signatures, etc.) y datos fácilmente recreables.
  • Cantidad ilimitada de almacenamiento.

Instance

  • Las tarifas más caras (mismo precio que el almacenamiento Persistent).
  • Archivada cuando el TTL llega a 0, se puede restaurar usando la operación RestoreFootprintOp.
  • Comparte el mismo TTL que la instancia del contrato. Si la instancia del contrato no ha sido archivada, los datos de la instancia están garantizados como accesibles y no archivados.
  • Cantidad limitada de almacenamiento disponible.
  • Adecuado para estados de contrato "compartidos" que no pueden ser Temporary (es decir, cuentas de administración, metadatos del contrato, etc.).

Persistent

  • Las tarifas más caras (mismo precio que el almacenamiento Instance).
  • Archivada cuando el TTL llega a 0, se puede restaurar usando la operación RestoreFootprintOp.
  • No comparte el mismo TTL que la instancia del contrato. Si la instancia del contrato no está archivada, los datos Persistent pueden ser archivados y necesitar ser restaurados antes de invocar el contrato.
  • Cantidad ilimitada de almacenamiento.
  • Adecuado para datos de usuario que no pueden ser Temporary (es decir, saldos).

Prácticas recomendadas de datos de contrato

Como regla general, el almacenamiento Temporary solo debe usarse para datos que se pueden recrear fácilmente o que son válidos solo por un período de tiempo, mientras que el almacenamiento Persistent o Instance debe usarse para datos que no se pueden recrear y que deben mantenerse permanentemente, como el saldo de tokens de un usuario.

Cada tipo de almacenamiento tiene su propio espacio de claves. Para demostrar esto, consulta el fragmento de código a continuación:

const EXAMPLE_KEY: Symbol = symbol_short!("KEY");
env.storage().persistent().set(&EXAMPLE_KEY, 1);
env.storage().temporary().set(&EXAMPLE_KEY, 2);

env.storage().persistent().get(&EXAMPLE_KEY); // Returns Ok(1)
env.storage().temporary().get(&EXAMPLE_KEY); // Returns Ok(2)

Todo el almacenamiento Instance se guarda en una única instancia de contrato LedgerEntry y comparte un único TTL. Esto significa que una llamada a env.storage().instance().extend_ttl() extenderá el TTL de todas las entradas Instance, así como de la instancia del contrato en sí. También extenderá la entrada del código del contrato, de la cual hablaremos en la próxima sección. Para el almacenamiento Temporary y Persistent, cada entrada tiene su propio TTL y debe extenderse individualmente. La interfaz es un poco diferente y toma la clave de la entrada que se está extendiendo, así como el valor de extensión de TTL.

Una llamada a extend_ttl(N) asegura que el TTL actual de la entrada de la instancia del contrato sea al menos N ledgers. Por ejemplo, si se llama a extend_ttl(100) y la entrada de la instancia del contrato tiene un TTL actual de 50 ledgers, el TTL se extenderá hasta 100 ledgers. Si se llama a extend_ttl(100) y la entrada de la instancia del contrato tiene un TTL actual de 150 ledgers, el TTL no se extenderá y la llamada a extend_ttl() no tendrá efecto.

Además de las extensiones de TTL definidas por el contrato que utilizan la función extend_ttl(), el TTL de una entrada de dato del contrato se puede extender a través de la operación ExtendFootprintTTLOp.

Código de contrato y tiempos de vida del contrato

Las entradas de código de contrato y las instancias de contrato tienen tiempos de vida, y hay un par de formas de extenderlos. Métodos como env.storage().instance().extend_ttl() y env.deployer().extend_ttl() extienden tanto el código del contrato como la instancia del contrato. La verificación de umbral y las extensiones se realizan de forma independiente tanto para el código del contrato como para la instancia del contrato, por lo que es posible que una se extienda pero no la otra, dependiendo de cuáles sean los TTL actuales.

Si deseas extender el código del contrato o la instancia del contrato por separado, puedes usar env.deployer().extend_ttl_for_code() y env.deployer().extend_ttl_for_contract_instance() respectivamente. Llamar a ambos con los mismos parámetros es equivalente a llamar solo a env.deployer().extend_ttl().

Comportamiento de las transacciones que intentan acceder a una entrada Persistent archivada

Aquí hay algunos puntos importantes a tener en cuenta en relación a las entradas archivadas -

  1. Una transacción de Soroban que tiene una clave para una entrada Persistent archivada en el pie de página fallará inmediatamente durante la etapa de aplicación antes de la ejecución del contrato. No importa si el contrato mismo iba a acceder a la entrada.
  2. Debido al punto anterior de que las entradas archivadas nunca pueden entrar en la lógica del contrato inteligente, no hay razón para escribir código en tu contrato para manejar entradas archivadas. Lo mismo se aplica a los casos de prueba del contrato: aunque puedas querer escribir pruebas que verifiquen si tu lógica de extensión es correcta, no necesitas escribir pruebas de archivo porque la transacción fallará antes de llegar al contrato. Sin embargo, es posible acceder a una entrada archivada en un caso de prueba de Soroban, en cuyo caso el host se bloqueará. Puedes leer más sobre esto en Extensiones de TTL de Prueba.
  3. Las entradas persistentes archivadas nunca pueden ser recreadas. En su lugar, deben ser restauradas. Una vez restauradas, pueden ser modificadas o eliminadas.

Términos y Semántica

En Vivo Hasta Ledger

Cada entrada de ContractData y ContractCode tiene un campo liveUntilLedger almacenado en su LedgerEntry. La entrada ya no está en vivo (es decir, está archivada o eliminada, dependiendo del tipo de almacenamiento) cuando current_ledger > liveUntilLedger.

TTL

El Tiempo de Vida (TTL) de una entrada se define como cuántos ledgers quedan hasta que la entrada ya no esté activa. Por ejemplo, si el ledger actual es 5 y el ledger hasta el que vive una entrada es 15, entonces el TTL de la entrada es 10 ledgers.

TTL Mínimo

Para cada tipo de entrada, hay un TTL mínimo que la entrada tendrá al ser creada o restaurada. Este mínimo de TTL se aplica automáticamente a nivel de protocolo.

El TTL mínimo es un parámetro de red. Consulta el referencia de recursos para encontrar los valores actuales.

TTL Máximo

En cualquier ledger dado, el TTL de una entrada puede ser extendido hasta el TTL máximo. Este es un parámetro de red (consulta la tabla de límites de recursos para el TTL máximo actual). El TTL máximo no se aplica en función de cuándo se creó una entrada, sino en función del ledger actual. For example, if an entry is created on January 1st, 2024, its TTL could initially be extended up to January 1st, 2025. After this initial TTL extension, if the entry received another TTL extension later on January 10th, 2024, the TTL could be extended up to January 10th, 2025. La función max_ttl() se puede usar para determinar el TTL máximo permitido actual.

Operaciones

ExtendFootprintTTLOp

Semántica

XDR:

/*
Threshold: low
Result: ExtendFootprintTTLResult
*/
struct ExtendFootprintTTLOp
{
ExtensionPoint ext;
uint32 extendTo;
};

ExtendFootprintTTLOp es una operación de Soroban que extenderá el ledger hasta el que vive de las entradas especificadas en el conjunto de lectura única del pie de página. El conjunto de lectura y escritura debe estar vacío. La extensión asegurará que el TTL de las entradas será al menos extendTo ledgers desde ahora.

Veamos este ejemplo a continuación.

Ex. Last closed ledger (LCL) = 5, Current Ledger = 6, liveUntilLedger = 8

entry1.liveUntilLedger = 10
entry2.liveUntilLedger = 14
entry3.liveUntilLedger = 10000

entry1.liveUntilLedger will be updated to 14 so it will live for 8 more ledgers, including
the current ledger, and the entry can be accessed in ledgers [6, 13].

entry2 and entry3 will not be updated because they already have an
liveUntilLedger that is large enough.

Recursos de transacción

ExtendFootprintTTLOp es una operación de Soroban, y por lo tanto debe ser la única operación en una transacción. La transacción también necesita poblar la extensión de transacción SorobanTransactionData explicada aquí. Para completar SorobanResources, utiliza la simulación de la transacción mencionada en el enlace proporcionado, o asegúrate de que readBytes incluya la clave y el tamaño de cada entrada en el conjunto de readOnly.

RestoreFootprintOp

XDR:

/*
Threshold: low
Result: RestoreFootprintOp
*/
struct RestoreFootprintOp
{
ExtensionPoint ext;
};

RestoreFootprintOp es una operación de Soroban que restaurará las entradas archivadas especificadas en el conjunto de lectura y escritura del pie de página y las hará accesibles nuevamente. El conjunto de lectura única del pie de página debe estar vacío. Una entrada archivada es aquella cuya propiedad liveUntilLedger es menor que el número de ledger actual. Solo las entradas persistentes e instancias pueden ser restauradas.

La entrada restaurada tendrá su ledger hasta el que vive extendido al mínimo que la red permite para las entradas recién creadas, que es current_ledger_number + 4095 para entradas persistentes. El valor mínimo de TTL es un parámetro de configuración de red y está sujeto a actualizaciones (probablemente aumentos) a través de actualizaciones de la red.

Recursos de transacción

RestoreFootprintOp es una operación de Soroban, y por lo tanto debe ser la única operación en una transacción. La transacción también necesita poblar la extensión de transacción SorobanTransactionData explicada aquí. Para completar SorobanResources, utiliza la simulación de transacción mencionada en el enlace proporcionado, o asegúrate de que writeBytes incluya la clave y el tamaño de cada entrada en el conjunto de readWrite y de que extendedMetaDataSizeBytes sea al menos el doble de writeBytes.


Ejemplos

Hemos hecho nuestro mejor esfuerzo por construir herramientas alrededor del archivo estatal tanto en el servidor RPC de Stellar como en el SDK de JavaScript para facilitar el manejo de esto, y este conjunto de ejemplos demuestra cómo aprovecharlo.

Visión General

Tanto la restauración como la extensión del TTL de las entradas de ledger siguen un proceso de tres pasos, sin importar su naturaleza (datos de contrato, instancias, etc.):

  1. Identificar las entradas de ledger. Esto generalmente significa adquirirlas de un servidor RPC de Stellar como parte de tu simulación de transacción inicial (consulta la documentación sobre simulación de transacciones y el método RPC simulateTransaction).

  2. Preparar tu operación. Esto implica describir las entradas de ledger dentro de la operación correspondiente (es decir, ExtendFootprintTTLOp o RestoreFootprintOp) y su pie de ledger (el campo SorobanTransactionData), luego simularla para completar la información de tarifas y uso de recursos (al restaurar, generalmente ya tienes resultados de simulación).

  3. Enviar la transacción y comenzar de nuevo con lo que estabas tratando de hacer en primer lugar.

Cada uno de los ejemplos a continuación seguirá una estructura como esta. Trabajaremos a través de dos escenarios diferentes:

  1. una pieza de datos persistentes en mi contrato está archivada
  2. mi instancia de contrato o el WASM está archivado

Recuerda, sin embargo, que cualquier combinación de estos escenarios puede ocurrir en la realidad.

Preparación

Para ayudar con la estructura del código, reutilizaremos la función rudimentaria de sondeo de transacciones con reintentos submitTx que describimos en otra guía.

En el siguiente código, también aprovecharemos Server.prepareTransaction. Este es un método útil que, dado una transacción, la simulará, luego enmendará la transacción con los resultados de la simulación (tarifas, etc.) y devolvera eso. Luego, solo puede ser firmada y enviada. También utilizaremos SorobanDataBuilder, una abstracción conveniente que nos permite usar un patrón de constructor para establecer las huellas de almacenamiento apropiadas para una transacción.

Ejemplo: ¡Mis datos están archivados!

Comenzaremos con la ocurrencia más probable: mi pieza de datos persistentes está archivada porque no he interactuado con mi contrato en un tiempo. ¿Cómo puedo hacerlo accesible nuevamente?

En este ejemplo, asumiremos dos cosas: el contrato en sí sigue activo (es decir, otros han estado extendiendo su TTL mientras has estado ausente) y no sabes cómo se representa tu dato archivado en el ledger. Si lo supieras, podrías omitir los pasos a continuación donde lo averiguamos y simplemente establecer la huella de restauración directamente. El proceso implica tres pasos discretos:

  1. Simula nuestra transacción como normalmente lo harías.
  2. Si la simulación lo indica, realizamos la restauración a través de Operation.restoreFootprint usando sus pistas.
  3. Volvemos a intentar ejecutar nuestra transacción inicial.

Veamos eso en código:

import {
BASE_FEE,
Networks,
Keypair,
TransactionBuilder,
SorobanDataBuilder,
SorobanRpc,
xdr,
} from "@stellar/stellar-sdk"; // add'l imports to preamble
const { Api, assembleTransaction } = SorobanRpc;

// assume that `server` is the Server() instance from the preamble

async function submitOrRestoreAndRetry(
signer: Keypair,
tx: Transaction,
): Promise<Api.GetTransactionResponse> {
// We can't use `prepareTransaction` here because we want to do
// restoration if necessary, basically assembling the simulation ourselves.
const sim = await server.simulateTransaction(tx);

// Other failures are out of scope of this tutorial.
if (!Api.isSimulationSuccess(sim)) {
throw sim;
}

// If simulation didn't fail, we don't need to restore anything! Just send it.
if (!Api.isSimulationRestore(sim)) {
const prepTx = assembleTransaction(tx, sim);
prepTx.sign(signer);
return submitTx(prepTx);
}

//
// Build the restoration operation using the RPC server's hints.
//
const account = await server.getAccount(signer.publicKey());
let fee = parseInt(BASE_FEE);
fee += parseInt(sim.restorePreamble.minResourceFee);

const restoreTx = new TransactionBuilder(account, { fee: fee.toString() })
.setNetworkPassphrase(Networks.TESTNET)
.setSorobanData(sim.restorePreamble.transactionData.build())
.addOperation(Operation.restoreFootprint({}))
.build();

restoreTx.sign(signer);

const resp = await submitTx(restoreTx);
if (resp.status !== Api.GetTransactionStatus.SUCCESS) {
throw resp;
}

//
// now that we've restored the necessary data, we can retry our tx using
// the initial data from the simulation (which, hopefully, is still
// up-to-date)
//
const retryTxBuilder = TransactionBuilder.cloneFrom(tx, {
fee: (parseInt(tx.fee) + parseInt(sim.minResourceFee)).toString(),
sorobanData: sim.transactionData.build(),
});
// because we consumed a sequence number when restoring, we need to make sure
// we set the correct value on this copy
retryTxBuilder.source.incrementSequenceNumber();

const retryTx = retryTxBuilder.build();
retryTx.sign(signer);

return submitTx(retryTx);
}

Ten en cuenta que cuando se requiere restauración, la simulación sigue teniendo éxito. La forma en que sabemos que algo necesita ser restaurado es la presencia de una estructura restorePreamble en la respuesta del RPC. Esto contiene tanto la huella como la tarifa necesarias para la restauración, mientras que el resto de la respuesta contiene la simulación de invocación como si se hubiera realizado primero esa restauración.

¡Esto es genial, ya que significa menos viajes para comenzar de nuevo!

Ejemplo: ¡Mi contrato está archivado!

Como puedes imaginar, si tu instancia de contrato desplegada o el código que la respalda están archivados, no se podrá cargar para ejecutar tus invocaciones. Recuerda que hay una relación uno a muchos distinta en la cadena entre el código de un contrato y las instancias desplegadas de ese contrato:

Necesitamos ambos para que nuestras llamadas al contrato funcionen.

Veamos cómo se pueden recuperar. El proceso de recuperación es un poco diferente: mientras que no necesitamos simulación para averiguar las huellas, sí necesitamos realizar una búsqueda adicional de entradas de ledger. Podemos aprovechar Contract.getFootprint() para obtener la clave del ledger utilizada por una instancia de contrato dada, pero eso no nos proporcionará su código WASM de respaldo. Para eso, recrearemos este ejemplo aquí.

También necesitamos simulación para averiguar las tarifas para nuestra restauración. Sin embargo, esto puede ser fácilmente cubierto por el helper Server.prepareTransaction del SDK, que realizará simulación y ensamblaje para nosotros:

import {
BASE_FEE,
Contract,
Keypair,
Networks,
TransactionBuilder,
SorobanDataBuilder,
Operation,
SorobanRpc,
} from "@stellar/stellar-sdk";

async function restoreContract(
signer: Keypair,
c: Contract,
): Promise<SorobanRpc.Api.GetTransactionResponse> {
const instance = c.getFootprint();

const account = await server.getAccount(signer.publicKey());
const wasmEntry = await server.getLedgerEntries(
getWasmLedgerKey(instance)
);

const restoreTx = new TransactionBuilder(account, { fee: BASE_FEE })
.setNetworkPassphrase(Networks.TESTNET)
.setSorobanData(
// Set the restoration footprint (remember, it should be in the
// read-write part!)
new SorobanDataBuilder().setReadWrite([
instance,
wasmEntry
]).build(),
)
.addOperation(Operation.restoreFootprint({}))
.build();

const preppedTx = await server.prepareTransaction(restoreTx);
preppedTx.sign(signer);
return submitTx(preppedTx);
}

function getWasmLedgerKey(entry: xdr.ContractDataEntry): {
return xdr.LedgerKey.contractCode(
new xdr.LedgerKeyContractCode({
hash: entry.val().instance().wasmHash()
})
);
}

Lo bueno de este enfoque es que restaurará tanto la instancia como el código WASM de respaldo si es necesario, omitiendo cualquiera si ya están en el estado del ledger.