
Blockchain y los contratos inteligentes siguen siendo un tema candente entre los desarrolladores y técnicos. Hay mucha investigación y discusión sobre su futuro y hacia dónde se mueve todo y hacia dónde conducirá. En Waves Platform tenemos nuestra propia opinión sobre lo que deberían ser los contratos inteligentes, y en este artículo te diré cómo los hicimos, qué problemas encontramos y por qué no son como los contratos inteligentes de otros proyectos de blockchain (en primer lugar Ethereum).
Este artículo también es una guía para aquellos que desean comprender cómo funcionan los contratos inteligentes en la red de Waves, tratar de escribir su propio contrato y familiarizarse con las herramientas que los desarrolladores ya tienen a su disposición.
¿Cómo llegamos a tal vida?
A menudo nos preguntaron cuándo obtuvimos contratos inteligentes, porque a los desarrolladores les gustó la facilidad de trabajar con la red, la velocidad de la red (gracias a Waves NG ) y el bajo nivel de comisiones. Sin embargo, los contratos inteligentes brindan mucho más espacio para la imaginación.
Los contratos inteligentes se han vuelto muy populares en los últimos años debido a la difusión de la cadena de bloques. Aquellos que se han encontrado con la tecnología blockchain en su trabajo, cuando mencionan contratos inteligentes, generalmente piensan en Ethereum y Solidity. Pero hay muchas plataformas blockchain con contratos inteligentes, y la mayoría de ellas simplemente repitieron lo que hizo Ethereum (máquina virtual + su propio lenguaje de contrato). En este repositorio hay una lista interesante con diferentes lenguajes y enfoques.
¿Qué es un contrato inteligente?
En un sentido amplio, un contrato inteligente es un protocolo diseñado para respaldar, verificar y hacer cumplir los términos de una transacción o la ejecución de contratos entre las partes. La idea fue propuesta por primera vez en 1996 por Nick Szabo, pero los contratos inteligentes se han vuelto populares solo en los últimos años.
Desde un punto de vista técnico (que nos interesa más), un contrato inteligente es un algoritmo (código) que se ejecuta no en un servidor o computadora, sino en muchos (o todos) nodos en la red blockchain, es decir. descentralizado
Como funciona
El primer prototipo de un contrato inteligente en la cadena de bloques se considera correctamente Bitcoin Script, incompleto por Turing, un lenguaje basado en pila en la red Bitcoin. No hay un concepto de cuenta en Bitcoin; en cambio, hay entradas y salidas. En Bitcoin, al realizar una transacción (crear una salida), es necesario hacer referencia a la transacción de recepción (entrada). Si está interesado en los detalles técnicos del dispositivo Bitcoin, le recomiendo que lea esta serie de artículos . Como no hay cuentas en Bitcoin, Bitcoin Script determina en qué caso se puede gastar una u otra salida.
Ethereum ofrece muchas más funciones, como Proporciona Solidity, un lenguaje completo de Turing que se ejecuta en una máquina virtual dentro de cada nodo. Con un gran poder viene una gran responsabilidad, y con una amplia gama de posibilidades: un número bastante grande de restricciones, de las que hablaremos más adelante.
Ondas de contratos inteligentes
Como escribí anteriormente, a menudo nos preguntaban sobre los contratos inteligentes, pero no queríamos hacer "me gusta en el aire" o "me gusta en anyblockchainname", y hay muchas razones para esto . Por lo tanto, analizamos los casos existentes para contratos y cómo podemos ayudar a resolver problemas reales con su ayuda.
Después de analizar los escenarios de uso, descubrimos que hay 2 grandes categorías de tareas que generalmente se resuelven mediante contratos inteligentes:
- Tareas simples y directas como multisig, intercambios atómicos o custodia.
- dApps, aplicaciones descentralizadas completas con lógica de usuario. Para ser más precisos, este es un back-end para aplicaciones descentralizadas. Los ejemplos más llamativos son Cryptokitties o Bancor.
También hay un tercer tipo de contrato más popular: los tokens. En la red Ethereum, por ejemplo, la gran mayoría de los contratos de trabajo son tokens estándar ERC20. En Waves, para crear tokens no es necesario hacer contratos inteligentes, ya que son parte de la cadena de bloques en sí, y para emitir un token (con la capacidad de intercambiarlo inmediatamente en un intercambio descentralizado (DEX)) es suficiente enviar una transacción de tipo de emisión (transacción de emisión).
Para los dos tipos de tareas anteriores (por simplicidad llamaremos casos simples y complejos), los requisitos para el lenguaje, los contratos y los conceptos son muy diferentes. Sí, podemos decir que tener un lenguaje completo de Turing puede resolver problemas simples y complejos, pero hay una condición importante: el lenguaje debe ayudar a evitar errores. Este requisito también es importante para los idiomas ordinarios, y para los idiomas de contrato inteligente es especialmente importante, porque las operaciones están relacionadas financieramente, y los contratos a menudo son inmutables, y no hay forma de corregir un error de manera rápida y fácil.
Dado el tipo de tareas descritas anteriormente, decidimos movernos gradualmente y dar una herramienta para resolver problemas simples como primer paso, y dar un lenguaje tan fácil como el siguiente paso para implementar fácilmente cualquier lógica de usuario. Como resultado, el sistema resultó ser mucho más poderoso de lo que imaginamos al comienzo del viaje.
Hagamos cuentas inteligentes
Poco a poco, llegamos al concepto de cuentas inteligentes, que están diseñadas para resolver tareas principalmente simples. Su idea es muy similar a Bitcoin Script: se pueden agregar reglas adicionales a la cuenta que determinan la validez de la transacción saliente. Los principales requisitos para las cuentas inteligentes fueron:
- Máxima seguridad. Casi todos los meses puede encontrar noticias de que se encontró otra vulnerabilidad en los contratos modelo de Ethereum. Queríamos evitar esto.
- No hay necesidad de gas, por lo que la comisión es fija. Para hacer esto, el script debe ejecutarse en un período de tiempo predecible y tener límites de tamaño bastante estrictos.
Antes de continuar con los detalles técnicos de la implementación y redacción de contratos, describimos algunas características de la cadena de bloques Waves que serán importantes para una mejor comprensión:
- La cadena de bloques Waves actualmente tiene 13 tipos diferentes de transacciones.

- En la cadena de bloques Waves, no entradas y salidas (como en Bitcoin), sino cuentas (como, por ejemplo, en Nxt). Una transacción se realiza en nombre de una cuenta específica.
- Por defecto, la exactitud de una transacción está determinada por el estado actual de la cadena de bloques y la validez de la firma en nombre de la cual se envía la transacción. La representación JSON de la transacción parece bastante simple:

Como ya tenemos diferentes tipos de transacciones en la cadena de bloques, decidimos que no haríamos una entidad separada como una cuenta inteligente, sino que agregaríamos una nueva transacción que convierta una cuenta normal en una inteligente. Cualquier cuenta puede convertirse en una cuenta inteligente con reglas de validación de transacción modificadas, para esto, la cuenta simplemente debe enviar una transacción del tipo SetScriptTransaction
, que contiene el contrato compilado.
En el caso de una cuenta inteligente, el contrato es una regla de validación para cada _ transacción saliente.
¿Y qué hay del gas?
Una de las principales tareas que nos proponemos es eliminar el gas para operaciones simples. Esto no significa que no habrá comisión. Es necesario para que los mineros tengan interés en ejecutar scripts. Abordamos el tema desde el punto de vista práctico y decidimos realizar pruebas de rendimiento y calcular la velocidad de varias operaciones. Para esto, se desarrollaron puntos de referencia con JMH. Los resultados se pueden ver aquí . Las limitaciones resultantes son:
- El script debe ejecutarse más rápido que 20 operaciones de verificación de firma, lo que significa que las comprobaciones para una cuenta inteligente no serán más de 20 veces más lentas que para una cuenta normal. El tamaño del script no debe exceder los 8 KB.
- Para que sea rentable que los mineros cumplan contratos inteligentes, establecemos una comisión adicional mínima para cuentas inteligentes por un monto de 0.004 ONDAS. La comisión mínima en la red de Waves para una transacción es 0.001 WAVES, en el caso de una cuenta inteligente - 0.005 WAVES.
Lenguaje para contratos inteligentes
Una de las tareas más difíciles fue la creación de su propio lenguaje de contratos inteligentes. Tomar cualquier lenguaje existente de turing completo y adaptar (recortar) para nuestras tareas parece estar disparando desde un cañón a gorriones: además de esto, depender de la base de código de otra persona en un proyecto de blockchain es extremadamente riesgoso .
Tratemos de imaginar cuál debería ser el lenguaje ideal para contratos inteligentes. En mi opinión, cualquier lenguaje de programación debería obligar a escribir código "correcto" y seguro, es decir idealmente debería haber una manera correcta. Sí, si lo desea, puede escribir código completamente ilegible y no compatible en cualquier idioma, pero esto debería ser más difícil que escribirlo correctamente (hola PHP y JavaScript). Al mismo tiempo, el lenguaje debe ser conveniente para el desarrollo. Dado que el lenguaje se ejecuta en todos los nodos de la red, es necesario que sea lo más eficiente posible: la ejecución diferida puede ahorrar una gran cantidad de recursos. También quiero tener un sistema de tipos poderoso en el lenguaje, preferiblemente algebraico, porque ayuda a describir el contrato de la manera más clara posible y acercarse al sueño del "Código es ley". Si formalizamos nuestros requisitos un poco más, obtenemos los siguientes parámetros de idioma:
- Ser estrictamente y estáticamente escrito. La escritura fuerte elimina automáticamente muchos posibles errores de programador.
- Tenga un sistema de tipo potente para dificultarle dispararse en el pie.
- Sea perezoso para no desperdiciar los preciosos ciclos de procesador.
- Tiene funciones específicas en la biblioteca estándar para trabajar con blockchain, por ejemplo, hashes. Al mismo tiempo, la biblioteca de idiomas estándar no debe sobrecargarse, porque siempre debe haber una forma correcta.
- No tiene excepciones en tiempo de ejecución.
En nuestro lenguaje RIDE, tratamos de tener en cuenta estas características importantes, y dado que desarrollamos mucho en Scala y nos gusta la programación funcional, el lenguaje en algunos aspectos es similar a Scala y F #.
Los mayores problemas en la implementación en la práctica surgieron con el último requisito, porque si no tiene excepciones en el idioma, entonces, por ejemplo, la operación de adición tendrá que devolver una Opción , que deberá verificarse por desbordamiento, lo que definitivamente será un inconveniente para los desarrolladores. Las excepciones fueron un compromiso, pero sin la capacidad de atraparlas: si hubo una excepción, entonces la transacción no era válida. Otro problema fue transferir al lenguaje todos los modelos de datos que tenemos en la cadena de bloques. Ya he descrito que en Waves hay 13 tipos diferentes de transacciones que deben admitirse en el idioma y tener acceso a todos sus campos.
La información completa sobre las operaciones disponibles y los tipos de datos en RIDE se encuentra en la página de descripción del idioma . Entre las características interesantes del lenguaje, también podemos destacar el hecho de que el lenguaje está basado en expresiones, es decir, todo es expresión, así como la presencia de coincidencia de patrones, lo que le permite describir convenientemente las condiciones para los diferentes tipos de transacciones:
match tx { case t:TransferTransaction => t.recepient case t:MassTransferTransaction => t.transfers case _ => throw }
Si está interesado en aprender cómo se organiza el trabajo con el código RIDE, debe consultar el documento técnico , que describe todas las etapas del trabajo con un contrato: análisis, compilación, deserialización, cálculo de la complejidad y ejecución del script. Las dos primeras etapas: el análisis y la compilación se realizan fuera de la cadena, solo el contrato compilado en base64 ingresa a la cadena de bloques. La deserialización, el cálculo y la ejecución de la complejidad se realizan en cadena y varias veces en diferentes etapas:
- Cuando recibe una transacción y la agrega a UTX, de lo contrario, el nodo de la cadena de bloques aceptará la transacción, por ejemplo, a través de la API REST, pero nunca entrará en el bloque.
- Cuando se forma un bloque, el nodo de minería valida las transacciones y se requiere el script.
- Tras la recepción por parte de los nodos no mineros de un bloque y la validación de las transacciones incluidas en el mismo.
Cada optimización en el trabajo con contratos se vuelve valiosa, ya que se realiza muchas veces en muchos nodos de la red. Ahora los nodos Waves se ejecutan silenciosamente en máquinas virtuales por $ 15 en DigitalOcean, a pesar del aumento en las cargas de trabajo después del lanzamiento de cuentas inteligentes.
Cual es el resultado?
Ahora veamos qué tenemos en Waves. Escribamos nuestro primer contrato propio, que sea el contrato multisig estándar 2-de-3. Para escribir un contrato, puede usar el IDE en línea (ajuste para el idioma, un tema para un artículo separado). Cree un nuevo contrato vacío (Nuevo → Contrato vacío).
En primer lugar, anunciaremos las claves públicas de Alice, Bob y Cooper, que controlarán la cuenta. Necesitará 2 de sus 3 firmas:
let alicePubKey = base58'B1Yz7fH1bJ2gVDjyJnuyKNTdMFARkKEpV' let bobPubKey = base58'7hghYeWtiekfebgAcuCg9ai2NXbRreNzc' let cooperPubKey = base58'BVqYXrapgJP9atQccdBPAgJPwHDKkh6A8'
La documentación describe la función sigVerify
, que le permite verificar la firma de la transacción:

