A Bitcoin protocol analyzer written in Go. Parses raw Bitcoin transactions and block-file data directly from the wire format — no external APIs, no Bitcoin node required. Ships as both a JSON-emitting CLI and a self-hosted web visualizer.
The codebase is a single Go package with a dual-entry-point design:
| Mode | Entry | Build |
|---|---|---|
| CLI | main.go |
go run . |
| Web server | web.go (//go:build web) |
go run -tags web web.go ... |
web.go carries a //go:build web constraint so the two entry points never
conflict during a plain go build. Both modes share the same parser, script,
and hash layers.
main.go / web.go ← entry points (CLI flags | HTTP handlers)
│
├─ parser.go ← raw-byte transaction deserialization
├─ block.go ← blk*.dat / rev*.dat parsing + XOR decoding
├─ undo.go ← UTXO undo-record deserialization + decompression
├─ script.go ← script classification, disassembly, OP_RETURN decoding
├─ address.go ← address derivation (Base58Check, Bech32, Bech32m)
├─ hash.go ← double-SHA256, TXID/WTXID, Merkle root
└─ report.go ← JSON report assembly
Parses the Bitcoin wire format byte-by-byte: 4-byte little-endian version,
VarInt input/output counts, per-input scriptSig, 4-byte sequence, per-output
value and scriptPubKey, and SegWit witness stacks. SegWit transactions are
detected by the 0x00 0x01 marker/flag pair immediately after the version
field (BIP141). Legacy and SegWit code-paths diverge at that branch point and
reconverge for locktime.
Bitcoin Core obfuscates blk*.dat and rev*.dat on disk by XOR-ing every
byte against a rotating key stored in xor.dat. block.go strips this layer
before any parsing begins, then seeks the 0xD9B4BEF9 magic bytes that
separate blocks within the file, and extracts the 80-byte block header followed
by the transaction vector.
The undo (revert) files store prevout values in Bitcoin Core's custom
base-128 VarInt encoding with a non-linear compression scheme. undo.go
implements ReadBase128VarInt and the full DecompressAmount inverse function
to recover satoshi values. Compressed scripts are reconstructed via
DecompressScript, which handles all five nSize codes (P2PKH, P2SH, compressed
P2PK with even/odd Y, uncompressed P2PK) — including secp256k1 Y-coordinate
recovery from X using the modular square root y = (x³+7)^((P+1)/4) mod P.
After all transactions in a block are parsed, the engine recomputes the Merkle root from scratch: each TXID is double-SHA256 hashed, byte-reversed to standard display order, then the tree is built bottom-up with the Bitcoin-standard odd-node duplication rule. The result is compared against the 32-byte field in the 80-byte block header to cryptographically verify the transaction set.
Classifies every scriptPubKey and scriptSig into one of: p2pkh, p2sh,
p2wpkh, p2wsh, p2tr, op_return, or unknown. Input spend-types include
nested SegWit (p2sh-p2wpkh, p2sh-p2wsh) and Taproot keypath vs scriptpath
(distinguished by witness stack size and control-block prefix byte). Full opcode
disassembly covers all Bitcoin Core opcodes; OP_RETURN payloads are decoded with
support for OP_PUSHDATA1/2/4 and multiple concatenated pushes.
Weight is computed as (non_witness_bytes × 4) + witness_bytes, then converted
to vbytes via ceiling division. The report also computes a segwit_savings
breakdown showing weight_if_legacy vs weight_actual and the resulting
savings_pct — the exact metric miners use for block-space pricing.
- P2PKH / P2SH — Base58Check encoding with the correct mainnet version byte.
- P2WPKH / P2WSH — Bech32 encoding (
bc1q...) per BIP173. - P2TR — Bech32m encoding (
bc1p...) per BIP350.
Each input's sequence field is decoded: bit 31 disables relative timelocks,
bit 22 selects time vs block units, and the low 16 bits carry the value (×512
seconds for time-based). BIP125 RBF is flagged when any input has
sequence < 0xFFFFFFFE.
- Go 1.23+
go version # should print go1.23 or highergo run . fixtures/transactions/tx_legacy_p2pkh.json
# JSON report printed to stdout and written to out/<txid>.jsonOr via the shell wrapper:
./cli.sh fixtures/transactions/tx_legacy_p2pkh.json# Decompress sample fixtures first (one-time)
gunzip -k fixtures/blocks/*.dat.gz
./cli.sh --block fixtures/blocks/blk04330.dat \
fixtures/blocks/rev04330.dat \
fixtures/blocks/xor.dat
# Writes out/<block_hash>.json for every block in the file./web.sh # starts on http://127.0.0.1:3000
PORT=8080 ./web.shAPI endpoints:
GET /api/health→{ "ok": true }POST /api/analyze→ transaction analysis (JSON body: fixture format)POST /api/analyze-block→ block analysis (multipart:blk,rev,xor)
{
"network": "mainnet",
"raw_tx": "0200000001...",
"prevouts": [
{
"txid": "aa...11",
"vout": 0,
"value_sats": 123456,
"script_pubkey_hex": "0014..."
}
]
}prevouts do not need to be in the same order as the transaction inputs —
they are matched by (txid, vout) tuple.
{
"ok": true,
"network": "mainnet",
"segwit": true,
"txid": "...",
"wtxid": "...",
"version": 2,
"locktime": 800000,
"size_bytes": 222,
"weight": 561,
"vbytes": 141,
"total_input_sats": 123456,
"total_output_sats": 120000,
"fee_sats": 3456,
"fee_rate_sat_vb": 24.51,
"rbf_signaling": true,
"locktime_type": "block_height",
"locktime_value": 800000,
"segwit_savings": { "..." : "..." },
"vin": [ "..." ],
"vout": [ "..." ],
"warnings": [ { "code": "RBF_SIGNALING" } ]
}On error:
{ "ok": false, "error": { "code": "INVALID_TX", "message": "..." } }| Type | Input classify | Output classify | Address |
|---|---|---|---|
| P2PKH | ✓ | ✓ | 1... |
| P2SH | ✓ | ✓ | 3... |
| P2WPKH | ✓ | ✓ | bc1q... |
| P2WSH | ✓ | ✓ | bc1q... |
| P2TR keypath | ✓ | ✓ | bc1p... |
| P2TR scriptpath | ✓ | — | bc1p... |
| P2SH-P2WPKH | ✓ | — | — |
| P2SH-P2WSH | ✓ | — | — |
| OP_RETURN | — | ✓ | null |
MIT