Merge pull request #662 from maraoz/script/improvements

Improve Script API
This commit is contained in:
Braydon Fuller 2014-12-04 18:23:25 -05:00
commit 802a0a55dd
4 changed files with 243 additions and 31 deletions

View File

@ -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,

View File

@ -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;

View File

@ -92,13 +92,22 @@ describe('Opcode', function() {
Opcode('OP_16')
];
describe('@isSmallIntOp', function() {
var testSmallInt = function() {
Opcode.isSmallIntOp(this).should.equal(true);
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(op));
it('should work for small int ' + op, testSmallInt.bind(null, i, op));
}
});
describe('@isSmallIntOp', function() {
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, testIsSmallInt.bind(null, op));
}
it('should work for non-small ints', function() {

View File

@ -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);
});
});
});