Bitcoins the hard way: Using the raw Bitcoin protocol Review
Bitcoins the hard way: Using the raw Bitcoin protocol
www.righto.com
Bitcoins the Hard Way Review Guide: Using the Raw Bitcoin Protocol (Righto.com) — Everything You Need to Know + FAQ
What if you could talk to Bitcoin with no wallet software, no helper libraries—just hex over a socket? Would you try it?
I read Ken Shirriff’s classic “Bitcoins the hard way: Using the raw Bitcoin protocol” and built a fresh, practical review for today’s network. If you’ve ever wanted to see what really happens between your node and the wider Bitcoin network, this is that “lights-on” moment.
The benefit for you: clarity and confidence. You’ll see what Ken nailed, what changed by 2025, and how to reproduce the experience safely—so you learn the raw protocol without risking a single sat you care about.
Describe problems or pain
Most tutorials gloss over the gritty parts. That’s why so many promising experiments end with confusing errors or stuck transactions. Here are the roadblocks I see trip people up:
- Endianness whiplash: Transaction IDs and block hashes often get shown big-endian in UIs, but on the wire you’ll reverse the full 32 bytes. Get it wrong and your prevout won’t exist from the network’s point of view.
- Message framing mistakes: Bitcoin P2P messages have magic bytes, command names, payload lengths, and checksums. One off-by-one in the length or a bad checksum and peers will silently drop you.
- Varints and length fields: Miscount a script length or varint and your transaction becomes gibberish. A common failure: pushing a signature length that doesn’t match the DER you actually included.
- Signature gotchas: Non-canonical DER signatures or the wrong sighash flag lead to rejections like non-mandatory-script-verify-flag (Non-canonical DER signature) or mandatory-script-verify-flag-failed.
- Bitcoin Script confusion: Even “simple” P2PKH has rules for minimal pushes and exact stack behavior. One mismatch and CHECKSIG fails.
- Post-2014 changes: SegWit moved signatures into the witness, so txid vs wtxid can surprise you. If your mental model is pre-SegWit, mempools can say “no” even though you think the math checks out.
- Fee policy and RBF realities: Modern nodes enforce feerate floors and standardness rules. Build a valid transaction with too-low fees and it’ll just sit there. Use Replace-By-Fee the wrong way and peers won’t replace it.
- Weird node errors: Broadcasting “odd” traffic can get you disconnected without much explanation. Some legacy features (like BIP37 bloom filters) are legacy; many public nodes won’t entertain them.
- Risk to funds on mainnet: A single endianness mixup or a mistaken sighash can strand coins. It’s not theoretical—I’ve seen perfectly “valid-looking” hex that no mempool will accept.
If you want references to the rules you’ll run into, check the source-of-truth materials you’ll see throughout this guide: BIP141 (SegWit), BIP143 (SegWit sighash), Bitcoin Core mempool policy, and BIP125 (RBF). They’re dry, but they explain 99% of the “why did my tx get rejected?” mysteries.
Quick example of the classic trap: you copy a txid from a block explorer (displayed big-endian), paste it straight into your prevout without reversing the 32 bytes, and end up referencing a UTXO that doesn’t exist. Everything else can be perfect—and your transaction is still invalid.
Promise solution
Here’s what I’m going to do for you in this review-guide:
- Explain what Ken’s piece actually teaches at the wire level—so you understand the idea, not just the hex.
- Map those lessons to today’s rules (2025), so you don’t get stuck on SegWit-era changes or mempool quirks.
- Call out pitfalls with plain-English fixes and show you how to verify each step with your own node.
- Give you a clean, testnet-first plan to try the raw protocol yourself, including safe defaults and quick sanity checks.
- Suggest a few tiny automations (where it makes sense) so you learn the hard parts without doing repetitive, error-prone steps.
The result: you’ll understand the raw Bitcoin protocol at a level that makes wallet software feel transparent instead of magical.
Who this review-guide is for
- Builders: Wallet devs, backend engineers, node operators who want to reason about transactions without guesswork.
- Auditors and researchers: Folks verifying transaction correctness, fee policy behavior, or network responses.
- Curious Bitcoiners: Anyone comfortable with a terminal and hex who wants to see how Bitcoin really speaks.
You don’t need to be a cryptography wizard. If you can copy a txid, read a few bytes of hex, and run a command, you’re good.
Safety first (how not to lose funds)
- Stay off mainnet at first: Use testnet or regtest. You’ll get the exact same mechanics with zero risk.
- Use throwaway keys: Generate new keys just for this. Never reuse addresses and never paste real keys into one-off scripts.
- Verify locally at every step: Use your node to check tx decoding and mempool policy before any broadcast.
- Let your node be the truth: Trust your node’s view of fees and standardness. Explorers can be inconsistent.
- Pass fee and policy gates: If
testmempoolaccept
says “no,” fix it before you even think about sending.
By the way, I’ll point you to specific commands and known-good checks so you’re never guessing. Want to know exactly what Ken’s guide shows on the wire and how it maps to 2025 rules? Let’s walk through that next—ready to see what the handshake and message structure look like when you strip out the wallet magic?
What Ken’s “Bitcoins the hard way” actually shows you
I remember the first time I followed Ken Shirriff’s walkthrough: I felt like I had peeled the UI off Bitcoin and was talking to it face-to-face. No safety rails, just bytes and rules. That’s the power of his post—it shows the protocol as a living thing, not a black box.
“Once you see the wire, you can’t unsee it.”
Here’s what you actually learn when you speak Bitcoin directly, the same way Ken does—hands-on, byte-by-byte, with real wire messages and a transaction you assemble like a watchmaker.
The handshake: version and verack
Every conversation starts with a polite hello. You open a TCP connection to a node (yours or a public peer) and send a version message. If the node accepts you, you’ll get its version back and then a verack (version acknowledgment). You reply with your own verack. That’s it—you’re in.
What goes into version:
- protocol version (int32): tells the peer what you can understand
- services (uint64): capabilities bitfield (e.g., NODE_NETWORK)
- timestamp (int64): your local time
- addr_recv and addr_from: the peer’s and your network addresses
- nonce (uint64): random ID to detect self-connections
- user agent (var_str): something like /Satoshi:25.0.0/
- start_height (int32): your best known block height
- relay (bool): whether you want tx relays
On the wire, every P2P message starts with a 24-byte header. For mainnet you’ll see magic bytes f9 be b4 d9, then the ASCII command (padded to 12 bytes), the payload length (4 bytes), and a 4-byte checksum.
For example, a header for a version message looks like:
- Magic: f9beb4d9
- Command: 76657273696f6e0000000000 (ASCII “version” + padding)
- Length: e.g., 6a000000 (depends on payload)
- Checksum: first 4 bytes of double-SHA256(payload)
When your peer responds with its version and a verack, you answer with your verack. From here you can send inv, getdata, tx, ping/pong, and the rest of the gossip vocabulary. If you’ve never done this manually, watching the bytes line up with the message spec is thrilling.
Message framing and endianness
Bitcoin’s P2P envelope is neat and predictable, but the endianness rules inside payloads are where people stub their toes.
- Header: straightforward—big-endian ASCII for commands, fixed sizes
- Payload: structures can mix little-endian integers with varints and byte arrays
- Checksums: always the first four bytes of double-SHA256(payload)
The classic gotcha: txids and block hashes are displayed big-endian in explorers, but serialized little-endian in transactions. When you reference a previous output, you must reverse the txid bytes.
Example—prevout in a transaction (txid little-endian + vout):
- Explorer shows txid: e2a8...9b7c
- In the hex, you must write it reversed: 7c9b...a8e2
It feels weird at first, but once you accept that UI-friendly representations and wire serialization differ, the rest snaps into place.
Transaction anatomy by hand
Ken’s piece shines brightest when you build a transaction from spare parts. You see every byte you’re putting on the line.
- version (4 bytes): e.g., 01000000
- vin count (varint): e.g., 01 for one input
- each input:
- prevout txid (32 bytes, little-endian)
- vout (4 bytes, little-endian): e.g., 00000000 for the first output
- scriptSig length (varint)
- scriptSig (bytes): you’ll add this after signing
- sequence (4 bytes): usually ffffffff for legacy
- vout count (varint): e.g., 01
- each output:
- amount (8 bytes, little-endian) in satoshis
- scriptPubKey length (varint)
- scriptPubKey (bytes)
- locktime (4 bytes): e.g., 00000000
For a classic P2PKH pay-to address, the scriptPubKey is:
76 a9 14 <20-byte pubKeyHash> 88 ac
(OP_DUP OP_HASH160 PUSH20 <hash160(pubkey)> OP_EQUALVERIFY OP_CHECKSIG)
A tiny 1-in, 1-out, pre-SegWit transaction (before signing) looks like this shape, with your own bytes plugged in:
01000000 | 01 | <32-byte txid LE> | 00000000 | 00 | ffffffff | 01 | <8-byte amount LE> | 19 | 76a914<20-byte hash160>88ac | 00000000
That “00” scriptSig length is a placeholder until you sign. After you attach a signature and pubkey, that field becomes a nonzero varint and the scriptSig bytes follow.
Script basics (P2PKH era view)
Ken sticks to the most common script of the time: P2PKH. It’s simple, readable, and perfect for understanding the execution model.
- scriptPubKey on the output locks coins to a 20-byte hash160 of a public key
- scriptSig in the spending input provides:
- DER-encoded ECDSA signature + sighash byte
- compressed or uncompressed public key (33 or 65 bytes)
- During validation, Bitcoin runs scriptSig then scriptPubKey; the stack must end true
Typical scriptSig byte structure:
<PUSH_sig> 30...01 | <PUSH_pubkey> 02/03/04...
The 01 at the end of the signature is SIGHASH_ALL. If you forget it, you haven’t signed anything the network recognizes. That tiny byte carries your intent for which parts of the transaction are committed by the signature.
If you want a friendly reference while you read hex, the Script primer on Learn Me A Bitcoin is concise and solid.
Signing: SIGHASH and ECDSA
This is where your transaction becomes real. For legacy P2PKH (pre-SegWit), the signing steps are specific and strict:
- Start with the unsigned transaction
- For the input you’re signing:
- Replace its scriptSig with the locking script (the scriptPubKey you’re trying to satisfy)
- Set all other inputs’ scriptSig to empty
- Append the 4-byte sighash type: 01000000 for SIGHASH_ALL
- Double-SHA256 this serialization to get the message hash
- Use your private key to produce a DER-encoded ECDSA signature
- Place the signature (plus one sighash byte) and your public key into scriptSig
Real-world signature bytes always start with 30 (DER sequence). You’ll see components like 30 44 02 20 ... 02 20 ... and then the sighash byte 01. The public key is typically 33 bytes, starting with 02 or 03 (compressed).
When you re-serialize the transaction with the now-populated scriptSig, your final hex is what you broadcast. If you’ve respected endianness, varints, and the exact sighash rules, nodes will accept it. For an authoritative deep-dive into the wire rules that Ken surfaces, the Bitcoin Core developer reference is gold: Transactions and P2P networking.
I love how this process strips the magic away. You feel the protocol breathing—one header, one varint, one signature at a time. But here’s the question that naturally follows: what changes when signatures live in a separate witness, txids and wtxids split, and policies have moved on since 2014? Keep going—I’ll show exactly what stays the same and what you need to update right now.
2025 reality check: what’s outdated and what still works
“The nature of Bitcoin is such that once version 0.1 was released, the core design was set in stone.”
That line is famous for a reason. The bones haven’t changed, but the muscles have. If you’re going to talk to Bitcoin “by hand” today, you’ll need to speak SegWit, respect modern mempool rules, and know which P2P features nodes expect in 2025.
SegWit changed transactions (txid vs wtxid)
The old raw P2PKH flow still works. But most real-world spends today are SegWit, so your mental model needs this upgrade:
- Two IDs:txid is the double-SHA256 of the non-witness serialization (no signatures), while wtxid commits to the full SegWit serialization (signatures included). See BIP141 for the exact layout.
- Why it matters: With SegWit (v0), your txid no longer changes when you tweak the signature—malleability is largely out of the way. Mempools and peers increasingly speak in wtxid terms (BIP339), so you’ll see both IDs in the wild.
- Signing changed: SegWit v0 signatures follow BIP143: the preimage commits to the amount being spent and a scriptCode. Leave either out and your signature is invalid. This is the #1 tripwire for folks porting 2014-era steps.
- Taproot exists: If you go P2TR (bc1p...), signatures are Schnorr (64 bytes, not DER), with BIP341/342 rules. You can stick to P2WPKH (bc1q...) for your first “by hand” build—simpler, modern, and cheap.
Real-world weight savings are huge. A typical input:
- P2PKH input: ~148 vbytes (592 wu)
- P2WPKH input: ~68 vbytes (272 wu)
That’s why “legacy-only” guides feel expensive on today’s network. If you want your handcrafted tx to actually get relayed at sane fees, SegWit is your friend.
References: BIP141, BIP143, BIP341
Policy changes: fees, standardness, and RBF
Mempools are stricter than they were in 2014, and congestion can push minimums up temporarily. Here’s the short, practical list:
- Feerates are in sat/vB: Your transaction’s weight (wu) divided by 4 gives vbytes. Price it accordingly. During busy periods, nodes raise their “mempool min fee,” so 1 sat/vB may be ignored.
- Standard scripts only: Expect relay for P2PKH, P2SH, P2WPKH, P2WSH, P2TR, and small OP_RETURN outputs (about 80 bytes max data). Bare multisig is still nonstandard for relay.
- Dust is policy, not consensus: Outputs below the dust threshold get rejected by most mempools. Ballpark today:
- P2PKH dust ≈ 546 sats at 1 sat/vB
- P2WPKH dust ≈ 294 sats at 1 sat/vB
These scale with assumed fees to spend them later.
- RBF (Replace-By-Fee): Opt-in RBF (BIP125) is widely respected. Set any input’s nSequence < 0xfffffffe (e.g., 0xfffffffd) to allow replacement. Full-RBF exists on some nodes, but don’t assume the network supports non-signaled replacements end-to-end.
- Strict encodings: Signatures must be strict DER and low-S (BIP66/standardness). Non-minimal pushes or weird script forms will get you bounced.
Quick sanity trick before you broadcast: use your own node’s testmempoolaccept. If your node won’t take it, the public network probably won’t either.
References: BIP125, BIP66
P2P updates you should know
The wire is busier and smarter than a decade ago. When you open a TCP connection in 2025, here’s what to expect:
- wtxid-relay (BIP339): Peers can announce and request by wtxid. If you see or send the wtxidrelay message after handshake, you’re speaking the modern dialect.
- Compact blocks (BIP152): Most nodes will negotiate sendcmpct to speed block relay. Useful to know when you’re monitoring your tx confirmations.
- addr v2 (BIP155): Addresses aren’t just IPv4/IPv6 anymore; Tor v3 and others use the v2 format via sendaddrv2.
- feefilter (BIP133): Nodes may filter inventory announcements below their fee floor, so don’t be surprised if your low-fee tx doesn’t get echoed back everywhere.
- Bloom filters (BIP37): Considered legacy and often disabled. Light clients prefer compact block filters (BIP157/158), not the old filterload dance.
- Service flags have meaning: NODE_WITNESS, NODE_NETWORK_LIMITED, etc. If you connect to random peers hoping to fetch old blocks or txs, you’ll hit limits. Your own full node remains your anchor.
References: BIP339, BIP152, BIP155, BIP133, BIP157
Address formats and change outputs
Base58 “1…” addresses still work, but most wallets default to Bech32/Bech32m:
- P2WPKH (SegWit v0):bc1q... (testnet: tb1q...), defined in BIP173. Smaller inputs, lower fees.
- Taproot (SegWit v1):bc1p... (testnet: tb1p...), with Bech32m checksums per BIP350.
If you’re crafting transactions by hand today, build change as SegWit too. Mixing legacy inputs or change creates fee bloat and odd patterns. For reference:
- P2WPKH output: 31 bytes
- P2PKH output: 34 bytes
Three bytes sound tiny until you multiply by millions of transactions—or you’re fighting for a low-fee confirmation window.
Security gotchas
Hand-rolled transactions are empowering, but mistakes here are expensive. I keep this checklist on my desk:
- SegWit signing includes the amount: BIP143 requires it. If the wrong amount goes into the preimage, your signature won’t verify.
- Right place for signatures: Legacy puts DER signatures in scriptSig; SegWit v0 puts them in the witness (sig first, then pubkey for P2WPKH). Taproot uses Schnorr and different witness rules.
- Endianness will bite you: prevout txid in the input is little-endian; UIs show big-endian. Reverse bytes when you reference a txid as an input.
- Encode carefully: DER signatures must be strict; varints must be minimal; scripts must be push-only where required. Mempools enforce these as policy.
- Don’t forget RBF: Mark replaceable (nSequence < 0xfffffffe) so you can bump fees without starting over. It’s standard practice in 2025.
- Privacy is fragile: Broadcasting directly to a single public peer can fingerprint your origin; address reuse and predictable change hurt you. Use your own node, consider Tor, and rotate keys.
If all that sounds like a lot, good—that’s the point. When you feel that slight nervous buzz while crafting a raw tx, you’re finally paying attention to the rules your wallet quietly follows for you.
Want a modern, testnet-first plan to try this without risking a sat? In the next section, I’ll show you exactly how to set up a node, speak the wire, and hand-build a P2WPKH transaction—step by step, with fee/policy checks baked in. Ready to actually see your own hex confirm?
How to try “the hard way” today without breaking things
“Measure twice, broadcast once.” That’s the energy I bring to working the raw Bitcoin protocol in 2025. You can absolutely do this safely—and have a blast—if you set up a clean playground, practice good habits, and sanity-check every byte.
The first time you watch a hand‑rolled transaction hit your mempool, it feels like launching a paper airplane that crosses an ocean.
Setup: node and playground
Start where mistakes are free.
- Install Bitcoin Core (latest). On a dedicated data folder, I run testnet or regtest. If you plan to inspect transactions by txid later, turn on the index.
bitcoin.conf (pick one network):
- For testnet:
testnet=1
server=1
txindex=1
listen=1
- For regtest:
regtest=1
server=1
txindex=1
listen=1
Fund your sandbox:
- Regtest: create a wallet and mine your own spendable coins:
bitcoin-cli -regtest createwallet rawplayground
ADDR=$(bitcoin-cli -regtest getnewaddress)
bitcoin-cli -regtest generatetoaddress 101 $ADDR
- Testnet: use a faucet and your node’s wallet:
bitcoin-cli -testnet createwallet rawplayground
ADDR=$(bitcoin-cli -testnet getnewaddress)
- Send a small amount from a reputable faucet (search “bitcoin testnet faucet” or try this one).
Pick a UTXO (we’ll spend it by hand):
bitcoin-cli -testnet listunspent 0
(or-regtest
)- Choose one UTXO with fields
txid
,vout
,amount
, and youraddress
.
Why this setup works: your own node is the referee and relay. Testnet/regtest means mistakes are cheap. With txindex=1
you can fetch any transaction by txid, which makes debugging painless.
Wire talk: open a connection and handshake
I like to talk to my own node first. It’s friendlier than random peers and won’t rate-limit you for learning.
- Ports:
- Mainnet:
8333
- Testnet:
18333
- Regtest:
18444
- Mainnet:
- Network magic:
- Mainnet:
0xf9beb4d9
- Testnet:
0x0b110907
- Regtest:
0xfabfb5da
- Mainnet:
Minimal plan:
- Open a TCP socket to
127.0.0.1:18333
(or18444
on regtest). - Send a
version
message with:protocolversion
: ask your node withbitcoin-cli getnetworkinfo
and use that number.services
:0
is fine for a client.user agent
: something like/rawplayground:0.1/
.
- Read their
version
, reply withverack
, expect theirverack
. - Optional niceties after verack:
sendheaders
to prefer header announcements.wtxidrelay
(BIP339) so inv/tx relay uses wtxid.
Pro tip: launch bitcoind with -debug=net
and compare your hex payloads against debug.log
. When in doubt, the log is truth.
Build a SegWit P2WPKH transaction by hand
This is the fun part. We’ll craft a native SegWit spend (P2WPKH, BIP141) to a bech32 address. You’ll see exactly what libraries hide.
- 1) Create a keypair and address
- Generate a random 32-byte private key. Derive compressed pubkey (33 bytes) via secp256k1.
- Compute
HASH160(pubkey)
to get the 20-byte keyhash. - Build scriptPubKey for P2WPKH:
0x00 0x14 <20-byte keyhash>
. - Encode bech32 (testnet HRP
tb
): your address looks liketb1q...
(BIP173).
- 2) Choose your input (prevout)
txid
fromlistunspent
(remember: when serializing the outpoint, txid bytes are reversed).vout
as 4-byte little-endian.- sequence: use
0xfffffffd
to signal RBF-friendly behavior (BIP125 policy).
- 3) Decide your outputs
- One P2WPKH to your recipient: scriptPubKey
0x00 0x14 <hash160>
. - One P2WPKH change back to yourself (don’t skip change; it teaches correct fee math).
- One P2WPKH to your recipient: scriptPubKey
- 4) Build the unsigned transaction (SegWit form)
version
: 4 bytes (use 2).marker
0x00
,flag
0x01
.vin
count + each input (outpoint, scriptSig empty length0x00
,sequence
).vout
count + each output (8-byte amount in sats, varint script length, script bytes).witness
section is empty for now (we add after signing).locktime
:0
for now.
- 5) Compute the SIGHASH preimage (BIP143)
hashPrevouts = sha256d( concat(outpoint for each input) )
hashSequence = sha256d( concat(sequence for each input) )
hashOutputs = sha256d( concat(all outputs) )
- scriptCode for P2WPKH is the legacy P2PKH script with length:
0x19 0x76 0xa9 0x14 <20-byte keyhash> 0x88 0xac
- amount is the exact input amount in sats (8 bytes LE)—don’t guess this number; fetch it from the UTXO you are spending.
- Assemble the preimage fields per BIP143 and append
SIGHASH_ALL (0x01)
. - Double-SHA256 the preimage.
- 6) Sign and build witness
- Sign the hash with your private key (ECDSA/secp256k1). Encode DER and append
0x01
sighash byte. - Witness stack (for this input):
[ 0x02, <sig length> <sig+01>, 0x21 <33-byte pubkey> ]
- Insert the witness into the transaction and re-serialize.
- Sign the hash with your private key (ECDSA/secp256k1). Encode DER and append
- 7) Sanity checks
- txid is the sha256d of the non-witness serialization (omit marker/flag and witness).
- wtxid is the sha256d of the full SegWit serialization (with witness).
- Run
bitcoin-cli -testnet decoderawtransaction <hex>
and verify every field.
Reality check on size: a typical 1-in/2-out P2WPKH transaction is about ~140 vbytes (input ~68 vB, each output ~31 vB, plus overhead). That helps you set fees accurately.
Specs for reference: BIP141 (SegWit), BIP143 (sighash for v0).
Fee and policy checks
Fee mistakes are the #1 reason handcrafted transactions bounce. I keep it simple and data-driven.
- Estimate feerate:
bitcoin-cli estimatesmartfee 2
returns a feerate. Convert to sat/vB. - Compute fee:
fee = ceil(vbytes * sat_per_vbyte)
. Aim a tad higher on testnet where blocks are irregular. - Change output: send the remainder to a fresh P2WPKH address you control. Avoid tiny change (dust); Core’s dust threshold for P2WPKH is roughly in the few hundred sats range—stay above ~546 sats to be safe.
- RBF-friendly: keep
sequence=0xfffffffd
so you can bump fee if needed (BIP125 policy). - Policy gate: before broadcast, run:
bitcoin-cli -testnet testmempoolaccept '["<yourhex>"]'
If it saysallowed: true
, you’re good. Otherwise, it tells you exactly what policy you tripped.
Broadcast, monitor, and confirm
Once it passes policy, you can push it to your node or speak it on the wire like a real peer.
- RPC route:
bitcoin-cli -testnet sendrawtransaction <hex>
- P2P route: after the handshake, send a
tx
message with your raw bytes. Your node should reply and relay if valid. - Watch it:
bitcoin-cli getrawmempool | grep "<txid>"
bitcoin-cli getmempoolentry <txid>
- On regtest, mine your own confirmation:
generatetoaddress 1 <addr>
. - On testnet, use an explorer as a second opinion (I prefer my node, but a public view like mempool.space/testnet is handy).
Troubleshooting common errors
- “Witness program mismatch” or “non-mandatory-script-verify-flag” errors
- Check your scriptCode: for P2WPKH it must be the legacy P2PKH script with a 0x19 length prefix.
- Ensure the amount used in the preimage matches the input’s actual value to the satoshi.
- “Missing inputs”
- You referenced the wrong outpoint, or forgot to reverse the txid bytes when serializing.
- Verify the prevout really exists and is unspent:
gettxout <txid> <vout>
.
- “Insufficient fee” / too-low feerate
- Recalculate vbytes. Remember: weight = base_bytes*4 + witness_bytes, vbytes = ceil(weight/4).
- Use RBF and increase the fee by spending the same inputs with a higher feerate.
- “non-mandatory-script-verify-flag (Signature must be zero for failed CHECK(MULTI)SIG operation)”
- Your signature or pubkey isn’t what the script expects. Confirm DER format and the
0x01
sighash byte is appended. - Check you used the compressed pubkey that corresponds to the address’ keyhash.
- Your signature or pubkey isn’t what the script expects. Confirm DER format and the
- “TX decode failed” / malformed varints
- Double-check varint lengths on scripts and witness pushes. One wrong length byte breaks everything.
- Node disconnects after you speak
- Bad checksum in your P2P message header, wrong command name padding, or incorrect payload length. Compare bytes with
-debug=net
logs.
- Bad checksum in your P2P message header, wrong command name padding, or incorrect payload length. Compare bytes with
My quick debug loop:
- Re-run
decoderawtransaction
and compare against what you intended to build. - Fetch the prevout with
getrawtransaction <txid> true
to confirm the script and amount you’re signing against. - Hash the preimage independently in two tools (or two scripts). Both should match.
- If stuck, recreate the same spend with
bitcoin-cli createrawtransaction
+fundrawtransaction
+signrawtransactionwithkey
and diff field-by-field against your handcrafted hex. The mismatch tells you exactly what to fix.
Why this works in practice: you’re aligning with today’s consensus (SegWit) and relay policy, using your node as the reality check. Research on mempool behavior consistently shows that sane feerates, standard scripts, and RBF-ready sequences lead to smoother relay and confirmation, especially under congestion. You’ll hit far fewer walls when you build like modern wallets do.
Curious what’s “safe enough” on mainnet, whether you really need a full node, or how raw P2P compares to JSON-RPC when you just want to broadcast? I’ve collected the answers and the gotchas—ready for a rapid-fire Q&A next.
FAQ: everything curious readers ask about “Bitcoins the hard way”
Is it safe to use the raw Bitcoin protocol?
Yes—on testnet or regtest. That’s where I practice and break things on purpose. On mainnet, it’s only safe if you’re fluent in fees, mempool policy, and signature rules. My rule: if my node’s testmempoolaccept says “ok”: true, I’m good to broadcast. If not, I fix it before risking a sat.
- Use throwaway keys and isolate experiments.
- Check feerate in vbytes; aim for a realistic mempool minimum and consider the rolling minimum fee your node reports.
- Validate locally with your own node; don’t trust a random public endpoint to tell you the truth.
“Safe” = testnet/regtest + your node says yes + you understand what you signed.
Do I need a full node to do this?
If you want reliable, repeatable results and decent privacy, yes. Public nodes can throttle, filter, or disconnect you, and you’ll leak your transaction assembly patterns. Running Bitcoin Core gives you:
- Accurate policy feedback via testmempoolaccept, not guesswork.
- Privacy: you’re not handing your raw tx attempts to random peers.
- Consistency: same view of UTXOs, fees, and rules you’ll broadcast into.
Does Ken’s guide still work after SegWit?
The core ideas absolutely hold. The classic P2PKH flow still works, but today most transactions use SegWit (P2WPKH/P2WSH). Multiple industry dashboards have shown SegWit usage hovering around 80–90% of transactions in recent years, so it’s worth learning.
- Key shift: signatures move to the witness; txid excludes them, wtxid includes them.
- Why it matters: malleability protections and lower fees via weight/vbytes.
- What to update: change your sighash preimage rules to BIP143 (SegWit) or BIP341 (Taproot) as needed.
What’s the difference between JSON-RPC and raw P2P?
I use both, but for different reasons:
- JSON-RPC: local control plane. Ask your node to decode, validate, or broadcast. Great for learning and checking your work.
- Raw P2P: the public wire. You speak version/verack/addr/inv/tx to peers. Great for understanding network behavior and message formats.
Think of RPC as your cockpit, and P2P as the air traffic radio.
How can I broadcast a raw transaction?
Two straightforward routes:
- RPC: hand your hex to sendrawtransaction. Quick and reliable.
bitcoin-cli sendrawtransaction 02000000000101...
- P2P: open TCP to your node, send version and get verack, then push a tx message with your raw bytes. Watch for inv/getdata chatter to confirm relay.
How do I sign a transaction without a library?
I keep a checklist for P2WPKH (BIP143) and Taproot (BIP341). High-level flow:
- Generate keys and derive the address that matches your spend type.
- Assemble the unsigned tx: inputs, outputs, locktime, sequence.
- Compute the sighash preimage:
- P2WPKH: hashPrevouts || hashSequence || outpoint || scriptCode (standard P2PKH template with your pubKeyHash) || value || nSequence || hashOutputs || nLocktime || sighashType.
- Taproot (key spend): BIP341 rules with sighash_default (0x00); no legacy scriptCode; commit to amounts per input; use the BIP340 tagged hash for Schnorr.
- Sign:
- P2WPKH: ECDSA over the double SHA256 of the preimage. Encode DER, append sighash byte.
- Taproot: Schnorr (BIP340) over the BIP341 message. No DER, fixed 64-byte signature, optional annex/leaf data if script path.
- Place the signature:
- P2WPKH witness: [signature+hashtype] [compressed pubkey].
- Taproot witness: [schnorr signature] for key spend, or include control block and script for script path.
- Serialize the final transaction and verify with your node’s decoder before broadcast.
Gotchas I see the most: wrong scriptCode for P2WPKH, forgetting the input amount in the preimage, not enforcing low-S for ECDSA, or mixing up SIGHASH flags.
Why are txids shown in little endian sometimes?
Because Bitcoin’s internals love little-endian, while most UIs show big-endian hex. When you reference a prevout, you usually reverse the 32-byte txid you copied from a block explorer before putting it on the wire.
Displayed txid (big-endian): 1a2b3c...fe
Prevout bytes (wire, little-endian): fe...3c2b1a
If your signature checks fail and everything looks right, this byte order mismatch is the classic culprit.
Can I learn this without risking real BTC?
Absolutely. That’s the whole point of testnet and regtest. My quick pattern on regtest:
- Create a wallet and address.
- Mint blocks to yourself (101 blocks to mature coinbase).
- Handcraft a SegWit spend against those UTXOs, run testmempoolaccept, then broadcast.
bitcoin-cli createwallet lab
bitcoin-cli getnewaddress
bitcoin-cli generatetoaddress 101 tb1...
bitcoin-cli testmempoolaccept '["02000000000101..."]'
You get real mechanics, zero real-world risk, and complete control over fees and confirmations.
Bonus: what about fees and RBF in 2025?
Policies tightened over the years. Nodes expect sane feerates and standard scripts, and RBF is common so you can bump fees safely. I treat replaceability as a feature: set sequence < 0xfffffffe to opt-in, then adjust if the mempool gets spicy. Keep an eye on your node’s mempool minimum—under congestion, a higher rolling minimum can make a low-fee raw tx disappear without ever relaying.
Want my short checklist and the exact order I verify a hand-built SegWit spend before hitting send? That’s next—plus my verdict on whether this approach still makes you a sharper Bitcoiner in 2025. Ready for the wrap-up?
My verdict: a timeless classic, updated for today’s Bitcoin
Ken Shirriff’s walkthrough is still the piece I send people to when they ask, “How does Bitcoin really work?” It removes the training wheels and puts you on the wire. The magic only fades once, and then everything starts making sense: why txids are byte-reversed in prevouts, how nodes decide to talk to you, where signatures actually sit, and what your wallet has been shielding you from all along.
Today, the network expects SegWit transactions, reasonable feerates, and clean behavior as a peer. That’s not a bug—it’s a chance to learn the current rules that keep Bitcoin reliable at scale. If you want proof that this path pays off, look at how the ecosystem handled fee spikes during recent congestion: mempool.space charts showed sustained backlogs and fee bands shifting within hours, and the transactions that sailed through were the ones that respected modern policy and sizing. The skills you build by speaking raw P2P and hand-crafting SegWit transactions translate directly to shipping safer wallets, better tooling, and fewer “why won’t my tx relay?” headaches.
When I first rebuilt the flow with a SegWit spend, I made one classic mistake: I forgot to commit the input amount in the sighash preimage and watched my node politely refuse the transaction. Two minutes with testmempoolaccept and a hex diff later, fixed. That kind of feedback loop turns mystery into muscle memory.
Want sources to keep you grounded? Check Ken’s original post on Righto.com, the SegWit specs (BIP141 and BIP143), mempool policy notes in the Bitcoin Optech Newsletter, and current relay behavior like wtxid-relay (BIP339). When you want to sanity-check fee pressure, open the mempool.space dashboard and see how your vbytes compete in the real world.
Who will get the most value
- Wallet and tooling developers who want fewer edge-case bugs. If you can serialize a SegWit input from scratch and pass mempool policy, off-by-one errors and mis-sized change outputs stop haunting your releases.
- Auditors and security engineers verifying transaction correctness. Rebuilding the sighash step by step catches malleability assumptions, wrong flags, and nonstandard scripts before they cost you.
- Researchers and educators explaining how the network actually relays data. Watching version/verack, inv/tx/getdata, and compact block behavior in your logs makes classroom diagrams feel real.
- Power users who want to stop guessing. If fee spikes or policy changes hit, you’ll understand exactly why a transaction stalled and how to fix it—without waiting on wallet updates.
Bonus: if you work with PSBT or Taproot day to day, this foundation makes the “why” behind those formats click, so your mental model stays solid as features evolve.
A quick checklist for your first run
- Use your own node on testnet or regtest so you control peers, fees, and confirmations. Keep logs on and watch what your peer says back.
- Recreate the handshake and log messages (version/verack, then inv/tx). Save raw payloads and compare to your node’s debug log to catch framing mistakes early.
- Build a P2WPKH transaction by hand: correct prevout endianness, proper scriptCode and input amounts in the preimage, DER-encoded signature, and a clean witness stack [sig, pubkey].
- Verify with testmempoolaccept before broadcast. Fix fee rate, dust, and standardness issues locally instead of spamming peers.
- Compare your hex to a known-good tx. Create the same spend with your node’s wallet or bitcoin-tx/psbt tooling, then diff field-by-field.
- Only then try a small mainnet spend. Start with a low-stakes UTXO, set RBF, and watch how quickly peers announce your wtxid under normal fee pressure.
If something fails, it’s almost always one of four things: wrong txid byte order, missing input amount in the SegWit preimage, an underpriced feerate, or a subtle script length mismatch. Fix those and you’re golden.
Conclusion
This is the rare learning path that turns Bitcoin from a black box into a clear, predictable machine. Ken’s piece lights the way; a few 2025 habits—SegWit-first transactions, policy awareness, and wtxid-based thinking—get you the rest of the way home. Follow the safe workflow, keep your node close, and you won’t just send a handcrafted transaction. You’ll know exactly why it works, and you’ll be ready for whatever the mempool throws at you next.
If you want me to publish a minimal testnet script set and some known-good hex for comparison, tell me in the comments on the blog. I’ll bundle them with notes so you can replicate the whole journey in under an hour.