DEV Community

이관호(Gwanho LEE)
이관호(Gwanho LEE)

Posted on

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

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>
Enter fullscreen mode Exit fullscreen mode

Output (locking script):

OP_DUP OP_HASH160 <Bob’s publicKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Enter fullscreen mode Exit fullscreen mode

Diagram – Legacy Transaction

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

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)
Enter fullscreen mode Exit fullscreen mode

Witness:

<signature> <publicKey>
Enter fullscreen mode Exit fullscreen mode

Output (locking script – unchanged):

OP_DUP OP_HASH160 <Bob’s publicKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Enter fullscreen mode Exit fullscreen mode

Diagram – SegWit Transaction

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

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)