Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 93 additions & 30 deletions src/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,54 +96,117 @@ Script.prototype.parse = function() {
}
}


/**
* Compare the script to known templates of scriptPubKey.
*
* This method will compare the script to a small number of standard script
* templates and return a string naming the detected type.
*
* Currently supported are:
* Address:
* Pubkeyhash
* Paying to a Bitcoin address which is the hash of a pubkey.
* OP_DUP OP_HASH160 [pubKeyHash] OP_EQUALVERIFY OP_CHECKSIG
* Example:
*
* Pubkey:
* Paying to a public key directly.
* [pubKey] OP_CHECKSIG
* Example: txid 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098
*
* Scripthash (P2SH)
* Paying to an address which is the hash of a script
* OP_HASH160 [Scripthash] OP_EQUAL
* Example:
*
* Multisig
* Paying to multiple pubkeys and require a number of the signatures
* m [pubkey] [pubkey] [pubkey] n OP_CHECKMULTISIG
* Example:
*
* Strange:
* Any other script (no template matched).
*/

// Below is the current standard set of out types
/* const char* GetTxnOutputType(txnouttype t)
{
switch (t)
{
case TX_NONSTANDARD: return "nonstandard";
case TX_PUBKEY: return "pubkey";
case TX_PUBKEYHASH: return "pubkeyhash";
case TX_SCRIPTHASH: return "scripthash";
case TX_MULTISIG: return "multisig";
case TX_NULL_DATA: return "nulldata";
}
return NULL;
}*/

// https://github.com/bitcoin/bitcoin/blob/19e5b9d2dfcac4efadba636745485d9660fb1abe/src/script.cpp#L75

