├── .gitignore ├── Makefile ├── README.md ├── set.html ├── test ├── bitcore_utxo.js ├── pence.test.js ├── difficulty.test.js ├── set.test.js ├── bitcore_basic.test.js ├── bitcore_multisig.test.js └── bitcore_p2cm.test.js ├── calculator.html ├── package.json ├── set.js ├── alicebob.js ├── pence.js ├── difficulty.js └── spec.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | browser_*.js 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ./node_modules/.bin/mocha -t 10000 --reporter list 3 | 4 | .PHONY: test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Penny Bank - Bitcoin Microtransaction Smart Contracts 2 | ===================================================== 3 | 4 | See the [site](http://quartzjer.github.io/pennybank/) or go directly to the [draft spec](spec.md). 5 | -------------------------------------------------------------------------------- /set.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | pennies: 7 |
8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /test/bitcore_utxo.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var crypto = require('crypto'); 3 | var bitcore = require('bitcore'); 4 | var explorers = require('bitcore-explorers'); 5 | var insight = new explorers.Insight(bitcore.Networks.testnet); 6 | 7 | var address = "myvsPNW9SgpmcGhBrYYowc5Q2cVHKqmuBP"; 8 | 9 | describe('challenge', function(){ 10 | 11 | it('should have unspents', function(done){ 12 | insight.getUnspentUtxos(address, function(err, utxos) { 13 | if (err) { 14 | console.log("insight utxo err",err); 15 | process.exit(1); 16 | } 17 | expect(utxos.length).to.be.above(0); 18 | utxos.forEach(function(utxo){ 19 | console.log(address,utxo.toJSON()); 20 | }); 21 | done(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /calculator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | current difficulty: loading...
7 | BTC:
8 | pennies:
9 | 24 | 25 | -------------------------------------------------------------------------------- /test/pence.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var crypto = require('crypto'); 3 | var libpence = require('../pence.js'); 4 | 5 | describe('pence', function(){ 6 | 7 | it('should export an object', function(){ 8 | expect(libpence).to.be.an('object'); 9 | }); 10 | 11 | it('should export a function', function(){ 12 | expect(libpence.pence).to.be.a('function'); 13 | }); 14 | 15 | it('should generate a pence', function(){ 16 | var pows = libpence.pence(10); 17 | expect(pows).to.be.an('object'); 18 | expect(pows.p0.length).to.be.equal(5); 19 | expect(pows.pN.length).to.be.equal(5); 20 | expect(pows.digest.length).to.be.equal(32); 21 | expect(pows.ID.length).to.be.equal(20); 22 | expect(pows.N).to.be.equal(10); 23 | }); 24 | 25 | it('should make identical', function(){ 26 | var pence = libpence.pence(10); 27 | var pence2 = libpence.pence(10, pence.nonce, pence.p0); 28 | expect(pence.ID.toString('hex')).to.be.equal(pence2.ID.toString('hex')); 29 | }); 30 | 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /test/difficulty.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var crypto = require('crypto'); 3 | var libdifficulty = require('../difficulty.js'); 4 | 5 | describe('difficulty', function(){ 6 | 7 | it('should export an object', function(){ 8 | expect(libdifficulty).to.be.an('object'); 9 | }); 10 | 11 | it('should export a function', function(){ 12 | expect(libdifficulty.difficulty).to.be.a('function'); 13 | }); 14 | 15 | it('should generate a difficulty from fixed values', function(done){ 16 | libdifficulty.difficulty({getdifficulty:'4.944639068824144E10',bcperblock:'2500000000'}, function(err, hashes){ 17 | expect(err).to.not.exist(); 18 | expect(hashes.toString()).to.be.equal('8494954859132248544'); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should fetch current difficulty from blockexplorer api', function(done){ 24 | libdifficulty.difficulty({}, function(err, hashes){ 25 | expect(err).to.not.exist(); 26 | expect(hashes).to.exist(); 27 | done(); 28 | }); 29 | }); 30 | 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pennybank", 3 | "version": "0.0.1", 4 | "description": "Penny Bank - Distributed Bitcoin Microtransactions", 5 | "dependencies": { 6 | "bcoin": "^0.14.4", 7 | "bignum": "^0.9.2", 8 | "bitcore": "git://github.com/quartzjer/bitcore.git", 9 | "bitcore-explorers": "^0.10.4", 10 | "browserify-bignum": "^1.3.0-2", 11 | "minimist": "^1.1.1" 12 | }, 13 | "devDependencies": { 14 | "chai": "*", 15 | "mocha": "*" 16 | }, 17 | "browser": { 18 | "bignum": "browserify-bignum" 19 | }, 20 | "scripts": { 21 | "test": "make test", 22 | "calculator": "browserify difficulty.js > browser_difficulty.js", 23 | "set": "browserify set.js > browser_set.js" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/quartzjer/pennybank.git" 28 | }, 29 | "keywords": [ 30 | "bitcoin", 31 | "microtransaction", 32 | "penny bank" 33 | ], 34 | "author": "Jeremie Miller ", 35 | "license": "Public Domain", 36 | "bugs": { 37 | "url": "https://github.com/quartzjer/pennybank/issues" 38 | }, 39 | "homepage": "https://github.com/quartzjer/pennybank" 40 | } 41 | -------------------------------------------------------------------------------- /set.js: -------------------------------------------------------------------------------- 1 | // this creates/verifies sets of proof of work challenges 2 | 3 | var crypto = require('crypto'); 4 | var pence = require('./pence'); 5 | 6 | exports.generate = function(N) 7 | { 8 | var ret = {N:N}; 9 | ret.pence = {}; 10 | ret.secrets = {}; 11 | ret.nonce = crypto.randomBytes(24); 12 | 13 | for(var i = 0; i < 100; i++) 14 | { 15 | var p = pence.pence(N, ret.nonce); 16 | ret.secrets[p.ID.toString('hex')] = p; 17 | ret.pence[p.ID.toString('hex')] = p.pN.toString('hex'); 18 | } 19 | 20 | return ret; 21 | } 22 | 23 | exports.verify = function(set, secrets) 24 | { 25 | // generate a set from each secret and remove it by ID 26 | var p0; 27 | while(p0 = secrets.pop()) 28 | { 29 | var p = pence.pence(set.N, set.nonce, p0); 30 | var id = p.ID.toString('hex'); 31 | if(!set.pence[id]) return false; 32 | // the pN must also match 33 | if(set.pence[id].toString('hex') != p.pN.toString('hex')) return false; 34 | delete set.pence[id]; 35 | } 36 | 37 | // should only be one left un-verified 38 | if(Object.keys(set.pence).length != 1) return false; 39 | 40 | // all good 41 | return true; 42 | } 43 | 44 | // in browser 45 | if(typeof window !== "undefined") 46 | { 47 | window.libset = exports; 48 | } 49 | -------------------------------------------------------------------------------- /test/set.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var crypto = require('crypto'); 3 | var libset = require('../set.js'); 4 | 5 | describe('set', function(){ 6 | 7 | it('should export an object', function(){ 8 | expect(libset).to.be.an('object'); 9 | }); 10 | 11 | it('should export a function', function(){ 12 | expect(libset.generate).to.be.a('function'); 13 | }); 14 | 15 | it('should generate a set', function(){ 16 | var set = libset.generate(10); 17 | expect(set).to.be.an('object'); 18 | 19 | expect(set.pence).to.be.an('object'); 20 | expect(Object.keys(set.pence).length).to.be.equal(100); 21 | 22 | // check one pence 23 | var ID = Object.keys(set.pence)[0]; 24 | expect(ID).to.be.a('string'); 25 | expect(ID.length).to.be.equal(40); 26 | expect(set.pence[ID]).to.be.a('string'); 27 | expect(set.pence[ID].length).to.be.equal(10); 28 | }); 29 | 30 | it('should verify a set', function(){ 31 | var set = libset.generate(10); 32 | var ID = Object.keys(set.pence)[0]; 33 | delete set.secrets[ID]; 34 | var secrets = Object.keys(set.secrets).map(function(id){ return set.secrets[id].p0; }); 35 | expect(secrets.length).to.be.equal(99); 36 | expect(libset.verify(set, secrets)).to.be.equal(true); 37 | expect(Object.keys(set.pence).length).to.be.equal(1); 38 | }); 39 | 40 | 41 | 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /alicebob.js: -------------------------------------------------------------------------------- 1 | var libset = require('./set.js'); 2 | 3 | // example code showing flow of an Alice / Bob exchange 4 | 5 | var Alice = {who:'Alice'}; 6 | var Bob = {who:'Bob'}; 7 | 8 | // Alice generates a new set 9 | Alice.set = libset.generate(1000); 10 | 11 | // Alice uses the shareable parts in an open 12 | Alice.open = {pence:Alice.set.pence, N:Alice.set.N, nonce:Alice.set.nonce}; 13 | 14 | // Alice sends open to Bob 15 | 16 | // Bob generates a set and open response 17 | Bob.set = libset.generate(Alice.open.N); 18 | Bob.open = {pence:Bob.set.pence, N:Bob.set.N, nonce:Bob.set.nonce}; 19 | 20 | // Bob selects one pence randomly from Alice's open 21 | var random = Math.floor(Math.random()*100); 22 | Bob.Alice = {N:Alice.open.N, nonce:Alice.open.nonce}; 23 | Bob.Alice.ID = Object.keys(Alice.open.pence)[random]; 24 | Bob.Alice.pN = Alice.open.pence[Bob.Alice.ID]; 25 | Bob.open.ID = Bob.Alice.ID; // to send back to Alice 26 | 27 | // Bob sends the open response to Alice 28 | 29 | // Alice selects one pence randomly from Bob's open 30 | var random = Math.floor(Math.random()*100); 31 | Alice.Bob = {N:Bob.open.N, nonce:Bob.open.nonce}; 32 | Alice.Bob.ID = Object.keys(Bob.open.pence)[random]; 33 | Alice.Bob.pN = Bob.open.pence[Alice.Bob.ID]; 34 | 35 | // Alice generates confirmation for Bob 36 | Alice.secret = Alice.set.secrets[Bob.open.ID]; 37 | delete Alice.set.pence[Bob.open.ID]; 38 | Alice.opened = {ID:Alice.Bob.ID}; 39 | Alice.opened.secrets = Object.keys(Alice.set.pence).map(function(id){return Alice.set.secrets[id].p0;}); 40 | 41 | // Alice sends opened to Bob 42 | 43 | // Bob verifies secrets 44 | libset.verify(Bob.Alice, Alice.opened.secrets); 45 | 46 | // Bob creates final response 47 | Bob.secret = Bob.set.secrets[Alice.opened.ID]; 48 | delete Bob.set.pence[Alice.opened.ID]; 49 | Bob.opened = {}; 50 | Bob.opened.secrets = Object.keys(Bob.set.pence).map(function(id){return Bob.set.secrets[id].p0;}); 51 | 52 | // Bob sends opened to Alice 53 | 54 | // Alice verifies, fully negotiated mutual pence 55 | libset.verify(Alice.Bob, Bob.opened.secrets); 56 | -------------------------------------------------------------------------------- /pence.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var bignum = require('bignum'); 3 | var argv = require('minimist')(process.argv.slice(2)); 4 | var difficulty = require('./difficulty').difficulty; 5 | 6 | // generate a pence of the given size 7 | exports.pence = function(N, nonce, p0){ 8 | 9 | // start with a random nonce and p0 10 | if(!nonce) nonce = crypto.randomBytes(24); 11 | if(!p0) p0 = crypto.randomBytes(5); 12 | 13 | // track the pence digest 14 | var digest = crypto.createHash('sha256').update(p0).digest() 15 | 16 | // hash it forward for each N at this difficulty 17 | var pN = p0; 18 | var buf = new Buffer(32); 19 | nonce.copy(buf); // initialize w/ nonce 20 | var seq = new Buffer(4); 21 | for(var j = 1; j <= N; j++) 22 | { 23 | seq.writeUInt32BE(j,0); // get seq in bytes 24 | seq.copy(buf,24,1,4); // write in the new sequence 25 | pN.copy(buf,27); // copy in last penny 26 | var hash = crypto.createHash('sha256').update(buf).digest(); 27 | digest = crypto.createHash('sha256').update(digest).update(buf).digest(); 28 | pN = hash.slice(0,5); 29 | } 30 | 31 | var ret = {N:N, nonce:nonce, p0:p0, pN:pN, digest:digest}; 32 | ret.ID = crypto.createHash('rmd160').update(digest).digest(); 33 | // console.log('PENCE',ret) 34 | return ret; 35 | }; 36 | 37 | // handy debugging to run as command line 38 | if(process && process.argv[1] && process.argv[1].indexOf('pence.js') != -1) 39 | { 40 | var satoshi = bignum(argv.btc * (100*1000000)); 41 | if(!satoshi) return console.error('missing arg: --btc x'); 42 | if(typeof argv.debug != 'boolean') argv.debug = true; 43 | difficulty(false, function(err, hashes){ 44 | if(err) return console.error('errored',err); 45 | hashes = hashes.div(100*1000000); // hashes-per-satoshi 46 | var total = hashes.mul(satoshi); 47 | var count = total.div(bignum(2).pow(40)).toNumber(); 48 | var start = Date.now(); 49 | var pows = exports.pence(count); 50 | console.log('p0',pows.p0.toString('hex'),'pN',pows.pN.toString('hex'),'N',count,'in',Date.now()-start,'ms'); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/bitcore_basic.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var crypto = require('crypto'); 4 | var bitcore = require('bitcore'); 5 | 6 | var A_secret = crypto.randomBytes(16); 7 | var B_secret = crypto.randomBytes(16); 8 | 9 | console.log("secrets",A_secret.toString('hex'),B_secret.toString('hex')); 10 | 11 | function hash160(buf) 12 | { 13 | var sha256 = crypto.createHash('sha256').update(buf).digest(); 14 | return crypto.createHash('ripemd160').update(sha256).digest(); 15 | } 16 | 17 | var A_hash = hash160(A_secret); 18 | var B_hash = hash160(B_secret); 19 | 20 | var P2CM = bitcore.Script() 21 | .add('OP_HASH160') 22 | .add(A_hash) 23 | .add('OP_EQUALVERIFY') 24 | .add('OP_HASH160') 25 | .add(B_hash) 26 | .add('OP_EQUAL'); 27 | 28 | var P2CM_IN = bitcore.Script().add(B_secret).add(A_secret); 29 | var verified = bitcore.Script.Interpreter().verify(P2CM_IN, P2CM); 30 | console.log(P2CM_IN,P2CM,verified); 31 | 32 | // test multisig 33 | var privateKey1 = new bitcore.PrivateKey('KwF9LjRraetZuEjR8VqEq539z137LW5anYDUnVK11vM3mNMHTWb4'); 34 | var privateKey2 = new bitcore.PrivateKey('L4PqnaPTCkYhAqH3YQmefjxQP6zRcF4EJbdGqR8v6adtG9XSsadY'); 35 | var publicKey1 = privateKey1.publicKey; 36 | var publicKey2 = privateKey2.publicKey; 37 | var P2SHScript = new bitcore.Script.buildMultisigOut([publicKey1, publicKey2], 1); 38 | console.log(P2SHScript.toString()); 39 | var P2SHFund = P2SHScript.toScriptHashOut(); 40 | console.log(P2SHFund); 41 | var address = P2SHFund.toAddress(); 42 | console.log(address); 43 | 44 | 45 | // first we create a transaction 46 | var uxto = { 47 | address: address, 48 | txId: '66e64ef8a3b384164b78453fa8c8194de9a473ba14f89485a0e433699daec140', 49 | outputIndex: 0, 50 | script: P2SHFund, 51 | satoshis: 100000 52 | }; 53 | var tx = new bitcore.Transaction().from(uxto,[publicKey1, publicKey2],1).to(P2SHFund.toAddress(), 100000).sign(privateKey1); 54 | //console.log(tx.inputs[0].getSignatures(tx, privateKey1, 0)); 55 | var signature = tx.getSignatures(privateKey1)[0].signature.toBuffer(); 56 | console.log(signature); 57 | 58 | var P2SH_IN = bitcore.Script().add(signature); 59 | var verified = bitcore.Script.Interpreter().verify(P2SH_IN, P2SHScript); 60 | console.log(P2SH_IN,verified); 61 | 62 | -------------------------------------------------------------------------------- /difficulty.js: -------------------------------------------------------------------------------- 1 | var http = require('https'); 2 | var bignum = require('bignum'); 3 | var argv = require('minimist')(process.argv.slice(2),{string:'getdifficulty'}); 4 | 5 | // export a current difficulty fetch/calc 6 | exports.difficulty = function(args, cbDone) 7 | { 8 | if(args) argv = args; 9 | fetch('getdifficulty', function(err, data){ 10 | var difficulty = bignum(parseInt(JSON.parse(data))); // "4.944639068824144E10" 11 | var hashperblock = difficulty.mul(bignum(2).pow(48)).div(0xffff); // https://en.bitcoin.it/wiki/Difficulty#What_network_hash_rate_results_in_a_given_difficulty.3F 12 | fetch('bcperblock', function(err, data){ 13 | var btc = bignum(data).div(100000000); // 2500000000 right now 14 | if(argv.debug) console.log('node difficulty.js --getdifficulty',difficulty.toString(),'--bcperblock',data); 15 | var hashes = hashperblock.div(btc); 16 | if(argv.debug) console.log('hashes per btc:',hashes.toString()); 17 | cbDone(err, hashes, difficulty); 18 | }); 19 | }); 20 | } 21 | 22 | // handy debugging to run as command line 23 | if(process && process.argv[1] && process.argv[1].indexOf('difficulty.js') != -1) 24 | { 25 | if(typeof argv.debug != 'boolean') argv.debug = true; 26 | exports.difficulty(false, function(err, hashes){ 27 | if(err) return console.log('errored',err); 28 | console.log('current hashes required per bitcoin:',hashes); 29 | console.log('current hashes required per satoshi:',hashes.div(100000000)); 30 | }); 31 | } 32 | 33 | // in browser 34 | if(typeof window !== "undefined") 35 | { 36 | window.difficulty = exports.difficulty; 37 | window.bignum = bignum; 38 | } 39 | 40 | function fetch(what, cbDone) 41 | { 42 | // short-cut for local/offline testing 43 | if(argv[what]) return cbDone(null, argv[what]); 44 | 45 | // fetch current value 46 | if(argv.debug) console.log('fetching current',what); 47 | http.get({ 48 | protocol: 'https:', 49 | host: 'blockchain.info', 50 | path: '/q/'+what+'?cors=true', 51 | withCredentials: false 52 | }, function(resp) { 53 | var body = ''; 54 | resp.on('data', function(d) { 55 | body += d; 56 | }); 57 | resp.on('end', function() { 58 | cbDone(null, body); 59 | }); 60 | }); 61 | 62 | } 63 | -------------------------------------------------------------------------------- /test/bitcore_multisig.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var crypto = require('crypto'); 3 | var bitcore = require('bitcore'); 4 | var explorers = require('bitcore-explorers'); 5 | var insight = new explorers.Insight(bitcore.Networks.testnet); 6 | 7 | // testnet 8 | var privateKey = bitcore.PrivateKey({ 9 | "bn":"8c026a359a13f707a3497ef58da45b628958ff98b5f33322cf29ede12fcfd56f", 10 | "compressed":true, 11 | "network":"testnet" 12 | }); 13 | var address = privateKey.toAddress(); 14 | 15 | describe('challenge', function(){ 16 | 17 | it('should have an address', function(){ 18 | expect(address.toString()).to.be.equal('myvsPNW9SgpmcGhBrYYowc5Q2cVHKqmuBP'); 19 | }); 20 | 21 | it('should perform a testnet multisig transaction and refund', function(done){ 22 | getUTXO(address, function(utxo){ 23 | console.log(utxo.toJSON()); 24 | 25 | // test sending a small bit to a p2sh multisig 26 | var privateKey1 = new bitcore.PrivateKey('612b3ca3f368cf2658c2e1777d2fa28e6bcde8ea19312cbf69e09e7333e13994',bitcore.Networks.testnet); 27 | var privateKey2 = new bitcore.PrivateKey('d65788b9947b41625ffff946bc145187c6b85d1686e60becdf34567f17478730',bitcore.Networks.testnet); 28 | var publicKey1 = privateKey1.publicKey; 29 | var publicKey2 = privateKey2.publicKey; 30 | var P2SHScript = new bitcore.Script.buildMultisigOut([publicKey1, publicKey2], 1); 31 | var P2SHFund = P2SHScript.toScriptHashOut(); 32 | 33 | var tx = new bitcore.Transaction() 34 | .from(utxo) 35 | .to(P2SHFund.toAddress(), 9000) 36 | .change(address) 37 | .sign(privateKey); 38 | 39 | console.log("tx",tx); 40 | 41 | broadcast(tx, function(id){ 42 | console.log("funded to",id); 43 | 44 | var tx2 = new bitcore.Transaction() 45 | .from({txId:id, outputIndex:0, inputIndex:0, satoshis:9000, script:P2SHFund.toString()}, [publicKey1, publicKey2], 1) 46 | .to(address, 8000) 47 | .sign(privateKey2); 48 | 49 | console.log("tx2",tx2.serialize()); 50 | broadcast(tx2, function(id2){ 51 | console.log("funded back to",id2); 52 | expect(id2).to.be.a("string"); 53 | done() 54 | }); 55 | 56 | }); 57 | 58 | }); 59 | }) 60 | }) 61 | 62 | 63 | 64 | 65 | function broadcast(tx, done) 66 | { 67 | insight.broadcast(tx, function(err, id) { 68 | if (err) { 69 | console.log("insight broadcast err",err); 70 | process.exit(1); 71 | } 72 | done(id); 73 | }); 74 | } 75 | 76 | function getUTXO(address, done) 77 | { 78 | insight.getUnspentUtxos(address, function(err, utxos) { 79 | if (err) { 80 | console.log("insight utxo err",err); 81 | process.exit(1); 82 | } 83 | // utxos.forEach(function(utxo){console.log(utxo.toJSON());}); 84 | done(utxos[0]); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /test/bitcore_p2cm.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var crypto = require('crypto'); 4 | var bitcore = require('bitcore'); 5 | var Sighash = require('../node_modules/bitcore/lib/transaction/sighash'); // temporary workaround 6 | var explorers = require('bitcore-explorers'); 7 | var insight = new explorers.Insight(bitcore.Networks.testnet); 8 | 9 | // example successful testnet p2cm http://explorer.chain.com/transactions/22ac9b05f765ac5c7e830b826718f1c43a686f7d398c00bc84bf1c79d51d4499 10 | 11 | // testnet 12 | var privateKey = bitcore.PrivateKey({ 13 | "bn":"8c026a359a13f707a3497ef58da45b628958ff98b5f33322cf29ede12fcfd56f", 14 | "compressed":true, 15 | "network":"testnet" 16 | }); 17 | var address = privateKey.toAddress(); 18 | console.log("address",address); 19 | 20 | describe('challenge', function(){ 21 | 22 | it('should p2cm', function(done){ 23 | getUTXO(address, function(utxo){ 24 | console.log(utxo.toJSON()); 25 | 26 | // test sending a small bit to a p2cm 27 | var privateKey1 = new bitcore.PrivateKey('612b3ca3f368cf2658c2e1777d2fa28e6bcde8ea19312cbf69e09e7333e13994',bitcore.Networks.testnet); 28 | var privateKey2 = new bitcore.PrivateKey('d65788b9947b41625ffff946bc145187c6b85d1686e60becdf34567f17478730',bitcore.Networks.testnet); 29 | var publicKey1 = privateKey1.publicKey; 30 | var publicKey2 = privateKey2.publicKey; 31 | 32 | var P2CMScript = new bitcore.Script.buildMultisigOut([publicKey1, publicKey2], 1); 33 | 34 | // now do the secrets/hashing part 35 | var A_secret = new Buffer("a236d85656fb05bf157d5328f191c7e6","hex"); 36 | var B_secret = new Buffer("65b3e07dbb2861cad500906e1af5c2c6","hex"); 37 | 38 | // prepend (in reverse order) 39 | P2CMScript 40 | .prepend('OP_EQUALVERIFY') 41 | .prepend(hash160(B_secret)) 42 | .prepend('OP_HASH160') 43 | .prepend('OP_EQUALVERIFY') 44 | .prepend(hash160(A_secret)) 45 | .prepend('OP_HASH160') 46 | 47 | console.log(P2CMScript.toString()); 48 | 49 | var P2SHFund = P2CMScript.toScriptHashOut(); 50 | 51 | var tx = new bitcore.Transaction() 52 | .from(utxo) 53 | .to(P2SHFund.toAddress(), 20000) 54 | .change(address) 55 | .fee(10000) 56 | .sign(privateKey); 57 | 58 | console.log("tx",tx); 59 | 60 | broadcast(tx, function(id){ 61 | console.log("funded to",id); 62 | 63 | var tx2 = new bitcore.Transaction() 64 | .from({txId:id, outputIndex:0, inputIndex:0, satoshis:20000, script:P2SHFund.toString()}, [publicKey1, publicKey2], 1, P2CMScript) 65 | .to(address, 10000) 66 | .fee(10000) 67 | .sign(privateKey1); 68 | 69 | var s = tx2.inputs[0].script; 70 | console.log('\ntx2 input script',s); 71 | var data = s.chunks.pop(); // remove the last item, the p2sh input data 72 | s.add(B_secret); // add secret data to match the p2cm script 73 | s.add(A_secret); 74 | s.add(data); // put the p2sh input data back at the end 75 | 76 | /* 77 | // work around hard-wired multisig to get the signature (TODO make a real input class for P2CM) 78 | var signature = Sighash.sign(tx2, privateKey1, 1, 0, P2CMScript).toBuffer(); 79 | console.log("tx2 signed",signature.toString("hex")); 80 | 81 | // create a scriptsig with the valid secrets to redeem 82 | var s = new bitcore.Script(); 83 | s.add('OP_0'); 84 | s.add(Buffer.concat([signature,new Buffer("00","hex")]); 85 | s.add(B_secret); 86 | s.add(A_secret); 87 | s.add(P2CMScript.toBuffer()); 88 | console.log("INPUT",s.toString()); 89 | */ 90 | // replace w/ our updated script 91 | tx2.inputs[0].setScript(s); 92 | 93 | console.log("\ntx2 json",tx2.toJSON()); 94 | console.log("\ntx2 raw hex",tx2.serialize()) 95 | broadcast(tx2, function(id2){ 96 | console.log("funded back to",id2); 97 | expect(id2).to.exist(); 98 | done() 99 | }); 100 | 101 | }); 102 | 103 | }); 104 | }); 105 | }); 106 | 107 | 108 | 109 | function broadcast(tx, done) 110 | { 111 | insight.broadcast(tx, function(err, id) { 112 | expect(err).to.not.exist(); 113 | done(id); 114 | }); 115 | } 116 | 117 | function getUTXO(address, done) 118 | { 119 | insight.getUnspentUtxos(address, function(err, utxos) { 120 | expect(err).to.not.exist(); 121 | var max = utxos[0]; 122 | utxos.forEach(function(utxo){ 123 | if(utxo.satoshis > max.satoshis) max = utxo; 124 | // console.log(utxo.toJSON()); 125 | }); 126 | done(max); 127 | }); 128 | } 129 | 130 | function hash160(buf) 131 | { 132 | var sha256 = crypto.createHash('sha256').update(buf).digest(); 133 | return crypto.createHash('ripemd160').update(sha256).digest(); 134 | } 135 | 136 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Microtransaction Smart Contracts 2 | 3 | > This is a work-in-progress, once test implementations are interoperating this document will be reorganized and cleaned up, feedback is still encouraged even in this state 4 | 5 | ## Abstract 6 | 7 | The architecture of the bitcoin blockchain requires [fees on every transaction](https://en.bitcoin.it/wiki/Transaction_fees) in order to reward the network for storing the ledger, small microtransactions are simply [not economically valuable](http://www.coindesk.com/new-study-low-bitcoin-transaction-fees-unsustainable/) enough to maintain in a distributed blockchain. 8 | 9 | This outlines a simple technique to create a smart contract that puts a larger bitcoin value in mutual escrow between two parties, such that miniscule amounts of that escrow value can be transacted without requiring additional trust, timelocks, oracles, or other third parties, and while minimizing the potential for fees and "dust" transactions. It shows how to create a temporary side-ledger to use for exchanging the microtransactions based on the same proof-of-work mining value of the bitcoin blockchain. 10 | 11 | ## Motivation 12 | 13 | With the rules for accepted P2SH opcodes relaxing [in 0.10](https://github.com/bitcoin/bitcoin/blob/v0.10.0/doc/release-notes.md#standard-script-rules-relaxed-for-p2sh-addresses), new types of scripts can be used in transactions and will accepted into the blockchain by updated miners. While many opcodes are still [disabled](https://en.bitcoin.it/wiki/Script#Words) to minimize the risk of a hard fork, only a common [OP_HASH160](https://en.bitcoin.it/wiki/Script#Crypto) is required to enable proof-of-work based microtransaction smart contracts. 14 | 15 | The existing [micropayment channels](https://en.bitcoin.it/wiki/Contracts#Example_7:_Rapidly-adjusted_.28micro.29payments_to_a_pre-determined_party) technique demonstrates how to modify a private transaction but requires a trust model based on timelocks and access to update signatures per value exchange, which is not ideal in many microtransaction situations. The proposed [zero-knowledge contingent payment](https://en.bitcoin.it/wiki/Zero_Knowledge_Contingent_Payment) is also a good foundation, but instead of an external protocol the contingency function is included here as part of the transaction itself. 16 | 17 | There is also some similarities to the [sidechains paper](http://www.blockstream.com/sidechains.pdf) in that this proposal has the properties of trustlessness (not relying on external parties) and uses lists of hashes to verify proof-of-work, but the scope is limited to acting as a simple transient side-ledger versus a two-way pegged full sidechain. 18 | 19 | ## Model 20 | 21 | A "penny bank" is a mechanism for placing some amount of bitcoin on hold between two parties without involving another third party, such that those two parties can then exchange smaller amounts of value over time independently. This requires that one or both parties be willing to source that amount of value and have it locked in an escrow between them, so that only through cooperation can it be unlocked again. 22 | 23 | The penny bank creation process negotiates a simple escrow where the funds are guaranteed to be available to the two parties, but only they can mutually agree to release any funds. If either party stops cooperating or misbehaves, the funds at that point remain frozen until cooperation begins again or the remaining proof of work is performed. 24 | 25 | In many common microtransaction scenarios there is some prior trust or reputation with one of the parties (such as service providers) where having some funds locked in an escrow with them is not very risky. When there is limited or no trust then the locked value should be small to reduce the risk, the only side-effect being a larger percentage of fees on the transaction to fund it. 26 | 27 | This proposal also only currently focuses on the core locking mechanism and exchanges, it is possible to add timelocks and create more complex transactions that further reduce the risk of funds remaining locked. 28 | 29 | # Specification 30 | 31 | In order to perform micro-transactions two parties must first establish that a larger value is guaranteed to be available to fund the smaller exchanges with a verifiable proof-of-work. This larger transaction is private to both parties while transacting and acts as the "bank", it is only ever broadcast to the network at the end or whenever either party is finished. The individual micro-transactions are always private and not broadcast, they are instead accounted for between the two parties as reducing the proof-of-work referenced in the bank transaction. 32 | 33 | 34 | ## Pay to Script Hash Conditional Multisig (P2CM) 35 | 36 | > A *Conditional Multisig* script is only accepted as a [P2SH](https://en.bitcoin.it/wiki/Pay_to_script_hash) in version [0.10 or later](https://github.com/bitcoin/bitcoin/blob/v0.10.0/doc/release-notes.md#standard-script-rules-relaxed-for-p2sh-addresses). 37 | 38 | This template allows any two or more parties to create a multisig transaction that additionally requires some secret data from each party to be processed. 39 | 40 | The conditional multisig script template used here is: 41 | ``` 42 | OP_HASH160 OP_EQUALVERIFY OP_HASH160 OP_EQUALVERIFY 43 | ``` 44 | 45 | A valid scriptSig requires three data pushes, one for each of the two `OP_HASH160` as the source data (the secrets) to generate a match for the given hash, and one signature from A or B to ensure nobody else can claim the value with the secret data alone. 46 | 47 | ## Penny Bank (PB) 48 | 49 | A Penny Bank (abbreviated `PB`) is the shared state between two parties that have agreed to exchange microtransactions pinned to the blockchain through a single larger transaction. The microtransaction value is exchanged by sending `pennies` back and forth which are verified as being part of a `pence` from each party that is negotiated during setup. The hashes of both `pence` are incorporated into a `P2CM` to guarantee funds are available for the `pennies`. 50 | 51 | ### Penny 52 | 53 | The `PB` contains many small proof-of-work challenges, each one is called a `penny` and is private to one party until revealed to and verified by the other party. 54 | 55 | A `penny` is exchanged as an 8 byte value: 3 bytes of a sequence number in big endian followed by a 5-byte secret. 56 | 57 | 58 | ### Pence 59 | 60 | A `pence` is defined as a 24 byte random `nonce`, an initial penny called `p0`, and a total number of pennies called `N`. The individual pennies are derived performing a SHA-256 digest of the 24 byte random nonce combined with the previous penny: `p2 = 0x000002 + sha256(nonce + p1).slice(0,5)`. Each sequentially increasing penny's 5 byte secret is the first 5 bytes of the previous one's digest output. 61 | 62 | Given any penny all higher sequences can be immediately calculated, but lower ones can only be derived through brute force hashing each preceeding 5 byte secret. 63 | 64 | The `N` number of pennies in a `pence` must represent a [difficulty](#value) *equal to or greater than the total `PB` bitcoin value*, it must require at least as many hashes to do these proofs as it would be to mine new bitcoin of that value. 65 | 66 | ### Pence ID 67 | 68 | Every `pence` has a unique public/visible ID that is the 20 byte RIPEMD-160 of a `pence digest`, which is calculated when generating the pennies. The `pence digest` is a roll-up hash of each penny starting with `p0`: `sha256(sha256(sha256(sha256(p0),p1),p2),pN)`. 69 | 70 | This digest can only be calculated by obtaining or deriving the source `p0` value which can then be immediately verified against the 20 byte ID. This is the private source data from each party that locks the `P2CM` to the total proof of work defined by each `pence`. 71 | 72 | ### Opening 73 | 74 | A private [2-of-2 multisig](https://bitcoin.org/en/developer-guide#multisig) input `PB` transaction is created that sends the main balance available to the `P2CM` (as a P2SH) output, and includes a `P2PKH` for each of the parties to carry forward the balances not being used for or already exchanged in microtransactions. 75 | 76 | Similar to [micropayment channels](https://en.bitcoin.it/wiki/Contracts#Example_7:_Rapidly-adjusted_.28micro.29payments_to_a_pre-determined_party), this primary transaction is kept private between the two parties and only used as a last resort if either party misbehaves. The un-broadcast transaction can also be updated and "re-balanced" over time as value is exchanged, adjusting the amounts of the outputs and generating new signatures. 77 | 78 | In order to guarantee a `PB` is funded without being broadcast, a `P2SH` specifying it as the output is broadcast and validated before exchanging any microtransactions. 79 | 80 | ### Closing 81 | 82 | When either party wants to settle and close the `PB`, the balances are updated and the `P2CM` is removed so that just normal outputs remain. 83 | 84 | As a last resort, either party may broadcast the last signed transaction which will freeze the `PB` at that point and the value remaining sent to the `P2CM` will be locked until either party either calculates the remaining pennies or they begin cooperating again. 85 | 86 | 87 | ### Penny Value (difficulty based) 88 | 89 | The value of every bitcoin is backed by the current [difficulty](https://en.bitcoin.it/wiki/Difficulty), which reduces to a number of hashes-per-satoshi ([example formula](http://bitcoin.stackexchange.com/questions/12013/how-many-hashes-create-one-bitcoin/12030#12030). 90 | 91 | Currently, the difficulty of [40007470271.271](https://bitcoinwisdom.com/bitcoin/difficulty) is based on the rate of 270,591,326 GH/s, which results in approximately [65 GH](https://www.google.com/#q=((270%2C591%2C326+*+60+*+10)+%2F+25)+%2F+100%2C000%2C000) to back the value of one satoshi. 92 | 93 | A single penny locks the first 5 bytes of a digest, requiring up to 2^40 hashes (about 1,100 GH) to derive. One penny would currently represent just under [17 satoshis](http://www.wolframalpha.com/input/?i=%282%5E40%29%2F%28%28%28270%2C591%2C326%2C000%2C000%2C000+*+60+*+10%29+%2F+25%29+%2F+100%2C000%2C000%29) of work. The difficulty slowly increases as computing power increases, so the hashes-per-satoshi will also go up. Since the hashes-per-penny is currently fixed at the 5 byte size, the number of satoshis per penny will conversely go down over time. 94 | 95 | The maximum sequence of a `pence` is 2^24 (about 17M), so the highest value of a single `PB` is currently [284M satoshi](http://www.wolframalpha.com/input/?i=%28%282%5E40%29%2F%28%28%28270%2C591%2C326%2C000%2C000%2C000+*+60+*+10%29+%2F+25%29+%2F+100%2C000%2C000%29+*+%282%5E24%29%29), or about 2.8 BTC. 96 | 97 | ## Two-Party Penny Banks 98 | 99 | > documentation here is a higher level work in progress, detailed transaction examples forthcoming 100 | 101 | When Alice wants to perform microtransactions with Bob, they begin by creating a larger set of 100 `pence` to offer for negotiating a `PB` with the `nonce` and `N` number of pennies in each set being identical, but the `p0` being unique to each. 102 | 103 | An example set: 104 | ```json 105 | { 106 | "N":1234, 107 | "nonce":"736711cf55ff95fa967aa980855a0ee9f7af47d6287374a8", 108 | "pence":{ 109 | "76a914c9f826620292b696af47ebd2013418e4e6ab6f9288ac":"85548d3df0", 110 | ... 111 | } 112 | } 113 | ``` 114 | 115 | Each `pence` has a key that is the hex of its ID and the value is the `pN` to validate pennies. 116 | 117 | Bob then selects one of the pence and challenges Alice to reveal the `p0` of all of the others in order to validate that they are all sized and calculated correctly (a partial/confidence-based [zero-knowledge proof](http://en.wikipedia.org/wiki/Zero-knowledge_proof)). Once Bob has validated a set and selected a single `pence` from Alice they perform the same process in reverse to have Alice choose/validate a `pence` from Bob as well. 118 | 119 | At this point both Alice and Bob have enough knowledge to use the sequences of small proof-of-works that verifiably add up to a larger bitcoin value and can create a `P2CM` transaction. The required conditional multisig script is generated using both of the ripemd160 digests of the selected `pence`, one from Alice and one from Bob. 120 | 121 | Once both Alice and Bob exchange their signatures of the agreed upon `PB` transaction, then Alice creates and broadcasts a normal `P2SH` to fund it which Bob can validate like any normal bitcoin transaction. The value is then locked and inaccessible to either without cooperation or work. 122 | 123 | As Alice and Bob exchange the actual small asset/values in a microtransaction they also exchange the pennies to represent that value of satoshis as a sequence difference from the last one. A penny at any lower point in the pence can be sent to unlock the difference in value from the previous one. 124 | 125 | If either party misbehaves or stops providing value, the other has a valid transaction to broadcast to permanently freeze the exchange at that point. If the `pence` data is stored by both then at any point in the future the two parties may begin cooperating again by exchanging them and using the frozen `P2CM` as the input. Either side may also decide at some point in the future to perform the remaining hashing work to derive the correct hashes and claim the `P2CM` value themselves. 126 | 127 | Summary steps: 128 | 129 | * Alice->Bob offer a set 130 | * Bob->Alice choose and verify a pence from the set and offer a set in return 131 | * Alice->Bob choose and verify a pence, create and sign a PB and send to Bob 132 | * Bob->Alice return signed PB 133 | * Alice broadcasts funding of PB 134 | * Bob verifies funding, value is now locked in the PB 135 | * either can broadcast it and freeze it at that point locking the balances in place 136 | * when finished, the PB is rebalanced with normal outputs, signed, and broadcast 137 | 138 | 139 | ## Multi-Party Penny Bankers 140 | 141 | > TODO: work in progress, this is how microtransactions can scale to larger use-cases 142 | 143 | Anyone can create a pair of Penny Banks with one or more well-known public "Penny Bankers", one for credits and one for debits. These `PBs` can then be used as a method to perform small microtransactions with any third party without requiring a `PB` for each third party, minimizing the risk and amount of bitcoin locked in any `PB`. The "Banker" will manage the pair of private PBs and also be available to any third party to clear microtransactions with them or their banker. 144 | 145 | When initiating an exchange with a third party, the sender must share the identity of the Banker along with the current debit `pence` to act as the "account" so that the third party can validate that it is valid and currently funded. 146 | 147 | The recipient must also create/have a `PB` with either the same Banker or with a Banker that will clear values with the sender's. Each `penny` can then be validated immediately and locally and should then be exchanged with their Banker into their private credit `PB`. Exchanging these offline is possible but increases the risk of any individual `penny` becoming invalid since the time delay between receiving and clearing is a window for the sender to double-spend them. 148 | 149 | Using multiple Bankers who independently clear with each other helps minimize the visibility of the actual parties performing the microtransactions. 150 | 151 | A `penny` can be represented as a globally unique 28-byte value when prepended with the `pence` ID (ripmemd160 digest). 152 | --------------------------------------------------------------------------------