Especificación de Scilla en Maude Specification of Scilla in Maude - Trabajo de Fin de Máster Curso 2020-2021 Autor - E-Prints Complutense
←
→
Transcripción del contenido de la página
Si su navegador no muestra la página correctamente, lea el contenido de la página a continuación
Especificación de Scilla en Maude Specification of Scilla in Maude Trabajo de Fin de Máster Curso 2020–2021 Autor Laura de la Fuente Lorenzo Director Adrián Riesco Rodríguez Máster en Ingeniería Informática Facultad de Informática Universidad Complutense de Madrid
Especificación de Scilla en Maude Specification of Scilla in Maude Trabajo de Fin de Máster en Ingeniería Informática Departamento de Sistemas Informáticos y Computación Autor Laura de la Fuente Lorenzo Director Adrián Riesco Rodríguez Convocatoria: Junio/Julio 2021 Calificación: 7,5 Máster en Ingeniería Informática Facultad de Informática Universidad Complutense de Madrid 13 de julio de 2021
Resumen Especificación de Scilla en Maude Este trabajo busca proporcionar una especificación formal al lenguaje de contratos inteligentes Scilla mediante el lenguaje Maude, para así poder ejecutar contratos reales siempre que estén escritos adecuadamente y realizar un análisis para detectar partes de código muerto, es decir, variables que no van a ser usadas en ningún momento. Para llevar a cabo esta implementación, se ha tenido que realizar una transformación previa del archivo de entrada por ciertas limitaciones que tiene Maude, se han creado dos gramáticas: la primera, muy parecida a la sintaxis de Scilla, se utiliza para analizar sintácticamente el archivo de entrada; por su lado, la segunda gramática es más funcional y se utliza para trabajar internamente. Para la descripción de la semántica se ha creado una memoria para poder almacenar las variables con los valores y el nombre, entradas y cuerpo de los procedimientos, funciones y transiciones de Scilla, y distintas reglas de reescritura que permiten actualizar esos datos en la memoria y ejecutar las operaciones. También, el usuario podrá interactuar con el sistema por medio de la entrada/salida, donde puede introducir un contrato válido, indicar si quiere analizarlo o ejecutarlo y en el caso de que se quiera ejecutar, introducir los valores necesarios para ello. También recibirá respuesta por parte del sistema, en el caso del análisis, indicando si el contrato es correcto o si tiene partes que no se usan, mostrando cuáles son y a qué función, transición o procedimiento corresponde y, en el caso de la ejecución, devolviendo el estado de las variables que son globales y además, la información de la transición ejecutada. Por último, se busca también que el análisis sea escalable para no solo analizar contratos escritos en Scilla, sino en cualquier otro lenguaje. Para ello, se va ha parametrizado el código, es decir, en vez de poner la sintaxis concreta de las partes que se quieren analizar de Scilla dentro del código que realiza el análisis, se especifica aparte y se pasa como parámetro. Palabras clave Maude, Scilla, lógica de reescritura, contratos inteligentes, metarrepresentación, especifi- cación formal. v
Abstract Specification of Scilla in Maude The present project seeks to provide a formal specification to the Scilla smart contract language using the Maude language, in order to be able to execute real contracts, as long as they are properly written, and to perform an analysis in order to detect parts of dead code, specifically variables that are not going to be used at any time. In order to implement it, the input file had to be previously converted due to certain limitations of Maude. Thus, two grammars have been created. On the one hand, the first one, very similar to Scilla syntax, is used to syntactically analyze the input file. On the other hand, the second grammar is more functional and is used to work internally. For the description of the semantics, a memory has been created to save the variables that include the values, the names, the entries and the body of Scilla procedures, functions and transitions, as well as different rewrite-rules that allow updating these data in the memory and implementing the operations. In addition, the users can interact with the system through input/output, where they can enter a valid contract, indicate whether they want to analyze or execute it and, in that case, enter the necessary values for it. The users will also receive a response from the system, in the case of analysis, indicating if the contract is correct or if it has modules that are not used, showing which ones are and to which function, transition or procedure they correspond, and, in the case of execution, returning the status of the variables that are global, as well as the information of the transition executed. Finally, the analysis is also intended to be scalable to not only analyze contracts written in Scilla language, but also in any other language. For this purpose, the code has been parameterized, that is, instead of introducing the specific syntax of the Scilla modules to be analyzed within the code that performs the analysis, it is specified separately and passed as a parameter. Keywords Maude, Scilla, rewriting logic, smart contracts, metarepresentation, formal specification. vii
Índice 1. Introducción 1 1.1. Motivación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2. Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3. Plan de trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2. Preliminares 5 2.1. Maude . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.2. Contratos inteligentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.2.1. Solidity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.3. Scilla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.4. Trabajo relacionado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3. Descripción del Trabajo 19 3.1. Entrada/Salida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.2. Preparse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.3. Metaparse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.4. Parse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.5. Memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.6. Pedir, cargar parámetros y ejecutar transición . . . . . . . . . . . . . . . . . 27 3.7. Análisis de código muerto . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 4. Pruebas 31 5. Conclusiones y Trabajo Futuro 45 6. Introduction 47 6.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 ix
6.2. Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 6.3. Work plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 7. Conclusions and Future Work 51
Índice de figuras 1.1. Diagrama de Gantt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2.1. Ejemplo de un contrato escrito en Solidity ([Sol20]) . . . . . . . . . . . . . . 10 2.2. Estructura básica de un contrato de Scilla ([Sci19]) . . . . . . . . . . . . . . 11 2.3. Fallos en distintos contratos y sus pérdidas [HSR+ 18]. . . . . . . . . . . . . 16 3.1. Representación del flujo que sigue el sistema internamente . . . . . . . . . . 20 3.2. Código de ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.3. Representación del flujo que sigue la Entrada/Salida . . . . . . . . . . . . . 22 3.4. Código de ejemplo tras la transformación a Qids . . . . . . . . . . . . . . . 23 3.5. Código de ejemplo tras el preparse . . . . . . . . . . . . . . . . . . . . . . . 24 3.6. Código de ejemplo tras el metaparse . . . . . . . . . . . . . . . . . . . . . . 25 3.7. Código de ejemplo tras el parse . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.8. Código de ejemplo tras guardar en memoria . . . . . . . . . . . . . . . . . . 27 3.9. Código de ejemplo tras la ejecución . . . . . . . . . . . . . . . . . . . . . . . 28 4.1. Diagrama de flujo sobre la ejecución del sistema . . . . . . . . . . . . . . . . 31 6.1. Gantt diagram . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 xi
Índice de tablas 2.1. Comparación entre K-framework y compiladores conocidos ([MR13]) . . . . 16 3.1. Tabla resumen sobre los archivos, los módulos y una descripción de su con- tenido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 xiii
Capı́tulo 1 Introducción En este trabajo se desarrolla una especificación en lógica de reescritura del lenguaje de contratos inteligentes Scilla usando Maude. A partir de esta especificación, se desea que el usuario pueda ejecutar cualquier contrato escrito en Scilla, introduciendo de manera interactiva los parámetros necesarios para ello. Por otro lado, se ha querido realizar un análisis sencillo para detectar partes de código no utilizados y así poder generar contratos más eficientes. A continuación, se explica la motivación de este trabajo, exponiendo también sus obje- tivos y, por último, se añade un apartado sobre cuál ha sido el plan de trabajo, detallando las partes que tiene, indicando el orden en el que se han realizado e ilustrando el proceso con un diagrama de Gantt donde se muestra la duración en días de cada una de esas tareas. 1.1. Motivación En los últimos años, gracias al avance de la informática, se ha ido automatizando distintos aspectos de la vida cotidiana que antes era impensable realizar. Hace pocos años, tras la aparición de las monedas viruales como el Bitcoin [Fra15] y de las bases de datos distribuidas, surgió la idea de los contratos inteligentes para poder realizar transacciones de dinero o acuerdos de cualquier tipo de una forma segura y sin que interfieran terceras personas, como por ejemplo, los abogados o los bancos. Hoy en día hay varios lenguajes que implementan esta idea, como es el caso de Solidity y Scilla. La importante función que ejercen estos contratos obliga a que no puedan cometer fallos y a que sean seguros. Por esto mismo, es importante detectar los errores que puedan tener rápidamente para así evitar consecuencias graves. En este sentido, tener una especificación formal de estos lenguajes nos permitiría analizarlos y ver los posibles errores que puedan tener antes de que sean desplegados. Por otro lado, uno de los grandes inconvenientes de estos contratos es que son difíciles de entender para personas sin experiencia en programación, por lo que pueden seguir siendo necesarias terceras personas que “traduzcan” el significado de los contratos. Para resolver este problema, Scilla es un lenguaje sencillo y de fácil aprendizaje que sacrifica parte de la expresividad de otros lenguajes como Solidity a cambio de legibilidad, por lo que es un buen candidato para usarse de manera frecuente y por público no especializado. Por esta 1
2 Capítulo 1. Introducción razón, y porque no hay demasiados estudios sobre él, se ha seleccionado este lenguaje para desarrollar una especificación formal que pueda servir como base para realizar posteriores análisis que permitan su verificación. 1.2. Objetivos Los objetivos principales de este trabajo son, por un lado, proporcionar una semántica formal al lenguaje de contratos inteligentes Scilla para, posteriormente, poder realizar diversos análisis sobre él. A modo ilustrativo, en este trabajo se ha implementado uno que consiste en ver si hay partes del código que no sean utilizadas. Gracias a ello se puede obtener un código más eficiente, ya que al saber qué partes de código hay sin usar, se puede evitar realizar operaciones innecesarias que consumen tiempo y además, ocupan espacio en memoria. Este análisis se desea que sea escalable para así permitir que, además de proporcionar esta información sobre contratos en Scilla, se pueda extender para cualquier otro lenguaje con cambios mínimos. Aparte de permitir realizar este análisis, el sistema tiene que ser capaz de interactuar con el usuario y de ejecutar un contrato real válido, es decir, que esté bien escrito sintác- ticamente y, además, que siga exactamente la sintaxis real de Scilla, lo que requiere de un análisis previo por ciertas limitaciones que tiene Maude, como se verá más adelante. 1.3. Plan de trabajo A continuación enumeramos las tareas realizadas durante el desarrollo: 1. Estudio del lenguaje Scilla desde cero, analizando su sintaxis y posteriormente, viendo ejemplos sobre estos contratos y sus ejecuciones. 2. Repasar el lenguaje Maude y estudiar nuevos aspectos que no se habían visto ante- riormente, como es el caso de la meta-representación y la entrada/salida. 3. Implementar la gramática del lenguaje sin meta-representación. En un primer mo- mento se consideró en diseñar esta gramática usando el tipo String para las variables en vez de usar los tokens genéricos. Esta idea se descartó porque requería modificar los contratos reales. 4. Implementar la primera gramática del lenguaje con meta-representación, basándose en la primera gramática que se ha generado en el apartado anterior. 5. Realizar el análisis sintáctico del lenguaje. 6. Implementar la segunda gramática, es decir, la gramática interna para resolver los tokens a la hora de usar las reglas. 7. Diseñar la memoria donde se van a guardar las variables, transiciones y procedimien- tos. 8. Implementar las reglas que van a guardar los datos en la memoria, en este caso, en cada una de las reglas se tiene el contrato parseado, una memoria para tener las variables que son globales, las funcionesy procedimientos y otra para las transiciones.
1.3. Plan de trabajo 3 9. Implementar las reglas que realizarán la ejecución del contrato. En este caso solo se tienen las dos memorias y se realiza la ejecución sobre ellas. 10. Implementar las reglas a cargo de entrada/salida para que el usuario pueda inter- actuar con el sistema, permitiéndole cargar un archivo que contenga un contrato de Scilla, indicar si quiere realizar un análisis o ejecutarlo y, en el caso de la ejecución, introducir los parámetros necesarios. 11. Implementar el preparse, creado para modificar el archivo de entrada para eliminar ambigüedades y caracteres no soportados por Maude. 12. Realizar el análisis del código muerto, es decir, código declarado pero no utilizado. Una vez visto el orden en el que tiene lugar cada una de las tareas, a continuación, en la figura 1.1 se muestra un diagrama de Gantt con las tareas en el mismo orden y la duración en días de cada una de ellas. Figura 1.1: Diagrama de Gantt El resto de la memoria se organiza como sigue: En el capítulo 2 se explica los lenguajes usados y los estudios o proyectos que se han realizado con anterioridad y están relacionados con este trabajo. En el capítulo 3 se verá a fondo cómo se ha desarrollado el trabajo, explicando las partes que contiene y cómo se ha implementado cada una de ellas. En el capítulo 4 se va a ver cómo funciona el sistema desarrollado y se pondrán unos ejemplos de contratos de Scilla para ilustrar su funcionamiento. En el capítulo 5 se desarrollan las conclusiones de este trabajo y las distintas líneas de trabajo que se puede desarrollar a partir de él.
4 Capítulo 1. Introducción El código fuente de la aplicación está disponible en https://github.com/laudf/Especificacion- y-verificacion-de-Scilla-en-Maude bajo licencia MIT.
Capı́tulo 2 Preliminares Como ya se ha mencionado en la introducción, este proyecto consiste en la implementa- ción del lenguaje de programación de contratos inteligentes Scilla utilizando el lenguaje de especificación Maude. Para ello, en esta sección se presentará brevemente Maude. Asimis- mo, se realizará una breve introducción a los contratos inteligentes, explicando a fondo el lenguaje Scilla y comparándolo con otros lenguajes de este tipo como Solidity. Por último, se tratará el trabajo relacionado, incluyendo los distintos casos de éxito para lenguajes como Java y C y la creación de la KEVM para Ethereum. 2.1. Maude Maude [CDE+ 07] es un lenguaje de programación basado en lógica de reescritura que usa lógica ecuacional para definir las estructuras de datos y reglas de reescritura para definir el comportamiento dinámico de los sistemas. Según el artículo [MR06], la lógica de reescritura surgió de la necesidad de unificar dos tipos distintos de marcos semánticos para lenguajes de programación. Por un lado, se tiene la semántica ecuacional, cuyas definiciones formales tienen la forma de ecuaciones, y que es adecuada para sistemas deterministas. Por otro lado, está la semántica operacional estructurada, donde las definiciones son reglas que proporcionan una descripción paso a paso de la ejecución del programa, por lo que este método prevalece en los lenguajes concurrentes. Como cada uno de los dos métodos comentados tienen sus ventajas e inconvenientes, se ha decidió unifircarlos en un marco único, la llamada lógica de reescritura. Maude es un lenguaje de más alto nivel que otros lenguajes imperativos, lo que permite resolver problemas complejos de una manera más sencilla que lenguajes como C o Java. Además, sus fundamentos matemáticos permiten razonar sobre los sistemas que definen. Y no solo eso, también proporciona una implementación muy eficiente de un comprobador de modelos para lógica lineal temporal que permite verificar si los sistemas satisfacen ciertas propiedades y, en el caso de no verificarlas, es capaz de mostrar contraejemplos para ellas. A continuación, se presenta la sintáxis básica de Maude. Las unidades básicas son los módulos, de los que encontramos dos tipos: Módulos funcionales: aquí se definen los tipos de datos y las operaciones y cuya sintaxis es la siguiente: 5
6 Capítulo 2. Preliminares fmod is endfm Los tipos se declaran con el comando sort, los subtipos con subsort y los operado- res con op. Además, los operadores pueden tener distintas propiedades como pueden ser la asociatividad (assoc), conmutatividad (comm), idempotencia (idem), elemento identidad (id) y constructor (ctor). Para declarar las operaciones se usa el comando eq y para las variables se usa var o vars si es una variable o varias respectivamente. Un ejemplo sencillo sería: fmod Naturales is sort Nat . op 0 : -> Nat [ctor] . op sig : Nat -> Nat [ctor] . op suma : Nat Nat -> Nat [ctor comm assoc] . vars N N1 : Nat . eq suma(0, N) = N . eq suma(sig(N), N1) = sig(suma(N, N1)) . endfm Donde se define el módulo de los naturales, se declara el tipo Nat y se especifica la operación suma. También se pueden importar otros módulos predeterminados o que se hayan creado con anterioridad. En este caso solo se usará la palabra reservada pr, que indica que el módulo a continuación se importa en modo protecting, por lo que no se podrán añadir nuevos constructores ni identificar términos que antes fuesen diferentes. Módulos del sistema: en este caso se especifica una teoría de reescritura, es decir, se definen reglas de reescritura. Su estructura es parecida a la de los módulos explicados anteriormente, pero en vez de usar fmod y endfm se usa mod y endm. Las reglas sirven para cambiar de estado y permite ejecutar el sistema que se haya definido y su sintaxis es: rl [estado1] => [estado2] .. A continuación presentamos algunos módulos predefinidos útiles en este trabajo: BOOL: este módulo se importa por defecto. Define el tipo Bool con dos contructoras, true y false, y varias funciones como igualdad sintáctica (_==_), negación (not_), conjunción lógica (_and_) y disyunción (_or_). NAT: este módulo define los tipos Zero, que incluye al número 0, NzNat, que identifica los números naturales sin incluir el 0, y Nat, que es la unión de los tipos anteriores. Define también operaciones propias de los números naturales, como la suma (_+_) o la diferencia simétrica (sd(_,_)). INT: define el tipo Int, que se corresponde con los números enteros, y define operaciones propias de los enteros, como la resta (_-_).
2.1. Maude 7 STRING: define el tipo String, que engloba las cadenas de caracteres delimitadas por comillas dobles. QID: define el tipo Qid, que englobalas cadena de caracteres precedida por ’. Maude también tiene la opción de interactuar con el usuario mediante entrada/salida. Los flujos estándar en Maude son los mismos que para cualquier proceso Unix: entrada estándar (stdin), salida estándar (stdout) y el error estándar (stderr). Estos flujos en Maude se corresponden con tres objetos externos, definidos en un módulo predefinido llamado STD-STREAM en el archivo file.maude. Hay distintas operaciones que utilizan estos objetos y son las que se usarán para interactuar con el usuario: getLine solicita al usuario información y su sintaxis es: getLine(stdin, , ). Este mensaje siempre recibe como respuesta un mensaje gotLine que indica que el usuario ya ha introducido la información solicitada, cuya sintaxis es: gotLine(, stdin, ) write muestra un mensaje por la salida estándar. Su sintaxis es: write(stdout, , ) Este mensaje siempre recibe un mensaje wrote como respuesta para indicar que ya ha finalizado la escritura. Además de los flujos estándar, Maude también implementa entrada/salida mediante ficheros de texto. Para ello, se tiene un objeto externo llamado fileManager que funciona como administrador. Para abrir un archivo se usa el mensaje openFile(fileManager, , , ), donde se solicita al administrador que abra el archivo . Los permisos pueden ser de escritura ("w"), lectura ("r") o ambos ("a"). Si se abre el archivo, se devuelve un mensaje openedFile con un identificador del archivo que se ha abierto. En caso de error se devuelve un mensaje fileError con un texto explicativo de por qué no se ha podido abrir el archivo correctamente. Gracias a que la lógica de reescritura es reflexiva, Maude implementa un módulo META-LEVEL que permite usar términos y módulos de Maude como datos normales. Es importante en este trabajo entender cómo se meta-representan términos: la idea principal es usar notación prefija, precediendo cada operador por una comilla (’) e incluyendo los argumentos, meta-representados, entre corchetes y separados por comas. Las constantes siguen esta notación, incluyendo su tipo con un punto, mientras que las variables usan dos puntos. Por ejemplo, la meta-representación de x + 1 sería ’_+_[’x:Nat,’s_[’0.Zero]], donde asumimos que x tiene el tipo Nat. Presentamos a continuación algunas funciones útiles del módulo META-LEVEL: metaRewrite reescribe un término usando tanto las ecuaciones como las reglas, a imitación del comando rewrite del nivel objeto. Tiene tres parámetros: la meta- representación del módulo en el que se ejecuta el comando, la del término que será reescrito y un límite superior en el número de reescrituras. Esta función devuelve el término resultado y su tipo si la reescritura tiene éxito y un error en caso contrario. metaParse se encarga de devolver la meta-representación de un término. Recibe un módulo donde se tienen definidas las ecuaciones y una lista de Qids que se usarán
8 Capítulo 2. Preliminares para construir el término. Esta función devuelve un término y su tipo en caso de que la lista de Qid se corresponda con un término correcto en el módulo y un error en otro caso. Por último, se introducirán los concepto básicos de programación parametrizada en Maude, es decir, introducir parámetros como argumentos a los módulos. Esto ayuda a conseguir un código escalable. Posee los siguientes bloques básicos: Teorías: en ellas se declaran los requisitos que tienen que satisfacer los parámetros con los que se va a instanciar el módulo parametrizado. Su sintaxis es similar a los módulos, pero en vez de usar mod y endm usaremos fth y endfth. Módulos parametrizados por una serie de teorías. En estos módulos se pueden usar las funciones y los tipos de las teorías, aunque todavía no estén instanciados a valores concretos. Vistas: se usan para especificar cómo un cierto módulo satisface una teoría ya creada. Su sintaxis es: view from to is endv Módulos instanciados, que usan las vistas para concretar las teorías de los módulos parametrizados. 2.2. Contratos inteligentes Los contratos inteligentes [FP19] son programas informáticos centrados en ejecutar acuerdos entre dos partes. Estos contratos pueden ser llamados tanto por personas físicas o jurídicas como por máquinas o por otros programas, de ahí que no necesiten la intervención humana para ejecutarse. En los años 90 ya se empezó a pensar en este tipo de contratos: un criptólogo llamado Nick Szabo [BBV] quería desarrollar unos protocolos informáticos que permitieran sustituir a los abogados y los correspondientes trámites legales que conlleva realizar un acuerdo entre dos o más partes. Sin embargo, en este momento era algo impensable ya que no había una plataforma segura donde almacenarlos. Esto cambió a partir de la llegada de las bases de datos descentralizadas, en particular con la cadena de bloques (blockchain) [Bas17] como ejemplo más popular. Estos contratos inteligentes tienen muchas aplicaciones en el mundo real como, por ejemplo, la automatización de pagos, asegurándose así que llega la cantidad que se pide en el momento y a la persona que corresponde; cambios de propiedad si el registro está almacenado en la blockchain; transacciones energéticas; propiedad intelectual, sirviendo en este caso, para cumplir determinados acuerdos sobre las licencias de propiedad intelectual o facilitar el pago a los titulares de ella; seguros; apuestas; compras automáticas y vota- ciones, entre otros muchos. Con estas premisas pueden surgir varias preguntas: ¿Cómo se actualizan los contratos inteligentes si necesitan recibir información del exterior? Esto pue- de pasar en el caso de las apuestas deportivas que se necesita saber qué equipo ha ganado,
2.3. Scilla 9 o si hay dos personas implicadas en el mismo fin, por ejemplo, ahorrar una cantidad de dinero. Además, ¿quién asegura que la otra persona no lo va a sacar sin contar con él? Como solución para la primera pregunta contamos con unas herramientas informáticas llamadas oráculos (oracle) que permiten actualizar el estado de un contrato aportándole información del exterior. Para contestar la segunda consideramos una función, llamada multifirma, que consiste en obligar a aceptar la transición a todas las partes del contrato para que se pueda ejecutar. En resumen, estos contratos tienen tanto ventajas como inconvenientes. Por otro lado, estos contratos pueden tener diversos problemas en cuanto a seguridad se refiere, como ataques de denegación de servicio (DoS) por la realización de cálculos infintos. Para solucionar esto, hay un elemento que tienen todos estos contratos llamado “gas”, que es una unidad que mide el esfuerzo, en cuanto computación se refiere, que tiene ejecutar una determinada operación. Cuando se ejecuta una transición hay una cantidad de gas disponible y va disminuyendo a medida que se van ejecutando operaciones, por lo que si la transición se queda sin gas, no puede seguir ejecutándose y salta una excepción por falta de gas, por lo que nunca se podrán tener cálculos infinitos. Por último, estos contratos tienen algunas ventajas: aportan autonomía, seguridad y confianza. Pero también tiene algunos inconvenientes, ya que no pueden ser útiles en todos los ámbitos de la vida: no hay regulación legal para ellos. Pero a pesar de este inconveniente la sociedad está evolucionando y cada vez más gente esta haciendo uso de estas tecnologías. A continuación, se comentarán diversos lenguajes de contratos inteligentes. 2.2.1. Solidity Solidity [Mod18, Sol21a] es un lenguaje de programación de alto nivel orientado a ob- jetos para programar contratos inteligentes. Este lenguaje es usado por varias plataformas de cadenas de bloques, siendo Ethereum [AW19] una de las más conocidas. Este lenguaje, influido por otros lenguajes como C++, Python y JavaScript, fue propuesto por Gavin Wood a mediados de 2014 y desarrollado más tarde por un equipo de Ethereum. Tiene un tipado estático, es decir, la comprobación de tipos se realiza en tiempo de compilación en vez de en tiempo de ejecución, admite herencia, bibliotecas, variables de estado, definición de funciones, modificadores de funciones, eventos, estructuras de datos y tipos enumera- dos. El uso más cotidiano que se le da a los contratos programados con este lenguaje son votaciones, subastas o crowdfunding [Sol21b]. Su estructura básica es la que se muestra en la figura 2.1, donde la primera línea indica a partir de qué versión de Solidity se puede ejecutar ese contrato, luego se especifica el nombre del contrato y dentro de él se declaran todas las variables de estado, funciones, modificadores de función (usados para poder modificar el comportamiento de las funciones de una manera ágil, pudiendo comprobar automáticamente una condición antes de ejecutar una función), eventos, estructuras y enumerados. 2.3. Scilla Scilla [Sci19] es otro lenguaje de programación para contratos inteligentes, desarrollado para crear estos contratos en una cadena de bloques llamada Zilliqa. Este lenguaje está
10 Capítulo 2. Preliminares pragma solidity >= 0.4 contract SimpleStorage{ uint storeData; function set(uint x) public { storeData = x; } function get() public view returns (uint){ return storeData; } } Figura 2.1: Ejemplo de un contrato escrito en Solidity ([Sol20]) caracterizado por su sencillez y por su sintáxis reducida, lo que facilita la programación para colectivos que no tienen mucha experiencia en programación e impone una estructura determinada a los contratos inteligentes, lo que hace que sean menos vulnerables a ataques. Sin embargo, no es un lenguaje Turing-completo, por lo que cierta funcionalidad disponible en otros lenguajes no estará disponible en Scilla. Su estructura básica se presenta en la figura 2.2. Observamos que en primer lugar es necesario indicar la versión de Scilla. Aunque en este ejemplo no hay ninguna biblioteca estándar importada, en el caso de requerirlo se puede hacer con import NombreBiblioteca y estas bibliotecas son BoolUtils, IntUtils, ListUtils, NatUtils y PairUtils, donde se definen operaciones específicas de cada tipo. A continuación, se pueden definir bibliotecas nuevas con la palabra reservada library, como se ve en la figura, y seguidamente iría el cuerpo de esa biblioteca con expresiones let, que se explicarán más adelante. A continuación se codifica el contrato propiamente dicho, indicando su nombre, los parámetros inmutables y las restricciones, en el caso de que las haya. Estas restricciones son requisitos que se imponen a los parámetros inmutables, para evitar que se despliegue el contrato con valores que no tienen sentido. Después se indican los campos modificables, definidos con field. Estos campos se inicializan con un valor y luego pueden ser modificados en las transiciones o procedimientos. Estas transiciones y procedimientos sirven para definir funciones. La sintaxis de ambos es muy parecida, su diferencia radica en que las transiciones son públicas y pueden llamarse desde el exterior del contrato y los procedimientos, por el contrario, son privados y solo pueden llamarse desde otro procedimiento o transición. Es importante indicar que hay algunos parámetros que están implícitos. Por una parte tenemos los siguientes parámetros inmutables: _this_address: indica la dirección del contrato y es de tipo ByStr20 (este tipo será explicado más adelante). _creation_block: indica el número de bloque donde se ha desplegado el contrato y es de tipo BNum. Por otro lado, también hay otros parámetros implícitos en las transiciones que son:
2.3. Scilla 11 Figura 2.2: Estructura básica de un contrato de Scilla ([Sci19]) _sender: expresa la dirección de la cuenta que realizó la transición y es de tipo ByStr20. _amount: expresa la cantidad entrante y es de tipo Uint128. Estos parámetros se pasan directamente a los procedimientos que son llamados por las transiciones. Una vez aclarada la estructura de un contrato de Scilla, se pasa a ver los tipos de datos que se pueden tener y sus operaciones, empezando por los primitivos, que son los tipos más básicos, y continuando con los algebraicos, que son tipos compuestos definidos mediante constructoras. Dentro de los tipos primitivos están: Enteros: engloban los enteros con signo y sin signo. Pueden ser de 32, 64, 128 o 256 bits. Un ejemplo sería: let a = Uint32 1
12 Capítulo 2. Preliminares Para declarar el entero sin signo de 32 bits con valor 1. Las operaciones que se puede realizar con ellos son: eq (igualdad), add (suma), sub (resta), div (división), rem (resto), lt (menor), pow (potencia), isqrt (raíz cuadrada), to_nat (convertir el entero al tipo nat que explicaremos más adelante) y to_(u)int32/64/128/256 (para transformarlo a otro tipo de integer). Estas operaciones se declaran usando la expresión builtin. Un ejemplo para declarar la operación suma sería builtin add x1 x2, donde x1 y x2 están declarados como enteros del mismo tipo. Aclarar que todos los parámetros de las operaciones tienen que ser del mismo tipo, excepto en la operación pow, que el segundo argumento siempre tiene que ser del tipo Uint32 y el primero de cualquiera. Otra excepción es la operación isqrt, que solo admite el tipo de entero sin signo. Este tipo además tiene una biblioteca predefinida (IntUtils) que aporta más operaciones, sobre todo de comparación entre dos enteros. String: este tipo corresponde a las cadenas de caracteres delimitadas entre comillas. Sus operaciones son: eq (igualdad), concat (concatenar dos strings), substr (obtener parte del string), to_string (convertir un tipo entero o hash a un string) y strlen (obtener la longitud del string). La forma de declararlas es igual que para los enteros. Cadenas de bytes (ByStr y ByStr x, donde x es la longitud): se escribe utilizan- do caracteres hexadecimales, precedidos por “0x”. Admite las siguientes operaciones declaradas también con builtin: to_uint (convertirlo a un valor equivalente del entero), concat, strlen y eq. Este tipo es el general, además encontramos las direc- ciones, que corresponden a un tipo específico (ByStr20, cadenas de 20 bytes). Mapas: son listas de parejas clave-valor y se declaran con Map kt vt (donde kt es el tipo de la clave y vt el tipo del valor) y para declarar el mapa vacío sería Emp kt vt. Las operaciones que admite pueden ser de dos tipos: • In_place: modifica el mapa sin realizar copias de él. Estas operaciones pueden ser múltiples, es decir, referirse a varias claves a la vez). • Funcionales: no modifica el mapa original y normalmente se usan para el diseño de bibliotecas. Se implementan las mismas operaciones en ambos casos, la única diferencia es que se escriben de forma distinta. Permiten insertar un par clave-valor, si no está la clave ya en el mapa, o modificar el valor si ya está; obtener el valor de una clave; saber si una clave tiene un valor asociado; eliminar el par clave-valor; convertir el mapa en una lista; y devolver el número de pares que hay (estas dos últimas operaciones no distinguen entre operación funcional o in_place). Número de bloque: se declara con la palabra BNum y un entero. Las operaciones que admite este tipo son: eq (igualdad), blt (menor), badd (suma) y bsub (resta). En cuanto a los tipos algebraicos, se tiene: Booleanos (Bool): tiene dos constructores, True y False. Para este tipo hay una biblioteca estándar (BoolUtils) que aporta operaciones lógicas sobre ellos.
2.3. Scilla 13 Opciones (Option): tiene los constructores Some, que indica la presencia de valor y recibe un argumento (el valor), y None que es la ausencia de valor y no tiene argumentos. Este tipo es útil para definir funciones parciales. Listas (List) tiene también dos constructores, Nil corresponde a la lista vacía y Cons construye la lista no vacía recibiendo dos argumentos, el primero corresponde a la cabeza de la lista y el segundo al resto. Este tipo también tiene una biblioteca estándar (ListUtils) que aporta operaciones para interactuar con ellas, como por ejemplo, obtener la cabecera, filtrar en la lista, etc. Pares (Pair) tiene un solo constructor Pair con dos argumentos. En este caso, tam- bién hay una biblioteca ya definida (PairUtils) con dos operaciones, fst, que extrae el primer elemento del par y snd, que extrae el segundo. Naturales: tiene dos constructoras, Zero que representa el 0 y no tiene argumentos y Succ con un argumento y representa el sucesor de un número. También tiene una biblioteca estándar ya definida (NatUtils). También el usuario puede definir tipos propios con la siguiente sintaxis: type = | | ... A continuación se especifican las expresiones y declaraciones para usar estos tipos tanto en las bibliotecas definidas por el usuario como en los procedimientos y transiciones. Las expresiones a continuación se usan principalmente en las bibliotecas, aunque tam- bién pueden usarse dentro de las declaraciones de transiciones y procedimientos: let x = f se usa para declarar una variable (x) en las bibliotecas y f puede ser cualquier tipo de los comentados anteriormente. let x = f in expr declara la variable x e indica que solo se puede usar dentro del ámbito de expr. {; ; ...}: usado para representar mensajes o eventos, don- de cada entrada tiene la forma b : x. Decir que estos eventos y mensajes tienen entradas obligatorias. Para los eventos, solo hay una y es _eventname (String), que indica el nombre del evento. Los mensajes tienen tres entradas obligatorias: _tag (String), _recipient (BStr20) y _amount (Uint128), que indican el nombre de la transación que se invocará en el contrato destinatario si _recipient es la dirección del contrato, si no se ignora; la dirección de la cadena de bloques a la que se envía el contrato; y la cantidad que se transfiere, respectivamente. fun(x:T) =>expr es una función cuya entrada es x de tipo T y devuelve un valor. f x aplica la función f al parámetro x.
14 Capítulo 2. Preliminares tfun ’T =>expr: función que toma a ’T como un tipo paramétrico y devuelve un valor. Sirve para crear funciones de biblioteca y es útil cuando no se sabe qué tipo va a tener el valor devuelto. @x T indica que el parámetro de entrada de la función x va a tomar el tipo T. Esto se usa cuando se tiene una tfun. builtin f lx (mencionado al explicar las operaciones de los tipos): aplicar la ope- ración f con los parámetros lx. match: tiene la misma función que un if y su sintaxis es: match x with | Opcion1 => ... | Opcion2 => ... ... end Donde opcion1, opcion2, ... son los posibles valores que puede tomar x. A continuación, se explican las declaraciones, que se usan solo dentro de transiciones y procedimientos: x
2.4. Trabajo relacionado 15 de programación Java y C. También se hará mención a la creación de la KEVM por este mismo autor y, posteriormente, se analizarán de forma muy breve dos estudios sobre el límite de gas y los problemas que tienen los callbacks 1 así como la solución propuesta. En [MR06] se define una hoja de ruta para la definición de semánticas en lógica de reescritura y, en particular, en Maude. En primer lugar, se hace notar que a la hora de especificar un lenguaje de programación hay muchos estilos diferentes para hacerlo según lo que se quiera analizar, por lo que para un mismo lenguaje puede haber varias definiciones en reescritura lógica. Además, se menciona que se han realizado especificaciones de grandes fragmentos de lenguajes complejos, como Java, usando lógica de reescritura en Maude. Un ejemplo de esto es JavaFAN [FMR04], una herramienta de análisis de software para Java con un muy buen rendimiento, realizando la especificación en Maude de la semántica de la JVM (la maquina virtual de Java o Java Virtual Machine). Esto nos permite analizar programas concretos que en algunos casos proporcionan mejores resultados que algunas herramientas conocidas de análisis de Java. Lo que se ha visto hasta el momento tiene un problema y es la falta de escalabilidad de las semánticas. Una de las causas es la falta de modularidad, es decir, la falta de ver la semántica como la unión de varias partes. Por lo tanto, se buscó un método que resolviese este problema, dando lugar al llamado K-framework [MR13]. El K-framework es un entorno específico para la definición de semánticas de lenguajes y se encarga de definir funcionalidad para simplificar el proceso. En sus inicios, este entorno estaba basado en Maude pero sus últimas versiones se han implementado en Java. El éxito de este entorno se fundamenta en tres elementos: configuraciones, cálculos y reglas. Las configuraciones organizan el programa en una estructura de datos llamada celda, que están etiquetadas y se pueden anidar. Por otro lado, los cálculos lo que hacen es ampliar el programa y, por último, las reglas generalizan las reglas de reescritura normales indicando qué partes del término leen, escriben o les son indiferentes. Se puede ver un ejemplo de esta técnica sobre el lenguaje de programación IMP explicado al detalle en el artículo [MR13], concretamente en la sección 3.1. Este método se puede ejecutar sobre Maude usando la herramienta K-Maude, que permite transformar las definiciones del lenguaje K en Maude para poder ejecutarlo y analizarlo. También es posible exportarlo a Latex para generar la documentación [SR10]. A continuación, se pasará a hablar de los trabajos relacionados con la reescritura del lenguaje C [MR13] usando el entorno K-framework, destacando que es la semántica más compleja que se ha realizado de este lenguaje hasta el día de hoy. Es especialmente intere- sante notar que esta semántica se ha ejecutado con éxito en la mayoría de los ejemplos que hay en el manual de Kernigham y Ritchie2 . Si se observa la tabla 2.1 se puede apreciar que la semántica creada con K-framework es una muy buena alternativa al compararla con los compiladores GCC, ICC y Clang, ya que da resultados muy similares y en algunos casos mejores. Otra carácterística a favor es el tiempo que tarda en ejecutarlos, ya que la mayoría (más del 90 % de los casos de éxito) se ejecutan en menos de 5 segundos cada uno de ellos. Tras ver lo que es el método K-framework y su uso en lenguajes de programación complejos como es el caso de C, se pasa a explicar en que consiste KEVM [HSR+ 18]. 1 Un callback es un valor devuelto por una función y que se usa como argumento en otra. 2 Manual que cubre todas las carácterísticas de ANSI C, un estándar para el lenguaje C.
16 Capítulo 2. Preliminares Compilador/Semánticas Pruebas éxito Porcentaje K-framework 770 99.2 GCC 768 99 ICC 771 99.4 Clang 763 98.3 Tabla 2.1: Comparación entre K-framework y compiladores conocidos ([MR13]) Figura 2.3: Fallos en distintos contratos y sus pérdidas [HSR+ 18]. Esta herramienta es un entorno de especificación de la EVM de Ethereum3 usando el K-framework. Esta semántica para la EVM tiene tres elementos principales. El primero de ellos es una sintaxis del lenguaje en estilo EBNF4 , el segundo es una configuración propia para describir el estado y, por último, las reglas de transición para ejecutar los programas. Estos elementos van a ser fundamentales para especificar cualquier semántica de contratos inteligentes. En comparación, en Maude tenemos ecuaciones para definir la sintaxis del lenguaje; reglas, que ejecutarán el contrato; y una configuración de los estados que se van a tener y cuyas transiciones especificarán las reglas. La necesidad de desarrollar esta nueva herramienta surge de la escasa seguridad y de los graves errores que han tenido ciertos contratos inteligentes causando pérdidas bastante cuantiosas a las partes involucradas (en la imágen 2.3 se representas los distintos contratos inteligentes que han sufrido algún fallo y sus respectivas pérdidas [HSR+ 18]). Esto se complica aún más ya que la EVM admite que un contrato llame a otro para así reutilizar código a través de las bibliotecas. La mayoría de estos fallos se podrían haber evitado si esos contratos se hubieran analizado formalmente y esto es lo que va a aportar KEVM. En definitiva, KEVM es una herramienta muy potente pero no realiza la especificación de Scilla. Además, Maude es un lenguaje que proporciona muchos tipos análisis, por lo que una especificación en Maude es potencialmente analizable con una amplia variedad de técnicas. A continuación comentamos distintos estudios sobre el gas en los contratos inteligen- tes, para así enfatizar la importancia de los métodos formales en este tipo de análisis. En [ACG+ 21] se presenta un análisis cuyo objetivo es inferir los límites de gas que no sean constantes en los contratos inteligentes. Para ello usan una herramienta llamada GASTAP que analiza contratos inteligentes teniendo en cuanta el gas. GASTAP funciona en base a un contrato inteligente que toma como entrada, aportando unas cotas superiores de gas para aquellas funciones o transiciones que sean públicas. Usando esta herramienta se con- siguieron unos buenos resultados, ya que se encontraron cotas para aproximadamente el 3 EVM es la máquina virtual de Ethereum, es decir, el intérprete de los contratos inteligentes 4 Es una notación formal para definir la sintaxis de lenguajes.
2.4. Trabajo relacionado 17 90 % de los contratos que se analizaron. Por último, en [AGR+ 20] se analizan los callbaks en los contratos inteligentes. Estos callbaks pueden desencadenar algunos errores sobre todo en entornos abiertos. De hecho, muchos atacantes lo aprovechan para llevar a cabo su objetivo. Esto es lo que se quiere evitar buscando la modularidad, es decir, lo que se pretende es asegurar que las llamadas que se producen desde exterior no pueden afectar al comportamineto interno del contrato. Este estudio descibe una técnica para solucionarlo usando resolutores SMT (Satisfiability Modulo Theories), que permiten generar contraejemplos que ayudan a comprender y así solucionar el error del callback. Esta análisis ha sido implementado y aplicado con éxito a contratos reales, aproximadamente a los 150 contratos más importantes de Ethereum. En esencia, los autores decompilan los programas de código de bytes (el formato que genera la EVM) para tener una representación intermedia, para luego sobre ella realizar la verificación de la modularidad mediante SMT. En definitiva, uno de los objetivos de tener una especificación formal de Scilla en Maude es poder ser capaz de hacer análisis similares a ellos en el futuro.
Capı́tulo 3 Descripción del Trabajo En esta sección se hablará de cómo se ha desarrollado este trabajo. En particular, presentaremos una herramienta que permite analizar sintácticamente contratos inteligentes, ejecutarlos y realizar sobre ellos un análisis para detectar partes de código que no se usan. Este análisis está parametrizado por distintos elementos de la gramática, por lo que sería reutilizable si en el futuro se extendiese la herramienta con nuevos lenguajes de contratos inteligentes. Todo el código se encuentra en el siguiente GitHub (https://github.com/laudf/ Especificacion-y-verificacion-de-Scilla-en-Maude) y es interesante conocer los cua- tro ficheros principales en los que se distribuye: Preparse: este archivo solo contiene un módulo funcional con una única operación llamada preparse que se encarga de preparar el archivo que se recibe como entrada para poder parsearlo posteriormente. Por ejemplo, en Maude los guiones bajos (_) los detecta como posiciones para colocar los argumentos, por lo que no se puede definir ningún operador constante con este símbolo. Gracias al preparse se puede modificar todas los “_” por “-”. Este aspecto se explicará en más profundidad en la sección 3.2. Gramatica_def: en este archivo se tienen dos gramáticas, ambas en módulos funcionales. En la primera de ellas está la gramática que se usa para realizar el análisis sintáctico usando la función metaParse y que tiene categorías sintácticas que no están com- pletamente definidas, como son los tokens, que necesitan un análisis posterior para ser resueltos. Por otro lado, hay otra gramática que utiliza tipos propios de Mau- de para representar aquellos términos que quedaron “abiertos” en la fase anterior. Aunque esta representación es ya distinta a la usada por Scilla, será la que se use internamente para la ejecución. Asimismo, se define un módulo funcional, donde se realiza el parse, es decir, la modificación de la gramática de Scilla a la representación interna deseada para trabajar con ella. Y por último, se define otro módulo funcional para definir la memoria, usada para guardar las variables, declaraciones, expresiones, transiciones y procedimientos. Por último, contiene otros módulos de sistema donde se define la semántica, es decir, estos módulos contienen las reglas para acceder a memoria y para realizar la ejecución propiamente dicha. EntradaSalida: este archivo, que importa los anteriores, contiene toda la interacción del usuario con el sistema. Está formado por un único módulo de sistema que contiene 19
20 Capítulo 3. Descripción del Trabajo todas las reglas necesarias para la entrada/salida. CodigoMuerto: dentro de este archivo se realiza el análisis para detectar código muerto dentro de una función, procedimiento o transición. Es decir, este código muerto son partes que están declaradas pero que nunca se usan. En la sección 4 se verán ejemplos de ello. La figura 3.1 muestra un diagrama de flujo ilustrando la ejecución que se realiza inter- namente. una vez que se tiene el fichero cargado y leido, se pasa al preparse, metaparse y parse, luego se hace una distinción en función de lo que decida el usuario, si pulsa 1, se analizará el código y se acabará la ejecución del sistema o si, por el contrario, se pulsa cualquier otro valor distinto de 1, se guardan los datos en la memoria, se solicitan los pa- rámetros necesarios y una transición a ejecutar, una vez que se tienen todos los datos, se ejecuta la transición, se devuelven unos valores y se vuelve a pedir otra vez una transición. La parte de pedir transición, sus parámetros de entrada y ejecutarlo se realiza de forma reiterada hasta que el usuario decida salir. Figura 3.1: Representación del flujo que sigue el sistema internamente Una vez vistos los archivos que tiene el trabajo y el flujo que sigue, se va a explicar cada una de las partes de forma detallada en base a un ejemplo sencillo (figura 3.2), donde simplemente se obtiene el valor de un campo modificable. 3.1. Entrada/Salida La entrada/salida, como ya se ha mencionado antes, sirve para que el usuario pueda in- teractuar con el sistema. Está programado dentro del archivo llamado EntradaSalida.maude, en el módulo de sistema SciMaude. Anteriormente, en la sección 2.1, se ha comentado cómo era la estructura básica de un programa de entrada/salida leyendo de archivo, y ahora se verá las reglas nuevas que se
3.1. Entrada/Salida 21 scilla_version 0 contract HelloWorld () field welcome_msg : String = "Hola" transition getHello () r
22 Capítulo 3. Descripción del Trabajo suprimir indica que se quieren suprimir campos inutilizados en la memoria. cogerTransición indica que el estado debe pedir el nombre de la transición y coger la parte de la memoria que corresponda. pedirParamTransicion pide los parámetros de entrada de la transición elegida. cargarParamTransicion carga los parámetros de entrada de la transición con los valores introducidos por el usuario. eleccion sirve para preguntar si se quiere ejecutar el código o, por el contrario, si se quiere analizar. eleccionHecha comprueba si la elección del usuario ha sido ejecutar el código o analizarlo. analisis indica que se va a analizar si el contrato introducido tiene código muerto. terminar indica que el programa ha finalizado tras analizarlo. Figura 3.3: Representación del flujo que sigue la Entrada/Salida En la figura 3.3 se muestra una representación del flujo que siguen las reglas de la parte de entrada/salida. Estas reglas tienen la siguiente funcionalidad: La regla parsing se encarga de realizar la conversión de String a Qid, usando la función predefinida tokenize. Si aplicamos esta transformación al ejemplo en la figura 3.2 se obtiene el resultado en la figura 3.4, donde simplemente se ha convertido cada “palabra” en un Qid. Sobre está lista aplicaremos las fases de preparse, metaparse y parse, como explicaremos en las siguientes secciones.
También puede leer