TeachMeBitcoin

BUG: Encoding a Unix timestamp as if it were a block height

From TeachMeBitcoin, the free encyclopedia Reading time: 9 min

15. Timelock Attack Vectors and Edge Cases

Introduction: Timelocks Are Not Magic Bullet Security

While timelocks provide powerful guarantees in Bitcoin, they are complex and have numerous edge cases that have caused real-world losses and near-misses. Understanding the failure modes is as important as understanding the happy paths.

Attack Vector 1: Timelock Type Mismatch

Description: Mixing block-height and timestamp values in CLTV/CSV creates scripts that will never validate correctly — or worse, scripts that validate when they shouldn't.

Example Bug:

# BUG: Encoding a Unix timestamp as if it were a block height
WRONG_locktime = 1700000000  # This is a Unix timestamp (Jan 2024)
# But if you put 1700000000 in an nLockTime that should be block-height-mode,
# Bitcoin consensus sees it as block height (it's < 500,000,000? NO — it's WAY above)
# So this WILL be treated as a timestamp. But if the CLTV script value was
# intended as block height, the comparison may never succeed.

# CORRECT approach: always verify the comparison type
def validate_cltv_before_broadcasting(script_locktime, tx_nlocktime):
    THRESHOLD = 500_000_000
    if (script_locktime >= THRESHOLD) != (tx_nlocktime >= THRESHOLD):
        raise ValueError("FATAL: CLTV type mismatch — transaction will be unspendable!")

Attack Vector 2: nSequence Misconfiguration Prevents Spending

Description: If the spending transaction's nSequence is set incorrectly, a CSV script will never validate. Common mistakes:

# BUG: Setting nSequence to SEQUENCE_FINAL (0xFFFFFFFF)
# This disables both nLockTime (breaking CLTV) AND BIP68 (breaking CSV)

# BUG: Setting nSequence to 0x80000001 (disable flag + value)
# The disable flag (bit 31) tells the node to ignore BIP68
# CSV sees the disable flag on the input and FAILS

# BUG: Using tx version 1 with CSV
# CSV requires tx version >= 2. Version 1 transactions ignore BIP68.
# If the tx is version 1, CSV will FAIL even if nSequence is correct

# Correct nSequence setup for CSV:
def compute_nsequence_for_csv(blocks):
    """Compute valid nSequence for a CSV(blocks) script"""
    if blocks > 0xFFFF:
        raise ValueError("CSV block count exceeds maximum (65535)")
    # No type flag (blocks), no disable flag
    return blocks  # Just the block count in bits 0-15

Attack Vector 3: The Fee Sniping / Locktime Randomization Issue

Description: By convention, most Bitcoin wallets set nLockTime to the current block height minus a random offset (for privacy and fee-sniping protection). This can cause issues with CLTV scripts if the nLockTime is set to a value below the CLTV requirement.

# Modern wallets set nLockTime to approximately current_block - random(0, 100)
# This is FINE for normal transactions.
# But for CLTV-spending transactions, you MUST set nLockTime >= CLTV value.

# Wrong (wallet default locktime):
nLockTime = current_block - 50  # Wallet randomization
# If CLTV requires 850000 and current_block is 850100, this is 850050 — OK!
# But if current_block is 849900 and the wallet sets 849850, CLTV FAILS.

# Correct:
nLockTime = max(cltv_requirement, current_block - small_random_offset)

Attack Vector 4: UTXO Confirmation Race for CSV

Description: With CSV, the timer starts when the UTXO is confirmed in a block. If the funding transaction is unconfirmed (in the mempool), the CSV clock hasn't started. Transactions that try to spend a CSV-encumbered output before it's sufficiently confirmed will be rejected.

# Scenario: Lightning channel setup
# Funding TX is broadcast but not yet confirmed.
# Attacker (or buggy software) tries to broadcast commitment TX
# spending the funding output with CSV.
# RESULT: Transaction rejected — UTXO isn't confirmed yet!

# Safe check before broadcasting a CSV spend:
def can_broadcast_csv_spend(utxo_txid, required_confirmations, current_block):
    utxo_conf_block = get_confirmation_block(utxo_txid)
    if utxo_conf_block is None:
        return False, "UTXO not yet confirmed — CSV clock not started"

    elapsed = current_block - utxo_conf_block
    if elapsed < required_confirmations:
        return False, f"Need {required_confirmations - elapsed} more blocks"

    return True, "CSV condition satisfied"

Attack Vector 5: Eclipse Attack on Timelocks

Description: An eclipse attack isolates a node from the honest network, feeding it a fake blockchain. A victim node might be shown a "chain" where it appears that enough blocks have passed to satisfy a CSV or CLTV condition — but in reality, the real chain hasn't advanced that far.

This is particularly dangerous for Lightning node operators. If a channel partner broadcasts an old commitment transaction during an eclipse, the victim can't see it (they're isolated) and the CSV timeout runs out before they can respond.

Mitigations:

Attack Vector 6: Miner Timestamp Manipulation

Description: For timestamp-based timelocks (nLockTime or CSV time mode), miners can slightly manipulate the block timestamp. The network allows timestamps up to 2 hours in the future and requires only that the timestamp is greater than the Median Time Past (MTP) of the last 11 blocks.

# Miner timestamp attack:
# A colluding set of miners could potentially:
# 1. Mine blocks with timestamps far in the future (up to 2 hours ahead)
# 2. Advance the MTP faster than wall-clock time
# 3. Trigger timestamp-based CLTV timelocks prematurely

# Practical impact:
# The MTP advances by ~2 hours at most per block (protocol limit).
# To advance MTP by a significant amount, miners need to control many consecutive blocks.
# This attack is expensive and not practical for small timelock advantages.

