Los eventos de solidez son lo más parecido a una declaración de "impresión" o "console.log" en Ethereum. Te explicamos cómo funcionan y cuándo usarlos. También entraremos en muchos detalles técnicos que a menudo se omiten en otros recursos.
Aquí hay un ejemplo mínimo para emitir un evento de solidez.
contract ExampleContract { // We will explain the significance of the indexed parameter later. event ExampleEvent(address indexed sender, uint256 someValue); function exampleFunction(uint256 someValue) public { emit ExampleEvent(sender, someValue); } }
Quizás los eventos más conocidos son los que emiten los tokens ERC20 cuando se transfieren. El remitente, el destinatario y la cantidad se registran en un evento.
emit Transfer(from, to, amount);
¿No es esto redundante? Ya podemos mirar las transacciones pasadas para ver las transferencias, y luego podríamos mirar los datos de llamada de la transacción para ver la misma información.
Esto es correcto, uno podría eliminar eventos y no tener ningún efecto en la lógica comercial del contrato inteligente. Sin embargo, esta no sería una forma eficiente de ver la historia.
El cliente de Ethereum no tiene una API para enumerar las transacciones por "tipo", por ejemplo, "todas las transacciones de transferencia de un ERC20". Estas son sus opciones si desea consultar las transacciones:
obtenerTransacción
getTransactionFromBlock
La API getTransactionFromBlock
solo puede decirle qué transacciones ocurrieron en un bloque en particular, no puede apuntar a un contrato inteligente que se dirija a múltiples bloques.
getTransaction
solo puede inspeccionar transacciones para las que conoce el hash de transacción.
Los eventos, por otro lado, se pueden recuperar mucho más fácilmente.
Aquí están las opciones de cliente de Ethereum:
eventos
eventos.todosloseventos
obtenerEventosPasados
Cada uno de estos requiere especificar la dirección del contrato inteligente que el solicitante desea examinar y devuelve un subconjunto (o todos) de los eventos que emitió un contrato inteligente de acuerdo con los parámetros de consulta especificados.
Esta es la idea clave de por qué usaría Eventos para realizar un seguimiento de las transacciones en lugar de las transacciones en sí : Ethereum no proporciona un mecanismo para obtener todas las transacciones de un contrato inteligente, pero sí proporciona un mecanismo para obtener todos los eventos de un contrato inteligente.
¿Por qué es esto? Hacer que los eventos se puedan recuperar rápidamente requiere una sobrecarga de almacenamiento adicional. Si Ethereum hiciera esto para cada transacción, la cadena sería considerablemente más grande. Con los eventos, los programadores de Solidity pueden ser selectivos sobre qué tipo de información vale la pena pagar por la sobrecarga de almacenamiento adicional, para permitir una recuperación rápida fuera de la cadena.
Este es un ejemplo del uso de la API descrita anteriormente. En este código, el cliente se suscribe a eventos de un contrato inteligente. Estos ejemplos están todos en Javascript.
Este código activa una devolución de llamada cada vez que un token ERC20 emite un evento de transferencia.
const { ethers } = require("ethers"); // const provider = your provider const abi = [ "event Transfer(address indexed from, address indexed to, uint256 value)" ]; const tokenAddress = "0x..."; const contract = new ethers.Contract(tokenAddress, abi, provider); contract.on("Transfer", (from, to, value, event) => { console.log(`Transfer event detected: from=${from}, to=${to}, value=${value}`); });
Si queremos ver los eventos retroactivamente, podemos usar el siguiente código. En este ejemplo, buscamos transacciones de aprobación en el pasado en un token ERC20.
const ethers = require('ethers'); const tokenAddress = '0x...'; const filterAddress = '0x...'; const tokenAbi = [ { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } ]; const tokenContract = new ethers.Contract(tokenAddress, tokenAbi, provider); // this line filters for Approvals for a particular address. const filter = tokenContract.filters.Approval(filterAddress, null, null); tokenContract.queryFilter(filter).then((events) => { console.log(events); });
Si desea buscar un intercambio entre dos direcciones conocidas particulares (si existe tal transacción), ethers.js. código sería el siguiente:
tokenContract.filters.Transfer(address1, address2, null);
Aquí hay un ejemplo similar en web3.js en lugar de ethers.js. Tenga en cuenta que se agregan los parámetros de consulta fromBlock
y toBlock
(para indicar que solo nos importan los eventos entre estos bloques), y demostraremos la capacidad de escuchar varias direcciones como remitentes. Las direcciones se combinan con la condición "O".
const Web3 = require('web3'); const web3 = new Web3('https://rpc-endpoint'); const contractAddress = '0x...'; // The address of the ERC20 contract const contractAbi = [ { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } ]; const contract = new web3.eth.Contract(contractAbi, contractAddress); const senderAddressesToWatch = ['0x...', '0x...', '0x...']; // The addresses to watch for transfers from const filter = { fromBlock: 0, toBlock: 'latest', topics: [ web3.utils.sha3('Transfer(address,address,uint256)'), null, senderAddressesToWatch, ] }; contract.getPastEvents('Transfer', { filter: filter, fromBlock: 0, toBlock: 'latest', }, (error, events) => { if (!error) { console.log(events); } });
El ejemplo anterior funciona porque el evento Aprobar (y Transferir) en ERC20 establece que el remitente sea indexado. Aquí está la declaración del evento de aprobación ERC20 en Solidity.
event Approval(address indexed owner, address indexed spender, uint256 value);
Si el argumento "propietario" no estaba indexado, el código javascript anterior fallaría silenciosamente. La implicación aquí es que no puede filtrar los eventos ERC20 que tienen un value
específico (cantidad de token) para la transferencia, porque eso no está indexado. Debe extraer todos los eventos y filtrarlos por el lado de javascript; no se puede hacer en el cliente Ethereum.
Un argumento indexado para una declaración de evento se denomina tema .
La mejor práctica generalmente aceptada para los eventos es registrarlos siempre que ocurra un cambio de estado potencialmente importante. Algunos ejemplos incluyen:
Cambiar el titular del contrato
Éter en movimiento
Realización de un comercio
No todos los cambios de estado requieren un evento. La pregunta que los desarrolladores de Solidity deberían hacerse es "¿alguien estaría interesado en recuperar o descubrir esta transacción rápidamente?"
Esto requerirá cierto juicio subjetivo. Recuerde, un parámetro no indexado no se puede buscar directamente, pero aun así puede ser información útil cuando va acompañado de un parámetro indexado. Una buena manera de obtener una intuición para esto es observar cómo las bases de código establecidas diseñan sus eventos.
Como regla general, las cantidades de criptomonedas no deben indexarse y una dirección sí, pero esta regla no debe aplicarse a ciegas. Los índices solo le permiten obtener rápidamente valores exactos, no un rango de valores.
Un ejemplo de esto sería agregar un evento cuando se acuñan tokens porque las bibliotecas subyacentes ya emiten este evento.
Los eventos están cambiando de estado; alteran el estado de la cadena de bloques almacenando el registro. Por lo tanto, no se pueden usar en funciones de vista (o puras).
Los eventos no son tan útiles para depurar como lo son console.log e print en otros idiomas; debido a que los eventos en sí mismos cambian de estado, no se emiten si una transacción se revierte.
Para los argumentos no indexados, no existe un límite intrínseco para la cantidad de argumentos, aunque, por supuesto, se aplican límites de gas y tamaño del contrato. El siguiente ejemplo sin sentido es solidez válida:
contract ExampleContract { event Numbers(uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256); }
De manera similar, no existe un límite intrínseco para la longitud de cadenas o matrices almacenadas en un registro.
Sin embargo, no puede haber más de tres argumentos indexados (temas) en un evento. Un evento anónimo puede tener 4 argumentos indexados (cubriremos esta distinción más adelante).
Un argumento con cero eventos también es válido.
Los siguientes eventos se comportan de manera idéntica.
event NewOwner(address newOwner); event NewOwner(address);
En general, incluir el nombre de la variable sería ideal porque la semántica detrás del siguiente ejemplo es muy ambigua (¡no es así como debe declarar eventos!)
event Trade(address,address,address,uint256,uint256);
Podemos adivinar que las direcciones corresponden al remitente, y las direcciones del token, mientras que los uint256es corresponden a las cantidades, pero esto es difícil de descifrar.
Es convencional poner en mayúscula el nombre de un evento, pero el compilador no lo requiere. Esta sería una declaración mucho mejor:
event Trade(address trader, address token1, address token2, uint256 token1Amount, uint256 token2Amount);
Cuando un evento se declara en un contrato padre, puede ser emitido por el contrato hijo. Los eventos son internos y no se pueden modificar para que sean privados o públicos.
Aquí hay un ejemplo:
contract ParentContract { event NewNumber(uint256 number); function doSomething(uint256 number) public { emit NewNumber(number); } } contract ChildContract is ParentContract { function doSomethingElse(uint256 number) public { emit NewNumber(number); // valid } }
De manera similar, los eventos pueden declararse en una interfaz y usarse en el elemento secundario, como en el siguiente ejemplo.
interface IExampleInterface { event Deposit(address indexed sender, uint256 amount); } contract ExampleContract is IExampleInterface { function deposit() external payable { emit Deposit(msg.sender, msg.value); // also valid } }
La EVM (Ethereum Virtual Machine) identifica los eventos con el keccak256 de su firma.
Para las versiones de solidity 0.8.15 o superiores, también puede recuperar el selector mediante el miembro .selector.
pragma solidity ^0.8.15; contract ExampleContract { event SomeEvent(uint256 blocknum, uint256 indexed timestamp); function selector() external pure returns (bool) { // true return SomeEvent.selector == keccak256("SomeEvent(uint256,uint256)"); } }
El selector de eventos es en realidad un tema en sí mismo (lo discutiremos más adelante en una sección posterior).
Marcar variables como indexadas o no no cambia el selector.
Los eventos se pueden marcar como anónimos, en cuyo caso no tendrán selector. Esto significa que el código del lado del cliente no puede aislarlos específicamente como un subconjunto como nuestros ejemplos anteriores. La única manera de que el código del lado del cliente vea un evento anónimo es obtener todos los eventos del contrato inteligente.
pragma solidity ^0.8.15; contract ExampleContract { event SomeEvent(uint256 blocknum, uint256 timestamp) anonymous; function selector() public pure returns (bool) { // ERROR: does not compile, anonymous events don't have selectors return SomeEvent.selector == keccak256("SomeEvent(uint256,uint256)"); } }
Debido a que la firma del evento se usa como uno de los índices, una función anónima puede tener cuatro temas indexados, ya que la firma de la función se "libera" como uno de los temas.
Un evento anónimo puede tener hasta cuatro temas indexados. Un evento no anónimo puede tener hasta tres.
contract ExampleContract { // valid event SomeEvent(uint256 indexed, uint256 indexed, address indexed, address indexed) anonymous; }
Los eventos anónimos rara vez se utilizan en la práctica.
Esta sección describe eventos en el nivel de ensamblado de EVM. Esta sección se puede omitir para programadores nuevos en el desarrollo de blockchain .
Para recuperar cada transacción que haya ocurrido con un contrato inteligente, el cliente de Ethereum tendría que escanear cada bloque, lo que sería una operación de E/S extremadamente pesada; pero Ethereum utiliza una optimización importante.
Los eventos se almacenan en una estructura de datos de Bloom Filter para cada bloque. Un Bloom Filter es un conjunto probabilístico que responde eficientemente si un miembro está en el conjunto o no. En lugar de escanear todo el bloque, el cliente puede preguntarle al filtro bloom si se emitió un evento en el bloque. Esto permite al cliente escanear la cadena de bloques mucho más rápido para encontrar eventos.
Los filtros Bloom son probabilísticos: a veces devuelven incorrectamente que un elemento es miembro del conjunto, incluso si no lo es. Cuantos más miembros se almacenen en un filtro de floración, mayor será la posibilidad de error y más grande debe ser el filtro de floración (en cuanto al almacenamiento) para compensar esto. Debido a esto, Ethereum no almacena todas las transacciones en un filtro Bloom. Hay muchos menos eventos que transacciones. Esto mantiene manejable el tamaño de almacenamiento en la cadena de bloques.
Cuando el cliente obtiene una respuesta de membresía positiva de un filtro de floración, debe escanear el bloque para verificar que se haya producido el evento. Sin embargo, esto solo sucederá para un pequeño subconjunto de bloques, por lo que, en promedio, el cliente de Ethereum ahorra muchos cálculos al verificar primero el filtro de floración para detectar la presencia de eventos.
En la representación intermedia de Yul, la distinción entre argumentos indexados (temas) y argumentos no indexados se vuelve clara.
Las siguientes funciones yul están disponibles para emitir eventos (y su código de operación EVM lleva el mismo nombre). La tabla se copia de la documentación de yul con alguna simplificación.
código de operación | Uso |
---|---|
log0(p, s) | registro sin temas y datos mem[p…(p+s)) |
log1(p, s, t1) | registro con tema t1 y datos mem[p…(p+s)) |
log2(p, s, t1, t2) | registro con temas t1, t2 y datos mem[p…(p+s)) |
log3(p, s, t1, t2, t3) | registro con temas t1, t2, t3 y datos mem[p…(p+s)) |
log4(p, s, t1, t2, t3, t4) | registro con temas t1, t2, t3, t4 y datos mem[p…(p+s)) |
Un registro puede tener hasta 4 temas, pero un evento de solidez no anónimo puede tener hasta 3 argumentos indexados. Esto se debe a que el primer tema se usa para almacenar la firma del evento. No hay código de operación o función Yul para emitir más de cuatro temas.
Los parámetros no indexados simplemente se codifican en abi en la región de memoria [p…(p+s)) y se emiten como una secuencia de bytes larga.
Recuerde anteriormente que no había límite en principio para la cantidad de argumentos no indexados que puede tener un evento en Solidity. La razón subyacente es que no hay un límite explícito sobre cuánto tiempo toma la región de memoria a la que apuntan los dos primeros parámetros del código de operación de registro. Por supuesto, existen límites proporcionados por el tamaño del contrato y los costos de gas de expansión de memoria.
Los eventos son sustancialmente más baratos que escribir en variables de almacenamiento. Los eventos no están destinados a ser accesibles mediante contratos inteligentes, por lo que la relativa falta de gastos generales justifica un costo de gasolina más bajo.
La fórmula de cuánto gas cuesta un evento es la siguiente ( fuente ):
375 + 375 * num_topics + 8 * data_size + mem_expansion cost
Cada evento cuesta al menos 375 de gasolina. Se paga 375 adicionales por cada parámetro indexado. Un evento no anónimo tiene el selector de eventos como un parámetro indexado, por lo que el costo se incluye la mayor parte del tiempo. Luego pagamos 8 veces el número de palabras de 32 bytes escritas en la cadena. Debido a que esta región se almacena en la memoria antes de ser emitida, también se debe tener en cuenta el costo de expansión de la memoria.
En general, la intuición de que cuanto más registras, más pagas en gasolina es correcta.
Los eventos son para que los clientes recuperen rápidamente las transacciones que pueden ser de su interés. Aunque no alteran la funcionalidad del contrato inteligente, permiten al programador especificar qué transacciones deben recuperarse rápidamente. Esto facilita que las dapps resuman rápidamente información importante.
Los eventos son relativamente baratos en términos de gas en comparación con el almacenamiento, pero el factor más importante en su costo es la cantidad de parámetros indexados, suponiendo que el codificador no use una cantidad excesiva de memoria.
¿Te gusta lo que ves aquí? Vea nuestro Bootcamp de solidez avanzado para obtener más información.
La imagen principal de este artículo fue generada por AI Image Generator de HackerNoon a través del mensaje "Eventos en Solidity".