Ethereum logs tutorial series: Logs and filters

Introduction

Alright, so why do programmers never get lost in the woods?! Because they always follow the trail of logs they've left behind (ba-dum-tss).

In the world of software development, logs are an indispensable tool for understanding and troubleshooting the behavior of applications. By acting as a detailed journal of system events, errors, and other relevant information, logs provide developers with crucial insights into the inner workings of their programs. As applications continue to grow more complex and interconnected, the importance of logs becomes increasingly apparent.

In Ethereum, logs take on a distinct role compared to those in traditional software development. They provide a mechanism to store and access data generated by smart contracts during their execution. These logs, which are stored as immutable entries within transaction receipts, provide insights into events, state changes, and data storage related to smart contracts. Now, the utilization of the said information is only possible through convenient means of access to these logs. This is where filters come in.

Filters are mechanisms that allow external applications, developers, and users to efficiently search, access, and monitor logs generated by the Ethereum blockchain. By applying specific criteria to filter logs, they enable targeted retrieval of relevant information from the vast amount of data stored in the form of logs.

One can think of Ethereum logs as newspaper articles. They contain recorded information about specific events, stories, or data, just as logs hold information about events and data generated by smart contracts. Newspapers provide a means of communication and record-keeping, much like logs in the Ethereum ecosystem.

On the other hand, filters can be compared to search engines that help users find relevant articles or information from newspapers based on specific criteria or keywords. Filters in Ethereum allow developers and DApps to efficiently access specific data from logs or other aspects of the blockchain without having to go through the entire blockchain, just as search engines enable users to find the exact information they are looking for without reading all available newspapers.

Now, what follows is an in-depth analysis of Ethereum logs, filters, and their working. Whether you're a seasoned Ethereum developer or just getting started with the platform, this in-depth analysis will provide you with the knowledge you need to make the most of Ethereum logs and filters. Let's get started!

Analyzing Ethereum logs

Ethereum logs are data structures created as a result of events emitted by smart contracts during their execution. The key difference between logs and events is that events are a programming feature in Solidity that enables smart contracts to communicate and interact with external applications, while logs are the data structures generated by the Ethereum Virtual Machine (EVM) when events are emitted. Smart contracts can emit events to indicate specific occurrences or state changes, which are logged and stored as part of transaction receipts.

Ethereum logs primarily serve two main purposes: event logging and data storage. Developers and DApps can access these logs to trigger further actions or to track important events. Additionally, logs offer a cost-effective way to store data off-chain while maintaining a reference on the blockchain, reducing gas costs and enhancing efficiency.

📘

To get a more in-depth idea about event logs, check out Tracking some Bored Apes: The Ethereum event logs tutorial

To understand the basic components of an Ethereum log, let's consider a simple example of a token transfer event log.

The Transfer event is used in a simple token smart contract to record and communicate token transfers between addresses:

/**
 * @dev Emitted when `value` tokens are moved from one account (`from`) 
 * to another (`to`).
 * Note that `value` may be zero.
 */
event Transfer(address indexed from, address indexed to, uint256 value);

The event takes three parameters: the sender's address, the recipient's address, and the transferred token value. When the transfer function is called within the smart contract, the Transfer event is emitted, generating the following Ethereum log entry:

{
    address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x00000000000000000000000037b2b98ea5d620a4064fc954e9f374c8a28e2125',
      '0x000000000000000000000000dc412bbc1875e588166211defa9c84ac195094cf'
    ],
    data: '0x0000000000000000000000000000000000000000000000000000000047868c00',
    blockNumber: 14673517,
    transactionHash: '0x63aff25171e502638926200a480fbd0365ca0c76d036acaa866f8af5d26e9a61',
    transactionIndex: 311,
    blockHash: '0xfa470ae2ccbe4d1a83b2ab039e300b4fc6d2904db80f8f8a0f4ab890f4240a09',
    logIndex: 531,
    removed: false,
    id: 'log_3cd63a01'
  }

