Convertir FunC en funcional con Haskell: cómo Serokell ganó el concurso Telegram Blockchain

Probablemente hayas escuchado eso de Telegram está a punto de lanzar la plataforma blockchain Ton. Pero quizá te hayas perdido la noticia que no hace mucho Telegram anunció una competencia para la implementación de uno o más contratos inteligentes para esta plataforma.

El equipo de Serokell, con amplia experiencia en el desarrollo de grandes proyectos blockchain, no podía quedarse al margen. Delegamos a cinco empleados al concurso y dos semanas más tarde obtuvieron el primer lugar bajo el (in)modesto apodo aleatorio de Sexy Chameleon. En este artículo hablaré de cómo lo hicieron. Esperamos que en los próximos diez minutos al menos leas una historia interesante y, como máximo, encuentres en ella algo útil que puedas aplicar en tu trabajo.

Pero comencemos con un poco de contexto.

La competencia y sus condiciones.

Así, las principales tareas de los participantes fueron la implementación de uno o más de los contratos inteligentes propuestos, así como la formulación de propuestas para mejorar el ecosistema TON. La competencia se desarrolló del 24 de septiembre al 15 de octubre y los resultados no se anunciaron hasta el 15 de noviembre. Bastante tiempo, teniendo en cuenta que durante este tiempo Telegram logró realizar y anunciar los resultados de concursos sobre el diseño y desarrollo de aplicaciones en C++ para probar y evaluar la calidad de las llamadas VoIP en Telegram.

Seleccionamos dos contratos inteligentes de la lista propuesta por los organizadores. Para uno de ellos, utilizamos herramientas distribuidas con TON y el segundo se implementó en un nuevo lenguaje desarrollado por nuestros ingenieros específicamente para TON e integrado en Haskell.

La elección de un lenguaje de programación funcional no es casual. En nuestro blog corporativo A menudo hablamos de por qué pensamos que la complejidad de los lenguajes funcionales es una enorme exageración y por qué generalmente los preferimos a los orientados a objetos. Por cierto, también contiene original de este artículo.

¿Por qué decidimos participar?

En resumen, porque nuestra especialización son proyectos complejos y no estándar que requieren habilidades especiales y, a menudo, tienen valor científico para la comunidad de TI. Apoyamos firmemente el desarrollo del código abierto y nos ocupamos de su popularización, además de cooperar con las principales universidades rusas en el campo de la informática y las matemáticas.

Las interesantes tareas del concurso y la participación en nuestro querido proyecto Telegram fueron en sí mismas una excelente motivación, pero el fondo de premios se convirtió en un incentivo adicional. 🙂

Investigación de blockchain de TON

Seguimos de cerca los nuevos desarrollos en blockchain, inteligencia artificial y aprendizaje automático e intentamos no perdernos ningún lanzamiento significativo en cada una de las áreas en las que trabajamos. Por lo tanto, cuando comenzó la competencia, nuestro equipo ya estaba familiarizado con las ideas de Libro blanco de TONELADA. Sin embargo, antes de comenzar a trabajar con TON, no analizamos la documentación técnica ni el código fuente real de la plataforma, por lo que el primer paso fue bastante obvio: un estudio exhaustivo de la documentación oficial sobre sitio web y repositorios de proyectos.

Cuando comenzó el concurso el código ya había sido publicado, así que para ahorrar tiempo decidimos buscar una guía o resumen escrito por por los usuarios. Desafortunadamente, esto no dio ningún resultado; aparte de las instrucciones para ensamblar la plataforma en Ubuntu, no encontramos ningún otro material.

La documentación en sí estaba bien investigada, pero era difícil de leer en algunas áreas. Muy a menudo tuvimos que volver a ciertos puntos y pasar de descripciones de alto nivel de ideas abstractas a detalles de implementación de bajo nivel.

Sería más fácil si la especificación no incluyera ninguna descripción detallada de la implementación. Es más probable que la información sobre cómo una máquina virtual representa su pila distraiga a los desarrolladores que crean contratos inteligentes para la plataforma TON que los ayude.

