From 62ea45a524b0f06546596a9d0221ca31a0ba7215 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 18 Mar 2015 16:54:16 -0300 Subject: [PATCH] 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); + }); + }); });