# Safer alternative: use BLOCK HEIGHT for timelocks when possible.
# Block height cannot be manipulated (one block = one block).

Attack Vector 7: Script Standardness and Relay Policies

Description: Even if a CLTV/CSV script is consensus-valid, Bitcoin nodes have relay policies (standardness rules) that may refuse to relay or accept it into their mempool. Non-standard transactions won't propagate across the network, even if technically valid.

# Potential standardness issues with timelock scripts:
# 1. Script size limit: scriptPubKey must be <= 10,000 bytes (consensus)
#    But most nodes enforce stricter relay limits.
# 2. P2SH redeemScript: must be <= 520 bytes for standardness.
# 3. P2WSH witnessScript: must be <= 3,600 bytes for standardness.
# 4. Stack depth limits: Bitcoin script has a maximum stack depth of 1,000 items.
# 5. Signature operations (sigops): excessive sigops make scripts non-standard.

# BEST PRACTICE: Always test scripts on testnet before mainnet deployment.
# Use bitcoin-script-debugger or similar tools to verify standardness.
def check_script_standardness(script_bytes):
    if len(script_bytes) > 10_000:
        return "FAIL: Script exceeds 10,000 bytes (consensus limit)"
    if len(script_bytes) > 1_650:
        return "WARNING: Script may not be relayed by default nodes"
    return "OK"

Attack Vector 8: Timelocks in Reorg Scenarios

Description: During a blockchain reorganization (reorg), blocks can be "unconfirmed." If an output that satisfied a CSV condition was "confirmed" in a block that gets reorged away, the CSV condition is no longer satisfied.

Scenario:
  Block 900000: Funding TX confirmed (UTXO created)
  Block 900144: Spending TX broadcast (CSV(144) satisfied)
  Block 900145: Large reorg begins! Chain reorgs back to 900010

After reorg:
  Block 900010: New chain tip
  Funding TX may or may not be re-confirmed (depends on reorg depth)
  Spending TX is NO LONGER VALID — only 0 blocks since UTXO confirmation
  Must wait for UTXO to be re-confirmed + 144 more blocks!

This is why Lightning implementations require a certain number of confirmation depth before considering a channel "open" — typically 3–6 confirmations for routing nodes, and more for large channels.

Attack Vector 9: Dust Outputs and Timelocked Change

Description: Timelock scripts add bytes to the scriptPubKey, making the output larger and increasing the minimum "dust threshold" for that output. If a timelock script creates change output below the dust threshold, it may be rejected by nodes as unspendable.

# Dust calculation with CLTV/CSV:
# Standard P2PKH dust limit: 546 satoshis
# P2SH dust limit: 540 satoshis  
# P2WSH dust limit: 330 satoshis

# For a P2WSH CLTV output, the witnessScript and witness data 
# for spending are also counted toward the virtual size.
# Always ensure outputs exceed the dust threshold.

def minimum_output_value(script_type, feerate_sats_per_vbyte):
    """Calculate minimum non-dust output value"""
    # Rough estimates for spending TX sizes
    if script_type == "P2PKH":
        spend_vbytes = 148
    elif script_type == "P2WPKH":
        spend_vbytes = 68
    elif script_type == "P2WSH_CLTV":
        spend_vbytes = 110  # Estimate for CLTV witness
    else:
        spend_vbytes = 100  # Conservative default

    # Dust threshold: 3× the fee to spend
    return 3 * spend_vbytes * feerate_sats_per_vbyte

# At 10 sat/vbyte, P2WSH CLTV minimum: 3 × 110 × 10 = 3300 sats

Attack Vector 10: Lightning Force-Close Timing Attacks

Description: An attacker with a channel can time a force-close (unilateral close) strategically to cause maximum damage. By broadcasting an old commitment transaction just as the CSV window is about to expire, they give the victim minimal time to respond.

Attack scenario:
  Channel with csv_delay = 144 blocks
  Attacker waits until victim is known to be offline (travel, power outage)
  Attacker broadcasts old commitment TX (stealing extra funds)
  144 blocks pass (~24 hours)
  Victim comes back online: too late! CSV expired, funds stolen.

Mitigations:
  1. Use watchtower services to monitor on behalf of offline nodes
  2. Increase csv_delay for high-value channels (at cost of slower force-closes)
  3. Use channel backup tools to store all prior commitment states
  4. Never keep large balances in channels if you'll be offline for days

Best Practices Summary

Timelock Script Best Practices:

1. ALWAYS use block height (not timestamp) unless calendar accuracy is essential.

2. ALWAYS test scripts on testnet with actual spending before mainnet deployment.

3. ALWAYS verify nSequence values match CSV requirements exactly.

4. ALWAYS set tx version = 2 when using CSV-encumbered inputs.

5. NEVER set nSequence = 0xFFFFFFFF on a CSV-spending input.

6. ADD adequate confirmation buffer for CSV — don't rely on 0-confirmation UTXOs.

7. USE watchtowers for any Lightning channels with significant balances.

8. DOCUMENT the complete spending conditions with every script, not just the address.

9. CONSIDER reorg depth when calculating effective CSV/CLTV windows.

10. AUDIT all timelock scripts professionally before locking significant value.
☕ Help support TeachMeBitcoin

TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation:

Ethereum: 0x578417C51783663D8A6A811B3544E1f779D39A85
Bitcoin: bc1q77k9e95rn669kpzyjr8ke9w95zhk7pa5s63qzz
Solana: 4ycT2ayqeMucixj3wS8Ay8Tq9NRDYRPKYbj3UGESyQ4J
Address copied to clipboard!