Merge pull request #662 from maraoz/script/improvements
Improve Script API
This commit is contained in:
commit
802a0a55dd
|
@ -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,
|
||||
|
|
135
lib/script.js
135
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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
112
test/script.js
112
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue