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:
- A configured Go development environment (Go 1.18 or newer is recommended).
The
go-ethereumSDK installed in your project. You can add it using:go get github.com/ethereum/go-ethereum- Access to an Ethereum node, either by running your own (like Geth or Nethermind) or through a service provider like Infura or Alchemy. This access is typically via a WebSocket or HTTP endpoint.
- The compiled ABI (Application Binary Interface) of the smart contract whose events you want to read. This is usually a JSON file generated during the contract compilation process.
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.
- An event can have up to three
indexedparameters. - The first topic (
vLog.Topics[0]) is always the keccak256 hash of the event signature (e.g.,ItemSet(bytes32,bytes32)). - Subsequent topics (
vLog.Topics[1],vLog.Topics[2], etc.) contain the encoded values of theindexedparameters.
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
}