From 7016991366d3b6f9de7039ce1987b73033efcec6 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Mon, 7 Dec 2015 18:43:52 -0800 Subject: [PATCH] order keys and signatures for multisig correctly. --- lib/bcoin/script.js | 5 +++ lib/bcoin/tx.js | 88 +++++++++++++++++++++++++++++++++++---------- lib/bcoin/utils.js | 2 ++ lib/bcoin/wallet.js | 22 +++++++----- test/wallet-test.js | 2 ++ 5 files changed, 93 insertions(+), 26 deletions(-) diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index 8db84dd4..615d1e96 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -1,4 +1,5 @@ var bcoin = require('../bcoin'); +var bn = require('bn.js'); var constants = bcoin.protocol.constants; var utils = bcoin.utils; var assert = bcoin.utils.assert; @@ -265,6 +266,10 @@ script.multisig = function(keys, m, n) { // [ [ n ], 'checkmultisig' ] // ); + 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, diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 70ffe2c9..d677b5c2 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -144,25 +144,32 @@ TX.prototype.signature = function(input, key, type) { }; // Build the scriptSigs for inputs, excluding the signatures -TX.prototype.scriptInput = function(input, pub, nsigs) { +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 m = s[0]; + var n = s[s.length - 2]; // If using pushdata instead of OP_1-16: - if (Array.isArray(m)) - m = m[0]; - for (var i = 0; i < m; i++) + if (Array.isArray(n)) + n = n[0]; + for (var i = 0; i < n; i++) input.script[i + 1] = []; return; } @@ -171,8 +178,8 @@ TX.prototype.scriptInput = function(input, pub, nsigs) { // p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script] if (bcoin.script.isScripthash(s)) { input.script = [ [] ]; - var m = pub[0] - constants.opcodes['1'] + 1; - for (var i = 0; i < m; i++) + var n = pub[pub.length - 2] - constants.opcodes['1'] + 1; + for (var i = 0; i < n; i++) input.script[i + 1] = []; // P2SH requires the redeem script after signatures input.script.push(pub); @@ -213,20 +220,65 @@ TX.prototype.signInput = function(input, key, type) { // 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)) + if (bcoin.script.isScripthash(s)) { len--; + redeem = bcoin.script.decode(input.script[input.script.length - 1]); + } else { + redeem = s; + } + var m = redeem[0]; + var n = redeem[s.length - 2]; + // If using pushdata instead of OP_1-16: + if (Array.isArray(m)) + m = m[0]; + if (Array.isArray(n)) + n = n[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++) { - // Already signed - if (utils.isEqual(input.script[i], signature)) - break; + if (input.script[i].length) { + totalSigs++; + continue; + } - if (input.script[i].length === 0) { + if (i - 1 === ki) { + if (totalSigs >= m) + continue; input.script[i] = signature; - break; + totalSigs++; } } + + // All signatures added. Finalize by removing empty slots. + if (totalSigs >= m) { + for (var i = len - 1; i >= 1; i--) { + if (!input.script[i].length) + input.script.splice(i, 1); + } + } + return; } @@ -234,9 +286,9 @@ TX.prototype.signInput = function(input, key, type) { }; // Build the scriptSig and sign it -TX.prototype.scriptSig = function(input, key, pub, type, nsigs) { +TX.prototype.scriptSig = function(input, key, pub, type) { // Build script for input - this.scriptInput(input, pub, nsigs); + this.scriptInput(input, pub); // Sign input this.signInput(input, key, type); @@ -403,7 +455,7 @@ TX.prototype.maxSize = function maxSize() { var size = copy.render().length; // Add size for signatures and public keys - copy.inputs.forEach(function(input) { + copy.inputs.forEach(function(input, i) { // Get the previous output's script // var s = input.out.tx.outputs[input.out.index].script; @@ -435,7 +487,7 @@ TX.prototype.maxSize = function maxSize() { if (bcoin.script.isScripthash(s)) { var script = this.inputs[i].script; var redeem, m, n; - if (script) { + if (script.length) { redeem = script[script.length - 1]; m = redeem[0]; n = redeem[redeem.length - 2]; @@ -475,7 +527,7 @@ TX.prototype.maxSize = function maxSize() { // 2. Add inputs with utxos and change output: // - this.fillUnspent(unspentItems, [changeAddr]); // 3. Fill input scripts (for each input): -// - this.scriptInput(input, pub, [nsigs]) +// - this.scriptInput(input, pub) // - this.signInput(input, key, [sigHashType]) TX.prototype.utxos = function utxos(unspent) { // NOTE: tx should be prefilled with all outputs diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index faa13036..f96ded1c 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -417,6 +417,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); }); diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index afe2d1e4..bc12be50 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -40,6 +40,8 @@ function Wallet(options, passphrase) { } else if (options.hd) { this.hd = bcoin.hd.priv(options); this.key = this.hd.pair; + } else if (options.key) { + this.key = options.key; } else if (options.passphrase) { this.key = bcoin.ecdsa.genKeyPair({ pers: options.scope, @@ -74,7 +76,7 @@ function Wallet(options, passphrase) { throw new Error('n ranges between 1 and 7'); } - if (this.sharedKeys.length !== this.m - 1) { + if (this.sharedKeys.length < this.m - 1) { throw new Error(this.m + ' public keys required'); } @@ -153,7 +155,7 @@ Wallet.prototype.getPublicKey = function getPublicKey(enc) { if (this.addressType === 'p2sh') { var keys = this.getPublicKeys(); - pub = bcoin.script.encode(bcoin.script.multisig(keys, m, n)); + pub = bcoin.script.encode(bcoin.script.multisig(keys, this.m, this.n)); } if (enc === 'base58') @@ -166,10 +168,15 @@ Wallet.prototype.getPublicKey = function getPublicKey(enc) { Wallet.prototype.getPublicKeys = function() { var keys = this.sharedKeys.slice().map(utils.toKeyArray); - if (keys.length < this.m) { - var pub = this.key.getPublic(this.compressed, 'array'); - keys.push(pub); - } + + // if (keys.length < this.m) { + var pub = this.key.getPublic(this.compressed, 'array'); + keys.push(pub); + + keys = keys.sort(function(a, b) { + return new bn(a).cmp(new bn(b)) > 0; + }); + return keys; }; @@ -286,7 +293,6 @@ Wallet.prototype.sign = function sign(tx, type, inputs) { var pub = this.getPublicKey(); var key = this.key; - var nsigs = this.m; inputs = inputs || tx.inputs; @@ -296,7 +302,7 @@ Wallet.prototype.sign = function sign(tx, type, inputs) { if (!input.out.tx || !this.ownOutput(input.out.tx)) return false; - tx.scriptSig(input, key, pub, type, nsigs); + tx.scriptSig(input, key, pub, type); return true; }, this); diff --git a/test/wallet-test.js b/test/wallet-test.js index bf48ce63..61d4a411 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -46,6 +46,7 @@ describe('Wallet', function() { it('should multisign/verify TX', function() { var w = bcoin.wallet(); + var k2 = bcoin.wallet().getPublicKey(); // Input transcation var src = bcoin.tx({ @@ -53,6 +54,7 @@ describe('Wallet', function() { value: 5460 * 2, minSignatures: 1, keys: [ w.getPublicKey(), w.getPublicKey().concat(1) ] + // keys: [ w.getPublicKey(), k2 ] }, { value: 5460 * 2, address: w.getAddress() + 'x'