From 2a3baf39b9a6b3c1cb80990f7fba49a4b3bd1ef1 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 13 May 2021 09:27:50 -0700 Subject: [PATCH] feat(send transaction): Allowing specific node for broadcasting txs --- src/routes/v4/full-node/rawtransactions.js | 38 ++++++++++++++++++---- start-dev-example.sh | 5 +++ test/v4/raw-transactions.js | 3 ++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/routes/v4/full-node/rawtransactions.js b/src/routes/v4/full-node/rawtransactions.js index f38b677..1fcf84c 100644 --- a/src/routes/v4/full-node/rawtransactions.js +++ b/src/routes/v4/full-node/rawtransactions.js @@ -140,7 +140,7 @@ class RawTransactions { const options = _this.routeUtils.getAxiosOptions() // Loop through each height and creates an array of requests to call in parallel - const promises = hexes.map(async hex => { + const promises = hexes.map(async (hex) => { options.data.id = 'decoderawtransaction' options.data.method = 'decoderawtransaction' options.data.params = [hex] @@ -152,7 +152,7 @@ class RawTransactions { const axiosResult = await _this.axios.all(promises) // Retrieve the data part of the result. - const result = axiosResult.map(x => x.data.result) + const result = axiosResult.map((x) => x.data.result) res.status(200) return res.json(result) @@ -254,7 +254,7 @@ class RawTransactions { const options = _this.routeUtils.getAxiosOptions() // Loop through each hex and create an array of promises - const promises = hexes.map(async hex => { + const promises = hexes.map(async (hex) => { options.data.id = 'decodescript' options.data.method = 'decodescript' options.data.params = [hex] @@ -267,7 +267,7 @@ class RawTransactions { const resolved = await Promise.all(promises) // Retrieve the data from each resolved promise. - const result = resolved.map(x => x.data.result) + const result = resolved.map((x) => x.data.result) res.status(200) return res.json(result) @@ -348,7 +348,7 @@ class RawTransactions { } // Loop through each txid and create an array of promises - const promises = txids.map(async txid => + const promises = txids.map(async (txid) => _this.getRawTransactionsFromNode(txid, verbose) ) @@ -436,7 +436,8 @@ class RawTransactions { return res.json({ error: 'hex must be an array' }) } - const options = _this.routeUtils.getAxiosOptions() + let options = _this.routeUtils.getAxiosOptions() + options = _this.sendTxOptions(options) // Enforce array size rate limits if (!_this.routeUtils.validateArraySize(req, hexes)) { @@ -536,7 +537,8 @@ class RawTransactions { }) } - const options = _this.routeUtils.getAxiosOptions() + let options = _this.routeUtils.getAxiosOptions() + options = _this.sendTxOptions(options) // RPC call options.data.id = 'sendrawtransaction' @@ -557,6 +559,28 @@ class RawTransactions { return _this.errorHandler(err, res) } } + + // This method modifies the default axios options. It attempts to inject + // a specific full node to use when broadcasting transactions. This is useful + // because it leverages the built-in protections that a full node has against + // accidental double spends. It mitigates a corner-case when rapidly spending + // TXs on load balanced nodes. By piping all TX sends through a single node, + // accidental double spends can be reduced. + sendTxOptions (options) { + try { + if (process.env.RPC_SENDURL) { + console.log(`original options: ${JSON.stringify(options, null, 2)}`) + options.baseURL = process.env.RPC_SENDURL + + console.log(`modified options: ${JSON.stringify(options, null, 2)}`) + } + + return options + } catch (err) { + wlogger.error('Error in rawtransactions.js/sendTxOptions()') + throw err + } + } } module.exports = RawTransactions diff --git a/start-dev-example.sh b/start-dev-example.sh index d4a2adb..12f28c8 100755 --- a/start-dev-example.sh +++ b/start-dev-example.sh @@ -15,6 +15,11 @@ export RPC_BASEURL=http://$RPC_IP/ export RPC_USERNAME=bitcoin export RPC_PASSWORD=password +# (optional) If load-balancing multiple full nodes, it's best to pick a single +# one for broadcasting transactions to the network. Prevents accidental +# double spends. +#export RPC_SENDURL=http://$RPC_IP/ + # SLPDB export SLPDB_PASS_GP=somelongpassword export SLPDB_URL=http://:12300/ diff --git a/test/v4/raw-transactions.js b/test/v4/raw-transactions.js index 7a4101d..587a12c 100644 --- a/test/v4/raw-transactions.js +++ b/test/v4/raw-transactions.js @@ -1047,10 +1047,12 @@ describe('#Raw-Transactions', () => { // Save the existing RPC URL. const savedUrl = process.env.BITCOINCOM_BASEURL const savedUrl2 = process.env.RPC_BASEURL + const savedUrl3 = process.env.RPC_SENDURL // Manipulate the URL to cause a 500 network error. process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/' process.env.RPC_BASEURL = 'http://fakeurl/api/' + process.env.RPC_SENDURL = 'http://fakeurl/api/' req.params.hex = '020000000136697692fed77bc4f5b6885295d0c56d1d0280fb578f445ce42be4eb6db381f2010000006a4730440220473adba0e7da14f0abf4817bbd591741ecb8da6544b998f10341f6704f5f05280220405221c626cb7edcf333367ebd469aff3f5a2169e37ee58eebb811ffc2fbc9e0412102202ff86325c5d903171fa5a2895c4efb3765105115460dc96f113048ddb69b47feffffff027a621b00000000001976a914e0a8ffc3b91e35f46618d6db90f66397989abf0588ac38041300000000001976a914a741f282af390bc7ea8c4375a3a56401d668564288ac2c330900' @@ -1060,6 +1062,7 @@ describe('#Raw-Transactions', () => { // Restore the saved URL. process.env.BITCOINCOM_BASEURL = savedUrl process.env.RPC_BASEURL = savedUrl2 + process.env.RPC_SENDURL = savedUrl3 assert.isAbove( res.statusCode,