The basic components of the given Ethereum log include:

  1. address — the address of the contract that generated this log.
  2. topics — an array of topics associated with the log. Topics are indexed event parameters that can be used to filter logs.
  3. data — the log data as a hexadecimal string. This contains the non-indexed event parameters.
  4. blockNumber — the number of the block in which the log was created.
  5. transactionHash — the hash of the transaction that generated this log.
  6. transactionIndex — the index of the transaction within the block.
  7. blockHash — the hash of the block that contains this log.
  8. logIndex — the index of the log within the block. In this case, the log index is 531.
  9. removed — a boolean value that indicates whether the log was removed due to a chain reorganization.
  10. id — a unique identifier for this log entry.

📘

Find more explanations and examples in the Chainstack API reference | eth_getLogs.

Understanding filters

Ethereum filters are essential tools for developers when working with Ethereum-based applications. They facilitate real-time monitoring and interaction with the Ethereum blockchain, offering developers the ability to listen for specific events, state changes, or transactions. Filters help create responsive, event-driven applications that continuously adapt to changes on the blockchain.

There are three primary types of filters in Ethereum:

Log filters

Log filters are essential for monitoring and reacting to events generated by smart contracts on the Ethereum blockchain. These events can include but are not limited to, transferring tokens, updating contract states, or invoking specific contract functions. Log filters allow developers to fetch logs based on certain conditions, such as:

  • Contract address: Monitor events emitted by a specific smart contract.
  • Event signature: Filter logs by the event's unique signature, which is the Keccak-256 hash of the event definition (e.g., Transfer(address,address,uint256)).
  • Block range: Limit the logs fetched to a specific range of blocks.
  • Indexed event parameters: Filter logs based on specific indexed event parameters, which are used to narrow down the logs relevant to the application's needs.

Logs contain valuable information, including the event signature, contract address, block number, transaction hash, and event parameters. By utilizing log filters, developers can track contract events and update their applications accordingly.

Block filters

Block filters focus on monitoring newly generated blocks on the Ethereum blockchain. These filters are useful for applications that need to react to new blocks, such as:

  • Fetching the latest gas prices
  • Verifying transactions included in a block
  • Analyzing block data, such as miner rewards, difficulty, and uncle blocks

Block filters provide developers with real time updates on new blocks mined, allowing them to retrieve block header data. This data includes the block number, miner, timestamp, parent hash, and other pertinent information. By monitoring new blocks, developers can ensure their applications stay up-to-date with the latest blockchain data.

Pending transaction filters

Pending transaction filters are designed to monitor and track pending transactions in the Ethereum network. These filters are beneficial for applications that require information about unconfirmed transactions, such as:

  • Displaying pending transactions in a user's wallet
  • Analyzing transaction behavior, like gas price bidding or transaction propagation
  • Estimating transaction confirmation times

Pending transaction filters notify developers when a new transaction enters the pending state, providing the transaction hash. Developers can then fetch the transaction data using the hash, which includes the sender, recipient, value, gas price, and other relevant transaction details.

Accessing logs using filters

Prerequisites

Access an Ethereum node:

  1. Head over to Chainstack and create an account.
  2. Deploy an Ethereum Mainnet node in Chainstack.

Set up a Node project:

  1. Install the following dependencies in your system:

    Once you have everything, the next step is to set up a Node project and install the web.js library.

  2. Create a new directory.

  3. Open a terminal in the directory.

  4. Use the following command to initialize a Node project:

    npm init -y
    
  5. Once the project is initialized, use the following command to install the web.js package:

    npm install web3
    
  6. Within the project, create a new JavaScript file, fetchLogs.js, and add the following lines of code:

    const Web3 = require('web3');
    
    // Add your Chainstack node endpoint (HTTPS)
    const chainstackRpcUrl = 'YOUR_CHAINSTACK_ENDPOINT';
    const web3 = new Web3(new Web3.providers.HttpProvider(chainstackRpcUrl));
    
    // USDC token contract address on Ethereum mainnet
    const usdcContractAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
    
    // The Transfer event signature for ERC20 tokens (keccak256 hash of "Transfer(address,address,uint256)")
    const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
    
    // Define the filter options for fetching USDC transfer logs
    const filterOptions = {
      fromBlock: 'latest', // Start from the genesis block
      address: usdcContractAddress,
      topics: [transferEventSignature],
    };
    
    // Fetch logs using the filter options
    web3.eth.getPastLogs(filterOptions).then(logs => {
      // Define the Transfer event ABI
      const transferEventInputs = [
        { type: 'address', name: 'from', indexed: true },
        { type: 'address', name: 'to', indexed: true },
        { type: 'uint256', name: 'value', indexed: false },
      ];
    
      // Process each log
      logs.forEach(log => {
        // Decode the log data
        const decodedLog = web3.eth.abi.decodeLog(transferEventInputs, log.data, log.topics.slice(1));
    
        // Log the decoded transfer event data
        console.log('USDC Transfer:');
        console.log('  From:', decodedLog.from);
        console.log('  To:', decodedLog.to);
    		console.log('  Value (in USDC):', parseInt(decodedLog.value) / 1e6);
      });
    }).catch(error => {
      console.error('Error:', error);
    });
    

    In this code example, we demonstrate how to fetch USDC token transfer event logs using log filters in web3.js. The purpose of this code is to provide a clear and concise example for developers to understand how to interact with the Ethereum blockchain and obtain event logs, specifically for the USDC stablecoin. This information can be useful for applications that need to track token transfers or analyze token transaction history.

