En este artículo vamos a presentar el lenguaje de programación Solidity.
Habrá realmente dos entregas, siendo la próxima orientada a conceptos más avanzados, así como a patrones y seguridad.
Antes de mostrarlo, vamos a entender qué son los archiconocidos Smart Contracts, así como las alternativas para implementarlos.
El Smart Contract es un concepto propuesto por el informático Nick Szabo con la intención de extender la funcionalidad de las transacciones electrónicas comerciales (como por ejemplo a través de un TPV), al mundo digital; de forma que se pudiera intercambiar cualquier cosa de valor de forma transparente y sin disputas entre dos desconocidos, al tiempo de reforzar la seguridad evitando ataques de tipo man-in-the-middle.
Esta idea se materializa en un programa informático escrito en un lenguaje de programación de alto nivel que se compila y se despliega en una red basada en tecnología Blockchain como es Ethereum, y que es ejecutado por todos los nodos de dicha red.
Podemos automatizar muchas soluciones mediante este tipo de programas, que serán proporcionalmente tan complejos como el caso de uso concreto a implementar, pero seguro que lograremos una alternativa más simple y eficiente.
Ethereum es la plataforma más popular para escribir este tipo de programas, pero no es la única. Por ejemplo en Hyperledger Fabric existen los denominados Chaincode que pueden ser escritos en Go, Node.js y Java.
Centrándonos en Ethereum, los Smart Contracts se ejecutan por la EVM (Ethereum Virtual Machine) sobre bytecode compilado. Por lo tanto es factible pensar que pueden existir varios lenguajes de programación de alto nivel para escribir estos contratos, y que tras compilar este código fuente, obtenemos el bytecode que se desplegará en la red y que será ejecutado o correrá sobre la EVM.
Los lenguajes de programación disponibles en Ethereum son:
- Serpent, parecido a Python pero ya descatalogado y sin mantenimiento.
- LLL, siglas de Low-level Lisp-like Language, muy poco mantenido (no tiene ni repositorio propio) y apenas usado.
- Mutan, parecido a Go pero fue rápidamente descatalogado en el 2015.
- Solidity, es el lenguaje más popular, usado y con mayor soporte a fecha de hoy, y responsable del declive de los tres anteriores. Es parecido a C y JavaScript. Su última versión, en este momento es v0.4.25.
No obstante, se aproximan nuevos lenguajes que pueden hacer tambalear a Solidity, aunque en su defensa cabe decir que se está cocinando una revisión importante, la v0.5.0.
Estos lenguajes que ya están apareciendo y que no hay que perderlos de vista son los siguientes. Algunos asociados o creados para una determinada plataforma Blockchain.
- Vyper, similar a Python, y diseñado para dar seguridad, simplicidad de uso y capacidad de auditoría. Existen módulos de Ethereum implementados ya con este lenguaje.
- Bamboo, cuyo compilador está escrito en el lenguaje OCaml; es experimental todavía y pretende ser fácil de leer basándose en estados.
- Plutus, se basa en Haskell y es el lenguaje de programación elegido para la plataforma Blockchain Cardano.
- Lisk, esta plataforma Blockchain potencia la programación de DApps, y utiliza NodeJS/JavaScript como lenguaje de programación de Smart Contracts, facilitando el trabajo al desarrollador.
- Chain, esta empresa proporciona infraestructura DLT empresarial mediante SDKs en varios lenguajes como son Java, NodeJS y Ruby.
- Scilla es un lenguaje de nivel intermedio orientado a la seguridad y verificación formal para proporcionar altas garantías de ejecución libre de errores. Ahora mismo es experimental y acotado a la plataforma Zilliqa encaminada a superar los problemas de escalabilidad existentes en las redes públicas, pero su objetivo es poder ser usado en otras redes blockchains.
- Pact es el lenguaje de programación de Smart Contracts de la plataforma Kadena; es un lenguaje robusto, sencillo y seguro, y la plataforma está dirigida a escenarios de gran rendimiento y críticos.
- Liquidity es el lenguaje elegido para la plataforma Blockchain Tezos. Basado también en el lenguaje OCaml, da cobertura completa al lenguaje Michelson: todo lo que se pueda escribir en el lenguaje Michelson se puede escribir en Liquidity. Prioriza la simplicidad frente a la multifuncionalidad.
Dado que Solidity es el lenguaje de programación más utilizado para escribir Smart Contracts en Ethereum, empecemos a examinarlo. Realmente trataremos la siguiente información como una cheat sheet ya que como cualquier lenguaje, se necesitaría un libro entero para enseñarlo. La documentación oficial la encontraremos aquí.
Fichero fuente, directiva Pragma y Comentarios
El código Solidity está encapsulado en Contratos que son la estructura fundamental de las aplicaciones descentralizadas Ethereum. Todas las variables y funciones forman parte de un contrato y son el punto inicial de cualquier proyecto.
Un fichero fuente tiene extensión .sol y puede contener varias definiciones de contratos.
Solidity es un lenguaje de tipado estático.
pragma solidity ^0.4.24 contract MyContract {
// This is a sigle-line comment /* * This is a * multi-line comment */ /** * @author Steve McQueen * Ethereum Natural Specification (NatSpec) format */ }
La línea pragma indica que el fichero fuente está escrito para la versión del compilador 0.4.24 y superiores hasta la 0.5.X.
Value Types
Son variables de tipo value type las que se se pasan por valor (se copian) cuando se usan como argumentos en funciones o en asignaciones. Un cambio de valor en la variable original o destino no afecta al valor de la otra: están aisladas y mantienen su propio valor.
Boolean
La palabra clave es bool y tiene valores true o false. bool isAlive; // inicializado a false
Usa 1 byte de almacenamiento.
Number
La palabra clave es int o uint.
Son enteros con signo y sin signo respectivamente, en el rango de int8..int256 y uint8..uint256 bits. En incrementos de 8 bits, y siendo el tamaño máximo 32 bytes.
Por defecto uint es el alias de uint256 y int es el alias de int256. int temperature; // entero con signo inicializado a 0 uint age; // entero sin signo inicializado a 0
A pesar de que la sintaxis permita números de punto flotante fixed o ufixed y sus alternativas fixedMxN o ufixedMxN, por el momento no se puede trabajar con ellos así que no existen.
Address
La palabra clave es address y maneja las direcciones Ethereum (direcciones de contratos y cuentas EOA) que son 20 bytes o 160 bits (equivalente a uint160).
Este tipo de objeto tiene funciones y variables miembro, siendo las más importantes la propiedad balance para solicitar el saldo de la dirección (en wei) y el método transfer para enviar Ether (también en wei) a una dirección. address x = 0x123; address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) { x.transfer(10); // desde myAddress a 'x' }
Enum
Las enumeraciones son útiles para crear tipos constantes predefinidos. La palabra clave es enum. enum TransferType { Ordinary, Urgent } // sin punto y coma al final TransferType defaultType = TransferType.Ordinary;
uint8 myType = uint8(TransferType.Urgent); // conversión explícita a enteros
Byte
bytes1, bytes2, bytes3 hasta bytes32 son tipos de arrays de bytes de tamaño fijo.
byte es un alias de bytes1.
bytes11 welcome = "Hello World"; byte x = welcome[1]; // [indice_de_acceso] es solamente de lectura
// x es un byte cuyo contenido es 01100101, // equivalente a 101 en decimal y 0x65 en hex, // y finalmente la letra ASCII 'e'
Reference Types
Los tipos por referencia, no almacenan su valor directamente en ellos mismos, si no que almacenan la dirección de memoria (puntero) donde el valor está almacenado.
Estos tipos ya pueden manejar más de 32 bytes de tamaño de memoria.
El funcionamiento en las asignaciones y en el paso de parámetros, es la creación de una nueva variable por parte de la EVM copiando el puntero de la variable original.
Structures
La palabra clave es struct y nos permite definir un nuevo tipo que agrupe a diferentes variables (de tipos también diferentes), es decir, nos permite agrupar datos relacionados en una única entidad. struct Task { uint id; string desc; bytes32 content; uint start; uint end; }
Task updatePolicies = Task(9, "", 0x45433, 1539613928, 1546300799); // creamos una instancia de la estructura Task
Mappings
La palabra claves es mapping y es como una estructura tipo HashMap o 'diccionario', en la que se asocia una key con un valor.
Por defecto no es iterable ni tampoco tiene un operador de longitud (en otros lenguajes de programación sí existen ambas funcionalidades).
Además el valor existe para todas las keys, siendo el valor por defecto de dicha variable/tipo (0x0, 0, ...). Es como si toda posible key estuviera preinicializada.
mapping (uint => address) customers; customers[4] = 0xca35b7d915458ef540ade6068dfe2f44e8fa733c; // [indice_de_acceso]
// almacenamos la dirección 0xca35b7d915458ef540ade6068dfe2f44e8fa733c en la clave 4
Los mappings solamente pueden declararse como variables de estado y su localización de memoria es de tipo storage (estos conceptos se tratan más adelante).
Sin embargo si se declaran en funciones deben hacer referencia a los mappings declarados como variables de estado.
Arrays y Strings
Los arrays en Solidity pueden ser de tamaño fijo o dinámicos (tamaño no definido en la declaración). int[5] stores; // tamaño fijo stores = [int(10), 22, 3, 4, 3];
byte[] flags; // tamaño dinámico flags = new byte[](10); // inicialización con el operador 'new'
En Solidity, las cadenas (entendiéndose como una secuencia de bytes de longitud variable), se pueden crear usando bytes y string.
bytes es para crear una cadena plana, mientras que string es para crear una cadena codificada en UTF8.
string h = "Hello"; // almacenado como UTF8
// si no nos importa la codificación y podemos prever el tamaño, // es mejor usar bytesX e incluso bytes porque son más baratos en cuestión de 'gas'
Los Arrays y String poseen las propiedades:
- index para leer un elemento individual (excepto para string) o escribir un elemento individual (excepto para string y byte).
- push para agregar elementos en un Array dinámico.
- length para obtener la longitud excepto para string. También se puede establecer (escribir) la longitud en un Array dinámico y en bytes.
bool[100] accounts; uint len = accounts.length;
bool[] other; other.push(true);
other.length = 200;
Ubicación de datos
La EVM proporciona cuatro zonas de almacenamiento para datos. Es importante conocerlas porque según dónde se haga la declaración de variables, y también dependiendo de su tipo , habrá restricciones y reglas de funcionamiento.
- storage es el almacenamiento permanente, dentro de cada nodo Ethereum, disponible para el contrato y todas sus funciones.
- memory es la memoria temporal disponible para cada función de un contrato, y que desaparece una vez finalizada la ejecución de la misma.
- calldata es una zona de la memoria no modificable donde se almacena los datos de ejecución de las funciones como son sus argumentos.
- stack es la pila de la EVM donde almacena variables y valores intermedios mientras está ejecutando instrucciones. Es posible que se desborde, lo que implicaría el lanzamiento de una excepción.
Por ejemplo, las variables declaradas como variables de estado (aquellas declaradas a nivel de contrato, no dentro de las funciones que contenga el mismo) almacenan sus valores actuales en el espacio de almacenamiento storage.
Ámbito de las variables de estado
Las variables de estado, state variables, tienen los siguientes cualificadores que modifican su comportamiento.
internal es el valor por defecto (si no se especifica) y significa que puede ser utilizada (leída y escrita) por el contrato que la declara, así como los contratos hijos (los que heredan de ése). La herencia la veremos en la siguiente entrega.
int internal total;
private implica que la variable puede ser usada (leída y escrita) únicamente por el contrato que la ha declarado; incluso los contratos heredados no pueden usarla.
int private total;
public implica que se puede leída por cualquier contrato, pero escrita únicamente por el contrato que la declara, así como los contratos hijos de ese contrato. Además el compilador genera un método 'get' para ella.
int public total;
constant implica que no se pueda modificar el valor de la variable. Este valor debe ser asignado directamente en la declaración. Es usado junto con los tres anteriores.
int constant internal total = 10;
Funciones y visibilidad
Las funciones pueden aceptar parámetros, ejecutar lógica, leer o escribir en variables de estado, y devolver valores mediante la palabra clave returns () a la entidad que la llamó.
Si la función devuelve múltiples valores, entonces devuelve un tipo tupla, tuple type, que es simplemente una lista de objetos.
Todas las funciones tienen nombre, excepto la denominada fallback que veremos más adelante.
Igual que las variables de estado, tienen modificadores de visibilidad, que son:
- internal es el tipo de función que puede ser usada por el contrato actual y por todos los contratos hijos. Son funciones que no se pueden acceder desde fuera del contrato.
- private implica que la función es usada únicamente por el contrato que la declara. No puede ser usada ni por los contratos hijos que tenga ese contrato.
- public hace que sea visible totalmente la función y usada tanto internamente como externalmente (desde fuera). Es el valor por defecto.
- external es para dar visibilidad únicamente a la función desde fuera. Internamente no se puede usar, aunque existe la nomenclatura this.f() que lo permite. Generalmente usan menos gas que las public cuando reciben muchos parámetros de entrada.
- view es un atributo que puede ser usado junto con los anteriores modificadores e indica que la función solamente lee valores (variables de estado) sin modificar el estado de la red blockchain, es decir, sin modificar dichas variables de estado y sin ejecutar rutinas que cambien el estado de la red (como puede ser invocar eventos, crear nuevos contratos con el operador new o llamar a otras funciones que sí cambien el estado). Anteriormente se conocían como funciones constantes (constant).
- pure es otro atributo que puede ser usado junto con los anteriores modificadores y es más restrictivo que view ya que implica que además de no modificar el estado de la red, la función en cuestión no puede acceder a las variables de estado del contrato.
function defaultCar() public pure returns (string, int) { string memory brand = "OPEL"; int cv = 1300; return (brand, cv); }
Constructor, operador delete y operación self-destruct
Solidity soporta la declaración opcional de un único constructor en un contrato.
La palabra clave es constructor () y es invocado en el momento de desplegar el contrato. Es útil para inicializar variables de estado y puede tener parámetros de entrada que deberán ser alimentados mientras se despliega el mismo. Puede ser public o internal.
Por último, en versiones anteriores de Solidity debía ser una función con el nombre del contrato.
Versiones anteriores de Solidity:
pragma solidity ^0.4.11; contract Trailer {
uint public volume; function Trailer (uint _volume) {
volume = _volume; } }
Últimas versiones de Solidity;
pragma solidity ^0.4.24; contract Trailer {
uint public volume; constructor (uint _volume) public {
volume = _volume; } }
El operador delete se aplica para eliminar el valor de cualquier variable, retomando el valor por defecto que tiene en su inicialización, esto es, todos sus bits a cero.
int[] balances; mapping (int => int) counters;
balances.push(100); counters[2000] = 55;
delete balances; // borramos todos los elementos del array y su longitud vuelve a ser cero. delete counters[2]; // aplicado a la 'key', el 'value' es borrado
El método selfdestruct () (anteriormente suicide ()), elimina o destruye el actual contrato, y manda los fondos que tuviera (su balance interno) a una dirección dada.
pragma solidity ^0.4.24; contract Mortal {
function close(address recipient) public { selfdestruct(recipient); } }
Modificadores de función
Un modificador, cuya palabra clave es modifier es simplemente una función que se ejecuta justamente antes de la función a la que está 'modificando'. El objetivo es cambiar el comportamiento natural de una función. Veamos un ejemplo.
pragma solidity ^0.4.24;
contract Descent { mapping(uint => uint) public children;
modifier onlyLargeFamily (uint head) { require(children[head] >= 3); // aborta la ejecución si la condición no se cumple _; // cuerpo de la función }
constructor() public { children[1] = 1; children[2] = 4; }
function getDiscount(uint head) onlyLargeFamily(head) public returns (uint discount) { discount = getDefaultDiscount(); return discount; }
}
En este ejemplo, solamente se obtiene el descuento por defecto si 'head' es 2 ya que posee más de 3 hijos según la restricción del modificador.
Como vemos los modificadores se declaran a nivel de contrato y pueden tener argumentos; el carácter especial subrayado _ significa que se ejecutará la función sobre la cual actúa el modificador.
Como los modificadores puede asociarse a múltiples funciones, su uso asegura reutilización y un código más limpio.
payable y fallback
Este modificador y esta función respectivamente, están asociados a la transferencia y recepción de Ether. Veámoslo.
Si invocamos una función de un contrato que está marcada con el modificador payable entonces puede recibir Ether.
Este modificador hará que dicha función pueda recibir Ether y la cantidad enviada está disponible en msg.value que en el número de wei enviado en el mensaje.
Por ejemplo, el siguiente contrato sencillo acumula Ether cuando llamas a su función 'payMe', y el total lo mantenemos en la variable 'amount'.
pragma solidity ^0.4.24;
contract Sponger { uint public amount = 0; function payMe() public payable { amount += msg.value; } }
Por otro lado, un contrato puede definir una función anónima, sin parámetros y sin poder retornar nada, que se llama fallback function, que se invoca cuando se llama al contrato con un identificador de función no válido. Si no se declara esta función fallback, en este escenario, se desencadenaría una excepción.
El cuerpo de esta función anónima, no debería de realizar más que la emisión de un evento, ya que no puede consumir más de 2300 gas, cantidad pequeña para cualquier lógica que se quiera implementar.
Si a esta función la añadimos el modificador payable entonces el contrato puede recibir también Ether. Es decir, se ejecutará esta función cada vez que el contrato reciba Ether sin ninguna llamada de función.
pragma solidity ^0.4.24;
contract Rejection { event EventFallback(address who); // Al no haber más funciones, esta fallback function será llamada // por todos los mensajes enviados a este contrato. // Pero lanzará una excepción si se la envía Ether, ya que no posee // el modificador 'payable' function() public { emit EventFallback(msg.sender); } }
contract Sink { // Este contrato en cambio guarda todo el Ether enviado a él, sin forma de recuperarlo function() public payable { } }
Eventos
Los eventos se declaran en un contrato como una función sin cuerpo, y posteriormente se lanzan (se emiten) dentro de las funciones. Es el mecanismo que tiene Solidity de informar a la aplicación (por ejemplo desde JavaScript) que llama a la función del contrato , del estado de la ejecución. Dicho de otra manera, es el medio por el cual el contrato informa a las aplicaciones externas (en vez de que tengan que realizar 'polling' por ejemplo).
pragma solidity ^0.4.24; contract Garage {
struct Car { bytes32 registration; uint timestamp; } Car[] cars;
event EventRegister(bytes32 indexed _registration, uint _timestamp);
function register(bytes32 registration, uint timestamp) public { Car memory myCar = Car(registration, timestamp); cars.push(myCar); emit EventRegister(registration, timestamp); }
}
Como vemos se declaran con event y se lanzan con emit aunque en las versiones anteriores de Solidity no era necesario poner esta última palabra.
Además los parámetros del evento los podemos marcar con indexed que implica que podrán ser usados como criterios de búsqueda o filtrado; solamente se permite un máximo de 3 parámetros indexados.
La información del evento y sus valores se almacenan como parte de la transacción dentro de los bloques y es lo que comúnmente denominamos registro de eventos o log.
Compilación
Antes de finalizar, vamos a profundizar un poco más en lo que expusimos al principio sobre el proceso de compilación y despliegue de Smart Contracts.
Cuando ejecutamos una compilación de Solidity, obtenemos un fichero JSON que contiene esencialmente:
- Bytecode de la EVM (códigos de operación). Hay más de 100 op-codes. Un extracto:
- Definición de la ABI (Application Binary Interface).
Que contiene todos los metadatos de las funciones y eventos del contrato. Esta ABI es necesaria tanto para la invocación de contratos como para el despliegue.
Conclusiones
En este punto y al final de este post podemos concluir, que las optimizaciones son importantes en estos Sistemas: Los ciclos de reloj y el almacenamiento cuestan Ether (coste de gas) y un uso ineficiente de ambos provocará que el código vaya lento y que tarde más en sincronizar la red.
Truffle
Para los lectores que quieran seguir investigando y desarrollando, y no quieran esperar a la siguiente entrega, os animo a seguir el tutorial y realizar la aplicación Tienda de animales.
En ella se utiliza la omnipresente herramienta Truffle que esencialmente es un marco de trabajo para escribir, compilar, desplegar y testar tanto Smart Contracts como DApps.
Ayuda en la Integración Continua pues se basa en el proceso de construcción básico: Build/Compile + Test + Deploy/Migrate.
También podemos aprovechar y utilizar Remix, que es un IDE basado en Web que nos permite escribir Smart Contracts, depurarlos, y también desplegarlos.
Para estar al día de siguientes posts de la serie y otros temas, ¡síguenos en Twitter!