diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index b3ce3946..f52804e7 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -22,6 +22,8 @@ exports.genesis = { // address versions exports.addr = { normal: 0, + p2pkh: 0, + multisig: 0, p2sh: 5 }; @@ -157,6 +159,11 @@ 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, diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index cfd558f3..166dad63 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -151,6 +151,10 @@ script.execute = function execute(s, stack, tx) { if (type !== 1) return false; + // XXX Deal with different hashTypes besides `all` + // if (typeof tx === 'function') + // tx = tx(constants.rhashType[type]); + var res = bcoin.ecdsa.verify(tx, sig.slice(0, -1), pub); if (o === 'checksigverify') { if (!res) @@ -195,6 +199,10 @@ script.execute = function execute(s, stack, tx) { if (type !== 1) return false; + // XXX Deal with different hashTypes besides `all` + // if (typeof tx === 'function') + // tx = tx(constants.rhashType[type]); + var res = false; for (; !res && j < n; j++) res = bcoin.ecdsa.verify(tx, sig.slice(0, -1), keys[j]); @@ -221,6 +229,23 @@ script.execute = function execute(s, stack, tx) { return true; }; +script.redemption = function(publicKeys, m, n) { + if (publicKeys.length !== m) { + throw new Error('wrong amount of pubkeys for redeem script'); + } + var mcode = constants.opcodes['1'] + (m - 1); + var ncode = constants.opcodes['1'] + (n - 1); + var redemption = []; + redemption.push(mcode); + publicKeys.forEach(function(pubkey) { + redemption.push(pubkey.length); + redemption = redemption.concat(pubkey); + }); + redemption.push(ncode); + redemption.push(constants.opcodes.checkmultisig); + return redemption; +}; + script.isPubkeyhash = function isPubkeyhash(s, hash) { if (s.length !== 5) return false; @@ -296,14 +321,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) { diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index ff56709d..ea1909fa 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -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)) @@ -111,6 +112,110 @@ TX.prototype._inputIndex = function _inputIndex(hash, index) { return -1; }; +TX.prototype.signature = function(input, key) { + // 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).toDER(); + + // Add the sighash as a single byte to the signature + signature = signature.concat(constants.hashType[type]); + + return signature; +}; + +// Build the scriptSigs for inputs, excluding the signatures +TX.prototype.scriptInput = function(input, pub, nsigs) { + // Get the previous output's subscript + var s = input.out.tx.getSubscript(input.out.index); + + // P2PKH and simple tx + if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) { + input.script = [ constants.opcodes['0'], pub ]; + return; + } + + // Multisig + // raw format: OP_FALSE [sig-1] [sig-2] ... + if (bcoin.script.isMultisig(s)) { + if (!nsigs) { + throw new Error('`nsigs` is required for multisig'); + } + input.script = [ constants.opcodes['false'] ]; + for (var i = 0; i < nsigs; i++) { + input.script[i + 1] = constants.opcodes['0']; + } + return; + } + + // P2SH multisig + // p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script] + if (bcoin.script.isScripthash(s)) { + input.script = [ constants.opcodes['false'] ]; + var m = pub[0] - constants.opcodes['1'] + 1; + for (var i = 0; i < m; i++) { + input.script[i + 1] = constants.opcodes['0']; + } + // P2SH requires the redeem script after signatures + if (bcoin.script.isScripthash(s)) { + input.script.push(pub); + } + return; + } + + throw new Error('could not identify prev_out type'); +}; + +// Sign the now-built scriptSigs +TX.prototype.signInput = function(input, key) { + // 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).toDER(); + + // Add the sighash as a single byte to the signature + signature = signature.concat(constants.hashType[type]); + + // P2PKH and simple tx + if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) { + input.script[0] = signature; + return; + } + + // Multisig + // empty array == OP_FALSE == OP_0 + // 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 l = input.script.length; + if (bcoin.script.isScripthash(s)) { + l--; + } + for (var i = 0; i < l; i++) { + input.script[i + 1] = signature; + } + } +}; + +// Build the scriptSig and sign it +TX.prototype.scriptSig = function(input, key, pub, nsigs) { + // Build script for input + tx.scriptInput(input, pub, nsigs); + + // Sign input + tx.signInput(input, key); + + return this.input.script; +}; + TX.prototype.input = function input(i, index) { this._input(i, index); return this; @@ -155,7 +260,7 @@ TX.prototype.out = function out(output, value) { // outputs: [ [ 2 ], 'key1', 'key2', [ 2 ], 'checkmultisig' ] // in reality: // outputs: [ [ 2 ], [0,1,...], [2,3,...], [ 2 ], 'checkmultisig' ] - } else if (bcoin.wallet.validAddress(output.address, 'p2sh')) { + } else if (bcoin.wallet.validateAddress(output.address, 'p2sh')) { // p2sh transaction // https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki // hash160 [20-byte-redeemscript-hash] equal @@ -224,6 +329,9 @@ TX.prototype.verify = function verify(index, force) { var subscript = input.out.tx.getSubscript(input.out.index); var hash = this.subscriptHash(i, subscript, 'all'); + // XXX Deal with different hashTypes besides `all` + // var hash = this.subscriptHash.bind(this, i, subscript); + var stack = []; bcoin.script.execute(input.script, stack); var prev = input.out.tx.outputs[input.out.index].script; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 0fd52154..78309e2f 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -29,7 +29,7 @@ function Wallet(options, passphrase) { this.key = null; this.loaded = false; this.lastTs = 0; - this.publicKeys = options.publicKeys; + this.sharedKeys = options.sharedKeys; if (options.priv instanceof bcoin.hd.priv) { this.hd = options.priv; @@ -58,24 +58,28 @@ function Wallet(options, passphrase) { this.fee = 10000; this.dust = 5460; + this.addressType = options.addressType || 'normal'; + + // Multisig + this.sharedKeys = (options.sharedKeys || []).map(utils.toKeyArray); this.m = options.m || 1; this.n = options.n || 1; - this.publicKeys = options.publicKeys; - if (this.publicKeys) { - if (this.publicKeys.length < this.n) { - this.publicKeys.push(this.getPublicKey()); - } + // 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.publicKeys.length !== this.n) { - throw new Error(this.n + ' public keys required'); - } + 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'); } this._init(); @@ -142,7 +146,12 @@ Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { }; Wallet.prototype.getPublicKey = function getPublicKey(enc) { - var pub = this.key.getPublic(this.compressed, 'array'); + var pub; + if (this.addressType === 'p2sh') + pub = this.getRedemption(); + else + pub = this.key.getPublic(this.compressed, 'array'); + if (enc === 'base58') return utils.toBase58(pub); else if (enc === 'hex') @@ -162,7 +171,7 @@ Wallet.prototype.getAddress = function getAddress() { Wallet.hash2addr = function hash2addr(hash, version) { hash = utils.toArray(hash, 'hex'); - version = constants.addr[version || 'normal']; + version = constants.addr[version || this.addressType]; hash = [ version ].concat(hash); var addr = hash.concat(utils.checksum(hash)); @@ -173,7 +182,7 @@ Wallet.addr2hash = function addr2hash(addr, version) { if (!Array.isArray(addr)) addr = utils.fromBase58(addr); - version = constants.addr[version || 'normal']; + version = constants.addr[version || this.addressType]; if (addr.length !== 25) return []; @@ -187,15 +196,8 @@ Wallet.addr2hash = function addr2hash(addr, version) { return addr.slice(1, -4); }; -Wallet.validAddress = function validAddr(addr, version) { - if (!addr) - return false; - - return !!Wallet.addr2hash(addr, version).length; -}; - -Wallet.prototype.validateAddress = function validateAddress(addr) { - var p = Wallet.addr2hash(addr); +Wallet.prototype.validateAddress = function validateAddress(addr, version) { + var p = Wallet.addr2hash(addr, version); return p.length !== 0; }; Wallet.validateAddress = Wallet.prototype.validateAddress; @@ -219,10 +221,8 @@ Wallet.prototype.ownOutput = function ownOutput(tx, index) { if (bcoin.script.isMultisig(s, key)) return true; - if (bcoin.script.isScripthash(s) - && utils.isEqual(s[1], this.getP2SHHash())) { + if (bcoin.script.isScripthash(s, hash)) return true; - } return false; }, this); @@ -254,10 +254,8 @@ Wallet.prototype.ownInput = function ownInput(tx, index) { if (bcoin.script.isMultisig(s, key)) return true; - if (bcoin.script.isScripthash(s) - && utils.isEqual(s[1], this.getP2SHHash())) { + if (bcoin.script.isScripthash(s, hash)) return true; - } return false; }, this); @@ -308,8 +306,6 @@ Wallet.prototype.sign = function sign(tx, type, inputs, off) { // 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)) { - // XXX Check own? - // || (bcoin.script.isScripthash(s) && utils.isEqual(s[1], this.getP2SHHash())) { if (!input.script || !input.script.length) { input.script = [ [], signature ]; } else if (!~input.script.indexOf(signature)) { @@ -320,7 +316,7 @@ Wallet.prototype.sign = function sign(tx, type, inputs, off) { // P2SH requires a redeem script after signatures if (bcoin.script.isScripthash(s)) { if (input.script.length - 1 === this.m) { - input.script.push(this.getP2SHRedemption()); + input.script.push(this.getRedemption()); } } @@ -330,6 +326,30 @@ Wallet.prototype.sign = function sign(tx, type, inputs, off) { return inputs.length; }; +Wallet.prototype.sign = function sign(tx, type, inputs) { + if (!type) + type = 'all'; + + var pub = this.getPublicKey(); + var key = this.key; + var nsigs = this.m; + + inputs = inputs || tx.inputs; + + // Add signature script to each input + inputs = inputs.filter(function(input) { + // Filter inputs that this wallet own + if (!input.out.tx || !this.ownOutput(input.out.tx)) + return false; + + tx.scriptSig(input, key, pub, nsigs); + + return true; + }, this); + + return inputs.length; +}; + Wallet.prototype.addTX = function addTX(tx, block) { return this.tx.add(tx); }; @@ -445,56 +465,29 @@ Wallet.prototype.getChange = function fill(tx) { }; /** - * P2SH (and Multisig) + * P2SH+Multisig Redemption */ -Wallet.prototype.getP2SHHash = function() { - return this.getP2SH().hash; -}; - -Wallet.prototype.getP2SHAddress = function() { - return this.getP2SH().address; -}; - -Wallet.prototype.getP2SHRedemption = function() { - return this.getP2SH().redemption; -}; - -Wallet.prototype.getP2SH = function(redeem) { - this.publicKeys = this.publicKeys.map(function(key) { - return utils.toKeyArray(key); - }); - var redemption = redeem || this._createMultisigRedemption(); - var hash = utils.ripasha(redemption); - return { - hash: hash, - address: Wallet.hash2addr(hash, 'p2sh'), - redemption: redemption - }; -}; - -Wallet.prototype._createMultisigRedemption = function() { - var publicKeys = this.publicKeys; - var mcode = constants.opcodes['1'] + (this.m - 1); - var ncode = constants.opcodes['1'] + (this.n - 1); - var redemption = []; - redemption.push(mcode); - this.publicKeys.forEach(function(pubkey) { - redemption.push(pubkey.length); - redemption = redemption.concat(pubkey); - }, this); - redemption.push(ncode); - redemption.push(constants.opcodes.checkmultisig); - return redemption; +Wallet.prototype.getRedemption = function() { + var sharedKeys = this.sharedKeys.slice().map(utils.toKeyArray); + if (sharedKeys.length < this.m) { + var pub = this.key.getPublic(this.compressed, 'array'); + sharedKeys.push(pub); + } + return bcoin.script.redemption(sharedKeys, m, n); }; Wallet.prototype.toJSON = function toJSON() { return { v: 1, type: 'wallet', - pub: this.getPublicKey('base58'), + pub: utils.toBase58(this.key.getPublic(this.compressed, 'array')), priv: this.getPrivateKey('base58'), - tx: this.tx.toJSON() + tx: this.tx.toJSON(), + addressType: this.addressType, + sharedKeys: utils.toBase58(this.sharedKeys), + m: this.m, + n: this.n }; }; @@ -528,7 +521,11 @@ Wallet.fromJSON = function fromJSON(json) { var w = new Wallet({ priv: priv, pub: pub, - compressed: compressed + compressed: compressed, + addressType: json.addressType, + sharedKeys: json.sharedKeys, + m: json.m, + n: json.n }); w.tx.fromJSON(json.tx);