From b9041a015762bfdd2c06344b3d2a8d835208a183 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sat, 16 Jan 2016 04:50:33 -0800 Subject: [PATCH] sign unknown txs. --- lib/bcoin/script.js | 22 ++++- lib/bcoin/tx.js | 208 ++++++++++++++++++++++++++------------------ lib/bcoin/wallet.js | 156 +++++++++++++++++++-------------- 3 files changed, 236 insertions(+), 150 deletions(-) diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index 50ea24f7..bbe19b90 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -1405,9 +1405,7 @@ script.isNulldata = function isNulldata(s) { if (s.length !== 2) return false; - res = s[0] === 'ret' - && Array.isArray(s[1]) - && s[1].length <= constants.script.maxOpReturn; + res = s[0] === 'ret' && script.isData(s[1]); if (!res) return false; @@ -1659,6 +1657,10 @@ script.isCoinbase = function isCoinbase(s, block, strict) { return coinbase; }; +// Detect script array types. Note: these functions +// are not mutually exclusive. Only use for +// verification, not detection. + script.isHash = function isHash(hash) { if (!utils.isBuffer(hash)) return false; @@ -1683,6 +1685,20 @@ script.isSignature = function isSignature(sig, allowZero) { return sig.length >= 9 && sig.length <= 73; }; +script.isEmpty = function isEmpty(data) { + if (!utils.isBuffer(data)) + return false; + + return data.length === 0; +}; + +script.isData = function isData(data) { + if (!utils.isBuffer(data)) + return false; + + return data.length <= constants.script.maxOpReturn; +}; + // https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki /** * A canonical signature exists of: <30> <02> <02> diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 71e6d480..1807cff1 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -189,7 +189,7 @@ TX.prototype.scriptInput = function scriptInput(index, pub, redeem) { // P2PKH input.script = [ [], pub ]; } else if (bcoin.script.isMultisig(s)) { - // Bare Multisig + // Multisig // Technically we should create m signature slots, // but we create n signature slots so we can order // the signatures properly. @@ -201,6 +201,12 @@ TX.prototype.scriptInput = function scriptInput(index, pub, redeem) { // Fill script with `n` signature slots. for (i = 0; i < n; i++) input.script[i + 1] = []; + } else { + // Likely a non-standard scripthash multisig + // input. Just set up the empty array. Also, + // only allow nonstandard types for scripthash. + if (redeem) + input.script = [ [] ]; } // P2SH requires the redeem script after signatures @@ -313,7 +319,9 @@ TX.prototype.signInput = function signInput(index, key, type) { input.script[0] = signature; return true; - } else if (bcoin.script.isPubkeyhash(s)) { + } + + if (bcoin.script.isPubkeyhash(s)) { // P2PKH // Something is wrong. Abort. @@ -331,100 +339,134 @@ TX.prototype.signInput = function signInput(index, key, type) { input.script[0] = signature; return true; - } else if (bcoin.script.isMultisig(s)) { + } + + if (bcoin.script.isMultisig(s)) { // Multisig + // Grab the redeem script's keys to figure + // out where our key should go. + keys = s.slice(1, -2); + // Grab `m` value (number of sigs required). m = s[0]; // Grab `n` value (number of keys). n = s[s.length - 2]; - - // Something is very wrong here. Abort. - if (len - 1 > n) + } else { + // Only allow non-standard signing for + // scripthash. + if (len !== input.script.length - 1) return false; - // Count the number of current signatures. - signatures = 0; - for (i = 1; i < len; i++) { - if (Array.isArray(input.script[i]) && input.script[i].length) - signatures++; + keys = []; + + for (i = 0; i < s.length; i++) { + if (bcoin.script.isKey(s[i])) + keys.push(s[i]); } - // Signatures are already finalized. - if (signatures === m && len - 1 === m) - return true; - - // This can happen in a case where another - // implementation adds signatures willy-nilly - // or by `m`. Add some signature slots for - // us to use. - while (len - 1 < n) { - input.script.splice(len, 0, []); - len++; - } - - // Grab the redeem script's keys to figure - // out where our key should go. - keys = s.slice(1, -2); - - // Find the key index so we can place - // the signature in the same index. - for (ki = 0; ki < keys.length; ki++) { - if (utils.isEqual(pub, keys[ki])) - break; - } - - // Our public key is not in the prev_out - // script. We tried to sign a transaction - // that is not redeemable by us. - if (ki === keys.length) - return false; - - // Offset key index by one to turn it into - // "sig index". Accounts for OP_0 byte at - // the start. - ki++; - - // Add our signature to the correct slot - // and increment the total number of - // signatures. - if (ki < len && signatures < m) { - if (Array.isArray(input.script[ki]) && !input.script[ki].length) { - input.script[ki] = signature; - signatures++; - } - } - - // All signatures added. Finalize. - if (signatures >= m) { - // Remove empty slots left over. - for (i = len - 1; i >= 1; i--) { - if (Array.isArray(input.script[i]) && !input.script[i].length) { - input.script.splice(i, 1); - len--; - } - } - - // Remove signatures which are not required. - // This should never happen except when dealing - // with implementations that potentially handle - // signature slots differently. - while (signatures > m) { - input.script.splice(len - 1, 1); - signatures--; - len--; - } - - // Sanity checks. - assert.equal(signatures, m); - assert.equal(len - 1, m); - } - - return signatures === m; + n = keys.length; + m = n; } - return false; + // Something is very wrong here. Abort. + if (len - 1 > n) + return false; + + // Count the number of current signatures. + signatures = 0; + for (i = 1; i < len; i++) { + if (bcoin.script.isSignature(input.script[i])) + signatures++; + } + + // Signatures are already finalized. + if (signatures === m && len - 1 === m) + return true; + + // This can happen in a case where another + // implementation adds signatures willy-nilly + // or by `m`. Add some signature slots for + // us to use. + while (len - 1 < n) { + input.script.splice(len, 0, []); + len++; + } + + // Find the key index so we can place + // the signature in the same index. + for (ki = 0; ki < keys.length; ki++) { + if (utils.isEqual(pub, keys[ki])) + break; + } + + // Our public key is not in the prev_out + // script. We tried to sign a transaction + // that is not redeemable by us. + if (ki === keys.length) + return false; + + // Offset key index by one to turn it into + // "sig index". Accounts for OP_0 byte at + // the start. + ki++; + + // Add our signature to the correct slot + // and increment the total number of + // signatures. + if (ki < len && signatures < m) { + if (bcoin.script.isEmpty(input.script[ki])) { + input.script[ki] = signature; + signatures++; + } + } + + // All signatures added. Finalize. + if (signatures >= m) { + // Remove empty slots left over. + for (i = len - 1; i >= 1; i--) { + if (bcoin.script.isEmpty(input.script[i])) { + input.script.splice(i, 1); + len--; + } + } + + // Remove signatures which are not required. + // This should never happen except when dealing + // with implementations that potentially handle + // signature slots differently. + while (signatures > m) { + input.script.splice(len - 1, 1); + signatures--; + len--; + } + + // Sanity checks. + assert.equal(signatures, m); + assert.equal(len - 1, m); + } + + return signatures === m; +}; + +TX.prototype.finalize = function finalize() { + var i, input, s, len, j; + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + s = input.out.tx.getSubscript(input.out.index); + len = s.length; + + if (bcoin.script.isScripthash(s)) + len--; + + for (j = len - 1; j >= 1; j--) { + if (bcoin.script.isEmpty(input.script[j])) { + input.script.splice(j, 1); + len--; + } + } + } }; TX.prototype.scriptSig = function scriptSig(index, key, pub, redeem, type) { diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index e350f681..232c6ff4 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -77,21 +77,59 @@ function Wallet(options, passphrase) { this.key = bcoin.ecdsa.genKeyPair(); } - this.type = 'pubkeyhash'; + // 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 = []; - this.m = 1; - this.n = 1; + this.m = options.m || 1; + this.n = options.n || 1; - this.prefix = 'bt/wallet/' + this.getKeyAddress() + '/'; + if (this.n > 1) { + if (this.type !== 'multisig') + this.type = 'scripthash'; + if (this.type === 'scripthash') + this.subtype = 'multisig'; + } - this.multisig(options.multisig || {}); + if (network.prefixes[this.type] == null) + throw new Error('Unknown prefix: ' + this.type); + + this.nmax = this.type === 'scripthash' + ? (this.compressed ? 15 : 7) + : 3; + + 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); + + this.addKey(this.getPublicKey()); + + (options.keys || []).forEach(function(key) { + this.addKey(key); + }, this); if (this.redeem) { if (!bcoin.script.isEncoded(this.redeem)) this.redeem = bcoin.script.encode(this.redeem); this.type = 'scripthash'; + this.subtype = null; } + this.prefix = 'bt/wallet/' + this.getKeyAddress() + '/'; + this.tx = new bcoin.txPool(this); this._init(); @@ -132,40 +170,6 @@ Wallet.prototype._init = function init() { }); }; -Wallet.prototype.multisig = function multisig(options) { - var pub = this.getPublicKey(); - - options.type = options.type || options.prefix; - options.keys = options.keys || options.pubkeys || []; - - this.type = options.type || 'pubkeyhash'; - this.keys = []; - this.m = options.m || 1; - this.n = options.n || 1; - this.nmax = this.type === 'scripthash' - ? (this.compressed ? 15 : 7) - : 3; - - this.addKey(pub); - - if (network.prefixes[this.type] == null) - throw new Error('Unknown prefix: ' + this.type); - - options.keys.forEach(function(key) { - this.addKey(key); - }, this); - - // Use p2sh multisig by default - if (!options.type && (this.keys.length > 1 || this.n > 1)) - this.type = 'scripthash'; - - 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); -}; - Wallet.prototype.addKey = function addKey(key) { key = utils.toBuffer(key); @@ -243,16 +247,39 @@ Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { }; Wallet.prototype.getScript = function getScript() { + if (this.type !== 'scripthash') + return; + if (this.redeem) return this.redeem.slice(); + + if (this.subtype === 'pubkey') + return bcoin.script.encode([this.getPublicKey(), 'checksig']); + + if (this.subtype === 'pubkeyhash') { + return bcoin.script.encode([ + 'dup', + 'hash160', + this.getKeyHash(), + 'eqverify', + 'checksig' + ]); + } + return bcoin.script.encode(bcoin.script.redeem(this.keys, this.m, this.n)); }; Wallet.prototype.getScriptHash = function getScriptHash() { + if (this.type !== 'scripthash') + return; + return utils.ripesha(this.getScript()); }; Wallet.prototype.getScriptAddress = function getScriptAddress() { + if (this.type !== 'scripthash') + return; + return Wallet.hash2addr(this.getScriptHash(), this.type); }; @@ -370,8 +397,10 @@ Wallet.prototype.ownOutput = function ownOutput(tx, index) { if (bcoin.script.isMultisig(s, keys)) return true; - if (bcoin.script.isScripthash(s, scripthash)) - return true; + if (scripthash) { + if (bcoin.script.isScripthash(s, scripthash)) + return true; + } return false; }, this); @@ -407,8 +436,10 @@ Wallet.prototype.ownInput = function ownInput(tx, index) { // if (bcoin.script.isMultisigInput(input.script, keys, tx, i)) // return true; - if (bcoin.script.isScripthashInput(input.script, redeem)) - return true; + if (redeem) { + if (bcoin.script.isScripthashInput(input.script, redeem)) + return true; + } if (!input.out.tx) return false; @@ -424,8 +455,10 @@ Wallet.prototype.ownInput = function ownInput(tx, index) { if (bcoin.script.isMultisig(s, keys)) return true; - if (bcoin.script.isScripthash(s, scripthash)) - return true; + if (scripthash) { + if (bcoin.script.isScripthash(s, scripthash)) + return true; + } return false; }, this); @@ -589,7 +622,7 @@ Wallet.prototype.toAddress = function toAddress() { Wallet.prototype.toJSON = function toJSON(encrypt) { return { v: 2, - type: 'wallet', + name: 'wallet', network: network.type, encrypted: encrypt ? true : false, label: this.label, @@ -599,16 +632,15 @@ Wallet.prototype.toJSON = function toJSON(encrypt) { priv: encrypt ? encrypt(this.getPrivateKey('base58')) : this.getPrivateKey('base58'), - tx: this.tx.toJSON(), xprivkey: this.hd ? (encrypt ? encrypt(this.hd.xprivkey) : this.hd.xprivkey) : null, - multisig: this.n > 1 ? { - type: this.type, - keys: this.keys.map(utils.toBase58), - m: this.m, - n: this.n - } : undefined + type: this.type, + subtype: this.subtype, + keys: this.keys.map(utils.toBase58), + m: this.m, + n: this.n, + tx: this.tx.toJSON() }; }; @@ -616,7 +648,7 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { var priv, pub, xprivkey, multisig, compressed, key, w; assert.equal(json.v, 2); - assert.equal(json.type, 'wallet'); + assert.equal(json.name, 'wallet'); if (json.network) assert.equal(json.network, network.type); @@ -647,15 +679,6 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { compressed = pub[0] !== 0x04; } - if (json.multisig) { - multisig = { - type: json.multisig.type, - keys: json.multisig.keys.map(utils.fromBase58), - m: json.multisig.m, - n: json.multisig.n - }; - } - if (json.xprivkey) { xprivkey = json.xprivkey; if (json.encrypted) @@ -668,7 +691,12 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { priv: priv, pub: pub, compressed: compressed, - multisig: multisig + multisig: multisig, + type: json.type, + subtype: json.subtype, + keys: json.keys.map(utils.fromBase58), + m: json.m, + n: json.n }); w.tx.fromJSON(json.tx);