Simulación de Transacciones
Huella
Como se menciona en la sección de persistir datos, un contrato solo puede cargar o almacenar entradas CONTRACT_DATA
que se declaran en una huella asociada con su invocación.
Una huella es un conjunto de claves de ledger, cada una marcada como de solo lectura o de lectura y escritura. Las claves de solo lectura están disponibles para la transacción para leer; las claves de lectura y escritura están disponibles para leer, escribir o ambos.
Cualquier transacción de Soroban enviada por un usuario debe ir acompañada de esta huella. Una única huella abarca todos los datos leídos y escritos por todos los contratos invocados transitivamente por la transacción: no solo el contrato inicial que llama la transacción, sino también todos los contratos que llama, y así sucesivamente.
Dado que puede ser difícil para un usuario saber qué entradas de ledger intentará leer o escribir una determinada llamada de contrato (especialmente las entradas que son causadas por otros contratos dentro de una transacción), el host proporciona un mecanismo auxiliar simulateTransaction
que ejecuta una transacción contra un snapshot temporal, posiblemente desactualizado, del ledger. El mecanismo simulateTransaction
no está limitado a solo leer o escribir el contenido de una huella; más bien registra una huella que describe la ejecución de la transacción, descarta los efectos de la ejecución y luego devuelve la huella grabada a su llamador.
Esta huella proporcionada por la simulación puede luego ser utilizada para acompañar una presentación "real" de la misma transacción a la red para una ejecución real. Si el estado del ledger ha cambiado demasiado entre el momento de la simulación y la presentación real, la huella puede estar demasiado desactualizada y ya no identificar con precisión las claves que la transacción necesita leer y/o escribir, en cuyo caso la simulación debe ser reintentada para actualizar la huella.
En cualquier caso (ya sea exitoso o fallido), la transacción real se ejecutará atómicamente, de manera determinista y con semántica de consistencia serializable. Una huella inexacta simplemente provoca un fallo determinista de la transacción, no un error de lectura desactualizada. Todos los efectos de una transacción fallida son descartados, como sucedería ante cualquier otro error.
Autorización
Consulta el resumen de autenticación y la sección de autorización de transacciones para obtener información general sobre la autorización de Soroban: esta sección se refiere específicamente a cómo funciona la simulación junto con los requisitos de autorización.
El mecanismo de simulación de transacciones de Soroban se puede utilizar para precomputar los árboles de SorobanAuthorizedInvocation
que deben ser autorizados por las Address
es para que todas las verificaciones de require_auth
pasen. Se puede invocar de dos maneras diferentes:
Modo de grabación
El entorno de host Soroban proporciona un modo de simulación que graba todo el contexto (dirección, ID de contrato, función, argumentos, etc.) involucrado en llamadas a require_auth
.
Estos registros se añaden a un árbol SorobanAuthorizedInvocation
y se marcan como exitosos. Luego, después de que la invocación haya terminado, la simulación de transacciones devuelve todos los árboles grabados, así como valores nonce generados aleatoriamente para las firmas esperadas.
Dada esta información de la simulación, el cliente solo necesita proporcionar estos árboles y nonces a las Address
es involucradas en la invocación para su firma, y luego construir la transacción final combinando la salida de la simulación con las firmas correspondientes.
Ten en cuenta que el modo de autenticación de "grabación" nunca emula fallas de autorización. Esto se debe a que la autorización fallida siempre es una situación "excepcional" (es decir, las direcciones Address
para las cuales no anticipas una autorización exitosa no deberían ser utilizadas en primer lugar). Es similar a cómo, por ejemplo, el mecanismo simulateTransaction
no emula fallas causadas por la huella incorrecta.
Si deseas validar firmas, debes usar simulateTransaction
en el modo de "aplicación", que verificará las firmas antes de ejecutar la transacción en on-chain.
Modo de aplicación
El modo de autenticación de grabación es una opción para simulateTransaction
. Sin embargo, al tratar con los contratos de cuentas personalizadas, por ejemplo, puede ser necesario simular el código __check_auth
de la cuenta personalizada (que está omitted en el modo de autenticación de grabación), para obtener su huella en el ledger.
Esto se llama ejecutar simulaciones con el modo de autenticación "restrictivo". Esto es básicamente equivalente a ejecutar la transacción en on-chain (con un estado del ledger posiblemente ligeramente obsoleto); por lo tanto, requiere que todas las firmas sean válidas.
Desde la perspectiva de un desarrollador, la diferencia entre estos es si las entradas de autorización están presentes en la operación InvokeHostFunction
enviada a simulateTransaction
. Los ejemplos a continuación destacan esta distinción en detalle, pero la historia corta es que pasar auth
a Operation.invokeContractFunction
(que es un envoltorio de conveniencia de invokeHostFunction
) implicará el modo de aplicación.
Uso del SDK
A continuación, demostraremos las diversas maneras en que puedes invocar la simulación de transacciones, así como resaltar algunas utilidades disponibles en el SDK de TypeScript para la autorización.
Cubriré tres tipos de invocaciones:
- Una invocación simple en la que la cuenta de origen de la transacción es la única firmante del árbol de invocación.
- Una invocación en la que dos cuentas necesitan firmar el árbol de invocación.
- Una invocación ejecutada en modo de aplicación para confirmar que las firmas son correctas.
Ejemplo 1: autorización de la cuenta de origen.
En esta variante, aprovecharemos la variante de "autorización de la cuenta de origen": esto es cuando la cuenta de origen en la transacción es la única que necesita firmar para la invocación (ver la variante de "cuenta de origen" de SorobanCredentials
). En este escenario, la firma en la transacción misma implica directamente la firma de la invocación.
- JavaScript
import {
Asset,
Keypair,
Networks,
Operation,
authorizeEntry,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import { Server, assembleTransaction } from "@stellar/stellar-sdk/rpc";
const s = Server("https://soroban-testnet.stellar.org");
// Pretend is is a real, funded account.
const signer = Keypair.random();
const xlmContract = Asset.native().contractId(Networks.TESTNET);
async function main() {
const tx = new TransactionBuilder(await s.loadAccount(signer.publicKey()), {
networkPassphrase: Networks.TESTNET,
fee: BASE_FEE,
})
.addOperation(
Operation.invokeContractFunction(
xlmContract,
[
["balance", "symbol"],
[signer.publicKey(), "address"],
].map((val, type) => nativeToScVal(val, { type })),
),
)
.build();
const preppedTx = s.prepareTransaction(tx);
preppedTx.sign(signer);
const sendTx = await s.sendTransaction(preppedTx);
return s.pollTransaction(sendTx.hash);
}
main().catch((e) => console.error(e));
Ten en cuenta que, a diferencia del siguiente ejemplo, no necesitamos hacer la simulación por separado. Esto es porque podemos firmar la transacción tal como está, en lugar de necesitar inspeccionar sus entradas de autorización.
Ejemplo 2: autenticación multipartita.
En esta variante, ampliaremos las firmas requeridas a más de una parte, por lo que la cuenta fuente ya no es suficiente. Aprovecharemos el helper authorizeEntry
, que está diseñado específicamente para facilitar la firma de las entradas devueltas por la simulación de transacciones.
- TypeScript
import {
Asset,
Keypair,
Networks,
Operation,
authorizeEntry,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import { Server, assembleTransaction } from "@stellar/stellar-sdk/rpc";
const s = Server("https://soroban-testnet.stellar.org");
// Pretend these are real, funded accounts.
const signers = [Keypair.random(), Keypair.random()];
const xlmContract = Asset.native().contractId(Networks.TESTNET);
async function main() {
// Notice that the source account is the first keypair, but the transfer
// occurs *from* the second keypair, which means the second keypair will
// need to sign for an authorization entry to approve the transfer.
const tx = new TransactionBuilder(
await s.loadAccount(signers[0].publicKey()),
{
networkPassphrase: Networks.TESTNET,
fee: BASE_FEE,
},
)
.addOperation(
Operation.invokeContractFunction(
xlmContract,
[
["transfer", "symbol"],
[signers[1].publicKey(), "address"], // from
[signers[0].publicKey(), "address"], // to
[1000, "i128"], // amount
].map((val, type) => nativeToScVal(val, { type })),
),
)
.build();
const simResult = s.simulateTransaction(tx);
// For every auth entry that needs signing, sign it with the correct keypair.
//
// Inject the auths back into the simulation result so they
// get assembled into our transaction.
simResult.result.auth = simResult.result.auth.map((entry) =>
authorizeEntry(
entry,
// Ignore source account entries, which is handled as a no-op.
entry.credentials().switch() !==
xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount()
? signers.find(
// Find the keypair that matches the entry's address.
(signer) =>
Address.fromScAddress(
entry.credentials().address().address(),
).toString() === signer.publicKey(),
)
: null,
response.latestLedger + 12, // signature is valid for ~1m
Networks.TESTNET,
),
);
const preppedTx = assembleTransaction(tx, simResult);
preppedTx.sign(signers[0]);
const sendTx = await s.sendTransaction(preppedTx);
return s.pollTransaction(sendTx.hash);
}
main().catch((e) => console.error(e));
Alternativamente, podríamos bajar un paso más en el stack y construir las entradas de autorización nosotros mismos usando authorizeInvocation
, dándonos control total sobre la "pila de llamadas" que se está invocando. Esto puede ser útil si deseas autorizar invocaciones específicas, construir las invocaciones tú mismo o reducir el ancho de banda de red que usas al compartir entradas para que otras partes firmen.
Ejemplo 3: modo de aplicación.
En este ejemplo, aprovecharemos el modo de aplicación de la autenticación de la simulación de transacciones, que, cuando se le dan entradas de autorización firmadas, asegurará que son las firmas necesarias y suficientes para la ejecución de la transacción.
Para mantener las cosas realmente simples, no haremos mucho código. En su lugar, simplemente mostraremos la diferencia con el ejemplo anterior: todo lo que necesitamos hacer es ejecutar la simulación una vez más.
- preppedTx.sign(signers[0]);
-
- const sendTx = await s.sendTransaction(preppedTx);
+ const resimTx = await s.prepareTransaction(preppedTx);
+ resimTx.sign(signers[0]);
+ const sendTx = await s.sendTransaction(resimTx);