diff --git a/src/script.js b/src/script.js index 8c71c5c..052175e 100644 --- a/src/script.js +++ b/src/script.js @@ -12,6 +12,16 @@ var REVERSE_OPS = (function () { return result })() +var LIST_DECODE_NAMES = [ + decodePubKeyOutput, + decodePubKeyHashOutput, + decodeMultisigOutput, + decodeNullDataOutput, + decodeScriptHashOutput, + decodeWitnessPubKeyHashOutput, + decodeWitnessScriptHashOutput +] + var OP_INT_BASE = OPS.OP_RESERVED // OP_1 - 1 function compile (chunks) { @@ -167,15 +177,34 @@ function isPubKeyHashInput (script) { isCanonicalPubKey(chunks[1]) } -function isPubKeyHashOutput (script) { - var buffer = compile(script) +function decodePubKeyHashOutput (buffer, chunks) { + if (buffer.length !== 25) { + throw new Error('pub-key-hash output script is 25 bytes') + } + if (buffer[0] !== OPS.OP_DUP) { + throw new Error('PubKeyHash script missing OP_DUP') + } + if (buffer[1] !== OPS.OP_HASH160) { + throw new Error('PubKeyHash script missing OP_HASH160') + } + if (buffer[2] !== 0x14) { + throw new Error('Incorrect opcode for pubkeyhash') + } + if (buffer[23] !== OPS.OP_EQUALVERIFY) { + throw new Error('PubKeyHash script missing OP_EQUALVERIFY') + } + if (buffer[24] !== OPS.OP_CHECKSIG) { + throw new Error('PubKeyHash script missing OP_CHECKSIG') + } - return buffer.length === 25 && - buffer[0] === OPS.OP_DUP && - buffer[1] === OPS.OP_HASH160 && - buffer[2] === 0x14 && - buffer[23] === OPS.OP_EQUALVERIFY && - buffer[24] === OPS.OP_CHECKSIG + return { + type: 'pubkeyhash', + pubKeyHash: chunks[2] + } +} + +function isPubKeyHashOutput (script) { + return determinesTypeOrNonstandard([decodePubKeyHashOutput], script) === 'pubkeyhash' } function isPubKeyInput (script) { @@ -185,12 +214,27 @@ function isPubKeyInput (script) { isCanonicalSignature(chunks[0]) } -function isPubKeyOutput (script) { - var chunks = decompile(script) +function decodePubKeyOutput (script, chunks) { + if (script[0] !== 0x21 && script[0] !== 0x41) { + throw new Error('Bad (or non-minimal) public key length for pub') + } + if (!isCanonicalPubKey(chunks[0])) { + throw new Error('pub-key output does not have a canonical public key') + } + if (chunks[1] !== OPS.OP_CHECKSIG) { + throw new Error('pub-key output missing OP_CHECKSIG operator') + } + if (chunks.length !== 2) { + throw new Error('pub-key output has two elements') + } + return { + type: 'pubkey', + publicKey: chunks[0] + } +} - return chunks.length === 2 && - isCanonicalPubKey(chunks[0]) && - chunks[1] === OPS.OP_CHECKSIG +function isPubKeyOutput (script) { + return determinesTypeOrNonstandard([decodePubKeyOutput], script) === 'pubkey' } function isScriptHashInput (script, allowIncomplete) { @@ -206,32 +250,69 @@ function isScriptHashInput (script, allowIncomplete) { // is redeemScript a valid script? if (redeemScriptChunks.length === 0) return false - return classifyInput(scriptSigChunks, allowIncomplete) === classifyOutput(redeemScriptChunks) + return classifyInput(scriptSigChunks, allowIncomplete) === classifyOutput(lastChunk) +} + +function decodeScriptHashOutput (chunks) { + if (chunks[0] !== OPS.OP_HASH160) { + throw new Error() + } + if (chunks[1] !== 0x14) { + throw new Error() + } + if (chunks[22] !== OPS.OP_EQUAL) { + throw new Error() + } + return { + type: 'scripthash', + scriptHash: chunks[1] + } } function isScriptHashOutput (script) { - var buffer = compile(script) + return determinesTypeOrNonstandard([decodeScriptHashOutput], script) === 'scripthash' +} - return buffer.length === 23 && - buffer[0] === OPS.OP_HASH160 && - buffer[1] === 0x14 && - buffer[22] === OPS.OP_EQUAL +function decodeWitnessPubKeyHashOutput (script, chunks) { + if (script.length !== 22) { + throw new Error('P2WPKH script should be 22 bytes') + } + if (script[0] !== OPS.OP_0) { + throw new Error('Missing v0 prefix for witness keyhash') + } + if (script[1] !== 0x14) { + throw new Error('Witness keyhash length marker is wrong') + } + + return { + type: 'witnesspubkeyhash', + witnessKeyHash: chunks[2] + } } function isWitnessPubKeyHashOutput (script) { - var buffer = compile(script) + return determinesTypeOrNonstandard([decodeWitnessPubKeyHashOutput], script) === 'witnesspubkeyhash' +} - return buffer.length === 22 && - buffer[0] === OPS.OP_0 && - buffer[1] === 0x14 +function decodeWitnessScriptHashOutput (script, chunks) { + if (script.length !== 34) { + throw new Error('P2WSH script should be 34 bytes') + } + if (script[0] !== OPS.OP_0) { + throw new Error('Missing v0 prefix for witness script hash') + } + if (script[1] !== 0x20) { + throw new Error('Witness program length marker is wrong') + } + + return { + type: 'witnessscripthash', + witnessScriptHash: chunks[2] + } } function isWitnessScriptHashOutput (script) { - var buffer = compile(script) - - return buffer.length === 34 && - buffer[0] === OPS.OP_0 && - buffer[1] === 0x20 + return determinesTypeOrNonstandard([decodeWitnessScriptHashOutput], script) === 'witnessscripthash' } // allowIncomplete is to account for combining signatures @@ -250,8 +331,7 @@ function isMultisigInput (script, allowIncomplete) { return chunks.slice(1).every(isCanonicalSignature) } -function parseMultisigScript (scriptPubKey) { - var chunks = decompile(scriptPubKey) +function decodeMultisigOutput (scriptPubKey, chunks) { if (chunks.length < 4) { throw new Error('Multisig script should contain at least 4 elements') } @@ -286,6 +366,7 @@ function parseMultisigScript (scriptPubKey) { } return { + type: 'multisig', nRequiredSigs: m, nPublicKeys: n, publicKeyBuffers: keys @@ -293,39 +374,47 @@ function parseMultisigScript (scriptPubKey) { } function isMultisigOutput (script) { - try { - parseMultisigScript(script) - return true - } catch (e) { - return false - } + return determinesTypeOrNonstandard([decodeMultisigOutput], script) === 'multisig' } function isNullDataOutput (script) { - var chunks = decompile(script) - return chunks[0] === OPS.OP_RETURN + return determinesTypeOrNonstandard([decodeNullDataOutput], script) === 'nulldata' +} + +function decodeNullDataOutput (script, chunks) { + if (script[0] !== OPS.OP_RETURN) { + throw new Error('Missing OP_RETURN at start of script') + } + + return { + type: 'nulldata' + } +} + +function determinesTypeOrNonstandard (functions, script) { + if (!types.Array(functions)) { + throw new Error('Must provide an array of functions to determinesTypeOrNonstandard') + } + if (!types.Buffer(script)) { + throw new Error('Must provide a script to determinesTypeOrNonstandard') + } + var decoded + var decompiled = decompile(script) + var type = 'nonstandard' + for (var i = 0; i < functions.length && type === 'nonstandard'; i++) { + try { + decoded = functions[i](script, decompiled) + type = decoded.type + } catch (e) { + + } + } + + return type } function classifyOutput (script) { - var chunks = decompile(script) - - if (isWitnessPubKeyHashOutput(chunks)) { - return 'witnesspubkeyhash' - } else if (isWitnessScriptHashOutput(chunks)) { - return 'witnessscripthash' - } else if (isPubKeyHashOutput(chunks)) { - return 'pubkeyhash' - } else if (isScriptHashOutput(chunks)) { - return 'scripthash' - } else if (isMultisigOutput(chunks)) { - return 'multisig' - } else if (isPubKeyOutput(chunks)) { - return 'pubkey' - } else if (isNullDataOutput(chunks)) { - return 'nulldata' - } - - return 'nonstandard' + return determinesTypeOrNonstandard(LIST_DECODE_NAMES, script) } function classifyInput (script, allowIncomplete) { @@ -426,7 +515,7 @@ function witnessScriptHashInput (scriptSig, scriptPubKey) { // OP_0 [signatures ...] function multisigInput (signatures, scriptPubKey) { if (scriptPubKey) { - var scriptData = parseMultisigScript(scriptPubKey) + var scriptData = decodeMultisigOutput(scriptPubKey, decompile(scriptPubKey)) if (signatures.length < scriptData.nRequiredSigs) throw new Error('Not enough signatures provided') if (signatures.length > scriptData.nPublicKeys) throw new Error('Too many signatures provided') } @@ -449,32 +538,41 @@ module.exports = { isCanonicalPubKey: isCanonicalPubKey, isCanonicalSignature: isCanonicalSignature, isDefinedHashType: isDefinedHashType, - isPubKeyHashInput: isPubKeyHashInput, - isPubKeyHashOutput: isPubKeyHashOutput, - isPubKeyInput: isPubKeyInput, + + decodePubKeyOutput: decodePubKeyOutput, + decodePubKeyHashOutput: decodePubKeyHashOutput, + decodeMultisigOutput: decodeMultisigOutput, + decodeScriptHashOutput: decodeScriptHashOutput, + decodeNullDataOutput: decodeNullDataOutput, + decodeWitnessPubKeyHashOutput: decodeWitnessPubKeyHashOutput, + decodeWitnessScriptHashOutput: decodeWitnessScriptHashOutput, + isPubKeyOutput: isPubKeyOutput, - isScriptHashInput: isScriptHashInput, + isPubKeyHashOutput: isPubKeyHashOutput, + isMultisigOutput: isMultisigOutput, isScriptHashOutput: isScriptHashOutput, + isNullDataOutput: isNullDataOutput, isWitnessPubKeyHashOutput: isWitnessPubKeyHashOutput, isWitnessScriptHashOutput: isWitnessScriptHashOutput, - isMultisigInput: isMultisigInput, - isMultisigOutput: isMultisigOutput, - isNullDataOutput: isNullDataOutput, - - parseMultisigScript: parseMultisigScript, classifyOutput: classifyOutput, + + isPubKeyInput: isPubKeyInput, + isPubKeyHashInput: isPubKeyHashInput, + isMultisigInput: isMultisigInput, + isScriptHashInput: isScriptHashInput, classifyInput: classifyInput, + pubKeyOutput: pubKeyOutput, pubKeyHashOutput: pubKeyHashOutput, + multisigOutput: multisigOutput, scriptHashOutput: scriptHashOutput, + nullDataOutput: nullDataOutput, witnessPubKeyHashOutput: witnessPubKeyHashOutput, - witnessScriptHashInput: witnessScriptHashInput, witnessScriptHashOutput: witnessScriptHashOutput, - multisigOutput: multisigOutput, pubKeyInput: pubKeyInput, pubKeyHashInput: pubKeyHashInput, - scriptHashInput: scriptHashInput, multisigInput: multisigInput, - nullDataOutput: nullDataOutput + scriptHashInput: scriptHashInput, + witnessScriptHashInput: witnessScriptHashInput } diff --git a/src/transaction_builder.js b/src/transaction_builder.js index c34ec17..402917f 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -108,7 +108,7 @@ function expandOutput (script, scriptType, ourPubKey) { var scriptChunks = bscript.decompile(script) if (!scriptType) { - scriptType = bscript.classifyOutput(scriptChunks) + scriptType = bscript.classifyOutput(script) } var pubKeys = [] diff --git a/test/address.js b/test/address.js index a254343..e765eb0 100644 --- a/test/address.js +++ b/test/address.js @@ -36,15 +36,6 @@ describe('address', function () { }) }) - fixtures.valid.forEach(function (f) { - it('parses (as chunks) ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () { - var chunks = bscript.decompile(bscript.fromASM(f.script)) - var address = baddress.fromOutputScript(chunks, networks[f.network]) - - assert.strictEqual(address, f.base58check) - }) - }) - fixtures.invalid.fromOutputScript.forEach(function (f) { it('throws when ' + f.script.slice(0, 30) + '... ' + f.exception, function () { var script = bscript.fromASM(f.script) diff --git a/test/fixtures/script.json b/test/fixtures/script.json index c4c6d07..86fc8f8 100644 --- a/test/fixtures/script.json +++ b/test/fixtures/script.json @@ -226,18 +226,90 @@ { "description": "non-canonical pubkey (too long)", "scriptPubKey": "02359c6e3f04cefbf089cf1d6670dc47c3fb4df68e2bad1fa5a369f9ce4b42bbd1ffffff OP_CHECKSIG" + }, + { + "description": "last operator is wrong for pubkey-output", + "scriptPubKeyHex": "21027a71801ab59336de37785c50005b6abd8ea859eecce1edbe8e81afa74ee5c752ae" + }, + { + "description": "missing OP_CHECKSIG", + "scriptPubKeyHex": "21027a71801ab59336de37785c50005b6abd8ea859eecce1edbe8e81afa74ee5c752" + }, + { + "description": "non-canonical pubkey (bad prefix)", + "scriptPubKey": "427a71801ab59336de37785c50005b6abd8ea859eecce1edbe8e81afa74ee5c752 OP_CHECKSIG" + }, + { + "description": "has extra opcode at the end isPubKeyOutput", + "scriptPubKey": "027a71801ab59336de37785c50005b6abd8ea859eecce1edbe8e81afa74ee5c752 OP_CHECKSIG OP_0" } ], "isPubKeyHashOutput": [ { "description": "non-minimal encoded isPubKeyHashOutput (non BIP62 compliant)", "scriptPubKeyHex": "76a94c14aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac" + }, + { + "description": "bad OP_DUP isPubKeyHashOutput", + "scriptPubKeyHex": "aca914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac" + }, + { + "description": "bad OP_HASH160 isPubKeyHashOutput", + "scriptPubKeyHex": "76ac14aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac" + }, + { + "description": "bad OP_EQUALVERIFY isPubKeyHashOutput", + "scriptPubKeyHex": "76a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5acac" + }, + { + "description": "bad OP_CHECKSIG isPubKeyHashOutput", + "scriptPubKeyHex": "76a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c58888" + }, + { + "description": "bad length isPubKeyHashOutput", + "scriptPubKeyHex": "76a920aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac" + }, + { + "description": "has something at the end isPubKeyHashOutput", + "scriptPubKeyHex": "76a920aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac00" } ], "isScriptHashOutput": [ { "description": "non-minimal encoded isScriptHashOutput (non BIP62 compliant)", "scriptPubKeyHex": "a94c14c286a1af0947f58d1ad787385b1c2c4a976f9e7187" + }, + { + "description": "wrong OP_HASH160 opcode", + "scriptPubKeyHex": "ac4c14c286a1af0947f58d1ad787385b1c2c4a976f9e7187" + }, + { + "description": "wrong length marker", + "scriptPubKeyHex": "a916c286a1af0947f58d1ad787385b1c2c4a976f9e7187" + }, + { + "description": "wrong OP_EQUAL opcode", + "scriptPubKeyHex": "a914c286a1af0947f58d1ad787385b1c2c4a976f9e7188" + } + ], + "isWitnessPubKeyHashOutput": [ + { + "description": "wrong version", + "scriptPubKeyHex": "51149090909090909090909090909090909090909090" + }, + { + "description": "wrong length marker", + "scriptPubKeyHex": "00209090909090909090909090909090909090909090" + } + ], + "isWitnessScriptHashOutput": [ + { + "description": "wrong version", + "scriptPubKeyHex": "51209090909090909090909090909090909090909090909090909090909090909090" + }, + { + "description": "wrong length marker", + "scriptPubKeyHex": "00219090909090909090909090909090909090909090909090909090909090909090" } ], "isMultisigOutput": [ diff --git a/test/script.js b/test/script.js index 0a23de6..0a2bf5b 100644 --- a/test/script.js +++ b/test/script.js @@ -10,7 +10,16 @@ var fixtures = require('./fixtures/script.json') describe('script', function () { // TODO - describe.skip('isCanonicalPubKey', function () {}) + describe('isCanonicalPubKey', function () { + it('rejects if not provided a Buffer', function () { + assert.strictEqual(false, bscript.isCanonicalPubKey(0)) + }) + it('rejects smaller than 33', function () { + for (var i = 0; i < 33; i++) { + assert.strictEqual(false, bscript.isCanonicalPubKey(new Buffer('', i))) + } + }) + }) describe.skip('isCanonicalSignature', function () {}) describe('fromASM/toASM', function () {