Merge branch 'satoshi-p2sh-hd'

This commit is contained in:
Christopher Jeffrey 2015-12-09 16:38:27 -08:00
commit f939d73b38
19 changed files with 4888 additions and 317 deletions

2050
etc/english.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
var bcoin = exports;
var elliptic = require('elliptic');
bcoin.ecdsa = elliptic.ecdsa(elliptic.nist.secp256k1);
bcoin.ecdsa = elliptic.ec('secp256k1');
bcoin.utils = require('./bcoin/utils');
bcoin.bloom = require('./bcoin/bloom');
bcoin.protocol = require('./bcoin/protocol');
@ -13,3 +13,6 @@ bcoin.chain = require('./bcoin/chain');
bcoin.wallet = require('./bcoin/wallet');
bcoin.peer = require('./bcoin/peer');
bcoin.pool = require('./bcoin/pool');
bcoin.hd = require('./bcoin/hd');
bcoin.protocol.network.set(process.env.BCOIN_NETWORK || 'main');

View File

@ -3,7 +3,7 @@ var EventEmitter = require('events').EventEmitter;
var bcoin = require('../bcoin');
var constants = bcoin.protocol.constants;
var preload = bcoin.protocol.preload;
var network = bcoin.protocol.network;
var utils = bcoin.utils;
var assert = utils.assert;
@ -26,6 +26,7 @@ function Chain(options) {
};
this.orphan = {
map: {},
bmap: {},
count: 0
};
this.index = {
@ -37,6 +38,20 @@ function Chain(options) {
};
this.request = new utils.RequestCache();
var preload = network.preload;
// Start from the genesis block
// if we're a full node.
if (this.options.fullNode) {
preload = {
v: preload.v,
type: preload.type,
hashes: preload.hashes.slice(0, 1),
ts: preload.ts.slice(0, 1),
heights: preload.heights.slice(0, 1)
};
}
this.fromJSON(preload);
// Last TS after preload, needed for fill percent
@ -53,25 +68,35 @@ function compareTs(a, b) {
}
Chain.prototype._init = function _init() {
var self = this;
if (!this.storage)
return;
utils.nextTick(function() {
self.emit('debug', 'Chain is loading.');
});
this.loading = true;
var self = this;
var s = this.storage.createReadStream({
start: this.prefix,
end: this.prefix + 'z'
});
s.on('data', function(data) {
var hash = data.key.slice(self.prefix.length);
self._addIndex(hash, data.value.ts, data.value.height);
});
s.on('error', function(err) {
self.emit('error', err);
});
s.on('end', function() {
self.loading = false;
self.emit('load');
self.emit('debug', 'Chain successfully loaded.');
});
};
@ -222,6 +247,7 @@ Chain.prototype.add = function add(block) {
if (!this._probeIndex(hash, block.ts) && !prevProbe) {
this.orphan.count++;
this.orphan.map[prev] = block;
this.orphan.bmap[block.hash('hex')] = block;
var range = this._getRange(hash, block.ts, true);
var hashes = this.index.hashes.slice(range.start, range.end + 1);
@ -247,6 +273,7 @@ Chain.prototype.add = function add(block) {
// We have orphan child for this block - add it to chain
block = this.orphan.map[hash];
delete this.orphan.map[hash];
delete this.orphan.bmap[block.hash('hex')];
this.orphan.count--;
} while (true);
@ -388,6 +415,65 @@ Chain.prototype.getLast = function getLast(cb) {
return cb(this.index.hashes[this.index.hashes.length - 1]);
};
Chain.prototype.getStartHeight = function getStartHeight() {
if (!this.options.fullNode) {
if (this.options.startHeight != null) {
return this.options.startHeight;
}
return 0;
}
return this.index.heights[this.index.heights.length - 1];
};
Chain.prototype.locatorHashes = function(index) {
var chain = this.index.hashes;
var hashes = [];
var top = chain.length - 1;
var step = 1;
var i;
if (typeof index === 'string') {
for (i = top; i >= 0; i--) {
if (chain[i] === index) {
top = i;
break;
}
}
} else if (typeof index === 'number') {
top = index;
}
i = top;
for (;;) {
hashes.push(chain[i]);
i = i - step;
if (i <= 0) {
hashes.push(chain[0]);
break;
}
if (hashes.length >= 10)
step *= 2;
}
return hashes;
};
Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) {
var self = this;
if (typeof hash !== 'string') {
hash = hash.hash('hex');
}
var orphans = this.orphan.bmap;
var orphanRoot = hash;
while (orphans[orphanRoot.prevBlock])
orphanRoot = orphans[orphanRoot.prevBlock];
return orphanRoot;
};
Chain.prototype.toJSON = function toJSON() {
var keep = 1000;
@ -432,6 +518,7 @@ Chain.prototype.toJSON = function toJSON() {
return {
v: 1,
type: 'chain',
network: network.type,
hashes: first.hashes.concat(last.hashes),
ts: first.ts.concat(last.ts),
heights: first.heights.concat(last.heights)
@ -441,16 +528,18 @@ Chain.prototype.toJSON = function toJSON() {
Chain.prototype.fromJSON = function fromJSON(json) {
assert.equal(json.v, 1);
assert.equal(json.type, 'chain');
if (json.network)
assert.equal(json.network, network.type);
this.index.hashes = json.hashes.slice();
this.index.ts = json.ts.slice();
this.index.heights = json.heights.slice();
if (this.index.bloom)
this.index.bloom.reset();
else
this.index.bloom = new bcoin.bloom(28 * 1024 * 1024, 16, 0xdeadbee0);
this.index.bloom = new bcoin.bloom(28 * 1024 * 1024, 16, 0xdeadbeef);
if (this.index.hashes.length === 0)
this.add(new bcoin.block(constants.genesis, 'block'));
this.add(new bcoin.block(network.genesis, 'block'));
for (var i = 0; i < this.index.hashes.length; i++) {
this.index.bloom.add(this.index.hashes[i], 'hex');

686
lib/bcoin/hd.js Normal file
View File

@ -0,0 +1,686 @@
/**
* hd.js - hierarchical determistic seeds and keys (BIP32, BIP39)
* https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
* https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
*/
/**
* Code adapted from bitcore-lib:
* https://github.com/bitpay/bitcore-lib/blob/master/lib/hdprivatekey.js
* https://github.com/bitpay/bitcore-lib/blob/master/lib/hdpublickey.js
* https://github.com/ryanxcharles/fullnode/blob/master/lib/bip32.js
*
* Copyright (c) 2013-2015 BitPay, Inc.
*
* Parts of this software are based on Bitcoin Core
* Copyright (c) 2009-2015 The Bitcoin Core developers
*
* Parts of this software are based on fullnode
* Copyright (c) 2014 Ryan X. Charles
* Copyright (c) 2014 reddit, Inc.
*
* Parts of this software are based on BitcoinJS
* Copyright (c) 2011 Stefan Thomas <justmoon@members.fsf.org>
*
* Parts of this software are based on BitcoinJ
* Copyright (c) 2011 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var hd = exports;
/**
* Modules
*/
var bcoin = require('../bcoin');
var hash = require('hash.js');
var bn = require('bn.js');
var elliptic = require('elliptic');
var utils = bcoin.utils;
var assert = utils.assert;
var network = bcoin.protocol.network;
var EventEmitter = require('events').EventEmitter;
var english = require('../../etc/english.json');
var ec = elliptic.curves.secp256k1;
/**
* HD Seeds
*/
function HDSeed(options) {
if (!(this instanceof HDSeed))
return new HDSeed(options);
this.bits = options.bits || 128;
this.entropy = options.entropy || HDSeed._entropy(this.bits / 8);
this.mnemonic = options.mnemonic || HDSeed._mnemonic(this.entropy);
if (options.passphrase !== undefined) {
this.seed = this.createSeed(options.passphrase);
}
}
HDSeed.create = function(options) {
var obj = new HDSeed(options);
return obj.seed || obj;
};
HDSeed.prototype.createSeed = function(passphrase) {
this.passphrase = passphrase || '';
return pbkdf2(this.mnemonic, 'mnemonic' + passphrase, 2048, 64);
};
HDSeed._entropy = function(size) {
return randomBytes(size);
};
HDSeed._mnemonic = function(entropy) {
var bin = '';
for (var i = 0; i < entropy.length; i++) {
bin = bin + ('00000000' + entropy[i].toString(2)).slice(-8);
}
var mnemonic = [];
for (i = 0; i < bin.length / 11; i++) {
var wi = parseInt(bin.slice(i * 11, (i + 1) * 11), 2);
mnemonic.push(english[wi]);
}
return mnemonic.join(' ');
};
/**
* HD Keys
*/
var HARDENED = 0x80000000;
var MAX_INDEX = 2 * HARDENED;
var MIN_ENTROPY = 128 / 8;
var MAX_ENTROPY = 512 / 8;
var PARENT_FINGER_PRINT_SIZE = 4;
var PATH_ROOTS = ['m', 'M', 'm\'', 'M\''];
/**
* HD Private Key
*/
function HDPriv(options) {
var data;
if (!(this instanceof HDPriv))
return new HDPriv(options);
if (!options)
options = { seed: new HDSeed({ passphrase: '' }) };
if (typeof options === 'string' && options.indexOf('xprv') === 0)
options = { xkey: options };
if (options.passphrase)
options.seed = new HDSeed({ passphrase: options.passphrase });
if (options.seed) {
if (typeof options.seed === 'object' && !(options.seed instanceof HDSeed)) {
options.seed = new HDSeed(options.seed);
}
this.seed = options.seed;
data = this._seed(options.seed);
} else if (options.xkey) {
data = this._unbuild(options.xkey);
} else {
data = options;
}
data = this._normalize(data, network.prefixes.xprivkey);
this.data = data;
this._build(data);
}
HDPriv.prototype._normalize = function(data, version) {
var b;
data.version = version || network.prefixes.xprivkey;
data.version = +data.version;
data.depth = +data.depth;
if (typeof data.parentFingerPrint === 'number') {
b = [];
utils.writeU32BE(b, data.parentFingerPrint, 0);
data.parentFingerPrint = b;
}
data.childIndex = +data.childIndex;
if (typeof data.chainCode === 'string')
data.chainCode = utils.toArray(data.chainCode, 'hex');
data.privateKey = data.privateKey || data.priv;
if (data.privateKey) {
if (data.privateKey.getPrivate)
data.privateKey = data.privateKey.getPrivate().toArray();
else if (typeof data.privateKey === 'string')
data.privateKey = utils.toKeyArray(data.privateKey);
}
data.publicKey = data.publicKey || data.pub;
if (data.publicKey) {
if (data.publicKey.getPublic)
data.publicKey = data.privateKey.getPublic(true, 'array');
else if (typeof data.publicKey === 'string')
data.publicKey = utils.toKeyArray(data.publicKey);
}
if (typeof data.checksum === 'number') {
b = [];
utils.writeU32BE(b, data.checksum, 0);
data.checksum = b;
}
return data;
};
HDPriv.prototype._seed = function(seed) {
if (seed instanceof HDSeed)
seed = seed.seed;
if (utils.isHex(seed))
seed = utils.toArray(seed, 'hex');
if (seed.length < MIN_ENTROPY || seed.length > MAX_ENTROPY)
throw new Error('entropy not in range');
var hash = sha512hmac(seed, 'Bitcoin seed');
return {
// version: network.prefixes.xprivkey,
depth: 0,
parentFingerPrint: 0,
childIndex: 0,
chainCode: hash.slice(32, 64),
privateKey: hash.slice(0, 32),
checksum: null
};
};
HDPriv.prototype._unbuild = function(xkey) {
var raw = utils.fromBase58(xkey);
var data = {};
var off = 0;
data.version = utils.readU32BE(raw, off);
off += 4;
data.depth = raw[off];
off += 1;
data.parentFingerPrint = utils.readU32BE(raw, off);
off += 4;
data.childIndex = utils.readU32BE(raw, off);
off += 4;
data.chainCode = raw.slice(off, off + 32);
off += data.chainCode.length;
off += 1; // nul byte
data.privateKey = raw.slice(off, off + 32);
off += data.privateKey.length;
data.checksum = utils.readU32BE(raw, off);
off += 4;
var hash = utils.dsha256(raw.slice(0, -4));
if (data.checksum !== utils.readU32BE(hash, 0))
throw new Error('checksum mismatch');
return data;
};
HDPriv.prototype._build = function(data) {
var sequence = [];
var off = 0;
utils.writeU32BE(sequence, data.version, off);
off += 4;
sequence[off] = data.depth;
off += 1;
utils.copy(data.parentFingerPrint, sequence, off, true);
off += data.parentFingerPrint.length;
utils.writeU32BE(sequence, data.childIndex, off);
off += 4;
utils.copy(data.chainCode, sequence, off, true);
off += data.chainCode.length;
sequence[off] = 0;
off += 1;
utils.copy(data.privateKey, sequence, off, true);
off += data.privateKey.length;
var checksum = utils.dsha256(sequence).slice(0, 4);
utils.copy(checksum, sequence, off, true);
off += 4;
var xprivkey = utils.toBase58(sequence);
var pair = bcoin.ecdsa.keyPair({ priv: data.privateKey });
var privateKey = pair.getPrivate().toArray();
var publicKey = pair.getPublic(true, 'array');
var size = PARENT_FINGER_PRINT_SIZE;
var fingerPrint = utils.ripesha(publicKey).slice(0, size);
this.version = data.version;
this.depth = data.depth;
this.parentFingerPrint = data.parentFingerPrint;
this.childIndex = data.childIndex;
this.chainCode = data.chainCode;
this.privateKey = privateKey;
// this.checksum = checksum;
this.xprivkey = xprivkey;
this.fingerPrint = fingerPrint;
this.publicKey = publicKey;
this.hdpub = new HDPub(this);
this.xpubkey = this.hdpub.xpubkey;
this.pair = bcoin.ecdsa.keyPair({ priv: this.privateKey });
};
HDPriv.prototype.derive = function(index, hard) {
if (typeof index === 'string')
return this.deriveString(index);
hard = index >= HARDENED ? true : hard;
if (index < HARDENED && hard === true)
index += HARDENED;
var index_ = [];
utils.writeU32BE(index_, index, 0);
var data = hard
? [0].concat(this.privateKey).concat(index_)
: data = [].concat(this.publicKey).concat(index_);
var hash = sha512hmac(data, this.chainCode);
var leftPart = new bn(hash.slice(0, 32));
var chainCode = hash.slice(32, 64);
var privateKey = leftPart.add(new bn(this.privateKey)).mod(ec.curve.n).toArray();
return new HDPriv({
// version: this.version,
depth: this.depth + 1,
parentFingerPrint: this.fingerPrint,
childIndex: index,
chainCode: chainCode,
privateKey: privateKey
});
};
// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
HDPriv._getIndexes = function(path) {
var steps = path.split('/');
var root = steps.shift();
var indexes = [];
if (~PATH_ROOTS.indexOf(path))
return indexes;
if (!~PATH_ROOTS.indexOf(root))
return null;
for (var i = 0; i < steps.length; i++) {
var step = steps[i];
var hard = step[step.length - 1] === '\'';
if (hard)
step = step.slice(0, -1);
if (!step || step[0] === '-')
return null;
var index = +step;
if (hard)
index += HARDENED;
indexes.push(index);
}
return indexes;
};
HDPriv.isValidPath = function(path, hard) {
if (typeof path === 'string') {
var indexes = HDPriv._getIndexes(path);
return indexes !== null && indexes.every(HDPriv.isValidPath);
}
if (typeof path === 'number') {
if (path < HARDENED && hard === true) {
path += HARDENED;
}
return path >= 0 && path < MAX_INDEX;
}
return false;
};
HDPriv.prototype.deriveString = function(path) {
if (!HDPriv.isValidPath(path))
throw new Error('invalid path');
var indexes = HDPriv._getIndexes(path);
return indexes.reduce(function(prev, index) {
return prev.derive(index);
});
};
/**
* HD Public Key
*/
function HDPub(options) {
if (!(this instanceof HDPub))
return new HDPriv(options);
var data;
if (typeof options === 'string' && options.indexOf('xpub') === 0)
options = { xkey: options };
if (options.xkey)
data = this._unbuild(options.xkey);
else
data = options;
data = this._normalize(data, network.prefixes.xpubkey);
this.data = data;
this._build(data);
}
HDPub.prototype._normalize = HDPriv.prototype._normalize;
HDPub.prototype._unbuild = function(xkey) {
var raw = utils.fromBase58(xkey);
var data = {};
var off = 0;
data.version = utils.readU32BE(raw, off);
off += 4;
data.depth = raw[off];
off += 1;
data.parentFingerPrint = utils.readU32BE(raw, off);
off += 4;
data.childIndex = utils.readU32BE(raw, off);
off += 4;
data.chainCode = raw.slice(off, off + 32);
off += data.chainCode.length;
data.publicKey = raw.slice(off, off + 33);
off += data.publicKey.length;
data.checksum = utils.readU32BE(raw, off);
off += 4;
var hash = utils.dsha256(raw.slice(0, -4));
if (data.checksum !== utils.readU32BE(hash, 0))
throw new Error('checksum mismatch');
return data;
};
HDPub.prototype._build = function(data) {
var sequence = [];
var off = 0;
utils.writeU32BE(sequence, data.version, off);
off += 4;
sequence[off] = data.depth;
off += 1;
utils.copy(data.parentFingerPrint, sequence, off, true);
off += data.parentFingerPrint.length;
utils.writeU32BE(sequence, data.childIndex, off);
off += 4;
utils.copy(data.chainCode, sequence, off, true);
off += data.chainCode.length;
utils.copy(data.publicKey, sequence, off, true);
off += data.publicKey.length;
var checksum = utils.dsha256(sequence).slice(0, 4);
utils.copy(checksum, sequence, off, true);
off += 4;
if (!data.checksum || !data.checksum.length)
data.checksum = checksum;
else if (utils.toHex(checksum) !== utils.toHex(data.checksum))
throw new Error('checksum mismatch');
var xpubkey = utils.toBase58(sequence);
var publicKey = data.publicKey;
var size = PARENT_FINGER_PRINT_SIZE;
var fingerPrint = utils.ripesha(publicKey).slice(0, size);
this.version = data.version;
this.depth = data.depth;
this.parentFingerPrint = data.parentFingerPrint;
this.childIndex = data.childIndex;
this.chainCode = data.chainCode;
this.publicKey = publicKey;
// this.checksum = checksum;
this.xpubkey = xpubkey;
this.fingerPrint = fingerPrint;
this.xprivkey = data.xprivkey;
this.pair = bcoin.ecdsa.keyPair({ pub: this.publicKey });
};
HDPub.prototype.derive = function(index, hard) {
if (typeof index === 'string')
return this.deriveString(index);
if (index >= HARDENED || hard)
throw new Error('invalid index');
if (index < 0)
throw new Error('invalid path');
var index_ = [];
utils.writeU32BE(index_, index, 0);
var data = [].concat(this.publicKey).concat(index_);
var hash = sha512hmac(data, this.chainCode);
var leftPart = new bn(hash.slice(0, 32));
var chainCode = hash.slice(32, 64);
var pair = bcoin.ecdsa.keyPair({ pub: this.publicKey });
var pubkeyPoint = ec.curve.g.mul(leftPart).add(pair.pub);
var publicKey = bcoin.ecdsa.keyFromPublic(pubkeyPoint).getPublic(true, 'array');
return new HDPub({
// version: network.prefixes.xpubkey,
depth: this.depth + 1,
parentFingerPrint: this.fingerPrint,
childIndex: index,
chainCode: chainCode,
publicKey: publicKey
});
};
HDPub.isValidPath = function(arg) {
if (typeof arg === 'string') {
var indexes = HDPriv._getIndexes(arg);
return indexes !== null && indexes.every(HDPub.isValidPath);
}
if (typeof arg === 'number')
return arg >= 0 && arg < HARDENED;
return false;
};
HDPub.prototype.deriveString = function(path) {
if (~path.indexOf('\''))
throw new Error('cannot derive hardened');
else if (!HDPub.isValidPath(path))
throw new Error('invalid path');
var indexes = HDPriv._getIndexes(path);
return indexes.reduce(function(prev, index) {
return prev.derive(index);
});
};
/**
* Make HD keys behave like elliptic KeyPairs
*/
[HDPriv, HDPub].forEach(function(HD) {
HD.prototype.validate = function validate() {
return this.pair.validate.apply(this.pair, arguments);
};
HD.prototype.getPublic = function getPublic() {
return this.pair.getPublic.apply(this.pair, arguments);
};
HD.prototype.getPrivate = function getPrivate() {
return this.pair.getPublic.apply(this.pair, arguments);
};
HD.prototype.sign = function sign(msg) {
return this.pair.sign.apply(this.pair, arguments);
};
HD.prototype.verify = function verify(msg, signature) {
return this.pair.verify.apply(this.pair, arguments);
};
HD.prototype.inspect = function inspect() {
return this.pair.inspect.apply(this.pair, arguments);
};
HD.prototype.__defineGetter__('pub', function() {
return this.pair.pub;
});
HD.prototype.__defineGetter__('priv', function() {
return this.pair.priv;
});
});
/**
* Helpers
*/
var isBrowser = (typeof process !== 'undefined' && process.browser)
|| typeof window !== 'undefined';
function sha512hmac(data, salt) {
if (isBrowser) {
var hmac = hash.hmac(hash.sha512, utils.toArray(salt));
return hmac.update(utils.toArray(data)).digest();
}
var crypto = require('crypto');
var hmac = crypto.createHmac('sha512', new Buffer(salt));
var h = hmac.update(new Buffer(data)).digest();
return Array.prototype.slice.call(h);
}
function randomBytes(size) {
if (isBrowser) {
var a = Uint8Array(size);
(window.crypto || window.msCrypto).getRandomValues(a);
var buf = new Array(size);
utils.copy(a, buf, 0);
return buf;
}
var crypto = require('crypto');
return Array.prototype.slice.call(crypto.randomBytes(size));
}
/**
* PDKBF2
* Credit to: https://github.com/stayradiated/pbkdf2-sha512
* Copyright (c) 2014, JP Richardson Copyright (c) 2010-2011 Intalio Pte, All Rights Reserved
*/
function pbkdf2(key, salt, iterations, dkLen) {
'use strict';
var hLen = 64;
if (dkLen > (Math.pow(2, 32) - 1) * hLen)
throw Error('Requested key length too long');
if (typeof key !== 'string' && typeof key.length !== 'number')
throw new TypeError('key must a string or array');
if (typeof salt !== 'string' && typeof salt.length !== 'number')
throw new TypeError('salt must a string or array');
if (typeof key === 'string')
key = utils.toArray(key, null);
if (typeof salt === 'string')
salt = utils.toArray(salt, null);
var DK = new Array(dkLen);
var U = new Array(hLen);
var T = new Array(hLen);
var block1 = new Array(salt.length + 4);
var l = Math.ceil(dkLen / hLen);
var r = dkLen - (l - 1) * hLen;
utils.copy(salt.slice(0, salt.length), block1, 0);
for (var i = 1; i <= l; i++) {
block1[salt.length + 0] = (i >> 24 & 0xff);
block1[salt.length + 1] = (i >> 16 & 0xff);
block1[salt.length + 2] = (i >> 8 & 0xff);
block1[salt.length + 3] = (i >> 0 & 0xff);
U = sha512hmac(block1, key);
utils.copy(U.slice(0, hLen), T, 0);
for (var j = 1; j < iterations; j++) {
U = sha512hmac(U, key);
for (var k = 0; k < hLen; k++)
T[k] ^= U[k];
}
var destPos = (i - 1) * hLen;
var len = (i === l ? r : hLen);
utils.copy(T.slice(0, len), DK, 0);
}
return DK;
}
/**
* Expose
*/
hd.seed = HDSeed;
hd.priv = HDPriv;
hd.pub = HDPub;
hd.pbkdf2 = pbkdf2;

View File

@ -89,6 +89,17 @@ Peer.prototype._init = function init() {
self._error(err);
});
if (this.pool.options.fullNode) {
this.once('version', function() {
var ip = self.socket && self.socket.remoteAddress || '0.0.0.0';
self.pool.emit('debug',
'Sent version (%s): height=%s',
ip, this.pool.chain.getStartHeight());
self.pool.emit('debug', 'version (%s): sending locator hashes', ip);
self.loadHeaders(self.chain.locatorHashes(), 0);
});
}
this._ping.timer = setInterval(function() {
self._write(self.framer.ping([
0xde, 0xad, 0xbe, 0xef,
@ -98,9 +109,7 @@ Peer.prototype._init = function init() {
// Send hello
this._write(this.framer.version({
height: this.options.startHeight != null
? this.options.startHeight
: 0,
height: this.pool.chain.getStartHeight(),
relay: this.options.relay
}));
@ -167,18 +176,8 @@ Peer.prototype.broadcast = function broadcast(items) {
};
Peer.prototype.updateWatch = function updateWatch() {
if (!this.pool.options.relay) {
if (this.ack) {
var self = this;
if (this.pool.block.lastHash)
this.loadBlocks([ self.pool.block.lastHash ], 0);
else
this.pool.chain.getLast(function(hash) {
self.loadBlocks([ hash ], 0);
});
}
if (this.pool.options.fullNode)
return;
}
if (this.ack)
this._write(this.framer.filterLoad(this.bloom, 'none'));
@ -292,6 +291,8 @@ Peer.prototype._onPacket = function onPacket(packet) {
return this._handleVersion(payload);
else if (cmd === 'inv')
return this._handleInv(payload);
else if (cmd === 'headers')
return this._handleHeaders(payload);
else if (cmd === 'getdata')
return this._handleGetData(payload);
else if (cmd === 'addr')
@ -437,15 +438,6 @@ Peer.prototype._handleInv = function handleInv(items) {
});
this.emit('blocks', blocks);
if (!this.pool.options.relay) {
if (txs.length)
this.emit('txs', txs.map(function(tx) {
return tx.hash;
}));
this.getData(items);
return;
}
if (txs.length === 0)
return;
@ -455,6 +447,38 @@ Peer.prototype._handleInv = function handleInv(items) {
this.getData(txs);
};
Peer.prototype._handleHeaders = function handleHeaders(headers) {
var self = this;
headers = headers.map(function(header) {
header.prevBlock = utils.toHex(header.prevBlock);
header.merkleRoot = utils.toHex(header.merkleRoot);
header.hash = utils.toHex(utils.dsha256(header._raw));
return header;
});
if (this.pool.options.fullNode) {
for (var i = 0; i < headers.length; i++) {
var header = headers[i];
var hash = header.hash;
// if (this.chain.orphan.bmap[hash]) {
if (this.chain.orphan.map[header.prevBlock]) {
this.loadHeaders(this.chain.locatorHashes(), this.chain.getOrphanRoot(hash));
continue;
}
if (!this.chain.index.bloom.test(hash, 'hex') || i === headers.length - 1)
this.getData([{ type: 'block', hash: hash }]);
}
}
this.emit('headers', headers);
};
Peer.prototype.loadHeaders = function loadHeaders(hashes, stop) {
this._write(this.framer.getHeaders(hashes, stop));
};
Peer.prototype.loadBlocks = function loadBlocks(hashes, stop) {
this._write(this.framer.getBlocks(hashes, stop));
};

View File

@ -7,13 +7,18 @@ var utils = bcoin.utils;
var assert = utils.assert;
function Pool(options) {
var self = this;
if (!(this instanceof Pool))
return new Pool(options);
EventEmitter.call(this);
this.options = options || {};
this.options.relay = this.options.relay !== false;
this.options.fullNode = !!this.options.fullNode;
this.options.relay == null
? (this.options.fullNode ? false : true)
: this.options.relay;
this.storage = this.options.storage;
this.destroyed = false;
this.size = options.size || 32;
@ -40,7 +45,9 @@ function Pool(options) {
storage: this.storage,
// Since regular blocks contain transactions and full merkle
// trees, it's risky to cache 2000 blocks. Let's do 100.
cacheLimit: !this.options.relay ? 100 : null
cacheLimit: this.options.fullNode ? 100 : null,
fullNode: this.options.fullNode,
startHeight: this.options.startHeight
});
this.watchMap = {};
this.bloom = new bcoin.bloom(8 * 1024,
@ -85,19 +92,32 @@ function Pool(options) {
this.createConnection = options.createConnection;
assert(this.createConnection);
this._init();
this.chain.on('debug', function() {
var args = Array.prototype.slice.call(arguments);
self.emit.apply(self, ['debug'].concat(args));
});
if (!this.chain.loading) {
this._init();
} else {
this.chain.once('load', function() {
self._init();
});
}
}
inherits(Pool, EventEmitter);
module.exports = Pool;
Pool.prototype._init = function _init() {
var self = this;
this._addLoader();
for (var i = 0; i < this.size; i++)
this._addPeer(0);
this._load();
var self = this;
this.chain.on('missing', function(hash, preload, parent) {
if (self.options.fullNode) return;
self._request('block', hash, { force: true });
self._scheduleRequests();
self._loadRange(preload);
@ -142,6 +162,8 @@ Pool.prototype._addLoader = function _addLoader() {
clearTimeout(timer);
});
if (this.options.fullNode) return;
function destroy() {
// Chain is full and up-to-date
if (self.chain.isFull()) {
@ -198,6 +220,8 @@ Pool.prototype.isFull = function isFull() {
};
Pool.prototype._loadRange = function _loadRange(hashes, force) {
if (this.options.fullNode) return;
if (!hashes)
return;
@ -220,6 +244,8 @@ Pool.prototype._loadRange = function _loadRange(hashes, force) {
};
Pool.prototype._load = function _load() {
if (this.options.fullNode) return;
if (this.request.queue.length >= this.load.hwm) {
this.load.hiReached = true;
return false;
@ -295,19 +321,38 @@ Pool.prototype._addPeer = function _addPeer(backoff) {
self._scheduleRequests();
});
peer.on('merkleblock', function(block) {
// Reset backoff, peer seems to be responsive
backoff = 0;
if (!this.options.fullNode) {
peer.on('merkleblock', function(block) {
// Reset backoff, peer seems to be responsive
backoff = 0;
self._response(block);
self.chain.add(block);
self.emit('chain-progress', self.chain.fillPercent(), peer);
self.emit('block', block, peer);
});
if (!this.options.relay) {
self._response(block);
self.chain.add(block);
self.emit('chain-progress', self.chain.fillPercent(), peer);
self.emit('block', block, peer);
});
} else {
peer.on('block', function(block) {
peer.emit('merkleblock', block);
backoff = 0;
var height = self.chain.index.hashes.length;
var hash = block.hash('hex');
if (!self.chain.index.bloom.test(block.prevBlock, 'hex')) {
peer.loadHeaders(self.chain.locatorHashes(), self.chain.getOrphanRoot(block));
}
self._response(block);
self.chain.add(block);
// if (!self.chain.index.bloom.test(block.prevBlock, 'hex')) {
// peer.loadHeaders(self.chain.locatorHashes(), self.chain.getOrphanRoot(block));
// } else if (self.chain.index.hashes.length === height) {
// return;
// }
self.emit('chain-progress', self.chain.fillPercent(), peer);
self.emit('block', block, peer);
});
}
@ -365,8 +410,11 @@ Pool.prototype._removePeer = function _removePeer(peer) {
Pool.prototype.watch = function watch(id) {
if (id instanceof bcoin.wallet) {
this.watch(id.getAddress());
this.watch(id.getPublicKey());
// this.watch(id.getAddress());
this.watch(id.getFullHash());
this.watch(id.getFullPublicKey());
this.watch(id.getOwnHash());
this.watch(id.getOwnPublicKey());
return;
}
@ -415,8 +463,10 @@ Pool.prototype.unwatch = function unwatch(id) {
Pool.prototype.addWallet = function addWallet(w, defaultTs) {
if (this.wallets.indexOf(w) !== -1)
return false;
this.watch(w.getHash());
this.watch(w.getPublicKey());
this.watch(w.getFullHash());
this.watch(w.getFullPublicKey());
this.watch(w.getOwnHash());
this.watch(w.getOwnPublicKey());
var self = this;
var e = new EventEmitter();
@ -447,8 +497,10 @@ Pool.prototype.removeWallet = function removeWallet(w) {
if (i == -1)
return;
this.wallets.splice(i, 1);
this.unwatch(w.getHash());
this.unwatch(w.getPublicKey());
this.unwatch(w.getFullHash());
this.unwatch(w.getFullPublicKey());
this.unwatch(w.getOwnHash());
this.unwatch(w.getOwnPublicKey());
}
Pool.prototype.search = function search(id, range, e) {

View File

@ -3,21 +3,6 @@ var utils = bcoin.utils;
exports.minVersion = 70001;
exports.version = 70002;
exports.magic = 0xd9b4bef9;
exports.genesis = {
version: 1,
prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0 ],
merkleRoot: utils.toArray(
'4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
'hex'
).reverse(),
ts: 1231006505,
bits: 0x1d00ffff,
nonce: 2083236893
};
// version - services field
exports.services = {
@ -49,9 +34,9 @@ exports.opcodes = {
pushdata1: 0x4c,
pushdata2: 0x4d,
pushdata4: 0x4e,
negate1: 0x4f,
// negate1: 0x4f,
nop: 0x61,
nop1: 0x61,
if_: 0x63,
notif: 0x64,
else_: 0x67,
@ -129,12 +114,17 @@ exports.opcodes = {
checksig: 0xac,
checksigverify: 0xad,
checkmultisig: 0xae,
checkmultisigverify: 0xaf
checkmultisigverify: 0xaf,
checklocktimeverify: 0xb1
};
exports.opcodes['-1'] = 0x50 + -1;
for (var i = 1; i <= 16; i++)
exports.opcodes[i] = 0x50 + i;
for (var i = 0; i <= 7; i++)
exports.opcodes['nop' + (i + 3)] = 0xb2 + i;
exports.opcodesByVal = new Array(256);
Object.keys(exports.opcodes).forEach(function(name) {
exports.opcodesByVal[exports.opcodes[name]] = name;
@ -148,8 +138,15 @@ exports.hashType = {
anyonecaypay: 0x80
};
exports.rhashType = Object.keys(exports.hashType).reduce(function(out, type) {
out[exports.hashType[type]] = type;
return out;
}, {});
exports.block = {
maxSize: 1000000,
maxSigops: 1000000 / 50,
maxOrphanTx: 1000000 / 100
};
exports.locktimeThreshold = 500000000; // Tue Nov 5 00:53:20 1985 UTC

View File

@ -1,4 +1,5 @@
var bcoin = require('../../bcoin');
var network = require('./network');
var constants = require('./constants');
var utils = bcoin.utils;
var assert = utils.assert;
@ -27,7 +28,7 @@ Framer.prototype.header = function header(cmd, payload) {
var h = new Array(24);
// Magic value
writeU32(h, constants.magic, 0);
writeU32(h, network.magic, 0);
// Command
var len = writeAscii(h, cmd, 4);
@ -201,7 +202,15 @@ Framer.prototype.filterClear = function filterClear() {
return this.packet('filterclear', []);
};
Framer.prototype.getHeaders = function getHeaders(hashes, stop) {
return this._getBlocks('getheaders', hashes, stop);
};
Framer.prototype.getBlocks = function getBlocks(hashes, stop) {
return this._getBlocks('getblocks', hashes, stop);
};
Framer.prototype._getBlocks = function _getBlocks(cmd, hashes, stop) {
var p = [];
writeU32(p, constants.version, 0);
var off = 4 + varint(p, hashes.length, 4);
@ -227,7 +236,7 @@ Framer.prototype.getBlocks = function getBlocks(hashes, stop) {
p[off + len] = 0;
assert.equal(off + len, p.length);
return this.packet('getblocks', p);
return this.packet(cmd, p);
};
Framer.tx = function tx(tx) {

View File

@ -3,4 +3,4 @@ var protocol = exports;
protocol.constants = require('./constants');
protocol.framer = require('./framer');
protocol.parser = require('./parser');
protocol.preload = require('./preload');
protocol.network = require('./network');

View File

@ -0,0 +1,178 @@
var bcoin = require('../../bcoin');
var utils = bcoin.utils;
/**
* Network
*/
var network = exports;
network.set = function(type) {
var net = network[type];
utils.merge(network, net);
};
/**
* Main
*/
var main = network.main = {};
main.prefixes = {
pubkey: 0,
script: 5,
privkey: 128,
xpubkey: 0x0488b21e,
xprivkey: 0x0488ade4
};
utils.merge(main.prefixes, {
normal: main.prefixes.pubkey,
p2pkh: main.prefixes.pubkey,
multisig: main.prefixes.pubkey,
p2sh: main.prefixes.script
});
main.type = 'main';
main.seeds = [
'seed.bitcoin.sipa.be', // Pieter Wuille
'dnsseed.bluematt.me', // Matt Corallo
'dnsseed.bitcoin.dashjr.org', // Luke Dashjr
'seed.bitcoinstats.com', // Christian Decker
'bitseed.xf2.org', // Jeff Garzik
'seed.bitcoin.jonasschnelli.ch' // Jonas Schnelli
];
main.port = 8333;
main.alertKey = utils.toArray(''
+ '04fc9702847840aaf195de8442ebecedf5b095c'
+ 'dbb9bc716bda9110971b28a49e0ead8564ff0db'
+ '22209e0374782c093bb899692d524e9d6a6956e'
+ '7c5ecbcd68284',
'hex');
main.checkpoints = [
{ height: 11111, hash: '0000000069e244f73d78e8fd29ba2fd2ed618bd6fa2ee92559f542fdb26e7c1d' },
{ height: 33333, hash: '000000002dd5588a74784eaa7ab0507a18ad16a236e7b1ce69f00d7ddfb5d0a6' },
{ height: 74000, hash: '0000000000573993a3c9e41ce34471c079dcf5f52a0e824a81e7f953b8661a20' },
{ height: 105000, hash: '00000000000291ce28027faea320c8d2b054b2e0fe44a773f3eefb151d6bdc97' },
{ height: 134444, hash: '00000000000005b12ffd4cd315cd34ffd4a594f430ac814c91184a0d42d2b0fe' },
{ height: 168000, hash: '000000000000099e61ea72015e79632f216fe6cb33d7899acb35b75c8303b763' },
{ height: 193000, hash: '000000000000059f452a5f7340de6682a977387c17010ff6e6c3bd83ca8b1317' },
{ height: 210000, hash: '000000000000048b95347e83192f69cf0366076336c639f9b7228e9ba171342e' },
{ height: 216116, hash: '00000000000001b4f4b433e81ee46494af945cf96014816a4e2370f11b23df4e' },
{ height: 225430, hash: '00000000000001c108384350f74090433e7fcf79a606b8e797f065b130575932' },
{ height: 250000, hash: '000000000000003887df1f29024b06fc2200b55f8af8f35453d7be294df2d214' },
{ height: 279000, hash: '0000000000000001ae8c72a0b0c301f67e3afca10e819efa9041e458e9bd7e40' },
{ height: 295000, hash: '00000000000000004d9b4ef50f0f9d686fd69db2e03af35a100370c64632a983' }
];
main.checkpoints.tsLastCheckpoint = 1397080064;
main.checkpoints.txsLastCheckpoint = 36544669;
main.checkpoints.txsPerDay = 60000.0;
// http://blockexplorer.com/b/0
// http://blockexplorer.com/rawblock/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
main.genesis = {
version: 1,
_hash: utils.toArray(
'000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
'hex'
).reverse(),
prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0 ],
merkleRoot: utils.toArray(
'4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
'hex'
).reverse(),
ts: 1231006505,
bits: 0x1d00ffff,
nonce: 2083236893
};
main.magic = 0xd9b4bef9;
main.preload = require('./preload');
/**
* Testnet (v3)
* https://en.bitcoin.it/wiki/Testnet
*/
var testnet = network.testnet = {};
testnet.type = 'testnet';
testnet.prefixes = {
pubkey: 111,
script: 196,
privkey: 239,
xpubkey: 0x043587cf,
xprivkey: 0x04358394
};
utils.merge(testnet.prefixes, {
normal: testnet.prefixes.pubkey,
p2pkh: testnet.prefixes.pubkey,
multisig: testnet.prefixes.pubkey,
p2sh: testnet.prefixes.script
});
testnet.seeds = [
'testnet-seed.alexykot.me',
'testnet-seed.bitcoin.petertodd.org',
'testnet-seed.bluematt.me',
'testnet-seed.bitcoin.schildbach.de'
];
testnet.port = 18333;
testnet.alertKey = utils.toArray(''
+ '04302390343f91cc401d56d68b123028bf52e5f'
+ 'ca1939df127f63c6467cdf9c8e2c14b61104cf8'
+ '17d0b780da337893ecc4aaff1309e536162dabb'
+ 'db45200ca2b0a',
'hex');
testnet.checkpoints = [
{ height: 546, hash: '000000002a936ca763904c3c35fce2f3556c559c0214345d31b1bcebf76acb70' }
];
testnet.checkpoints.tsLastCheckpoint = 1338180505;
testnet.checkpoints.txsLastCheckpoint = 16341;
testnet.checkpoints.txsPerDay = 300;
// http://blockexplorer.com/testnet/b/0
// http://blockexplorer.com/testnet/rawblock/000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943
testnet.genesis = {
version: 1,
_hash: utils.toArray(
'000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943',
'hex'
).reverse(),
prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0 ],
merkleRoot: utils.toArray(
'4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
'hex'
).reverse(),
ts: 1296688602,
bits: 0x1d00ffff,
nonce: 414098458
};
testnet.magic = 0x0709110b;
testnet.preload = {
'v': 1,
'type': 'chain',
'hashes': [utils.toHex(testnet.genesis._hash)],
'ts': [testnet.genesis.ts],
'heights': [0]
};

View File

@ -6,6 +6,7 @@ var bcoin = require('../../bcoin');
var utils = bcoin.utils;
var assert = utils.assert;
var constants = require('./constants');
var network = require('./network');
var readU32 = utils.readU32;
var readU64 = utils.readU64;
@ -71,7 +72,7 @@ Parser.prototype.parse = function parse(chunk) {
Parser.prototype.parseHeader = function parseHeader(h) {
var magic = readU32(h, 0);
if (magic !== constants.magic) {
if (magic !== network.magic) {
return this._error('Invalid magic value: ' + magic.toString(16));
}
@ -97,6 +98,8 @@ Parser.prototype.parsePayload = function parsePayload(cmd, p) {
return this.parseInvList(p);
else if (cmd === 'merkleblock')
return this.parseMerkleBlock(p);
else if (cmd === 'headers')
return this.parseHeaders(p);
else if (cmd === 'block')
return this.parseBlock(p);
else if (cmd === 'tx')
@ -135,10 +138,6 @@ Parser.prototype.parseVersion = function parseVersion(p) {
// Relay
var relay = p.length > off ? p[off] === 1 : true;
// NOTE: Could just do this to make relay
// `false` when it's not included:
// var relay = p[off] === 1;
return {
v: v,
services: services,
@ -226,6 +225,43 @@ Parser.prototype.parseMerkleBlock = function parseMerkleBlock(p) {
};
};
Parser.prototype.parseHeaders = function parseHeaders(p) {
if (p.length < 81)
return this._error('Invalid headers size');
var result = readIntv(p, 0);
var off = result.off;
var count = result.r;
var headers = [];
if (p.length >= off + 81) {
for (var i = 0; i < count && off + 81 < p.length; i++) {
var header = {};
var start = off;
header.version = readU32(p, off);
off += 4;
header.prevBlock = p.slice(off, off + 32);
off += 32;
header.merkleRoot = p.slice(off, off + 32);
off += 32;
header.ts = readU32(p, off);
off += 4;
header.bits = readU32(p, off);
off += 4;
header.nonce = readU32(p, off);
off += 4;
var r = readIntv(p, off);
header.totalTX = r.r;
off = r.off;
header._raw = p.slice(start, start + 80);
headers.push(header);
}
}
return headers;
};
Parser.prototype.parseBlock = function parseBlock(p) {
if (p.length < 81)
return this._error('Invalid block size');

View File

@ -1,6 +1,8 @@
var bcoin = require('../bcoin');
var bn = require('bn.js');
var constants = bcoin.protocol.constants;
var utils = bcoin.utils;
var assert = bcoin.utils.assert;
var script = exports;
script.decode = function decode(s) {
@ -23,8 +25,8 @@ script.decode = function decode(s) {
continue;
}
// Raw number
if (b >= 0x51 && b <= 0x60) {
// Raw number (-1 and 1-16)
if (b === 0x4f || (b >= 0x51 && b <= 0x60)) {
opcodes.push(b - 0x50);
continue;
}
@ -62,6 +64,7 @@ script.encode = function encode(s) {
// Push value to stack
if (Array.isArray(instr)) {
if (instr.length === 0) {
// OP_FALSE
res.push(0);
} else if (1 <= instr.length && instr.length <= 0x4b) {
res = res.concat(instr.length, instr);
@ -109,113 +112,635 @@ script.subscript = function subscript(s) {
return res;
};
script.execute = function execute(s, stack, tx) {
script.verify = function verify(hash, sig, pub) {
var k = bcoin.ecdsa.keyFromPublic(pub);
// Points at Infinity make verify() throw.
// This specifically throws on wallet-test.js
// where [1] is concatted to the pubkey.
if (k.getPublic().isInfinity())
return false;
// Use a try catch in case there are
// any uncaught errors for bad inputs in verify().
try {
return bcoin.ecdsa.verify(hash, sig, pub);
} catch (e) {
return false;
}
};
script._next = function(to, s, pc) {
var depth = 0;
while (s[pc]) {
var o = s[pc];
if (o === 'if_' || o === 'notif')
depth++;
else if (o === 'else_')
depth--;
else if (o === 'endif')
depth--;
if (depth < 0)
break;
if (depth === 0 && o === to)
return pc;
if (o === 'else_')
depth++;
pc++;
}
return -1;
};
script.execute = function execute(s, stack, tx, index) {
s = s.slice();
if (s.length > 10000)
return false;
var lastSep = -1;
stack.alt = stack.alt || [];
for (var pc = 0; pc < s.length; pc++) {
var o = s[pc];
if (Array.isArray(o)) {
stack.push(o);
} else if (o === 'dup') {
if (stack.length === 0)
return false;
continue;
}
stack.push(stack[stack.length - 1]);
} else if (o === 'hash160') {
if (stack.length === 0)
return false;
if (o === -1 || (o >= 1 && o <= 16)) {
stack.push([o]);
continue;
}
stack.push(utils.ripesha(stack.pop()));
} else if (o === 'eqverify' || o === 'eq') {
if (stack.length < 2)
return false;
var res = utils.isEqual(stack.pop(), stack.pop());
if (o === 'eqverify') {
if (!res)
return false;
} else {
stack.push(res ? [ 1 ] : []);
switch (o) {
case 'nop1':
case 'nop3':
case 'nop4':
case 'nop5':
case 'nop6':
case 'nop7':
case 'nop8':
case 'nop9':
case 'nop10': {
break;
}
} else if (o === 'checksigverify' || o === 'checksig') {
if (!tx || stack.length < 2)
return false;
var pub = stack.pop();
var sig = stack.pop();
var type = sig[sig.length - 1];
if (type !== 1)
return false;
var res = bcoin.ecdsa.verify(tx, sig.slice(0, -1), pub);
if (o === 'checksigverify') {
if (!res)
case 'if_':
case 'notif': {
var val = false;
if (stack.length < 1)
return false;
} else {
stack.push(res ? [ 1 ] : []);
var v = stack.pop();
val = new bn(v).cmpn(0) !== 0;
if (o === 'notif')
val = !val;
var if_ = pc;
var else_ = script._next('else_', s, pc);
var endif = script._next('endif', s, pc);
// Splice out the statement blocks we don't need
if (val) {
if (endif === -1)
return false;
if (else_ === -1) {
s.splice(endif, 1);
s.splice(if_, 1);
} else {
s.splice(else_, (endif - else_) + 1);
s.splice(if_, 1);
}
} else {
if (endif === -1)
return false;
if (else_ === -1) {
s.splice(if_, (endif - if_) + 1);
} else {
s.splice(endif, 1);
s.splice(if_, (else_ - if_) + 1);
}
}
// Subtract one since we removed the if/notif opcode
pc--;
break;
}
} else if (o === 'checkmultisigverify' || o === 'checkmultisig') {
if (!tx || stack.length < 3)
case 'else_': {
return false;
var n = stack.pop();
if (n.length !== 1 || !(1 <= n[0] && n[0] <= 3))
}
case 'endif': {
return false;
n = n[0];
if (stack.length < n + 1)
}
case 'verify': {
if (stack.length === 0)
return false;
if (new bn(stack[stack.length - 1]).cmpn(0) === 0)
return false;
break;
}
case 'ret': {
return false;
}
case 'toaltstack': {
if (stack.length === 0)
return false;
stack.alt.push(stack.pop());
break;
}
case 'fromaltstack': {
if (stack.alt.length === 0)
return false;
stack.push(stack.alt.pop());
break;
}
case 'ifdup': {
if (stack.length === 0)
return false;
if (new bn(stack[stack.length - 1]).cmpn(0) !== 0)
stack.push(new bn(stack[stack.length - 1]).toArray());
break;
}
case 'depth': {
stack.push(new bn(stack.length).toArray());
break;
}
case 'drop': {
if (stack.length === 0)
return false;
stack.pop();
break;
}
case 'dup': {
if (stack.length === 0)
return false;
stack.push(stack[stack.length - 1]);
break;
}
case 'nip': {
if (stack.length < 2)
return false;
stack.splice(stack.length - 2, 1);
break;
}
case 'over': {
if (stack.length < 2)
return false;
stack.push(stack[stack.length - 2]);
break;
}
case 'pick':
case 'roll': {
if (stack.length < 2)
return false;
var n = new bn(stack.pop()).toNumber();
if (n < 0 || n >= stack.length)
return false;
var v = stack[-n - 1];
if (o === 'roll')
stack.splice(stack.length - n - 1, 1);
stack.push(v);
break;
}
case 'rot': {
if (stack.length < 3)
return false;
var v3 = stack[stack.length - 3];
var v2 = stack[stack.length - 2];
var v1 = stack[stack.length - 1];
stack[stack.length - 3] = v2;
stack[stack.length - 2] = v3;
v2 = stack[stack.length - 2];
stack[stack.length - 2] = v1;
stack[stack.length - 1] = v2;
break;
}
case 'swap': {
if (stack.length < 2)
return false;
var v2 = stack[stack.length - 2];
var v1 = stack[stack.length - 1];
stack[stack.length - 2] = v1;
stack[stack.length - 1] = v2;
break;
}
case 'tuck': {
if (stack.length < 2)
return false;
stack.splice(stack.length - 2, 0, stack[stack.length - 1]);
break;
}
case 'drop2': {
if (stack.length < 2)
return false;
stack.pop();
stack.pop();
break;
}
case 'dup2': {
if (stack.length < 2)
return false;
var v1 = stack[stack.length - 1];
var v2 = stack[stack.length - 2];
stack.push(v1);
stack.push(v2);
break;
}
case 'dup3': {
if (stack.length < 3)
return false;
var v1 = stack[stack.length - 1];
var v2 = stack[stack.length - 2];
var v3 = stack[stack.length - 3];
stack.push(v1);
stack.push(v2);
stack.push(v3);
break;
}
case 'over2': {
if (stack.length < 4)
return false;
var v1 = stack[stack.length - 4];
var v2 = stack[stack.length - 3];
stack.push(v1);
stack.push(v2);
break;
}
case 'rot2': {
if (stack.length < 6)
return false;
var v1 = stack[stack.length - 6];
var v2 = stack[stack.length - 5];
stack.splice(stack.length - 6, 2);
stack.push(v1);
stack.push(v2);
break;
}
case 'swap2': {
if (stack.length < 4)
return false;
var v4 = stack[stack.length - 4];
var v3 = stack[stack.length - 3];
var v2 = stack[stack.length - 2];
var v1 = stack[stack.length - 1];
stack[stack.length - 4] = v2;
stack[stack.length - 2] = v4;
stack[stack.length - 3] = v1;
stack[stack.length - 1] = v3;
break;
}
case 'size': {
if (stack.length < 1)
return false;
stack.push(new bn(stack[stack.length - 1].length || 0).toArray());
break;
}
case 'add1':
case 'sub1':
case 'negate':
case 'abs':
case 'not':
case 'noteq0': {
if (stack.length < 1)
return false;
var n = new bn(stack.pop());
switch (o) {
case 'add1':
n.iadd(1);
break;
case 'sub1':
n.isub(1);
break;
case 'negate':
n = n.neg();
break;
case 'abs':
if (n.cmpn(0) < 0)
n = n.neg();
break;
case 'not':
n = n.cmpn(0) === 0;
break;
case 'noteq0':
n = n.cmpn(0) !== 0;
break;
default:
return false;
}
stack.push(n.toArray());
break;
}
case 'add':
case 'sub':
case 'booland':
case 'boolor':
case 'numeq':
case 'numeqverify':
case 'numneq':
case 'lt':
case 'gt':
case 'lte':
case 'gte':
case 'min':
case 'max': {
switch (o) {
case 'add':
case 'sub':
case 'booland':
case 'boolor':
case 'numeq':
case 'numeqverify':
case 'numneq':
case 'lt':
case 'gt':
case 'lte':
case 'gte':
case 'min':
case 'max':
if (stack.length < 2)
return false;
var n2 = new bn(stack.pop());
var n1 = new bn(stack.pop());
var n = new bn(0);
switch (o) {
case 'add':
n = n1.add(b2);
break;
case 'sub':
n = n1.sub(n2);
break;
case 'booland':
n = n1.cmpn(0) !== 0 && n2.cmpn(0) !== 0;
break;
case 'boolor':
n = n1.cmpn(0) !== 0 || n2.cmpn(0) !== 0;
break;
case 'numeq':
n = n1.cmp(n2) === 0;
break;
case 'numeqverify':
n = n1.cmp(n2) === 0;
break;
case 'numneq':
n = n1.cmp(n2) !== 0;
break;
case 'lt':
n = n1.cmp(n2) < 0;
break;
case 'gt':
n = n1.cmp(n2) > 0;
break;
case 'lte':
n = n1.cmp(n2) <= 0;
break;
case 'gte':
n = n1.cmp(n2) >= 0;
break;
case 'min':
n = n1.cmp(n2) < 0 ? n1 : n2;
break;
case 'max':
n = n1.cmp(n2) > 0 ? n1 : n2;
break;
default:
return false;
}
var res = n.cmpn(0) !== 0;
if (o === 'numeqverify') {
if (!res)
return false;
} else {
stack.push(n.toArray());
// stack.push(res ? [ 1 ] : []);
}
break;
case 'within':
if (stack.length < 3)
return false;
var n3 = new bn(stack.pop());
var n2 = new bn(stack.pop());
var n1 = new bn(stack.pop());
var val = n2.cmp(n1) <= 0 && n1.cmp(n3) < 0;
stack.push(val.cmpn(0) !== 0 ? [ 1 ] : []);
break;
}
var keys = [];
for (var i = 0; i < n; i++) {
var key = stack.pop();
if (!(33 <= key.length && key.length <= 65))
break;
}
case 'codesep': {
lastSep = pc;
break;
}
case 'ripemd160': {
if (stack.length === 0)
return false;
stack.push(utils.ripemd160(stack.pop()));
break;
}
case 'sha1': {
if (stack.length === 0)
return false;
stack.push(utils.sha1(stack.pop()));
break;
}
case 'sha256': {
if (stack.length === 0)
return false;
stack.push(utils.sha256(stack.pop()));
break;
}
case 'hash256': {
if (stack.length === 0)
return false;
stack.push(utils.dsha256(stack.pop()));
break;
}
case 'hash160': {
if (stack.length === 0)
return false;
stack.push(utils.ripesha(stack.pop()));
break;
}
case 'eqverify':
case 'eq': {
if (stack.length < 2)
return false;
var res = utils.isEqual(stack.pop(), stack.pop());
if (o === 'eqverify') {
if (!res)
return false;
} else {
stack.push(res ? [ 1 ] : []);
}
break;
}
case 'checksigverify':
case 'checksig': {
if (!tx || stack.length < 2)
return false;
keys.push(key);
}
var m = stack.pop();
if (m.length !== 1 || !(1 <= m[0] && m[0] <= n))
return false;
m = m[0];
if (stack.length < m + 1)
return false;
// Get signatures
var succ = 0;
for (var i = 0, j = 0; i < m && j < n; i++) {
var pub = stack.pop();
var sig = stack.pop();
var type = sig[sig.length - 1];
if (type !== 1)
if (!constants.rhashType[type & 0x7f])
return false;
var res = false;
for (; !res && j < n; j++)
res = bcoin.ecdsa.verify(tx, sig.slice(0, -1), keys[j]);
if (res)
succ++;
}
// Extra value
stack.pop();
var res = succ >= m;
if (o === 'checkmultisigverify') {
if (!res)
if (!script.isValidSig(sig))
return false;
} else {
stack.push(res ? [ 1 ] : []);
var subscript = s.slice(lastSep + 1);
var hash = tx.subscriptHash(index, subscript, type);
var res = script.verify(hash, sig.slice(0, -1), pub);
if (o === 'checksigverify') {
if (!res)
return false;
} else {
stack.push(res ? [ 1 ] : []);
}
break;
}
case 'checkmultisigverify':
case 'checkmultisig': {
if (!tx || stack.length < 3)
return false;
var n = stack.pop();
if (n.length !== 1 || !(1 <= n[0] && n[0] <= 3))
return false;
n = n[0] || 0;
if (stack.length < n + 1)
return false;
var keys = [];
for (var i = 0; i < n; i++) {
var key = stack.pop();
if (!(33 <= key.length && key.length <= 65))
return false;
keys.push(key);
}
var m = stack.pop();
if (m.length !== 1 || !(1 <= m[0] && m[0] <= n))
return false;
m = m[0] || 0;
if (stack.length < m + 1)
return false;
// Get signatures
var succ = 0;
for (var i = 0; i < m; i++) {
var sig = stack.pop();
var type = sig[sig.length - 1];
if (!constants.rhashType[type & 0x7f])
return false;
var subscript = s.slice(lastSep + 1);
var hash = tx.subscriptHash(index, subscript, type);
if (!script.isValidSig(sig))
return false;
// Strict order:
var res = script.verify(hash, sig.slice(0, -1), keys.pop());
if (res)
succ++;
}
// Extra value
stack.pop();
var res = succ >= m;
if (o === 'checkmultisigverify') {
if (!res)
return false;
} else {
stack.push(res ? [ 1 ] : []);
}
break;
}
case 'checklocktimeverify': {
// OP_CHECKLOCKTIMEVERIFY = OP_NOP2
// input: [[], sig1, sig2, 1]
// prev_out: [[lock], 'checklocktimeverify', 'drop',
// 'dup', 'hash160', pubkey, 'equalverify', 'checksig']
if (!tx || stack.length === 0)
return false;
var lock = new bn(stack[stack.length - 1]).toNumber();
if (lock < 0)
return false;
var threshold = constants.locktimeThreshold;
if (!(
(tx.lock < threshold && lock < threshold) ||
(tx.lock >= threshold && lock >= threshold)
)) {
return false;
}
if (lock > tx.lock)
return false;
if (!tx.inputs[index] || tx.inputs[index].seq === 0xffffffff)
return false;
break;
}
case 'eval_': {
// OP_EVAL = OP_NOP1
// var evalScript = script.decode(stack.pop());
// if (!Array.isArray(evalScript))
// return false;
// var res = script.execute(evalScript, stack, tx, index);
// if (!res)
// return false;
break;
}
default: {
// Unknown operation
return false;
}
} else {
// Unknown operation
return false;
}
}
if (stack.length + stack.alt.length > 1000)
return false;
return true;
};
script.multisig = function(keys, m, n) {
if (keys.length < m)
throw new Error('wrong amount of pubkeys for multisig script');
assert(m >= 1 && m <= n);
assert(n >= 1 && n <= 7);
// Format:
// op_[m] [pubkey1-len] [pubkey1] ... op_[n] op_checkmultisig
// Using pushdata ops for m and n:
// return [ [ m ] ].concat(
// keys,
// [ [ n ], 'checkmultisig' ]
// );
// Keys need to be in a predictable order.
keys = keys.sort(function(a, b) {
return new bn(a).cmp(new bn(b)) > 0;
});
// Using OP_1-16 for m and n:
return [ m ].concat(
keys,
[ n, 'checkmultisig' ]
);
};
script.isPubkeyhash = function isPubkeyhash(s, hash) {
if (s.length !== 5)
return false;
@ -253,17 +778,21 @@ script.isMultisig = function isMultisig(s, key) {
return false;
var m = s[0];
if (typeof m === 'number' && m >= 1 && m <= 16)
m = [m];
if (!Array.isArray(m) || m.length !== 1)
return false;
m = m[0];
m = m[0] || 0;
if (s[s.length - 1] !== 'checkmultisig')
return false;
var n = s[s.length - 2];
if (typeof n === 'number' && n >= 1 && n <= 16)
n = [n];
if (!Array.isArray(n) || n.length !== 1)
return false;
n = n[0];
n = n[0] || 0;
if (n + 3 !== s.length)
return false;
@ -291,14 +820,22 @@ script.isPubkeyhashInput = function isPubkeyhashInput(s) {
33 <= s[1].length && s[1].length <= 65;
};
script.isScripthash = function isScripthash(s) {
script.isScripthash = function isScripthash(s, hash) {
if (s.length !== 3)
return false;
return s[0] === 'hash160' &&
Array.isArray(s[1]) &&
s[1].length === 20 &&
s[2] === 'eq';
var ret = s[0] === 'hash160' &&
Array.isArray(s[1]) &&
s[1].length === 20 &&
s[2] === 'eq';
if (!ret)
return false;
if (hash)
return utils.isEqual(s[1], hash);
return true;
};
script.isNullData = function isNullData(s) {
@ -309,3 +846,98 @@ script.isNullData = function isNullData(s) {
Array.isArray(s[1]) &&
s[1].length <= 40;
};
// https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki
/**
* A canonical signature exists of: <30> <total len> <02> <len R> <R> <02> <len S> <S> <hashtype>
* Where R and S are not negative (their first byte has its highest bit not set), and not
* excessively padded (do not start with a 0 byte, unless an otherwise negative number follows,
* in which case a single 0 byte is necessary and even required).
*
* See https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623
*
* This function is consensus-critical since BIP66.
*/
script.isValidSig = function(sig) {
// Empty signature. Not strictly DER encoded, but allowed to provide a
// compact way to provide an invalid signature for use with CHECK(MULTI)SIG
if (sig.length === 0)
return true;
// Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] [sighash]
// * total-length: 1-byte length descriptor of everything that follows,
// excluding the sighash byte.
// * R-length: 1-byte length descriptor of the R value that follows.
// * R: arbitrary-length big-endian encoded R value. It must use the shortest
// possible encoding for a positive integers (which means no null bytes at
// the start, except a single one when the next byte has its highest bit set).
// * S-length: 1-byte length descriptor of the S value that follows.
// * S: arbitrary-length big-endian encoded S value. The same rules apply.
// * sighash: 1-byte value indicating what data is hashed (not part of the DER
// signature)
// Minimum and maximum size constraints.
if (sig.length < 9)
return false;
if (sig.length > 73)
return false;
// A signature is of type 0x30 (compound).
if (sig[0] !== 0x30)
return false;
// Make sure the length covers the entire signature.
if (sig[1] !== sig.length - 3)
return false;
// Extract the length of the R element.
var lenR = sig[3];
// Make sure the length of the S element is still inside the signature.
if (5 + lenR >= sig.length)
return false;
// Extract the length of the S element.
var lenS = sig[5 + lenR];
// Verify that the length of the signature matches the sum of the length
// of the elements.
if (lenR + lenS + 7 !== sig.length)
return false;
// Check whether the R element is an integer.
if (sig[2] !== 0x02)
return false;
// Zero-length integers are not allowed for R.
if (lenR === 0)
return false;
// Negative numbers are not allowed for R.
if (sig[4] & 0x80)
return false;
// Null bytes at the start of R are not allowed, unless R would
// otherwise be interpreted as a negative number.
if (lenR > 1 && (sig[4] === 0x00) && !(sig[5] & 0x80))
return false;
// Check whether the S element is an integer.
if (sig[lenR + 4] !== 0x02)
return false;
// Zero-length integers are not allowed for S.
if (lenS === 0)
return false;
// Negative numbers are not allowed for S.
if (sig[lenR + 6] & 0x80)
return false;
// Null bytes at the start of S are not allowed, unless S would otherwise be
// interpreted as a negative number.
if (lenS > 1 && (sig[lenR + 6] === 0x00) && !(sig[lenR + 7] & 0x80))
return false;
return true;
};

View File

@ -3,6 +3,7 @@ var bn = require('bn.js');
var bcoin = require('../bcoin');
var utils = bcoin.utils;
var assert = utils.assert;
var constants = bcoin.protocol.constants;
function TX(data, block) {
if (!(this instanceof TX))
@ -40,6 +41,10 @@ function TX(data, block) {
// ps = Pending Since
this.ps = this.ts === 0 ? +new Date() / 1000 : 0;
this.change = data.change || null;
this.fee = data.fee || 10000;
this.dust = 5460;
}
module.exports = TX;
@ -56,6 +61,11 @@ TX.prototype.render = function render() {
return bcoin.protocol.framer.tx(this);
};
TX.prototype.input = function input(i, index) {
this._input(i, index);
return this;
};
TX.prototype._input = function _input(i, index) {
if (i instanceof TX)
i = { tx: i, index: index };
@ -87,10 +97,10 @@ TX.prototype._input = function _input(i, index) {
var index = this._inputIndex(hash, index);
if (index !== -1) {
var ex = this.inputs[index];
ex.out.tx = input.out.tx || ex.out.tx;
ex.seq = input.seq || ex.seq;
ex.script = input.script.length ? input.script : ex.script;
input.out.tx = input.out.tx || ex.out.tx;
input.seq = input.seq || ex.seq;
input.script = input.script.length ? input.script : ex.script;
this.inputs[index] = input;
} else {
this.inputs.push(input);
index = this.inputs.length - 1;
@ -111,14 +121,186 @@ TX.prototype._inputIndex = function _inputIndex(hash, index) {
return -1;
};
TX.prototype.input = function input(i, index) {
this._input(i, index);
return this;
TX.prototype.signature = function(input, key, type) {
if (!type)
type = 'all';
if (typeof type === 'string')
type = bcoin.protocol.constants.hashType[type];
// Get the previous output's subscript
var s = input.out.tx.getSubscript(input.out.index);
// Get the hash of the current tx, minus the other inputs, plus the sighash.
var hash = this.subscriptHash(tx.inputs.indexOf(input), s, type);
// Sign the transaction with our one input
var signature = bcoin.ecdsa.sign(hash, key.priv).toDER();
// Add the sighash as a single byte to the signature
signature = signature.concat(type);
return signature;
};
TX.prototype.out = function out(output, value) {
// Build the scriptSigs for inputs, excluding the signatures
TX.prototype.scriptInput = function(input, pub) {
// Get the previous output's subscript
var s = input.out.tx.getSubscript(input.out.index);
// Already has a script template (at least)
if (input.script.length)
return;
// P2PKH and simple tx
if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) {
input.script = [ [], pub ];
return;
}
// NOTE for multisig: Technically we should create m signature slots,
// but we create n signature slots so we can order the signatures properly.
// Multisig
// raw format: OP_FALSE [sig-1] [sig-2] ...
if (bcoin.script.isMultisig(s)) {
input.script = [ [] ];
var n = s[s.length - 2];
// If using pushdata instead of OP_1-16:
if (Array.isArray(n))
n = n[0] || 0;
for (var i = 0; i < n; i++)
input.script[i + 1] = [];
return;
}
// P2SH multisig
// p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script]
if (bcoin.script.isScripthash(s)) {
input.script = [ [] ];
var redeem = bcoin.script.decode(pub);
var n = redeem[redeem.length - 2];
// If using pushdata instead of OP_1-16:
if (Array.isArray(n))
n = n[0] || 0;
for (var i = 0; i < n; i++)
input.script[i + 1] = [];
// P2SH requires the redeem script after signatures
input.script.push(pub);
return;
}
throw new Error('scriptInput(): could not identify prev_out type');
};
// Sign the now-built scriptSigs
TX.prototype.signInput = function(input, key, type) {
if (!type)
type = 'all';
if (typeof type === 'string')
type = bcoin.protocol.constants.hashType[type];
// Get the previous output's subscript
var s = input.out.tx.getSubscript(input.out.index);
// Get the hash of the current tx, minus the other inputs, plus the sighash.
var hash = this.subscriptHash(this.inputs.indexOf(input), s, type);
// Sign the transaction with our one input
var signature = bcoin.ecdsa.sign(hash, key.priv).toDER();
// Add the sighash as a single byte to the signature
signature = signature.concat(type);
// P2PKH and simple tx
if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) {
input.script[0] = signature;
return;
}
// Multisig
// raw format: OP_FALSE [sig-1] [sig-2] ...
// p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script]
if (bcoin.script.isMultisig(s) || bcoin.script.isScripthash(s)) {
var len = input.script.length;
var redeem;
if (bcoin.script.isScripthash(s)) {
len--;
redeem = bcoin.script.decode(input.script[input.script.length - 1]);
} else {
redeem = s;
}
var m = redeem[0];
// If using pushdata instead of OP_1-16:
if (Array.isArray(m))
m = m[0] || 0;
var keys = redeem.slice(1, -2);
var pub = key.getPublic(true, 'array');
var pubn = key.getPublic(false, 'array');
// Find the key index so we can place
// the signature in the same index.
for (var ki = 0; ki < keys.length; ki++) {
if (utils.isEqual(pub, keys[ki]) || utils.isEqual(pubn, keys[ki]))
break;
}
if (ki === keys.length)
throw new Error('Public key is not in the prev_out script');
if (ki + 1 > len - 1)
throw new Error('No signature slot available');
// Add our signature to the correct slot
// and count the total number of signatures.
var totalSigs = 0;
for (var i = 1; i < len; i++) {
if (Array.isArray(input.script[i]) && input.script[i].length) {
totalSigs++;
continue;
}
if (i - 1 === ki) {
if (totalSigs >= m)
continue;
input.script[i] = signature;
totalSigs++;
}
}
// All signatures added. Finalize by removing empty slots.
if (totalSigs >= m) {
for (var i = len - 1; i >= 1; i--) {
if (Array.isArray(input.script[i]) && !input.script[i].length)
input.script.splice(i, 1);
}
}
return;
}
throw new Error('signInput(): could not identify prev_out type');
};
// Build the scriptSig and sign it
TX.prototype.scriptSig = function(input, key, pub, type) {
// Build script for input
this.scriptInput(input, pub);
// Sign input
this.signInput(input, key, type);
return input.script;
};
TX.prototype.output = function output(output, value) {
if (output instanceof bcoin.wallet)
output = output.getAddress();
if (typeof output === 'string') {
output = {
address: output,
@ -126,34 +308,86 @@ TX.prototype.out = function out(output, value) {
};
}
var script = output.script ? output.script.slice() : [];
this.outputs.push({
value: new bn(output.value),
script: this.scriptOutput(output)
});
// Multisig script if given addresses
if (Array.isArray(output.keys || output.address)) {
var keys = output.keys || output.address;
return this;
};
// compat
TX.prototype.out = TX.prototype.output;
TX.prototype.scriptOutput = function(options) {
var script = options.script ? options.script.slice() : [];
if (Array.isArray(options.keys || options.address)) {
// Raw multisig transaction
// https://github.com/bitcoin/bips/blob/master/bip-0010.mediawiki
// https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki
// https://github.com/bitcoin/bips/blob/master/bip-0019.mediawiki
// [required-sigs] [pubkey-hash1] [pubkey-hash2] ... [number-of-keys] checkmultisig
var keys = options.keys || options.address;
if (keys === options.address) {
keys = keys.map(function(address) {
return bcoin.wallet.addr2hash(address, 'normal');
});
}
keys = keys.map(function(key) {
if (typeof key === 'string')
return utils.toKeyArray(key);
return key;
});
// compat:
options.m = options.minSignatures || options.m;
var m = options.m || keys.length;
var n = options.n || keys.length;
assert(m >= 1 && m <= n);
if (options.hash)
assert(n >= 1 && n <= 7);
else
assert(n >= 1 && n <= 3);
script = bcoin.script.multisig(keys, m, n);
} else if (bcoin.wallet.validateAddress(options.address, 'p2sh')) {
// p2sh transaction
// https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki
// hash160 [20-byte-redeemscript-hash] equal
script = [
[ output.minSignatures || keys.length ]
].concat(
keys,
[ [ keys.length ], 'checkmultisig' ]
);
// Default script if given address
} else if (output.address) {
'hash160',
bcoin.wallet.addr2hash(options.address, 'p2sh'),
'eq'
];
} else if (options.address) {
// p2pkh transaction
// dup hash160 [pubkey-hash] equalverify checksig
script = [
'dup',
'hash160',
bcoin.wallet.addr2hash(output.address),
bcoin.wallet.addr2hash(options.address, 'normal'),
'eqverify',
'checksig'
];
}
this.outputs.push({
value: new bn(output.value),
script: script
});
// make it p2sh
if (options.hash) {
var redeem = script;
var hash = utils.ripesha(bcoin.script.encode(redeem));
script = [
'hash160',
hash,
'eq'
];
script.redeem = redeem;
}
return this;
return script;
};
TX.prototype.getSubscript = function getSubscript(index) {
@ -167,13 +401,14 @@ TX.prototype.getSubscript = function getSubscript(index) {
TX.prototype.subscriptHash = function subscriptHash(index, s, type) {
var copy = this.clone();
if (typeof type === 'string')
type = bcoin.protocol.constants.hashType[type];
copy.inputs.forEach(function(input, i) {
input.script = index === i ? s : [];
});
var verifyStr = copy.render();
verifyStr = verifyStr.concat(
bcoin.protocol.constants.hashType[type], 0, 0, 0
);
utils.writeU32(verifyStr, type, verifyStr.length);
var hash = utils.dsha256(verifyStr);
return hash;
@ -184,6 +419,9 @@ TX.prototype.verify = function verify(index, force) {
if (!force && this.ts !== 0)
return true;
if (this.inputs.length === 0)
return false;
return this.inputs.every(function(input, i) {
if (index !== undefined && index !== i)
return true;
@ -193,20 +431,46 @@ TX.prototype.verify = function verify(index, force) {
assert(input.out.tx.outputs.length > input.out.index);
var subscript = input.out.tx.getSubscript(input.out.index);
var hash = this.subscriptHash(i, subscript, 'all');
var stack = [];
bcoin.script.execute(input.script, stack);
var prev = input.out.tx.outputs[input.out.index].script;
var res = bcoin.script.execute(prev, stack, hash);
if (bcoin.script.isScripthash(prev)) {
// p2sh transactions cannot have anything
// other than pushdata ops in the scriptSig
var push = !input.script.slice(1).every(Array.isArray);
if (push)
return false;
}
bcoin.script.execute(input.script, stack, this, i);
var res = bcoin.script.execute(prev, stack, this, i);
if (!res)
return false;
return stack.length > 0 && utils.isEqual(stack.pop(), [ 1 ]);
// Might be necessary for arithmetic:
// if (stack.length === 0 || new bn(stack.pop()).cmp(0) !== 0)
if (stack.length === 0 || !utils.isEqual(stack.pop(), [ 1 ]))
return false;
if (bcoin.script.isScripthash(prev)) {
var redeem = input.script[input.script.length - 1];
if (!Array.isArray(redeem))
return false;
redeem = bcoin.script.decode(redeem);
res = bcoin.script.execute(redeem, stack, this, i);
if (!res)
return false;
}
return true;
}, this);
};
TX.prototype.isCoinbase = function isCoinbase() {
return this.inputs.length === 1 && +this.inputs[0].out.hash === 0;
};
TX.prototype.maxSize = function maxSize() {
// Create copy with 0-script inputs
var copy = this.clone();
@ -217,9 +481,14 @@ TX.prototype.maxSize = function maxSize() {
var size = copy.render().length;
// Add size for signatures and public keys
copy.inputs.forEach(function(input) {
var s = input.out.tx.outputs[input.out.index].script;
if (bcoin.script.isPubkeyhash(s)) {
copy.inputs.forEach(function(input, i) {
// Get the previous output's script
// var s = input.out.tx.outputs[input.out.index].script;
// Get the previous output's subscript
var s = input.out.tx.getSubscript(input.out.index);
if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) {
// Signature + len
size += 74;
// Pub key + len
@ -227,23 +496,170 @@ TX.prototype.maxSize = function maxSize() {
return;
}
// Multisig
// Empty byte
size += 1;
// Signature + len
size += 74;
});
if (bcoin.script.isMultisig(s)) {
// Multisig
// Empty byte
size += 1;
// Signature + len
var m = s[0];
// If using pushdata instead of OP_1-16:
if (Array.isArray(m))
m = m[0] || 0;
assert(m >= 1 && m <= 3);
size += 74 * m;
return;
}
if (bcoin.script.isScripthash(s)) {
var script = this.inputs[i].script;
var redeem, m, n;
if (script.length) {
redeem = bcoin.script.decode(script[script.length - 1]);
m = redeem[0];
n = redeem[redeem.length - 2];
// If using pushdata instead of OP_1-16:
if (Array.isArray(m))
m = m[0] || 0;
if (Array.isArray(n))
n = n[0] || 0;
} else {
// May end up in a higher fee if we
// do not have the redeem script available.
m = 7;
n = 7;
}
assert(m >= 1 && m <= n);
assert(n >= 1 && n <= 7);
// Multisig
// Empty byte
size += 1;
// Signature + len
size += 74 * m;
// Redeem script
// m byte
size += 1;
// 1 byte length + 65 byte pubkey
size += 66 * n;
// n byte
size += 1;
// checkmultisig byte
size += 1;
return;
}
}, this);
return size;
};
// Building a TX:
// 1. Add outputs:
// - this.output({ address: ..., value: ... });
// - this.output({ address: ..., value: ... });
// 2. Add inputs with utxos and change output:
// - this.fillUnspent(unspentItems, [changeAddr]);
// 3. Fill input scripts (for each input):
// - this.scriptInput(input, pub)
// - this.signInput(input, key, [sigHashType])
TX.prototype.utxos = function utxos(unspent) {
// NOTE: tx should be prefilled with all outputs
var cost = this.funds('out');
// Use initial fee for starters
var fee = 1;
// total = cost + fee
var total = cost.add(new bn(this.fee));
var inputs = this.inputs.slice();
var utxos = [];
var lastAdded = 0;
function addInput(unspent, i) {
// Add new inputs until TX will have enough funds to cover both
// minimum post cost and fee
var index = this._input(unspent);
utxos.push(this.inputs[index]);
lastAdded++;
return this.funds('in').cmp(total) < 0;
}
// Transfer `total` funds maximum
// var unspent = wallet.unspent();
unspent.every(addInput, this);
// Add dummy output (for `left`) to calculate maximum TX size
this.output({ address: null, value: new bn(0) });
// Change fee value if it is more than 1024 bytes
// (10000 satoshi for every 1024 bytes)
do {
// Calculate maximum possible size after signing
var byteSize = this.maxSize();
var addFee = Math.ceil(byteSize / 1024) - fee;
total.iadd(new bn(addFee * this.fee));
fee += addFee;
// Failed to get enough funds, add more inputs
if (this.funds('in').cmp(total) < 0)
unspent.slice(lastAdded).every(addInput, this);
} while (this.funds('in').cmp(total) < 0 && lastAdded < unspent.length);
// Still failing to get enough funds
if (this.funds('in').cmp(total) < 0) {
this.inputs = inputs;
this.outputs.pop();
this.cost = total;
return null;
}
// How much money is left after sending outputs
var left = this.funds('in').sub(total);
// Clear the tx of everything we added.
this.inputs = inputs;
this.outputs.pop();
this.cost = total;
// Return necessary utxos and change.
return {
utxos: utxos,
change: left,
cost: total
};
};
TX.prototype.fillUnspent = function fillUnspent(unspent, change) {
var result = this.utxos(unspent);
if (!result)
return result;
result.utxos.forEach(function(utxo) {
this.input(utxo, null);
}, this);
// Not enough money, transfer everything to owner
if (result.change.cmpn(this.dust) < 0) {
// NOTE: that this output is either `postCost` or one of the `dust` values
this.outputs[this.outputs.length - 1].value.iadd(result.change);
} else {
this.output({
address: change || this.change,
value: result.change
});
}
return result;
};
TX.prototype.inputAddrs = function inputAddrs() {
return this.inputs.filter(function(input) {
return bcoin.script.isPubkeyhashInput(input.script);
}).map(function(input) {
var pub = input.script[1];
var hash = utils.ripesha(pub);
return bcoin.wallet.hash2addr(hash);
return bcoin.wallet.hash2addr(hash, 'normal');
});
};

View File

@ -2,6 +2,7 @@ var utils = exports;
var bn = require('bn.js');
var hash = require('hash.js');
var util = require('util');
function toArray(msg, enc) {
if (Array.isArray(msg))
@ -118,6 +119,14 @@ utils.fromBase58 = function fromBase58(str) {
return z.concat(res.toArray());
};
utils.ripemd160 = function ripemd160(data, enc) {
return hash.ripemd160().update(data, enc).digest();
};
utils.sha1 = function sha1(data, enc) {
return hash.sha1().update(data, enc).digest();
};
utils.ripesha = function ripesha(data, enc) {
return hash.ripemd160().update(utils.sha256(data, enc)).digest();
};
@ -417,6 +426,8 @@ RequestCache.prototype.fullfill = function fullfill(id, err, data) {
utils.asyncify = function asyncify(fn) {
return function _asynicifedFn(err, data1, data2) {
if (!fn)
return;
utils.nextTick(function() {
fn(err, data1, data2);
});
@ -466,3 +477,56 @@ utils.isIP = function(ip) {
return 0;
};
utils.isHex = function(msg) {
return typeof msg === 'string' && /^[0-9a-f]+$/i.test(msg);
};
utils.toKeyArray = function(msg) {
if (typeof msg !== 'string')
return msg;
if (utils.isHex(msg))
return utils.toArray(msg, 'hex');
return utils.fromBase58(msg);
};
utils.inspect = function(obj) {
return typeof obj !== 'string'
? util.inspect(obj, null, 20, true)
: obj;
};
utils.print = function(msg) {
return typeof msg === 'object'
? process.stdout.write(utils.inspect(msg) + '\n')
: console.log.apply(console, arguments);
};
utils.debug = function() {
var args = Array.prototype.slice.call(arguments);
args[0] = '\x1b[31m' + args[0] + '\x1b[m';
return utils.print.apply(null, args);
};
utils.merge = function(target) {
var args = Array.prototype.slice.call(arguments, 1);
args.forEach(function(obj) {
Object.keys(obj).forEach(function(key) {
target[key] = obj[key];
});
});
return target;
};
utils.fromBTC = function(btc) {
var satoshi = new bn(+btc || 0);
satoshi.imuln(100000000);
return satoshi;
};
utils.ntoBTC = function(satoshi) {
satoshi = new bn(Math.floor(+satoshi || 0).toString(16), 16);
return bcoin.utils.toBTC(satoshi);
};

View File

@ -5,6 +5,8 @@ var inherits = require('inherits');
var EventEmitter = require('events').EventEmitter;
var utils = bcoin.utils;
var assert = utils.assert;
var constants = bcoin.protocol.constants;
var network = bcoin.protocol.network;
function Wallet(options, passphrase) {
if (!(this instanceof Wallet))
@ -22,6 +24,7 @@ function Wallet(options, passphrase) {
if (!options)
options = {};
this.options = options;
this.compressed = typeof options.compressed !== 'undefined' ?
options.compressed : true;
this.storage = options.storage;
@ -29,18 +32,45 @@ function Wallet(options, passphrase) {
this.loaded = false;
this.lastTs = 0;
if (options.passphrase) {
if (options.priv instanceof bcoin.hd.priv) {
this.hd = options.priv;
this.key = this.hd;
} else if (options.pub instanceof bcoin.hd.pub) {
this.hd = options.pub;
this.key = this.hd;
} else if (options.hd) {
this.hd = bcoin.hd.priv(options);
this.key = this.hd;
} else if (options.key) {
if ((options.key instanceof bcoin.hd.priv)
|| (options.key instanceof bcoin.hd.pub)) {
this.hd = options.key;
this.key = options.key;
} else {
this.key = options.key;
}
} else if (options.passphrase) {
this.key = bcoin.ecdsa.genKeyPair({
pers: options.scope,
entropy: hash.sha256().update(options.passphrase).digest()
});
} else if (options.priv || options.pub) {
this.key = bcoin.ecdsa.keyPair(options.priv || options.pub, 'hex');
this.key = bcoin.ecdsa.keyPair({
priv: options.priv,
pub: options.pub
});
} else {
this.key = bcoin.ecdsa.genKeyPair();
}
this.prefix = 'bt/' + this.getAddress() + '/';
this.addressType = 'normal';
this.sharedKeys = [];
this.m = 1;
this.n = 1;
this.multisig(options.multisig || {});
this.prefix = 'bt/' + this.getOwnAddress() + '/';
this.tx = new bcoin.txPool(this);
// Just a constants, actually
@ -84,6 +114,48 @@ Wallet.prototype._init = function init() {
});
};
Wallet.prototype.multisig = function multisig(options) {
var pub = this.getOwnPublicKey();
options.type = options.type || options.addressType;
options.keys = options.keys || options.sharedKeys;
this.addressType = options.type || 'normal';
// Multisig
this.sharedKeys = (options.keys || []).map(utils.toKeyArray);
this.m = options.m || 1;
this.n = options.n || 1;
this.sharedKeys = this.sharedKeys.filter(function(key) {
return !utils.isEqual(key, pub);
});
// Use p2sh multisig by default
if (!options.addressType && this.sharedKeys.length)
this.addressType = 'p2sh';
if (this.m < 1 || this.m > this.n)
throw new Error('m ranges between 1 and n');
if (this.n < 1 || this.n > 7)
throw new Error('n ranges between 1 and 7');
if (this.sharedKeys.length < this.m - 1)
throw new Error(this.m + ' public keys required');
};
Wallet.prototype.derive = function derive() {
var options = this.options;
if (!this.hd)
throw new Error('wallet is not HD');
options.priv = this.hd.derive.apply(this.hd, arguments);
return bcoin.wallet(options);
};
Wallet.prototype.getPrivateKey = function getPrivateKey(enc) {
var priv = this.key.getPrivate();
if (priv)
@ -95,7 +167,7 @@ Wallet.prototype.getPrivateKey = function getPrivateKey(enc) {
if (enc === 'base58') {
// We'll be using ncompressed public key as an address
var arr = [ 128 ];
var arr = [ network.prefixes.privkey ];
// 0-pad key
while (arr.length + priv.length < 33)
@ -110,39 +182,99 @@ Wallet.prototype.getPrivateKey = function getPrivateKey(enc) {
}
};
Wallet.prototype.getPublicKey = function getPublicKey(enc) {
var pub = this.key.getPublic(this.compressed, 'array');
Wallet.prototype.getFullPublicKey = function getFullPublicKey(enc) {
var pub = this.getOwnPublicKey();
if (this.addressType === 'p2sh') {
var keys = this.getPublicKeys();
pub = bcoin.script.encode(bcoin.script.multisig(keys, this.m, this.n));
}
if (enc === 'base58')
return utils.toBase58(pub);
else if (enc === 'hex')
return utils.toHex(pub);
else
return pub;
};
Wallet.prototype.getOwnPublicKey = function getOwnPublicKey(enc) {
var pub = this.key.getPublic(this.compressed, 'array');
if (enc === 'base58')
return utils.toBase58(pub);
else if (enc === 'hex')
return utils.toHex(pub);
else
return pub;
};
Wallet.prototype.getPublicKey = function getPublicKey(enc) {
return this.getFullPublicKey(enc);
};
Wallet.prototype.getPublicKeys = function() {
var pub = this.getOwnPublicKey();
this.sharedKeys = this.sharedKeys.filter(function(key) {
return !utils.isEqual(key, pub);
});
var keys = this.sharedKeys.slice().map(utils.toKeyArray);
keys.push(pub);
// Keys need to be in a predictable order.
keys = keys.sort(function(a, b) {
return new bn(a).cmp(new bn(b)) > 0;
});
return keys;
};
Wallet.prototype.getFullHash = function getFullHash() {
return utils.ripesha(this.getFullPublicKey());
};
Wallet.prototype.getFullAddress = function getFullAddress() {
return Wallet.hash2addr(this.getFullHash(), this.addressType);
};
Wallet.prototype.getOwnHash = function getOwnHash() {
return utils.ripesha(this.getOwnPublicKey());
};
Wallet.prototype.getOwnAddress = function getOwnAddress() {
return Wallet.hash2addr(this.getOwnHash(), this.addressType);
};
Wallet.prototype.getHash = function getHash() {
return utils.ripesha(this.getPublicKey());
return utils.ripesha(this.getFullPublicKey());
};
Wallet.prototype.getAddress = function getAddress() {
return Wallet.hash2addr(this.getHash());
return Wallet.hash2addr(this.getFullHash(), this.addressType);
};
Wallet.hash2addr = function hash2addr(hash) {
Wallet.hash2addr = function hash2addr(hash, version) {
hash = utils.toArray(hash, 'hex');
// Add version
hash = [ 0 ].concat(hash);
version = network.prefixes[version || 'normal'];
hash = [ version ].concat(hash);
var addr = hash.concat(utils.checksum(hash));
return utils.toBase58(addr);
};
Wallet.addr2hash = function addr2hash(addr) {
Wallet.addr2hash = function addr2hash(addr, version) {
if (!Array.isArray(addr))
addr = utils.fromBase58(addr);
version = network.prefixes[version || 'normal'];
if (addr.length !== 25)
return [];
if (addr[0] !== 0)
if (addr[0] !== version)
return [];
var chk = utils.checksum(addr.slice(0, -4));
@ -152,15 +284,19 @@ Wallet.addr2hash = function addr2hash(addr) {
return addr.slice(1, -4);
};
Wallet.prototype.validateAddress = function validateAddress(addr) {
var p = Wallet.addr2hash(addr);
Wallet.prototype.validateAddress = function validateAddress(addr, version) {
if (!addr)
return false;
var p = Wallet.addr2hash(addr, version);
return p.length !== 0;
};
Wallet.validateAddress = Wallet.prototype.validateAddress;
Wallet.prototype.ownOutput = function ownOutput(tx, index) {
var hash = this.getHash();
var key = this.getPublicKey();
var scriptHash = this.getFullHash();
var hash = this.getOwnHash();
var key = this.getOwnPublicKey();
var outputs = tx.outputs.filter(function(output, i) {
if (index !== undefined && index !== i)
return false;
@ -176,6 +312,9 @@ Wallet.prototype.ownOutput = function ownOutput(tx, index) {
if (bcoin.script.isMultisig(s, key))
return true;
if (bcoin.script.isScripthash(s, scriptHash))
return true;
return false;
}, this);
if (outputs.length === 0)
@ -185,8 +324,9 @@ Wallet.prototype.ownOutput = function ownOutput(tx, index) {
};
Wallet.prototype.ownInput = function ownInput(tx, index) {
var hash = this.getHash();
var key = this.getPublicKey();
var scriptHash = this.getFullHash();
var hash = this.getOwnHash();
var key = this.getOwnPublicKey();
var inputs = tx.inputs.filter(function(input, i) {
if (index !== undefined && index !== i)
@ -206,6 +346,9 @@ Wallet.prototype.ownInput = function ownInput(tx, index) {
if (bcoin.script.isMultisig(s, key))
return true;
if (bcoin.script.isScripthash(s, scriptHash))
return true;
return false;
}, this);
if (inputs.length === 0)
@ -214,35 +357,22 @@ Wallet.prototype.ownInput = function ownInput(tx, index) {
return inputs;
};
Wallet.prototype.sign = function sign(tx, type, inputs, off) {
Wallet.prototype.sign = function sign(tx, type, inputs) {
if (!type)
type = 'all';
assert.equal(type, 'all');
if (!off)
off = 0;
var pub = this.getFullPublicKey();
var key = this.key;
var pub = this.getPublicKey();
inputs = inputs || tx.inputs;
// Add signature script to each input
inputs = inputs.filter(function(input, i) {
inputs = inputs.filter(function(input) {
// Filter inputs that this wallet own
if (!input.out.tx || !this.ownOutput(input.out.tx))
return false;
var s = input.out.tx.getSubscript(input.out.index);
var hash = tx.subscriptHash(off + i, s, type);
var signature = bcoin.ecdsa.sign(hash, this.key).toDER();
signature = signature.concat(bcoin.protocol.constants.hashType[type]);
if (bcoin.script.isPubkeyhash(s)) {
input.script = [ signature, pub ];
return true;
}
// Multisig
input.script = [ [], signature ];
tx.scriptSig(input, key, pub, type);
return true;
}, this);
@ -272,89 +402,40 @@ Wallet.prototype.balance = function balance() {
Wallet.prototype.fill = function fill(tx, cb) {
cb = utils.asyncify(cb);
// NOTE: tx should be prefilled with all outputs
var cost = tx.funds('out');
// Use initial fee for starters
var fee = 1;
// total = cost + fee
var total = cost.add(new bn(this.fee));
var lastAdded = 0;
function addInput(unspent, i) {
// Add new inputs until TX will have enough funds to cover both
// minimum post cost and fee
tx.input(unspent);
lastAdded++;
return tx.funds('in').cmp(total) < 0;
}
// Transfer `total` funds maximum
var unspent = this.unspent();
unspent.every(addInput, this);
// Add dummy output (for `left`) to calculate maximum TX size
tx.out(this, new bn(0));
// Change fee value if it is more than 1024 bytes
// (10000 satoshi for every 1024 bytes)
do {
// Calculate maximum possible size after signing
var byteSize = tx.maxSize();
var addFee = Math.ceil(byteSize / 1024) - fee;
total.iadd(new bn(addFee * this.fee));
fee += addFee;
// Failed to get enough funds, add more inputs
if (tx.funds('in').cmp(total) < 0)
unspent.slice(lastAdded).every(addInput, this);
} while (tx.funds('in').cmp(total) < 0 && lastAdded < unspent.length);
// Still failing to get enough funds, notify caller
if (tx.funds('in').cmp(total) < 0) {
var result = tx.fillUnspent(this.unspent(), this.getAddress());
if (!result) {
var err = new Error('Not enough funds');
err.minBalance = total;
return cb(err);
err.minBalance = tx.cost;
cb(err);
return null;
}
// How much money is left after sending outputs
var left = tx.funds('in').sub(total);
// Not enough money, transfer everything to owner
if (left.cmpn(this.dust) < 0) {
// NOTE: that this output is either `postCost` or one of the `dust` values
tx.outputs[tx.outputs.length - 2].value.iadd(left);
left = new bn(0);
}
// Change or remove last output if there is some money left
if (left.cmpn(0) === 0)
tx.outputs.pop();
else
tx.outputs[tx.outputs.length - 1].value = left;
// Sign transaction
this.sign(tx);
cb(null, tx);
return tx;
};
Wallet.prototype.toJSON = function toJSON() {
return {
v: 1,
type: 'wallet',
pub: this.getPublicKey('base58'),
network: network.type,
pub: this.getOwnPublicKey('base58'),
priv: this.getPrivateKey('base58'),
tx: this.tx.toJSON()
tx: this.tx.toJSON(),
multisig: {
type: this.addressType,
keys: this.sharedKeys.map(utils.toBase58),
m: this.m,
n: this.n
}
};
};
Wallet.fromJSON = function fromJSON(json) {
assert.equal(json.v, 1);
assert.equal(json.type, 'wallet');
if (json.network)
assert.equal(json.network, network.type);
var priv;
var pub;
@ -363,7 +444,7 @@ Wallet.fromJSON = function fromJSON(json) {
if (json.priv) {
var key = bcoin.utils.fromBase58(json.priv);
assert(utils.isEqual(key.slice(-4), utils.checksum(key.slice(0, -4))));
assert.equal(key[0], 128);
assert.equal(key[0], network.prefixes.privkey);
key = key.slice(0, -4);
if (key.length === 34) {
@ -379,10 +460,14 @@ Wallet.fromJSON = function fromJSON(json) {
compressed = pub[0] !== 0x04;
}
if (json.multisig && json.multisig.keys)
json.multisig.keys = json.multisig.keys.map(utils.toKeyArray);
var w = new Wallet({
priv: priv,
pub: pub,
compressed: compressed
compressed: compressed,
multisig: json.multisig
});
w.tx.fromJSON(json.tx);

View File

@ -22,9 +22,9 @@
"homepage": "https://github.com/indutny/bcoin",
"dependencies": {
"async": "^0.8.0",
"bn.js": "^0.10.0",
"elliptic": "^0.14.1",
"hash.js": "^0.2.0",
"bn.js": "^4.5.0",
"elliptic": "^6.0.2",
"hash.js": "^1.0.3",
"inherits": "^2.0.1"
},
"devDependencies": {

95
test/hd-test.js Normal file
View File

@ -0,0 +1,95 @@
// var assert = require('assert');
var bn = require('bn.js');
var bcoin = require('../');
var utils = bcoin.utils;
var assert = utils.assert;
describe('HD', function() {
var phrase = 'volume doll flush federal inflict tomato result property total curtain shield aisle';
var seed = '5559092716434b83f158bffb51337a944529ae30d7e62d46d3be0c66fa4b36e8d60ccfd2c976b831885dc9df9ac3716ee4bf90003f25621070a49cbea58f528b';
var master_priv = 'xprv9s21ZrQH143K35zTejeVRhkXgegDFUVpoh8Mxs2BQmXEB4w9SZ1CuoJPuQ2KGQrS1ZF3Pk7V7KWHn7FqR2JbAE9Bh8PURnrFnrmArj4kxos';
var master_pub = 'xpub661MyMwAqRbcFa4vkmBVnqhGEgWhewDgAv3xmFRny74D3sGHz6KTTbcskg2vZEMbEwxc4oaR435oczhSu4GdNwhwiVewcewU8A1Rr6HehAU';
var child1_priv = 'xprv9v414VeuxMoGt3t7jzkPni79suCfkgFwjxG38X2wgfg2mrYtV4Bhj3prhDDCcBiJrz9n4xLYoDtBFRuQmreVLKzmiZAqvbGpx5q4yHfzfah';
var child1_pub = 'xpub693MU1BonjMa6Xxar2HQ9r3tRw3AA8yo7BBdvuSZF1D1eet32bVxGr9LYViWMtaLfQaa2StXeUmDG5VELFkU9pc3yfTzCk61WQJdR6ezj7a';
var child2_pub = 'xpub693MU1BonjMa8MMoz9opJhrFejcXcGmhMP9gzySLsip4Dz1UrSLT4i2pAimHDyM2onW2H2L2HkbwrZqoizQLMoErXu8mPYxDf8tJUBAfBuT';
var child3_priv = 'xprv9v414VeuxMoGusHLt8GowZuX6hn3Cp3qzAE6Cb2jKPH5MBgLJu2CWuiLKTdWV8WoNFYvpCcBfbpWfeyEQ8zytZW5qy39roTcugBGUkeAvCc';
var child3_pub = 'xpub693MU1BonjMa8MMoz9opJhrFejcXcGmhMP9gzySLsip4Dz1UrSLT4i2pAimHDyM2onW2H2L2HkbwrZqoizQLMoErXu8mPYxDf8tJUBAfBuT';
var child4_priv = 'xprv9v414VeuxMoGyViVYuzEN5vLDzff3nkrH5Bf4KzD1iTeY855Q4cCc6xPPNoc6MJcsqqRQiGqR977cEEGK2mhVp7ALKHqY1icEw3Q9UmfQ1v';
var child4_pub = 'xpub693MU1BonjMaBynxewXEjDs4n2W9TFUheJ7FriPpa3zdQvQDwbvT9uGsEebvioAcYbtRUU7ge4yVgj8WDLrwtwuXKTWiieFoYX2B1JYUEfK';
var child5_priv = 'xprv9xaK29Nm86ytEwsV9YSsL3jWYR6KtZYY3cKdjAbxHrwKyxH9YWoxvqKwtgQmExGpxAEDrwB4WK9YG1iukth3XiSgsxXLK1W3NB31gLee8fi';
var child5_pub = 'xpub6BZfReuexUYBTRwxFZyshBgF6SvpJ2GPQqFEXZ1ZrCUJrkcJ648DUdeRjx9QiNQxQvPcHYV3rGkvuExFQbVRS4kU5ynx4fAsWWhHgyPh1pP';
var child6_priv = 'xprv9xaK29Nm86ytGx9uDhNKUBjvbJ1sAEM11aYxGQS66Rmg6oHwy7HbB6kWwMHvukzdbPpGhfNXhZgePWFHm1DCh5PACPFywJJKr1AnUJTLjUc';
var child6_pub = 'xpub6BZfReuexUYBVSENKiuKqKgf9KrMZh4rNoUZ4nqhemJeybd6Webqiu4zndBwa9UB4Jvr5jB5Bcgng6reXAKCuDiVm7zhzJ13BUDBiM8HidZ';
var master, child1, child2, child3, child4, child5, child6;
it('should create a pbkdf2 seed', function() {
var checkSeed = utils.toHex(bcoin.hd.pbkdf2(phrase, 'mnemonic' + 'foo', 2048, 64));
assert.equal(checkSeed, seed);
});
it('should create master private key', function() {
master = bcoin.hd.priv({ seed: seed });
assert.equal(master.xprivkey, master_priv);
assert.equal(master.xpubkey, master_pub);
});
it('should derive(0) child from master', function() {
child1 = master.derive(0);
assert.equal(child1.xprivkey, child1_priv);
assert.equal(child1.xpubkey, child1_pub);
});
it('should derive(1) child from master public key', function() {
child2 = master.hdpub.derive(1);
assert.equal(child2.xpubkey, child2_pub);
});
it('should derive(1) child from master', function() {
child3 = master.derive(1);
assert.equal(child3.xprivkey, child3_priv);
assert.equal(child3.xpubkey, child3_pub);
});
it('should derive(2) child from master', function() {
child4 = master.derive(2);
assert.equal(child4.xprivkey, child4_priv);
assert.equal(child4.xpubkey, child4_pub);
});
it('should derive(0) child from child(2)', function() {
child5 = child4.derive(0);
assert.equal(child5.xprivkey, child5_priv);
assert.equal(child5.xpubkey, child5_pub);
});
it('should derive(1) child from child(2)', function() {
child6 = child4.derive(1);
assert.equal(child6.xprivkey, child6_priv);
assert.equal(child6.xpubkey, child6_pub);
});
it('should deserialize master private key', function() {
master._unbuild(master.xprivkey);
});
it('should deserialize master public key', function() {
master.hdpub._unbuild(master.hdpub.xpubkey);
});
it('should create an hd seed', function() {
var seed = new bcoin.hd.seed({
// I have the same combination on my luggage:
entropy: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
passphrase: 'foo'
});
});
});

View File

@ -44,4 +44,46 @@ describe('Script', function() {
var decoded = bcoin.script.decode(encoded);
assert(bcoin.script.isNullData(decoded))
})
it('should handle if statements correctly', function () {
var inputScript = [1, 2];
var prevOutScript = [2, 'eq', 'if_', 3, 'else_', 4, 'endif', 5];
var stack = [];
bcoin.script.execute(inputScript, stack);
var res = bcoin.script.execute(prevOutScript, stack);
assert(res);
assert.deepEqual(stack.slice(), [[1], [3], [5]]);
var inputScript = [1, 2];
var prevOutScript = [9, 'eq', 'if_', 3, 'else_', 4, 'endif', 5];
var stack = [];
bcoin.script.execute(inputScript, stack);
var res = bcoin.script.execute(prevOutScript, stack);
assert(res);
assert.deepEqual(stack.slice(), [[1], [4], [5]]);
var inputScript = [1, 2];
var prevOutScript = [2, 'eq', 'if_', 3, 'endif', 5];
var stack = [];
bcoin.script.execute(inputScript, stack);
var res = bcoin.script.execute(prevOutScript, stack);
assert(res);
assert.deepEqual(stack.slice(), [[1], [3], [5]]);
var inputScript = [1, 2];
var prevOutScript = [9, 'eq', 'if_', 3, 'endif', 5];
var stack = [];
bcoin.script.execute(inputScript, stack);
var res = bcoin.script.execute(prevOutScript, stack);
assert(res);
assert.deepEqual(stack.slice(), [[1], [5]]);
var inputScript = [1, 2];
var prevOutScript = [9, 'eq', 'notif', 3, 'endif', 5];
var stack = [];
bcoin.script.execute(inputScript, stack);
var res = bcoin.script.execute(prevOutScript, stack);
assert(res);
assert.deepEqual(stack.slice(), [[1], [3], [5]]);
})
});

View File

@ -2,6 +2,33 @@ var assert = require('assert');
var bn = require('bn.js');
var bcoin = require('../');
function printScript(input) {
var scripts = [];
var script = input.script;
scripts.push(script);
var prev = input.out.tx.outputs[input.out.index].script;
scripts.push(prev);
if (bcoin.script.isScripthash(prev)) {
var redeem = bcoin.script.decode(input.script[input.script.length - 1]);
scripts.push(redeem);
}
scripts = scripts.map(function(script) {
return script.map(function(chunk) {
if (Array.isArray(chunk)) {
if (chunk.length === 0)
return [0];
return [bcoin.utils.toHex(chunk)];
}
if (typeof chunk === 'number')
return [chunk];
return chunk;
});
});
scripts.forEach(function(script) {
console.log(script);
});
}
describe('Wallet', function() {
it('should generate new key and address', function() {
var w = bcoin.wallet();
@ -46,13 +73,15 @@ describe('Wallet', function() {
it('should multisign/verify TX', function() {
var w = bcoin.wallet();
var k2 = bcoin.wallet().getPublicKey();
// Input transcation
var src = bcoin.tx({
outputs: [{
value: 5460 * 2,
minSignatures: 1,
address: [ w.getPublicKey(), w.getPublicKey().concat(1) ]
keys: [ w.getPublicKey(), w.getPublicKey().concat(1) ]
// keys: [ w.getPublicKey(), k2 ]
}, {
value: 5460 * 2,
address: w.getAddress() + 'x'
@ -223,4 +252,88 @@ describe('Wallet', function() {
cb();
});
it('should verify 2-of-3 p2sh tx', function(cb) {
var hd = bcoin.hd.priv();
var hd1 = hd.derive(0);
var hd2 = hd.derive(1);
var hd3 = hd.derive(2);
// Generate 3 key pairs
var key1 = bcoin.ecdsa.genKeyPair();
var key2 = bcoin.ecdsa.genKeyPair();
var key3 = bcoin.ecdsa.genKeyPair();
// var key1 = hd1;
// var key2 = hd2;
// var key3 = hd3;
// Grab the 3 pubkeys
var pub1 = key1.getPublic(true, 'array');
var pub2 = key2.getPublic(true, 'array');
var pub3 = key3.getPublic(true, 'array');
// Create 3 2-of-3 wallets with our pubkeys as "shared keys"
var w1 = bcoin.wallet({
key: key1,
multisig: {
type: 'p2sh',
keys: [pub2, pub3],
m: 2,
n: 3
}
});
var w2 = bcoin.wallet({
key: key2,
multisig: {
type: 'p2sh',
keys: [pub1, pub3],
m: 2,
n: 3
}
});
var w3 = bcoin.wallet({
key: key3,
multisig: {
type: 'p2sh',
keys: [pub1, pub2],
m: 2,
n: 3
}
});
var receive = bcoin.wallet();
// Our p2sh address
var addr = w1.getAddress();
assert.equal(w1.getAddress(), addr);
assert.equal(w2.getAddress(), addr);
assert.equal(w3.getAddress(), addr);
// Add a shared unspent transaction to our wallets
var utx = bcoin.tx();
utx.output({ address: addr, value: 5460 * 10 });
w1.addTX(utx);
w2.addTX(utx);
w3.addTX(utx);
// Create a tx requiring 2 signatures
var send = bcoin.tx();
send.output({ address: receive.getAddress(), value: 5460 });
assert(!send.verify());
var result = w1.fill(send);
assert(result);
// printScript(send.inputs[0]);
assert(!send.verify());
w2.sign(send);
assert(send.verify());
send.inputs[0].script[2] = [];
assert(!send.verify());
cb();
});
});