add ECPair module
This commit is contained in:
parent
c66b8883f7
commit
7559ee880d
3 changed files with 478 additions and 0 deletions
142
src/ecpair.js
Normal file
142
src/ecpair.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
var assert = require('assert')
|
||||
var base58check = require('bs58check')
|
||||
var bcrypto = require('./crypto')
|
||||
var ecdsa = require('./ecdsa')
|
||||
var ecurve = require('ecurve')
|
||||
var networks = require('./networks')
|
||||
var randomBytes = require('randombytes')
|
||||
var typeForce = require('typeforce')
|
||||
|
||||
var Address = require('./address')
|
||||
var BigInteger = require('bigi')
|
||||
|
||||
function findNetworkByWIFVersion (version) {
|
||||
for (var networkName in networks) {
|
||||
var network = networks[networkName]
|
||||
|
||||
if (network.wif === version) return network
|
||||
}
|
||||
|
||||
assert(false, 'Unknown network')
|
||||
}
|
||||
|
||||
function ECPair (d, Q, options) {
|
||||
options = options || {}
|
||||
|
||||
var compressed = options.compressed === undefined ? true : options.compressed
|
||||
var network = options.network === undefined ? networks.bitcoin : options.network
|
||||
|
||||
typeForce('Boolean', compressed)
|
||||
assert('pubKeyHash' in network, 'Unknown pubKeyHash constants for network')
|
||||
|
||||
if (d) {
|
||||
assert(d.signum() > 0, 'Private key must be greater than 0')
|
||||
assert(d.compareTo(ECPair.curve.n) < 0, 'Private key must be less than the curve order')
|
||||
|
||||
assert(!Q, 'Unexpected publicKey parameter')
|
||||
Q = ECPair.curve.G.multiply(d)
|
||||
|
||||
// enforce Q is a public key if no private key given
|
||||
} else {
|
||||
typeForce('Point', Q)
|
||||
}
|
||||
|
||||
this.compressed = compressed
|
||||
this.d = d
|
||||
this.Q = Q
|
||||
this.network = network
|
||||
}
|
||||
|
||||
// Public access to secp256k1 curve
|
||||
ECPair.curve = ecurve.getCurveByName('secp256k1')
|
||||
|
||||
ECPair.fromPublicKeyBuffer = function (buffer, network) {
|
||||
var Q = ecurve.Point.decodeFrom(ECPair.curve, buffer)
|
||||
|
||||
return new ECPair(null, Q, {
|
||||
compressed: Q.compressed,
|
||||
network: network
|
||||
})
|
||||
}
|
||||
|
||||
ECPair.fromWIF = function (string) {
|
||||
var payload = base58check.decode(string)
|
||||
var version = payload.readUInt8(0)
|
||||
var compressed
|
||||
|
||||
if (payload.length === 34) {
|
||||
assert.strictEqual(payload[33], 0x01, 'Invalid compression flag')
|
||||
|
||||
// truncate the version/compression bytes
|
||||
payload = payload.slice(1, -1)
|
||||
compressed = true
|
||||
|
||||
// no compression flag
|
||||
} else {
|
||||
assert.equal(payload.length, 33, 'Invalid WIF payload length')
|
||||
|
||||
// Truncate the version byte
|
||||
payload = payload.slice(1)
|
||||
compressed = false
|
||||
}
|
||||
|
||||
var network = findNetworkByWIFVersion(version)
|
||||
var d = BigInteger.fromBuffer(payload)
|
||||
|
||||
return new ECPair(d, null, {
|
||||
compressed: compressed,
|
||||
network: network
|
||||
})
|
||||
}
|
||||
|
||||
ECPair.makeRandom = function (options) {
|
||||
options = options || {}
|
||||
|
||||
var rng = options.rng || randomBytes
|
||||
var buffer = rng(32)
|
||||
typeForce('Buffer', buffer)
|
||||
assert.equal(buffer.length, 32, 'Expected 256-bit Buffer from RNG')
|
||||
|
||||
var d = BigInteger.fromBuffer(buffer)
|
||||
d = d.mod(ECPair.curve.n)
|
||||
|
||||
return new ECPair(d, null, options)
|
||||
}
|
||||
|
||||
ECPair.prototype.toWIF = function () {
|
||||
assert(this.d, 'Missing private key')
|
||||
|
||||
var bufferLen = this.compressed ? 34 : 33
|
||||
var buffer = new Buffer(bufferLen)
|
||||
|
||||
buffer.writeUInt8(this.network.wif, 0)
|
||||
this.d.toBuffer(32).copy(buffer, 1)
|
||||
|
||||
if (this.compressed) {
|
||||
buffer.writeUInt8(0x01, 33)
|
||||
}
|
||||
|
||||
return base58check.encode(buffer)
|
||||
}
|
||||
|
||||
ECPair.prototype.getAddress = function () {
|
||||
var pubKey = this.getPublicKeyBuffer()
|
||||
|
||||
return new Address(bcrypto.hash160(pubKey), this.network.pubKeyHash)
|
||||
}
|
||||
|
||||
ECPair.prototype.getPublicKeyBuffer = function () {
|
||||
return this.Q.getEncoded(this.compressed)
|
||||
}
|
||||
|
||||
ECPair.prototype.sign = function (hash) {
|
||||
assert(this.d, 'Missing private key')
|
||||
|
||||
return ecdsa.sign(ECPair.curve, hash, this.d)
|
||||
}
|
||||
|
||||
ECPair.prototype.verify = function (hash, signature) {
|
||||
return ecdsa.verify(ECPair.curve, hash, signature, this.Q)
|
||||
}
|
||||
|
||||
module.exports = ECPair
|
234
test/ecpair.js
Normal file
234
test/ecpair.js
Normal file
|
@ -0,0 +1,234 @@
|
|||
/* global describe, it, beforeEach */
|
||||
/* eslint-disable no-new */
|
||||
|
||||
var assert = require('assert')
|
||||
var ecdsa = require('../src/ecdsa')
|
||||
var ecurve = require('ecurve')
|
||||
var networks = require('../src/networks')
|
||||
var proxyquire = require('proxyquire')
|
||||
var sinon = require('sinon')
|
||||
|
||||
var BigInteger = require('bigi')
|
||||
var ECPair = require('../src/ecpair')
|
||||
|
||||
var fixtures = require('./fixtures/ecpair.json')
|
||||
|
||||
describe('ECPair', function () {
|
||||
describe('constructor', function () {
|
||||
it('defaults to compressed', function () {
|
||||
var keyPair = new ECPair(BigInteger.ONE)
|
||||
|
||||
assert.equal(keyPair.compressed, true)
|
||||
})
|
||||
|
||||
it('supports the uncompressed option', function () {
|
||||
var keyPair = new ECPair(BigInteger.ONE, null, {
|
||||
compressed: false
|
||||
})
|
||||
|
||||
assert.equal(keyPair.compressed, false)
|
||||
})
|
||||
|
||||
it('supports the network option', function () {
|
||||
var keyPair = new ECPair(BigInteger.ONE, null, {
|
||||
compressed: false,
|
||||
network: networks.testnet
|
||||
})
|
||||
|
||||
assert.equal(keyPair.network, networks.testnet)
|
||||
})
|
||||
|
||||
it('throws if compressed option is not a bool', function () {
|
||||
assert.throws(function () {
|
||||
new ECPair(null, null, {
|
||||
compressed: 2
|
||||
}, /Expected Boolean, got 2/)
|
||||
})
|
||||
})
|
||||
|
||||
it('throws if public and private key given', function () {
|
||||
var qBuffer = new Buffer('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', 'hex')
|
||||
var Q = ecurve.Point.decodeFrom(ECPair.curve, qBuffer)
|
||||
|
||||
assert.throws(function () {
|
||||
new ECPair(BigInteger.ONE, Q)
|
||||
}, /Unexpected publicKey parameter/)
|
||||
})
|
||||
|
||||
it('throws if network is missing pubKeyHash constants', function () {
|
||||
assert.throws(function () {
|
||||
new ECPair(null, null, {
|
||||
network: {}
|
||||
}, /Unknown pubKeyHash constants for network/)
|
||||
})
|
||||
})
|
||||
|
||||
fixtures.valid.forEach(function (f) {
|
||||
it('calculates the public point for ' + f.WIF, function () {
|
||||
var d = new BigInteger(f.d)
|
||||
var keyPair = new ECPair(d, null, {
|
||||
compressed: f.compressed
|
||||
})
|
||||
|
||||
assert.equal(keyPair.getPublicKeyBuffer().toString('hex'), f.Q)
|
||||
})
|
||||
})
|
||||
|
||||
fixtures.invalid.constructor.forEach(function (f) {
|
||||
it('throws on ' + f.d, function () {
|
||||
var d = new BigInteger(f.d)
|
||||
|
||||
assert.throws(function () {
|
||||
new ECPair(d)
|
||||
}, new RegExp(f.exception))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPublicKeyBuffer', function () {
|
||||
var keyPair
|
||||
|
||||
beforeEach(function () {
|
||||
keyPair = new ECPair(BigInteger.ONE)
|
||||
})
|
||||
|
||||
it('wraps Q.getEncoded', sinon.test(function () {
|
||||
this.mock(keyPair.Q).expects('getEncoded')
|
||||
.once().calledWith(keyPair.compressed)
|
||||
|
||||
keyPair.getPublicKeyBuffer()
|
||||
}))
|
||||
})
|
||||
|
||||
describe('fromWIF', function () {
|
||||
fixtures.valid.forEach(function (f) {
|
||||
it('imports ' + f.WIF + ' correctly', function () {
|
||||
var keyPair = ECPair.fromWIF(f.WIF)
|
||||
|
||||
assert.equal(keyPair.d.toString(), f.d)
|
||||
assert.equal(keyPair.compressed, f.compressed)
|
||||
assert.equal(keyPair.network, networks[f.network])
|
||||
})
|
||||
})
|
||||
|
||||
fixtures.invalid.fromWIF.forEach(function (f) {
|
||||
it('throws on ' + f.string, function () {
|
||||
assert.throws(function () {
|
||||
ECPair.fromWIF(f.string)
|
||||
}, new RegExp(f.exception))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('toWIF', function () {
|
||||
fixtures.valid.forEach(function (f) {
|
||||
it('exports ' + f.WIF + ' correctly', function () {
|
||||
var keyPair = ECPair.fromWIF(f.WIF)
|
||||
var result = keyPair.toWIF()
|
||||
|
||||
assert.equal(result, f.WIF)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeRandom', function () {
|
||||
var d = new Buffer('0404040404040404040404040404040404040404040404040404040404040404', 'hex')
|
||||
var exWIF = 'KwMWvwRJeFqxYyhZgNwYuYjbQENDAPAudQx5VEmKJrUZcq6aL2pv'
|
||||
|
||||
describe('uses randombytes RNG', function () {
|
||||
it('generates a ECPair', function () {
|
||||
var stub = { randombytes: function () { return d } }
|
||||
var ProxiedECPair = proxyquire('../src/ecpair', stub)
|
||||
|
||||
var keyPair = ProxiedECPair.makeRandom()
|
||||
assert.equal(keyPair.toWIF(), exWIF)
|
||||
})
|
||||
|
||||
it('passes the options param', sinon.test(function () {
|
||||
var options = {
|
||||
compressed: true
|
||||
}
|
||||
|
||||
// FIXME: waiting on https://github.com/cjohansen/Sinon.JS/issues/613
|
||||
// this.mock(ECPair).expects('constructor')
|
||||
// .once().calledWith(options)
|
||||
|
||||
ECPair.makeRandom(options)
|
||||
}))
|
||||
})
|
||||
|
||||
it('allows a custom RNG to be used', function () {
|
||||
var keyPair = ECPair.makeRandom({
|
||||
rng: function (size) { return d.slice(0, size) }
|
||||
})
|
||||
|
||||
assert.equal(keyPair.toWIF(), exWIF)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAddress', function () {
|
||||
fixtures.valid.forEach(function (f) {
|
||||
it('returns ' + f.address + ' for ' + f.WIF, function () {
|
||||
var keyPair = ECPair.fromWIF(f.WIF)
|
||||
|
||||
assert.equal(keyPair.getAddress().toString(), f.address)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ecdsa wrappers', function () {
|
||||
var keyPair, hash
|
||||
|
||||
beforeEach(function () {
|
||||
keyPair = ECPair.makeRandom()
|
||||
hash = new Buffer(32)
|
||||
})
|
||||
|
||||
it('uses the secp256k1 curve by default', function () {
|
||||
var secp256k1 = ecurve.getCurveByName('secp256k1')
|
||||
|
||||
for (var property in secp256k1) {
|
||||
// FIXME: circular structures in ecurve
|
||||
if (property === 'G') continue
|
||||
if (property === 'infinity') continue
|
||||
|
||||
var actual = ECPair.curve[property]
|
||||
var expected = secp256k1[property]
|
||||
|
||||
assert.deepEqual(actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
describe('signing', function () {
|
||||
it('wraps ecdsa.sign', sinon.test(function () {
|
||||
this.mock(ecdsa).expects('sign')
|
||||
.once().calledWith(ECPair.curve, hash, keyPair.d)
|
||||
|
||||
keyPair.sign(hash)
|
||||
}))
|
||||
|
||||
it('throws if no private key is found', function () {
|
||||
keyPair.d = null
|
||||
|
||||
assert.throws(function () {
|
||||
keyPair.sign(hash)
|
||||
}, /Missing private key/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify', function () {
|
||||
var signature
|
||||
|
||||
beforeEach(function () {
|
||||
signature = keyPair.sign(hash)
|
||||
})
|
||||
|
||||
it('wraps ecdsa.verify', sinon.test(function () {
|
||||
this.mock(ecdsa).expects('verify')
|
||||
.once().calledWith(ECPair.curve, hash, signature, keyPair.Q)
|
||||
|
||||
keyPair.verify(hash, signature)
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
102
test/fixtures/ecpair.json
vendored
Normal file
102
test/fixtures/ecpair.json
vendored
Normal file
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"valid": [
|
||||
{
|
||||
"d": "1",
|
||||
"Q": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"compressed": true,
|
||||
"network": "bitcoin",
|
||||
"address": "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH",
|
||||
"WIF": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"
|
||||
},
|
||||
{
|
||||
"d": "1",
|
||||
"Q": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8",
|
||||
"compressed": false,
|
||||
"network": "bitcoin",
|
||||
"address": "1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm",
|
||||
"WIF": "5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf"
|
||||
},
|
||||
{
|
||||
"d": "19898843618908353587043383062236220484949425084007183071220218307100305431102",
|
||||
"Q": "02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340",
|
||||
"compressed": true,
|
||||
"network": "bitcoin",
|
||||
"address": "1MasfEKgSiaSeri2C6kgznaqBNtyrZPhNq",
|
||||
"WIF": "KxhEDBQyyEFymvfJD96q8stMbJMbZUb6D1PmXqBWZDU2WvbvVs9o"
|
||||
},
|
||||
{
|
||||
"d": "48968302285117906840285529799176770990048954789747953886390402978935544927851",
|
||||
"Q": "024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34",
|
||||
"compressed": true,
|
||||
"network": "bitcoin",
|
||||
"address": "1LwwMWdSEMHJ2dMhSvAHZ3g95tG2UBv9jg",
|
||||
"WIF": "KzrA86mCVMGWnLGBQu9yzQa32qbxb5dvSK4XhyjjGAWSBKYX4rHx"
|
||||
},
|
||||
{
|
||||
"d": "48968302285117906840285529799176770990048954789747953886390402978935544927851",
|
||||
"Q": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6",
|
||||
"compressed": false,
|
||||
"network": "bitcoin",
|
||||
"address": "1zXcfvKCLgsFdJDYPuqpu1sF3q92tnnUM",
|
||||
"WIF": "5JdxzLtFPHNe7CAL8EBC6krdFv9pwPoRo4e3syMZEQT9srmK8hh"
|
||||
},
|
||||
{
|
||||
"d": "48968302285117906840285529799176770990048954789747953886390402978935544927851",
|
||||
"Q": "024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34",
|
||||
"compressed": true,
|
||||
"network": "testnet",
|
||||
"address": "n1TteZiR3NiYojqKAV8fNxtTwsrjM7kVdj",
|
||||
"WIF": "cRD9b1m3vQxmwmjSoJy7Mj56f4uNFXjcWMCzpQCEmHASS4edEwXv"
|
||||
},
|
||||
{
|
||||
"d": "48968302285117906840285529799176770990048954789747953886390402978935544927851",
|
||||
"Q": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6",
|
||||
"compressed": false,
|
||||
"network": "testnet",
|
||||
"address": "mgWUuj1J1N882jmqFxtDepEC73Rr22E9GU",
|
||||
"WIF": "92Qba5hnyWSn5Ffcka56yMQauaWY6ZLd91Vzxbi4a9CCetaHtYj"
|
||||
},
|
||||
{
|
||||
"d": "115792089237316195423570985008687907852837564279074904382605163141518161494336",
|
||||
"Q": "0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"compressed": true,
|
||||
"network": "bitcoin",
|
||||
"address": "1GrLCmVQXoyJXaPJQdqssNqwxvha1eUo2E",
|
||||
"WIF": "L5oLkpV3aqBjhki6LmvChTCV6odsp4SXM6FfU2Gppt5kFLaHLuZ9"
|
||||
}
|
||||
],
|
||||
"invalid": {
|
||||
"constructor": [
|
||||
{
|
||||
"exception": "Private key must be greater than 0",
|
||||
"d": "-1"
|
||||
},
|
||||
{
|
||||
"exception": "Private key must be greater than 0",
|
||||
"d": "0"
|
||||
},
|
||||
{
|
||||
"exception": "Private key must be less than the curve order",
|
||||
"d": "115792089237316195423570985008687907852837564279074904382605163141518161494337"
|
||||
},
|
||||
{
|
||||
"exception": "Private key must be less than the curve order",
|
||||
"d": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
|
||||
}
|
||||
],
|
||||
"fromWIF": [
|
||||
{
|
||||
"exception": "Invalid compression flag",
|
||||
"string": "ju9rooVsmagsb4qmNyTysUSFB1GB6MdpD7eoGjUTPmZRAApJxRz"
|
||||
},
|
||||
{
|
||||
"exception": "Invalid WIF payload length",
|
||||
"string": "7ZEtRQLhCsDQrd6ZKfmcESdXgas8ggZPN24ByEi5ey6VJW"
|
||||
},
|
||||
{
|
||||
"exception": "Invalid WIF payload length",
|
||||
"string": "5qibUKwsnMo1qDiNp3prGaQkD2JfVJa8F8Na87H2CkMHvuVg6uKhw67Rh"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue