diff --git a/doc/POST_pushtx.md b/doc/POST_pushtx.md index 029fe37..e0ed61e 100644 --- a/doc/POST_pushtx.md +++ b/doc/POST_pushtx.md @@ -10,6 +10,7 @@ Parameters must be passed in the body of the request as url encoded arguments. ## Parameters * **tx** - `hex string` - The raw transaction hex * **at** - `string` (optional) - Access Token (json web token). Required if authentication is activated. Alternatively, the access token can be passed through the `Authorization` HTTP header (with the `Bearer` scheme). +* **strict_mode_vouts** (optional) - `string` - A pipe-separated list of outpoints indices. A strict verification is enforced on these outpoints before the transaction is pushed. Strict mode checks that addresses associated to these outputs aren't reused. If verifications fail, push is aborted and an error is returned. ### Example @@ -18,6 +19,7 @@ Parameters must be passed in the body of the request as url encoded arguments. POST /pushtx/ tx=abcdef0123456789 +strict_mode_vouts=0|2|3 ``` #### Success @@ -32,11 +34,18 @@ Status code 200 with JSON response: #### Failure Status code 400 with JSON response: ```json +{ + "status": "error", + "error": "" +} +``` +or +```json { "status": "error", "error": { - "message": "", - "code": "" + "message": [vouts], + "code": "VIOLATION_STRICT_MODE_VOUTS" } } ``` diff --git a/docker/my-dojo/.env b/docker/my-dojo/.env index e212025..5352e51 100644 --- a/docker/my-dojo/.env +++ b/docker/my-dojo/.env @@ -13,7 +13,7 @@ COMPOSE_CONVERT_WINDOWS_PATHS=1 DOJO_VERSION_TAG=1.7.0 DOJO_DB_VERSION_TAG=1.2.0 DOJO_BITCOIND_VERSION_TAG=1.6.0 -DOJO_NODEJS_VERSION_TAG=1.6.0 +DOJO_NODEJS_VERSION_TAG=1.7.0 DOJO_NGINX_VERSION_TAG=1.5.0 DOJO_TOR_VERSION_TAG=1.4.0 DOJO_EXPLORER_VERSION_TAG=1.3.0 diff --git a/lib/errors.js b/lib/errors.js index 10b4a92..b8bef58 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -75,6 +75,7 @@ module.exports = { pushtx: { NLOCK_MISMATCH: 'nLockTime in script does not match nLockTime in transaction', SCHEDULED_TOO_FAR: 'nLockTime is set to far in the future', - SCHEDULED_BAD_ORDER: 'Order of hop and nLockTime values must be consistent' + SCHEDULED_BAD_ORDER: 'Order of hop and nLockTime values must be consistent', + VIOLATION_STRICT_MODE_VOUTS: 'VIOLATION_STRICT_MODE_VOUTS' } } diff --git a/package-lock.json b/package-lock.json index c22196a..2a84bca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "samourai-dojo", - "version": "1.6.0", + "version": "1.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0da9cec..9ea48f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "samourai-dojo", - "version": "1.6.0", + "version": "1.7.0", "description": "Backend server for Samourai Wallet", "main": "accounts/index.js", "scripts": { diff --git a/pushtx/pushtx-processor.js b/pushtx/pushtx-processor.js index cfe0ecb..0ba6d4c 100644 --- a/pushtx/pushtx-processor.js +++ b/pushtx/pushtx-processor.js @@ -8,11 +8,20 @@ const bitcoin = require('bitcoinjs-lib') const zmq = require('zeromq') const Logger = require('../lib/logger') const errors = require('../lib/errors') +const db = require('../lib/db/mysql-db-wrapper') const RpcClient = require('../lib/bitcoind-rpc/rpc-client') const network = require('../lib/bitcoin/network') +const activeNet = network.network const keys = require('../keys')[network.key] const status = require('./status') +let Sources +if (network.key == 'bitcoin') { + Sources = require('../lib/remote-importer/sources-mainnet') +} else { + Sources = require('../lib/remote-importer/sources-testnet') +} + /** * A singleton providing a wrapper @@ -25,6 +34,7 @@ class PushTxProcessor { */ constructor() { this.notifSock = null + this.sources = new Sources() // Initialize the rpc client this.rpcClient = new RpcClient() } @@ -38,6 +48,45 @@ class PushTxProcessor { this.notifSock.bindSync(config.uriSocket) } + /** + * Enforce a strict verification mode on a list of outputs + * @param {string} rawtx - raw bitcoin transaction in hex format + * @param {array} vouts - output indices (integer) + * @returns {array} returns the indices of the faulty outputs + */ + async enforceStrictModeVouts(rawtx, vouts) { + const faultyOutputs = [] + const addrMap = {} + let tx + try { + tx = bitcoin.Transaction.fromHex(rawtx) + } catch(e) { + throw errors.tx.PARSE + } + // Check in db if addresses are known and have been used + for (let vout of vouts) { + if (vout >= tx.outs.length) + throw errors.txout.VOUT + const output = tx.outs[vout] + const address = bitcoin.address.fromOutputScript(output.script, activeNet) + const nbTxs = await db.getAddressNbTransactions(address) + if (nbTxs == null || nbTxs > 0) + faultyOutputs.push(vout) + else + addrMap[address] = vout + } + // Checks with indexer if addresses are known and have been used + if (Object.keys(addrMap).length > 0) { + if (keys.indexer.active != 'local_bitcoind') { + const results = await this.sources.getAddresses(Object.keys(addrMap)) + for (let r of results) + if (r.ntx > 0) + faultyOutputs.push(addrMap[r.address]) + } + } + return faultyOutputs + } + /** * Push transactions to the Bitcoin network * @param {string} rawtx - raw bitcoin transaction in hex format diff --git a/pushtx/pushtx-rest-api.js b/pushtx/pushtx-rest-api.js index 7c9dcbd..c17923a 100644 --- a/pushtx/pushtx-rest-api.js +++ b/pushtx/pushtx-rest-api.js @@ -152,6 +152,27 @@ class PushTxRestApi { if (!validator.isHexadecimal(query.tx)) return this._traceError(res, errors.body.INVDATA) + if (query.strict_mode_vouts) { + try { + const vouts = query.strict_mode_vouts.split('|').map(v => parseInt(v, 10)) + if (vouts.some(isNaN)) + throw errors.txout.VOUT + if (vouts.length > 0) { + let faults = await pushTxProcessor.enforceStrictModeVouts(query.tx, vouts) + if (faults.length > 0) { + return this._traceError(res, { + 'message': JSON.stringify({ + 'message': faults, + 'code': errors.pushtx.VIOLATION_STRICT_MODE_VOUTS + }) + }) + } + } + } catch(e) { + return this._traceError(res, e) + } + } + try { const txid = await pushTxProcessor.pushTx(query.tx) HttpServer.sendOkData(res, txid) @@ -198,10 +219,7 @@ class PushTxRestApi { if (msg.code && msg.message) { Logger.error(null, 'PushTx : Error ' + msg.code + ': ' + msg.message) - ret = { - message: msg.message, - code: msg.code - } + ret = msg } else { Logger.error(err.message, 'PushTx : ') ret = err.message diff --git a/pushtx/transactions-scheduler.js b/pushtx/transactions-scheduler.js index 68425c2..25edc68 100644 --- a/pushtx/transactions-scheduler.js +++ b/pushtx/transactions-scheduler.js @@ -57,6 +57,7 @@ class TransactionsScheduler { // Iterate over the transactions for a few validations let lastHopProcessed = -1 let lastLockTimeProcessed = -1 + const faults = [] for (let entry of script) { // Compute delta height (entry.nlocktime - nltTx0) @@ -77,6 +78,30 @@ class TransactionsScheduler { if (entry.nlocktime < lastLockTimeProcessed) throw errors.pushtx.SCHEDULED_BAD_ORDER } + // Enforce strcit_mode_vouts if required + const vouts = entry.strict_mode_vouts + if (vouts) { + try { + if (vouts.some(isNaN)) + throw errors.txout.VOUT + if (vouts.length > 0) { + let faultsTx = await pushTxProcessor.enforceStrictModeVouts(entry.tx, vouts) + if (faultsTx.length > 0) { + const txid = bitcoin.Transaction.fromHex(entry.tx).getId() + for (let vout of faultsTx) { + faults.push({ + "txid": txid, + "hop": entry.hop, + "vouts": vout + }) + } + } + } + } catch(e) { + throw e + } + } + // Prepare verification of next hop lastHopProcessed = entry.hop lastLockTimeProcessed = entry.nlocktime // Update scheduled height if needed @@ -84,6 +109,16 @@ class TransactionsScheduler { entry.nlocktime = baseHeight + entry.delta } + // Return if strict_mode_vout has detected errors + if (faults.length > 0) { + throw { + 'message': JSON.stringify({ + 'message': faults, + 'code': errors.pushtx.VIOLATION_STRICT_MODE_VOUTS + }) + } + } + let parentTxid = null let parentNlocktime = baseHeight