Nix: armando el proyecto

En Serokell somos grandes fans Nix. Recopilamos nuestros proyectos con él y los implementamos usando nixops, e instalado en todos nuestros servidores Nix OS. Gracias a esto, todas nuestras compilaciones son reproducibles y funcionan en cualquier sistema operativo en el que se pueda instalar Nix.

Entonces comenzamos creando Superposición de Nix con expresión para ensamblaje TON. Con su ayuda, compilar TON es lo más sencillo posible:

$ cd ~/.config/nixpkgs/overlays && git clone https://github.com/serokell/ton.nix
$ cd /path/to/ton/repo && nix-shell
[nix-shell]$ cmakeConfigurePhase && make

Tenga en cuenta que no necesita instalar ninguna dependencia. Nix hará todo mágicamente por ti, ya sea que estés usando NixOS, Ubuntu o macOS.

Programación para TON

El código de contrato inteligente en TON Network se ejecuta en la máquina virtual TON (TVM). TVM es más complejo que la mayoría de las otras máquinas virtuales y tiene una funcionalidad muy interesante, por ejemplo, puede funcionar con continuaciones и enlaces a datos.

Además, los chicos de TON crearon tres nuevos lenguajes de programación:

Cincuenta es un lenguaje de programación de pila universal que se parece Adelante. Su súper habilidad es la capacidad de interactuar con TVM.

FunC es un lenguaje de programación de contratos inteligentes similar a C y se compila en otro lenguaje: Fift Assembler.

Quinto ensamblador — Biblioteca Fift para generar código binario ejecutable para TVM. Fifth Assembler no tiene compilador. Este Lenguaje específico de dominio integrado (eDSL).

Nuestra competencia funciona

Finalmente, es hora de observar los resultados de nuestros esfuerzos.

Canal de pago asincrónico

El canal de pago es un contrato inteligente que permite a dos usuarios enviar pagos fuera de la cadena de bloques. Como resultado, no solo ahorra dinero (no hay comisión), sino también tiempo (no tiene que esperar a que se procese el siguiente bloque). Los pagos pueden ser tan pequeños como se desee y con la frecuencia necesaria. En este caso, las partes no tienen que confiar entre sí, ya que el contrato inteligente garantiza la equidad del acuerdo final.

Encontramos una solución bastante simple al problema. Dos partes pueden intercambiar mensajes firmados, cada uno de los cuales contiene dos números: el monto total pagado por cada parte. Estos dos números funcionan como reloj vectorial en sistemas distribuidos tradicionales y establecer el orden "sucedió antes" en las transacciones. Utilizando estos datos el contrato podrá resolver cualquier posible conflicto.

De hecho, un número es suficiente para implementar esta idea, pero dejamos ambos porque de esta manera podríamos crear una interfaz de usuario más conveniente. Además, decidimos incluir el monto del pago en cada mensaje. Sin él, si el mensaje se pierde por algún motivo, aunque todos los importes y el cálculo final serán correctos, es posible que el usuario no note la pérdida.

Para probar nuestra idea, buscamos ejemplos del uso de un protocolo de canal de pago tan simple y conciso. Sorprendentemente, encontramos sólo dos:

  1. Descripción un enfoque similar, sólo para el caso de un canal unidireccional.
  2. Tutorial, que describe la misma idea que la nuestra, pero sin explicar muchos detalles importantes, como la corrección general y los procedimientos de resolución de conflictos.

Quedó claro que tiene sentido describir nuestro protocolo en detalle, prestando especial atención a su exactitud. Después de varias iteraciones, la especificación estaba lista y ahora usted también puede hacerlo. mirala.

Implementamos el contrato en FunC y escribimos la utilidad de línea de comando para interactuar con nuestro contrato completamente en Fift, según lo recomendado por los organizadores. Podríamos haber elegido cualquier otro idioma para nuestra CLI, pero estábamos interesados ​​en probar Fit para ver cómo funcionaba en la práctica.

