Source: tracker/tracker.js

/**
 * @file Ethereum taint tracker.
 * @module tracker/tracker
 */

'use strict'

// Imports
const fs = require('fs')
const mkdirp = require('mkdirp')
const EventEmitter = require('events')
const Cache = require('../cache/cache')
const ChainAgent = require('../chain/etherscan')
const Address = require('../primitives/address')
const Taint = require('../primitives/taint')

// Resources
var undef

/**
 * Private members store.
 * @private
 */
const privs = new WeakMap()

/**
 * Get any tainted untraced address.
 * @private
 * @param {Set<Address>} tainted - Tainted addresses.
 * @param {Set<Address>} traced - Traced addresses.
 * @return {Address|null} A tainted untraced address.
 */
function getTaintedUntraced (tainted, traced) {
  var address
  for (address of tainted) {
    if (!traced.has(address)) {
      return address
    }
  }
  return null
}

/**
 * Check whether save exists.
 * @param {string} sourceHex - Hex representation of taint source address.
 * @param {number} startBlock - Start block of tainting.
 * @return {boolean} Whether save exists.
 */
async function saveExists (sourceHex, startBlock) {
  return new Promise((resolve, reject) => {
    const dirPath = 'trace/' + sourceHex + '-' + startBlock
    fs.access(dirPath, err => {
      if (err) {
        resolve(false)
      } else {
        resolve(true)
      }
    })
  })
}

/**
 * Create directory and all parent directories.
 * @param {string} dirPath - Path to directory.
 * @return {undefined}
 */
async function createDirectory (dirPath) {
  return new Promise((resolve, reject) => {
    mkdirp(dirPath, err => {
      if (err) {
        reject(err)
      } else {
        resolve()
      }
    })
  })
}

/**
 * Initialize file.
 * @param {string} filePath - Path to file.
 * @param {string} data - Initial data.
 * @return {undefined}
 */
async function initializeFile (filePath, data) {
  return new Promise((resolve, reject) => {
    fs.writeFile(filePath, data, err => {
      if (err) {
        reject(err)
      } else {
        resolve()
      }
    })
  })
}

/**
 * Open file.
 * @param {string} filePath - Path to file.
 * @param {string} mode - File mode.
 * @return File descriptor.
 */
async function openFile (filePath, mode) {
  return new Promise((resolve, reject) => {
    fs.open(filePath, mode, (err, fd) => {
      if (err) {
        reject(err)
      } else {
        resolve(fd)
      }
    })
  })
}

/**
 * Close file.
 * @param fd - File descriptor.
 * @return {undefined}
 */
async function closeFile (fd) {
  return new Promise((resolve, reject) => {
    fs.close(fd, err => {
      if (err) {
        reject(err)
      } else {
        resolve()
      }
    })
  })
}

/**
 * Create empty file.
 * @param {string} filePath - Path to file.
 * @return {undefined}
 */
async function createEmptyFile (filePath) {
  const fd = await openFile(filePath, 'wx')
  await closeFile(fd)
}

/**
 * Read file.
 * @param {string} filePath - Path to file.
 * @return {string} File data.
 */
async function readFile (filePath) {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, 'utf8', (err, data) => {
      if (err) {
        reject(err)
      } else {
        resolve(data)
      }
    })
  })
}

/**
 * Write file.
 * @param {string} filePath - Path to file.
 * @param {string} data - File data.
 * @return {undefined}
 */
async function writeFile (filePath, data) {
  return new Promise((resolve, reject) => {
    fs.writeFile(filePath, data, err => {
      if (err) {
        reject(err)
      } else {
        resolve()
      }
    })
  })
}

/**
 * Initialize save.
 * @param {string} sourceHex - Hex representation of taint source address.
 * @param {number} startBlock - Start block of tainting.
 * @return {undefined}
 */
async function initializeSave (sourceHex, startBlock) {
  const dirPath = 'trace/' + sourceHex + '-' + startBlock
  await createDirectory(dirPath)
  const taintedFilePath = dirPath + '/tainted'
  const taintedFileData = sourceHex + '|' + startBlock + '\n'
  await initializeFile(taintedFilePath, taintedFileData)
  const tracedFilePath = dirPath + '/traced'
  await createEmptyFile(tracedFilePath)
}

/**
 * Record address tainted.
 * @param {string} sourceHex - Hex representation of source address.
 * @param {number} sourceStartBlock - Start block of source tainting.
 * @param {string} addressHex - Hex representation of tainted address.
 * @param {number} startBlock - Start block of tainting.
 * @return {undefined}
 */
async function recordTainted (sourceHex, sourceStartBlock, addressHex, startBlock) {
  await new Promise((resolve, reject) => {
    const filePath = 'trace/' + sourceHex + '-' + sourceStartBlock + '/tainted'
    const data = addressHex + '|' + startBlock + '\n'
    fs.appendFile(filePath, data, err => {
      if (err) {
        reject(err)
      } else {
        resolve()
      }
    })
  })
}

/**
 * Record address traced.
 * @param {string} sourceHex - Hex representation of source address.
 * @param {number} startBlock - Start block of tainting.
 * @param {string} addressHex - Hex representation of traced address.
 * @return {undefined}
 */
async function recordTraced (sourceHex, startBlock, addressHex) {
  await new Promise((resolve, reject) => {
    const filePath = 'trace/' + sourceHex + '-' + startBlock + '/traced'
    const data = addressHex + '\n'
    fs.appendFile(filePath, data, err => {
      if (err) {
        reject(err)
      } else {
        resolve()
      }
    })
  })
}

/**
 * Delete address traced.
 * @todo Improve efficiency of this procedure.
 * @param {string} sourceHex - Hex representation of source address.
 * @param {number} startBlock - Start block of tainting.
 * @param {string} addressHex - Hex representation of address needing new tracing.
 * @return {undefined}
 */
async function deleteTraced (sourceHex, startBlock, addressHex) {
  const filePath = 'trace/' + sourceHex + '-' + startBlock + '/traced'
  const fileData = await readFile(filePath)
  const fileLines = fileData.split('\n')
  const newLines = []
  for (let i = 0, line; i < fileLines.length; i++) {
    line = fileLines[i]
    if (line !== addressHex) {
      newLines.append(line)
    }
  }
  const newData = newLines.join('\n')
  await writeFile(filePath, newData)
}

/**
 * Process a transaction.
 */
async function processTransaction (
  tracker,
  taint,
  source,
  sourceStartBlock,
  address,
  tx,
  tainted,
  taintedFrom,
  traced
) {
  // No target
  if (tx.to === null) {
    return
  }

  // Input
  if (tx.to === address) {
    return
  }

  // No value
  if (tx.amount.wei.equals(0)) {
    return
  }

  // Already propagated
  if (tx.hasTaint(taint)) {
    return
  }

  // Record propagated
  tx.addTaint(taint)

  // Add to tainted list
  tainted.add(tx.to)

  // Already tainted
  if (tx.to.hasTaint(taint)) {
    if (taintedFrom.get(tx.to) > tx.block.number) {
      taintedFrom.set(tx.to, tx.block.number)
      traced.delete(tx.to)
      await deleteTraced(source.hex, sourceStartBlock, tx.to.hex)
      tracker.emit('reopenTrace', tx.to, taint)
    }
    return
  }

  // Record tainted
  taint.addRecipient(tx.to)
  tx.to.addTaint(taint)
  taintedFrom.set(tx.ot, tx.block.number)
  await recordTainted(source.hex, sourceStartBlock, tx.to.hex, tx.block.number)

  // Emit tainted
  tracker.emit('taint', tx.to, taint)
}

/**
 * New address tainted.
 * @event Tracker#taint
 * @type {Array}
 * @prop {module:primitives/address.Address} 0
 *     Tainted address.
 * @prop {module:primitives/taint.Taint} 1
 *     Propagated taint.
 */

/**
 * Acquiring page of transactions for tainted address.
 * @event Tracker#page
 * @type {Array}
 * @prop {module:primitives/address.Address} 0
 *     Tainted address.
 * @prop {number} 1
 *     Page number.
 */

/**
 * Processed transaction for tainted address.
 * @event Tracker#processedTransaction
 * @type {Array}
 * @prop {module:primitives/address.Address} 0
 *     Tainted address.
 * @prop {module:primitives/transaction.Transaction} 1
 *     Processed transaction.
 */

/**
 * Traced tainted address.
 * @event Tracker#tracedAddress
 * @type {Array}
 * @prop {module:primitives/address.Address} 0
 *     Tainted address.
 */

/**
 * Ethereum taint tracker.
 * @static
 * @emits Tracker#taint
 */
class Tracker extends EventEmitter {
  /**
   * No parameters.
   */
  constructor () {
    super()
    const priv = {}
    privs.set(this, priv)
    priv.cache = {
      block: new Cache(),
      address: new Cache(),
      tx: new Cache()
    }
    priv.chain = new ChainAgent(priv.cache)
    priv.pageSize = 50
    priv.tracing = false
    priv.canceling = false
  }

  /**
   * Request cancelation of a trace.
   * @return {undefined}
   */
  async cancelTrace () {
    const priv = privs.get(this)
    let res
    const prom = new Promise(resolve => { res = resolve })
    priv.canceling = res
    return prom
  }

  /**
   * Trace addresses tainted by specified source.
   * Starts asynchronous acquisition of all descendant tainting.
   * Each newly tainted address fires {@link event:Tracker#taint}.
   * Runs until extant chain data is exhausted.
   * Returned promise resolves when finished.
   * @todo Detect and use extant taint with same source.
   * @param {string} sourceHex
   *     Source address of taint. As an address hex.
   * @param {number} [startBlock=0]
   *     Start block of taint.
   * @return {undefined}
   */
  async traceAddresses (sourceHex, startBlock = 0) {
    // Validate arguments
    arg.addressHex(sourceHex)

    // Process arguments
    sourceHex = sourceHex.toLowerCase()

    // Private members
    const priv = privs.get(this)
    const cache = priv.cache
    const chain = priv.chain
    const pageSize = priv.pageSize
    if (priv.tracing) {
      throw new Error('Already tracing')
    }
    priv.tracing = true

    // Watch for trace failure
    try {
      // Get address
      let source = await cache.address.get(sourceHex)
      if (source === undef) {
        source = new Address(sourceHex)
        await cache.address.set(sourceHex, source)
      }

      // Create taint
      const taint = new Taint(source)
      source.addTaint(taint)

      // Working data
      const tainted = new Set()
      tainted.add(source)
      const taintedFrom = new Map()
      taintedFrom.set(source, startBlock)
      const traced = new Set()

      // Trace saving
      if (await saveExists(sourceHex, startBlock)) {
        const dirPath = 'trace/' + sourceHex + '-' + startBlock
        const taintedFilePath = dirPath + '/tainted'
        const taintedFileData = await readFile(taintedFilePath)
        const taintedFileLines = taintedFileData.split('\n')
        for (
          let i = 1, line, fields,
            addressHex, address,
            fromBlockString, fromBlock;
          i < taintedFileLines.length;
          i++
        ) {
          line = taintedFileLines[i]
          if (!line) continue
          fields = line.split('|')
          addressHex = fields[0]
          address = await cache.address.get(addressHex)
          if (address === undef) {
            address = new Address(addressHex)
            await cache.address.set(addressHex, address)
          }
          address.addTaint(taint)
          taint.addRecipient(address)
          tainted.add(address)
          fromBlockString = fields[1]
          fromBlock = Number.parseInt(fromBlockString)
          taintedFrom.set(address, fromBlock)
          this.emit('taint', address, taint)
        }
        const tracedFilePath = dirPath + '/traced'
        const tracedFileData = await readFile(tracedFilePath)
        const tracedFileLines = tracedFileData.split('\n')
        for (
          let i = 0, line,
            addressHex, address;
          i < tracedFileLines.length;
          i++
        ) {
          line = tracedFileLines[i]
          if (!line) continue
          addressHex = line
          address = await cache.address.get(addressHex)
          if (address === undef) {
            address = new Address(addressHex)
            await cache.address.set(addressHex, address)
          }
          traced.add(address)
          this.emit('tracedAddress', address)
        }
      } else {
        await initializeSave(sourceHex, startBlock)
      }

      // Trace
      for (
        let address = getTaintedUntraced(tainted, traced),
          fromBlockNumber;
        address !== null;
        traced.add(address),
        address = getTaintedUntraced(tainted, traced)
      ) {
        // Detect canceling
        if (priv.canceling) {
          priv.tracing = false
          priv.canceling()
          priv.canceling = false
          return
        }

        // Get address transactions
        let txs, numTxs, tx
        let page = 1
        do {
          // Detect canceling
          if (priv.canceling) {
            priv.tracing = false
            priv.canceling()
            priv.canceling = false
            return
          }

          // Get tainted from block number
          fromBlockNumber = taintedFrom.get(address)

          // Get next page of transactions
          this.emit(
            'page',
            address,
            page
          )
          txs = await chain
            .listAccountTransactions(address.hex, {
              startBlock: fromBlockNumber,
              page,
              pageSize
            })
          numTxs = txs.length

          // Process transactions
          for (var i = 0; i < numTxs; i++) {
            // Detect canceling
            if (priv.canceling) {
              priv.tracing = false
              priv.canceling()
              priv.canceling = false
              return
            }

            tx = txs[i]
            await processTransaction(
              this,
              taint,
              source,
              startBlock,
              address,
              tx,
              tainted,
              taintedFrom,
              traced
            )
            this.emit(
              'processedTransaction',
              address,
              tx
            )
          }

          // Increment page number
          page++
        } while (numTxs === pageSize)

        // Emit traced
        this.emit(
          'tracedAddress',
          address
        )

        // Record traced
        await recordTraced(sourceHex, startBlock, address.hex)
      }

      // End tracing
      priv.tracing = false
      priv.canceling = false
    } catch (e) {
      priv.tracing = false
      throw e
    }
  }
}

// Expose
module.exports = Tracker

// Circular imports
const arg = require('../util/arg')