Skip to content

Repository files navigation

gochain

CI Go tests License: MIT

A blockchain written from scratch in Go — proof of work, persistence, a signed UTXO transaction model, ECDSA wallets and TCP peer-to-peer sync.

Built to the engineering standard of a production service: property-based and fuzz tests, a security-reviewed network layer, static analysis, Docker and CI.

Why Go?

Go is the dominant language of blockchain infrastructure — go-ethereum (geth), the Cosmos SDK / Tendermint and Hyperledger Fabric are all written in Go — so this implementation maps directly to the work blockchain teams actually do.

Architecture

gochain/
├── cmd/gochain/      # CLI: wallets, chain operations, node + sync
└── internal/
    ├── wallet/       # ECDSA keys, Base58Check addresses, wallet file
    ├── tx/           # signed UTXO transactions (sign + verify)
    ├── block/        # Block (transactions) + serialization
    ├── pow/          # proof of work: mining + validation
    ├── chain/        # persistent UTXO blockchain (BoltDB)
    └── network/      # TCP peer-to-peer block replication

Clean, acyclic dependencies: wallet/txblockpowchainnetwork. Wallet keys use the modern crypto/ecdsa encoding APIs (Go 1.25+).

What it does

  • Proof of Work: every block is mined to a difficulty target; tampering breaks it.
  • UTXO model: coins live in unspent outputs; transfers create payment + change.
  • Cryptographic security: outputs are locked to a public-key hash; spending requires an ECDSA signature verified before mining (forgeries rejected).
  • Persistence: the chain is stored in an embedded BoltDB file.
  • P2P sync & fork choice: a fresh node downloads a peer's chain over TCP, fully validates every block (proof of work, signatures, coinbase rules, no double-spend, value conservation), and follows the longest valid chain, reorganising onto a peer's branch when it grows longer.

CLI

go build -o gochain ./cmd/gochain

A=$(./gochain createwallet | awk '{print $NF}')
B=$(./gochain createwallet | awk '{print $NF}')
./gochain createblockchain -address "$A"          # genesis reward (10) to A
./gochain send -from "$A" -to "$B" -amount 4      # A's wallet signs; chain verifies
./gochain getbalance -address "$A"                # 6 (change)

# Peer-to-peer: serve on one node, sync from another
./gochain startnode -port 3000
GOCHAIN_DB=node-b.db ./gochain sync -peer localhost:3000

Docker

A multi-stage build produces a tiny (~12 MB) distroless, non-root image:

docker build -t gochain .
docker run --rm -e GOCHAIN_WALLET=/tmp/w.dat gochain createwallet

# or run a node with docker-compose (chain stored in a named volume)
docker compose up --build

Testing & hardening

Correctness and robustness are verified at several levels. The unit, property and integration suites run under the Go race detector in CI on every push; the fuzz targets run on demand (see below).

  • Unit & integration tests across every package — crypto, signing, persistence, and end-to-end TCP peer sync.
  • Property-based tests (rapid), asserting invariants over thousands of generated inputs:
    • transaction hashing is deterministic and independent of the ID field;
    • sign → verify soundness — a valid signature verifies, and tampering with the signature or any signed output is rejected;
    • block serialization is round-trip idempotent;
    • value conservation & no double-spend — a transfer preserves total value, and an overspend is rejected with balances left intact.
  • Native Go fuzzing of the two decoders that parse untrusted bytes — the block deserializer and the P2P wire-message decoder — proving they never panic on hostile input (their seed corpora also run as part of the normal -race suite).

Security review

A focused review of the network- and consensus-facing code found and closed the issues below, each now guarded by a regression test:

Area Issue Fix
P2P decode Unbounded gob decode of peer input allowed a memory-exhaustion (OOM) DoS 16 MiB read cap via io.LimitReader
P2P I/O No socket deadlines — a silent peer could pin a goroutine indefinitely Per-exchange read/write deadlines + dial timeout
Transactions An input's output index was used without a lower-bound check → panic on a crafted negative index Full bounds check; the transaction is rejected cleanly
Consensus Received blocks were trusted on valid proof of work alone — cheap at this difficulty, so forged transactions could be injected Received blocks are fully validated on their own branch (signatures, coinbase rules, no double-spend, value conservation); the node then follows the longest valid chain (fork choice)

Running the checks

go test -race ./...                                          # unit + property + integration
golangci-lint run ./...                                      # static analysis (.golangci.yml)
go test -run '^$' -fuzz FuzzDeserializeBlock -fuzztime 30s ./internal/block/
go test -run '^$' -fuzz FuzzDecodeMessage    -fuzztime 30s ./internal/network/
go test -bench=Run -benchmem ./internal/pow                 # mining benchmark

License

MIT © 2026 Furkan Karafil

About

Blockchain from scratch in Go — PoW, UTXO, ECDSA wallets, P2P; race/property/fuzz tested, CI, Docker

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages