Modelo de actor para las convocatorias de contratos

El modelo de actor es un patrón de diseño utilizado para construir sistemas distribuidos fiables. Los principios fundamentales son que cada actor tiene acceso exclusivo a su propio estado interno y que los actores no pueden llamarse entre sí directamente. En su lugar, envían mensajes a través de un Dispatcher, que mantiene el estado del sistema y asigna direcciones a código y almacenamiento. Fundamentalmente, el patrón Actor puede encapsularse en esta interfaz:

pub trait Actor {
  fn handle(msgPayload: &[u8]) -> Vec<Msg>;
}
pub struct Msg {
  pub destination: Vec<u8>,
  pub payload: Vec<u8>,
}

Este es el modelo básico que se utilizó para modelar los contratos en CosmWasm. Usted puede ver la misma influencia en la función:

pub fn handle<T: Storage>(store: &mut T, params: Params, msg: Vec<u8>) -> Result<Response>

La respuesta contiene un Rust Vec y algunos metadatos, mientras que store proporciona acceso al estado interno del contrato y params hace referencia al contexto global inmutable.

De este diseño básico pueden derivarse algunos otros aspectos útiles:

  • En primer lugar, existe un acoplamiento débil entre los actores, limitado al formato de los paquetes de datos (el destinatario debe admitir un superconjunto de lo que usted envía). No hay API complejas ni punteros de función que pasar. Esto se parece mucho al uso de llamadas REST o RPC como frontera entre servicios, que es una forma escalable de componer sistemas de múltiples proveedores.

  • En segundo lugar, cada Actor puede ejecutarse efectivamente en su propio hilo, con su propia cola. Esto permite tanto la concurrencia como la ejecución serializada dentro de cada actor. Esto significa que el método Handle anterior no puede ser ejecutado en medio de una llamada Handle previamente ejecutada. Handle es una llamada síncrona y retorna antes de que el Actor pueda procesar el siguiente mensaje. Esto implica que CosmWasm previene los ataques de reentrada por diseño.

Información

El contrato puede acceder directamente al estado de otros contratos a través de una técnica llamada raw querying. Sin embargo, un contrato nunca puede escribir en el estado de otro contrato.

Otro aspecto importante relacionado con CosmWasm es la localidad: Para que dos actores se comuniquen, el creador o usuario de un contrato debe enviar un mensaje al actor. Los actores sólo pueden comunicarse con otros actores después de haber recibido la dirección del otro actor. Esta es una forma flexible de establecer topologías de forma distribuida, y sólo requiere codificar el formato de los datos que se pasarán a dichas direcciones. Una vez que se establezcan algunas interfaces estándar (como ERC20, ERC721, ENS, etc.), podremos soportar la composibilidad entre grandes clases de contratos y diferentes códigos de respaldo.

Beneficios de seguridad

Al imponer un estado interno privado, un contrato dado puede garantizar todas las transiciones válidas en su estado interno. Esto contrasta con el modelo de capacidades utilizado en Cosmos SDK, donde a los módulos de confianza se les pasa un StoreKey en su constructor, lo que permite el acceso completo de lectura y escritura al almacenamiento de otros módulos. En el SDK de Cosmos, podemos auditar los módulos antes de llamarlos, y pasar de forma segura un conjunto tan poderoso de derechos en tiempo de compilación. Sin embargo, en un sistema de contratos inteligentes, no hay comprobaciones en tiempo de compilación, y necesitamos establecer límites más estrictos entre los contratos. Esto nos permite razonar exhaustivamente sobre todas las transiciones posibles en el estado de un contrato (y utilizar métodos de tipo quick-check para probarlo).

Como se mencionó anteriormente, la ejecución serializada evita todas las ejecuciones concurrentes del código de un contrato. Al forzar la ejecución serializada, el contrato escribirá todos los cambios en el almacenamiento antes de salir y tendrá una vista apropiada cuando el siguiente mensaje sea procesado. La forma en que CosmWasm está construido, se asemeja a tener un mutex automático sobre todo el código del contrato. Esto evita los ataques de reentrada, que son los vectores de ataque más comunes para los contratos inteligentes construidos en Ethereum.

Un ejemplo de ataque de reentrada que se evita por diseño en CosmWasm es el siguiente: El Contrato A llama al Contrato B, que a su vez llama al Contrato A. Puede haber cambios locales en la memoria del Contrato A desde la primera llamada (por ejemplo, deducir un saldo), que aún no se han mantenido, por lo que la segunda llamada puede utilizar el estado obsoleto por segunda vez (por ejemplo, autorizar el envío de un saldo dos veces).

Ejecución atómica

Un problema con el envío de mensajes es comprometer atómicamente un cambio de estado a través de dos contratos. Hay muchos casos en los que queremos asegurarnos de que todos los mensajes devueltos se han procesado correctamente antes de confirmar nuestro estado. Para solucionar este problema, antes de ejecutar un Msg que proviene de una transacción externa, creamos un SavePoint del almacén de datos global y pasamos un subconjunto al primer contrato. A continuación, ejecutamos todos los mensajes devueltos dentro de la misma sub-transacción. Si todos los mensajes tienen éxito, podemos confirmar la subtransacción. Si algún mensaje falla (o nos quedamos sin gasolina), abortamos la ejecución y retrocedemos el estado a antes de que se ejecutara el primer contrato.

Esto nos permite actualizar el código de forma optimista, confiando en el rollback para la gestión de errores. Por ejemplo, si un intercambio coincide con una operación entre dos tokens "ERC20", puede cumplir la oferta y devolver dos mensajes para mover el token A al comprador y el token B al vendedor. Los tokens ERC20 utilizan un concepto de asignación, por lo que el propietario "permite" a la bolsa mover hasta X tokens de su cuenta. Al ejecutar los mensajes devueltos, resulta que el comprador no tiene suficientes tokens B (o proporcionó una asignación insuficiente). Este mensaje fallará, haciendo que se revierta toda la secuencia. Si la transacción falla, la oferta no se marca como cumplida, y ningún token cambia de manos.

Mientras que muchos desarrolladores pueden sentirse más cómodos pensando en llamar directamente al otro contrato en su ruta de ejecución y manejar los errores, casi todos los mismos casos se pueden lograr con un enfoque optimista de actualización y retorno. Además, no hay lugar para cometer errores en el código de gestión de errores del contrato.

Vinculación dinámica de módulos anfitriones

Los aspectos de localidad y acoplamiento suelto significan que ni siquiera necesitamos vincularnos a otros contratos CosmWasm. Podemos enviar mensajes a cualquier cosa para la que el Dispatcher tenga una dirección. Por ejemplo, podemos devolver un SendMsg, que será procesado por el módulo nativo x/bank en Cosmos SDK, moviendo tokens nativos. Como definimos interfaces estándar para composabilidad, podemos definir interfaces para llamar a módulos core y luego pasar la dirección al módulo nativo en el constructor del contrato.

Mensajería entre cadena de bloques

Dado que el modelo Actor no intenta realizar llamadas síncronas a otro contrato, y en su lugar sólo devuelve un mensaje "para ser ejecutado", es una buena opción para realizar llamadas a contratos de cadena cruzada utilizando IBC. La única advertencia aquí es que la garantía de ejecución atómica que proporcionamos anteriormente ya no se aplica. La otra llamada no será realizada por el mismo despachador, por lo que necesitamos almacenar un estado intermedio en el propio contrato. Esto significa un estado que no se puede cambiar hasta que se conozca el resultado de la llamada IBC. Una vez conocido el resultado de la llamada IBC, el estado intermedio puede aplicarse o revertirse de forma segura.

Por ejemplo, si queremos mover tokens de la cadena A a la cadena B, primero nos prepararíamos para ejecutar las siguientes transacciones:

  • El contrato A debe reducir el suministro de fichas del emisor.

  • El contrato A debe crear un "depósito en garantía" para los tokens vinculados al ID del mensaje IBC, al remitente y a la cadena receptora.

  • El contrato A compromete el estado y devuelve un mensaje para iniciar una transacción IBC a la cadena B.

  • Si el envío IBC falla, el contrato se revierte atómicamente como se ha indicado anteriormente.

Transcurrido un tiempo, el módulo IBC devolverá al contrato de token un mensaje de éxito o de error/tiempo de espera:

  • El contrato A validaría que el mensaje procede del gestor IBC (autorización) y hace referencia a un ID de mensaje IBC conocido que tiene en custodia.

  • En caso de éxito, se eliminaría el depósito en garantía y los tokens depositados se colocarían en una cuenta para la "Cadena B" (lo que significa que sólo un futuro mensaje IBC de la Cadena B podrá liberarlos).

  • Si se tratara de un error, se eliminaría el depósito en garantía y las fichas depositadas se devolverían a la cuenta del remitente original.

Puede imaginarse un escenario similar en casos de transferencia de propiedad de NFT, apuestas entre cadenas, etc.

Last updated