Examples

Taproot: Vaults and Cold Storage

Overview

Create spending restrictions and delayed withdrawal mechanisms for enhanced security. Vaults provide multi-tier access with hot wallets for daily spending and cold storage with time delays for large withdrawals.

Key Benefits:

  • Hot wallet for convenient small withdrawals
  • Cold storage with 30-day delay for security
  • Emergency freeze to prevent theft
  • Privacy via key path for hot wallet

Withdrawal Delay: 21,600 blocks (~30 days at 2 min/block)


Script Tree Structure

Commitment
├── Left: Hot wallet (immediate)
└── Right:
    ├── Left: Delayed withdrawal (30 days + cold key)
    └── Right: Emergency freeze (burn key)

Implementation

import { PrivateKey, Script, Opcode, buildScriptPathTaproot } from 'lotus-lib'

// Create keys
const hotWalletKey = new PrivateKey()
const coldStorageKey = new PrivateKey()
const emergencyKey = new PrivateKey()

const withdrawalDelay = 121600 // current + 21,600 (~30 days)

// Script 1: Hot wallet (immediate spending)
const hotWalletScript = new Script()
  .add(hotWalletKey.publicKey.toBuffer())
  .add(Opcode.OP_CHECKSIG)

// Script 2: Cold storage with time delay
const coldStorageScript = new Script()
  .add(withdrawalDelay)
  .add(Opcode.OP_CHECKLOCKTIMEVERIFY)
  .add(Opcode.OP_DROP)
  .add(coldStorageKey.publicKey.toBuffer())
  .add(Opcode.OP_CHECKSIG)

// Script 3: Emergency freeze (provably unspendable)
const freezeScript = new Script().add(Opcode.OP_RETURN)

const scriptTree = {
  left: { script: hotWalletScript },
  right: {
    left: { script: coldStorageScript },
    right: { script: freezeScript },
  },
}

const { script: vaultScript } = buildScriptPathTaproot(
  hotWalletKey.publicKey,
  scriptTree,
)

console.log('Vault address:', vaultScript.toAddress().toString())

Transaction Formats

Vault Deposit Transaction

JSON:

{
  "version": 2,
  "inputs": [
    {
      "prevTxId": "funding_source_1234567890abcdef1234567890abcdef1234567890abcdef12345678",
      "outputIndex": 0,
      "scriptSig": "483045022100...",
      "sequence": 4294967295
    }
  ],
  "outputs": [
    {
      "satoshis": 50000000,
      "script": "62512102vault_commitment_1234567890abcdef1234567890abcdef1234567890ab"
    },
    {
      "satoshis": 1000000,
      "script": "76a914change_address...88ac"
    }
  ],
  "lockTime": 0
}

Vault Balance: 50,000,000 sats (50 XPI)


Hot Wallet Withdrawal (Key Path)

Small, frequent withdrawals with maximum privacy:

JSON:

{
  "version": 2,
  "inputs": [
    {
      "prevTxId": "vault_tx_id_1234567890abcdef...",
      "outputIndex": 0,
      "scriptSig": "41abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
      "sequence": 4294967295
    }
  ],
  "outputs": [
    {
      "satoshis": 100000,
      "script": "76a914recipient_address...88ac"
    },
    {
      "satoshis": 49890000,
      "script": "76a914vault_return...88ac"
    }
  ],
  "lockTime": 0
}

Input Script: 65-byte Schnorr signature (hot wallet key)

Size: ~110 bytes

Privacy: High - vault structure hidden

Use Case: Daily spending, small purchases


Cold Storage Withdrawal (Script Path)

Large withdrawals with 30-day security delay:

JSON:

{
  "version": 2,
  "inputs": [
    {
      "prevTxId": "vault_tx_id_1234567890abcdef...",
      "outputIndex": 0,
      "scriptSig": "473044022012345678...0220abcdef...<cold_storage_script><control_block>",
      "sequence": 4294967295
    }
  ],
  "outputs": [
    {
      "satoshis": 49800000,
      "script": "76a914cold_storage_destination...88ac"
    }
  ],
  "lockTime": 121600
}

Input Script Breakdown:

  • ECDSA signature from cold key (~72 bytes)
  • Cold storage script with timelock (~70 bytes)
  • Control block (33 + merkle proof)

Size: ~230 bytes

Privacy: Medium - reveals timelock

Use Case: Large withdrawals, long-term savings

Required: Must wait until block height ≥ 121600


Emergency Freeze (Burn Path)

Prevent theft by making funds unspendable:

JSON:

{
  "version": 2,
  "inputs": [
    {
      "prevTxId": "vault_tx_id_1234567890abcdef...",
      "outputIndex": 0,
      "scriptSig": "01<freeze_script><control_block>",
      "sequence": 4294967295
    }
  ],
  "outputs": [],
  "lockTime": 0
}

Input Script Breakdown:

  • Empty signature data
  • OP_RETURN freeze script
  • Control block proving script in tree

Result: Funds permanently unspendable (burned)

Use Case: Emergency response if keys compromised

Warning: This is irreversible! Funds are lost forever!


Vault Strategies

Conservative (Maximum Security)

// Hot wallet: 0.1 XPI daily limit
// Cold storage: 60-day delay
// No emergency freeze (too risky)

const hotLimit = 100000 // 0.1 XPI
const coldDelay = 43200 // ~60 days
// Hot wallet: 1 XPI weekly limit
// Cold storage: 30-day delay
// Emergency freeze available

const hotLimit = 1000000 // 1 XPI
const coldDelay = 21600 // ~30 days

Aggressive (Convenience)

// Hot wallet: 10 XPI limit
// Cold storage: 7-day delay
// Emergency freeze available

const hotLimit = 10000000 // 10 XPI
const coldDelay = 5040 // ~7 days

Security Considerations

Key Storage

Hot Wallet Key:

  • Online/convenient access
  • Lower security acceptable
  • Only controls small amounts
  • Can be phone/desktop wallet

Cold Storage Key:

  • Offline hardware wallet
  • Maximum security required
  • Controls full vault
  • Never connected to internet

Emergency Key:

  • Secure but accessible
  • For crisis situations only
  • Consider multisig (require 2+ parties)

Amount Limits

// Note: Script can't enforce limits without OP_CHECKTEMPLATEVERIFY
// Must be enforced in application logic

function enforceHotWalletLimit(amount: number) {
  const HOT_WALLET_LIMIT = 1000000 // 1 XPI

  if (amount > HOT_WALLET_LIMIT) {
    throw new Error(
      `Amount ${amount} exceeds hot wallet limit ${HOT_WALLET_LIMIT}`,
    )
  }

  // Use hot wallet key path
  return signWithHotWallet(amount)
}

function largWithdrawal(amount: number) {
  // Always use cold storage for large amounts
  return signWithColdStorage(amount)
}

Timelock Safety

// Too short: Not enough time to detect/prevent unauthorized withdrawal
const tooShort = 720 // ~1 day (risky for large amounts)

// Too long: Inconvenient for legitimate large withdrawals
const tooLong = 52560 // ~73 days (very inconvenient)

// Recommended: 30 days
const recommended = 21600 // ~30 days (balanced)

Considerations:

  • Larger amounts → longer delays
  • Higher security needs → longer delays
  • User convenience ↔ security trade-off

Monitoring and Alerts

Watch for Unauthorized Withdrawals

// Monitor vault address for transactions
async function monitorVault(vaultAddress: string, alertCallback: Function) {
  const watcher = new AddressWatcher(vaultAddress)

  watcher.on('transaction', async tx => {
    // Check if it's a cold storage withdrawal
    if (isTimelockedWithdrawal(tx)) {
      const unlockHeight = extractUnlockHeight(tx)
      const blocksUntilUnlock = unlockHeight - currentHeight

      if (blocksUntilUnlock > 0) {
        // Alert: Someone initiated cold storage withdrawal!
        alertCallback({
          type: 'cold_storage_withdrawal',
          amount: tx.outputs[0].satoshis,
          unlockHeight,
          timeRemaining: blocksUntilUnlock * 2, // minutes
        })
      }
    }

    // Check for emergency freeze
    if (isFreezeTransaction(tx)) {
      alertCallback({
        type: 'emergency_freeze',
        message: 'Vault has been frozen!',
      })
    }
  })
}

