BUG: Encoding a Unix timestamp as if it were a block height
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:
-
Run multiple outbound connections to diverse peers.
-
Use block explorers as an out-of-band chain tip verification.
-
Implement watchtowers that operate on independent network connections.
-
Increase the
csv_delayparameter to give more time for recovery.
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.
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: