diff --git a/lib/opcode.js b/lib/opcode.js index ccd6535..75e8120 100644 --- a/lib/opcode.js +++ b/lib/opcode.js @@ -46,6 +46,16 @@ Opcode.prototype.toString = function() { return str; }; +Opcode.smallInt = function(n) { + if (!(n >= 0 && n <= 16)) { + throw new Error('Invalid Argument: n must be between 0 and 16'); + } + if (n === 0) { + return Opcode('OP_0'); + } + return new Opcode(Opcode.map.OP_1 + n - 1); +}; + Opcode.map = { // push value OP_FALSE: 0, diff --git a/lib/script.js b/lib/script.js index 2474a5b..df418f2 100644 --- a/lib/script.js +++ b/lib/script.js @@ -3,7 +3,19 @@ var BufferReader = require('./encoding/bufferreader'); var BufferWriter = require('./encoding/bufferwriter'); var Opcode = require('./opcode'); +var PublicKey = require('./publickey'); +var Hash = require('./crypto/hash'); +var bu = require('./util/buffer'); +/** + * A bitcoin transaction script. Each transaction's inputs and outputs + * has a script that is evaluated to validate it's spending. + * + * See https://en.bitcoin.it/wiki/Script + * + * @constructor + * @param {Object|string|Buffer} [from] optional data to populate script + */ var Script = function Script(from) { if (!(this instanceof Script)) { return new Script(from); @@ -11,7 +23,7 @@ var Script = function Script(from) { this.chunks = []; - if (Buffer.isBuffer(from)) { + if (bu.isBuffer(from)) { return Script.fromBuffer(from); } else if (typeof from === 'string') { return Script.fromString(from); @@ -194,12 +206,7 @@ Script.prototype.isPublicKeyHashIn = function() { this.chunks[0].buf.length >= 0x47 && this.chunks[0].buf.length <= 0x49 && this.chunks[1].buf && - ( - // compressed public key - (this.chunks[1].buf[0] === 0x03 && this.chunks[1].buf.length === 0x21) || - // uncompressed public key - (this.chunks[1].buf[0] === 0x04 && this.chunks[1].buf.length === 0x41)) - ); + PublicKey.isValid(this.chunks[1].buf)); }; /** @@ -207,8 +214,8 @@ Script.prototype.isPublicKeyHashIn = function() { */ Script.prototype.isPublicKeyOut = function() { return this.chunks.length === 2 && - Buffer.isBuffer(this.chunks[0].buf) && - this.chunks[0].buf.length === 0x41 && + bu.isBuffer(this.chunks[0].buf) && + PublicKey.isValid(this.chunks[0].buf) && this.chunks[1] === Opcode('OP_CHECKSIG').toNumber(); }; @@ -217,7 +224,7 @@ Script.prototype.isPublicKeyOut = function() { */ Script.prototype.isPublicKeyIn = function() { return this.chunks.length === 1 && - Buffer.isBuffer(this.chunks[0].buf) && + bu.isBuffer(this.chunks[0].buf) && this.chunks[0].buf.length === 0x47; }; @@ -261,7 +268,7 @@ Script.prototype.isMultisigOut = function() { return (this.chunks.length > 3 && Opcode.isSmallIntOp(this.chunks[0]) && this.chunks.slice(1, this.chunks.length - 2).every(function(obj) { - return obj.buf && Buffer.isBuffer(obj.buf); + return obj.buf && bu.isBuffer(obj.buf); }) && Opcode.isSmallIntOp(this.chunks[this.chunks.length - 2]) && this.chunks[this.chunks.length - 1] === Opcode.map.OP_CHECKMULTISIG); @@ -275,7 +282,7 @@ Script.prototype.isMultisigIn = function() { return this.chunks[0] === 0 && this.chunks.slice(1, this.chunks.length).every(function(obj) { return obj.buf && - Buffer.isBuffer(obj.buf) && + bu.isBuffer(obj.buf) && obj.buf.length === 0x47; }); }; @@ -283,7 +290,7 @@ Script.prototype.isMultisigIn = function() { /** * @returns true if this is an OP_RETURN data script */ -Script.prototype.isOpReturn = function() { +Script.prototype.isDataOut = function() { return (this.chunks[0] === Opcode('OP_RETURN').toNumber() && (this.chunks.length === 1 || (this.chunks.length === 2 && @@ -303,7 +310,7 @@ Script.types.SCRIPTHASH_OUT = 'Pay to script hash'; Script.types.SCRIPTHASH_IN = 'Spend from script hash'; Script.types.MULTISIG_OUT = 'Pay to multisig'; Script.types.MULTISIG_IN = 'Spend from multisig'; -Script.types.OP_RETURN = 'Data push'; +Script.types.DATA_OUT = 'Data push'; Script.identifiers = {}; Script.identifiers.PUBKEY_OUT = Script.prototype.isPublicKeyOut; @@ -312,9 +319,9 @@ Script.identifiers.PUBKEYHASH_OUT = Script.prototype.isPublicKeyHashOut; Script.identifiers.PUBKEYHASH_IN = Script.prototype.isPublicKeyHashIn; Script.identifiers.MULTISIG_OUT = Script.prototype.isMultisigOut; Script.identifiers.MULTISIG_IN = Script.prototype.isMultisigIn; -Script.identifiers.OP_RETURN = Script.prototype.isOpReturn; Script.identifiers.SCRIPTHASH_OUT = Script.prototype.isScriptHashOut; Script.identifiers.SCRIPTHASH_IN = Script.prototype.isScriptHashIn; +Script.identifiers.DATA_OUT = Script.prototype.isDataOut; /** * @returns {object} The Script type if it is a known form, @@ -369,7 +376,7 @@ Script.prototype._addByType = function(obj, prepend) { this._addOpcode(obj, prepend); } else if (obj.constructor && obj.constructor.name && obj.constructor.name === 'Opcode') { this._addOpcode(obj, prepend); - } else if (Buffer.isBuffer(obj)) { + } else if (bu.isBuffer(obj)) { this._addBuffer(obj, prepend); } else if (typeof obj === 'object') { this._insertAtPosition(obj, prepend); @@ -402,13 +409,15 @@ Script.prototype._addOpcode = function(opcode, prepend) { Script.prototype._addBuffer = function(buf, prepend) { var opcodenum; var len = buf.length; - if (buf.length > 0 && buf.length < Opcode.map.OP_PUSHDATA1) { - opcodenum = buf.length; - } else if (buf.length < Math.pow(2, 8)) { + if (len === 0) { + return; + } else if (len > 0 && len < Opcode.map.OP_PUSHDATA1) { + opcodenum = len; + } else if (len < Math.pow(2, 8)) { opcodenum = Opcode.map.OP_PUSHDATA1; - } else if (buf.length < Math.pow(2, 16)) { + } else if (len < Math.pow(2, 16)) { opcodenum = Opcode.map.OP_PUSHDATA2; - } else if (buf.length < Math.pow(2, 32)) { + } else if (len < Math.pow(2, 32)) { opcodenum = Opcode.map.OP_PUSHDATA4; } else { throw new Error('You can\'t push that much data'); @@ -421,4 +430,88 @@ Script.prototype._addBuffer = function(buf, prepend) { return this; }; + +// high level script builder methods + +/** + * @returns a new Multisig output script for given public keys, + * requiring m of those public keys to spend + * @param {PublicKey[]} pubkeys - list of all public keys controlling the output + * @param {number} m - amount of required signatures to spend the output + */ +Script.buildMultisigOut = function(pubkeys, m) { + var s = new Script(); + s.add(Opcode.smallInt(m)); + for (var i = 0; i < pubkeys.length; i++) { + var pubkey = pubkeys[i]; + s.add(pubkey.toBuffer()); + } + s.add(Opcode.smallInt(pubkeys.length)); + s.add(Opcode('OP_CHECKMULTISIG')); + return s; +}; + +/** + * @returns a new pay to public key hash output for the given + * address or public key + * @param {(Address|PublicKey)} to - destination address or public key + */ +Script.buildPublicKeyHashOut = function(to) { + if (to instanceof PublicKey) { + to = to.toAddress(); + } + var s = new Script(); + s.add(Opcode('OP_DUP')) + .add(Opcode('OP_HASH160')) + .add(to.hashBuffer) + .add(Opcode('OP_EQUALVERIFY')) + .add(Opcode('OP_CHECKSIG')); + return s; +}; + +/** + * @returns a new pay to public key output for the given + * public key + */ +Script.buildPublicKeyOut = function(pubkey) { + var s = new Script(); + s.add(pubkey.toBuffer()) + .add(Opcode('OP_CHECKSIG')); + return s; +}; + +/** + * @returns a new OP_RETURN script with data + * @param {(string|Buffer)} to - the data to embed in the output + */ +Script.buildDataOut = function(data) { + if (typeof data === 'string') { + data = new Buffer(data); + } + var s = new Script(); + s.add(Opcode('OP_RETURN')) + .add(data); + return s; +}; + +/** + * @returns a new pay to script hash script for given script + * @param {Script} script - the redeemScript for the new p2sh output + */ +Script.buildScriptHashOut = function(script) { + var s = new Script(); + s.add(Opcode('OP_HASH160')) + .add(Hash.sha256ripemd160(script.toBuffer())) + .add(Opcode('OP_EQUAL')); + return s; +}; + + +/** + * @returns a new pay to script hash script that pays to this script + */ +Script.prototype.toScriptHashOut = function() { + return Script.buildScriptHashOut(this); +}; + module.exports = Script; diff --git a/test/opcode.js b/test/opcode.js index c9d8307..41ccc43 100644 --- a/test/opcode.js +++ b/test/opcode.js @@ -92,13 +92,22 @@ describe('Opcode', function() { Opcode('OP_16') ]; + describe('@smallInt', function() { + var testSmallInt = function(n, op) { + Opcode.smallInt(n).toString().should.equal(op.toString()); + }; + for (var i = 0; i < smallints.length; i++) { + var op = smallints[i]; + it('should work for small int ' + op, testSmallInt.bind(null, i, op)); + } + }); describe('@isSmallIntOp', function() { - var testSmallInt = function() { - Opcode.isSmallIntOp(this).should.equal(true); + var testIsSmallInt = function(op) { + Opcode.isSmallIntOp(op).should.equal(true); }; for (var i = 0; i < smallints.length; i++) { var op = smallints[i]; - it('should work for small int ' + op, testSmallInt.bind(op)); + it('should work for small int ' + op, testIsSmallInt.bind(null, op)); } it('should work for non-small ints', function() { diff --git a/test/script.js b/test/script.js index ce0d896..818da0f 100644 --- a/test/script.js +++ b/test/script.js @@ -4,6 +4,8 @@ var should = require('chai').should(); var bitcore = require('..'); var Script = bitcore.Script; var Opcode = bitcore.Opcode; +var PublicKey = bitcore.PublicKey; +var Address = bitcore.Address; describe('Script', function() { @@ -187,22 +189,22 @@ describe('Script', function() { }); - describe('#isOpReturn', function() { + describe('#isDataOut', function() { it('should know this is a (blank) OP_RETURN script', function() { - Script('OP_RETURN').isOpReturn().should.equal(true); + Script('OP_RETURN').isDataOut().should.equal(true); }); it('should know this is an OP_RETURN script', function() { var buf = new Buffer(40); buf.fill(0); - Script('OP_RETURN 40 0x' + buf.toString('hex')).isOpReturn().should.equal(true); + Script('OP_RETURN 40 0x' + buf.toString('hex')).isDataOut().should.equal(true); }); it('should know this is not an OP_RETURN script', function() { var buf = new Buffer(40); buf.fill(0); - Script('OP_CHECKMULTISIG 40 0x' + buf.toString('hex')).isOpReturn().should.equal(false); + Script('OP_CHECKMULTISIG 40 0x' + buf.toString('hex')).isDataOut().should.equal(false); }); }); @@ -309,8 +311,8 @@ describe('Script', function() { it('should classify MULTISIG in', function() { Script('OP_0 0x47 0x3044022002a27769ee33db258bdf7a3792e7da4143ec4001b551f73e6a190b8d1bde449d02206742c56ccd94a7a2e16ca52fc1ae4a0aa122b0014a867a80de104f9cb18e472c01').classify().should.equal(Script.types.MULTISIG_IN); }); - it('should classify OP_RETURN', function() { - Script('OP_RETURN 1 0x01').classify().should.equal(Script.types.OP_RETURN); + it('should classify OP_RETURN data out', function() { + Script('OP_RETURN 1 0x01').classify().should.equal(Script.types.DATA_OUT); }); it('should classify public key out', function() { Script('41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 OP_CHECKSIG').classify().should.equal(Script.types.PUBKEY_OUT); @@ -325,6 +327,9 @@ describe('Script', function() { describe('#add and #prepend', function() { + it('should add these ops', function() { + Script().add(Opcode('OP_RETURN')).add(new Buffer('')).toString().should.equal('OP_RETURN'); + }); it('should add these ops', function() { Script().add('OP_CHECKMULTISIG').toString().should.equal('OP_CHECKMULTISIG'); Script().add('OP_1').add('OP_2').toString().should.equal('OP_1 OP_2'); @@ -375,4 +380,99 @@ describe('Script', function() { }); }); + describe('#buildMultisigOut', function() { + var pubkey_hexs = [ + '022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da', + '03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9', + '021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18', + '02bf97f572a02a8900246d72c2e8fa3d3798a6e59c4e17de2d131d9c60d0d9b574', + '036a98a36aa7665874b1ba9130bc6d318e52fd3bdb5969532d7fc09bf2476ff842', + '033aafcbead78c08b0e0aacc1b0cdb40702a7c709b660bebd286e973242127e15b', + ]; + var test_mn = function(m, n) { + var pubkeys = pubkey_hexs.slice(0, n).map(PublicKey); + var s = Script.buildMultisigOut(pubkeys, m); + should.exist(s); + s.isMultisigOut().should.equal(true); + }; + for (var n = 1; n < 6; n++) { + for (var m = 1; m <= n; m++) { + it('should create ' + m + '-of-' + n, test_mn.bind(null, m, n)); + } + } + }); + describe('#buildPublicKeyHashOut', function() { + it('should create script from livenet address', function() { + var address = Address.fromString('1NaTVwXDDUJaXDQajoa9MqHhz4uTxtgK14'); + var s = Script.buildPublicKeyHashOut(address); + should.exist(s); + s.toString().should.equal('OP_DUP OP_HASH160 20 0xecae7d092947b7ee4998e254aa48900d26d2ce1d OP_EQUALVERIFY OP_CHECKSIG'); + s.isPublicKeyHashOut().should.equal(true); + }); + it('should create script from testnet address', function() { + var address = Address.fromString('mxRN6AQJaDi5R6KmvMaEmZGe3n5ScV9u33'); + var s = Script.buildPublicKeyHashOut(address); + should.exist(s); + s.toString().should.equal('OP_DUP OP_HASH160 20 0xb96b816f378babb1fe585b7be7a2cd16eb99b3e4 OP_EQUALVERIFY OP_CHECKSIG'); + s.isPublicKeyHashOut().should.equal(true); + }); + it('should create script from public key', function() { + var pubkey = new PublicKey('022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da'); + var s = Script.buildPublicKeyHashOut(pubkey); + should.exist(s); + s.toString().should.equal('OP_DUP OP_HASH160 20 0x9674af7395592ec5d91573aa8d6557de55f60147 OP_EQUALVERIFY OP_CHECKSIG'); + s.isPublicKeyHashOut().should.equal(true); + }); + }); + describe('#buildPublicKeyOut', function() { + it('should create script from public key', function() { + var pubkey = new PublicKey('022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da'); + var s = Script.buildPublicKeyOut(pubkey); + should.exist(s); + s.toString().should.equal('33 0x022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da OP_CHECKSIG'); + s.isPublicKeyOut().should.equal(true); + }); + }); + describe('#buildDataOut', function() { + it('should create script from empty data', function() { + var data = new Buffer(''); + var s = Script.buildDataOut(data); + should.exist(s); + s.toString().should.equal('OP_RETURN'); + s.isDataOut().should.equal(true); + }); + it('should create script from some data', function() { + var data = new Buffer('bacacafe0102030405', 'hex'); + var s = Script.buildDataOut(data); + should.exist(s); + s.toString().should.equal('OP_RETURN 9 0xbacacafe0102030405'); + s.isDataOut().should.equal(true); + }); + it('should create script from string', function() { + var data = 'hello world!!!'; + var s = Script.buildDataOut(data); + should.exist(s); + s.toString().should.equal('OP_RETURN 14 0x68656c6c6f20776f726c64212121'); + s.isDataOut().should.equal(true); + }); + }); + describe('#buildScriptHashOut', function() { + it('should create script from another script', function() { + var inner = new Script('OP_DUP OP_HASH160 20 0x06c06f6d931d7bfba2b5bd5ad0d19a8f257af3e3 OP_EQUALVERIFY OP_CHECKSIG'); + var s = Script.buildScriptHashOut(inner); + should.exist(s); + s.toString().should.equal('OP_HASH160 20 0x45ea3f9133e7b1cef30ba606f8433f993e41e159 OP_EQUAL'); + s.isScriptHashOut().should.equal(true); + }); + }); + describe('#toScriptHashOut', function() { + it('should create script from another script', function() { + var s = new Script('OP_DUP OP_HASH160 20 0x06c06f6d931d7bfba2b5bd5ad0d19a8f257af3e3 OP_EQUALVERIFY OP_CHECKSIG'); + var sho = s.toScriptHashOut(); + sho.toString().should.equal('OP_HASH160 20 0x45ea3f9133e7b1cef30ba606f8433f993e41e159 OP_EQUAL'); + sho.isScriptHashOut().should.equal(true); + }); + }); + + });