Comenzar con Rust y Solidity
En este tutorial, exploraremos Rust y Solidity, dos poderosos lenguajes de programación. Rust, un lenguaje de programación de sistemas, es conocido por sus características de seguridad, concurrencia y rendimiento, que pueden ser ventajosas al crear contratos inteligentes. Por otro lado, Solidity es un lenguaje de alto nivel específicamente diseñado para crear contratos inteligentes en la Máquina Virtual de Ethereum. Esta sección tiene como objetivo proporcionar una visión general de alto nivel de las similitudes y diferencias entre los dos lenguajes.
Tabla de Contenidos
- Sintaxis de Solidity, Tipos de Datos y Estructuras Básicas
- Sintaxis de Rust, Tipos de Datos y Modelo de Propiedad
- Escribir e Interactuar con Contratos Inteligentes Simples
Sintaxis de Solidity
Solidity es un lenguaje de programación diseñado específicamente para crear contratos inteligentes en la Máquina Virtual de Ethereum (EVM). Tiene una sintaxis similar a JavaScript y admite una variedad de tipos de datos y estructuras.
pragma solidity ^0.8.0;
contract HelloWorld {
function sayHello() public pure returns (string memory) {
return "Hello, World!";
}
}
Tipos de Datos
Solidity admite varios tipos de datos, tales como:
- Booleano:
bool
- Entero:
int
(con signo) yuint
(sin signo) - Dirección:
address
- Cadena:
string
- Bytes:
bytes
(de tamaño dinámico) ybytes32
(de tamaño fijo) - Matrices: de tamaño dinámico o de tamaño fijo y pueden ser declaradas con varios tipos.
- Estructuras:
struct
- Enumeraciones:
enum
- Mapeo:
mapping
Aquí hay ejemplos de implementaciones para cada tipo de dato:
pragma solidity ^0.8.0;
contract DataTypesExample {
// Boolean
bool public isCompleted = false;
// Integer (signed and unsigned)
int256 public signedInteger = -10;
uint256 public unsignedInteger = 10;
// Address
address public userAddress = 0x742d35Cc6634C0532925a3b844Bc454e4438f44e;
// String
string public greeting = "Hello, World!";
// Bytes (dynamic-size and fixed-size)
bytes public dynamicBytes = "hello, solidity";
bytes32 public fixedBytes = "hello, solidity";
// Arrays (dynamic-size and fixed-size)
uint[] public dynamicArray = [1, 2, 3];
uint[5] public fixedArray = [1, 2, 3, 4, 5];
address[] public dynamicAddressArray = [0xd41d1744871f42Bb724D777A2d0Bf53FB43a0040, 0x1f514ae9834aEAF6c2c3eb6D20E27e865F419010];
address[3] public fixedAddressArray = [0xC90cd0D820D6dc447B3cD9545185B046873786A6, 0x401997E856CE51e0D4A8f26ce64952313BEA0E25, 0x221d3b9821f3Cc49B42E7dd487E2a6d1b3ed0E05];
bool[] public dynamicBoolArray = [true, false, true];
bool[2] public fixedBoolArray = [true, false];
// Struct
struct Person {
string name;
uint age;
}
Person public person = Person("Alice", 30);
// Enums
enum Status { Open, Closed, Pending }
Status public currentStatus = Status.Open;
Status public nextStatus = Status.Closed;
Status public previousStatus = Status.Pending;
// Mapping
mapping(address => uint) public balances;
constructor() {
balances[msg.sender] = 100;
}
}
Estructuras Básicas
Algunas de las estructuras básicas en Solidity incluyen:
Variables
: Declaradas con un tipo de dato y un identificador.Funciones
: Definidas con la palabra clavefunction
.Modificadores
: Utilizados para modificar el comportamiento de las funciones.Eventos
: Utilizados para registrar cambios en el estado del contrato.Herencia
: Solidity admite herencia simple y múltiple.
Exploraremos algunas de estas estructuras con más detalles en el próximo artículo, Conceptos Avanzados de Solidity.
Sintaxis de Rust
Rust es un lenguaje de programación que es muy adecuado para construir contratos inteligentes debido a su énfasis en la seguridad, la concurrencia y el rendimiento. Impone estrictas reglas de propiedad y préstamo para prevenir condiciones de carrera y otros errores comunes.
fn main() {
println!("Hello, world!");
}
Tipos de Datos
El Soroban Rust SDK admite una variedad de Tipos Incorporados que consisten en tipos Primitivos y Tipos Personalizados, tales como:
Tipos de Datos Primitivos
-
Enteros de 32 bits: con signo (
i32
) y sin signo (u32
) -
Enteros de 64 bits: con signo (
i64
) y sin signo (u64
) -
Enteros de 128 bits: con signo (
i128
) y sin signo (u128
) -
Bool (
bool
) -
Bytes, Cadenas (
Bytes
,BytesN
): arreglos de bytes y cadenas que pueden ser pasados a contratos y almacenados -
Vec (
Vec
): tipo de colección secuencial y indexable que puede crecer -
Mapa (
Map
): diccionario clave-valor ordenado -
Dirección (
Address
): identificador opaco universal utilizado en contratos -
Cadena (
String
): un tipo de arreglo que crece de forma contigua y contiene u8s y requiere que se pase un entorno -
Símbolo:
-
(
Symbol::new
): cadenas pequeñas y eficientes de hasta 32 caracteres de longitud y requieren que se pase un entorno -
(
symbol_short!
) cadenas pequeñas y eficientes de hasta 9 caracteres de longitud
Ambos están limitados a los caracteres
a-zA-Z0-9_
y están codificados en enteros de 64 bits. -
Tipos de Datos Personalizados
Estructuras
(con Campos Nombrados): Un tipo personalizado que consiste en campos nombrados almacenados en el ledger como unmap
de pares clave-valor.Estructuras
(con Campos Sin Nombre): Un tipo personalizado que consiste en campos sin nombre almacenados en el ledger como un vector de valores.Enumeración
(Variantes Unitarias y de Tupla): Un tipo personalizado que consiste en variantes unitarias y de tupla almacenadas en el ledger como un vector de dos elementos, donde el primer elemento es el nombre de la variante y el segundo es el valor.Enumeración
(Variantes Enteras): Un tipo personalizado que consiste en variantes enteras almacenadas en el ledger como el valoru32
.
Los siguientes son ejemplos de implementaciones para cada tipo de dato:
// Integer (signed and unsigned)
let unsigned_32_bit: u32 = 42;
let signed_32_bit: i32 = -42;
let unsigned_64_bit: u64 = 42;
let signed_64_bit: i64 = -42;
let unsigned_128_bit: u128 = 42;
let signed_128_bit: i128 = -42;
// Boolean
let boolean: bool = true;
// String
let msg: &str = "Hello";
String::from_slice(&env, msg)
// Symbols (short and new)
let symbol_short = symbol_short!("Sample"); // up to 9 chars
// env is &Env
let symbol_new = Symbol::new(env, "SampleSymbolExpression");
// Bytes (Bytes and BytesN)
let bytes = Bytes::from_slice(&env, &[1; 32]);
let bytes_n = BytesN::from_array(&env, &[0; 32]);
// Vec
let vec = vec![&env, 0, 1, 2, 3];
// Map
let map = map![&env, (2, 20), (1, 10)];
// Address
let address = Address::new([0u8; 32]);
// Struct (named fields)
pub struct State {
pub count: u32,
pub last_incr: u32,
}
struct Tuple(u32, String);
// Struct (unnamed fields)
pub struct State(pub u32, pub u32);
// Enum (unit and tuple variants)
pub enum Enum {
A,
B(u32),
}
// Enum (integer variants)
pub enum Enum {
A = 0,
B = 1,
}
Una Breve Introducción a Módulos, Macros, Estructuras, Traits y Macros de Atributo
En esta sección, proporcionaremos una introducción concisa a algunos conceptos fundamentales en Rust: Módulos
, Macros
, Estructuras
, Traits
, y Macros de Atributo
.
Estos conceptos son esenciales para entender y escribir código Rust eficiente, y te ayudarán en tu camino como desarrollador de contratos inteligentes.
1. Módulos
Los módulos en Rust se utilizan para organizar y separar el código en diferentes espacios de nombres. Permiten una mejor organización del código, reutilización y encapsulamiento. Para definir un módulo, utiliza la palabra clave mod
seguida de un bloque que contenga el contenido del módulo.
mod my_module {
pub fn my_function() {
println!("Hello from my_module!");
}
}
2. Macros
Macros en Rust son herramientas poderosas que te permiten hacer metaprogramación, permitiendo construir bloques de código reutilizables en tiempo de compilación.
Existen dos tipos básicos: macros declarativas y procedurales. La más común es la macro declarativa, o simplemente "macro", que se define con macro_rules!
. La más común es la macro declarativa, o simplemente "macro", que se define con macro_rules!
.
macro_rules! my_macro {
() => {
println!("Hello from my_macro!");
};
}
fn main() {
my_macro!();
}
3. Estructuras
Las estructuras son tipos de datos personalizados en Rust que permiten agrupar datos juntos. Proporcionan una forma de definir y crear estructuras de datos más complejas.
struct MyStruct {
field1: i32,
field2: String,
}
fn main() {
let my_instance = MyStruct {
field1: 42,
field2: String::from("Hello"),
};
}
4. Rasgos
Los traits en Rust definen un conjunto compartido de comportamientos que los tipos pueden usar tal como están (implementaciones predeterminadas) o implementar ellos mismos. Se pueden considerar como interfaces en otros lenguajes. Los traits se definen con la palabra clave trait
, y sus métodos pueden implementarse para diferentes tipos usando la palabra clave impl
.
trait MyTrait {
fn my_method(&self);
}
struct MyStruct;
impl MyTrait for MyStruct {
fn my_method(&self) {
println!("Hello from MyTrait's my_method!");
}
}
5. Macros de Atributo
Las macros de atributo en Rust son una forma de macros procedurales que te permiten definir atributos personalizados para diversos elementos del lenguaje, como funciones, estructuras y enumeraciones. Pueden modificar o generar código basado en los ítems anotados.
// To use an attribute macro, first import it with `use`
use my_attribute_macro::my_attribute;
// Then apply the attribute to an element in your code
#[my_attribute]
fn my_function() {
println!("Hello from my_function!");
}
Durante tu viaje como desarrollador de contratos inteligentes, encontrarás frecuentemente la macro de atributo #[contractimpl]
que exporta funciones accesibles públicamente al entorno de Soroban.
Las funciones que son accesibles públicamente en la implementación pueden ser invocadas por otros contratos, o directamente por transacciones, al ser desplegadas.
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol_short!("Hello"), to]
}
}
Modelo de Propiedad
Rust impone reglas estrictas de propiedad para manejar la memoria y los recursos:
- Cada valor tiene un único propietario.
- Cuando el propietario sale del alcance, el valor se desaloja automáticamente.
Préstamo
: Los valores pueden ser prestados como referencias inmutables o mutables.Duraciones
: Se utilizan para asegurar que las referencias sigan siendo válidas.
Dialectos de Contrato Inteligente
El desarrollo de contratos en Rust implica ciertas restricciones debido a características no disponibles en el entorno de despliegue o altos costos de ejecución. Por lo tanto, el código escrito para contratos puede verse como un dialecto distinto de Rust, centrándose en un comportamiento determinista y un tamaño de código minimizado.
Para aprender más sobre el Dialecto de Contratos de Rust, consulta la Página del Dialecto de Contratos de Rust.
Escribir e Interactuar con Contratos Inteligentes Simples
En esta sección, aprenderemos a escribir e interactuar con contratos inteligentes simples en Solidity y Rust.
Escribir un Contador Inteligente en Solidity
Aquí hay un ejemplo de un simple contrato inteligente de Solidity para un contador:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Counter {
uint256 private _count;
function getCount() public view returns (uint256) {
return _count;
}
function increment() public {
_count += 1;
}
}
Desglosemos el diseño del código línea por línea:
// SPDX-License-Identifier: UNLICENSED
Este es un comentario que identifica la licencia para el código. No es necesario para que el código se ejecute, pero es una buena práctica incluir información sobre la licencia.
pragma solidity ^0.8.0;
Esto especifica la versión de Solidity para la que se escribió este código. En este caso, es la versión 0.8.0
o superior.
contract Counter {}
Esto define un nuevo contrato de Solidity llamado Counter
.
uint256 private _count;
Esta es una variable privada
llamada _count
de tipo uint256
(entero sin signo). Esta variable se utilizará para almacenar el valor actual del contador. Está marcada como private
, lo que significa que solo puede ser accesible desde dentro del contrato.
function getCount() public view returns (uint256) {
return _count;
}
Esta es una función llamada getCount()
que devuelve el valor actual del contador. La función está marcada como public
, lo que significa que puede ser llamada desde fuera del contrato. La palabra clave view
indica que esta función no modifica el estado del contrato. La palabra clave returns
especifica el tipo de retorno de la función.
function increment() public {
_count += 1;
}
Esta es una función llamada increment()
que incrementa el contador en 1. No devuelve nada, pero modifica el estado del contrato. Al igual que getCount()
, está marcada como public
, lo que significa que puede ser llamada tanto desde dentro como desde fuera del contrato.
Interactuar con el Contador Inteligente de Solidity
Podemos interactuar con el contrato inteligente utilizando el Remix IDE
. Para hacerlo, sigue estos pasos:
-
Haz clic en el siguiente enlace para abrir el Gist en Remix.
-
Navega al archivo
Counter.sol
en el explorador de archivos.
- Presiona
Ctrl/Cmd + s
para compilar el contrato. - Navega a la pestaña
Deploy & Run Transactions
y haz clic en el botónDeploy
.
El contrato debería aparecer bajo la pestaña Deployed Contracts
:
- Haz clic en el botón
increment
para incrementar el contador. - Haz clic en el botón
getCount
para obtener el conteo actual.
Hasta este punto, hemos cubierto los conceptos básicos de escribir, desplegar en una EVM de sandbox y interactuar con un contrato inteligente simple utilizando Solidity. En la siguiente sección, ampliaremos nuestro conocimiento aprendiendo cómo lograr los mismos resultados usando Rust.
Escribir un Contador Inteligente en Rust
En esta sección, crearemos un programa en Rust que simule la funcionalidad del contrato inteligente Counter. Aquí hay un ejemplo de un contador simple en Rust:
#![no_std]
use soroban_sdk::{contractimpl, log, Env, Symbol};
const COUNTER: Symbol = symbol_short!("COUNTER");
#[contract]
pub struct IncrementContract;
#[contractimpl]
impl IncrementContract {
/// Increment increments an internal counter, and returns the value.
pub fn increment(env: Env) -> u32 {
// Get the current count.
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0.
log!(&env, "count: {}", count);
// Increment the count.
count += 1;
// Save the count.
env.storage().instance().set(&COUNTER, &count);
// Return the count to the caller.
count
}
/// get_count returns the current value of the counter.
pub fn get_count(env: Env) -> u32 {
env.storage().instance().get(&COUNTER).unwrap_or(0)
}
}
Este código es una implementación de un contrato inteligente escrito en Rust usando el Soroban Rust SDK
, un kit de herramientas de desarrollo de contratos inteligentes basado en Rust desarrollado por la Stellar Development Foundation (SDF). El Soroban Rust SDK proporciona un poderoso conjunto de herramientas para escribir contratos inteligentes que se ejecutan en la Máquina Virtual Soroban.
Aquí hay una explicación línea por línea de lo que hace el código:
#![no_std]
Este es un atributo de Rust que le dice al compilador de Rust que no enlace la biblioteca estándar de Rust. La biblioteca estándar es extensa, y al desplegar aplicaciones Soroban, queremos simplificar el proceso tanto como sea posible. Al usar no_std
, establecemos un punto de partida más ligero y "esquelético" para los proyectos, que abarca solo el núcleo de Rust y algunos otros componentes esenciales, en lugar de toda la amplitud de la biblioteca estándar.
use soroban_sdk::{contractimpl, log, Env, Symbol};
Este código importa elementos necesarios del Soroban Rust SDK para escribir un contrato inteligente. La contractimpl
macro se utiliza para implementar el contrato inteligente, mientras que la macro log
se utiliza para registrar mensajes. La estructura Env
representa el entorno en el que se está ejecutando el contrato, y el tipo Symbol
es un tipo de cadena pequeño y eficiente.
const COUNTER: Symbol = symbol_short!("COUNTER");
Esto crea un nuevo valor de Symbol
con la cadena "COUNTER". La constante COUNTER
se utiliza luego como clave para identificar el valor de conteo almacenado en el storage
del contrato.
#[contract]
pub struct IncrementContract;
Esto define una estructura pública, IncrementContract
, que contendrá la implementación del contrato inteligente.
#[contractimpl]
impl IncrementContract {}
Esta es una macro que implementa la estructura IncrementContract
como un contrato inteligente.
Como se mencionó anteriormente, el atributo #[contractimpl]
exporta funciones públicas al entorno de Soroban. Esto significa que estas funciones se vuelven accesibles dentro de la implementación y pueden ser invocadas por otros contratos o directamente por transacciones al ser desplegadas.
pub fn increment(env: Env) -> u32 {}
Esta es una función pública llamada increment
que toma una estructura Env
como argumento y devuelve un u32
. Env
es el entorno en el que se está ejecutando el contrato, y u32
es el tipo del valor devuelto por la función.
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0)); // If no value set, assume 0.
En esta línea de código, se está creando una variable mutable llamada count
de tipo entero sin signo de 32 bits (u32
). El entorno de almacenamiento se accede utilizando env.storage()
, y se recupera el valor asociado con la clave COUNTER
usando el método get
. Si no hay un valor establecido para la clave COUNTER
, se utiliza un valor predeterminado de 0.
log!(&env, "count: {}", count);
Esto registra el conteo actual utilizando la macro log
proporcionada por el Soroban Rust SDK.
count += 1;
Esto incrementa el conteo en 1.
env.storage().instance().set(&COUNTER, &count);
Esto guarda el conteo actualizado de nuevo en el almacenamiento del contrato utilizando el método set
en el objeto de almacenamiento.
count
Esto devuelve el conteo actualizado al llamador de la función.
pub fn get_count(env: Env) -> u32 {}
Esta es una función pública llamada get_count
que toma una estructura Env
como argumento y devuelve un u32
. Una vez más vemos el Env
, que es el entorno en el que se está ejecutando el contrato, y u32
como el tipo del valor devuelto por la función.
env.storage().instance().get(&COUNTER).unwrap_or(0)
Esta es una repetición del código que vimos anteriormente, que recupera el valor asociado con la clave COUNTER
del almacenamiento del contrato. Si no hay un valor establecido para la clave COUNTER
, se utiliza un valor predeterminado de 0. Finalmente, se llama al método unwrap()
para extraer el valor real del envoltorio Ok
, que luego se devuelve al llamador de la función.
Ahora que hemos escrito nuestro contrato inteligente, es momento de explorar cómo podemos interactuar con él utilizando el Stellar CLI, una de las muchas Herramientas para Desarrolladores robustas disponibles. Esta poderosa herramienta de línea de comandos nos permite interactuar con la Máquina Virtual Soroban desde una máquina local, brindándonos una forma eficiente y flexible de gestionar nuestro contrato inteligente.
Interaccionar con el Rust Smart Counter
Para interactuar con el contador de Rust, crea una nueva biblioteca de Rust usando el comando cargo new.
cargo new --lib increment
Una vez creado el proyecto, reemplaza el archivo src/lib.rs
con el ejemplo de código anterior.
// Remember to replace your lib.rs file with the code example above.
// This is just a reference to point you in the right direction.
#[contractimpl]
impl IncrementContract {...}
Luego, añade las siguientes dependencias al archivo Cargo.toml
:
[package]
name = "increment"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[features]
testutils = ["soroban-sdk/testutils"]
[dependencies]
soroban-sdk = "20.0.0"
[dev_dependencies]
soroban-sdk = { version = "20.0.0", features = ["testutils"] }
[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true
[profile.release-with-logs]
inherits = "release"
debug-assertions = true
Nota: Para una explicación detallada de la configuración de
Cargo.toml
utilizada en este tutorial, consulta el Ejemplo Hola Mundo.
A continuación, construye el proyecto utilizando el comando stellar contract build
.
cd increment
stellar contract build
El contrato compilado se ubicará en el directorio target/wasm32-unknown-unknown/release
.
Para interactuar con el contrato, podemos usar el comando stellar contract invoke
de la herramienta stellar-cli
. Aquí hay un ejemplo de invocar la función increment
en un contrato con ID 1:
stellar contract invoke \
--wasm target/wasm32-unknown-unknown/release/increment.wasm \
--id 1 \
-- \
increment
La salida debería ser el valor actual del contador, que en este caso es:
1
Puedes utilizar el mismo comando stellar contract invoke
para incrementar el contador varias veces.
Para obtener el valor actual del contador, puedes usar el siguiente comando:
stellar contract invoke \
--wasm target/wasm32-unknown-unknown/release/increment.wasm \
--id 1 \
-- \
get_count
La salida debería ser el valor actual del contador, suponiendo que el contador se ha incrementado 3 veces, la salida será:
3
And that's it! Has aprendido cómo escribir e interactuar con contratos inteligentes simples en Solidity y Rust. En las próximas secciones, aprenderemos sobre conceptos avanzados de contratos inteligentes, las similitudes y diferencias entre Solidity y Rust, y cómo desarrollar y desplegar contratos inteligentes con Soroban.