// supporting tx_null_data https://github.com/bitcoin/bitcoin/pull/3128
// https://helloblock.io/mainnet/transactions/ebc9fa1196a59e192352d76c0f6e73167046b9d37b8302b6bb6968dfd279b767
Script.prototype.getOutType = function() {
if (this.chunks[this.chunks.length - 1] == Opcode.map.OP_EQUAL &&
this.chunks[0] == Opcode.map.OP_HASH160 &&
this.chunks.length == 3) {
// Transfer to M-OF-N
return 'P2SH'
} else if (this.chunks.length == 5 &&
this.chunks[0] == Opcode.map.OP_DUP &&
this.chunks[1] == Opcode.map.OP_HASH160 &&
this.chunks[3] == Opcode.map.OP_EQUALVERIFY &&
this.chunks[4] == Opcode.map.OP_CHECKSIG) {
if (this.chunks.length == 5 &&
this.chunks[0] == Opcode.map.OP_DUP &&
this.chunks[1] == Opcode.map.OP_HASH160 &&
Array.isArray(this.chunks[2]) &&
this.chunks[2].length === 20 &&
this.chunks[3] == Opcode.map.OP_EQUALVERIFY &&
this.chunks[4] == Opcode.map.OP_CHECKSIG) {
// Transfer to Bitcoin address
return 'Pubkey'
return 'pubkeyhash';
} else if (this.chunks.length === 2 &&
Array.isArray(this.chunks[0]) &&
this.chunks[1] === Opcode.map.OP_CHECKSIG) {
// [pubkey] OP_CHECKSIG
return 'pubkey';
} else if (this.chunks[this.chunks.length - 1] == Opcode.map.OP_EQUAL &&
this.chunks[0] == Opcode.map.OP_HASH160 &&
Array.isArray(this.chunks[1]) &&
this.chunks[1].length === 20 &&
this.chunks.length == 3) {
// Transfer to M-OF-N
return 'scripthash';
} else if (this.chunks.length > 3 &&
// m is a smallint
isSmallIntOp(this.chunks[0]) &&
// n is a smallint
isSmallIntOp(this.chunks[this.chunks.length - 2]) &&
// n greater or equal to m
this.chunks[0] <= this.chunks[this.chunks.length - 2] &&
// n cannot be 0
this.chunks[this.chunks.length - 2] !== Opcode.map.OP_0 &&
// n is the size of chunk length minus 3 (m, n, OP_CHECKMULTISIG)
this.chunks.length - 3 === this.chunks[this.chunks.length - 2] - Opcode.map.OP_RESERVED &&
// last chunk is OP_CHECKMULTISIG
this.chunks[this.chunks.length - 1] == Opcode.map.OP_CHECKMULTISIG) {
return 'multisig'
} else if (this.chunks[0] === Opcode.map.OP_RETURN) {
return 'nulldata'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get some test cases to cover the added types?

} else {
return 'Strange'
return 'nonstandard';
}
}

function isSmallIntOp(opcode) {
return ((opcode == Opcode.map.OP_0) ||
((opcode >= Opcode.map.OP_1) && (opcode <= Opcode.map.OP_16)));
};

/**
* Returns the address corresponding to this output in hash160 form.
* Assumes strange scripts are P2SH
*/
Script.prototype.toScriptHash = function() {
var outType = this.getOutType()

if (outType == 'Pubkey') {
if (outType == 'pubkeyhash') {
return this.chunks[2]
}

if (outType == 'P2SH') {
if (outType == 'scripthash') {
return crypto.hash160(this.buffer)
}

Expand All @@ -154,11 +217,11 @@ Script.prototype.toScriptHash = function() {
Script.prototype.getToAddress = function() {
var outType = this.getOutType()

if (outType == 'Pubkey') {
if (outType == 'pubkeyhash') {
return new Address(this.chunks[2])
}

if (outType == 'P2SH') {
if (outType == 'scripthash') {
return new Address(this.chunks[1], 5)
}

Expand Down Expand Up @@ -197,23 +260,23 @@ Script.prototype.getFromAddress = function(){
*/
Script.prototype.getInType = function() {
if (this.chunks.length == 1 &&
Array.isArray(this.chunks[0])) {
Array.isArray(this.chunks[0])) {
// Direct IP to IP transactions only have the signature in their scriptSig.
// TODO: We could also check that the length of the data is correct.
return 'Pubkey'
return 'pubkey';
} else if (this.chunks.length == 2 &&
Array.isArray(this.chunks[0]) &&
Array.isArray(this.chunks[1])) {
return 'Address'
Array.isArray(this.chunks[0]) &&
Array.isArray(this.chunks[1])) {
return 'pubkeyhash';
} else if (this.chunks[0] == Opcode.map.OP_0 &&
this.chunks.slice(1).reduce(function(t, chunk, i) {
return t && Array.isArray(chunk) && (chunk[0] == 48 || i == this.chunks.length - 1)
}, true)) {
return 'Multisig'
this.chunks.slice(1).reduce(function(t, chunk, i) {
return t && Array.isArray(chunk) && (chunk[0] == 48 || i == this.chunks.length - 1);
}, true)) {
return 'multisig';
} else {
return 'Strange'
return 'nonstandard';
}
}
};

/**
* Returns the affected public key for this input.
Expand All @@ -230,9 +293,9 @@ Script.prototype.getInType = function() {
*/
Script.prototype.simpleInPubKey = function() {
switch (this.getInType()) {
case 'Address':
case 'pubkeyhash':
return this.chunks[1]
case 'Pubkey':
case 'pubkey':
// TODO: Theoretically, we could recover the pubkey from the sig here.
// See https://bitcointalk.org/?topic=6430.0
throw new Error('Script does not contain pubkey')
Expand Down
34 changes: 30 additions & 4 deletions test/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ describe('Script', function() {
p2shScriptPubKey = "a914e8c300c87986efa84c37c0519929019ef86eb5b487"
pubkeyScriptPubKey = "76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac"
addressScriptSig = "48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8"

// https://helloblock.io/transactions/09dd94f2c85262173da87a745a459007bb1eed6eeb6bfa238a0cd91a16cf7790
validMultisigScript = '5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae'

// https://helloblock.io/transactions/dfa8ff97f33cb83dbaa22ed3a99883218d6afd681d486b374496d145b39a63b7
// asm: "0 0 0 OP_CHECKMULTISIG"
invalidMultisigScript = '000000ae'

// https://helloblock.io/transactions/5e9be7fb36ee49ce84bee4c8ef38ad0efc0608b78dae1c2c99075297ef527890
// op_return
opreturnScript = '6a2606deadbeef03f895a2ad89fb6d696497af486cb7c644a27aa568c7a18dd06113401115185474'
})

describe('constructor', function() {
Expand All @@ -33,19 +44,34 @@ describe('Script', function() {
describe('getOutType', function() {
it('works for p2sh', function() {
var script = Script.fromHex(p2shScriptPubKey)
assert.equal(script.getOutType(), 'P2SH')
assert.equal(script.getOutType(), 'scripthash')
})

it('works for pubkey', function() {
it('works for pubkeyhash', function() {
var script = Script.fromHex(pubkeyScriptPubKey)
assert.equal(script.getOutType(), 'Pubkey')
assert.equal(script.getOutType(), 'pubkeyhash')
})

it(' > Supports Multisig', function() {
var script = Script.fromHex(validMultisigScript)
assert.equal(script.getOutType(), 'multisig')
})

it(' > Supports invalid Multisig', function() {
var script = Script.fromHex(invalidMultisigScript)
assert.equal(script.getOutType(), 'nonstandard')
})

it(' > Supports null_data (OP_RETURN)', function() {
var script = Script.fromHex(opreturnScript)
assert.equal(script.getOutType(), 'nulldata')
})
})

describe('getInType', function() {
it('works for address', function() {
var script = Script.fromHex(addressScriptSig)
assert.equal(script.getInType(), 'Address')
assert.equal(script.getInType(), 'pubkeyhash')
})
})

Expand Down