Tiempo estimado: 6 minutos de lectura
¿Qué son los eventos en Solidity?
Los eventos en Solidity son estructuras de información que se almacenan en el log de transacciones, una sección especial de la blockchain accesible mientras permanezca el bloque en que quedó registrada la transacción que emitió el evento.
Cada evento queda asociado en la blockchain a la dirección del smart contrat que lo emitió.
Los eventos en Solidity sirven para registrar datos accesibles en lectura desde clientes externos (se pueden incluso suscribir a determinados eventos para recibir las nuevos ocurrencias) como por ejemplo las aplicaciones descentralizadas o un backend centralizado.
Como no pueden ser accedidos desde los smart contract, su costo de almacenamiento en la blockchain (gas) es relativamente bajo. Por este motivo se usan para guardar informaciones muy variadas que dan cuenta de los hitos alcanzados en la ejecución de los smart contracts. En ocasiones se usan también para complementar los datos almacenados en los propios smart contracts, registrando grandes estructuras de información a bajo coste.
¿Cómo se crean los eventos en Solidity?
Sintácticamente se definen con la palabra clave «event», admitiendo parámetros al estilo de los errores personalizados en Solidity y pueden declararse a nivel de fichero o dentro de contratos convencionales, interfaces e incluso librerías.
Un ejemplo de evento emitido en un contrato de token ERC721 sería el siguiente:
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
Se observa que en la definición de los parámetros se puede agregar la palabra clave «indexed». Esto permite añadirlos en la sección especial de tópicos en lugar de en la sección común de datos. Los parámetros de este tipo, considerados tópicos del evento, se pueden utilizar a modo de filtros por los clientes externos en las búsquedas de eventos emitidos en la blockchain, facilitando y agilizando la tarea.
Cada tópico sólo puede almacenar palabras de 32 bytes; en caso de usar tipos referenciados como parámetros indexados (por ejemplo un string) se almacenará un hash en su lugar (keccak-256).
Un evento puede albergar hasta 4 tópicos, siendo siempre por defecto el tópico 0 el hash del «event signature» (el nombre del evento y el tipo de sus parámetros); de este modo siempre se permite filtrar por el nombre del evento.
Siendo así, queda espacio para definir hasta 3 parámetros de tipo indexado en cada evento. Existiría la posibilidad de omitir el tópico 0 para así contar con hasta 4 parámetros de tipo indexado. Se puede lograr mediante los eventos anónimos.
Para el ejemplo anterior:
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) anonymous;
Al emitir un evento anónimo no quedará registro de su nombre, por tanto se dificultarán las labores de filtrado y búsqueda de información en la blockchain. En general no se considera una práctica adecuada ya que va en contra del principio de transparencia inherente a la tecnología.
El resto de parámetros que no son de tipo indexado, se almacenan en la parte de datos del evento de la Blockchain en formato «ABI-encoded». Estos parámetros no se pueden usar como filtros en las búsquedas.
Recomiendo leer esta guía de eventos en Solidity para profundizar en los detalles técnicos.
¿Cómo depurar eventos en Hardhat?
La correcta emisión de los eventos programados en los smart contracts debería ser comprobada en las baterías de pruebas desarrolladas.
Para depurar los eventos mediante Hardhat se cuenta con varias extensiones de la herramienta chai.
Para comprobar que se emite el evento del ejemplo con unos determinados parámetros:
it('emits a transfer event', async function () {
await expect(contract.safeTransferFrom(owner, receiver, tokenId))
.to.emit(contract, 'Transfer').withArgs(owner, receiver, tokenId);
});
En caso de no querer comprobar específicamente el valor de un determinado argumento, se puede usar el predicado «anyValue» en su lugar:
it('emits a transfer event', async function () {
await expect(contract.safeTransferFrom(owner, receiver, tokenId))
.to.emit(contract, 'Transfer').withArgs(owner, receiver, anyValue);
});
La funcionalidad de los predicados en la herramienta chai permite evaluar si un valor cumple una determinada condición para darlo por bueno. El predicado «anyValue» devuelve siempre «true».
La siguiente función comprobaría mediante un predicado si el «tokenId» del evento «Transfer» es par:
function isEven(x) {
return x % 2n === 0n;
}
it('emits a transfer event', async function () {
await expect(contract.safeTransferFrom(owner, receiver, tokenId))
.to.emit(contract, 'Transfer').withArgs(owner, receiver, isEven);
});
Esta particularidad de los predicados podría ser útil también para capturar determinados valores emitidos en un evento y asignarlos a variables dentro de los tests.
En el siguiente ejemplo, se captura la dirección de un contrato creado en la llamada al smart contract «createContract» a través del evento emitido «ContractCreated»:
let contractAddress;
getAddress = (address) => {
contractAddress = address;
return true;
}
await expect(contract.createContract())
.to.emit(contract, "ContractCreated")
.withArgs(getAddress, anyValue, anyValue, anyValue);
Otro caso de uso sería asegurar que no se emite un determinado evento:
it('emits a transfer event', async function () {
await expect(contract.safeTransferFrom(owner, receiver, tokenId))
.to.not.emit(contract, 'Transfer');
});
¿Cómo se accede a los eventos desde clientes externos?
Existen numerosos mecanismos para acceder a la información almacenada en la blockchain a través de los eventos emitidos en los smart contracts.
Para las DApps, en función de la librería cliente utilizada para acceder a la red, se contará con unos métodos específicos.
La llamada json-RPC general para acceder a los eventos emitidos es eth_getLogs.
Como elementos de filtrado se incluyen entre otros los bloques inicial y final, la dirección del contrato emisor del evento y los parámetros indexados.
Al tratarse de una llamada que podría devolver mucha información (en función de los filtros usados), muchos proveedores limitan el número de resultados devueltos o el tiempo máximo de la consulta (revisar por ejemplo las condiciones de Alchemy o Infura).
Existen otras alternativas para acceder a esta información como las que proporcionan los exploradores de bloques a través de sus APIs o los servicios de algún «indexador» como The Graph.
Otras llamadas y APIs permiten incluso servicios de subscripción para recibir de manera puntual la información de los nuevos eventos emitidos.
A continuación se detallan varias formas de acceder los eventos a través de la librería viem en una script de Hardhat. Para ello conviene seguir la guía sobre integración de viem.
Se observa el uso de parámetros indexados del evento para filtrar los resultados:
let manager = await hre.viem.deployContract("MembershipManager", []);
const groupId = 1;
let hash;
// Calls contract method that emits a ContractCreated event
// Event signature is "event ContractCreated(uint256 indexed id,uint256 indexed groupId,string name)"
hash = await manager.write.createContract([1, groupId, "contract1"]);
await publicClient.waitForTransactionReceipt({ hash });
hash = await manager.write.createContract([2, groupId, "contract2"]);
await publicClient.waitForTransactionReceipt({ hash });
// This method just retrieves the latest emitted event
let contractCreated = await manager.getEvents.ContractCreated();
// This method retrieves every filtered event by groupId parameter
parentCreated = await publicClient.getContractEvents({
address: manager.address,
abi: membershipManagerAbi,
eventName: 'ContractCreated',
fromBlock: 'earliest',
toBlock: 'latest',
args: {
groupId,
}
})
// This method retrieves every filtered event by groupId parameter
parentCreated = await publicClient.getLogs({
address: manager.address,
fromBlock: 'earliest',
toBlock: 'latest',
args: {
groupId,
},
event: parseAbiItem('event ContractCreated(uint256 indexed id,uint256 indexed groupId,string name)')
});
Conclusión: registro de información para fiabilizar y descentralizar
La emisión de eventos en los smart contracts de la blockchain constituye un mecanismo para informar a las DApps y a otros clientes externos acerca del resultado de la ejecución de las transacciones.
También es muy útil para almacenar información de manera descentralizada más allá del estado de los smart contracts, con bajo consumo de gas.
Hoy día el principal inconveniente radica en el alto consumo de recursos que puede conllevar la búsqueda de eventos en la blockchain, siempre en función de los filtros utilizados. Esto puede imponer limitaciones tanto en el lado del proveedor que posibilita el acceso al nodo en modo lectura a la blockchain como por parte de la DApp o cliente externo que tiene que procesar la información devuelta.
Para paliar estas dificultades surgen agregadores e indexadores que preprocesan la información para devolver resultados más acotados y precisos.
Otro inconveniente que podría surgir en el futuro es el posible purgado de los eventos en la blockchain o la implementación de cambios que no sean compatibles con los mecanismos actuales.
A continuación se citan algunas propuestas en este sentido:
- EIP-7668 (remove bloom filters): propuesta para suprimir los eventos de la blockchain por su alto coste de almacenamiento acumulado con el paso del tiempo. Parece haber sido desestimada.
- EIP-7745 (two dimensional log filter data structure): propuesta para mantener los eventos en la blockchain mejorando las estructuras de datos para optimizar el espacio utilizado. Actualmente es un borrador de trabajo.
¿Usas eventos de forma habitual en tus smart contracts? ¿Qué utilidades les das? Puedes escribir tus comentarios para compartir tus experiencias.
Contáctame si quieres desarrollar una aplicación descentralizada capaz de recoger toda su información directamente desde la blockchain mediante eventos y otros mecanismos. ¡Muchas gracias!