/ pushtx / transactions-scheduler.js
transactions-scheduler.js
  1  /*!
  2   * pushtx/pushtx-rest-api.js
  3   * Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved.
  4   */
  5  
  6  
  7  import bitcoin from 'bitcoinjs-lib'
  8  import Logger from '../lib/logger.js'
  9  import errors from '../lib/errors.js'
 10  import db from '../lib/db/mysql-db-wrapper.js'
 11  import network from '../lib/bitcoin/network.js'
 12  import keysFile from '../keys/index.js'
 13  import { createRpcClient } from '../lib/bitcoind-rpc/rpc-client.js'
 14  import pushTxProcessor from './pushtx-processor.js'
 15  
 16  const keys = keysFile[network.key]
 17  
 18  /**
 19   * A class scheduling delayed push of transactions
 20   */
 21  class TransactionsScheduler {
 22  
 23      constructor() {
 24          this.rpcClient = createRpcClient()
 25      }
 26  
 27      /**
 28       * Schedule a set of transactions
 29       * according to a given sequential script
 30       * @param {object} script - scheduling script
 31       */
 32      async schedule(script) {
 33          // Check script length
 34          if (script.length > keys.txsScheduler.maxNbEntries)
 35              throw errors.body.SCRIPTSIZE
 36  
 37          // Order transactions by increasing hop values and nlocktime
 38          script.sort((a, b) => a.hop - b.hop || a.nlocktime - b.nlocktime)
 39  
 40          // Get the height of last block seen
 41          const info = await this.rpcClient.getblockchaininfo()
 42          const lastHeight = info.blocks
 43  
 44          // Get the nLockTime associated to the first transaction
 45          const nltTx0 = script[0].nlocktime
 46  
 47          // Check that nltTx0 is in allowed range of blocks
 48          if (nltTx0 > lastHeight + keys.txsScheduler.maxDeltaHeight)
 49              throw errors.pushtx.SCHEDULED_TOO_FAR
 50  
 51          // Compute base height for this script
 52          const baseHeight = Math.max(lastHeight, nltTx0)
 53  
 54          // Iterate over the transactions for a few validations
 55          let lastHopProcessed = -1
 56          let lastLockTimeProcessed = -1
 57          const faults = []
 58  
 59          for (let entry of script) {
 60              // Compute delta height (entry.nlocktime - nltTx0)
 61              entry.delta = entry.nlocktime - nltTx0
 62              // Check that delta is in allowed range
 63              if (entry.delta > keys.txsScheduler.maxDeltaHeight)
 64                  throw errors.pushtx.SCHEDULED_TOO_FAR
 65              // Decode the transaction
 66              const tx = bitcoin.Transaction.fromHex(entry.tx)
 67              // Check that nlocktimes are matching
 68              if (!(tx.locktime && tx.locktime === entry.nlocktime)) {
 69                  const message = `TransactionsScheduler.schedule() : nLockTime mismatch : ${tx.locktime} - ${entry.nlocktime}`
 70                  Logger.error(null, `PushTx : ${message}`)
 71                  throw errors.pushtx.NLOCK_MISMATCH
 72              }
 73              // Check that order of hop and nlocktime values are consistent
 74              if (entry.hop !== lastHopProcessed && entry.nlocktime < lastLockTimeProcessed) throw errors.pushtx.SCHEDULED_BAD_ORDER
 75              // Enforce strcit_mode_vouts if required
 76              const vouts = entry.strict_mode_vouts
 77              if (vouts) {
 78                  if (vouts.some((vout) => Number.isNaN(vout)))
 79                      throw errors.txout.VOUT
 80                  if (vouts.length > 0) {
 81                      let faultsTx = await pushTxProcessor.enforceStrictModeVouts(entry.tx, vouts)
 82                      if (faultsTx.length > 0) {
 83                          const txid = bitcoin.Transaction.fromHex(entry.tx).getId()
 84                          for (let vout of faultsTx) {
 85                              faults.push({
 86                                  'txid': txid,
 87                                  'hop': entry.hop,
 88                                  'vouts': vout
 89                              })
 90                          }
 91                      }
 92                  }
 93              }
 94              // Prepare verification of next hop
 95              lastHopProcessed = entry.hop
 96              lastLockTimeProcessed = entry.nlocktime
 97              // Update scheduled height if needed
 98              if (baseHeight !== nltTx0)
 99                  entry.nlocktime = baseHeight + entry.delta
100          }
101  
102          // Return if strict_mode_vout has detected errors
103          if (faults.length > 0) {
104              throw {
105                  'message': {
106                      'message': faults,
107                      'code': errors.pushtx.VIOLATION_STRICT_MODE_VOUTS
108                  }
109              }
110          }
111  
112          let parentTxid = null
113          let parentNlocktime = baseHeight
114  
115          // Check if first transactions should be sent immediately
116          while ((script.length > 0) && (script[0].nlocktime <= lastHeight) && (script[0].delta === 0)) {
117              await pushTxProcessor.pushTx(script[0].tx)
118              const tx = bitcoin.Transaction.fromHex(script[0].tx)
119              parentTxid = tx.getId()
120              parentNlocktime = script[0].nlocktime
121              script.splice(0, 1)
122          }
123  
124          // Store others transactions in database
125          let parentId = null
126  
127          for (let entry of script) {
128              const tx = bitcoin.Transaction.fromHex(entry.tx)
129  
130              const objectTx = {
131                  txid: tx.getId(),
132                  created: null,
133                  rawTx: entry.tx,
134                  parentId: parentId,
135                  parentTxid: parentTxid,
136                  delay: entry.nlocktime - parentNlocktime,   // Store delay relative to previous transaction
137                  trigger: entry.nlocktime
138              }
139  
140              parentId = await db.addScheduledTransaction(objectTx)
141              Logger.info(`PushTx : Registered scheduled tx ${objectTx.txid} (trigger=${objectTx.trigger})`)
142              parentTxid = tx.getId()
143              parentNlocktime = entry.nlocktime
144          }
145  
146  
147      }
148  
149  }
150  
151  export default TransactionsScheduler