Los argumentos de la función son el cuerpo de la transacción, la firma verificada y la clave pública. Un objeto tx
está disponible en el contrato en el ámbito global, en el que se almacena la información de la transacción. Este objeto tiene un campo tx.bodyBytes
que contiene los bytes de la transacción que se envía. También hay una variedad de tx.proofs
, que almacena firmas, que pueden ser hasta 8. Vale la pena señalar que, de hecho, puede enviar no solo firmas a tx.proofs
, sino cualquier otra información que pueda ser utilizada por el contrato.
Podemos asegurarnos de que se presenten todas las firmas y que estén en el orden correcto mediante 3 líneas simples:
let aliceSigned = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey )) then 1 else 0 let bobSigned = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey )) then 1 else 0 let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey )) then 1 else 0
Bueno, el último paso será verificar que se envíen al menos 2 firmas.
aliceSigned + bobSigned + cooperSigned >= 2
El contrato completo de 2 de 3 firmas múltiples se ve así:
# let alicePubKey = base58'B1Yz7fH1bJ2gVDjyJnuyKNTdMFARkKEpV' let bobPubKey = base58'7hghYeWtiekfebgAcuCg9ai2NXbRreNzc' let cooperPubKey = base58'BVqYXrapgJP9atQccdBPAgJPwHDKkh6A8' # let aliceSigned = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey )) then 1 else 0 let bobSigned = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey )) then 1 else 0 let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey )) then 1 else 0 # , 2 aliceSigned + bobSigned + cooperSigned >= 2
Tenga en cuenta: no hay palabras clave como return
en el código, porque la última línea ejecutada se considera el resultado del script, y es por eso que siempre debe devolver true
o false
En comparación, el contrato de firma múltiple Ethereum común parece mucho más complicado . Incluso variaciones relativamente simples se ven así:
pragma solidity ^0.4.22; contract SimpleMultiSig { uint public nonce; // (only) mutable state uint public threshold; // immutable state mapping (address => bool) isOwner; // immutable state address[] public ownersArr; // immutable state // Note that owners_ must be strictly increasing, in order to prevent duplicates constructor(uint threshold_, address[] owners_) public { require(owners_.length <= 10 && threshold_ <= owners_.length && threshold_ >= 0); address lastAdd = address(0); for (uint i = 0; i < owners_.length; i++) { require(owners_[i] > lastAdd); isOwner[owners_[i]] = true; lastAdd = owners_[i]; } ownersArr = owners_; threshold = threshold_; } // Note that address recovered from signatures must be strictly increasing, in order to prevent duplicates function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) public { require(sigR.length == threshold); require(sigR.length == sigS.length && sigR.length == sigV.length); // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191 bytes32 txHash = keccak256(byte(0x19), byte(0), this, destination, value, data, nonce); address lastAdd = address(0); // cannot have address(0) as an owner for (uint i = 0; i < threshold; i++) { address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]); require(recovered > lastAdd && isOwner[recovered]); lastAdd = recovered; } // If we make it here all signatures are accounted for. // The address.call() syntax is no longer recommended, see: // https://github.com/ethereum/solidity/issues/2884 nonce = nonce + 1; bool success = false; assembly { success := call(gas, destination, value, add(data, 0x20), mload(data), 0, 0) } require(success); } function () payable public {} }
El IDE tiene una consola incorporada que le permite compilar inmediatamente un contrato, implementarlo, crear transacciones y ver el resultado de la ejecución. Y si desea trabajar seriamente con los contratos, le recomiendo que busque en las bibliotecas los diferentes idiomas y el complemento para Visual Studio Code .
Si tiene picazón en las manos, al final del artículo encontrará los enlaces más importantes para comenzar la inmersión.
El sistema es más poderoso que el lenguaje.
La cadena de bloques Waves tiene tipos de datos especiales para almacenar datos: transacciones de datos . Funcionan como almacenamiento de valor clave asociado con una cuenta, es decir, en cierto sentido, este es el estado de la cuenta.

La fecha de transacción puede contener cadenas, números, valores booleanos y conjuntos de bytes de hasta 32 KB por clave. Un ejemplo de trabajo con Transacciones de datos, que le permite enviar una transacción solo si el almacenamiento del valor clave de la cuenta ya contiene el número 42 en la clave key
:
let keyName = "key" match (tx) { case tx:DataTransaction => let x = extract(getInteger(tx.sender, keyName)) x == 42 case _ => false }
Gracias a Data Transaction, las cuentas inteligentes se convierten en una herramienta extremadamente poderosa que le permite trabajar con oráculos, administrar el estado y describir convenientemente el comportamiento.

Este artículo describe cómo puede implementar NFT (tokens no fungibles) mediante transacciones de datos y un contrato inteligente que controla el estado. Como resultado, el estilo de la cuenta contendrá entradas del formulario:
+------------+-----------------------------------------------+ | Token Name | Owner Publc Key | +------------+-----------------------------------------------+ | "Token #1" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | | "Token #2" | "3tNLxyJnyxLzDkMkqiZmUjRqXe1UuwFeSyQ14GRYnGL" | | "Token #3" | "3wH7rENpbS78uohErXHq77yKzQwRyKBYhzCR9nKU17q" | | "Token #4" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | | "Token #5" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | +------------+-----------------------------------------------+
El contrato NFT en sí mismo parece extremadamente simple:
match tx { case dt: DataTransaction => let oldOwner = extract(getString(dt.sender, dt.data[0].key)) let newOwner = getBinary(dt.data, 0) size(dt.data) == 1 && sigVerify(dt.bodyBytes, dt.proofs[0], fromBase58String(oldOwner)) case _ => false }
Que sigue
El desarrollo adicional de los contratos inteligentes de Waves es Ride4DApps , que permitirá llamar contratos en otras cuentas, y un lenguaje (o sistema) completo de Turing que le permitirá resolver todo tipo de tareas, activar otras tareas, etc.
Otra dirección interesante para el desarrollo de contratos inteligentes en el ecosistema de Waves es Smart Assets, que funciona según un principio similar: contratos de Turing incompletos que se relacionan con el token. El contrato controla las condiciones bajo las cuales se pueden completar las transacciones de tokens. Por ejemplo, con su ayuda, será posible congelar tokens a una cierta altura de blockchain o prohibir el intercambio de tokens p2p. Puede leer más sobre activos inteligentes en el blog .
Bueno, al final volveré a dar una lista de lo que será necesario para comenzar a trabajar con contratos inteligentes en la red de Waves.
- La documentación
- IDE con consola
- Libro blanco para los más curiosos.