Lesson 1 of 4

Find and Fix a Go Bug by Following the Evidence

In this lesson, you will debug one small Go program all the way from a wrong result to a verified fix. The program calculates the balance of a Cardano address from a slice of UTxOs. Instead of starting with a list of tools, you will start with a broken result and use each tool only when it answers a specific question.

Prerequisites

  • Go 1.21+
  • Terminal access
  • Basic familiarity with Go functions, structs, and tests

What You Will Learn

  • How to turn a vague bug report into a reproducible failing test
  • How to narrow a Go bug before changing code
  • How to inspect live state with dlv
  • When go vet and go test -race are useful, and when they are not

The Problem

Suppose you have a small service that should total the lovelace held by one address.

We will keep one running system for the whole lesson:

Address -> Loader -> []UTxO -> BalanceService -> Total balance

The domain model is simple:

type UTxO struct {
    TxHash   string
    Index    uint32
    Lovelace int64
}

Here is the buggy service:

type BalanceService struct{}

func (s BalanceService) Total(utxos []UTxO) int64 {
    var total int64
    for _, utxo := range utxos {
        total = utxo.Lovelace
    }
    return total
}

If the address has three UTxOs worth 2_000_000, 3_000_000, and 5_000_000 lovelace, the correct total is 10_000_000. This code returns 5_000_000.

That is a good debugging problem because:

  • the result is clearly wrong
  • the program is small enough to inspect directly
  • the same workflow scales to larger services later

Step-by-Step Practice

Step 1: Reproduce the Bug with a Focused Test

Do not start by editing production code. First, make the failure repeatable:

func TestBalanceServiceTotal(t *testing.T) {
    svc := BalanceService{}

    utxos := []UTxO{
        {TxHash: "a", Index: 0, Lovelace: 2_000_000},
        {TxHash: "b", Index: 1, Lovelace: 3_000_000},
        {TxHash: "c", Index: 2, Lovelace: 5_000_000},
    }

    got := svc.Total(utxos)
    want := int64(10_000_000)

    if got != want {
        t.Fatalf("got %d, want %d", got, want)
    }
}

Run only the test that proves the bug:

go test ./... -run TestBalanceServiceTotal -v

This first step answers one question: "Can I make the bug fail on demand?"

If the answer is no, you are not ready to debug yet.

Step 2: Try the Smallest Possible Observation

Your first attempt should be small and temporary.

For example, you might log the value seen in each loop iteration:

func (s BalanceService) Total(utxos []UTxO) int64 {
    var total int64
    for _, utxo := range utxos {
        fmt.Println("before:", total, "current:", utxo.Lovelace)
        total = utxo.Lovelace
    }
    return total
}

This often tells you enough to spot a simple bug. Here it reveals that total is being replaced on each loop instead of increased.

That is a useful first move because it is cheap. It becomes a weak move when:

  • the function is called too many times
  • the bad value appears long before the failure shows up
  • you need to inspect state across several stack frames

Step 3: Use dlv When the Value Path Is Unclear

When one print statement is not enough, stop guessing and inspect the program state directly.

dlv is Delve, the standard debugger used with Go programs. It lets you pause execution, inspect variables, and move through code one line at a time.

You usually need to install it yourself:

go install github.com/go-delve/delve/cmd/dlv@latest

After installation, make sure your Go bin directory is on your PATH so the dlv command is available in the terminal.

Run the test under Delve:

dlv test ./path/to/pkg -- -test.run TestBalanceServiceTotal

Inside dlv, set a breakpoint on the loop and step through one iteration at a time:

b balance.go:8
c
n
p total
p utxo

Just enough command background for this lesson:

  • b balance.go:8 sets a breakpoint, which means "pause when execution reaches this line"
  • c continues running until the next breakpoint
  • n runs the next line without diving into another function call
  • p total prints the current value of total

Questions to answer while stepping:

  • What is total before this line runs?
  • What is utxo.Lovelace on this iteration?
  • Does the line update total the way I expect?

That is the real value of a debugger in Go. It is not for "debugging in general." It is for answering a precise state question when logs are no longer enough.

Step 4: Apply the Smallest Correct Fix

Once the root cause is visible, fix only that behavior:

func (s BalanceService) Total(utxos []UTxO) int64 {
    var total int64
    for _, utxo := range utxos {
        total += utxo.Lovelace
    }
    return total
}

Run the same focused test again:

go test ./... -run TestBalanceServiceTotal -v

Only after the targeted test passes should you widen validation:

go test ./...

Weak vs Better Debugging Moves

Weak move:

  • Read the code, guess the bug, and edit immediately.

Better move:

  • Write or run one failing test, inspect the wrong state, then change only the code that caused it.

Why the second approach is better:

  • you know the bug was real before the fix
  • you have a regression test after the fix
  • you avoid mixing diagnosis with random code changes

When to Use go vet and go test -race

These tools are important, but they answer specific questions.

Use go vet when you suspect suspicious code patterns such as:

  • bad format strings
  • copied lock values
  • unreachable or mistaken constructs that static analysis can spot

Run:

go vet ./...

Use go test -race when the bug smells like shared-state trouble:

  • flaky tests
  • inconsistent totals across runs
  • goroutines reading and writing the same data

Run:

go test ./... -race

In this lesson's bug, -race is not the main tool because the problem is deterministic and local. That is the rule of thumb:

  • use go test to reproduce the bug
  • use dlv when the state path is unclear
  • use go vet for suspicious code patterns
  • use -race for concurrency symptoms

Rule of Thumb

Do not open with every Go debugging tool at once.

Start with the smallest question you can answer:

  • Can I reproduce the bug?
  • Which value is wrong?
  • Where does it become wrong?
  • Which tool answers that specific question fastest?

That sequence is more reliable than memorizing a checklist.

You'll Know You're Successful When

  • You can turn a wrong balance result into a failing test before editing code
  • You can explain why the bug returned the last UTxO amount instead of the total
  • You can use dlv to inspect total and utxo inside the loop
  • You know when go vet and go test -race are relevant and when they are not

Practice Tasks

  • Change the bug so the function skips the first UTxO, then write a test that proves the new failure before fixing it.
  • Modify Total to return (int64, error) and add a bug where an error is ignored. Use a test to catch it, then confirm the fix.
  • Build a version of the balance calculation that uses goroutines and a shared accumulator without synchronization. Run go test -race, then fix the race.

Next Steps

  • Continue to lesson 099.2, where you improve the design of the same balance-calculation code once it is correct.
  • Reuse this debugging workflow in later modules when your programs become concurrent, networked, or harder to inspect by sight alone.

Setting Up Dolos

Several modules in this course require a local source of Cardano chain data. You have two mature options in the Go ecosystem:

  • Dolos — Rust, stable, TxPipe. The default for this course.
  • Dingo — Go, Blink Labs, built on top of gOuroboros. Under active development; Plutus V1/V2 validation is not yet complete, but it speaks the same Ouroboros protocols as Dolos.

This lesson walks through Dolos because it's the most predictable path to a working local node today. The rest of the course works the same way against Dingo — swap the socket path and everything downstream (Adder, the starter-kit programs, Apollo submissions) behaves identically. If you want to run an all-Go stack end-to-end, install Dingo instead using its README; the config file differs but the socket semantics are the same.

This lesson explains what Dolos is, what problem it solves, and how to get it running configured for the preprod testnet.


What Is Dolos?

Dolos is a Cardano data node built by TxPipe, written in Rust. It is not a full Cardano node — it does not validate blocks, produce blocks, or participate in consensus. It does one thing: keep a local, up-to-date copy of the chain and serve that data to clients through several APIs.

From the official docs:

"keeping an updated copy of the ledger and replying to queries from trusted clients, while requiring a small fraction of the resources."


What Problem Does Dolos Solve?

When tools like Adder follow the chain, they need somewhere to connect. There are three options:

Option

How it works

Trade-offs

Public relay (TCP)

Connect directly to a public Cardano relay over the internet

Simple, no local setup, but depends on a third party's uptime

Dolos (local socket)

Run Dolos locally; tools connect via a Unix socket

Fast, local, stable — you control your own data source. Course default.

Dingo (local socket)

Run the Go-native Dingo node locally; same socket semantics as Dolos

Fast, local, all-Go stack. Still maturing — Plutus validation in progress.

For this course, tools like Adder connect to Dolos via a local Unix socket. This mirrors how real indexers are deployed — co-located with a data node rather than depending on public infrastructure.

Dolos also exposes multiple APIs beyond the Ouroboros socket — including gRPC and a Mini Blockfrost HTTP endpoint — making it useful across modules as we move into querying and application development.


How Dolos Fits in the Stack

Cardano Network (preprod relays)
        ↓  N2N over TCP (Ouroboros)
   [ Dolos ]  ←  syncing the chain continuously
        ↓  via Unix socket / gRPC / HTTP
  [ Your tools ]  ←  Adder, Apollo, your application

Dolos connects outward to the Cardano network and maintains a local copy of the chain. Your tools connect inward to Dolos. Your application code never needs to reach out to the wider network directly.


Prerequisites

  • Linux, macOS, or Windows
  • ~5–10 GB free disk space for preprod chain data (1 week of history)

Step-by-Step Instructions

Step 1: Install Dolos

The TxPipe docs (docs.txpipe.io/dolos) cover all installation options:

