UNPKG

bitcoin-nanopayment

Version:
613 lines (510 loc) 22 kB
// Copyright 2013 Paul Kernfeld // This file is part of bitcoin-nanopayment. // bitcoin-nanopayment is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // bitcoin-nanopayment is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with bitcoin-nanopayment. If not, see <http://www.gnu.org/licenses/>. 'use strict'; // Internal imports var BigInteger = require('./jsbn-patched').BigInteger; var base58 = require('./base58'); var script = require('./script'); var util = require('./util'); // External imports var assert = require('assert'); var async = require('async'); var bitcoin = require('bitcoin'); var log = require('loglevel'); var sprintf = require("sprintf-js").sprintf; // Satoshis per Bitcoin. All math is done in Satoshis to avoid rounding errors. var SATOSHIS = 100000000.; // Hard-code the amount to pay for now (0.01 Bitcoins) var AMOUNT = 1000000; // 10 seconds should be plenty of time to round-trip a voucher request var DEFAULT_TIMEOUT = 10000; // Use the TX fee for a <1000 byte transaction. TODO actually calculate the right number var TX_FEE = 10000 * 2; // This is the amount you need in your account var AMOUNT_WITH_FEE = AMOUNT + TX_FEE; exports.AMOUNT_WITH_FEE = AMOUNT_WITH_FEE; // The version byte for testnet public addresses var TESTNET_PUBLIC_VERSION = 111; // How many possible txid's are there? 32 bytes = 16 ^ 64 var TXID_RANGE_BI = new BigInteger(); TXID_RANGE_BI.fromString('10000000000000000000000000000000000000000000000000000000000000000', 16); // Turn the target into a serialized, URL-safe string // // TODO: this should just take a JSON object, jeez var serializeTarget = exports.serializeTarget = function(amount, length, vout, start) { return amount + '-' + length + '-' + vout + '-' + start; }; // Convert this serialized target into a JS object representing the target var deserializeTarget = exports.deserializeTarget = function(target) { assert(typeof target === 'string', 'target must be a string'); var split = target.split('-'); assert( split.length === 4, 'target has ' + split.length + ' values, should have 4' ); return { amount: parseInt(split[0]), length: parseInt(split[1]), vout: parseInt(split[2]), start: split[3] }; }; // This creates a client object that accesses bitcoind/Bitcoin-Qt over RPC to // execute nanopayments. clientConfig should look like: // // { // timeout: integer (optional, milliseconds), // minConf: integer (optional), // bitcoind: { // host: string, // port: integer, // user: string, // pass: string // } // } exports.Client = function(clientConfig) { var bitcoindConfig = clientConfig.bitcoind; assert(bitcoindConfig, 'The bitcoindConfig must exist'); // This object encapsulates logic to request, send, and cash in vouchers. // Currently, it uses only one Bitcoin address. This does *not* relate to // bitcoind/Bitcoin-Qt's built-in account functionality. var Account = function(address, privateKey) { this.address = address; // Set default values var timeout = clientConfig.timeout; var minConf = clientConfig.minConf; timeout = typeof timeout === 'undefined' ? DEFAULT_TIMEOUT : timeout; minConf = typeof minConf === 'undefined' ? 0 : minConf; // Verify argument types assert(util.isAddress(address), 'address must be a valid Bitcoin address'); assert( util.isPrivateKey(privateKey), 'privatekey must be a valid private key' ); assert(typeof timeout == 'number', 'timeout must be a number'); assert(util.isInt(minConf), 'minConf must be an integer'); // Convert the address into hex representation var addressBinary = base58.decodeChecked(address); assert.equal(addressBinary.version, TESTNET_PUBLIC_VERSION); var addressHex = addressBinary.payload.toString('hex'); var client = new bitcoin.Client(bitcoindConfig); // The target allows us to validate and cash incoming vouchers. It contains // information about the range of txid's that may be guessed, and the hex // transaction we'll submit if we cash an incoming voucher. // // { // start: BigInteger, // length: integer, // offset: integer, // transactionHex: hex string // } // // If this is null, no incoming voucher is valid var currentTarget = null; // This queue is basically just a lock, so it's always called with a // concurrency of 1. The purpose is to prevent us from doing two operations // at the same time, e.g. double spending. var queue; queue = async.queue( function (task, cb) { task(cb); }, 1 ); // // This must be finished running before using this account can be used. // This function initializes the account by looking up the previous unspent // transactions into this address. These are necessary because we're creating // raw transactions, for which we need txid's // of previous transactions. // // TODO: verify that the private key matches the public key // // // When this finishes, it calls cb(err, previous). previous is: // { // balance: [int, in Satoshis], // txid: [hex string], // vout: [integer] // } // var getPrevious = function(cb) { var balance = null; var previousTxid; var previousVout; log.debug(sprintf("Getting previous tx for %s", address)); // Retrieve the txid of the most recent tx into my account client.listUnspent( minConf, 999999, [address], function(err, transactions) { if(err) { cb(err); return; } log.debug(sprintf("Unspent outputs for %s: %j", address, transactions)); // Select the largest unspent output. // // TODO: Add the ability to combine multiple outputs. for (var i in transactions) { var transactionAmount = Math.round(transactions[i].amount * SATOSHIS); if (balance === null || transactionAmount > balance) { previousTxid = transactions[i].txid; previousVout = transactions[i].vout; balance = transactionAmount; } } if (balance === null) { cb(sprintf('No unspent transactions found in %s. Maybe there is an unconfirmed transaction to this ' + 'address?', address)); } else if(balance < AMOUNT_WITH_FEE) { cb(sprintf("You don't have enough Bitcoins in %s. You need at least %i Satoshis to use this, but you " + "only have %i.", address, AMOUNT_WITH_FEE, balance)); } else { log.debug(sprintf("Previous txid/vout for %s was %s/%i", address, previousTxid, previousVout)); cb(null, { balance: balance, txid: previousTxid, vout: previousVout }); } } ); }; // // Produce a voucher request which specifies the range of acceptable txid's // to pay to. // // - inverseProbability - int, the probability of the voucher being cashable, // to the -1 power // - fromAddress - the Base58-encoded address from which to request voucher // // After calling this, we wait for a period of time before doing any more // voucher operations because doing other operations may invalidate the // target. After the period of time has expired, we unlock the queue so that // we can continue processing other operations. // // On failure, call cb(error) // // On success, call cb(null, target) where target is a serialized target // // The serialized target can be sent to the payer. // this.requestVoucher = function(inverseProbability, fromAddress, cb) { assert( util.isInt(inverseProbability), 'inverseProbability must be an integer' ); assert(inverseProbability >= 1, 'inverseProbability must be >= 1'); assert(util.isAddress(fromAddress), 'fromAddress must be a string'); // This is a separate fn so that we can push it onto the queue var requestVoucher = function(queueCb) { async.waterfall([ // Get the previous transaction info getPrevious, // Create the transaction function(previous, waterfallCb) { log.debug(sprintf("Creating request transaction for %s", address)); // We pay the amount for the transaction to the counterparty and send // the change to ourselves. This is backwards, yes, but we need this // to generate our secret txid. Don't worry, we're not actually // submitting this to the Bitcoin network yet. var outputs = {}; outputs[fromAddress] = AMOUNT / SATOSHIS; // We need to send change to ourself iff there's money left over // // TODO: pull this functionality out if (previous.balance > AMOUNT_WITH_FEE) { outputs[address] = (previous.balance - AMOUNT_WITH_FEE) / SATOSHIS; assert.equal( Math.round((outputs[fromAddress] + outputs[address]) * SATOSHIS + TX_FEE), previous.balance, 'Balance rounding error!?' ); } log.debug(sprintf("Outputs: %j", outputs)); client.cmd( 'createrawtransaction', [{'txid': previous.txid, 'vout': previous.vout}], outputs, waterfallCb ); }, // Sign the transaction function(transactionHex, headers, waterfallCb) { log.debug(sprintf("Signing request transaction %s", transactionHex)); client.cmd("signrawtransaction", transactionHex, waterfallCb); }, // Decode the signed transaction function(signed, headers, waterfallCb) { // Make sure Bitcoin-QT signed the transaction properly. // // TODO: would this ever fail? if (!signed.complete) { waterfallCb('Setup transaction not complete'); return; } log.debug(sprintf("Decoding transaction %s", signed)); client.cmd( 'decoderawtransaction', signed.hex, function(err, transactionJson) { waterfallCb(err, signed, transactionJson); } ); }, // Store the decoded transaction function(signed, transactionJson, waterfallCb) { // This is just a sanity check specific to this implementation assert( transactionJson.vout.length == 1 || transactionJson.vout.length == 2, 'Unexpected number of vouts' ); log.debug("Storing decoded transaction..."); var txid = transactionJson.txid; // The start of the target range is the txid minus a random offset var offset = util.cryptoRandom(inverseProbability); var offsetBi = new BigInteger(); offsetBi.fromInt(offset); var startBi = new BigInteger(); startBi.fromString(txid, 16); startBi = startBi.subtract(offsetBi).mod(TXID_RANGE_BI); // Store the target as a JSON object. We OVERWRITE the previous // target -- it's now invalid. Why? If we received a cashable // voucher for each target, we couldn't cash both. So, we can only // have one target open at a time. currentTarget = { start: startBi, length: inverseProbability, offset: offset, transactionHex: signed.hex }; // Success var target = serializeTarget( AMOUNT, currentTarget.length, 0, startBi.toString(16) ); waterfallCb(null, target); // Wait for a little before we free up the queue. This gives use a // time window to get paid before we being our next operation. setTimeout(queueCb, timeout); } ], cb); }; // The function that gets run when this gets popped off the queue is // actually a no-op. We manually call cb from inside the queued function, // so it's fine. queue.push(requestVoucher, function() {}); }; // // Create a voucher. // // - destinationAddress: the Base58-encoded address to which to pay the money // - target, a serialized target // // On failure, we call cb(error) // On success, we call cb(null, transactionHex) // this.createVoucher = function(destinationAddress, targetSerialized, cb) { // Check the inputs assert(util.isAddress(destinationAddress)); // Decode the target. If we get an error, the target is invalid. var target; try { target = deserializeTarget(targetSerialized); } catch (err) { cb('Error deserializing target: ' + err); return; } // We only create vouchers for exactly AMOUNT Satoshis if (target.amount != AMOUNT) { cb('Transaction amount was ' + target.amount + ', must be ' + AMOUNT); return; } // We can't pay ourselves using this method if(destinationAddress == address) { cb( 'The paying address may not be the same as the destination address'); return; } // This is a separate fn so we can push it onto the queue var createVoucher = function(queueCb) { async.waterfall([ // Get my previous tx info and balance getPrevious, // Ask the client to create this transaction. This doesn't actually send anything, it just // creates a binary string representing the transaction. function(previous, waterfallCb) { // Make sure we have enough money. Note that we don't actually need 2 * AMOUNT + TX_FEE, because we're // getting AMOUNT from the previous transaction. if(AMOUNT_WITH_FEE > previous.balance) { cb('Need ' + AMOUNT_WITH_FEE + ', only have ' + previous.balance); return; } // Our payment to the destination is the actual voucher. We must also return the money paid // in the original transaction back to the payee. So, we pay double the amount to the // destination address var outputs = {}; outputs[destinationAddress] = AMOUNT * 2 / SATOSHIS; // We need to send change to ourself iff there's money left over if (AMOUNT_WITH_FEE < previous.balance) { outputs[address] = (previous.balance - AMOUNT_WITH_FEE) / SATOSHIS; assert.equal( previous.balance + AMOUNT, // Inputs are this much Math.round((outputs[destinationAddress] + outputs[address]) * SATOSHIS + TX_FEE) // Outputs (+fee) ); } // Randomly guess a number in the target range var targetStartBi = new BigInteger(); targetStartBi.fromString(target.start, 16); var randomBi = new BigInteger(); randomBi.fromInt(util.cryptoRandom(target.length)); var previousTxidGuess = targetStartBi.add(randomBi).mod(TXID_RANGE_BI).toString(16); // Pad the guessed transaction ID with zeros while(previousTxidGuess.length < 64) { previousTxidGuess = '0' + previousTxidGuess; } var waterfallTranslator = function(err, transactionHex) { waterfallCb(err, transactionHex, previousTxidGuess); } client.cmd('createrawtransaction', [{'txid': previousTxidGuess, 'vout': target.vout}, {'txid': previous.txid, 'vout': previous.vout}], outputs, waterfallTranslator); }, // The transaction was successfully created. Now sign it. function(transactionHex, previousTxidGuess, waterfallCb) { var scriptPubKey = script.addressToScript(addressHex); client.cmd('signrawtransaction', transactionHex, [{'txid': previousTxidGuess,'vout': target.vout, 'scriptPubKey': scriptPubKey, 'redeemScript': privateKey}], waterfallCb); }, // We signed the transaction, now invalidate the current target function(signed, headers, waterfallCb) { // I think that this means that the transaction is potentially valid. TODO: verify assert(signed.complete); // Since we may send this voucher out and it's using the same prevout // as the current target, we must declare the current target // invalid. currentTarget = null; waterfallCb(null, signed.hex); } ], queueCb); }; queue.push(createVoucher, cb); }; // // Cash in a voucher. // // - transactionHex: a voucher payable to me // // This doesn't need to be synchronized using the queue. // // If the voucher is invalid, call cb(err) // If the voucher is valid, call cb(null, cashed) // // cashed is a boolean indicating whether this transaction was cashed // this.cashVoucher = function(transactionHex, cb) { // verify the txid client.cmd('decoderawtransaction', transactionHex, function(err, transactionJson) { if(err) { cb('Error decoding transaction: ' + err); return; } // This checks the 0th output of the transaction, and makes sure that it is paid to an // address within the targetrange that we've specified. // TODO: remove this check by looping over vouts if (transactionJson.vout.length < 1) { cb('there must be at least one vout (there may be another to get change)'); return; } // TODO: other vouts work too, maybe even add them up? // We expect double the amount, because we're getting back the amount we paid assert.equal(Math.round(transactionJson.vout[0]['value'] * SATOSHIS), AMOUNT * 2); var destinationAddress = transactionJson.vout[0].scriptPubKey.addresses[0]; // This is the txid that the payer guessed var previousTxid = transactionJson.vin[0].txid; var previousTxidBi = new BigInteger(); previousTxidBi.fromString(previousTxid, 16); // validate that toAddress isn't our own address if (destinationAddress != address) { cb('wrong destination address: is ' + destinationAddress + ', should be ' + address); return; } // If there's no target set up, we can't be paid if (!currentTarget) { cb('No target set up'); return; } // previous txid is lower than this target, no good if (previousTxidBi.compareTo(currentTarget.start) < 0) { cb('txid too low'); return; } // previous txid is higher than this target, no good var targetLength = new BigInteger(); targetLength.fromInt(currentTarget.length); var targetEnd = currentTarget.start.add(targetLength); if (previousTxidBi.compareTo(targetEnd) >= 0) { cb('txid too high'); return; } // Wrong guess. The voucher was valid but not cashable var targetOffset = new BigInteger(); targetOffset.fromInt(currentTarget.offset); if (previousTxidBi.compareTo(currentTarget.start.add(targetOffset)) !== 0) { // Destroy the target. Otherwise the payer could re-guess with a higher probability of // correctness currentTarget = null; cb(null, false); return; } // // The voucher was valid and cashable. Cash it! #getmoney // // First, submit the setup transaction client.cmd('sendrawtransaction', currentTarget.transactionHex, function(err, txid) { if(err) { // We couldn't submit the setup transaction? Yuck. Bitcoin-Qt closed, maybe? cb('Error sending setup transaction: ' + currentTarget.transactionHex + ' ' + err); return; } // Now that we've used this target, get rid of it currentTarget = null; // TODO: how do we tell the difference between invalid and uncashable if this call fails? client.cmd('sendrawtransaction', transactionHex, function(err, txid) { if(err) { cb({"message": "Error sending real transaction", "error": err}); } else { cb(null, true); } }); }); }); }; }; // Return an account with the given parameters. // // - address: string // - privateKey: string // // If creation succeeds, call cb(err) // If creation succeeds, call cb(null, account) // this.getAccount = function(address, privateKey, cb) { // This implementation actually always succeeds and returns synchronously cb(null, new Account(address, privateKey)); }; }; Object.freeze(exports);