There's a version of learning Bitcoin where you read the whitepaper, nod along, and move on. Then there's the version where you're staring at a terminal at 11pm wondering why two nodes refuse to talk to each other then you actually understand the answer when you figure it out.
We did the second version. This article is what came out of it.
By the end of one week, I understood what Bitcoin actually is under the hood, why the Lightning Network exists, what a payment channel really means, and what it looks like to build all of this from zero, in a local test environment.
No hype. No "Bitcoin is the future" takes. Just the engineering.
First: What Is Bitcoin, Actually?
Before Lightning makes any sense, Bitcoin has to make sense. And I mean the technical version, not the investment version.
Bitcoin is a distributed ledger :— a database that no single person owns or controls, replicated across thousands of computers around the world. Every 10 minutes or so, a new "block" of transactions gets added to this ledger. That chain of blocks is the blockchain.
Here's the key property that makes it interesting: once a transaction is written into the chain, it is practically irreversible. There's no customer service desk. There's no refund button. The database is append-only, and changing historical records would require redoing an astronomically expensive amount of computational work.
This is great for finality. It's terrible for speed.
The Problem: Bitcoin Is Slow By Design
Bitcoin's base layer processes roughly 7 transactions per second. Globally. For everyone.
Visa handles tens of thousands per second. M-Pesa handles millions of transactions per day in Kenya alone. Bitcoin, as a base layer, cannot compete on throughput, and that's not a bug, it's a consequence of the tradeoffs that make it trust-less and decentralized.
So what do you do if you want fast, cheap Bitcoin payments? You build on top of it. That's where Lightning comes in.
What Is the Lightning Network?
The Lightning Network is Bitcoin's Layer 2 — a payment network that sits on top of Bitcoin and inherits its security without inheriting its slowness.
The core idea is elegant: what if two people could transact with each other thousands of times, but only touch the blockchain twice?
Once to open the channel. Once to close it.
Everything in between happens off-chain, instantly, with near-zero fees.
The Analogy That Made It Click For Me
Imagine you and a colleague work in the same office and you frequently owe each other small amounts like lunch money, airtime, split bills. Every time you settle, you don't go to the bank. You keep a running tab. "I owe you 200, you owe me 350 net, you owe me 150." At the end of the month, one person pays the other. One transaction.
A Lightning channel is that tab, except it's crypto-graphically enforced, so neither party can cheat. You put real Bitcoin into it when you open it. The channel tracks who owns what. When you're done, you close it and the final balances get written to the blockchain.
Scale this to millions of people with overlapping channels, and you have a network where you can pay anyone, even without a direct channel to them just by routing through intermediate nodes.
What we Actually Built
This was a structured bootcamp environment with a real set of goals:
Compile Bitcoin Core from source (no sudo apt install shortcuts)
Build a Bitcoin Explorer CLI in Go using only the standard library
Set up a two-node Lightning Network with LND in regtest
Fund channels, make payments, and observe what happens
Let's take a tour of each stage.
Part 1: Compiling Bitcoin Core From Source
Most guides tell you to download a binary. We didn't do that. We compiled Bitcoin Core v31.99.0 directly from source code, without sudo privileges.
Why does this matter? Because in production fintech systems, you often can't just trust a binary you downloaded. Compiling from source means you're running exactly what the code says — nothing added, nothing modified.
The process taught me something I hadn't thought about before: software has dependencies on dependencies on dependencies. Getting Bitcoin Core to compile meant first ensuring the right versions of Boost, lib-event, and other libraries were present. The compiler errors are honest and they tell you exactly what's missing.
Once compiled, I ran it in regtest mode.
What Is Regtest?
Regtest (short for regression test) is a local, private Bitcoin network that only exists on your machine. You control everything: the mining, the blocks, the funds. You can mine 1000 blocks in a second. It's basically a sandbox.
This is how you develop and test anything Bitcoin-related without spending real money or waiting for real confirmations.
Part 2: Building a Bitcoin Explorer CLI in Go
With Bitcoin Core running, I built a command-line tool in Go that could query it.
Bitcoin Core exposes a JSON-RPC interface — you send it HTTP POST requests with JSON bodies, and it responds with blockchain data. Think of it as the node's API.
Here's a minimal example of what that looks like in Go:
go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type RPCRequest struct {
Method string json:"method"
Params []interface{} json:"params"
ID int json:"id"
JSONRPC string json:"jsonrpc"
}
func callRPC(method string, params []interface{}) (map[string]interface{}, error) {
req := RPCRequest{
Method: method,
Params: params,
ID: 1,
JSONRPC: "1.0",
}
body, _ := json.Marshal(req)
resp, err := http.Post(
"http://localhost:18443/",
"application/json",
bytes.NewBuffer(body),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
func main() {
info, _ := callRPC("getblockchaininfo", nil)
fmt.Println(info)
}
The constraint was standard library only, no external HTTP clients, no JSON helper packages beyond encoding/json. This was intentional. It forced me to understand exactly what was happening at the network level, not abstract it away.
The explorer could query block height, fetch transaction details by ID, decode raw transactions, and display UTXO (Unspent Transaction Output) information. UTXOs are how Bitcoin actually tracks balances — not accounts, but unspent outputs you have the right to spend.
Part 3: Setting Up Two LND Nodes
LND (Lightning Network Daemon) is the most widely used Lightning implementation, written in Go by Lightning Labs.
I set up two nodes: Alice and Bob. Each one was a separate LND instance, each connected to the same local Bitcoin Core regtest node.
The configuration looks roughly like this for Alice:
ini
[Application Options]
datadir=/home/user/bootcamp-lnd/alice/data
logdir=/home/user/bootcamp-lnd/alice/logs
listen=0.0.0.0:9735
rpclisten=0.0.0.0:10009
restlisten=0.0.0.0:8080
noseedbackup=true
[Bitcoin]
bitcoin.active=1
bitcoin.regtest=1
bitcoin.node=bitcoind
[Bitcoind]
bitcoind.rpchost=localhost
bitcoind.rpcuser=your_rpc_user
bitcoind.rpcpass=your_rpc_password
bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332
bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333
Bob gets his own instance on different ports. Same structure, different directories, different ports.
The ZMQ Issue (and Why It Matters)
One thing that tripped me up: LND communicates with Bitcoin Core not just through RPC, but also through ZMQ (ZeroMQ) — a messaging protocol that lets Bitcoin Core push new block and transaction notifications to LND in real time.
Without ZMQ configured correctly, LND would start but wouldn't know about new blocks. Payments would appear to hang. The fix was ensuring zmqpubrawblock and zmqpubrawtx were properly set on both the Bitcoin Core side and the LND config side, and that the ports matched.
This is the kind of thing a tutorial glosses over. In practice, it's the difference between a working node and a node that silently does nothing.
Part 4: Funding a Channel
Once both nodes were running, I connected them:
bash
Get Alice's node info
lncli --rpcserver=localhost:10009 getinfo
Connect Alice to Bob (using Bob's pubkey@host:port)
lncli --rpcserver=localhost:10009 connect @localhost:9736
Open a channel from Alice to Bob, funding it with 1,000,000 satoshis
lncli --rpcserver=localhost:10009 openchannel --node_key= --local_amt=1000000
Opening a channel requires an on-chain transaction. So I mined a few blocks to confirm it:
bash
bitcoin-cli -regtest generatetoaddress 6
After 3 confirmations, the channel was active. Alice had 1,000,000 satoshis of outbound liquidity :- meaning she could send up to that amount to Bob. Bob had zero outbound liquidity toward Alice unless he funded his side too.
This asymmetry is one of the most important (and often confusing) things about Lightning. Liquidity is directional. Having a channel doesn't mean you can pay in both directions, it depends on where the funds sit within that channel.
Part 5: Making a Payment
With the channel open, Bob creates an invoice, essentially a payment request:
bash
lncli --rpcserver=localhost:10036 addinvoice --amt=50000 --memo="Coffee"
This spits out a BOLT11 payment string :- a long encoded string that starts with lnbcrt... in regtest. It contains the amount, a payment hash, Bob's public key, and an expiry time.
Alice pays it:
bash
lncli --rpcserver=localhost:10009 payinvoice
This happens in under a second. No block confirmation required. The channel's internal balance shifts: Alice now has 950,000 satoshis, Bob has 50,000. The blockchain hasn't changed at all. That shift only gets recorded on-chain when the channel closes.
The LND REST API
At the end of the bootcamp, I was also given access to a remote regtest LND node via its REST API, a taste of what integrating Lightning into a real backend looks like.
LND exposes every operation through HTTP endpoints. To check node info:
bash
curl -k \
-H "Grpc-Metadata-macaroon: " \
https://your-lnd-node/v1/getinfo
Macaroons are LND's authentication mechanism bearer tokens with baked-in permissions. The admin macaroon can do everything. The invoice macaroon can only create and read invoices. You'd use the invoice macaroon in a web server that generates payment requests, and never expose the admin macaroon to anything internet-facing.
In Go, you'd integrate this with:
go
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // only for dev/regtest
}
client := &http.Client{Transport: tr}
req, _ := http.NewRequest("GET", "https://your-lnd-node/v1/getinfo", nil)
req.Header.Set("Grpc-Metadata-macaroon", adminMacaroonHex)
resp, err := client.Do(req)
This is the pattern you'd use when building a payment backend on top of Lightning, creating invoices on demand, polling for payment confirmation, and triggering downstream actions (like unlocking content or marking an order as paid) when the payment settles.
What This Changes About How I Think
Before this bootcamp, I understood Lightning conceptually. After it, I understand it structurally. There's a difference.
A few things that specifically shifted:
Settlement finality is not binary. On-chain Bitcoin is "final" after enough confirmations. Lightning is final the moment the HTLC (Hash Time Locked Contract) resolves. Different guarantees, different use cases. Knowing which one you need matters when you're designing a payment flow.
Top comments (0)