From 862235e57eb1a5ea04226a8406f61bbd320bd7f0 Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Wed, 6 Aug 2014 18:25:45 -0700 Subject: [PATCH] initial commit address, base58, base58check, hash all working with tests. base58check code taken from bitcore. --- .gitignore | 3 + index.js | 28 ++++++++ lib/address.js | 78 ++++++++++++++++++++ lib/base58.js | 2 + lib/base58check.js | 35 +++++++++ lib/constants.js | 15 ++++ lib/hash.js | 42 +++++++++++ package.json | 32 +++++++++ test/test.address.js | 152 +++++++++++++++++++++++++++++++++++++++ test/test.base58check.js | 52 ++++++++++++++ test/test.hash.js | 83 +++++++++++++++++++++ 11 files changed, 522 insertions(+) create mode 100644 .gitignore create mode 100644 index.js create mode 100644 lib/address.js create mode 100644 lib/base58.js create mode 100644 lib/base58check.js create mode 100644 lib/constants.js create mode 100644 lib/hash.js create mode 100644 package.json create mode 100644 test/test.address.js create mode 100644 test/test.base58check.js create mode 100644 test/test.hash.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2716af2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.swp +coverage +node_modules diff --git a/index.js b/index.js new file mode 100644 index 0000000..e0e45e8 --- /dev/null +++ b/index.js @@ -0,0 +1,28 @@ +var privsec = module.exports; + +privsec.deps = {}; +privsec.deps.bnjs = require('bn.js'); +privsec.deps.bs58 = require('bs58'); +privsec.deps.elliptic = require('elliptic'); +privsec.deps.hashjs = require('hash.js'); +privsec.deps.sha512 = require('sha512'); +privsec.deps.ripemd160 = require('ripemd160'); + +privsec.address = require('./lib/address'); +privsec.base58 = require('./lib/base58'); +privsec.base58check = require('./lib/base58check'); +privsec.constants = require('./lib/constants'); +privsec.hash = require('./lib/hash'); + +//privsec.bn = require('lib/bn'); +//privsec.key = require('lib/key'); +//privsec.point = require('lib/point'); +//privsec.privkey = require('lib/privkey'); +//privsec.pubkey = require('lib/pubkey'); +//privsec.script = require('lib/script'); +//privsec.scriptexec = require('lib/scriptexec'); +//privsec.tx = require('lib/tx'); +//privsec.txpartial = require('lib/txpartial'); + +//privsec.bip32 = require('lib/bip32'); +//privsec.bip70 = require('lib/bip70'); diff --git a/lib/address.js b/lib/address.js new file mode 100644 index 0000000..720bb88 --- /dev/null +++ b/lib/address.js @@ -0,0 +1,78 @@ +var base58check = require('./base58check'); +var constants = require('./constants'); + +function Address(str) { + if (!str) { + this.buf = undefined; + return; + } + if (typeof str !== 'string') + throw new Error('address: Input must be a string, or undefined'); + this.fromString(str); +}; + +Address.prototype.getNetwork = function() { + if (this.buf[0] === constants.mainnet.pubkeyHash || this.buf[0] === constants.mainnet.p2sh) + return 'mainnet'; + else if (this.buf[0] === constants.testnet.pubkeyHash || this.buf[0] === constants.testnet.p2sh) + return 'testnet'; + else + return 'unknown'; +}; + +Address.prototype.getHash = function() { + var pubkeyHash = this.buf.slice(1); + if (pubkeyHash.length === 20) + return pubkeyHash; + else + throw new Error('address: Hash must be exactly 20 bytes'); +}; + +Address.prototype.getType = function() { + if (this.buf[0] === constants.mainnet.pubkeyHash || this.buf[0] === constants.testnet.pubkeyHash) + return 'pubkeyHash'; + else if (this.buf[0] === constants.mainnet.p2sh || this.buf[0] === constants.testnet.p2sh) + return 'p2sh'; + else + return 'unknown'; +}; + +Address.prototype.isValid = function() { + if (Buffer.isBuffer(this.buf) && this.buf.length === 1 + 20) + return true; + else + return false; +}; + +Address.prototype.setBuf = function(buf, network, type) { + var version; + if (!Buffer.isBuffer(buf)) + throw new Error('address: buf must be a buffer'); + if (buf.length !== 20) + throw new Error('address: buf must be 20 bytes'); + if (typeof network === 'undefined') + throw new Error('address: Must specify network ("mainnet" or "testnet")'); + if (typeof type === 'undefined') + throw new Error('address: Must specify type ("pubkeyHash" or "p2sh")'); + if (network !== 'mainnet' && network !== 'testnet') + throw new Error('address: Unknown network'); + if (type !== 'pubkeyHash' && type !== 'p2sh') + throw new Error('address: Unknown type'); + + version = new Buffer([constants[network][type]]); + + this.buf = Buffer.concat([version, buf]); +}; + +Address.prototype.fromString = function(str) { + var buf = base58check.decode(str); + if (buf.length !== 1 + 20) + throw new Error('address: Addresses must be exactly 21 bytes'); + this.buf = buf; +} + +Address.prototype.toString = function() { + return base58check.encode(this.buf); +}; + +module.exports = Address; diff --git a/lib/base58.js b/lib/base58.js new file mode 100644 index 0000000..e7516a7 --- /dev/null +++ b/lib/base58.js @@ -0,0 +1,2 @@ +var bs58 = require('bs58'); +module.exports = bs58; diff --git a/lib/base58check.js b/lib/base58check.js new file mode 100644 index 0000000..15313d8 --- /dev/null +++ b/lib/base58check.js @@ -0,0 +1,35 @@ +var base58 = require('./base58'); +var sha256sha256 = require('./hash').sha256sha256; + +var base58check = module.exports; + +base58check.encode = function(buf) { + if (!Buffer.isBuffer(buf)) + throw new Error('base58check: Input must be a buffer'); + var checkedBuf = new Buffer(buf.length + 4); + var hash = sha256sha256(buf); + buf.copy(checkedBuf); + hash.copy(checkedBuf, buf.length); + return base58.encode(checkedBuf); +}; + +base58check.decode = function(s) { + if (typeof s !== 'string') + throw new Error('base58check: Input must be a string'); + + var buf = base58.decode(s); + + if (buf.length < 4) + throw new Error("base58check: Input string too short"); + + var data = buf.slice(0, -4); + var csum = buf.slice(-4); + + var hash = sha256sha256(data); + var hash4 = hash.slice(0, 4); + + if (csum.toString('hex') !== hash4.toString('hex')) + throw new Error("base58check: Checksum mismatch"); + + return data; +}; diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..5b90914 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,15 @@ +exports.mainnet = { + pubkeyHash: 0x00, + privkey: 0x80, + p2sh: 0x05, + bip32pubkey: 0x0488b21e, + bip32privkey: 0x0488ade4, +}; + +exports.testnet = { + pubkeyHash: 0x6f, + privkey: 0xef, + p2sh: 0xc4, + bip32pubkey: 0x043587cf, + bip32privkey: 0x04358394, +}; diff --git a/lib/hash.js b/lib/hash.js new file mode 100644 index 0000000..4219f86 --- /dev/null +++ b/lib/hash.js @@ -0,0 +1,42 @@ +var hashjs = require('hash.js'); +var sha512 = require('sha512'); +var ripemd160 = require('ripemd160'); + +var Hash = module.exports; + +Hash.sha256 = function(buf) { + if (!Buffer.isBuffer(buf)) + throw new Error('sha256 hash must be of a buffer'); + var hash = (new hashjs.sha256()).update(buf).digest(); + return new Buffer(hash); +}; + +Hash.sha256sha256 = function(buf) { + try { + return Hash.sha256(Hash.sha256(buf)); + } catch (e) { + throw new Error('sha256sha256 hash must be of a buffer'); + } +}; + +Hash.ripemd160 = function(buf) { + if (!Buffer.isBuffer(buf)) + throw new Error('ripemd160 hash must be of a buffer'); + var hash = ripemd160(buf); + return new Buffer(hash); +}; + +Hash.sha256ripemd160 = function(buf) { + try { + return Hash.ripemd160(Hash.sha256(buf)); + } catch (e) { + throw new Error('sha256ripemd160 hash must be of a buffer'); + } +}; + +Hash.sha512 = function(buf) { + if (!Buffer.isBuffer(buf)) + throw new Error('sha512 hash must be of a buffer'); + var hash = sha512(buf); + return new Buffer(hash); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7b8b67a --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "privsec", + "version": "0.0.0", + "description": "A bitcoin wallet prioritizing privacy and security.", + "main": "index.js", + "scripts": { + "test": "mocha" + }, + "keywords": [ + "bitcoin", + "bip32", + "bip37", + "bip70", + "stealth", + "merge", + "multisig" + ], + "dependencies": { + "bn.js": "=0.13.3", + "bs58": "=1.2.1", + "elliptic": "=0.15.7", + "hash.js": "=0.3.1", + "ripemd160": "=0.2.0", + "sha512": "=0.0.1" + }, + "devDependencies": { + "chai": "~1.9.1", + "mocha": "~1.21.0" + }, + "author": "Ryan X. Charles ", + "license": "MIT" +} diff --git a/test/test.address.js b/test/test.address.js new file mode 100644 index 0000000..53b3178 --- /dev/null +++ b/test/test.address.js @@ -0,0 +1,152 @@ +var should = require('chai').should(); +var constants = require('../lib/constants'); +var Address = require('../lib/address'); + +describe('Address', function() { + var pubkeyhash = new Buffer('3c3fa3d4adcaf8f52d5b1843975e122548269937', 'hex'); + var str = '1Cs8a3b7R5n4G9c8Cgbp9iW8BXbhv3SFt6'; + + it('should create a new address object', function() { + var address = new Address(); + should.exist(address); + }); + + it('should throw an error when input is not a string', function() { + (function() { + var address = new Address(5); + }).should.throw('address: Input must be a string, or undefined'); + }); + + describe('#getNetwork', function() { + + it('should return mainnet for pubkeyhash', function() { + var address = new Address(); + address.buf = Buffer.concat([new Buffer([constants.mainnet.pubkeyHash]), pubkeyhash]); + address.getNetwork().should.equal('mainnet'); + }); + + it('should return mainnet for p2sh', function() { + var address = new Address(); + address.buf = Buffer.concat([new Buffer([constants.mainnet.p2sh]), pubkeyhash]); + address.getNetwork().should.equal('mainnet'); + }); + + it('should return testnet for pubkeyhash', function() { + var address = new Address(); + address.buf = Buffer.concat([new Buffer([constants.testnet.pubkeyHash]), pubkeyhash]); + address.getNetwork().should.equal('testnet'); + }); + + it('should return testnet for p2sh', function() { + var address = new Address(); + address.buf = Buffer.concat([new Buffer([constants.testnet.p2sh]), pubkeyhash]); + address.getNetwork().should.equal('testnet'); + }); + + it('should return unknown', function() { + var address = new Address(); + address.buf = Buffer.concat([new Buffer([0x55]), pubkeyhash]); + address.getNetwork().should.equal('unknown'); + }); + + it('should throw an error if there is no buffer', function() { + var address = new Address(); + (function() { + address.getNetwork(); + }).should.throw(); + }); + + }); + + describe('#getHash', function() { + + it('should return the hash', function() { + var address = new Address(); + address.buf = Buffer.concat([new Buffer([0x00]), pubkeyhash]); + address.getHash().toString('hex').should.equal(pubkeyhash.toString('hex')); + }); + + it('should throw an error if the buffer is an invalid length', function() { + var address = new Address(); + address.buf = Buffer.concat([new Buffer([0x00]), pubkeyhash.slice(-1)]); + (function() { + address.getHash(); + }).should.throw('address: Hash must be exactly 20 bytes'); + }); + + }); + + describe('#getType', function() { + + it('should get the type of 2MxjnmaMtsJfyFcyG3WZCzS2RihdNuWqeX4 correctly', function() { + var addr = new Address(); + addr.fromString('2MxjnmaMtsJfyFcyG3WZCzS2RihdNuWqeX4'); + addr.getNetwork().should.equal('testnet'); + addr.getType().should.equal('p2sh'); + }); + + }); + + describe('#setBuf', function() { + + it('should convert this pubkeyhash on mainnet and type pubkeyHash to known address', function() { + var address = new Address(); + address.setBuf(pubkeyhash, 'mainnet', 'pubkeyHash'); + address.toString().should.equal('16VZnHwRhwrExfeHFHGjwrgEMq8VcYPs9r'); + }); + + it('should convert this pubkeyhash on mainnet and type p2sh to known address', function() { + var address = new Address(); + address.setBuf(pubkeyhash, 'mainnet', 'p2sh'); + address.toString().should.equal('37BahqRsFrAd3qLiNNwLNV3AWMRD7itxTo'); + }); + + it('should convert this pubkeyhash on testnet and type pubkeyHash to known address', function() { + var address = new Address(); + address.setBuf(pubkeyhash, 'testnet', 'pubkeyHash'); + address.toString().should.equal('mm1X5M2QWyHVjn7txrF7mmtZDpjCXzoa98'); + }); + + it('should convert this pubkeyhash on testnet and type p2sh to known address', function() { + var address = new Address(); + address.setBuf(pubkeyhash, 'testnet', 'p2sh'); + address.toString().should.equal('2MxjnmaMtsJfyFcyG3WZCzS2RihdNuWqeX4'); + }); + + it('should throw an error for an unknown type', function() { + var address = new Address(); + (function() { + address.setBuf(pubkeyhash, 'testnet', 'p2sh2'); + }).should.throw(); + }); + + it('should throw an error for an unknown network', function() { + var address = new Address(); + (function() { + address.setBuf(pubkeyhash, 'testnet2', 'p2sh'); + }).should.throw(); + }); + + }); + + describe('#fromString', function() { + + it('should decode 1Cs8a3b7R5n4G9c8Cgbp9iW8BXbhv3SFt6 correctly', function() { + var addr = new Address(); + addr.fromString('1Cs8a3b7R5n4G9c8Cgbp9iW8BXbhv3SFt6'); + addr.getHash().toString('hex').should.equal('82248027cfb0fe085b750f359fd1e43234e46c7f'); + }); + + }); + + describe('#toString', function() { + + it('should return 1Cs8a3b7R5n4G9c8Cgbp9iW8BXbhv3SFt6', function() { + var addr = new Address(); + addr.fromString('1Cs8a3b7R5n4G9c8Cgbp9iW8BXbhv3SFt6'); + addr.toString().should.equal('1Cs8a3b7R5n4G9c8Cgbp9iW8BXbhv3SFt6'); + }); + + }); + +}); diff --git a/test/test.base58check.js b/test/test.base58check.js new file mode 100644 index 0000000..8b50813 --- /dev/null +++ b/test/test.base58check.js @@ -0,0 +1,52 @@ +var should = require('chai').should(); +var base58check = require('../lib/base58check'); +var base58 = require('../lib/base58'); + +describe('base58check', function() { + var buf = new Buffer([0, 1, 2, 3, 253, 254, 255]); + var enc = "14HV44ipwoaqfg"; + + describe('#encode', function() { + + it('should encode the buffer accurately', function() { + base58check.encode(buf).should.equal(enc); + }); + + it('should throw an error when the input is not a buffer', function() { + (function() { + base58check.encode("string") + }).should.throw('base58check: Input must be a buffer'); + }); + + }); + + describe('#decode', function() { + + it('should decode this encoded value correctly', function() { + base58check.decode(enc).toString('hex').should.equal(buf.toString('hex')); + }); + + it('should throw an error when input is not a string', function() { + (function() { + base58check.decode(5); + }).should.throw('base58check: Input must be a string'); + }); + + it('should throw an error when input is too short', function() { + (function() { + base58check.decode(enc.slice(0, 1)); + }).should.throw('base58check: Input string too short'); + }); + + it('should throw an error when there is a checksum mismatch', function() { + var buf2 = base58.decode(enc); + buf2[0] = buf2[0] + 1; + var enc2 = base58.encode(buf2); + (function() { + base58check.decode(enc2); + }).should.throw('base58check: Checksum mismatch'); + }); + + }); + +}); diff --git a/test/test.hash.js b/test/test.hash.js new file mode 100644 index 0000000..eea3517 --- /dev/null +++ b/test/test.hash.js @@ -0,0 +1,83 @@ +var should = require('chai').should(); +var Hash = require('../lib/hash'); + +describe('hash', function() { + var buf = new Buffer([0, 1, 2, 3, 253, 254, 255]); + var str = "test string"; + + describe('#sha256', function() { + + it('should calculate the hash of this buffer correctly', function() { + var hash = Hash.sha256(buf); + hash.toString('hex').should.equal('6f2c7b22fd1626998287b3636089087961091de80311b9279c4033ec678a83e8'); + }); + + it('should throw an error when the input is not a buffer', function() { + (function() { + Hash.sha256(str); + }).should.throw('sha256 hash must be of a buffer'); + }); + + }); + + describe('#sha256sha256', function() { + + it('should calculate the hash of this buffer correctly', function() { + var hash = Hash.sha256sha256(buf); + hash.toString('hex').should.equal('be586c8b20dee549bdd66018c7a79e2b67bb88b7c7d428fa4c970976d2bec5ba'); + }); + + it('should throw an error when the input is not a buffer', function() { + (function() { + Hash.sha256sha256(str); + }).should.throw('sha256sha256 hash must be of a buffer'); + }); + + }); + + describe('#sha256ripemd160', function() { + + it('should calculate the hash of this buffer correctly', function() { + var hash = Hash.sha256ripemd160(buf); + hash.toString('hex').should.equal('7322e2bd8535e476c092934e16a6169ca9b707ec'); + }); + + it('should throw an error when the input is not a buffer', function() { + (function() { + Hash.sha256ripemd160(str); + }).should.throw('sha256ripemd160 hash must be of a buffer'); + }); + + }); + + describe('#ripemd160', function() { + + it('should calculate the hash of this buffer correctly', function() { + var hash = Hash.ripemd160(buf); + hash.toString('hex').should.equal('fa0f4565ff776fee0034c713cbf48b5ec06b7f5c'); + }); + + it('should throw an error when the input is not a buffer', function() { + (function() { + Hash.ripemd160(str); + }).should.throw('ripemd160 hash must be of a buffer'); + }); + + }); + + describe('#sha512', function() { + + it('should calculate the hash of this buffer correctly', function() { + var hash = Hash.sha512(buf); + hash.toString('hex').should.equal('c0530aa32048f4904ae162bc14b9eb535eab6c465e960130005feddb71613e7d62aea75f7d3333ba06e805fc8e45681454524e3f8050969fe5a5f7f2392e31d0'); + }); + + it('should throw an error when the input is not a buffer', function() { + (function() { + Hash.sha512(str); + }).should.throw('sha512 hash must be of a buffer'); + }); + + }); + +});