Para ser honesto, después de trabajar con Fift, no vimos ninguna razón convincente para preferir este lenguaje a los lenguajes populares y utilizados activamente con herramientas y bibliotecas desarrolladas. Programar en un lenguaje basado en pila es bastante desagradable, ya que hay que tener constantemente en la cabeza lo que hay en la pila y el compilador no ayuda con esto.

Por lo tanto, en nuestra opinión, la única justificación para la existencia de Fift es su papel como idioma anfitrión de Fift Assembler. ¿Pero no sería mejor incorporar el ensamblador TVM en algún lenguaje existente, en lugar de inventar uno nuevo para este propósito esencialmente único?

TVM Haskell eDSL

Ahora es el momento de hablar de nuestro segundo contrato inteligente. Decidimos desarrollar una billetera con múltiples firmas, pero escribir otro contrato inteligente en FunC sería demasiado aburrido. Queríamos agregar algo de sabor y ese era nuestro propio lenguaje ensamblador para TVM.

Al igual que Fift Assembler, nuestro nuevo lenguaje está integrado, pero elegimos Haskell como anfitrión en lugar de Fift, lo que nos permite aprovechar al máximo su sistema de tipos avanzado. Cuando se trabaja con contratos inteligentes, donde el coste de incluso un pequeño error puede ser muy alto, la escritura estática, en nuestra opinión, es una gran ventaja.

Para demostrar cómo se ve el ensamblador TVM integrado en Haskell, implementamos una billetera estándar en él. Aquí hay algunas cosas a las que debe prestar atención:

  • Este contrato consta de una función, pero puedes utilizar tantas como quieras. Cuando define una nueva función en el idioma anfitrión (es decir, Haskell), nuestro eDSL le permite elegir si desea que se convierta en una rutina separada en TVM o simplemente en línea en el punto de llamada.
  • Al igual que Haskell, las funciones tienen tipos que se verifican en tiempo de compilación. En nuestro eDSL, el tipo de entrada de una función es el tipo de pila que espera la función y el tipo de resultado es el tipo de pila que se producirá después de la llamada.
  • El código tiene anotaciones. stacktype, que describe el tipo de pila esperado en el punto de llamada. En el contrato de billetera original, estos eran solo comentarios, pero en nuestro eDSL en realidad son parte del código y se verifican en el momento de la compilación. Pueden servir como documentación o declaraciones que ayuden al desarrollador a encontrar el problema si el código cambia y el tipo de pila cambia. Por supuesto, dichas anotaciones no afectan el rendimiento en tiempo de ejecución, ya que no se genera ningún código TVM para ellas.
  • Este todavía es un prototipo escrito en dos semanas, por lo que todavía queda mucho trabajo por hacer en el proyecto. Por ejemplo, todas las instancias de las clases que ve en el código siguiente deberían generarse automáticamente.

Así es como se ve la implementación de una billetera multifirma en nuestro eDSL:

main :: IO ()
main = putText $ pretty $ declProgram procedures methods
  where
    procedures =
      [ ("recv_external", decl recvExternal)
      , ("recv_internal", decl recvInternal)
      ]
    methods =
      [ ("seqno", declMethod getSeqno)
      ]

data Storage = Storage
  { sCnt :: Word32
  , sPubKey :: PublicKey
  }

instance DecodeSlice Storage where
  type DecodeSliceFields Storage = [PublicKey, Word32]
  decodeFromSliceImpl = do
    decodeFromSliceImpl @Word32
    decodeFromSliceImpl @PublicKey

instance EncodeBuilder Storage where
  encodeToBuilder = do
    encodeToBuilder @Word32
    encodeToBuilder @PublicKey

data WalletError
  = SeqNoMismatch
  | SignatureMismatch
  deriving (Eq, Ord, Show, Generic)

instance Exception WalletError

