Programando en la L2 de Ethereum: Básicos de Cairo pt. 3

Escrito por @espejelomar. Mándame un DM y aprenderemos juntos 🥳.. Únete al mayor Meetup de habla hispana sobre StarkNet y al naciente Telegram. Antes de comenzar, te recomiendo que prepares tu equipo para programar en Cairo ❤️ con el primer tutorial y que revises los básicos de Cairo pt. 2.

🚀 El futuro de Ethereum es hoy y ya está aquí. Vamos a aprender a usar un ecosistema:

  • Sostiene a dYdX, DeFi que ya hizo cuatrocientos billones de trades y representa alrededor de un cuarto del total de las transacciones hechas en ethereum. Funcionan apenas desde hace 18 meses y constantemente vencen a Coinbase en volumen de trades. Redujeron el precio de las transacciones de 500 a 1,000 veces. Son tan baratas que no necesitan cobrar el gas a los usuarios 💸.
  • En la la semana del 7 al 13 de marzo de 2022, por primera vez, logró tener 33% más transacciones que Ethereum 💣.

Y apenas es el comienzo. Aprende un poco más sobre el ecosistema de Starkware en este texto corto. Saluda en el el canal #🌮-español en el Discord de StarkNet.


En la tercera parte de la serie de tutoriales básicos de Cairo profundizaremos en conceptos introducidos en la segunda sesión como los builtin, los felt y assert y sus variaciones. Además, introduciremos los arrays. Con lo aprendido en esta sesión seremos capaces de crear contratos básicos en Cairo 🚀.

1. Los builtin y su relación con los pointers

%builtins output

from starkware.cairo.common.serialize import serialize_word

func mult_dos_nums(num1, num2) -> (prod):
    return(num1 * num2)
end

func main{output_ptr: felt*}():    
    let (prod) = mult_dos_nums(2,2)
    serialize_word(prod)
    return ()
end

¿Recuerdas que introdujimos los builtins en la sesión pasada junto con los argumentos implícitos?

Cada builtin te da el derecho a usar un pointer que tendrá el nombre del builtin + “_ptr”. Por ejemplo, el builtin output, que definimos %builtins output al inicio de nuestro contrato, nos da derecho a usar el pointer output_ptr. El builtin range_check nos permite usar el pointer range_check_ptr. Estos pointers suelen usarse como argumentos implícitos que se actualizan automáticamente durante una función.

En la función para multiplicar dos números usamos %builtins output y, posteriormente, utilizamos su pointer al definir main: func main{output_ptr: felt*}():.

2. Más sobre lo interesante (raros?) que son los felts

The felt is pretty much the only data type that exists in Cairo, you can even omit it [its explicit statement] sometimes (StarkNet Bootcamp - Amsterdam - min 1:14:36).

Si bien no es necesario ser un@ expert@ en las cualidades matemáticas de los felts, es valioso conocer cómo funcionan. En el tutorial pasado los introdujimos por primera vez, ahora conoceremos cómo afectan cuando comparamos valores en Cairo.

La definición de un felt, en términos terrestres (la exacta esta aquí): un número entero que puede llegar a ser enorme (pero tiene límites). Por ejemplo: {...,-4,-3,-2,-1,0,+1,+2,+3,...}. Sí, incluye 0 y números negativos.

Cualquier valor que no se encuentre dentro de este rango causará un “overflow”: un error que ocurre cuando un programa recibe un número, valor o variable fuera del alcance de su capacidad para manejar (Techopedia).

Ahora entendemos los límites de los felt. Si el valor es 0.5, por ejemplo, tenemos un overflow. ¿Dónde experimentaremos overflows frecuentemente? En las divisiones. El siguiente contrato divide 9/3, revisa con assert que el resultado sea 3, e imprime el resultado.

*Recuerda lo que vimos al final del primer tutorial sobre cómo compilar y correr nuestros programas.

%builtins output

from starkware.cairo.common.serialize import serialize_word

func main{output_ptr: felt*}():
    tempvar x = 9/3
    assert x = 3
    serialize_word(x)
    
    return()
end

Hasta ahora todo hace sentido. ¿Pero qué pasa si el resultado de la división no es un entero como en el siguiente contrato?

%builtins output

from starkware.cairo.common.serialize import serialize_word

func main{output_ptr: felt*}():
    tempvar x = 10/3
    assert x = 10/3
    serialize_word(x)
    
    return()
end

Para empezar, nos imprime en consola el hermoso número 🌈: 1206167596222043737899107594365023368541035738443865566657697352045290673497. ¿Qué es esto y por qué nos lo retorna en vez de un apreciable punto decimal?

En la función arriba x no es un floating point, 3.33, ni es un entero redondeado con el resultado, 3. Es un entero que multiplicado por 3 nos dará 10 de vuelta (se ve como esta función 3 * x = 10) o también x puede ser un denominador que nos devuelva 3 (10 / x = 3). Veamos esto con el siguiente contrato:

%builtins output

from starkware.cairo.common.serialize import serialize_word

func main{output_ptr: felt*}():
    tempvar x = 10/3

    tempvar y = 3 * x
    assert y = 10
    serialize_word(y)
    

    tempvar z = 10 / x
    assert z = 3
    serialize_word(z)
    
    return()
end

Al compilar y correr este contrato obtenemos exactamente lo que buscabamos:

Program output:
  10
  3

Cairo logra esto al volver al realizar un overflowing de nuevo. No entremos en detalles matemáticos. Esto es algo poco intuitivo pero no te preocupes, hasta aquí lo podemos dejar.

Una vez que estás escribiendo contratos con Cairo no necesitas estar pensando constantemente en esto [las particularidades de los felts cuando están en divisiones]. Pero es bueno que estar consciente de cómo funcionan (StarkNet Bootcamp - Amsterdam - min 1:31:00).

3. Comparando felts 💪

Debido a las particularidades de los felts, comparar entre felts no es como en otros lenguajes de programación (como con 1 < 2).

En la librería starkware.cairo.common.math encontramos funciones que nos servirán para comparar felts (link a repositorio en GitHub). Por ahora usaremos assert_not_zero, assert_not_equal, assert_nn y assert_le. Hay más funciones para comparar felts en esta librería, te recomiendo que veas el repositorio de GitHub para explorarlas. El siguiente código del Bootcamp de StarkNet en Amsterdam sirve para entender lo que hace cada una de las funciones que importamos (lo alteré ligeramente).

%builtins range_check

from starkware.cairo.common.math import assert_not_zero, assert_not_equal, assert_nn, assert_le

func main{range_check_ptr : felt}():
    assert_not_zero(1)  # no es cero
    assert_not_equal(1, 2)  # no son iguales
    assert_nn(1)  # no es negativo (non-negative)
    assert_le(1, 10)  # menor o igual
    
    return ()
end

¿Sencillo, cierto? Solo son formas diferentes de hacer asserts.

¿Pero qué pasa si queremos comparar 10/3 < 10? Sabemos que esto es cierto, pero también sabemos que 10/3 nos dara un entero grande; el resultado de la división no es un entero por lo que cae fuera del rango de posibles valores que pueden tomar los felts. Habrá overflow y se generará el entero grande que naturalmente será mayor que 10 o incluso resultará que está fuera de los enteros posibles que un felt puede tomar (por lo grande que es).

En efecto la siguiente la función que compara 10/3 < 10 nos retornará un error: AssertionError: a = 2412335192444087475798215188730046737082071476887731133315394704090581346994 is out of range.

%builtins range_check

from starkware.cairo.common.math import assert_lt

func main{range_check_ptr : felt}():
    assert_lt(10/3, 10) # menor que
    
    return ()
end

¿Cómo hacemos entonces para comparar 10/3 < 10? Tenemos que volver a nuestras clases de secundaria/colegio. Simplemente eliminemos el 3 del denominador al multiplicar todo por 3; compararíamos 3*10/3 < 3*10 que es lo mismo que 10 < 30. Así solo estamos comparando enteros y nos olvidamos de lo exéntricos que son los felt. La siguiente función corre sin problema.

%builtins range_check

from starkware.cairo.common.math import assert_lt

func main{range_check_ptr : felt}():
    assert_lt(3*10/3, 3*10)
    
    return ()
end

4. La doble naturaleza de assert

Como hemos visto, assert es clave para la programación en Cairo. En los ejemplos arriba lo utilizamos para confirmar una declaración, assert y = 10. Este es un uso común en otros lenguajes de programación como Python. Pero en Cairo cuando tratas de assert algo que no está asignado aún, assert funciona para asignar. Mira esté ejemplo adaptado del Bootcamp de StarkNet en Amsterdam que también nos sirve para afianzar lo aprendido sobre las structs en el tutorial pasado:

%builtins output

from starkware.cairo.common.serialize import serialize_word

struct Vector2d:
    member x : felt
    member y : felt
end

func add_2d(v1 : Vector2d, v2 : Vector2d) -> (r : Vector2d):
    alloc_locals

    local res : Vector2d
    assert res.x = v1.x + v2.x
    assert res.y = v1.y + v2.y

    return (res)
end

func main{output_ptr: felt*}():
    
    let v1 = Vector2d(x = 1, y = 2)
    let v2 = Vector2d(x = 3, y = 4)

    let (sum) = add_2d(v1, v2)

    serialize_word(sum.x)
    serialize_word(sum.y)

    return()
end

Al correr assert res.x = v1.x + v2.x, el prover (más sobre esto más adelante) de Cairo detecta que res.x no existe por lo que le asigna el nuevo valor v1.x + v2.x. Si volvieramos a correr assert res.x = v1.x + v2.x, el prover sí compararía lo que encuentra asignado en res.x con lo que intentamos asignar. Es decir, el uso que ya conocíamos.

5. Arrays en Cairo

Cerremos este tutorial con una de las estructura de datos más importantes. Los arrays, arreglos en español, contienen elementos ordenados. Son muy comunes en programación. ¿Cómo funcionan en Cairo? Aprendamos creando un array de matrices 🙉. Sí, el escrito tiene un background en machine learning. El contrato abajo está comentado y examinaremos unicamente la parte de los arrays pues el lector ya conoce el resto.

%builtins output

from starkware.cairo.common.serialize import serialize_word
from starkware.cairo.common.alloc import alloc

struct Vector:
    member elements : felt*
end

struct Matrix:
    member x : Vector
    member y : Vector
end

func main{output_ptr: felt*}():

    # Definiendo un array, mi_array, de felts.
    let (mi_array : felt*) = alloc()

    # Asignando valores a tres elementos de mi_array.  
    assert mi_array[0] = 1
    assert mi_array[1] = 2
    assert mi_array[2] = 3

    # Creando los vectores Vector, por 
    # simplicidad usamos el mismo  mi_array para ambos.
    let v1 = Vector(elements = mi_array)
    let v2 = Vector(elements = mi_array)

    # Definiendo un array de matrices Matrix
    let (matrix_array : Matrix*) = alloc()

    # Llenando matrix_array con instancias de Matrix.
    # Cada instancia de Matrix contiene como members
    # a instancias de Vector.
    assert matrix_array[0] = Matrix(x = v1, y = v2)
    assert matrix_array[1] = Matrix(x = v1, y = v2)

    # Usamos assert para probar algunos valores en
    # nuestra matrix_array.
    assert matrix_array[0].x.elements[0] = 1
    assert matrix_array[1].x.elements[1] = 2

    # ¿Qupe valor crees que imprimirá? Respuesta: 3
    serialize_word(matrix_array[1].x.elements[2])

    return()
end

Creamos un array de felts llamado mi_array. Esta es la forma en que se define:

let (mi_array : felt*) = alloc()

Es poco intuitivo en comparación con lo fácil que es en Python y otros lenguajes. mi_array : felt* define una variable llamada mi_array que contendrá un pointer (ver tutorial pasado) a un felt (aún no definimos a qué felt). ¿Por qué? La documentación de Cairo nos ayuda:

Los arrays se pueden definir como un pointer (felt*) al primer elemento del array. A medida que se llena el array, los elementos ocupan celdas de memoria contiguas. La función alloc() se usa para definir un segmento de memoria que expande su tamaño cada vez que se escribe un nuevo elemento en el array (documentación de Cairo)”.

Entonces, en el caso de mi_array, al colocar el alloc() estamos indicando que el segmento de memoria al que la expresión mi_array apunta (recuerda que mi_array es solo el nombre de un pointer, felt*, en memoria) será expandido cada vez que se escriba un nuevo elemento en mi_array.

De hecho, si pasamos al repo donde se encuentra alloc() veremos que retorna (ptr : felt*). Es decir, nos regresa una tupla de un solo miembro que es un felt* (un pointer a un felt). Por ser una tupla la recibimos con un let y con mi_array : felt* entre paréntesis (ver básicos de Cairo pt. 2). Todo va haciendo sentido, ¿cierto 🙏?

Vemos que la definición de nuestro array de matrices es exactamente igual salvo que en vez de querer un array de felt queremos uno de Matrix:

let (matrix_array : Matrix*) = alloc()

Ya pasamos lo complicado 😴. Ahora veamos cómo rellenar nuestro array con structuras Matrix. Usamos assert y podemos indexar con [] la posición del array que queremos alterar o revisar:

assert matrix_array[0] = Matrix(x = v1, y = v2)

Lo que hicimos fue crear una Matrix(x = v1, y = v2) y asignarla a la posición 0 de nuestra matrix_array. Recuerda que empezamos a contar desde 0. Rellenar nuestro array de felt es aún más trivial: assert mi_array[0] = 1.

Después simplemente llamamos de diferentes maneras a elementos dentro de matrix_array. Por ejemplo, con matrix_array[1].x.elements[2] indicamos estos pasos:

  1. Llama al segundo, [1], elemento de matrix_array. Es decir, a Matrix(x = v1, y = v2).
  2. Llama al member x de Matrix. Es decir, a v1 = Vector(elements = mi_array).
  3. Llama al member elements de v1. Es decir, a mi_array.
  4. Llama al tercer, [2], elemento de mi_array. Es decir, a 3.

No es tan complicado pero es lo suficientemente satisfactorio 🤭.

6. Conclusión

Felicidades 🚀. Hemos profundizado en los básicos de 🏖 Cairo. Con este conocimiento puedes comenzar a hacer contratos sencillos en Cairo 🥳.

En los siguientes tutoriales aprenderemos más sobre los el manejo de la memoria; la common library de cairo; cómo funciona el compilador de Cairo; y más!

Cualquier comentario o mejora por favor comentar con @espejelomar 🌈.

Subscribe to Omar Espejel
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.