La odisea StarkNet: entendiendo Cairo

Antonio Ufano
StarkNet en español
9 min readJul 20, 2022

--

Artículo originalmente publicado en el blog de Chainstack. Crea una cuenta gratis en Chainstack y empieza a desarrollar aplicaciones en StarkNet o cualquier otra de las más de 12 blockchains soportadas.

Esta es la segunda parte de la serie “La odisea StarkNet”, en la cual repasamos StarkNet desde cero. Antes de seguir leyendo, asegúrate de leer la primera parte para aprender qué es StarkNet, qué son las ZK-proofs y aprender sobre las diferentes herramientas que necesitamos para empezar a desarrollar aplicaciones en StarkNet.

Este artículo es una introducción a Cairo, el lenguaje utilizado para escribir programas y smart contracts en StarkNet, y vamos a ver cómo interactuar con un contrato desde una aplicación web utilizando la libreria de Javascript de ArgentX.

Puedes encontrar todo el código de este artículo, incluyendo contratos y la aplicación web, en el siguiente repositorio en GitHub.

Introducción a Cairo

Cairo es… difícil. El equipo de StarkWare ha creado un repositorio con algunos ejercicios y ejemplos para empezar, y aunque es bastante útil, la curva de aprendizaje de este lenguaje es bastande dura y puede que incluso en este repositorio no sepas por donde empezar.

Así que, empecemos desde cero, y vamos a revisar unas cuantas cosas que conviene saber antes de empezar a escribir la primera línea de código.

¿Qué es felt?

felt significa "Field element" y es el único tipo de dato en Cairo. Simplificando, es un entero sin símbolo que es capaz de almacenar hasta 76 decimales. Podemos utilizarlo también para almacenar direcciones de contratos.

Cadenas de texto (strings)

Actualmente Cairo no tiene soporte para strings. Podemos almacenar pequeñas cadenas de texto en un felt pero se guardaran con su correspondiente valor numérico:

# = 448378203247 
let hello_string = 'hello'

Matrices (arrays)

Para utilizar matrices en Cairo, necesitamos crear un puntero que apunte al primer elemento del array utilizando felt* y alloc().

Para añadir nuevos elementos al array, utilizaremos assert (más información sobre este método después) y el puntero creado anteriormente. Aquí tienes un ejemplo:

%lang starknet
%builtins range_check
# import para utilizar alloc
from starkware.cairo.common.alloc import alloc
# view function que devuelve un felt y
# tiene range_check_ptr como un argumento implícito
@view
func array_demo(index : felt) -> (value : felt):
# Crea el puntero al principio del array
let (my_array : felt*) = alloc()
# guarda 3 como primer elemento en el array
assert [felt_array] = 3
# guarda 15 como el segundo elemento del array
assert [felt_array + 1] = 15
# guarda 33 como tercer elemento
assert [felt_array + 2] = 33
assert [felt_array + 9] = 18
# Lee del array utilizando el indice recibido en la funcion
let val = felt_array[index]
return (val)
end

Si intentamos leer el valor de un índice no valido del array, nuestro programa fallará y lanzará el siguiente mensaje de error: Unknown value for memory cell at address.

Podemos utilizar arrays como parámetros de funciones o devolverlos en las mismas, pero para ello tenemos que indicar tanto el array como su longitud. Para ello, hay que seguir la convención nombre_del_array y nombre_del_array_len. Aquí tienes un ejemplo:

%lang starknet 
%builtins pedersen range_check
from starkware.cairo.common.cairo_builtins import HashBuiltin
# Función que recibe un array como parámetro: el array y su longitud
@external
func array_play(array_param_len : felt, array_param : felt*) -> (res: felt):
# read first element of the array
let first = array_param[0]
# read last element of the array
let last = array_param[array_param_len - 1]
let res = first + last
return (res)
end

Si no utilizamos los nombres correctos en las variables, el compilador nos dará el siguiente mensaje de error: Array argument “array_param” must be preceded by a length argument named “array_param_len” of type felt.

Puedes encontrar un ejemplo de cómo usar arrays en un contrato Cairo en este enlace.

Structs y mappings

Los structs y mappings se declaran de manera muy similar a Solidity. Para definir un struct, utilizaremos la struct y member para cada uno de los atributos:

# Account struct 
struct Account:
member isOpen: felt
member balance: felt
end

Para crear un mapping en Cairo tenemos que definir los tipos y usar -> entre la clave y el valor. Por ejemplo, este es un mapping de felt y struct:

# Mapping llamado "accounts_storage" que guarda los detalles de la 
# cuenta de cada usuario utilizando su address como clave
@storage_var
func accounts_storage(address: felt) -> (account: Account):
end

También podemos devolver structs desde una función. Puedes encontrar un ejemplo en este contrato.

Declarar variables

Podemos crear alias que apuntan a variables utilizando let o asignarles valores con const, local o tempvar.

  • const se utiliza para definir constantes y su valor no se puede re asignar.
  • local utilizado para declarar variables locales. Su valor tampoco se puede re asignar y además tendremos que añadir el método alloc_locals en la función en la que queramos utilizarlas.
  • tempvar se utiliza para declarar variables temporales cuyo valor podemos re asignar.
  • let se utiliza para declarar variables por valor o por referencia con otras varibles. Su valor se puede re asignar.

Aquí tienes unos ejemplos para entender como funciona cada una:

%lang starknet# variable de estado
@storage_var
func storage_variable() -> (res : felt):
end
func variable_examples{}():
# necesario para variables locales
alloc_locals
# crea un alias por valor
let a = 5
let b = 3
# crea un alias por referencia. El valor de x es 5
let x = a
# constante, no se puede reasignar
const ten = 10
# res es 15 aqui, = 5 * 3
tempvar res = a * b
# varible local. c es 15
local c = ten + a
# reassigna variable
let b = 2
# reassigna tempvar. res es 10 =5 * 2
tempvar res = a * b
return ()end

Fíjate en que para reasignar una variable, también tenemos que indicar el tipo, como tempvar o let .

Variables de estado: escribir y leer

Las variables de estado que solemos declarar en los smart contracts, se denominan variables de almacenamiento o “storage variables” en Cairo. Para declararlas, debemos utilizar el decorador @storage_var y definir una función con el tipo. He aqui un ejemplo:

# Guarda el nùmero de cuentas en el estado del contrato
@storage_var func number_of_accounts() -> (res: felt):
end

Para leer y escribir datos en las variables de almacenamiento, tenemos que usar los métodos read y write, asegurándonos de que los tipos de datos que pasamos para escribir, son los mismos de los que hemos definido anteriormente.

# Keeps a counter of the number of accounts
@storage_var
func number_of_accounts() -> (res: felt):
end
# Creates an account for the user
@external
func readWriteAccounts{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}():
// read number of accounts from storage
let (n_accs) = number_of_accounts.read()
// writes number of accounts in storage
number_of_accounts.write(n_accs + 1)
return ()
end

Qué es {syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}

Si has leído algún contrato escrito en Cairo, probablemente habrás visto estas líneas multiples veces: {syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}.

Estos son argumentos implícitos de las funciones y Cairo los necesita para poder acceder a las variables de estado. Además range_check_ptr se utiliza para comparar números. Recuerda incluir estos parámetros en todas las funciones que accedan a variables de estado. Si no lo haces, el compilador te devolverá el siguiente error: Unknown identifier 'syscall_ptr' ... Unknown identifier 'syscall_ptr' 😉

Aserciones

assert es un método súper útil pero que puede usarse para propósitos completamente diferentes:

  • para comparar si el valor de dos variables es el mismo
  • para asignar valor a una variable

Aqui tienes un ejemplo:

%lang starknet@external
func demo_assert(guess : felt) :
const a = 7
tempvar b
# verifica si guess es 7
assert a = guess
# asigna 5 a la variable b
assert b = 5
# verifica si guess es 5
assert b = guess
return ()
end

Para otro tipo de comparaciones, puedes importar otros métodos desde la libreria starkware.cairo.common.math, como por ejemplo assert_not_zero, assert_not_equal, assert_in_range, assert_lt, assert_gt:

%lang starknetfrom starkware.cairo.common.math import (
assert_not_equal,
assert_in_range
assert_not_zero,
assert_lt,
assert_le,
)
@external
func assertions_demo(a: felt, b: felt):
assert_not_zero(a)
assert_not_equal(b, a)
assert_le(1, 100)
assert_lt(b, 1)
assert_in_range(b, 1, 60)
return ()
end

Funciones

Las funciones en los contratos se declaran utilizando los decoradores @external y @view. Las que declaramos como external se invocan por otros contratos o por usuarios y normalmente alteran variables de estado. Las funciones view solo leen variables de estado, no pueden alterarlo.

