Add support for bitcoin P2PK scripts/txes

-Adds a tx_type field to the tx model which is typically null for "normal" transaction types, but can also display 'p2pk' for bitcoin txes which require addtional encoding to reveal the P2PKH address as well as 'zksnarks' for transactions with hidden sender or receiver data
-Additional fixes for how data is displayed when a valid wallet address cannot be found
-Includes some small updates to how zksnarks transactions display hidden sender/receiver data
This commit is contained in:
joeuhren
2021-03-20 01:34:13 -06:00
parent 20c0a382a3
commit 66e3ca31e6
13 changed files with 330 additions and 58 deletions
+39 -16
View File
@@ -10,6 +10,7 @@ var mongoose = require('mongoose'),
Heavy = require('../models/heavy'),
lib = require('./explorer'),
settings = require('./settings'),
locale = require('./locale'),
fs = require('fs'),
coindesk = require('./apis/coindesk'),
async = require('async');
@@ -115,8 +116,8 @@ function find_tx(txid, cb) {
function save_tx(txid, blockheight, cb) {
lib.get_rawtransaction(txid, function(tx) {
if (tx && tx != 'There was an error. Check your console.') {
lib.prepare_vin(tx, function(vin) {
lib.prepare_vout(tx.vout, txid, vin, ((!settings.blockchain_specific.zksnarks.enabled || typeof tx.vjoinsplit === 'undefined' || tx.vjoinsplit == null) ? [] : tx.vjoinsplit), function(vout, nvin) {
lib.prepare_vin(tx, function(vin, tx_type_vin) {
lib.prepare_vout(tx.vout, txid, vin, ((!settings.blockchain_specific.zksnarks.enabled || typeof tx.vjoinsplit === 'undefined' || tx.vjoinsplit == null) ? [] : tx.vjoinsplit), function(vout, nvin, tx_type_vout) {
lib.syncLoop(vin.length, function (loop) {
var i = loop.iteration();
@@ -142,7 +143,8 @@ function save_tx(txid, blockheight, cb) {
total: total.toFixed(8),
timestamp: tx.time,
blockhash: tx.blockhash,
blockindex: blockheight
blockindex: blockheight,
tx_type: (tx_type_vout == null ? tx_type_vin : tx_type_vout)
});
newTx.save(function(err) {
@@ -172,6 +174,22 @@ function get_market_data(market, coin_symbol, pair_symbol, cb) {
return cb(null);
}
function check_add_db_field(model_obj, field_name, default_value, cb) {
// determine if a particular field exists in a db collection
model_obj.findOne({[field_name]: {$exists: false}}, function(err, model_data) {
// check if field exists
if (model_data) {
// add field to all documents in the collection
model_obj.updateMany({}, {
$set: { [field_name]: default_value }
}, function() {
return cb(true);
});
} else
return cb(false);
});
}
module.exports = {
// initialize DB
connect: function(database, cb) {
@@ -299,22 +317,27 @@ module.exports = {
}
},
check_txes: function(cb) {
Tx.findOne({}, function(err, tx) {
if (tx) {
// collection has data
// determine if tx_type field exists
check_add_db_field(Tx, 'tx_type', null, function(exists) {
return cb(true);
});
} else
return cb(false);
});
},
check_stats: function(coin, cb) {
Stats.findOne({coin: coin}, function(err, stats) {
if (stats) {
// collection exists, now check if it is missing the last_usd_price column
Stats.findOne({last_usd_price: {$exists: false}}, function(err, stats) {
if (stats) {
// the last_usd_price needs to be added to the collection
Stats.updateOne({coin: coin}, {
last_usd_price: 0
}, function() {
return cb(null);
});
}
// collection has data
// determine if last_usd_price field exists
check_add_db_field(Stats, 'last_usd_price', 0, function(exists) {
return cb(true);
});
return cb(true);
} else
return cb(false);
});
@@ -366,7 +389,7 @@ module.exports = {
var burn_addresses = settings.richlist_page.burned_coins.addresses;
// always omit the private address from the richlist
burn_addresses.push("private_tx");
burn_addresses.push('hidden_address');
if (list == 'received') {
// update 'received' richlist data
+217 -29
View File
@@ -1,5 +1,6 @@
var request = require('postman-request'),
settings = require('./settings'),
locale = require('./locale'),
Address = require('../models/address');
var base_server = 'http://127.0.0.1:' + settings.webserver.port + "/";
@@ -68,6 +69,56 @@ function convertHashUnits(hashes) {
}
}
function processVoutAddresses(address_list, vout_value, arr_vout, cb) {
// check if there are any addresses to process
if (address_list != null && address_list.length > 0) {
// check if vout address is unique, if so add to array, if not add its amount to existing index
module.exports.is_unique(arr_vout, address_list[0], function(unique, index) {
if (unique == true) {
// unique vout
module.exports.convert_to_satoshi(parseFloat(vout_value), function(amount_sat) {
arr_vout.push({addresses: address_list[0], amount: amount_sat});
return cb(arr_vout);
});
} else {
// already exists
module.exports.convert_to_satoshi(parseFloat(vout_value), function(amount_sat) {
arr_vout[index].amount = arr_vout[index].amount + amount_sat;
return cb(arr_vout);
});
}
});
} else {
// no address, move to next vout
return cb(arr_vout);
}
}
function encodeP2PKaddress(p2pk_descriptor, cb) {
// find the descriptor value
module.exports.get_descriptorinfo(p2pk_descriptor, function(descriptor_info) {
// check for errors
if (descriptor_info != null) {
// encode the address using the output descriptor
module.exports.get_deriveaddresses(descriptor_info.descriptor, function(p2pkh_address) {
// check for errors
if (p2pkh_address != null) {
// return P2PKH address
return cb(p2pkh_address);
} else {
// address could not be encoded
return cb(null);
}
});
} else {
// address could not be encoded
return cb(null);
}
});
}
module.exports = {
convert_to_satoshi: function(amount, cb) {
// fix to 8dp & convert to string
@@ -629,6 +680,67 @@ module.exports = {
}
},
get_descriptorinfo: function(descriptor, cb) {
// format the descriptor correctly for use in the getdescriptorinfo cmd
descriptor = 'pkh(' + descriptor.replace(' OP_CHECKSIG', '') + ')';
var cmd = prepareRpcCommand(settings.blockchain_specific.bitcoin.api_cmds.getdescriptorinfo, (descriptor ? [descriptor] : []));
if (!(cmd.method == '' && cmd.parameters.length == 0)) {
if (settings.api_cmds.use_rpc) {
rpcCommand([{method:cmd.method, parameters: cmd.parameters}], function(response) {
// check if an error msg was received from the rpc server
if (response == 'There was an error. Check your console.')
return cb(null);
else
return cb(response);
});
} else {
var uri = base_url + 'getdescriptorinfo?descriptor=' + encodeURIComponent(descriptor);
request({uri: uri, json: true, headers: {'User-Agent': 'eiquidus'}}, function (error, response, body) {
// check if an error msg was received from the web api server
if (body == 'There was an error. Check your console.')
return cb(null);
else
return cb(body);
});
}
} else {
// cmd not in use. return null.
return cb(null);
}
},
get_deriveaddresses: function(descriptor, cb) {
var cmd = prepareRpcCommand(settings.blockchain_specific.bitcoin.api_cmds.deriveaddresses, (descriptor ? [descriptor] : []));
if (!(cmd.method == '' && cmd.parameters.length == 0)) {
if (settings.api_cmds.use_rpc) {
rpcCommand([{method:cmd.method, parameters: cmd.parameters}], function(response) {
// check if an error msg was received from the rpc server
if (response == 'There was an error. Check your console.')
return cb(null);
else
return cb(response);
});
} else {
var uri = base_url + 'deriveaddresses?descriptor=' + encodeURIComponent(descriptor);
request({uri: uri, json: true, headers: {'User-Agent': 'eiquidus'}}, function (error, response, body) {
// check if an error msg was received from the web api server
if (body == 'There was an error. Check your console.')
return cb(null);
else
return cb(body);
});
}
} else {
// cmd not in use. return null.
return cb(null);
}
},
// synchonous loop used to interate through an array,
// avoid use unless absolutely neccessary
syncLoop: function(iterations, process, exit) {
@@ -903,32 +1015,63 @@ module.exports = {
prepare_vout: function(vout, txid, vin, vhidden, cb) {
var arr_vout = [];
var arr_vin = vin;
var tx_type = null;
module.exports.syncLoop(vout.length, function (loop) {
var i = loop.iteration();
// make sure vout has an address
if (vout[i].scriptPubKey.type != 'nonstandard' && vout[i].scriptPubKey.type != 'nulldata') {
// check if tx is public or private (private = if no out address)
if (vout[i].scriptPubKey.type != 'zerocoinmint' && typeof vout[i].scriptPubKey.addresses != 'undefined') {
// check if vout address is unique, if so add it array, if not add its amount to existing index
module.exports.is_unique(arr_vout, vout[i].scriptPubKey.addresses[0], function(unique, index) {
if (unique == true) {
// unique vout
module.exports.convert_to_satoshi(parseFloat(vout[i].value), function(amount_sat) {
arr_vout.push({addresses: vout[i].scriptPubKey.addresses[0], amount: amount_sat});
loop.next();
// check if this is a zerocoin tx
if (vout[i].scriptPubKey.type != 'zerocoinmint') {
var address_list = vout[i].scriptPubKey.addresses;
// check if there are one or more addresses in the vout
if (address_list == null || address_list.length == 0) {
// no addresses defined
// check if bitcoin features are enabled
if (settings.blockchain_specific.bitcoin.enabled == true) {
// assume the asm value is a P2PK (Pay To Pubkey) public key that should be encoded as a P2PKH (Pay To Pubkey Hash) address
encodeP2PKaddress(vout[i].scriptPubKey.asm, function(p2pkh_address) {
// check if the address was encoded properly
if (p2pkh_address != null) {
// mark this tx as p2pk
tx_type = 'p2pk';
// process vout addresses
processVoutAddresses(p2pkh_address, vout[i].value, arr_vout, function(vout_array) {
// save updated array
arr_vout = vout_array;
// move to next vout
loop.next();
});
} else {
// could not decipher the address, save as unknown and move to next vout
module.exports.convert_to_satoshi(parseFloat(vout[i].value), function(amount_sat) {
console.log('Failed to find vout address from tx ' + txid);
arr_vout.push({addresses: 'unknown_address', amount: amount_sat});
loop.next();
});
}
});
} else {
// already exists
// could not decipher the address, save as unknown and move to next vout
module.exports.convert_to_satoshi(parseFloat(vout[i].value), function(amount_sat) {
arr_vout[index].amount = arr_vout[index].amount + amount_sat;
console.log('Failed to find vout address from tx ' + txid);
arr_vout.push({addresses: 'unknown_address', amount: amount_sat});
loop.next();
});
}
});
} else {
// process vout addresses
processVoutAddresses(address_list, vout[i].value, arr_vout, function(vout_array) {
// save updated array
arr_vout = vout_array;
// move to next vout
loop.next();
});
}
} else {
// private tx
// TODO: save this data to be able to show an anon tx
// TODO: add support for zerocoin transactions
console.log('Zerocoin tx found. skipping for now as it is unsupported');
tx_type = "zerocoin";
loop.next();
}
} else {
@@ -942,20 +1085,22 @@ module.exports = {
vhidden.forEach(function(vanon, i) {
if (vanon.vpub_old > 0) {
module.exports.convert_to_satoshi(parseFloat(vanon.vpub_old), function(amount_sat) {
arr_vout.push({addresses: "private_tx", amount: amount_sat});
arr_vout.push({addresses: 'hidden_address', amount: amount_sat});
});
} else {
module.exports.convert_to_satoshi(parseFloat(vanon.vpub_new), function(amount_sat) {
if (vhidden.length > 0 && (!vout || vout.length == 0) && (!vin || vin.length == 0)) {
// hidden sender is sending to hidden recipient
// the sent and received values are not known in this case. only the fee paid is known and subtracted from the sender.
arr_vout.push({addresses: "private_tx", amount: 0});
arr_vout.push({addresses: 'hidden_address', amount: 0});
}
// add a private send address with the known amount sent
arr_vin.push({addresses: "private_tx", amount: amount_sat});
arr_vin.push({addresses: 'hidden_address', amount: amount_sat});
});
}
tx_type = "zksnarks";
});
}
@@ -966,13 +1111,13 @@ module.exports = {
arr_vout[0].amount = arr_vout[0].amount - arr_vin[0].amount;
arr_vin.shift();
return cb(arr_vout, arr_vin);
return cb(arr_vout, arr_vin, tx_type);
} else
return cb(arr_vout, arr_vin);
return cb(arr_vout, arr_vin, tx_type);
} else
return cb(arr_vout, arr_vin);
return cb(arr_vout, arr_vin, tx_type);
} else
return cb(arr_vout, arr_vin);
return cb(arr_vout, arr_vin, tx_type);
});
},
@@ -989,24 +1134,56 @@ module.exports = {
loop.next();
}, function() {
addresses.push({hash: 'coinbase', amount: amount});
return cb(addresses);
return cb(addresses, null);
});
} else {
module.exports.get_rawtransaction(input.txid, function(tx) {
if (tx) {
var tx_type = null;
module.exports.syncLoop(tx.vout.length, function (loop) {
var i = loop.iteration();
if (tx.vout[i].n == input.vout) {
if (tx.vout[i].scriptPubKey.addresses)
if (tx.vout[i].scriptPubKey.addresses) {
addresses.push({hash: tx.vout[i].scriptPubKey.addresses[0], amount:tx.vout[i].value});
loop.break(true);
loop.next();
loop.break(true);
loop.next();
} else {
// no addresses defined
// check if bitcoin features are enabled
if (settings.blockchain_specific.bitcoin.enabled == true) {
// assume the asm value is a P2PK (Pay To Pubkey) public key that should be encoded as a P2PKH (Pay To Pubkey Hash) address
encodeP2PKaddress(tx.vout[i].scriptPubKey.asm, function(p2pkh_address) {
// check if the address was encoded properly
if (p2pkh_address != null) {
// mark this tx as p2pk
tx_type = 'p2pk';
// save the P2PKH address
addresses.push({hash: p2pkh_address, amount: tx.vout[i].value});
} else {
// could not decipher the address, save as unknown and move to next vin
addresses.push({hash: 'unknown_address', amount: tx.vout[i].value});
console.log('Failed to find vin address from tx ' + input.txid);
}
loop.break(true);
loop.next();
});
} else {
// could not decipher the address, save as unknown and move to next vin
addresses.push({hash: 'unknown_address', amount: tx.vout[i].value});
console.log('Failed to find vin address from tx ' + input.txid);
loop.break(true);
loop.next();
}
}
} else
loop.next();
}, function() {
return cb(addresses);
return cb(addresses, tx_type);
});
} else
return cb();
@@ -1016,11 +1193,18 @@ module.exports = {
prepare_vin: function(tx, cb) {
var arr_vin = [];
var tx_type = null;
module.exports.syncLoop(tx.vin.length, function (loop) {
var i = loop.iteration();
module.exports.get_input_addresses(tx.vin[i], tx.vout, function(addresses) {
module.exports.get_input_addresses(tx.vin[i], tx.vout, function(addresses, tx_type_vin) {
// check if the tx type is set
if (tx_type_vin != null) {
// set the tx type return value
tx_type = tx_type_vin;
}
if (addresses && addresses.length) {
module.exports.is_unique(arr_vin, addresses[0].hash, function(unique, index) {
if (unique == true) {
@@ -1035,11 +1219,15 @@ module.exports = {
});
}
});
} else
} else {
// could not decipher the address, save as unknown and move to next vin
console.log('Failed to find vin address from tx ' + tx.txid);
arr_vin.push({addresses: 'unknown_address', amount: 0});
loop.next();
}
});
}, function() {
return cb(arr_vin);
return cb(arr_vin, tx_type);
});
}
};
+4
View File
@@ -61,8 +61,12 @@ exports.bits = "Bits",
exports.nonce = "Nonce",
exports.new_coins = "New Coins",
exports.proof_of_stake = "PoS",
exports.hidden_address = "Hidden Address",
exports.hidden_sender = "Hidden Sender",
exports.hidden_recipient = "Hidden Recipient",
exports.unknown_address = "Unknown Address",
exports.unknown_sender = "Unknown Sender",
exports.unknown_recipient = "Unknown Recipient",
exports.initial_index_alert = "Blockchain data is currently being synchronized. You may browse the site during this time, but keep in mind that data may not yet be fully accurate and some functionality may not work until synchronization is complete.",
exports.a_menu_showing = "Showing",
+16
View File
@@ -838,6 +838,22 @@ exports.api_cmds = {
// blockchain_specific: A collection of settings that pertain to non-standard blockchain features that can extend the functionality of the default explorer
exports.blockchain_specific = {
// bitcoin: A collection of settings that pertain to Bitcoin-specific scripts (P2PK support)
"bitcoin": {
// enabled: Enable/disable the use of bitcoin scripts in the explorer (true/false)
// If set to false, all P2PK transactions will be saved without addresses as they require special encoding to reveal the more familiar P2PKH address
// NOTE: Enabling this feature will require a full reindex of the blockchain data to fix any P2PK transactions that were previously not displaying addresses
"enabled": false,
//api_cmds: A collection of settings that pertain to the list of customizable bitcoin rpc api commands
// Not all blockchains utilize the same rpc cmds for accessing the internal daemon api. Use these settings to set alternate names for similar api cmds.
// Leaving a cmd value blank ( "" ) will completely disable use of that cmd.
"api_cmds": {
// getdescriptorinfo: Accepts a descriptor as input and returns an object with more detailed information, including its computed checksum
"getdescriptorinfo": "getdescriptorinfo",
// deriveaddresses: Accepts an output descriptor as input and returns an array containing one or more P2PKH addresses
"deriveaddresses": "deriveaddresses"
}
},
// heavycoin: A collection of settings that pertain to the democratic voting and reward capabilities of the heavycoin blockchain
"heavycoin": {
// enabled: Enable/disable the use of heavycoin features in the explorer (true/false)