The code starts by importing the Web3 library, which is the primary library for interacting with Ethereum nodes. We then provide the Chainstack node endpoint, which is used to connect to the Ethereum Mainnet. The USDC token contract address and the Transfer event signature are defined to be used in the log filter.

We create a filterOptions object, which contains the necessary filter parameters, such as the starting and ending blocks (fromBlock and toBlock), the contract address, and the event signature. This object is used as input for the web3.eth.getPastLogs() method, which fetches logs matching the filter options.

Upon successful retrieval of the logs, the function iterates through each log entry, decoding the log data using the Transfer event ABI. The decoded log information is then printed to the console, showing the sender, recipient, and value of each USDC transfer.

Subscribing to logs

In the above-given code, we use the web3.eth.getPastLogs() to perform a one-time query to the Ethereum node to retrieve logs matching the given filters. The result is a collection of logs from the specified block range that match the filter criteria. This approach is useful for obtaining historical event data or performing a one-time analysis of contract events.

To monitor the logs in real time, we need to use the web3.eth.subscribe() method. When we use the subscribe method, we create an ongoing subscription to a specific event, such as logs, new block headers, or pending transactions. The Ethereum node pushes new data to the subscriber in real time whenever the subscribed event occurs.

This approach is useful for building event-driven applications that need to react to changes on the blockchain as they happen. To use the subscribe function, we need to get the WSS endpoint of your Chainstack node.

📘

You can find more examples and explanations about the subscriptions method in the Chainstack API reference.

Once you fetch the endpoint, use the following code to subscribe to event logs:

const Web3 = require('web3');

// Add your Chainstack node endpoint (WSS)
const chainstackRpcUrl = 'YOUR_CHAINSTACK_ENDPOINT';

// Configure reconnect options for WebsocketProvider
const websocketOptions = {
  clientConfig: {
    reconnect: {
      auto: true, // Automatically attempt to reconnect
      delay: 5000, // Reconnect after 5 seconds
      maxRetries: 10, // Max number of retries
    },
  },
};

const web3 = new Web3(new Web3.providers.WebsocketProvider(chainstackRpcUrl, websocketOptions));

// USDC token contract address on Ethereum mainnet
const usdcContractAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';

// The Transfer event signature for ERC20 tokens (keccak256 hash of "Transfer(address,address,uint256)")
const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';

// Define the filter options for fetching USDC transfer logs
const filterOptions = {
  address: usdcContractAddress,
  topics: [transferEventSignature],
};

// Subscribe to logs and process USDC transfer events
web3.eth.subscribe('logs', filterOptions, (error, log) => {
  if (error) {
    console.error('Error:', error);
    return;
  }

  // Define the Transfer event ABI
  const transferEventInputs = [
    { type: 'address', name: 'from', indexed: true },
    { type: 'address', name: 'to', indexed: true },
    { type: 'uint256', name: 'value', indexed: false },
  ];

  // Decode the log data
  const decodedLog = web3.eth.abi.decodeLog(transferEventInputs, log.data, log.topics.slice(1));

  // Log the decoded transfer event data
  console.log('USDC Transfer:');
  console.log('  From:', decodedLog.from);
  console.log('  To:', decodedLog.to);
  console.log('  Value (in USDC):', parseInt(decodedLog.value) / 1e6);
});

