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