bip45 wallet

This commit is contained in:
Christopher Jeffrey 2016-02-03 19:44:14 -08:00
parent 28a2cab787
commit eacd1e2ece
6 changed files with 494 additions and 127 deletions

View File

@ -42,18 +42,6 @@ function Address(options) {
compressed: options.compressed
});
// Compatability
if (options.multisig) {
if (options.multisig.type)
options.type = options.multisig.type;
if (options.multisig.keys)
options.keys = options.multisig.keys;
if (options.multisig.m)
options.m = options.multisig.m;
if (options.multisig.n)
options.n = options.multisig.n;
}
this.type = options.type || 'pubkeyhash';
this.subtype = options.subtype;
this.keys = [];
@ -141,8 +129,8 @@ Address.prototype.removeKey = function removeKey(key) {
key = utils.toBuffer(key);
var index = this.keys.map(function(key, i) {
return utils.isEqual(key, pub) ? i : null;
var index = this.keys.map(function(pub, i) {
return utils.isEqual(pub, key) ? i : null;
}).filter(function(i) {
return i !== null;
})[0];
@ -226,9 +214,6 @@ Address.prototype.getScriptAddress = function getScriptAddress() {
};
Address.prototype.getPublicKey = function getPublicKey(enc) {
if (!this.key.priv)
return;
if (!enc) {
if (this._pub)
return this._pub;
@ -241,6 +226,32 @@ Address.prototype.getPublicKey = function getPublicKey(enc) {
return this.key.getPublic(enc);
};
Address.prototype.getHDPublicKey = function getPublicKey(enc) {
if (!this.key.hd)
return;
if (enc === 'base58')
return this.key.hd.xpubkey;
if (this.key.hd.isPublic)
return this.key.hd;
return this.key.hd.hdpub;
};
Address.prototype.getHDPrivateKey = function getPublicKey(enc) {
if (!this.key.hd)
return;
if (!this.key.hd.isPrivate)
return;
if (enc === 'base58')
return this.key.hd.xprivkey;
return this.key.hd;
};
Address.prototype.getKeyHash = function getKeyHash() {
if (this._hash)
return this._hash;
@ -521,6 +532,20 @@ Address.prototype.__defineGetter__('address', function() {
return this.getAddress();
});
Address.prototype.__defineGetter__('realType', function() {
if (this.type === 'scripthash')
return this.subtype;
return this.type;
});
Address.prototype.__defineGetter__('hdPrivateKey', function() {
return this.getHDPrivateKey();
});
Address.prototype.__defineGetter__('hdPublicKey', function() {
return this.getHDPublicKey();
});
Address.prototype.toJSON = function toJSON(encrypt) {
return {
v: 1,

View File

@ -252,12 +252,19 @@ HDPrivateKey.prototype.scan44 = function scan44(options, txByAddress, callback)
})(0);
};
HDPrivateKey.prototype.deriveRoot44 = function deriveRoot44(options) {
var coinType = options.coinType;
var accountIndex = options.accountIndex;
HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(options) {
var coinType, accountIndex, child;
if (this instanceof HDPublicKey)
if (typeof options === 'number')
options = { accountIndex: options };
coinType = options.coinType;
accountIndex = options.accountIndex;
if (this instanceof HDPublicKey) {
assert(this.isAccount44());
return this;
}
if (coinType == null)
coinType = network.type === 'main' ? 0 : 1;
@ -265,13 +272,17 @@ HDPrivateKey.prototype.deriveRoot44 = function deriveRoot44(options) {
assert(utils.isFinite(coinType));
assert(utils.isFinite(accountIndex));
return this
child = this
.derive(44, true)
.derive(coinType, true)
.derive(accountIndex, true);
assert(child.isAccount44());
return child;
};
HDPrivateKey.prototype.deriveBIP44 = function deriveBIP44(options, isPublic) {
HDPrivateKey.prototype.deriveBIP44 = function deriveBIP44(options) {
var chain = options.chain;
var addressIndex = options.addressIndex;
@ -282,7 +293,7 @@ HDPrivateKey.prototype.deriveBIP44 = function deriveBIP44(options, isPublic) {
assert(utils.isFinite(addressIndex));
return this
.deriveRoot44(options)
.deriveAccount44(options)
.derive(chain)
.derive(addressIndex);
};
@ -317,7 +328,7 @@ HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback)
var keys = [];
var root;
root = this.deriveRoot45(options);
root = this.derivePurpose45(options);
return (function chainCheck(chainConstant) {
return (function scanner(cosignerIndex) {
@ -372,10 +383,19 @@ HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback)
})(0);
};
HDPrivateKey.prototype.deriveRoot45 = function deriveRoot45(options) {
if (this instanceof HDPublicKey)
HDPrivateKey.prototype.derivePurpose45 = function derivePurpose45(options) {
var child;
if (this instanceof HDPublicKey) {
assert(this.isPurpose45());
return this;
return this.derive(45, true);
}
child = this.derive(45, true);
assert(child.isPurpose45());
return child;
};
HDPrivateKey.prototype.deriveBIP45 = function deriveBIP45(options) {
@ -391,7 +411,7 @@ HDPrivateKey.prototype.deriveBIP45 = function deriveBIP45(options) {
assert(utils.isFinite(addressIndex));
return this
.deriveRoot45(options)
.derivePurpose45(options)
.derive(cosignerIndex)
.derive(chain)
.derive(addressIndex);
@ -413,6 +433,14 @@ HDPrivateKey.prototype.deriveCosignerAddress = function deriveCosignerAddress(co
});
};
HDPrivateKey.prototype.isPurpose45 = function isPurpose45(options) {
return new bn(this.childIndex).toNumber() === constants.hd.hardened + 45;
};
HDPrivateKey.prototype.isAccount44 = function isAccount44(options) {
return new bn(this.depth).toNumber() === 3;
};
HDPrivateKey.getPath = function getPath(options) {
var purpose, coinType, accountIndex, chain, addressIndex;
@ -765,17 +793,20 @@ function HDPublicKey(options) {
}
HDPublicKey.prototype.scan44 = HDPrivateKey.prototype.scan44;
HDPublicKey.prototype.deriveRoot44 = HDPrivateKey.prototype.deriveRoot44;
HDPublicKey.prototype.deriveAccount44 = HDPrivateKey.prototype.deriveAccount44;
HDPublicKey.prototype.deriveBIP44 = HDPrivateKey.prototype.deriveBIP44;
HDPublicKey.prototype.deriveChange = HDPrivateKey.prototype.deriveChange;
HDPublicKey.prototype.deriveAddress = HDPrivateKey.prototype.deriveAddress;
HDPublicKey.prototype.scan45 = HDPrivateKey.prototype.scan45;
HDPublicKey.prototype.deriveRoot45 = HDPrivateKey.prototype.deriveRoot45;
HDPublicKey.prototype.derivePurpose45 = HDPrivateKey.prototype.derivePurpose45;
HDPublicKey.prototype.deriveBIP45 = HDPrivateKey.prototype.deriveBIP45;
HDPublicKey.prototype.deriveCosignerChange = HDPrivateKey.prototype.deriveCosignerChange;
HDPublicKey.prototype.deriveCosignerAddress = HDPrivateKey.prototype.deriveCosignerAddress;
HDPublicKey.prototype.isPurpose45 = HDPrivateKey.prototype.isPurpose45;
HDPublicKey.prototype.isAccount44 = HDPrivateKey.prototype.isAccount44;
HDPublicKey.isExtended = function isExtended(data) {
if (typeof data !== 'string')
return false;

View File

@ -33,6 +33,12 @@ function KeyPair(options) {
this.hd = options.hd || null;
this.compressed = options.compressed !== false;
if (options.privateKey)
options.priv = options.privateKey;
if (options.publicKey)
options.pub = options.publicKey;
if (options.priv instanceof bcoin.hd.priv) {
this.hd = options.priv;
this._key = options.priv.pair;

View File

@ -763,6 +763,12 @@ utils.sortKeys = function sortKeys(keys) {
});
};
utils.sortHDKeys = function sortHDKeys(keys) {
return keys.slice().sort(function(a, b) {
return new bn(a.publicKey).cmp(new bn(b.publicKey)) > 0;
});
};
utils.uniq = function(obj) {
var out = [];
var i = 0;

View File

@ -31,67 +31,323 @@ function Wallet(options) {
options = utils.merge({}, options);
if (options.hd) {
options.master = options.hd !== true
? bcoin.hd.priv(options.hd)
: bcoin.hd.priv();
delete options.hd;
}
if (options.priv
|| options.pub
|| options.key
|| options.personalization
|| options.entropy
|| options.compressed) {
if ((options.key instanceof bcoin.hd.priv)
|| options.key instanceof bcoin.hd.pub) {
options.master = options.key;
delete options.key;
} else if (options.priv instanceof bcoin.hd.priv) {
options.master = options.priv;
delete options.priv;
} else if (options.pub instanceof bcoin.hd.pub) {
options.master = options.pub;
delete options.pub;
}
}
this.options = options;
this.addresses = [];
this.master = options.master || null;
if (this.master && !(this.master instanceof bcoin.keypair))
this.master = bcoin.keypair({ hd: this.master });
this._addressTable = {};
this._labelMap = {};
this.accountIndex = options.accountIndex || 0;
this.addressDepth = options.addressDepth || 0;
this.changeDepth = options.changeDepth || 0;
this.cosignerIndex = 0;
this.purposeKeys = options.purposeKeys || [];
this.keys = options.keys || [];
this.hd = false;
this.hdpm = false;
this.multisig = false;
this.type = options.type || 'pubkeyhash';
this.subtype = options.subtype;
this.keys = [];
this.m = options.m || 1;
this.n = options.n || 1;
this.nmax = this.type === 'scripthash'
? (this.compressed !== false ? 15 : 7)
: 3;
if (this.n > 1) {
if (this.type !== 'multisig')
this.type = 'scripthash';
if (this.type === 'scripthash')
this.subtype = 'multisig';
}
if (this.master)
this.hd = true;
if (this.master && this.type === 'scripthash' && this.subtype === 'multisig')
this.hdpm = true;
if (this.type === 'multisig' || this.subtype === 'multisig')
this.multisig = true;
if (network.prefixes[this.type] == null)
throw new Error('Unknown prefix: ' + this.type);
if (this.m < 1 || this.m > this.n)
throw new Error('m ranges between 1 and n');
if (this.n < 1 || this.n > this.nmax)
throw new Error('n ranges between 1 and ' + this.nmax);
if (this.hdpm) {
this.purposeKey = this.master.hd.isPublic
? this.master.hd
: this.master.hd.derivePurpose45();
} else if (this.hd) {
this.accountKey = this.master.hd.isPublic
? this.master.hd
: this.master.hd.deriveAccount44(this.accountIndex);
}
if (!options.addresses)
options.addresses = [];
if (!options.addresses.length)
options.addresses.push(utils.merge({}, options));
options.addresses.forEach(function(address) {
this.addAddress(address);
}, this);
// Create a non-master account address if we don't have one.
if (this.master) {
for (i = 0; i < this.addresses.length; i++) {
if (this.addresses[i].key.hd && !this.addresses[i].change)
break;
}
if (i === this.addresses.length)
this.createNewAddress(this._cleanOptions(options.addresses[0]));
if (options.priv
|| options.pub
|| options.key
|| options.personalization
|| options.entropy
|| options.compressed) {
options.addresses.push({
priv: options.priv,
pub: options.pub,
key: options.key,
personalization: options.personalization,
entropy: options.entropy,
compressed: options.compressed,
type: this.type,
subtype: this.subtype,
m: this.m,
n: this.n,
keys: [],
change: false
});
}
// Find the last change address if there is one.
for (i = this.addresses.length - 1; i >= 0; i--) {
if (this.addresses[i].change)
break;
}
if (i === -1)
this.changeAddress = this.createChangeAddress();
else
this.changeAddress = this.addresses[i];
this.storage = options.storage;
this.loading = true;
this.lastTs = 0;
if (!this.hdpm) {
if (options.addresses.length) {
this.current = bcoin.address(options.addresses[options.addresses.length - 1]);
this._firstKey = {
priv: this.current.key._key.getPrivate().toArray(),
pub: this.current.key._key.getPublic(true, 'array')
};
} else {
this._firstKey = this.createKey(false, Math.max(0, this.addressDepth - 1));
this.current = bcoin.address({ priv: this._firstKey.priv });
}
}
if (this.hdpm)
this.addKey(this.purposeKey);
else
this.addKey(this.current.publicKey);
(options.keys || []).forEach(function(key) {
this.addKey(key);
}, this);
}
inherits(Wallet, EventEmitter);
Wallet.prototype._initAddresses = function() {
var options = this.options;
options.addresses.forEach(function(address) {
address = this.addAddress(address);
if (!address.change)
this.current = address;
}, this);
if (this.hd) {
for (i = 0; i < this.addressDepth; i++)
this.createAddress(false, i);
for (i = 0; i < this.changeDepth; i++)
this.createAddress(true, i);
}
// Create a non-master account address if we don't have one.
if (this.addresses.length === 0)
this.createAddress();
// Find the last change address if there is one.
if (this.hd) {
if (this.changeDepth === 0)
this.changeAddress = this.createAddress(true);
else
this.changeAddress = this.addresses[this.addresses.length - 1];
} else {
for (i = this.addresses.length - 1; i >= 0; i--) {
if (this.addresses[i].change)
break;
}
if (i === -1)
this.changeAddress = this.createAddress(true);
else
this.changeAddress = this.addresses[i];
}
assert(this.current);
assert(!this.current.change);
assert(this.changeAddress.change);
this.prefix = 'bt/wallet/' + this.getKeyAddress() + '/';
this.tx = new bcoin.txPool(this);
this._init();
}
};
inherits(Wallet, EventEmitter);
Wallet.prototype.addKey = function addKey(key) {
var hdKey, has, i;
Wallet.prototype._cleanOptions = function _cleanOptions(options) {
return utils.merge(options, {
key: null,
priv: null,
pub: null,
hd: null
if (bcoin.hd.priv.isExtended(key))
key = bcoin.hd.priv(key);
else if (bcoin.hd.pub.isExtended(key))
key = bcoin.hd.pub(key);
if (key instanceof bcoin.keypair)
key = key.hd;
if (key instanceof bcoin.hd.priv)
key = key.hdpub;
if (key instanceof bcoin.hd.pub) {
hdKey = key;
key = hdKey.publicKey;
}
if (this.hdpm) {
if (!hdKey || !hdKey.isPurpose45())
throw new Error('Must add HD purpose keys to HD wallet.');
has = this.purposeKeys.some(function(pub) {
return pub.xpubkey === hdKey.xpubkey;
});
if (has)
return;
this.purposeKeys.push(hdKey);
if (this.purposeKeys.length === this.n)
this.finalizeKeys();
return;
}
key = utils.toBuffer(key);
has = this.keys.some(function(k) {
return utils.isEqual(k, key);
});
if (has)
return;
this.keys.push(key);
if (this.keys.length === this.n)
this.finalizeKeys();
};
Wallet.prototype.finalizeKeys = function finalizeKeys(key) {
if (this.hdpm) {
this.purposeKeys = utils.sortHDKeys(this.purposeKeys);
for (i = 0; i < this.purposeKeys.length; i++) {
if (utils.isEqual(this.purposeKeys[i].publicKey, this.purposeKey.publicKey)) {
this.cosignerIndex = i;
break;
}
}
this._initAddresses();
return;
}
this.keys = utils.sortKeys(this.keys);
this._initAddresses();
};
Wallet.prototype.removeKey = function removeKey(key) {
var hdKey, index;
if (bcoin.hd.priv.isExtended(key))
key = bcoin.hd.priv(key);
else if (bcoin.hd.pub.isExtended(key))
key = bcoin.hd.pub(key);
if (key instanceof bcoin.keypair)
key = key.hd;
if (key instanceof bcoin.hd.priv)
key = key.hdpub;
if (key instanceof bcoin.hd.pub) {
hdKey = key;
key = hd.publicKey;
}
if (this.hdpm) {
if (!hdKey || !hdKey.isPurpose45())
throw new Error('Must add HD purpose keys to HD wallet.');
index = this.purposeKeys.map(function(pub, i) {
return pub.xpubkey === hdKey.xpubkey ? i : null;
}).filter(function(i) {
return i !== null;
})[0];
if (index == null)
return;
this.purposeKeys.splice(index, 1);
return;
}
key = utils.toBuffer(key);
index = this.keys.map(function(pub, i) {
return utils.isEqual(pub, key) ? i : null;
}).filter(function(i) {
return i !== null;
})[0];
if (index == null)
return;
this.keys.splice(index, 1);
};
Wallet.prototype._init = function init() {
@ -134,7 +390,7 @@ Wallet.prototype._init = function init() {
};
Wallet.prototype.__defineGetter__('primary', function() {
return this.addresses[0];
return this.current;
});
Wallet.prototype._getAddressTable = function() {
@ -171,30 +427,52 @@ Wallet.prototype._addressIndex = function _addressIndex(address) {
return -1;
};
Wallet.prototype.createChangeAddress = function createChangeAddress(options) {
if (!options)
options = {};
Wallet.prototype.createAddress = function createAddress(change, index) {
var self = this;
var key = this.createKey(change, index);
var address;
options.change = true;
var options = {
priv: key.priv,
pub: key.pub,
type: this.type,
subtype: this.subtype,
m: this.m,
n: this.n,
keys: [],
change: change
};
if (this.master) {
options.priv =
this.master.key.hd.deriveChange(this.accountIndex, this.changeDepth++);
if (this.hdpm) {
this.purposeKeys.forEach(function(key, cosignerIndex) {
key = key
.derive(cosignerIndex)
.derive(change ? 1 : 0)
.derive(change ? self.changeDepth : self.addressDepth);
options.keys.push(key.publicKey);
});
this.keys = utils.sortKeys(options.keys);
} else {
this.keys.forEach(function(key, i) {
options.keys.push(key);
});
}
return this.addAddress(options);
};
Wallet.prototype.createNewAddress = function createNewAddress(options) {
if (!options)
options = {};
if (this.master) {
options.priv =
this.master.key.hd.deriveAddress(this.accountIndex, this.addressDepth++);
if (index == null) {
if (this.hd) {
if (change)
this.changeDepth++;
else
this.addressDepth++;
}
}
return this.addAddress(options);
address = this.addAddress(options);
if (!change)
this.current = address;
return address;
};
Wallet.prototype.hasAddress = function hasAddress(address) {
@ -220,12 +498,6 @@ Wallet.prototype.addAddress = function addAddress(address) {
if (this._addressIndex(address) !== -1)
return;
if (address.key.hd && address.key.hd.isMaster) {
assert(!this.master);
this.master = address;
return;
}
if (address._wallet)
address._wallet.removeAddress(address);
@ -282,14 +554,6 @@ Wallet.prototype.removeAddress = function removeAddress(address) {
return address;
};
Wallet.prototype.addKey = function addKey(key, i) {
return this.primary.addKey(key);
};
Wallet.prototype.removeKey = function removeKey(key) {
return this.primary.removeKey(key);
};
Wallet.prototype.getPrivateKey = function getPrivateKey(enc) {
return this.primary.getPrivateKey(enc);
};
@ -305,13 +569,49 @@ Wallet.prototype.getScripthash = function getScripthash() {
Wallet.prototype.getScriptAddress =
Wallet.prototype.getScriptaddress = function getScriptaddress() {
return this.primary.getScriptAddress();
return this.current.getScriptAddress();
};
Wallet.prototype.getPublicKey = function getPublicKey(enc) {
return this.primary.getPublicKey(enc);
};
Wallet.prototype.createKey = function createKey(change, index) {
var key, pub, priv;
if (!this.hd) {
if (this._firstKey) {
key = this._firstKey;
delete this._firstKey;
return key;
}
key = bcoin.ecdsa.genKeyPair();
return {
priv: key.getPrivate().toArray(),
pub: key.getPublic(true, 'array')
};
}
if (index == null)
index = change ? this.changeDepth : this.addressDepth;
if (this.hdpm) {
key = this.purposeKey
.derive(this.cosignerIndex)
.derive(change ? 1 : 0)
.derive(index);
} else {
key = this.accountKey
.derive(change ? 1 : 0)
.derive(index);
}
return {
priv: key.privateKey,
pub: key.publicKey
};
};
Wallet.prototype.getKeyHash =
Wallet.prototype.getKeyhash = function getKeyhash() {
return this.primary.getKeyHash();
@ -327,7 +627,7 @@ Wallet.prototype.getHash = function getHash() {
};
Wallet.prototype.getAddress = function getAddress() {
return this.primary.getAddress();
return this.current.getAddress();
};
Wallet.prototype.ownInput = function ownInput(tx, index) {

View File

@ -74,11 +74,9 @@ describe('Wallet', function() {
it('should multisign/verify TX', function() {
var w = bcoin.wallet({
multisig: {
type: 'multisig',
m: 1,
n: 2
}
type: 'multisig',
m: 1,
n: 2
});
// var k2 = w.getPublicKey().concat(1);
var k2 = bcoin.ecdsa.genKeyPair().getPublic(true, 'array');
@ -265,37 +263,38 @@ describe('Wallet', function() {
it('should verify 2-of-3 p2sh tx', function(cb) {
// Create 3 2-of-3 wallets with our pubkeys as "shared keys"
var w1 = bcoin.wallet({
multisig: {
type: 'scripthash',
m: 2,
n: 3
}
hd: true,
type: 'scripthash',
subtype: 'multisig',
m: 2,
n: 3
});
var w2 = bcoin.wallet({
multisig: {
type: 'scripthash',
m: 2,
n: 3
}
hd: true,
type: 'scripthash',
subtype: 'multisig',
m: 2,
n: 3
});
var w3 = bcoin.wallet({
hd: true,
multisig: {
type: 'scripthash',
m: 2,
n: 3
}
type: 'scripthash',
subtype: 'multisig',
m: 2,
n: 3
});
w3 = bcoin.wallet.fromJSON(w3.toJSON());
// w3 = bcoin.wallet.fromJSON(w3.toJSON());
var receive = bcoin.wallet();
w1.addKey(w2.getPublicKey());
w1.addKey(w3.getPublicKey());
w2.addKey(w1.getPublicKey());
w2.addKey(w3.getPublicKey());
w3.addKey(w1.getPublicKey());
w3.addKey(w2.getPublicKey());
w1.addKey(w2.purposeKey);
w1.addKey(w3.purposeKey);
w2.addKey(w1.purposeKey);
w2.addKey(w3.purposeKey);
w3.addKey(w1.purposeKey);
w3.addKey(w2.purposeKey);
// Our p2sh address
var addr = w1.getAddress();