The given code leverages the subscribe method available in the web3.js library to enable real-time monitoring of logs. The subscribe method sets up a WebSocket connection with the Ethereum node, allowing for efficient and continuous updates on new events without the need for polling. In this specific example, the code creates a subscription to the 'logs' event, applying a filter to focus on the USDC token contract's Transfer events. Upon receiving a new log event matching the filter criteria, the callback function is triggered, processing and decoding the event data to extract valuable information about the USDC transfers occurring on the Ethereum network.

The code is also designed to automatically reconnect to the Ethereum node if the WebSocket connection is lost or interrupted. This is important to ensure that the script continues to monitor USDC transfers without manual intervention in case of connection issues.

The reconnection logic is implemented using the clientConfig option when creating a new instance ofWeb3.providers.WebsocketProvider. This option allows you to customize the behavior of the WebSocket client, allowing automatic reconnection attempts following the delay and maximum retries specified in the websocketOptions.

Making the best out of Ethereum logs and filters

When developing Ethereum applications, it's important to ensure that your code is efficient, secure, and scalable. To help you build reliable and performant applications, we have compiled a list of best practices for retrieving and using event logs and filters in Ethereum. These recommendations cover various aspects of development, such as safe block range, optimal coding practices, and more. By following these guidelines, you can improve your application's performance and ensure seamless interactions with the Ethereum network. Here are a few best practices that you can follow while using Ethereum logs and filters:

  1. Use the latest version of web3.js. Make sure to always use the most recent version of the web3.js library, as it includes the latest features, optimizations, and security fixes.
  2. Choose a safe block range. When retrieving event logs, it is essential to choose a reasonable block range. Requesting too large a range may lead to timeouts and slow response times. If you need to retrieve logs from a wide range, consider breaking the request into smaller chunks. Chainstack does not impose strict limitations on the block range you can query. However, for optimal performance, on the Ethereum network, we suggest keeping the block range within 5,000 blocks.
  3. Filter events by indexed parameters. Using indexed parameters in your event filters can significantly improve efficiency by allowing you to target specific logs. Make use of indexed event parameters whenever possible.
  4. Monitor for new events. Use the web3.js library's built-in event subscription feature to monitor new events in real-time. This can help you avoid the need to constantly poll for updates.
  5. Throttle your requests. To avoid overwhelming your Ethereum provider, throttle your requests to a reasonable rate. You can use libraries like lodash to implement request throttling easily.
  6. Use the fromBlock parameter wisely. When retrieving historical event logs, be mindful of the fromBlock parameter. It is often more efficient to start from a block that you know contains relevant events rather than querying from the genesis block.
  7. Cache logs locally. Cache event logs locally to minimize the need for frequent API calls. This can improve your application's performance and reduce the load on your Ethereum provider.
  8. Monitor gas usage. Keep track of gas usage for your contract interactions. Optimizing your contract's gas consumption can save your users money and improve the overall user experience.
  9. Secure your Ethereum provider. For higher security, ensure that you use properly secure connections with API keys and other security measures in your Chainstack nodes.

Conclusion

In conclusion, logs and filters are essential components of Ethereum-based applications. Logs provide valuable information about contract events, while filters allow developers to monitor and interact with the blockchain in real time. By leveraging these tools, developers can build event-driven applications that respond to changes on the blockchain and provide a seamless user experience. It's important to follow best practices when working with logs and filters to ensure that your code is efficient, secure, and scalable. By keeping these recommendations in mind, you can improve your application's performance and provide reliable interactions with the Ethereum network.

📘

See also

About the author

Sethu Raman Omanakuttan

🥑 Developer Advocate @ Chainstack
🛠️ BUIDLs on Ethereum, NEAR, The Graph protocol, and Oasis
💻 Majored in Computer Science and Technology
Sethu Raman | GitHub Sethu Raman | Twitter Sethu Raman | LinkedIN