Reading and Decoding Ethereum Event Logs with Go

·

Ethereum smart contracts can optionally emit "events," which are stored as logs within transaction receipts. These logs provide a valuable way for decentralized applications (dApps) and off-chain services to track contract state changes and specific on-chain occurrences. For developers using the Go programming language (Golang), the go-ethereum library offers powerful tools to read and interpret these events efficiently.

This guide will walk you through the entire process of querying, retrieving, and decoding event logs from the Ethereum blockchain using Go, complete with practical code examples.

Prerequisites for Reading Event Logs

Before you begin, ensure you have the following ready:

Step-by-Step: Querying and Decoding Events

1. Constructing a Filter Query

The first step is to define the scope of your search using a FilterQuery struct from the go-ethereum package. This struct allows you to specify precisely which logs you want to retrieve.

query := ethereum.FilterQuery{
    FromBlock: big.NewInt(2394201), // Starting block number
    ToBlock: big.NewInt(2394201),   // Ending block number (can use big.NewInt(latest) for latest)
    Addresses: []common.Address{    // Array of contract addresses to filter by
        contractAddress,            // Replace with your target contract address
    },
}

You can also filter by specific event topics, which is useful for indexed event parameters.

2. Fetching the Logs

Once the query is defined, use the FilterLogs method of your Ethereum client to execute the query and retrieve all matching logs.

logs, err := client.FilterLogs(context.Background(), query)
if err != nil {
    log.Fatal(err)
}

At this point, logs contains an array of raw, ABI-encoded log data. This data is not human-readable yet.

3. Importing the Contract ABI

To decode the raw log data, you need the contract's ABI. This ABI defines the structure of the events and functions. If you generated a Go binding for your contract using abigen, you can often access the ABI directly from the generated package.

contractAbi, err := abi.JSON(strings.NewReader(string(store.StoreABI))) // Example using a generated 'store' package
if err != nil {
    log.Fatal(err)
}

If you have the raw ABI JSON string, you can read it from a file or a string variable instead.

4. Decoding the Log Data

Iterate through the retrieved logs and decode each one using the ABI. You must know the name of the event and the data types of its parameters. You define a struct in Go whose fields match the types of the event's parameters.

for _, vLog := range logs {
    // Define an anonymous struct matching the event parameters
    event := struct {
        Key   [32]byte
        Value [32]byte
    }{}

    // Unpack the log data into the event struct
    err := contractAbi.Unpack(&event, "ItemSet", vLog.Data) // "ItemSet" is the event name
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Key:", string(event.Key[:]))   // Converts [32]byte to string
    fmt.Println("Value:", string(event.Value[:]))
}

5. Accessing Additional Log Information

Each log (vLog) contains valuable metadata about the transaction and block it originated from.

fmt.Println("Block Hash:", vLog.BlockHash.Hex())
fmt.Println("Block Number:", vLog.BlockNumber)
fmt.Println("Transaction Hash:", vLog.TxHash.Hex())

Understanding Indexed Events and Topics

In Solidity, event parameters can be marked with the indexed attribute. Indexed parameters are not stored in the main log data (vLog.Data) but instead are placed in a separate property called topics.

Here is how you can inspect the topics of a log:

for i, topic := range vLog.Topics {
    fmt.Printf("Topic[%d]: %s\n", i, topic.Hex())
}

You can manually verify the event signature hash:

eventSignature := []byte("ItemSet(bytes32,bytes32)")
hash := crypto.Keccak256Hash(eventSignature)
fmt.Println("Expected Topic0 (Event Signature Hash):", hash.Hex())

👉 Explore more strategies for advanced event filtering

Complete Code Example

The following is a consolidated example putting all the concepts together. It assumes you have a generated contract package (store in this case) from abigen.

File: event_read.go

package main

import (
    "context"
    "fmt"
    "log"
    "math/big"
    "strings"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/ethclient"
    store "your/project/path/contracts" // Import your generated contract bindings
)

func main() {
    // 1. Connect to an Ethereum node
    client, err := ethclient.Dial("wss://rinkeby.infura.io/ws") // Use a valid WS endpoint
    if err != nil {
        log.Fatal(err)
    }

    // 2. Define the contract address to watch
    contractAddress := common.HexToAddress("0x147B8eb97fD247D06C4006D269c90C1908Fb5D54")

    // 3. Create a filter query for a specific block range
    query := ethereum.FilterQuery{
        FromBlock: big.NewInt(2394201),
        ToBlock: big.NewInt(2394201),
        Addresses: []common.Address{contractAddress},
    }

    // 4. Fetch the logs
    logs, err := client.FilterLogs(context.Background(), query)
    if err != nil {
        log.Fatal(err)
    }

    // 5. Parse the contract ABI
    contractAbi, err := abi.JSON(strings.NewReader(string(store.StoreABI)))
    if err != nil {
        log.Fatal(err)
    }

    // 6. Iterate and decode each log
    for _, vLog := range logs {
        fmt.Printf("Block Hash: %s\n", vLog.BlockHash.Hex())
        fmt.Printf("Block Number: %d\n", vLog.BlockNumber)
        fmt.Printf("Tx Hash: %s\n", vLog.TxHash.Hex())

        event := struct {
            Key   [32]byte
            Value [32]byte
        }{}
        err := contractAbi.Unpack(&event, "ItemSet", vLog.Data)
        if err != nil {
            log.Printf("Failed to unpack log: %v", err)
            continue
        }

        fmt.Printf("Decoded Event - Key: %s, Value: %s\n", string(event.Key[:]), string(event.Value[:]))

        // 7. Print topics (for indexed parameters)
        for i, topic := range vLog.Topics {
            fmt.Printf("Topic[%d]: %s\n", i, topic.Hex())
        }
    }

    // 8. (Optional) Calculate and print the event signature hash
    eventSignature := []byte("ItemSet(bytes32,bytes32)")
    hash := crypto.Keccak256Hash(eventSignature)
    fmt.Println("Event Signature Hash (Keccak256):", hash.Hex())
}

Frequently Asked Questions

What is the main difference between vLog.Data and vLog.Topics?
vLog.Data contains the non-indexed parameters of an event in ABI-encoded form. vLog.Topics is an array where the first element is the hash of the event signature, and subsequent elements contain the values of any indexed parameters from the event. Indexed parameters are searchable and filterable, while non-indexed parameters are not.

How can I filter logs for a specific event?
You can filter by a specific event by adding its signature hash to the Topics field in your FilterQuery. For example, to filter for the first topic (the event signature), you would configure your query like this, which drastically reduces the number of logs returned and improves efficiency.

eventSignatureHash := crypto.Keccak256Hash([]byte("ItemSet(bytes32,bytes32)"))
query := ethereum.FilterQuery{
    ...
    Topics: [][]common.Hash{
        {eventSignatureHash},
    },
}

Why would I use WebSocket (WSS) endpoints instead of HTTP?
While both work for one-time queries using FilterLogs, a WebSocket connection is essential for real-time log monitoring using subscription methods like SubscribeFilterLogs. WebSockets provide a persistent connection that allows the node to push new logs to your application the moment they are confirmed, which is not possible with simple HTTP requests.

My Unpack call is failing. What could be wrong?
Common reasons for failure include a mismatch between the event name string and the actual event name in the contract, an incorrect ABI being used for parsing, or a struct definition in Go that does not exactly match the types and order of the event parameters in Solidity. Double-check all these elements for consistency.

Can I read logs from the latest block only?
Yes. You can use the latest tag when setting the ToBlock field in your FilterQuery. However, be aware that the tip of the blockchain is subject to reorgs. For crucial operations, it's often better to wait for a few confirmations by querying blocks slightly older than the latest.

query := ethereum.FilterQuery{
    FromBlock: big.NewInt(latestBlockNumber), // Or use a specific number
    ToBlock: big.NewInt(latestBlockNumber),
    // ... other fields
}

👉 Get advanced methods for handling real-time data streams