instance Enum WalletError where
  toEnum 33 = SeqNoMismatch
  toEnum 34 = SignatureMismatch
  toEnum _ = error "Uknown MultiSigError id"

  fromEnum SeqNoMismatch = 33
  fromEnum SignatureMismatch = 34

recvInternal :: '[Slice] :-> '[]
recvInternal = drop

recvExternal :: '[Slice] :-> '[]
recvExternal = do
  decodeFromSlice @Signature
  dup
  preloadFromSlice @Word32
  stacktype @[Word32, Slice, Signature]
  -- cnt cs sign

  pushRoot
  decodeFromCell @Storage
  stacktype @[PublicKey, Word32, Word32, Slice, Signature]
  -- pk cnt' cnt cs sign

  xcpu @1 @2
  stacktype @[Word32, Word32, PublicKey, Word32, Slice, Signature]
  -- cnt cnt' pk cnt cs sign

  equalInt >> throwIfNot SeqNoMismatch

  push @2
  sliceHash
  stacktype @[Hash Slice, PublicKey, Word32, Slice, Signature]
  -- hash pk cnt cs sign

  xc2pu @0 @4 @4
  stacktype @[PublicKey, Signature, Hash Slice, Word32, Slice, PublicKey]
  -- pubk sign hash cnt cs pubk

  chkSignU
  stacktype @[Bool, Word32, Slice, PublicKey]
  -- ? cnt cs pubk

  throwIfNot SignatureMismatch
  accept

  swap
  decodeFromSlice @Word32
  nip

  dup
  srefs @Word8

  pushInt 0
  if IsEq
  then ignore
  else do
    decodeFromSlice @Word8
    decodeFromSlice @(Cell MessageObject)
    stacktype @[Slice, Cell MessageObject, Word8, Word32, PublicKey]
    xchg @2
    sendRawMsg
    stacktype @[Slice, Word32, PublicKey]

  endS
  inc

  encodeToCell @Storage
  popRoot

getSeqno :: '[] :-> '[Word32]
getSeqno = do
  pushRoot
  cToS
  preloadFromSlice @Word32

El código fuente completo de nuestro contrato eDSL y billetera multifirma se puede encontrar en este repositorio. Y mas dicho en detalle sobre lenguajes integrados, nuestro colega Georgy Agapov.

Conclusiones sobre la competencia y TON.

En total, nuestro trabajo duró 380 horas (incluida la familiarización con la documentación, las reuniones y el desarrollo real). En el proyecto del concurso participaron cinco desarrolladores: CTO, líder de equipo, especialistas en plataformas blockchain y desarrolladores de software Haskell.

Encontramos recursos para participar en el concurso sin dificultad, ya que siempre resulta apasionante el espíritu de hackathon, el trabajo cercano en equipo y la necesidad de sumergirnos rápidamente en aspectos de las nuevas tecnologías. Varias noches de insomnio para lograr los máximos resultados en condiciones de recursos limitados se compensan con una experiencia invaluable y excelentes recuerdos. Además, trabajar en este tipo de tareas siempre es una buena prueba para los procesos de la empresa, ya que es extremadamente difícil lograr resultados verdaderamente decentes sin una interacción interna que funcione bien.

Dejando a un lado las letras: quedamos impresionados por la cantidad de trabajo realizado por el equipo de TON. Se las arreglaron para construir un sistema complejo, hermoso y, lo más importante, funcional. TON ha demostrado ser una plataforma con un gran potencial. Sin embargo, para que este ecosistema se desarrolle, es necesario hacer mucho más, tanto en términos de su uso en proyectos blockchain como en términos de mejorar las herramientas de desarrollo. Estamos orgullosos de ser ahora parte de este proceso.

Si después de leer este artículo todavía tienes alguna pregunta o tienes ideas sobre cómo usar TON para resolver tus problemas, Escríbenos — estaremos encantados de compartir nuestra experiencia.

Fuente: habr.com

Añadir un comentario