Hoy aprenderemos a crear un programa en Solana que logre las mismas cosas que el contrato de Solidity a continuación. También aprenderemos cómo Solana maneja problemas aritméticos como el desbordamiento (overflow).
contract Day2 {
event Result(uint256);
event Who(string, address);
function doSomeMath(uint256 a, uint256 b) public {
uint256 result = a + b;
emit Result(result);
}
function sayHelloToMe() public {
emit Who("Hello World", msg.sender);
}
}
Comencemos un nuevo proyecto
anchor init day2
cd day2
anchor build
anchor keys sync
Asegúrate de tener el validador de pruebas de Solana ejecutándose en una terminal:
solana-test-validator
Y los registros (logs) de Solana en otra:
solana logs
Asegúrate de que el programa recién generado funcione correctamente ejecutando las pruebas
anchor test --skip-local-validator
Proporcionando Argumentos a la Función
Antes de hacer cálculos matemáticos, cambiemos la función initialize para que reciba dos números enteros. Ethereum utiliza uint256 como el tamaño “estándar” de número entero. En Solana, es u64 — esto es equivalente a uint64 en Solidity.
Pasando números enteros sin signo
La función initialize por defecto se verá como la siguiente:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
Modifica la función initialize() en lib.rs para que quede de la siguiente manera.
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64) -> Result<()> {
msg!("You sent {} and {}", a, b);
Ok(())
}
Ahora necesitamos cambiar la prueba en ./tests/day2.ts.
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods
.initialize(new anchor.BN(777), new anchor.BN(888)).rpc();
console.log("Your transaction signature", tx);
});
Ahora vuelve a ejecutar anchor test --skip-local-validator.
Cuando miremos en los registros, deberíamos ver algo como lo siguiente:
Transaction executed in slot 367357:
Signature: 54iJFbtEE61T9X2WCLbMe8Dq2YYBzCLYE4qW2DqTsA4gZRgootcubLgHc1MHYncbP63sxNxEY8tJfgfgsdt1Ch4g
Status: Ok
Log Messages:
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
Program log: Instruction: Initialize
Program log: You sent 777 and 888
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY consumed 1116 of 200000 compute units
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY success
Pasando una cadena de texto (string)
Ahora vamos a ilustrar cómo pasar un string como argumento.
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64,
message: String) -> Result<()> {
msg!("You said {:?}", message);
msg!("You sent {} and {}", a, b);
Ok(())
}
Y cambiamos la prueba.
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods
.initialize(
new anchor.BN(777), new anchor.BN(888), "hello").rpc();
console.log("Your transaction signature", tx);
});
Cuando ejecutamos la prueba, vemos el nuevo registro.
Arreglo de números
A continuación agregamos una función (y una prueba) para ilustrar cómo pasar un arreglo de números. En Rust, un “vector”, o Vec, es lo que Solidity llama un “array” (arreglo).
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64,
message: String) -> Result<()> {
msg!("You said {:?}", message);
msg!("You sent {} and {}", a, b);
Ok(())
}
// added this function
pub fn array(ctx: Context<Initialize>,
arr: Vec<u64>) -> Result<()> {
msg!("Your array {:?}", arr);
Ok(())
}
Y actualizamos la prueba unitaria de la siguiente manera
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize(new anchor.BN(777), new anchor.BN(888), "hello").rpc();
console.log("Your transaction signature", tx);
});
// added this test
it("Array test", async () => {
const tx = await program.methods.array([new anchor.BN(777), new anchor.BN(888)]).rpc();
console.log("Your transaction signature", tx);
});
Y ejecutamos las pruebas nuevamente y revisamos los registros para ver la salida del arreglo:
Transaction executed in slot 368489:
Signature: 3TBzE3NddEY8KREv1FSXnieoyT6G6iNxF1n4hJHCeeWhAsUward3MEKm9WJHV4PMjPxeN2jRSRC9Rq8FUKjXoBQR
Status: Ok
Log Messages:
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
Program log: Instruction: Initialize
Program log: You said [777, 888]
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY consumed 1587 of 200000 compute units
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY success
Consejo: Si te atascas con un problema en tus pruebas de Anchor, intenta buscar en Google “Solana web3 js” en relación a tu error. La biblioteca de Typescript utilizada por Anchor es la biblioteca Solana Web3 JS.
Matemáticas en Solana
Matemáticas de coma flotante
Solana tiene algo de soporte nativo, aunque limitado, para operaciones de coma flotante.
Sin embargo, es mejor evitar las operaciones de coma flotante debido a lo computacionalmente intensivas que son (veremos un ejemplo de esto más adelante). Ten en cuenta que Solidity no tiene soporte nativo para operaciones de coma flotante.
Lee más sobre las limitaciones del uso de números de coma flotante aquí.
Desbordamiento Aritmético (Overflow)
El desbordamiento aritmético era un vector de ataque común en Solidity hasta que la versión 0.8.0 integró protección contra desbordamientos en el lenguaje por defecto. En Solidity 0.8.0 o superior, las comprobaciones de desbordamiento se realizan por defecto. Dado que estas comprobaciones consumen gas, a veces los desarrolladores las desactivan estratégicamente usando el bloque “unchecked”.
¿Cómo se defiende Solana contra el desbordamiento aritmético?
Método 1: overflow-checks = true en Cargo.toml
Si la clave overflow-checks se establece en true en el archivo Cargo.toml, entonces Rust añadirá comprobaciones de desbordamiento a nivel del compilador. Mira la captura de pantalla de Cargo.toml a continuación:

Si el archivo Cargo.toml está configurado de esta manera, no tienes que preocuparte por los desbordamientos.
Sin embargo, añadir comprobaciones de desbordamiento aumenta el costo de cómputo de la transacción (revisaremos esto en breve). Por lo tanto, bajo algunas circunstancias donde el costo de cómputo es un problema, podrías desear establecer overflow-checks en false. Para verificar estratégicamente los desbordamientos, puedes usar los operadores checked_* en Rust.
Método 2: usando los operadores checked_*.
Veamos cómo se aplican las comprobaciones de desbordamiento a las operaciones aritméticas dentro de Rust. Considera el siguiente fragmento de código Rust.
- En la línea 1, hacemos aritmética usando el operador habitual
+, el cual se desborda silenciosamente. - En la línea 2, usamos
.checked_add, el cual arrojará un error si ocurre un desbordamiento. Ten en cuenta que tenemos.checked_*disponible para otras operaciones, comochecked_subychecked_mul.
let x: u64 = y + z; // will silently overflow
let xSafe: u64 = y.checked_add(z).unwrap(); // will panic if overflow happens
// checked_sub, checked_mul, etc are also available
Ejercicio 1: Establece overflow-checks = true y crea un caso de prueba donde provoques un subdesbordamiento (underflow) en un u64 haciendo 0 - 1. Necesitarás pasar esos números como argumentos o el código no compilará. ¿Qué sucede?
Verás que la transacción falla (con un mensaje de error bastante críptico que se muestra a continuación) cuando se ejecuta la prueba. Eso se debe a que Anchor activó la protección contra desbordamiento:

Ejercicio 2: Ahora cambia overflow-checks a false, y luego ejecuta la prueba de nuevo. Deberías ver un valor de subdesbordamiento de 18446744073709551615.
Ejercicio 3: Con la protección contra desbordamiento desactivada en Cargo.toml, haz let result = a.checked_sub(b).unwrap(); con a = 0 y b = 1. ¿Qué sucede?
¿Deberías simplemente dejar overflow-checks = true en el archivo Cargo.toml para tu proyecto Anchor? Generalmente, sí. Pero si estás haciendo cálculos intensivos, podrías querer establecer overflow-checks a false y defenderte estratégicamente contra los desbordamientos en momentos clave para ahorrar costos de cómputo, lo cual demostraremos a continuación.
Introducción a las unidades de cómputo de Solana (Solana compute units 101)
En Ethereum, una transacción se ejecuta hasta que consume el “límite de gas” (gas limit) especificado por la transacción. Solana llama al “gas” una “unidad de cómputo” (compute unit). Por defecto, una transacción está limitada a 200,000 unidades de cómputo. Si se consumen más de 200,000 unidades de cómputo, la transacción se revierte.
Determinando los costos de cómputo de una transacción en Solana
Es cierto que Solana es barato de usar en comparación con Ethereum, pero eso no significa que tus habilidades de optimizoooor en el desarrollo de Ethereum sean inútiles. Midamos cuántas unidades de cómputo requieren nuestras funciones matemáticas.
La terminal de registros de Solana también muestra cuántas unidades de cómputo se utilizaron. A continuación, hemos proporcionado comparativas (benchmarks) para la resta comprobada (checked) y no comprobada (unchecked).
Con la protección contra desbordamiento desactivada, consume 824 unidades de cómputo:

Con la protección contra desbordamiento activada consume 872 unidades de cómputo:

Como puedes ver, realizar una simple operación matemática consume casi 1000 unidades. Dado que tenemos 200k unidades, solo podemos realizar unos pocos cientos de operaciones aritméticas simples dentro del límite de gas por transacción. Por lo tanto, aunque las transacciones en Solana son generalmente más baratas que en Ethereum, todavía estamos limitados por un tope de unidades de cómputo relativamente bajo y no podríamos llevar a cabo tareas computacionalmente intensivas como simulaciones de dinámica de fluidos en la cadena de Solana.
Retomaremos el costo de las transacciones más adelante.
Las potencias no usan la misma sintaxis que en Solidity
En Solidity, si queremos elevar x a la potencia de y, hacemos lo siguiente:
uint256 result = x ** y;
Rust no usa esta sintaxis. En su lugar, usa .pow
let x: u64 = 2; // it is important that the base's data type is explicit
let y = 3; // the exponent data type can be inferred
let result = x.pow(y);
También existe .checked_pow si te preocupa el desbordamiento.
Coma flotante
Una de las cosas buenas de usar Rust para contratos inteligentes es que no tenemos que importar bibliotecas como Solmate o Solady para hacer cálculos matemáticos. Rust es un lenguaje bastante sofisticado con muchas operaciones integradas, y si necesitamos alguna pieza de código, podemos buscar fuera del ecosistema de Solana un crate (así es como se llaman las bibliotecas en Rust) que haga el trabajo.
Calculemos la raíz cúbica de 50. La función de raíz cúbica para números de coma flotante está integrada en el lenguaje Rust con la función cbrt().
// note that we changed `a` to f32 (float 32)
// because `cbrt()` is not available for u64
pub fn initialize(ctx: Context<Initialize>, a: f32) -> Result<()> {
msg!("You said {:?}", a.cbrt());
Ok(());
}
¿Recuerdas que en una sección anterior mencionamos que los números de coma flotante pueden ser computacionalmente costosos? Bueno, aquí vemos que nuestra operación de raíz cúbica consumió más de 5 veces lo que una simple operación aritmética con enteros sin signo:
Transaction executed in slot unspecified:
Signature: VfvySG5vvVSAnsYLCsvB9N6PsuGwL39kKd1fMsyvuB7y5DUHURwQVHU9rv3Xkz5NJqGHLSXoWoW92zJb5VKYCEF
Status: Ok
Log Messages:
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
Program log: Instruction: Initialize
Program log: attempting to begin the function with 50
Program log: Result = 3.6840315
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY consumed 4860 of 200000 compute units
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY success
Ejercicio 4: Construye una calculadora que haga las operaciones de +, -, x, y ÷. y también sqrt y log10.
Publicado originalmente el 09 de febrero de 2024