Emergency Response

// If unauthorized cold storage withdrawal detected:
async function emergencyResponse(vaultUTXO) {
  // Option 1: Use hot wallet to move funds quickly
  const quickMoveTx = spendViaHotWallet(vaultUTXO, secureAddress)
  await broadcast(quickMoveTx)

  // Option 2: Freeze vault if compromise suspected
  const freezeTx = executeEmergencyFreeze(vaultUTXO, emergencyKey)
  await broadcast(freezeTx)

  // Funds now safe (either moved or frozen)
}

Advanced: Dynamic Vaults

Vault that can adjust limits without creating new addresses:

// Use state parameter to track spending limits
const stateBuffer = encodeVaultState({
  hotWalletLimit: 1000000,
  withdrawalDelay: 21600,
  lastWithdrawal: currentHeight,
})

// Create vault with state
const vaultWithState = buildPayToTaproot(commitment, stateBuffer)

// State is visible on-chain and updated with each spend
// Enables dynamic limits and rate limiting

Use Cases

Personal Savings

Long-term holdings with occasional access:

// Most funds in cold storage
// Small amount in hot wallet for emergencies
const savingsVault = buildVault({
  hotWalletLimit: 500000, // 0.5 XPI
  coldDelay: 21600, // 30 days
})

Business Treasury

Company funds with multiple access levels:

// Daily operations: Hot wallet (CEO)
// Large payments: Cold storage with delay (CFO)
// Emergency: Freeze capability (Security team)

const treasuryVault = buildVault({
  hotWalletKey: ceo.publicKey,
  coldStorageKey: cfo.publicKey,
  emergencyKeys: [security1.publicKey, security2.publicKey], // 2-of-2
})

Exchange Cold Storage

Cryptocurrency exchange secure storage:

// Hot wallet: 2% of funds (customer withdrawals)
// Cold storage: 98% of funds (security)
// Multiple signers for cold storage

const exchangeVault = buildEnterpriseVault({
  hotWalletKeys: [operator1, operator2], // 2-of-3 multisig
  coldStorageKeys: [security1, security2, security3, auditor], // 3-of-4
  delay: 43200, // 60 days for large amounts
})

Testing

Regtest Example

import { Networks } from 'lotus-lib'

// Create test vault
const testHot = new PrivateKey(undefined, Networks.regtest)
const testCold = new PrivateKey(undefined, Networks.regtest)

const testVault = buildVault({
  hotWallet: testHot.publicKey,
  coldStorage: testCold.publicKey,
  delay: 100, // Short delay for testing
  network: Networks.regtest,
})

// Test hot wallet withdrawal
await testHotWithdrawal(testVault, testHot)

// Test cold storage (mine 100 blocks first)
await mineBlocks(100)
await testColdWithdrawal(testVault, testCold)

Summary

Benefits:

  • ✅ Multi-tier security (hot/cold separation)
  • ✅ Privacy for daily spending (key path)
  • ✅ 30-day security delay for large amounts
  • ✅ Emergency freeze capability
  • ✅ Flexible configuration

Trade-offs:

  • Hot wallet has immediate access (security risk)
  • Cold storage delay inconvenient for urgent needs
  • Emergency freeze is irreversible
  • More complex to implement

When to Use:

  • Large holdings requiring security
  • Mix of frequent and infrequent access
  • Need emergency response capability
  • Security priority over convenience

When NOT to Use:

  • Small amounts (not worth complexity)
  • Need frequent large withdrawals
  • Can't manage multiple keys
  • Risk of accidental freeze


Last Modified: October 28, 2025

Copyright © Lotusia 2021-2025. All rights reserved.