Skip to content
Browse files

Initial

  • Loading branch information...
1 parent 7aed4cd commit 91b17456a4ccaa5b17382afbb896260b0459a04c @0i0 committed Oct 31, 2012
Showing with 620 additions and 4 deletions.
  1. +0 −4 README.md
  2. +73 −0 app.js
  3. +214 −0 color.js
  4. +10 −0 package.json
  5. +43 −0 public/css/reset.css
  6. +30 −0 public/css/style.css
  7. +176 −0 query.js
  8. +13 −0 views/home.jade
  9. +17 −0 views/layout.jade
  10. +44 −0 views/tx.jade
View
4 README.md
@@ -1,4 +0,0 @@
-bitcoinjs-color
-===============
-
-tool to check colored bitcoin on bitcoinjs
View
73 app.js
@@ -0,0 +1,73 @@
+var express = require('express');
+var bitcoin = require('bitcoinjs');
+var RpcClient = require('jsonrpc2').Client;
+var Util = module.exports = require('bitcoinjs').Util;
+
+var fs = require('fs');
+var init = require('bitcoinjs/daemon/init');
+var config = init.getConfig();
+
+var app = module.exports = express.createServer();
+
+var rpcClient = new RpcClient(config.jsonrpc.port, config.jsonrpc.host,
+ config.jsonrpc.username, config.jsonrpc.password);
+
+var rpc = rpcClient.connectSocket();
+
+rpc.on('connect', function () {
+ var moduleSrc = fs.readFileSync(__dirname + '/query.js', 'utf8');
+ rpc.call('definerpcmodule', ['explorer', moduleSrc], function (err) {
+ if (err) {
+ console.error('Error registering query module: '+err.toString());
+ }
+ });
+});
+
+var vm = require('vm')
+var str = fs.readFileSync(__dirname + '/color.js', 'utf8');
+this.console = console
+vm.runInNewContext(str, this);
+
+// Configuration
+
+app.configure(function(){
+ app.set('views',__dirname + '/views')
+ app.set('view engine', 'jade')
+ app.set('view options', { layout: false })
+ app.use(express.bodyParser());
+ app.use(express.methodOverride());
+ app.use(app.router);
+ app.use(express.static(__dirname + '/public'));
+});
+
+// Routes
+app.get('/',function(req, res){
+ res.render('home.jade',{})
+})
+
+app.get('/json/tx/:txHash', function(req, res, next){
+ hash = Util.decodeHex(req.params.txHash).reverse();
+ var hash64 = hash.toString('base64');
+ rpc.call('explorer.txquery', [hash64], function (err, tx) {
+ if (err) return next(err);
+ res.write(JSON.stringify(tx.tx));
+ res.end();
+ });
+});
+
+app.get('/tx/:txHash', function(req, res, next){
+ hash = Util.decodeHex(req.params.txHash).reverse();
+ var hash64 = hash.toString('base64');
+ rpc.call('explorer.txquery', [hash64], function (err, tx) {
+ if (err) return next(err);
+ res.render('tx.jade',
+ { tx : tx.tx
+ }
+ )
+ });
+});
+
+if (!module.parent) {
+ app.listen(3333);
+ console.info("Express server listening on port " + 3333);
+}
View
214 color.js
@@ -0,0 +1,214 @@
+//constants
+var COLOR = {
+ MIXED: -1,
+ UNKNOWN: -2,
+ DEFAULT: 0
+};
+
+function uncolored(i) {
+ return (i <= 0);
+}
+
+function compute_colors(inputs, outputs) {
+ // initialize state
+ var cur_amount = 0;
+ var cur_color = COLOR.UNKNOWN;
+ var i = 0; //input index
+
+ for (var o = 0; o < outputs.length; ++o) {
+ var want_amount = outputs[o].amount;
+
+ if (want_amount > 0) {
+ // normal output: make sure we have enough in cur_amount, eating as many inputs as necessary
+ while ((cur_amount < want_amount) && (i < inputs.length)) {
+
+ if (cur_amount == 0)
+ cur_color = inputs[i].color;
+ else if (cur_color != inputs[i].color)
+ cur_color = COLOR.MIXED;
+
+ cur_amount += inputs[i].amount;
+ ++i;
+ }
+
+ if (cur_amount < want_amount)
+ return false; // transaction itself is invalid
+
+ } else {
+ // deal with zero-valued output
+ if (cur_amount == 0 && i < inputs.length && inputs[i].amount == 0) {
+ // there is a matching zero-valued input, eat it and use its color
+ cur_color = inputs[i].color;
+ ++i;
+ } else {
+ // there is no matching input so we cannot deduce zero-valued output's
+ // color and thus use COLOR.MIXED
+ cur_color = COLOR.MIXED;
+ }
+ }
+
+ // color the output
+ outputs[o].color = cur_color;
+ cur_amount -= want_amount;
+ }
+
+ return true;
+}
+
+function validate_color_bands(inputs, errors) {
+ // inputs of same color should be adjacent,
+ // uncolored coins should go after colored.
+
+ var seen_colors = {};
+ var got_uncolored = false;
+ var last_color = COLOR.UNKNOWN;
+
+ for (var i = 0; i < inputs.length; ++i) {
+ var c = inputs[i].color;
+ if (c == COLOR.MIXED || c == COLOR.DEFAULT || c == COLOR.UNKNOWN) {
+ got_uncolored = true;
+ } else {
+ if (got_uncolored) errors.uncolored_before_colored = true;
+ if (c != last_color) {
+ if (seen_colors[c])
+ errors.color_band_broken = true;
+ else
+ seen_colors[c] = true;
+ last_color = c;
+ }
+ }
+ }
+}
+
+function validate_conservation(inputs, outputs, errors) {
+ // check whether color conservation rule was violated.
+ // besides general violation we try to detect two
+ // particular cases:
+ // * coins got mixed
+ // * fee was paid with colored coins
+
+ var input_color_amounts = {};
+ var output_color_amounts = {};
+
+ function getz(a, i) {
+ var val = a[i];
+ return val ? val : 0;
+ }
+
+ function sum_colors(points, sum) {
+ var sum_uncolored = 0,
+ sum_colored = 0;
+ for (var i = 0; i < points.length; ++i) {
+ var color = points[i].color,
+ amount = points[i].amount;
+
+ sum[color] = amount + getz(sum, color);
+
+ if (uncolored(color))
+ sum_uncolored += amount;
+ else
+ sum_colored += amount;
+ }
+ return {
+ sum_uncolored: sum_uncolored,
+ sum_colored: sum_colored
+ };
+ }
+
+ var input = sum_colors(inputs, input_color_amounts);
+ var output = sum_colors(outputs, output_color_amounts);
+
+ for (var c in output_color_amounts) {
+ if (!uncolored(c)) {
+ if (getz(output_color_amounts, c) > getz(input_color_amounts, c))
+ errors.conservation_gain = true;
+ }
+ }
+
+ console.log(input_color_amounts);
+
+ for (var c in input_color_amounts) {
+ if (!uncolored(c)) {
+ if (getz(output_color_amounts, c) < getz(input_color_amounts, c)) {
+ console.log("loss: (" + c+ ") " + getz(output_color_amounts, c) + "<" + getz(input_color_amounts, c));
+ errors.conservation_loss = true;
+ }
+ }
+ }
+
+
+ var mixed_delta = getz(output_color_amounts, COLOR.MIXED) - getz(input_color_amounts, COLOR.MIXED);
+ var color_delta = output.sum_colored - input.sum_colored;
+
+ if (mixed_delta > 0)
+ errors.got_mixed = true;
+
+ if (mixed_delta + color_delta < 0)
+ errors.color_fee = true;
+}
+
+function validate_zero_valued(inputs, outputs, errors) {
+ // one zero-valued input should match one zero-valued input
+
+ var zv_inputs_count = {};
+ var zv_outputs_count = {};
+
+ function getz(a, i) {
+ var val = a[i];
+ return val ? val : 0;
+ }
+
+ function count_zv(points, count) {
+ for (var i = 0; i < points.length; ++i) {
+ if (points[i].amount == 0) {
+ count[points[i].color] = 1 + getz(count, points[i].color);
+ }
+ }
+ }
+
+ count_zv(inputs, zv_inputs_count);
+ count_zv(outputs, zv_outputs_count);
+
+ for (var c in zv_inputs_count) {
+ if (getz(zv_inputs_count, c) > getz(zv_outputs_count, c))
+ errors.zero_input_mismatch = true;
+ }
+
+ for (var c in zv_outputs_count) {
+ if (getz(zv_inputs_count, c) < getz(zv_outputs_count, c))
+ errors.zero_output_mismatch = true;
+ }
+}
+
+function compute_and_validate_colors(inputs, outputs) {
+ var errors = {
+ got_mixed: false,
+ color_fee: false,
+ zero_input_mismatch: false,
+ zero_output_mismatch: false,
+ uncolored_before_colored: false,
+ color_band_broken: false,
+ conservation_gain: false,
+ conservation_loss: false
+ };
+ if (compute_colors(inputs, outputs)) {
+ validate_color_bands(inputs, errors);
+ validate_conservation(inputs, outputs, errors);
+ validate_zero_valued(inputs, outputs, errors);
+ return errors;
+ } else return false;
+}
+
+function test_compute_colors() {
+ var inputs = [{color: 1, amount:1},
+ {color: 1, amount:2},
+ {color: 2, amount:1},
+ {color: 0, amount:3}];
+ var outputs = [{color: -2, amount: 3},
+ {color: -2, amount: 1},
+ {color: -2, amount: 2}];
+ var errors = compute_and_validate_colors(inputs, outputs);
+ console.log(outputs);
+ return errors;
+}
+test_compute_colors()
View
10 package.json
@@ -0,0 +1,10 @@
+{ "name": "bitcoin-explorer"
+, "version": "0.0.1"
+, "description": "A block explorer clone built using BitcoinJS"
+, "author": "Lio Leo Hakim <0@0i0.io> (http://www.0i0.io)"
+, "dependencies":
+ { "express": ">=2.4.4"
+ , "jsonrpc2": ">=0.1.0"
+ , "jade" :">=0.0.1"
+ }
+}
View
43 public/css/reset.css
@@ -0,0 +1,43 @@
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
View
30 public/css/style.css
@@ -0,0 +1,30 @@
+body {
+background: hsl(0, 0%, 20%);
+color: hsl(0, 0%, 80%);
+font-family: 'Menlo'
+;
+font-size: 12px;
+}
+a { color: #06e; }
+a:visited { color: #06e; }
+a:hover { color: #06e; }
+a:focus { outline: thin dotted; }
+
+#json{
+ padding: 30px;
+}
+li,ul {
+ list-style: none;
+}
+ul {
+ margin-left: 20px;
+}
+li,ul {
+ list-style: none;
+}
+.property {
+ color: hsl(130, 35%, 63%);
+}
+.sep{
+ padding: 0 10px;
+}
View
176 query.js
@@ -0,0 +1,176 @@
+var bitcoin = require('../bitcoin');
+var Util = require('../util');
+var Step = require('step');
+var Bignum = bitcoin.bignum;
+
+function getOutpoints(blockChain, txStore, txs, callback) {
+ if (!Array.isArray(txs)) {
+ txs = [txs];
+ }
+
+ Step(
+ function fetchTxInputsStep() {
+ var group = this.group();
+ txs.forEach(function (tx) {
+ if (tx.isCoinBase()) return;
+
+ var callback = group();
+ tx.cacheInputs(blockChain, txStore, false, function (err, cache) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ tx.ins.forEach(function (txin) {
+ var prevTx = cache.txIndex[txin.getOutpointHash().toString('base64')];
+
+ txin.source = prevTx[txin.getOutpointIndex()];
+ });
+ callback();
+ });
+ });
+ },
+ function calculateStep(err) {
+ if (err) throw err;
+
+ txs.forEach(function (tx, i) {
+ tx.totalIn = Bignum(0);
+ tx.totalOut = Bignum(0);
+ tx.ins.forEach(function (txin, j) {
+ if (txin.isCoinBase()) return;
+
+ tx.totalIn = tx.totalIn.add(Util.valueToBigInt(txin.source.v));
+ });
+ // TODO: Add block value to totalIn for coinbase
+ tx.outs.forEach(function (txout, j) {
+ tx.totalOut = tx.totalOut.add(Util.valueToBigInt(txout.v));
+ });
+
+ if (!tx.isCoinBase()) tx.fee = tx.totalIn.sub(tx.totalOut);
+ });
+ this(null, txs);
+ },
+ callback
+ );
+};
+
+function formatTx(tx) {
+ var standard = tx.getStandardizedObject();
+ var data =
+ { 'hash' : standard.hash
+ , 'totalIn': Util.formatValue(tx.totalIn)
+ , 'totalOut': Util.formatValue(tx.totalOut)
+ , 'fee': (tx.fee) ? Util.formatValue(tx.fee) : "0.0"
+ , 'in' : []
+ , 'out' : []
+ }
+
+ tx.ins.forEach(function (txin, j) {
+ if (txin.isCoinBase()) {
+ console.log(txin);
+ data.in.push(
+ { type : 'coinBase'
+ }
+ )
+ return;
+ }
+ if (txin.source) {
+ data.in.push(
+ { sourceAddr : Util.pubKeyHashToAddress(txin.source.getScript().simpleOutPubKeyHash())
+ , value : Util.formatValue(txin.source.v)
+ , prev_tx : standard.in[j].prev_out.hash
+ }
+ )
+ }
+ })
+
+ tx.outs.forEach(function (txout, j) {
+ data.out.push(
+ { toAddr : Util.pubKeyHashToAddress(txout.getScript().simpleOutPubKeyHash())
+ , value : Util.formatValue(txout.v)
+ }
+ )
+ })
+
+ return data;
+}
+
+exports.txquery = function (args, opt, callback) {
+ var hash64 = args[0];
+ var hash = new Buffer(hash64, 'base64');
+
+ var storage = this.node.getStorage();
+ var blockChain = this.node.getBlockChain();
+ var txStore = this.node.getTxStore();
+
+ var result = {};
+
+ Step(
+ function getMemTxStep() {
+ txStore.get(hash, this);
+ },
+ function getChainTxStep(err, tx) {
+ if (tx) {
+ // We already found the transaction, just store it
+ result.tx = tx;
+ // And then skip to fetchOutpointsStep
+ throw 'fetchop';
+ } else {
+ // The transaction isn't in the memory pool, check the database
+ storage.getTransactionByHash(hash, this);
+ }
+ },
+ function getBlockHashStep(err, tx) {
+ if (err) throw err;
+
+ if (tx) {
+ // We found the transaction, store it
+ result.tx = tx;
+ } else {
+ // We still couldn't find the transaction, return undefined
+ callback(null, undefined);
+ return;
+ }
+
+ // Get the containing block
+ storage.getContainingBlock(hash, this);
+ },
+ function getBlockStep(err, blockHash) {
+ if (err) throw err;
+
+ if (!blockHash) {
+ // TODO: Strange orphan (corrupt db)
+ callback(null, new Error('Block containing transaction not indexed, '
+ + 'db corrupt.'));
+ return;
+ }
+
+ // Get the containing block
+ storage.getBlockByHash(blockHash, this);
+ },
+ function storeBlockStep(err, block) {
+ if (err) throw err;
+
+ if (!block) {
+ // TODO: Strange orphan (corrupt db)
+ callback(null, new Error('Block containing transaction not found, '
+ + 'db corrupt.'));
+ return;
+ }
+ result.block = block.getStandardizedObject();
+ this(null);
+ },
+ function fetchOutpointsStep(err) {
+ if (err && err !== 'fetchop') throw err;
+ getOutpoints(blockChain, txStore, result.tx, this);
+ },
+ function returnResultStep(err) {
+ if (err) throw err;
+
+ result.tx = formatTx(result.tx);
+
+ this(null, result);
+ },
+ callback
+ );
+};
View
13 views/home.jade
@@ -0,0 +1,13 @@
+extends layout
+block title
+ title NMVC
+ meta(name="description",content="")
+
+block content
+ ul
+ li
+ a(href="/addColor") add Color
+ li
+ a(href="/json/tx/d10d7127366229b341a559ddf83b53b9ac6c1b2ca792cb2e81edaf7a595d763b") example json tx
+ li
+ a(href="/tx/bf2bd6aefa3263988ac27fc1dc5be0e646143363031dc0976f971a73f7a64931") example tx
View
17 views/layout.jade
@@ -0,0 +1,17 @@
+!!! 5
+html.no-js(lang='en')
+ head
+ meta(charset="utf-8")
+
+ block title
+
+ block styles
+ link(rel="stylesheet", href="/css/reset.css")
+ link(rel="stylesheet", href="/css/style.css")
+
+ block topScripts
+
+ body
+ block content
+
+ block bottomScripts
View
44 views/tx.jade
@@ -0,0 +1,44 @@
+extends layout
+
+block content
+ div#json
+ ul
+ - for(var key in tx) {
+ - if (key == "in" || key == "out") {} else {
+ li
+ span.property !{key}
+ span.sep :
+ span.type-string !{tx[key]}
+ - }
+ - }
+ li
+ span.property in
+ span.sep :
+ ul
+ - for(var i = 0,n = tx.in.length; i<n ; i++) {
+ - for(var key in tx.in[i]) {
+ li
+ span.property !{key}
+ span.sep :
+ - if (key == "prev_tx") {
+ span.type-string
+ a(href="/tx/"+tx.in[i][key]) !{tx.in[i][key]}
+ - }else{
+ span.type-string !{tx.in[i][key]}
+ - }
+ - }
+ - }
+ li
+ span.property out
+ span.sep :
+ ul
+ - for(var i = 0,n = tx.out.length; i<n ; i++) {
+ - for(var key in tx.out[i]) {
+ li
+ span.property !{key}
+ span.sep :
+ span.type-string !{tx.out[i][key]}
+ - }
+ - }
+
+

0 comments on commit 91b1745

Please sign in to comment.
Something went wrong with that request. Please try again.