Vault design:
11. Timelock Script Patterns for Vaults
What Is a Bitcoin Vault?
A Bitcoin vault is a storage pattern that makes it impossible to immediately sweep all funds — even if the private key is compromised. The idea is to introduce a mandatory delay between the "request to spend" and the actual spending, giving the legitimate owner time to detect and cancel unauthorized transactions. Timelocks are the core primitive that makes this possible.
The term "vault" in Bitcoin typically refers to constructions using:
-
CLTV/CSV delays on the spending path.
-
Revocation mechanisms that allow the owner to "claw back" funds if an unauthorized spend is detected during the delay period.
Pattern 1: Basic Vault with CSV Delay
# Vault design:
# To spend vault funds, you must first "unvault" them to a hot wallet.
# The unvaulted UTXO has a CSV delay before it can be spent.
# During the delay, the vault owner can sweep back to cold storage.
# Step 1: Vault UTXO (cold storage)
vault_script:
OP_DUP OP_HASH160 <VaultKeyHash> OP_EQUALVERIFY OP_CHECKSIG
# OR use a more complex multi-sig vault
# Step 2: Unvaulting transaction creates a "hot pending" UTXO:
hot_pending_script (witnessScript):
OP_IF
# Path A: Spend to destination after CSV delay
144 OP_CHECKSEQUENCEVERIFY OP_DROP
OP_DUP OP_HASH160 <HotWalletKeyHash> OP_EQUALVERIFY OP_CHECKSIG
OP_ELSE
# Path B: Claw back to cold storage immediately (revocation)
OP_DUP OP_HASH160 <ColdStorageKeyHash> OP_EQUALVERIFY OP_CHECKSIG
OP_ENDIF
# If no theft:
# After 144 blocks, hot wallet sweeps funds normally.
# If theft detected:
# During the 144-block window, owner sweeps back to cold storage.
Pattern 2: Pre-Signed Vault (OP_VAULT Precursor)
Before OP_VAULT (a proposed new opcode), vaults were implemented with pre-signed transactions:
# Pre-signed vault construction:
# 1. Generate ephemeral key pair (vault key)
# 2. Pre-sign "unvault" and "recovery" transactions
# 3. DELETE the ephemeral private key (making the vault one-way)
# 4. The only way to spend is via the pre-signed transactions
# Pseudocode for vault setup:
import secrets
import hashlib
def create_vault(amount_sats, destination_address, recovery_address, delay_blocks=144):
# Step 1: Ephemeral vault key
vault_privkey = secrets.token_bytes(32)
vault_pubkey = privkey_to_pubkey(vault_privkey)
# Step 2: Create vault UTXO (send to vault pubkey)
vault_script = p2wpkh_script(vault_pubkey)
# Fund vault_script with amount_sats
# Step 3: Pre-sign unvault transaction
# This TX moves funds from vault → hot_pending_script
hot_pending_script = make_csv_script(
delay_blocks=delay_blocks,
spend_key=destination_address,
recovery_key=recovery_address
)
unvault_tx = create_transaction(
inputs=[vault_utxo],
outputs=[hot_pending_script]
)
unvault_sig = sign(unvault_tx, vault_privkey)
# Step 4: Securely delete vault_privkey
vault_privkey = None # In practice, use secure erasure
# Step 5: Pre-sign recovery transaction (from hot_pending_script)
recovery_tx = create_transaction(
inputs=[hot_pending_utxo],
outputs=[p2wpkh_script(recovery_address)]
)
return unvault_sig, recovery_tx
Pattern 3: Covenant-Style Vault
Covenants (restricted spending paths) combined with timelocks create more powerful vaults. While full covenants aren't natively in Bitcoin Script today, we can approximate them:
# Vault with two-level protection:
# Level 1: 24-hour delay (CSV = 144 blocks) before funds move to hot wallet
# Level 2: 7-day absolute locktime (CLTV) from vault creation for large moves
# Small withdrawals (< 0.1 BTC):
144 OP_CSV OP_DROP <HotWalletKey> OP_CHECKSIG
# Large withdrawals (> 0.1 BTC):
<VaultCreationBlock + 1008> OP_CLTV OP_DROP
1008 OP_CSV OP_DROP
2 <HotKey> <ColdKey> 2 OP_CHECKMULTISIG
# Recovery (immediate, any time):
2 <RecoveryKey1> <RecoveryKey2> <RecoveryKey3> 3 OP_CHECKMULTISIG
Pattern 4: Watchtower-Protected Vault
In production vault setups, watchtowers monitor the mempool for unauthorized unvault transactions and broadcast the revocation transaction:
# Watchtower monitoring logic (simplified)
def watchtower_monitor(mempool, vault_utxos, recovery_txs):
"""
Monitor mempool for unauthorized vault spends.
If detected, broadcast pre-signed recovery transaction.
"""
for tx in mempool.get_new_transactions():
for inp in tx.inputs:
if inp.utxo_id in vault_utxos:
# Unauthorized unvault detected!
utxo = vault_utxos[inp.utxo_id]
if not is_authorized(tx, utxo):
recovery_tx = recovery_txs[inp.utxo_id]
# Broadcast with high fee to beat the attacker
recovery_tx.set_fee(high_priority_fee())
broadcast(recovery_tx)
alert_owner(utxo, tx)
The OP_VAULT Proposal (BIP 345)
BIP 345 proposes new opcodes (OP_VAULT and OP_VAULT_RECOVER) to natively support vault semantics without pre-signed transactions, making vaults more flexible and not reliant on ephemeral key deletion. While not yet activated, it would complement CSV/CLTV by adding covenant-style spend restrictions that enforce where funds must go, not just when.
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: