Demystifying EVM Logs and Events
EVM logs and events play a crucial role in blockchain operations and infrastructure. This article provides an in-depth look at the mechanics of events within the Ethereum blockchain, the distinction between logs and events, and how this knowledge can be applied to create custom indexers. By understanding the underlying principles of EVM logs, developers can efficiently track and respond to blockchain activities, a fundamental component for Oracles and data querying services like The Graph protocol.
Events in blockchains are everywhere. There’s a whole infrastructure around events (including Oracles, Indexers and Graph protocol). We’re going to learn the internals of these events and difference between event and log, with this knowledge equipped we can even build our indexers.
The interesting fact about events is that internally these are the logs generated by Ethereum virtual machine (EVM) and recorded in transaction receipts.
Solidity added abstraction around these logs for event-oriented paradigm. Whenever a new block is generated on the chain, oracles and indexers search through transaction logs in the block to find specific events.
Logs Overview
In EVM, we use the term log instead of event (like console.log); each log generated by EVM has data associated with it and up to 4 topics. These topics have crucial role in indexing infrastructures (as we'll see later). The data can be of any length, but each topic has a fixed 32 bytes size.
EVM has 5 opcodes for logging, LOG0, LOG1 … LOG4, where the postfix number represents number of topics. For example, LOG0 has only data and no topic associated with it while LOG1 has one topic and data associated. The signature of LOG1 is as following:
LOG1(memory_offset,memory_length, topic0)
Let’s execute the following contract in EVM which generates a log.
pragma solidity ^0.8.17;
contract LogContract {
constructor() {
assembly {
log1(0,0,0xCAFEBABE) // We used assembly for LOG1 opcode
}
}
};
We generate a log with no data (0 offset and 0 length) and 1 topic 0xCAFEBABE which would be padded to 32 bytes when compiled. Let’s deploy this contract and get deployment transaction receipt using foundry.
Transaction receipt with generated logs highlighted in red
We get the following logs array in our transaction receipt.
[
{
"address": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
"topics": [
"0x00000000000000000000000000000000000000000000000000000000cafebabe"
],
"data": "0x",
"blockHash": "0x4e385ea8600abfc5288e6c4759b553ec801f207066697ae9aa898d2915e59a66",
"blockNumber": "0x1",
"transactionHash": "0xf36ed1a9b7e514af366b8429e8bbf3ae06b4cf679835b201094c3ee0701df6a7",
"transactionIndex": "0x0",
"logIndex": "0x0",
"transactionLogIndex": "0x0",
"removed": false
}
][
Our data is empty (0x) as it should be and our topic 0xCAFEBABE is present in the topics array.
logsBloom
There’s another section logsBloom in receipt right below the logs. logsBloom has an important role in indexing, it helps indexers search logs though blocks history efficiently. It’s actually a bloom filter data structure and identifies if a certain log is possibly present in the block or not present at all.
Solidity Events Abstraction
We discussed earlier that solidity adds an event abstraction around the EVM logs, let’s dig into it. If we look at the following "safeTransferFrom" transaction at Etherscan in hex format (hex has 32 bytes padding):
This log is generated using LOG4 opcode (4 topics) with zero length data. As we can see there are 4 topics and each topic have 32-byte fixed size. Let’s deep dive into topics.
Topic 0
This is where the Solidity event abstraction comes into play. Each solidity event has a signature, and whenever this event is emitted, the Topic 0 of log is always set to keccak256 hash of event signature. In our case Transfer event was emitted with signature Transfer(address, address, uint256) which has the following hash:
Storing keccak256 hash of event in topic helps indexers, oracles, and watchers detect events in a standard way. As the ERC event signatures are defined in the specs, we can build watchers to detect if any NFT was transferred or any ERC20 tokens were burned.
Topic 1, 2 and 3
These topics in solidity depend on the event signature, in our case the event is ERC721 Transfer, Topic 1 is the from address, Topic 2 is the to address and Topic 3 is the ID of the token.
Note:
Logs/events are not part of blockchain state!
Blockchain nodes are not required to store the logs as these logs are not part of the blockchain state. But we can always regenerate the logs by executing the transactions in EVM again, we’d get the same logs.
Conclusion
We discussed the EVM logs and event abstraction by Solidity around these logs, with this expertise we can now decode the logs next time we see any raw transaction receipt.
A lot our current infrastructure including Oracle networks depend on these events, for example Oracle watch for a Random Request event, whenever this event is emitted, oracle trigger the contract method to pass a random number (VRF).
NFT marketplaces use indexers to index NFTs data and then run queries on them such as filtering NFTs by traits, this filtering is not feasible directly because we’ve to read the whole blockchain; instead, our indexers index the chain once, storing all the related data in a local database so we can then run queries.
We can build our indexing products and infrastructures with the knowledge of event internals and fast indexing. How these indexers index so efficiently is still a mystery, which we may demystify next time (or may be not?).
This article was written by Humayun Javed, a blockchain engineer @ antematter.io.