From 62ea45a524b0f06546596a9d0221ca31a0ba7215 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 18 Mar 2015 16:54:16 -0300 Subject: [PATCH 1/5] refactor Address<->Script relation --- lib/address.js | 70 ++++++++++++++++++++++++++++++------------- lib/errors/spec.js | 3 ++ lib/publickey.js | 2 +- lib/script/script.js | 32 ++++++++++++-------- test/address.js | 45 ++++++++++++++++------------ test/script/script.js | 39 +++++++++++++++++++----- 6 files changed, 131 insertions(+), 60 deletions(-) diff --git a/lib/address.js b/lib/address.js index 4d74e4b..31ca3fc 100644 --- a/lib/address.js +++ b/lib/address.js @@ -2,10 +2,13 @@ var _ = require('lodash'); var $ = require('./util/preconditions'); +var errors = require('./errors'); var Base58Check = require('./encoding/base58check'); var Networks = require('./networks'); var Hash = require('./crypto/hash'); var JSUtil = require('./util/js'); +var Script = require('./script'); +var PublicKey = require('./publickey'); /** * Instantiate an address from an address String or Buffer, a public key or script hash Buffer, @@ -91,8 +94,6 @@ function Address(data, network, type) { * @returns {Object} An "info" object with "type", "network", and "hashBuffer" */ Address.prototype._classifyArguments = function(data, network, type) { - var PublicKey = require('./publickey'); - var Script = require('./script'); /* jshint maxcomplexity: 10 */ // transform and validate input data if ((data instanceof Buffer || data instanceof Uint8Array) && data.length === 20) { @@ -122,7 +123,7 @@ Address.PayToScriptHash = 'scripthash'; * @returns {Object} An object with keys: hashBuffer * @private */ -Address._transformHash = function(hash){ +Address._transformHash = function(hash) { var info = {}; if (!(hash instanceof Buffer) && !(hash instanceof Uint8Array)) { throw new TypeError('Address supplied is not a buffer.'); @@ -159,7 +160,7 @@ Address._transformObject = function(data) { * @returns {Object} An object with keys: network and type * @private */ -Address._classifyFromVersion = function(buffer){ +Address._classifyFromVersion = function(buffer) { var version = {}; version.network = Networks.get(buffer[0]); switch (buffer[0]) { // the version byte @@ -191,7 +192,7 @@ Address._classifyFromVersion = function(buffer){ * @returns {Object} An object with keys: hashBuffer, network and type * @private */ -Address._transformBuffer = function(buffer, network, type){ +Address._transformBuffer = function(buffer, network, type) { /* jshint maxcomplexity: 9 */ var info = {}; if (!(buffer instanceof Buffer) && !(buffer instanceof Uint8Array)) { @@ -208,7 +209,7 @@ Address._transformBuffer = function(buffer, network, type){ throw new TypeError('Address has mismatched network type.'); } - if (!bufferVersion.type || ( type && type !== bufferVersion.type )) { + if (!bufferVersion.type || (type && type !== bufferVersion.type)) { throw new TypeError('Address has mismatched type.'); } @@ -225,8 +226,7 @@ Address._transformBuffer = function(buffer, network, type){ * @returns {Object} An object with keys: hashBuffer, type * @private */ -Address._transformPublicKey = function(pubkey){ - var PublicKey = require('./publickey'); +Address._transformPublicKey = function(pubkey) { var info = {}; if (!(pubkey instanceof PublicKey)) { throw new TypeError('Address must be an instance of PublicKey.'); @@ -243,11 +243,10 @@ Address._transformPublicKey = function(pubkey){ * @returns {Object} An object with keys: hashBuffer, type * @private */ -Address._transformScript = function(script, network){ - var Script = require('./script'); +Address._transformScript = function(script, network) { var info = {}; if (!(script instanceof Script)) { - throw new TypeError('Address must be an instance of Script.'); + throw new TypeError('script must be an instance of Script.'); } if (script.isScriptHashOut()) { info.hashBuffer = script.getData(); @@ -255,9 +254,16 @@ Address._transformScript = function(script, network){ } else if (script.isPublicKeyHashOut()) { info.hashBuffer = script.getData(); info.type = Address.PayToPublicKeyHash; - } else { - info.hashBuffer = Hash.sha256ripemd160(script.toBuffer()); + } else if (script.isScriptHashIn()) { + // hash the redeemscript found at the end of the scriptSig + info.hashBuffer = Hash.sha256ripemd160(script.chunks[script.chunks.length - 1].buf); info.type = Address.PayToScriptHash; + } else if (script.isPublicKeyHashIn()) { + // hash the publickey found in the scriptSig + info.hashBuffer = Hash.sha256ripemd160(script.chunks[1].buf); + info.type = Address.PayToPublicKeyHash; + } else { + throw new errors.Script.CantDeriveAddress(script); } info.network = Networks.get(network) || Networks.defaultNetwork; return info; @@ -276,9 +282,8 @@ Address._transformScript = function(script, network){ * @return {Address} */ Address.createMultisig = function(publicKeys, threshold, network) { - var Script = require('./script'); - network = network || publicKeys[0].network; - return new Address(Script.buildMultisigOut(publicKeys, threshold), network || Networks.defaultNetwork); + network = network || publicKeys[0].network || Networks.defaultNetwork; + return Address.payingTo(Script.buildMultisigOut(publicKeys, threshold), network); }; /** @@ -290,8 +295,8 @@ Address.createMultisig = function(publicKeys, threshold, network) { * @returns {Object} An object with keys: hashBuffer, network and type * @private */ -Address._transformString = function(data, network, type){ - if( typeof(data) !== 'string' ) { +Address._transformString = function(data, network, type) { + if (typeof(data) !== 'string') { throw new TypeError('Address supplied is not a string.'); } var addressBuffer = Base58Check.decode(data); @@ -306,7 +311,7 @@ Address._transformString = function(data, network, type){ * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' * @returns {Address} A new valid and frozen instance of an Address */ -Address.fromPublicKey = function(data, network){ +Address.fromPublicKey = function(data, network) { var info = Address._transformPublicKey(data); network = network || Networks.defaultNetwork; return new Address(info.hashBuffer, network, info.type); @@ -332,12 +337,35 @@ Address.fromPublicKeyHash = function(hash, network) { * @returns {Address} A new valid and frozen instance of an Address */ Address.fromScriptHash = function(hash, network) { + $.checkArgument(hash, 'hash parameter is required'); var info = Address._transformHash(hash); return new Address(info.hashBuffer, network, Address.PayToScriptHash); }; /** - * Instantiate an address from a Script + * Builds a p2sh address paying to script. This will hash the script and + * use that to create the address. + * If you want to extract an address associated with a script instead, + * see {{Address#fromScript}} + * + * @param {Script} script - An instance of Script + * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' + * @returns {Address} A new valid and frozen instance of an Address + */ +Address.payingTo = function(script, network) { + $.checkArgument(script, 'script is required'); + $.checkArgument(script instanceof Script, 'script must be instance of Script'); + + return Address.fromScriptHash(Hash.sha256ripemd160(script.toBuffer()), network); +}; + +/** + * Extract address from a Script. The script must be of one + * of the following types: p2pkh input, p2pkh output, p2sh input + * or p2sh output. + * This will analyze the script and extract address information from it. + * If you want to transform any script to a p2sh Address paying + * to that script's hash instead, use {{Address#payingTo}} * * @param {Script} script - An instance of Script * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' @@ -494,7 +522,7 @@ Address.prototype.toString = function() { * @returns {string} Bitcoin address */ Address.prototype.inspect = function() { - return ''; + return ''; }; module.exports = Address; diff --git a/lib/errors/spec.js b/lib/errors/spec.js index 3270c3f..8413fd5 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -103,6 +103,9 @@ module.exports = [{ errors: [{ name: 'UnrecognizedAddress', message: 'Expected argument {0} to be an address' + }, { + name: 'CantDeriveAddress', + message: 'Can\'t derive address associated with script {0}, needs to be p2pkh in, p2pkh out, p2sh in, or p2sh out.' }] }, { name: 'HDPrivateKey', diff --git a/lib/publickey.js b/lib/publickey.js index fd363c7..1635869 100644 --- a/lib/publickey.js +++ b/lib/publickey.js @@ -1,6 +1,5 @@ 'use strict'; -var Address = require('./address'); var BN = require('./crypto/bn'); var Point = require('./crypto/point'); var Hash = require('./crypto/hash'); @@ -404,6 +403,7 @@ PublicKey.prototype._getID = function _getID() { * @returns {Address} An address generated from the public key */ PublicKey.prototype.toAddress = function(network) { + var Address = require('./address'); return Address.fromPublicKey(this, network || this.network); }; diff --git a/lib/script/script.js b/lib/script/script.js index 199fa49..af5ce61 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -1,7 +1,6 @@ 'use strict'; -var Address = require('../address'); var BufferReader = require('../encoding/bufferreader'); var BufferWriter = require('../encoding/bufferwriter'); var Hash = require('../crypto/hash'); @@ -30,6 +29,7 @@ var Script = function Script(from) { if (!(this instanceof Script)) { return new Script(from); } + var Address = require('../address'); this.chunks = []; @@ -283,15 +283,15 @@ Script.prototype.isScriptHashIn = function() { if (this.chunks.length === 0) { return false; } - var chunk = this.chunks[this.chunks.length - 1]; - if (!chunk) { + var redeemChunk = this.chunks[this.chunks.length - 1]; + if (!redeemChunk) { return false; } - var scriptBuf = chunk.buf; - if (!scriptBuf) { + var redeemBuf = redeemChunk.buf; + if (!redeemBuf) { return false; } - var redeemScript = new Script(scriptBuf); + var redeemScript = new Script(redeemBuf); var type = redeemScript.classify(); return type !== Script.types.UNKNOWN; }; @@ -546,7 +546,7 @@ Script.prototype.removeCodeseparators = function() { */ Script.buildMultisigOut = function(publicKeys, threshold, opts) { $.checkArgument(threshold <= publicKeys.length, - 'Number of required signatures must be less than or equal to the number of public keys'); + 'Number of required signatures must be less than or equal to the number of public keys'); opts = opts || {}; var script = new Script(); script.add(Opcode.smallInt(threshold)); @@ -598,6 +598,7 @@ Script.buildP2SHMultisigIn = function(pubkeys, threshold, signatures, opts) { * @param {(Address|PublicKey)} to - destination address or public key */ Script.buildPublicKeyHashOut = function(to) { + var Address = require('../address'); $.checkArgument(!_.isUndefined(to)); $.checkArgument(to instanceof PublicKey || to instanceof Address || _.isString(to)); if (to instanceof PublicKey) { @@ -650,6 +651,7 @@ Script.buildDataOut = function(data) { * @returns {Script} new pay to script hash script for given script */ Script.buildScriptHashOut = function(script) { + var Address = require('../address'); $.checkArgument(script instanceof Script || (script instanceof Address && script.isPayToScriptHash())); var s = new Script(); @@ -657,7 +659,7 @@ Script.buildScriptHashOut = function(script) { .add(script instanceof Address ? script.hashBuffer : Hash.sha256ripemd160(script.toBuffer())) .add(Opcode.OP_EQUAL); - s._network = script._network || script.network; + s._network = script._network || script.network; return s; }; @@ -699,9 +701,10 @@ Script.prototype.toScriptHashOut = function() { }; /** - * @return {Script} a script built from the address + * @return {Script} an output script built from the address */ Script.fromAddress = function(address) { + var Address = require('../address'); address = Address(address); if (address.isPayToScriptHash()) { return Script.buildScriptHashOut(address); @@ -713,14 +716,19 @@ Script.fromAddress = function(address) { /** * @param {Network=} network - * @return {Address} the associated address for this script + * @return {Address|boolean} the associated address for this script if possible, or false */ Script.prototype.toAddress = function(network) { + var Address = require('../address'); network = Networks.get(network) || this._network || Networks.defaultNetwork; - if (this.isPublicKeyHashOut() || this.isScriptHashOut()) { + var canConvertToAddress = this.isPublicKeyHashOut() || + this.isScriptHashOut() || + this.isPublicKeyHashIn() || + this.isScriptHashIn(); + if (canConvertToAddress) { return new Address(this, network); } - throw new Error('The script type needs to be PayToPublicKeyHash or PayToScriptHash'); + return false; }; /** diff --git a/test/address.js b/test/address.js index f857308..e456e70 100644 --- a/test/address.js +++ b/test/address.js @@ -28,7 +28,7 @@ describe('Address', function() { }); it('should throw an error because of bad network param', function() { - (function(){ + (function() { return new Address(PKHLivenet[0], 'main', 'pubkeyhash'); }).should.throw('Second argument must be "livenet" or "testnet".'); }); @@ -40,7 +40,7 @@ describe('Address', function() { }); describe('bitcoind compliance', function() { - validbase58.map(function(d){ + validbase58.map(function(d) { if (!d[2].isPrivkey) { it('should describe address ' + d[0] + ' as valid', function() { var type; @@ -57,8 +57,8 @@ describe('Address', function() { }); } }); - invalidbase58.map(function(d){ - it('should describe input ' + d[0].slice(0,10) + '... as invalid', function() { + invalidbase58.map(function(d) { + it('should describe input ' + d[0].slice(0, 10) + '... as invalid', function() { expect(function() { return new Address(d[0]); }).to.throw(Error); @@ -121,12 +121,12 @@ describe('Address', function() { should.exist(error); }); - it('isValid returns true on a valid address', function(){ + it('isValid returns true on a valid address', function() { var valid = Address.isValid('37BahqRsFrAd3qLiNNwLNV3AWMRD7itxTo', 'livenet'); valid.should.equal(true); }); - it('isValid returns false on network mismatch', function(){ + it('isValid returns false on network mismatch', function() { var valid = Address.isValid('37BahqRsFrAd3qLiNNwLNV3AWMRD7itxTo', 'testnet'); valid.should.equal(false); }); @@ -280,7 +280,7 @@ describe('Address', function() { it('should error because of incorrect type for script transform', function() { (function() { return Address._transformScript(new Buffer(20)); - }).should.throw('Address must be an instance of Script.'); + }).should.throw('script must be an instance of Script.'); }); it('should error because of incorrect type for string transform', function() { @@ -328,7 +328,7 @@ describe('Address', function() { it('should make this address from an uncompressed pubkey', function() { var pubkey = new PublicKey('0485e9737a74c30a873f74df05124f2aa6f53042c2fc0a130d6cbd7d16b944b00' + - '4833fef26c8be4c4823754869ff4e46755b85d851077771c220e2610496a29d98'); + '4833fef26c8be4c4823754869ff4e46755b85d851077771c220e2610496a29d98'); var a = Address.fromPublicKey(pubkey, 'livenet'); a.toString().should.equal('16JXnhxjJUhxfyx4y6H4sFcxrgt8kQ8ewX'); var b = new Address(pubkey, 'livenet', 'pubkeyhash'); @@ -336,23 +336,28 @@ describe('Address', function() { }); describe('from a script', function() { - it('should make this address from a script', function() { - var s = Script.fromString('OP_CHECKMULTISIG'); + it('should fail to build address from a non p2sh,p2pkh script', function() { + var s = new Script('OP_CHECKMULTISIG'); + (function() { + return new Address(s); + }).should.throw('needs to be p2pkh in, p2pkh out, p2sh in, or p2sh out'); + }); + it('should make this address from a p2pkh output script', function() { + var s = new Script('OP_DUP OP_HASH160 20 ' + + '0xc8e11b0eb0d2ad5362d894f048908341fa61b6e1 OP_EQUALVERIFY OP_CHECKSIG'); var buf = s.toBuffer(); var a = Address.fromScript(s, 'livenet'); - a.toString().should.equal('3BYmEwgV2vANrmfRymr1mFnHXgLjD6gAWm'); + a.toString().should.equal('1KK9oz4bFH8c1t6LmighHaoSEGx3P3FEmc'); var b = new Address(s, 'livenet'); - b.toString().should.equal('3BYmEwgV2vANrmfRymr1mFnHXgLjD6gAWm'); - var c = Address.fromScriptHash(bitcore.crypto.Hash.sha256ripemd160(buf), 'livenet'); - c.toString().should.equal('3BYmEwgV2vANrmfRymr1mFnHXgLjD6gAWm'); + b.toString().should.equal('1KK9oz4bFH8c1t6LmighHaoSEGx3P3FEmc'); }); - it('should make this address from other script', function() { - var s = Script.fromString('OP_CHECKSIG OP_HASH160'); + it('should make this address from a p2sh input script', function() { + var s = Script.fromString('OP_HASH160 20 0xa6ed4af315271e657ee307828f54a4365fa5d20f OP_EQUAL'); var a = Address.fromScript(s, 'livenet'); - a.toString().should.equal('347iRqVwks5r493N1rsLN4k9J7Ljg488W7'); + a.toString().should.equal('3GueMn6ruWVfQTN4XKBGEbCbGLwRSUhfnS'); var b = new Address(s, 'livenet'); - b.toString().should.equal('347iRqVwks5r493N1rsLN4k9J7Ljg488W7'); + b.toString().should.equal('3GueMn6ruWVfQTN4XKBGEbCbGLwRSUhfnS'); }); it('returns the same address if the script is a pay to public key hash out', function() { @@ -487,6 +492,8 @@ describe('Address', function() { it('can create an address from a set of public keys', function() { var address = Address.createMultisig(publics, 2, Networks.livenet); address.toString().should.equal('3FtqPRirhPvrf7mVUSkygyZ5UuoAYrTW3y'); + address = new Address(publics, 2, Networks.livenet); + address.toString().should.equal('3FtqPRirhPvrf7mVUSkygyZ5UuoAYrTW3y'); }); it('works on testnet also', function() { @@ -502,7 +509,7 @@ describe('Address', function() { it('fails if invalid array is provided', function() { expect(function() { - return Address.createMultisig([],3,'testnet'); + return Address.createMultisig([], 3, 'testnet'); }).to.throw('Number of required signatures must be less than or equal to the number of public keys'); }); }); diff --git a/test/script/script.js b/test/script/script.js index e949001..477c820 100644 --- a/test/script/script.js +++ b/test/script/script.js @@ -641,18 +641,17 @@ describe('Script', function() { it('priorize the network argument', function() { var script = new Script(liveAddress); script.toAddress(Networks.testnet).toString().should.equal(testAddress.toString()); - - var s = new Script('OP_DUP OP_HASH160 20 0x06c06f6d931d7bfba2b5bd5ad0d19a8f257af3e3 OP_EQUALVERIFY OP_CHECKSIG'); script.toAddress(Networks.testnet).network.should.equal(Networks.testnet); }); it('use the inherited network', function() { var script = new Script(liveAddress); script.toAddress().toString().should.equal(liveAddress.toString()); - var script = new Script(testAddress); + script = new Script(testAddress); script.toAddress().toString().should.equal(testAddress.toString()); }); it('uses default network', function() { - var script = new Script('OP_DUP OP_HASH160 20 0x06c06f6d931d7bfba2b5bd5ad0d19a8f257af3e3 OP_EQUALVERIFY OP_CHECKSIG'); + var script = new Script('OP_DUP OP_HASH160 20 ' + + '0x06c06f6d931d7bfba2b5bd5ad0d19a8f257af3e3 OP_EQUALVERIFY OP_CHECKSIG'); script.toAddress().network.should.equal(Networks.defaultNetwork); }); it('for a P2PKH address', function() { @@ -668,10 +667,36 @@ describe('Script', function() { script.toAddress().toString().should.equal(stringAddress); }); it('fails if content is not recognized', function() { - expect(function() { - return Script().toAddress(Networks.livenet); - }).to.throw(); + Script().toAddress(Networks.livenet).should.equal(false); + }); + + // taken from txid 7e519caca256423320b92e3e17be5701f87afecbdb3f53af598032bfd8d164f5 + it('works for p2pkh output', function() { + var script = new Script('OP_DUP OP_HASH160 20 ' + + '0xc8e11b0eb0d2ad5362d894f048908341fa61b6e1 OP_EQUALVERIFY OP_CHECKSIG'); + script.toAddress().toString().should.equal('1KK9oz4bFH8c1t6LmighHaoSEGx3P3FEmc'); + }); + it('works for p2pkh input', function() { + var script = new Script('72 0x3045022100eff96230ca0f55b1e8c7a63e014f48611ff1af40875ecd33dee9062d7a6f5e2002206320405b5f6992c756e03e66b21a05a812b60996464ac6af815c2638b930dd7a01 65 0x04150defa035a2c7d826d7d5fc8ab2154bd1bb832f1a5c8ecb338f436362ad232e428b57db44677c5a8bd42c5ed9e2d7e04e742c59bee1b40080cfd57dec64b23a'); + script.toAddress().toString().should.equal('1KK9oz4bFH8c1t6LmighHaoSEGx3P3FEmc'); }); + + // taken from txid fe1f764299dc7f3b5a8fae912050df2b633bf99554c68bf1c456edb9c2b63585 + it('works for p2sh output', function() { + var script = new Script('OP_HASH160 20 0x99d29051af0c29adcb9040034752bba7dde33e35 OP_EQUAL'); + script.toAddress().toString().should.equal('3FiMZ7stbfH2WG5JQ7CiuzrFo7CEnGUcAP'); + }); + it('works for p2sh input', function() { + var script = new Script('OP_FALSE 72 0x3045022100e824fbe979fac5834d0062dd5a4e82a898e00ac454bd254cd708ad28530816f202206251ff0fa4dd70c0524c690d4e4deb2bd167297e7bbdf6743b4a8050d681555001 37 0x512102ff3ae0aaa4679ea156d5581dbe6695cc0c311df0aa42af76670d0debbd8f672951ae'); + script.toAddress().toString().should.equal('3GYicPxCvsKvbJmZNBBeWkC3cLuGFhtrQi'); + }); + + // no address scripts + it('works for OP_RETURN script', function() { + var script = new Script('OP_RETURN 20 0x99d29051af0c29adcb9040034752bba7dde33e35'); + script.toAddress().should.equal(false); + }); + }); }); From a9328d76ff6438c66f8bda54eae2c4aa29314651 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 18 Mar 2015 17:22:32 -0300 Subject: [PATCH 2/5] add extra test --- lib/address.js | 8 ++++---- test/script/script.js | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/address.js b/lib/address.js index 31ca3fc..14d89f3 100644 --- a/lib/address.js +++ b/lib/address.js @@ -254,14 +254,14 @@ Address._transformScript = function(script, network) { } else if (script.isPublicKeyHashOut()) { info.hashBuffer = script.getData(); info.type = Address.PayToPublicKeyHash; - } else if (script.isScriptHashIn()) { - // hash the redeemscript found at the end of the scriptSig - info.hashBuffer = Hash.sha256ripemd160(script.chunks[script.chunks.length - 1].buf); - info.type = Address.PayToScriptHash; } else if (script.isPublicKeyHashIn()) { // hash the publickey found in the scriptSig info.hashBuffer = Hash.sha256ripemd160(script.chunks[1].buf); info.type = Address.PayToPublicKeyHash; + } else if (script.isScriptHashIn()) { + // hash the redeemscript found at the end of the scriptSig + info.hashBuffer = Hash.sha256ripemd160(script.chunks[script.chunks.length - 1].buf); + info.type = Address.PayToScriptHash; } else { throw new errors.Script.CantDeriveAddress(script); } diff --git a/test/script/script.js b/test/script/script.js index 477c820..64416dd 100644 --- a/test/script/script.js +++ b/test/script/script.js @@ -679,6 +679,8 @@ describe('Script', function() { it('works for p2pkh input', function() { var script = new Script('72 0x3045022100eff96230ca0f55b1e8c7a63e014f48611ff1af40875ecd33dee9062d7a6f5e2002206320405b5f6992c756e03e66b21a05a812b60996464ac6af815c2638b930dd7a01 65 0x04150defa035a2c7d826d7d5fc8ab2154bd1bb832f1a5c8ecb338f436362ad232e428b57db44677c5a8bd42c5ed9e2d7e04e742c59bee1b40080cfd57dec64b23a'); script.toAddress().toString().should.equal('1KK9oz4bFH8c1t6LmighHaoSEGx3P3FEmc'); + var s2 = new Script('71 0x3044022017053dad84aa06213749df50a03330cfd24d6b8e7ddbb6de66c03697b78a752a022053bc0faca8b4049fb3944a05fcf7c93b2861734d39a89b73108f605f70f5ed3401 33 0x0225386e988b84248dc9c30f784b06e02fdec57bbdbd443768eb5744a75ce44a4c'); + s2.toAddress().toString().should.equal('17VArX6GRE6i6MVscBUZoXwi6NhnHa68B7'); }); // taken from txid fe1f764299dc7f3b5a8fae912050df2b633bf99554c68bf1c456edb9c2b63585 From 3619c7c9e230ea9718867ec79d0c10581f072b52 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 18 Mar 2015 17:40:26 -0300 Subject: [PATCH 3/5] fix problematic cases --- lib/errors/spec.js | 3 ++ lib/script/script.js | 92 +++++++++++++++++++++++++------------------ test/script/script.js | 17 +++++++- 3 files changed, 72 insertions(+), 40 deletions(-) diff --git a/lib/errors/spec.js b/lib/errors/spec.js index 8413fd5..3815b59 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -106,6 +106,9 @@ module.exports = [{ }, { name: 'CantDeriveAddress', message: 'Can\'t derive address associated with script {0}, needs to be p2pkh in, p2pkh out, p2sh in, or p2sh out.' + }, { + name: 'InvalidBuffer', + message: 'Invalid script buffer: can\'t parse valid script from given buffer {0}' }] }, { name: 'HDPrivateKey', diff --git a/lib/script/script.js b/lib/script/script.js index af5ce61..06401b4 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -57,44 +57,51 @@ Script.fromBuffer = function(buffer) { var br = new BufferReader(buffer); while (!br.finished()) { - var opcodenum = br.readUInt8(); + try { + var opcodenum = br.readUInt8(); - var len, buf; - if (opcodenum > 0 && opcodenum < Opcode.OP_PUSHDATA1) { - len = opcodenum; - script.chunks.push({ - buf: br.read(len), - len: len, - opcodenum: opcodenum - }); - } else if (opcodenum === Opcode.OP_PUSHDATA1) { - len = br.readUInt8(); - buf = br.read(len); - script.chunks.push({ - buf: buf, - len: len, - opcodenum: opcodenum - }); - } else if (opcodenum === Opcode.OP_PUSHDATA2) { - len = br.readUInt16LE(); - buf = br.read(len); - script.chunks.push({ - buf: buf, - len: len, - opcodenum: opcodenum - }); - } else if (opcodenum === Opcode.OP_PUSHDATA4) { - len = br.readUInt32LE(); - buf = br.read(len); - script.chunks.push({ - buf: buf, - len: len, - opcodenum: opcodenum - }); - } else { - script.chunks.push({ - opcodenum: opcodenum - }); + var len, buf; + if (opcodenum > 0 && opcodenum < Opcode.OP_PUSHDATA1) { + len = opcodenum; + script.chunks.push({ + buf: br.read(len), + len: len, + opcodenum: opcodenum + }); + } else if (opcodenum === Opcode.OP_PUSHDATA1) { + len = br.readUInt8(); + buf = br.read(len); + script.chunks.push({ + buf: buf, + len: len, + opcodenum: opcodenum + }); + } else if (opcodenum === Opcode.OP_PUSHDATA2) { + len = br.readUInt16LE(); + buf = br.read(len); + script.chunks.push({ + buf: buf, + len: len, + opcodenum: opcodenum + }); + } else if (opcodenum === Opcode.OP_PUSHDATA4) { + len = br.readUInt32LE(); + buf = br.read(len); + script.chunks.push({ + buf: buf, + len: len, + opcodenum: opcodenum + }); + } else { + script.chunks.push({ + opcodenum: opcodenum + }); + } + } catch (e) { + if (e instanceof RangeError) { + throw new errors.Script.InvalidBuffer(buffer); + } + throw e; } } @@ -291,7 +298,16 @@ Script.prototype.isScriptHashIn = function() { if (!redeemBuf) { return false; } - var redeemScript = new Script(redeemBuf); + + var redeemScript; + try { + redeemScript = Script.fromBuffer(redeemBuf); + } catch (e) { + if (e instanceof errors.Script.InvalidBuffer) { + return false; + } + throw e; + } var type = redeemScript.classify(); return type !== Script.types.UNKNOWN; }; diff --git a/test/script/script.js b/test/script/script.js index 64416dd..6de5476 100644 --- a/test/script/script.js +++ b/test/script/script.js @@ -308,6 +308,16 @@ describe('Script', function() { it('should identify this known non-scripthashin', function() { Script('20 0000000000000000000000000000000000000000 OP_CHECKSIG').isScriptHashIn().should.equal(false); }); + + it('should identify this problematic non-scripthashin scripts', function() { + var s = new Script('71 0x3044022017053dad84aa06213749df50a03330cfd24d6' + + 'b8e7ddbb6de66c03697b78a752a022053bc0faca8b4049fb3944a05fcf7c93b2861' + + '734d39a89b73108f605f70f5ed3401 33 0x0225386e988b84248dc9c30f784b06e' + + '02fdec57bbdbd443768eb5744a75ce44a4c'); + var s2 = new Script('OP_RETURN 32 0x19fdb20634911b6459e6086658b3a6ad2dc6576bd6826c73ee86a5f9aec14ed9'); + s.isScriptHashIn().should.equal(false); + s2.isScriptHashIn().should.equal(false); + }); }); describe('#isScripthashOut', function() { @@ -670,25 +680,28 @@ describe('Script', function() { Script().toAddress(Networks.livenet).should.equal(false); }); - // taken from txid 7e519caca256423320b92e3e17be5701f87afecbdb3f53af598032bfd8d164f5 it('works for p2pkh output', function() { + // taken from tx 7e519caca256423320b92e3e17be5701f87afecbdb3f53af598032bfd8d164f5 var script = new Script('OP_DUP OP_HASH160 20 ' + '0xc8e11b0eb0d2ad5362d894f048908341fa61b6e1 OP_EQUALVERIFY OP_CHECKSIG'); script.toAddress().toString().should.equal('1KK9oz4bFH8c1t6LmighHaoSEGx3P3FEmc'); }); it('works for p2pkh input', function() { + // taken from tx 7e519caca256423320b92e3e17be5701f87afecbdb3f53af598032bfd8d164f5 var script = new Script('72 0x3045022100eff96230ca0f55b1e8c7a63e014f48611ff1af40875ecd33dee9062d7a6f5e2002206320405b5f6992c756e03e66b21a05a812b60996464ac6af815c2638b930dd7a01 65 0x04150defa035a2c7d826d7d5fc8ab2154bd1bb832f1a5c8ecb338f436362ad232e428b57db44677c5a8bd42c5ed9e2d7e04e742c59bee1b40080cfd57dec64b23a'); script.toAddress().toString().should.equal('1KK9oz4bFH8c1t6LmighHaoSEGx3P3FEmc'); + // taken from tx 7f8f95752a59d715dae9e0008a42e7968d2736741591bbfc6685f6e1649c21ed var s2 = new Script('71 0x3044022017053dad84aa06213749df50a03330cfd24d6b8e7ddbb6de66c03697b78a752a022053bc0faca8b4049fb3944a05fcf7c93b2861734d39a89b73108f605f70f5ed3401 33 0x0225386e988b84248dc9c30f784b06e02fdec57bbdbd443768eb5744a75ce44a4c'); s2.toAddress().toString().should.equal('17VArX6GRE6i6MVscBUZoXwi6NhnHa68B7'); }); - // taken from txid fe1f764299dc7f3b5a8fae912050df2b633bf99554c68bf1c456edb9c2b63585 it('works for p2sh output', function() { + // taken from tx fe1f764299dc7f3b5a8fae912050df2b633bf99554c68bf1c456edb9c2b63585 var script = new Script('OP_HASH160 20 0x99d29051af0c29adcb9040034752bba7dde33e35 OP_EQUAL'); script.toAddress().toString().should.equal('3FiMZ7stbfH2WG5JQ7CiuzrFo7CEnGUcAP'); }); it('works for p2sh input', function() { + // taken from tx fe1f764299dc7f3b5a8fae912050df2b633bf99554c68bf1c456edb9c2b63585 var script = new Script('OP_FALSE 72 0x3045022100e824fbe979fac5834d0062dd5a4e82a898e00ac454bd254cd708ad28530816f202206251ff0fa4dd70c0524c690d4e4deb2bd167297e7bbdf6743b4a8050d681555001 37 0x512102ff3ae0aaa4679ea156d5581dbe6695cc0c311df0aa42af76670d0debbd8f672951ae'); script.toAddress().toString().should.equal('3GYicPxCvsKvbJmZNBBeWkC3cLuGFhtrQi'); }); From e0b1ca0e107df961fdaac95fd39028b83733b3e6 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 18 Mar 2015 17:59:09 -0300 Subject: [PATCH 4/5] move some script logic from Address to Script --- lib/address.js | 26 +++++--------------------- lib/script/script.js | 41 +++++++++++++++++++++++++++++++++-------- test/address.js | 4 ++-- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/lib/address.js b/lib/address.js index 14d89f3..39cfcd6 100644 --- a/lib/address.js +++ b/lib/address.js @@ -244,28 +244,11 @@ Address._transformPublicKey = function(pubkey) { * @private */ Address._transformScript = function(script, network) { - var info = {}; - if (!(script instanceof Script)) { - throw new TypeError('script must be an instance of Script.'); - } - if (script.isScriptHashOut()) { - info.hashBuffer = script.getData(); - info.type = Address.PayToScriptHash; - } else if (script.isPublicKeyHashOut()) { - info.hashBuffer = script.getData(); - info.type = Address.PayToPublicKeyHash; - } else if (script.isPublicKeyHashIn()) { - // hash the publickey found in the scriptSig - info.hashBuffer = Hash.sha256ripemd160(script.chunks[1].buf); - info.type = Address.PayToPublicKeyHash; - } else if (script.isScriptHashIn()) { - // hash the redeemscript found at the end of the scriptSig - info.hashBuffer = Hash.sha256ripemd160(script.chunks[script.chunks.length - 1].buf); - info.type = Address.PayToScriptHash; - } else { + $.checkArgument(script instanceof Script, 'script must be a Script instance'); + var info = script.getAddressInfo(network); + if (!info) { throw new errors.Script.CantDeriveAddress(script); } - info.network = Networks.get(network) || Networks.defaultNetwork; return info; }; @@ -297,7 +280,7 @@ Address.createMultisig = function(publicKeys, threshold, network) { */ Address._transformString = function(data, network, type) { if (typeof(data) !== 'string') { - throw new TypeError('Address supplied is not a string.'); + throw new TypeError('data parameter supplied is not a string.'); } var addressBuffer = Base58Check.decode(data); var info = Address._transformBuffer(addressBuffer, network, type); @@ -372,6 +355,7 @@ Address.payingTo = function(script, network) { * @returns {Address} A new valid and frozen instance of an Address */ Address.fromScript = function(script, network) { + $.checkArgument(script instanceof Script, 'script must be a Script instance'); var info = Address._transformScript(script, network); return new Address(info.hashBuffer, network, info.type); }; diff --git a/lib/script/script.js b/lib/script/script.js index 06401b4..4945e1f 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -730,6 +730,33 @@ Script.fromAddress = function(address) { throw new errors.Script.UnrecognizedAddress(address); }; +/** + * @param {Network=} network + * @return {Address|boolean} the associated address information object + * for this script if any, or false + */ +Script.prototype.getAddressInfo = function() { + var Address = require('../address'); + var info = {}; + if (this.isScriptHashOut()) { + info.hashBuffer = this.getData(); + info.type = Address.PayToScriptHash; + } else if (this.isPublicKeyHashOut()) { + info.hashBuffer = this.getData(); + info.type = Address.PayToPublicKeyHash; + } else if (this.isPublicKeyHashIn()) { + // hash the publickey found in the scriptSig + info.hashBuffer = Hash.sha256ripemd160(this.chunks[1].buf); + info.type = Address.PayToPublicKeyHash; + } else if (this.isScriptHashIn()) { + // hash the redeemscript found at the end of the scriptSig + info.hashBuffer = Hash.sha256ripemd160(this.chunks[this.chunks.length - 1].buf); + info.type = Address.PayToScriptHash; + } else { + return false; + } + return info; +}; /** * @param {Network=} network * @return {Address|boolean} the associated address for this script if possible, or false @@ -737,14 +764,12 @@ Script.fromAddress = function(address) { Script.prototype.toAddress = function(network) { var Address = require('../address'); network = Networks.get(network) || this._network || Networks.defaultNetwork; - var canConvertToAddress = this.isPublicKeyHashOut() || - this.isScriptHashOut() || - this.isPublicKeyHashIn() || - this.isScriptHashIn(); - if (canConvertToAddress) { - return new Address(this, network); - } - return false; + var info = this.getAddressInfo(); + if (!info) { + return false; + } + info.network = Networks.get(network) || Networks.defaultNetwork; + return new Address(info); }; /** diff --git a/test/address.js b/test/address.js index e456e70..b971e5f 100644 --- a/test/address.js +++ b/test/address.js @@ -280,13 +280,13 @@ describe('Address', function() { it('should error because of incorrect type for script transform', function() { (function() { return Address._transformScript(new Buffer(20)); - }).should.throw('script must be an instance of Script.'); + }).should.throw('Invalid Argument: script must be a Script instance'); }); it('should error because of incorrect type for string transform', function() { (function() { return Address._transformString(new Buffer(20)); - }).should.throw('Address supplied is not a string.'); + }).should.throw('data parameter supplied is not a string.'); }); it('should make an address from a pubkey hash buffer', function() { From ef7eafbb0c33f34b26152c32430e80ef2b3c649f Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 18 Mar 2015 18:58:41 -0300 Subject: [PATCH 5/5] increase test coverage and fix some bugs --- lib/script/script.js | 11 +++-------- test/script/script.js | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/script/script.js b/lib/script/script.js index 4945e1f..523e88c 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -291,9 +291,6 @@ Script.prototype.isScriptHashIn = function() { return false; } var redeemChunk = this.chunks[this.chunks.length - 1]; - if (!redeemChunk) { - return false; - } var redeemBuf = redeemChunk.buf; if (!redeemBuf) { return false; @@ -449,14 +446,12 @@ Script.prototype.equals = function(script) { } var i; for (i = 0; i < this.chunks.length; i++) { - if (BufferUtil.isBuffer(this.chunks[i]) && !BufferUtil.isBuffer(script.chunks[i])) { - return false; - } else if (this.chunks[i] instanceof Opcode && !(script.chunks[i] instanceof Opcode)) { + if (BufferUtil.isBuffer(this.chunks[i].buf) && !BufferUtil.isBuffer(script.chunks[i].buf)) { return false; } - if (BufferUtil.isBuffer(this.chunks[i]) && !BufferUtil.equals(this.chunks[i], script.chunks[i])) { + if (BufferUtil.isBuffer(this.chunks[i].buf) && !BufferUtil.equals(this.chunks[i].buf, script.chunks[i].buf)) { return false; - } else if (this.chunks[i].num !== script.chunks[i].num) { + } else if (this.chunks[i].opcodenum !== script.chunks[i].opcodenum) { return false; } } diff --git a/test/script/script.js b/test/script/script.js index 6de5476..61cf787 100644 --- a/test/script/script.js +++ b/test/script/script.js @@ -11,6 +11,8 @@ var Opcode = bitcore.Opcode; var PublicKey = bitcore.PublicKey; var Address = bitcore.Address; +var script_valid = require('../data/bitcoind/script_valid'); + describe('Script', function() { it('should make a new script', function() { @@ -713,5 +715,22 @@ describe('Script', function() { }); }); + describe('equals', function() { + it('returns true for same script', function() { + Script('OP_TRUE').equals(Script('OP_TRUE')).should.equal(true); + }); + it('returns false for different chunks sizes', function() { + Script('OP_TRUE').equals(Script('OP_TRUE OP_TRUE')).should.equal(false); + }); + it('returns false for different opcodes', function() { + Script('OP_TRUE OP_TRUE').equals(Script('OP_TRUE OP_FALSE')).should.equal(false); + }); + it('returns false for different data', function() { + Script().add(new Buffer('a')).equals(Script('OP_TRUE')).should.equal(false); + }); + it('returns false for different data', function() { + Script().add(new Buffer('a')).equals(Script().add(new Buffer('b'))).should.equal(false); + }); + }); });