Double Spend Detection

Complete examples for detecting and handling double-spend attempts in BSV applications.

Overview

Double-spending is when someone attempts to spend the same UTXO in multiple transactions. While the blockchain prevents double-spends from being confirmed, applications need to detect attempts early to prevent fraud. This guide covers detection strategies, monitoring, and mitigation techniques for zero-confirmation and confirmed transactions.

Related SDK Components:

UTXO Tracking and Monitoring

import { Transaction, ARC } from '@bsv/sdk'

/**
 * UTXO Monitor
 *
 * Track UTXOs and detect double-spend attempts
 */
class UTXOMonitor {
  private trackedUTXOs: Map<string, TrackedUTXO> = new Map()
  private arc: ARC

  constructor(arcUrl: string = 'https://arc.taal.com') {
    this.arc = new ARC(arcUrl)
  }

  /**
   * Track a UTXO for double-spend detection
   */
  trackUTXO(utxo: {
    txid: string
    vout: number
    satoshis: number
    address: string
  }): void {
    const utxoKey = this.getUTXOKey(utxo.txid, utxo.vout)

    const tracked: TrackedUTXO = {
      txid: utxo.txid,
      vout: utxo.vout,
      satoshis: utxo.satoshis,
      address: utxo.address,
      status: 'unspent',
      trackedAt: Date.now(),
      spendingTxids: []
    }

    this.trackedUTXOs.set(utxoKey, tracked)

    console.log('Tracking UTXO:', utxoKey)
  }

  /**
   * Check if UTXO has been spent
   */
  async checkUTXOStatus(txid: string, vout: number): Promise<UTXOStatus> {
    try {
      const utxoKey = this.getUTXOKey(txid, vout)
      const tracked = this.trackedUTXOs.get(utxoKey)

      if (!tracked) {
        throw new Error('UTXO not tracked')
      }

      // Query transaction status
      const txStatus = await this.arc.getTransactionStatus(txid)

      // Check if any spending transactions exist
      const spendingTxs = await this.findSpendingTransactions(txid, vout)

      const status: UTXOStatus = {
        utxoKey,
        spent: spendingTxs.length > 0,
        spendingTxids: spendingTxs.map(tx => tx.txid),
        doubleSpendDetected: spendingTxs.length > 1,
        confirmedSpend: spendingTxs.some(tx => tx.confirmed),
        timestamp: Date.now()
      }

      // Update tracked UTXO
      if (status.spent) {
        tracked.status = status.doubleSpendDetected ? 'double-spend' : 'spent'
        tracked.spendingTxids = status.spendingTxids
      }

      if (status.doubleSpendDetected) {
        console.warn('DOUBLE-SPEND DETECTED:', utxoKey)
        console.warn('Spending transactions:', status.spendingTxids)
      }

      return status
    } catch (error) {
      throw new Error(`UTXO status check failed: ${error.message}`)
    }
  }

  /**
   * Find all transactions spending a UTXO
   */
  private async findSpendingTransactions(
    txid: string,
    vout: number
  ): Promise<Array<{ txid: string; confirmed: boolean }>> {
    try {
      // In production, query mempool and blockchain for spending txs
      // This is a simplified example
      const spendingTxs: Array<{ txid: string; confirmed: boolean }> = []

      // Query ARC for spending transactions
      // Note: Actual implementation depends on ARC API capabilities

      return spendingTxs
    } catch (error) {
      console.error('Error finding spending transactions:', error.message)
      return []
    }
  }

  /**
   * Monitor tracked UTXOs continuously
   */
  async startMonitoring(
    callback: (alert: DoubleSpendAlert) => void,
    interval: number = 5000
  ): Promise<void> {
    console.log('Starting UTXO monitoring...')

    const monitor = async () => {
      for (const [utxoKey, tracked] of this.trackedUTXOs.entries()) {
        if (tracked.status === 'unspent') {
          try {
            const status = await this.checkUTXOStatus(tracked.txid, tracked.vout)

            if (status.doubleSpendDetected) {
              const alert: DoubleSpendAlert = {
                utxoKey,
                txid: tracked.txid,
                vout: tracked.vout,
                spendingTxids: status.spendingTxids,
                detectedAt: Date.now(),
                severity: status.confirmedSpend ? 'high' : 'medium'
              }

              callback(alert)
            }
          } catch (error) {
            console.error('Monitoring error for', utxoKey, ':', error.message)
          }
        }
      }

      setTimeout(monitor, interval)
    }

    monitor()
  }

  /**
   * Remove UTXO from tracking
   */
  untrackUTXO(txid: string, vout: number): void {
    const utxoKey = this.getUTXOKey(txid, vout)
    this.trackedUTXOs.delete(utxoKey)
    console.log('Stopped tracking:', utxoKey)
  }

  /**
   * Get UTXO key
   */
  private getUTXOKey(txid: string, vout: number): string {
    return `${txid}:${vout}`
  }

  /**
   * Get all tracked UTXOs
   */
  getTrackedUTXOs(): TrackedUTXO[] {
    return Array.from(this.trackedUTXOs.values())
  }
}

interface TrackedUTXO {
  txid: string
  vout: number
  satoshis: number
  address: string
  status: 'unspent' | 'spent' | 'double-spend'
  trackedAt: number
  spendingTxids: string[]
}

interface UTXOStatus {
  utxoKey: string
  spent: boolean
  spendingTxids: string[]
  doubleSpendDetected: boolean
  confirmedSpend: boolean
  timestamp: number
}

interface DoubleSpendAlert {
  utxoKey: string
  txid: string
  vout: number
  spendingTxids: string[]
  detectedAt: number
  severity: 'low' | 'medium' | 'high'
}

/**
 * Usage Example
 */
async function utxoMonitorExample() {
  const monitor = new UTXOMonitor()

  // Track UTXO
  monitor.trackUTXO({
    txid: 'utxo-txid...',
    vout: 0,
    satoshis: 100000,
    address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'
  })

  // Check status
  const status = await monitor.checkUTXOStatus('utxo-txid...', 0)
  console.log('UTXO status:', status)

  // Start continuous monitoring
  await monitor.startMonitoring((alert) => {
    console.error('DOUBLE-SPEND ALERT:', alert)
    // Take action: notify admin, block payment, etc.
  })
}

Transaction Conflict Detection

Payment Security Manager

See Also

SDK Components:

Learning Paths:

Last updated