🍎 From UTXOs to SegWit: How Bitcoin Fixed Transaction Malleability and Lowered Fees



This content originally appeared on DEV Community and was authored by μ΄κ΄€ν˜Έ(Gwanho LEE)

Introduction

If you’re building Bitcoin or Lightning applications in Rust, understanding the difference between legacy transactions and SegWit is crucial.

In this post, I’ll walk you through:

  • How UTXOs and scripts work
  • The problems with legacy transactions
  • How SegWit solved these problems
  • Why this matters for the Lightning Network

1. Legacy Transactions (P2PKH)

In Bitcoin’s original system, when Alice wants to send BTC to Bob, she must:

  • Sign the transaction with her private key
  • Provide her public key
  • Specify Bob’s public key hash as the output

Legacy Transaction Structure

Input (scriptSig = Unlocking script):

<signature> <publicKey>

Output (locking script):

OP_DUP OP_HASH160 <Bob’s publicKeyHash> OP_EQUALVERIFY OP_CHECKSIG

Diagram – Legacy Transaction

[ Input ] -------------------------> [ Output ]
  scriptSig:                          locking script:
  <AliceSig> <AlicePubKey>            OP_DUP OP_HASH160 <BobPKH> OP_EQUALVERIFY OP_CHECKSIG

Here:

  • Alice proves ownership with her signature + public key.
  • Miners hash Alice’s public key, compare it to the UTXO’s public key hash, and check the signature.

2. Problems With Legacy Transactions

  • Malleability:

    Transaction IDs included the signature. If anyone slightly modified the signature, the txid changed. This caused big problems for advanced protocols like Lightning.

  • High Fees:

    Signatures and public keys are large. Bigger transactions β†’ higher fees.

3. Enter SegWit (P2WPKH)

SegWit (Segregated Witness) moved the unlocking data (signature + public key) out of the scriptSig into a new structure called the witness.

  • scriptSig is now empty (or minimal).
  • Witness data is not included in the transaction ID calculation.

SegWit Transaction Structure

Input (scriptSig):

(empty or minimal)

Witness:

<signature> <publicKey>

Output (locking script – unchanged):

OP_DUP OP_HASH160 <Bob’s publicKeyHash> OP_EQUALVERIFY OP_CHECKSIG

Diagram – SegWit Transaction

[ Input ] -----> [ Output ] + [ Witness ]
  scriptSig:       locking script:      witness data:
  (empty)          OP_DUP OP_HASH160    <AliceSig> <AlicePubKey>
                   <BobPKH>
                   OP_EQUALVERIFY
                   OP_CHECKSIG

4. Why This Matters

SegWit fixed two key issues:

  • No more malleability: Txids are stable because signatures aren’t included in the hash.
  • Lower fees: Witness data is discounted, making transactions smaller and cheaper.

5. Impact on Lightning Network

SegWit wasn’t just a cleanup. It enabled:

  • Lightning Network: Needs stable txids to work.
  • Scalability: More transactions fit into each block.
  • Future upgrades: Like Taproot, which builds on SegWit.

6. Rust Code Example

Here’s a simplified Rust example showing how legacy and SegWit signatures differ when building transactions.

use bitcoin::blockdata::transaction::Transaction;
use bitcoin::consensus::encode::serialize;
use bitcoin::util::sighash::SighashCache;
use bitcoin::{Amount, EcdsaSighashType};

// Legacy P2PKH signing
fn sign_legacy(tx: &Transaction, input_index: usize, script_pubkey: &bitcoin::Script, value: u64, privkey: &secp256k1::SecretKey) {
    let secp = secp256k1::Secp256k1::new();
    let mut cache = SighashCache::new(tx);
    let sighash = cache.legacy_signature_hash(input_index, script_pubkey, value, EcdsaSighashType::All).unwrap();
    let msg = secp256k1::Message::from_slice(&sighash).unwrap();
    let sig = secp.sign_ecdsa(&msg, privkey);
    println!("Legacy signature: {:?}", sig);
}

// SegWit P2WPKH signing
fn sign_segwit(tx: &Transaction, input_index: usize, script_code: &bitcoin::Script, value: u64, privkey: &secp256k1::SecretKey) {
    let secp = secp256k1::Secp256k1::new();
    let mut cache = SighashCache::new(tx);
    let sighash = cache.p2wpkh_signature_hash(input_index, script_code, Amount::from_sat(value), EcdsaSighashType::All).unwrap();
    let msg = secp256k1::Message::from_slice(&sighash[..]).unwrap();
    let sig = secp.sign_ecdsa(&msg, privkey);
    println!("SegWit signature: {:?}", sig);
}

Conclusion

By moving signatures into the witness field, Bitcoin became cheaper, safer, and faster.

As developers, understanding this transition is essential β€” not just for implementing wallets but also for building scalable Layer 2 solutions like Lightning.


This content originally appeared on DEV Community and was authored by μ΄κ΄€ν˜Έ(Gwanho LEE)