# view function que devuelve el numero de cuentas desde la variable 
# de almacenamiento
@view
func accountsOpen{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (res: felt):
let (res) = number_of_accounts.read()
return (res)
end
# Crea cuenta para el usuario y actualiza
# la variable de almacenamiento
@external
func openAccount{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}():
let (sender_address) = get_caller_address()
# checks if the user already has an account
let (user_account) = accounts_storage.read(sender_address)
assert user_account.isOpen = 0
let (n_accs) = number_of_accounts.read()
accounts_storage.write(sender_address, Account(isOpen=1, balance=0))
number_of_accounts.write(n_accs + 1)
return ()
end

Debemos indicar los valores que vamos a devolver en nuestras funciones, incluso si la función no devuelve ningún valor.

Estructura de un contrato

Los contratos escritos en Cairo tienen una estructura similar a los contratos escritos en otros lenguajes como Solidity:

  1. definición del lenguaje %lang starknet
  2. importación de librerias
  3. structs and variables de estado
  4. métodos y funciones

Comunicación entre L1-L2

Una de las cosas que podemos hacer en Cairo es enviar mensajes entre StarkNet (L2) y Ethereum (L1). Para ello, necesitamos desplegar un contrato en StarkNet que utilice el método send_message_to_l1 , que recibe como parámetros la dirección del contrato que va a recibir el mensaje en la L1, y los parámetros o variables que querramos enviar.

Además, tendremos que desplegar el correspondiente contrato en Ethereum para capturar esos mensajes. Este contrato tendrá que implementar la interfaz IStarknetCore que provee los métodos consumeMessageFromL2 y sendMessageToL2.

Puedes encontrar un tutorial paso a paso en la documentación de Chainstack.

Como interactuar con contratos

Puedes interactuar con contratos directamente desde Voyager, el explorador oficial de StarkNet.

Para interactuar con contratos desde una aplicación web, las librerias más comunes son starknet.js y @argent/get-starknet. La primera es una libreria que podemos utilizar tanto en un frontend como en un backend. Puedes encontrar la documentación de su API aqui. La segunda, es un wrapper que ayuda a interactuar con wallets como ArgentX y Braavos, aunque utiliza starknet.js como dependencia.

Ejemplo de aplicación web

En el siguiente repositorio encontrarás una pequeña aplicación web construida con Vue.js y @argent/get-starknet que te servirá como ejemplo para ver como interactuar con wallets y con un contrato desplegado en StarkNet.

Aquí puedes ver como conectar con la wallet:

import { connect } from '@argent/get-starknet'let starknet = nullconst connectWallet = async () => {
starknet = await connect()
console.log('startknet >>', starknet)
if (!starknet) {
throw Error(
'User rejected wallet selection or silent connect found nothing'
)
}
await starknet.enable()// Check if connection was successful
if (starknet.isConnected) {
console.log('starknet connected')
} else {
console.log('starknet wallet not connected')
}
}

Una vez que nuestra aplicación está conectada con la wallet del usuario, podemos utilizarla para interactuar con nuestro contrato mediante los métodos starknet.account.callContract para funciones @view o starknet.account.execute para funciones @external. Ambos requiren los siguientes parámetros:

  • contractAddress
  • entrypoint el nombre de la función del contrato
  • calldata un array con todos los parámetros que queremos enviar (opcional)

Aquí tienes un ejemplo de como usar ambas:

const CONTRACT_ADDRESS =
'0x07955c619cb22d08ece120ef4f5faa531318ac9934018ef28fdae9284b831b4d'
// Reads from the chain using callContract
const getUserNumber = async () => {
try {
const res = await starknet.provider.callContract({
contractAddress: CONTRACT_ADDRESS,
entrypoint: 'get_number',
})
console.log('res', res)
savedNumber.value = Number(`${res.result[0]}`)
} catch (error) {
console.error(error)
}
}
// Writes on-chain using execute
const saveNumber = async () => {
try {
const trxDetails = await starknet.account.execute({
contractAddress: CONTRACT_ADDRESS,
entrypoint: 'save_number',
calldata: [userNum.value],
})
console.log('trxDetails', trxDetails)
} catch (error) {
console.error(error)
}
}

Libreria para Python

Si prefieres utilizar Python, StarkNet.py es la libreria para ti. Tiene todos los métodos que necesitas para interactuar con contratos y la blockchain

Conclusión

Espero que este artículo te ayude a perderle el miedo a Cairo y te empuje a desarrollar aplicaciones en StarkNet.

Las soluciones de capa 2 (L2s) son cada vez más populares y StarkNet es una de las más populares. Hay hackathons casi cada mes y el equipo de StarkWare ha lanzado StarkGate, un bridge que permite enviar ETH y otros tokens desde Ethereum mainnet a Starknet.

Originally published at https://chainstack.com/blog.

--

--

Antonio Ufano
StarkNet en español

Developer Relations Engineer at MatterLabs. Scaling Ethereum with zkSync. Passionate about the web and the possibilities that blockchains can bring to it.