Despliegue de web ÐApp con Quorum+Angular+Python+Flask en un VPS con Ubuntu 16.04 [2]

En la segunda parte de la serie de artículos que cubre el desarrollo de una web ÐApp se centra en las funcionalidades que tendrá dicha aplicación y que en gran medida estará gestionada por los contratos escritos en Solidity.

Despliegue de web ÐApp con Quorum+Angular+Python+Flask en un VPS con Ubuntu 16.04
[1] [Quorum]
[2] [Solidity]
[3] [Python]
[4] [Angular]

En la primera parte de la serie de artículos se cubre la configuración de dos nodos Quorum basados en RAFT. Cada nodo representa a un usuario de la plataforma que bien presta libros o recibe en préstamos libros. Esta segunda parte se enfoca en la construcción de los smart contracts que dotarán de funcionalidad a la aplicación.

Backend – Solidity

La gestión de funciones internas que realiza una web ÐApp en la máquina virtual de Ethereum (EVM) se definen en los smart contracts. De entre los lenguajes usados para los smart contracts, el dominante es Solidity (con semejanza al Javascript).

Una guía divertida y con fundamento con la que aprender Solidity es CryptoZombies. En cada una de las lecciones se describen parámetros y trucos que el usuario debe programar de forma correcta. De una forma gradual y en poco tiempo un usuario salta de los aspectos más básicos a la creación de tokens basados en ERC721. Los contratos en este caso se basan en gran medida en el desarrollo de CryptoZombies y de OpenZeppelin.

GiveLibAck gira alrededor de la cesión o préstamo de libros. La funcionalidad descrita en el apartado de Procesos de negocio se consigue con una serie de contratos basados en herencias de otros contratos. El código que se detalla es una versión inicial que cubre la funcionalidad básica requerida sin cubrir todas las casuísticas por mor de desviar la atención a temas más relacionados con pruebas y optimización que a la construcción de un proyecto. Los contratos se distribuyen en cuatro archivos.

  • Base.sol: Gestión y transferencia del contrato y uso de contratos de Zeppelin para la gestión de overflow/underflow en funciones matemáticas así como uso de cuentas inexistentes (0x0)
  • Bookshelf.sol: Creación de libros
  • ERC721.sol: Guía de funciones para generación de tokens únicos no fungibles
  • GiveLibAck.sol: Gestión de los libros incluyendo su petición y entrega/retorno y transferencia basado en ERC721

El desarrollo de los smart contracts se puede realizar en un único contrato o bien usando herencia se pueden importar funciones definidas en otros contratos. Se opta por la segunda opción por claridad a la hora de programar. Es también preferible separar partes del código si se prevé que algunas partes serán inmutables tales como definiciones de contratos o funciones de bajo nivel sobre las que las funciones del smart contract de la aplicación se basará. A veces se pueden desplegar contratos con las funciones de base que posteriormente se invocan en los contratos. En este ejemplo se define Base.sol como punto de partida del resto de contratos y la versión usada es 0.4.23.

pragma solidity ^0.4.23;
Base.sol

Se define el contrato Base que tiene como elemento una dirección definida como pública y asignada a la variable owner.

address public owner;

Es recomendable que todo contrato tenga un propietario, que es el encargado de desplegar el mismo en la red. En este caso, el propietario es la cuenta que se usa para desplegar el contrato y que aparece en el constructor del objeto contrato Base. En msg.sender se almacena la dirección con la que se interactúa con el contrato. En este caso será la que despliegue el mismo.

constructor() {
owner = msg.sender;
if(owner == address(0)) throwError('[Base constructor] Owner address is 0x0');
}

En muchos ejemplos el constructor del objeto contrato se define como función con el mismo nombre

function Base(){}

aunque debiera estar definido con la cláusula

constructor(){}

Junto con la definición de la variable owner, se usa la librería SafeMath para las operaciones matemáticas de enteros sin signo de 256 (uint256).

using SafeMath for uint256;

Fácilmente se pueden crear contratos en Solidity para aplicaciones. La madurez del lenguaje y la máquina EVM no es tal como para bloquear posibles desbordes de pila o asignaciones no recomendables (ver las recomendaciones y precauciones).

Es por eso que se recomienda seguir las indicaciones de OpenZeppelin y otras organizaciones a la hora de implementar funciones y funcionalidades para no tener sustos entre otros como doble gasto, intrusiones o envíos al olvido de fondos en la cuenta 0.

Como se observa, en Solidity pueden definirse contratos y también librerías, que aún no siendo contratos disponen de funciones que pueden invocarse. La librería SafeMath para uint256 (y sus variantes para uint16, uint32 y uint64) redefinen las operaciones de suma, resta, multiplicación y división de enteros sin signo aplicando comprobaciones y restricciones para no tener desbordamientos comentados anteriormente. A partir de ese momento, dicha notación es la que debe usarse para modificar las variables de enteros con add, mul, div y sub. Así pues, en vez de hacer

i++;

se debe usar

i = i.add(1);

La librería Math, también de OpenZeppelin proporciona las funciones para max y min para uint64 y uint256.

El contrato Base tiene la función de transferencia de la propiedad del contrato (transferOwnership), la captura del bloque actual (timestamp; no confundir con la función timestamp() de UNIX al estar basados en la marca de tiempo del bloque actual) y para la alerta por errores (throwError).

Cuando se ejecutan las funciones transferOwnership se lanza el evento OwnershipTransferred, que se ha definido a nivel del objeto contrato Base. Lo mismo sucede con throwError y su evento Error.

En este contrato Base se definen dos modificadores: onlyOwner y validDestinationAddress. Los modificadores son funciones que deben cumplirse para poder permitir la ejecución de la función que la incluye. Son la alternativa a la llamada require() en cuanto dicho requisito es aplicable a más de una función.

Los modificadores pueden incluirse dentro de las funciones aunque es mejor hacerlo a nivel de parámetro de la llamada de la función como en el caso de transferOwnership.

function transferOwnership(address newOwner) public onlyOwner validDestinationAddress(newOwner) {
    emit OwnershipTransferred(owner, newOwner);
    owner = newOwner;
}

Al incluir un modificador como parámetro, debe añadirse como última línea de código de la función modificadora ‘_;’ como callback para que una vez ejecutada vuelva el puntero de ejecución a la función que la ha llamado.

modifier onlyOwner() {
    require(msg.sender == owner);
    _;
}

En el caso de validDestinationAddress, al tener el modificador un parámetro, éste debe pasarse al instanciarse como parámetro modificador de transferOwnership.

modifier validDestinationAddress( address to ) {
      require(to != address(0));
      require(to != address(this));
      _;
}

Así pues, para la transferencia de la propiedad de un contrato sólo podrá ejecutarse por el propietario del mismo y a una dirección válida.

En resumen, en Base.sol se define el contrato y su propiedad así como las librerías para aritmética de enteros.

Bookshelf.sol

Los libros almacenados en el sistema conservan el creador (que es el propietario inicial), el ISBN y el número de veces que se ha prestado. Por otro lado, una relación del libro y la cuenta (dirección) que actualmente lo posee permite saber de quién es cada libro así como si están prestados a otros usuarios (direcciones). Puede darse el caso que un usuario ceda la propiedad del libro a otro usuario, con lo que se actualizaría el valor del creador a la vez que el poseedor. Un libro, pues, tiene un propietario (owner) y un poseedor (borrower).

Bookshelf.sol, que hereda de Base.sol,

import "./Base.sol";
...
contract BookShelf is Base{
...
}

define el objeto Book que es una estructura de un libro conteniendo el ISBN (isbn), la dirección del creador (creator) y un contador de veces que se ha prestado el libro (lended_num). No se añaden más campos como el nombre del libro al no ser estrictamente necesario que se almacene en la cadena de bloques guardándose en una base de datos local o recuperando la información desde un servidor externo (web u oráculo).

Los datos en la EVM se guardan como almacenamiento (storage), memoria (memory) o de pila (stack). Como somera diferenciación, el almacenamiento se guarda en la cadena de bloques e implica gasto de gas en las operaciones de escritura. La información en memoria sólo es accesible durante la ejecución del smart contract y, por lo tanto, no es persistente. El coste de acceso o escritura es bajo en comparación al almacenamiento. La pila tiene unos costes similares a la memoria pero sólo permite 1024 elementos, siendo los primeros 16 realmente accesibles. En caso de llenar la pila, el contrato fallará.

Partiendo de la estructura Book se genera un array inicial de libros partiendo de Book llamado books y se definen cuatro mapeos de clave-valor:

  • bookToOwner devuelve la dirección del poseedor de un id de un libro
  • ownerBooksCount devuelve la cantidad de libros que están en posesión de una dirección
  • bookToBorrower devuelve la dirección del usuario que actualmente tiene el libro en préstamo
  • borrowerBooksCount devuelve la cantidad de libros que se le han prestado en el momento actual

Mientras que bookToOwner y bookToBorrower son públicos, ownerBooksCount y borrowerBookCount no lo son. Es por eso que al compilar el contrato no aparecen esas funciones.

Como funciones definidas está _createBook, interna, que incluye en el array de Book el nuevo libro con sus características, la asignación en el mapeo del libro a la dirección de su creador y el recuento de libros de la misma dirección. Finalmente invoca al evento NewBook con el identificador del libro y su ISBN.

createBook es la función pública que fuerza que el usuario tenga menos de cien libros registrados. De esta manera se pueden gestionar limitaciones según el tipo de usuario o historial del mismo.

ERC721.sol

La definición de los tokens según la propuesta ERC721, cuya flamante inicial implementación fue CryptoKitties creando una gran disrupción en la red de Ethereum. La diferencia con respecto a los token ERC20 es que no son fungibles ni divisibles sino que son únicos. Los tokens basados en la propuesta ERC721 cubre la representación de objetos únicos (obras de arte, propiedades inmobiliarias, equipos electrónicos, animales de compañía, jamones curados) u objetos coleccionables (autógrafos, tarjetas de collecionismo, sellos). Así como los wallets permiten la gestión de los tokens ERC20, no todos los gestores de wallets permiten los tokens ERC721.

En ERC721.sol se definen los tipos de funciones que se permiten y sus parámetros siguiendo la propuesta. Dichas funciones serán reutilizadas en GiveLibAck.sol.

GiveLibAck.sol

Este es el contrato que hereda de todos los contratos y librerías definidas anteriormente. Al heredar de Bookshelf, también hereda de Base.

Los procesos directamente relacionados con la gestión de los préstamos de libros y su transferencia se resumen en la creación de un libro, su préstamo a otro usuario, la devolución de un préstamo, la transferencia de un libro, la aprobación para la transferencia de un libro y la aceptación de la aprobación de transferencia de un libro. Los procesos y sus funciones se detallan a continuación.

GiveLibAck: procesos

En el contrato GiveLibAck se define la variable lendingfee con valor 0.001Ether como medida para prevenir que se haga un uso masivo de peticiones en la red. Dicho valor podría guardarse como escrow y entregarse de vuelta enteramente al depositante al devolver el libro. El valor puede ser actualizado por el propietario del contrato a través de setLendingFee. En este ejemplo se deja la funcionalidad para desarrollar en una fase posterior para mantener un código inicial de fácil comprensión y con la funcionalidad esperada.

Se crea una relación entre aprobaciones de transferencias de libros y las direcciones aprobadas con nombre bookApprovals.

Existe un único modificador, theOwnerOf, asegura que el libro es actual propietario del libro. Dicho modificador será usado para la operativa de préstamo de libros y no debe confundirse con onlyOwner que es relativo a la propiedad del contrato. theBorrowerOf confirma si el libro está cedido y quién es el actual receptor.

Las funciones anteriormente definidas en ERC721.sol se reutilizan para dotar de la funcionalidad esperada con respecto a la transferencia de la propiedad de los libros. Dichas funciones no aplican para la cesión temporal de los libros.

  • balanceOf se basa en ownerBooksCount para dar la cantidad de tokens (libros) que posee.
  • ownerOf se basa en bookToOwner para devolver la dirección del actual propietario del token (libro)

Ambas funciones se definen como view al implicar sólo lectura.

Para obtener los libros que dispone cada usuario es preferible repasar todos los libros registrados en books en vez de guardar la relación por motivos de eficiencia de escritura y consumo de gas en la operación de actualización (cuando un usuario cede un libro o cambia de propietario).

Así pues la función getBooksByOwner crea en memoria el resultado yendo por todos los libros en el array books y devuelve los ids de los libros que son propiedad de la cuenta pasada como parámetro.

De igual modo se estructura para conocer el estado de préstamos de libros y receptores de los mismos con getBooksByBorrower. Con este planteamiento, no es necesario crear una variable dentro del objeto Book que contenga el estado del libro (en préstamo o no) sino que se obtiene de la variable de listado de libros y cuentas a las que está prestado.

Para prestar un libro el proceso se inicia con lendBook. Hay diferentes restricciones en la función:

  • el libro no puede estar prestado a otra dirección
  • el destinatario no puede ser la misma dirección que el prestador
  • el destinatario no puede ser una dirección inválida (0x0)

El propietario del libro define el libro a prestar así como la dirección del prestatario. Se incluye dicho libro en la relación de libros prestados, se suma un préstamo al contador y un libro prestado al prestatario.

La devolución del libro se vehicula con returnBook. El único parámetro necesario es el identificador del libro ya que se conoce quién devuelve el libro (msg.sender) y a quién se le devuelve (bookToOwner). Se borra el contenido de asignación de la dirección del prestatario al libro (bookToBorrower) con la función delete. Se podrían combinar las dos funciones de préstamo y devolución para optimizar cantidad de código del contrato aunque se han separado para dotar de claridad y comprensión.

La cesión de libros se basa en la propuesta ERC-721. Para ceder libros existen dos alternativas:

  • transfer: el poseedor del libro cede/devuelve el libro
  • approve+takeOwnership: el poseedor aprueba la cesión de un libro y el receptor invoca el cambio de propiedad

Para que transfer se ejecute correctamente, el usuario que la ejecuta debe tener en posesión dicho libro. Lo mismo aplica para approve en la que el libro se incluye en un listado de clave-valor bookApprovals. Cuando un usuario ejecuta takeOwnership, se busca tal libro en bookApprovals para realizar la transferencia.

Pruebas

El código presentado anteriormente debe comprobarse que funciona como se ha definido. Una forma de comprobarlo es usando Remix de la misma manera descrita en el despliegue de un nodo RSK.

Para poder desplegar, se deben crear los archivos en el entorno. Una vez todos los archivos han sido creados, se selecciona el archivo de más alto nivel que en este caso es GiveLibAck.sol. Este archivo heredará del resto y, por lo tanto, serán incluídos a la hora de compilación.

Cada uno de los cuatro archivos de contratos deben crearse en el entorno dándole al símbolo (+) del lateral superior izquierda. En cada archivo nuevo, se procede a copiar el código desarrollado y darle el nombre adecuado.

Los archivos se listan en la carpeta browser.

GiveLibAck – Remix: Carga de archivos

Una vez se disponen los cuatro archivos y teniendo seleccionado GiveLibAck.sol, se procede a la compilación de los mismos (Start to compile). Al partir de GiveLibAck.sol, añadirá el resto de contratos necesarios y heredados. Una vez compilado, en la pestaña ‘Run’, se selecciona como entorno JavaScript VM que no requiere de MetaMask ni de interacción con Ropsten o Rinkeby al correr en local en el navegador del cliente. Se deja el resto de parámetros tal y como se proveen.

Las cinco cuentas que aparecen tienen como saldo 100 Ethers, suficientes para la ejecución de las transacciones de prueba.

GiveLibAck – Remix: cuentas

Una vez tenemos el contrato desplegado, se deben realizar una batería de pruebas para comprobar el correcto funcionamiento. Sin ser exhaustivos, se detallan algunas pruebas a modo ilustrativo en el entorno remix.

GiveLibAck – Remix: desplegando el contrato

Del siguiente bloque se selecciona GiveLibAck del listado presente para su despliegue. En esa lista salen todos los contratos y librerías que se han compilado entre los que aparecen Base, SafeMath, Bookself, etc. Apretando el botón de Deploy, se crea el contrato con la dirección seleccionada del recuadro inicial (en este caso 0xca35b7d915458ef540ade6068dfe2f44e8fa733c) y se despliega en la red con una dirección (0xed6d2178fea2354065b4eb85b114357bc253949fee972615c434d8feebfe1be7). Con esa dirección se puede instanciar el contrato a través de interfaces como Web3. Remix instancia automaticamente el contrato.

Una vez desplegado, aparecen todas las funciones públicas disponibles de todos los contratos compilados. Las de color rojizo implican escritura en la cadena de bloques y, por ende, gasto en gas y las azuladas son de lectura y, por lo tanto, gratuitas. En el lado derecho de cada función se muestran los parámetros de entrada esperados de cada función. La obligatoriedad de los parámetros se define a nivel de cada función, que puede gestionar la falta de un parámetro de entrada en caso necesario.

En el momento que se desee, se puede cambiar la cuenta (Account) en uso de entre las 5 disponibles con 100 Ethers cada una en el bloque superior.

La primera función que se puede probar es transferOwnership pasando como parámetro una dirección incorrecta (0, 0x0 o inexistente como 0x1234) o con una dirección diferente a la usada para la creación del contrato. Al apretar el botón de la función, la VM nos devolverá un error en dichas ejecuciones y en el caso de querer cambiar el propietario sin serlo, el gas usado por la transacción se consumirá de la cuenta usada. En el desarrollo propuesto usando nodos Quorum, el uso de gas es irrelevante si se define como cero a nivel de configuración.

Desde la cuenta usada para desplegar los contratos (0xca35b7d915458ef540ade6068dfe2f44e8fa733c) se puede transferir la gestión del contrato hacia otra cuenta (0x14723a09acff6d2a60dcdf7aa4aff308fddc160c).

GiveLibAck – Remix: transferOwner

Cambiando el propietario del contrato se observa la publicación del ‘event’ «OwnershipTransferred’.

[
{
"from": "0xbbf289d846208c16edc8474705c748aff07732db",
"topic": "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0",
"event": "OwnershipTransferred",
"args": {
"0": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
"1": "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
"previousOwner": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
"newOwner": "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
"length": 2
}
}
]

En cuanto a la creación de libros, la ejecución de createBook desde la cuenta #0 (0xca35b7d915458ef540ade6068dfe2f44e8fa733c) con los detalles de 2666 de Roberto Bolaño con devuelve el índice del libro creado con el evento NewBook.

GiveLibAck – Remix: createBook

La respuesta al ejecutarse muestra que se le asigna el id cero (libro #0).

[
{
"from": "0x9240ddc345d7084cc775eb65f91f7194dbbb48d8",
"topic": "0xf7e1dd4a7b905ff00f16c1281183f49592b87c112782803b7db506590de8f8d2",
"event": "NewBook",
"args": {
"0": "0",
"1": "9788420423920",
"bookId": "0",
"isbn": "9788420423920",
"length": 2
}
}
]

Como siguiente prueba, se crean un par de libros más desde la primera cuenta (0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c), otro con la segunda cuenta (0x14723a09acff6d2a60dcdf7aa4aff308fddc160c) y el cuarto libro con la primera de nuevo.

Ejecutando la búsqueda de los libros que son de la primera cuenta se observan los tres creados.

GiveLibAck – Remix: getBooksByOwner
{ "0": "uint256[]: 0,1,3" }

El libro #2 es de la segunda cuenta.

GiveLibAck – Remix: bookToOwner #2

Muestra que es de la segunda cuenta.

{
	"0": "address: 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C"
}

Se ejecuta el préstamo del libro de Roberto Bolaño (libro #0) al segundo usuario (segunda cuenta).

GiveLibAck – Remix: préstamo del libro #0

Se instancia el evento Lend con los valores de origen, destino y identificador del libro prestado.

[
{
"from": "0xc5266ca19406253bd9659c5689cc6dfcfd4633a8",
"topic": "0x11dd72a5d477527ee1cbe309212bb368745a98cf1dc3befcac7eef4988a957c2",
"event": "Lend",
"args": {
"0": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
"1": "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
"2": "0",
"from": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
"to": "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
"tokenId": "0",
"length": 3
}
}
]

bookToBorrower(0) devuelve

{ "0": "address: 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C" }

books(0) devuelve ya el incremento del número de préstamos del libro

{ "0": "uint64: isbn 9788420423920", "1": "address: creator 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c", "2": "uint16: lended_num 1" }

getBooksByBorrower(0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C) devuelve el libro que tiene en préstamo

{ "0": "uint256[]: 0" }

La devolución se realiza desde la segunda cuenta (0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C) mediante returnBook(0), cuya ejecución devuelve el evento Return.

[
{
"from": "0xc5266ca19406253bd9659c5689cc6dfcfd4633a8",
"topic": "0x75457583d91e0597e8497ed04c08e88e3fb24d8b586a60de172cef7186b67318",
"event": "Return",
"args": {
"0": "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
"1": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
"2": "0",
"from": "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
"to": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
"tokenId": "0",
"length": 3
}
}
]

Ahora que el libro está de vuelta con el propietario, bookToBorrower(0) devuelve la dirección 0x0. De la misma manera, el array de préstamos de la segunda cuenta se encuentra vacía. Comprobando con getBooksByBorrower(0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C) está vacío.

{ "0": "uint256[]: " }

Se realiza la prueba de transferencia permanente del libro #0 desde la primera cuenta (0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c) a la segunda cuenta con transfer (siguiendo la nomenclatura de ERC721).

GiveLibAck – Remix: transfer del libro #0

La ejecución se confirma satisfactoriamente.

[
{
"from": "0x692a70d2e424a56d2c6c27aa97d1a86395877b3a",
"topic": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"event": "Transfer",
"args": {
"0": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
"1": "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
"2": "0",
"_from": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
"_to": "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
"_tokenId": "0",
"length": 3
}
}
]

Ejecutando de nuevo el listado de libros de la cuenta #0 ya no aparece el libro #0. getBooksByOwner(0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c) devuelve

{ "0": "uint256[]: 1,3" }

El código presentado anteriormente es una guía inicial para la construcción de una web ÐApp de préstamo de libros. Pueden implementarse funcionalidades como donación del libro a otro usuario, pago por préstamo, etc. El código además de probarse debería analizarse para asgurar su corrección así como optimización de recursos usados.

Disponiendo de una red con nodos Quorum y un smart contract que permite la cesión temporal de libros, se puede proceder a la construcción de la interfaz backend de Python sobre un CherryPy+Flask.