The binary install methods below download the Dolos executable directly to your machine. This is why the prerequisites list disk space — Dolos stores the chain data it syncs locally on your hard drive. If you prefer not to install a binary, Docker is also supported (see below).

macOS / Linux (shell script):

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/txpipe/dolos/releases/latest/download/dolos-installer.sh | sh

macOS / Linux (Homebrew):

brew install txpipe/tap/dolos

Docker (alternative):

docker run -d \
  -v $(pwd)/dolos.toml:/etc/dolos/dolos.toml \
  -v $(pwd)/data:/data \
  ghcr.io/txpipe/dolos:latest daemon

Docker avoids installing a binary locally but you still need the same disk space for the chain data volume. For this course we recommend the binary install — it makes running dolos daemon from your terminal simpler.

Verify the binary install:

dolos --version

Step 2: Configure with dolos init

Create a directory for your Dolos data and config, then run the interactive setup:

mkdir ~/dolos
cd ~/dolos
dolos init

dolos init asks a series of questions and generates a dolos.toml configuration file. Here is the full set of choices for this course and the reasoning for each:

Note: On a fresh install with no existing data, dolos init will also ask whether you want to use Mithril for bootstrapping. Answer Yes — Mithril uses cryptographic snapshots to sync the chain quickly without downloading everything from genesis. This question is skipped if Dolos detects existing chain data on disk.

Question

Answer

Why

Which network are you connecting to?

Cardano PreProd

The testnet we use throughout the course

Do you want us to provide the genesis files?

Yes

Dolos downloads the genesis files automatically — only say No if you're supplying them yourself for a custom network

Which remote peer (relay) do you want to use?

preprod-node.world.dev.cardano.org:30000 (default)

The official IOG/Intersect preprod relay — accept the default

How much history do you want to keep on disk?

1 week

Enough for development without excessive disk usage. "Keep everything" is unnecessary for this course.

Do you want to use Mithril for bootstrapping? (fresh install only)

Yes

Syncs chain history from a verified snapshot rather than from genesis — much faster

Serve clients via gRPC?

Yes

The gRPC API is used in later modules for querying chain state

Serve clients via Blockfrost-like HTTP endpoint?

Yes

Useful for Module 202 queries — enable it now rather than reconfigure later

Serve clients via TRP endpoint?

No

Transaction submission is handled by Apollo/gOuroboros in this course, not Dolos

Serve clients via Ouroboros (node socket)?

Yes

This is what Adder connects to in Module 201 — the most critical option

Act as a relay for other nodes?

No

We're running Dolos as a local dev tool, not a network participant

Expected result:

config saved to dolos.toml
Dolos is ready!
- run `dolos daemon` to start the node

Step 3: Bootstrap the Chain

If this is a fresh Dolos installation (no existing data), you need to sync chain history before running the daemon. Rather than syncing from genesis, Dolos can import a Mithril snapshot — a cryptographically verified point-in-time copy of the chain state:

dolos bootstrap mithril

This downloads and imports the snapshot. It may take several minutes depending on your connection speed.

**Why it matters:**Syncing preprod from genesis would take a very long time. Mithril bootstrapping gets you to a recent chain tip quickly without sacrificing data integrity.

If you already have existing Dolos data from a previous setup, dolos init will detect it and skip the bootstrap automatically.


Step 4: Run the Daemon

From your ~/dolos directory:

dolos daemon

Dolos connects to the upstream relay and begins following the live chain. You will see log output as it syncs forward to the current tip. Once there, slot numbers in the logs slow to roughly one every 20 seconds — the preprod block time.

The dolos.socket file is created in your ~/dolos directory while the daemon is running. It disappears when Dolos stops. Keep this terminal open while working through the course modules.


Values to Note

When configuring tools in subsequent modules, you will need:

Value

Where to find it

Example

Socket path

The full path to dolos.socket in the directory where you ran dolos daemon

/home/yourname/dolos/dolos.socket

Network magic

dolos.toml[upstream]network_magic

1


Common Issues

dolos.socket: no such file or directory

Dolos is not running, or you are looking in the wrong directory. Start dolos daemon from your ~/dolos directory and confirm the socket file appears there.

Bootstrap fails or is very slow

Check your internet connection. If Mithril bootstrapping fails, try dolos bootstrap relay as a fallback — it syncs directly from a relay node (slower but more resilient).

Dolos stops unexpectedly

Check the terminal output for errors. A common cause is insufficient disk space — the chain data grows over time. Ensure you have headroom beyond the initial snapshot size.


Summary

Made by

TxPipe

Language

Rust

Role in this course

Local source of Cardano chain data for Adder and other tools

APIs exposed

Unix socket (Ouroboros), gRPC, Mini Blockfrost HTTP

Config file

dolos.toml in your Dolos directory

Network

Preprod — magic 1

Key commands

dolos initdolos bootstrap mithrildolos daemon