From 458abe069eaca088332d973f3a7dc7d4bd9a8bc5 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Wed, 13 May 2015 09:38:58 -0400 Subject: [PATCH] Fixed issue with handling invalid output scripts - Changed toObject serialization to always use a hexa string for a script - Updated inspect method to handle a null script - Roundtrip toObject/fromObject with an invalid script - Additional test coverage for Output --- .jshintrc | 2 +- lib/transaction/input/input.js | 2 +- lib/transaction/output.js | 88 +++++++++++++++++---------------- test/block.js | 4 +- test/transaction/input/input.js | 28 ++++++++--- test/transaction/output.js | 59 +++++++++++++++++----- test/transaction/transaction.js | 3 +- 7 files changed, 120 insertions(+), 66 deletions(-) diff --git a/.jshintrc b/.jshintrc index 40fd8c6..a32b29c 100644 --- a/.jshintrc +++ b/.jshintrc @@ -25,7 +25,7 @@ "maxparams": 4, // Maximum number of parameters for a function "maxstatements": 15, // Maximum number of statements in a function - "maxcomplexity": 6, // Cyclomatic complexity (http://en.wikipedia.org/wiki/Cyclomatic_complexity) + "maxcomplexity": 10, // Cyclomatic complexity (http://en.wikipedia.org/wiki/Cyclomatic_complexity) "maxdepth": 4, // Maximum depth of nested control structures "maxlen": 120, // Maximum number of cols in a line "multistr": true, // Allow use of multiline EOL escaping diff --git a/lib/transaction/input/input.js b/lib/transaction/input/input.js index ad77212..841ed18 100644 --- a/lib/transaction/input/input.js +++ b/lib/transaction/input/input.js @@ -119,7 +119,7 @@ Input.prototype.setScript = function(script) { this._scriptBuffer = script.toBuffer(); } else if (JSUtil.isHexa(script)) { // hex string script - this._scriptBuffer = new Buffer(script, 'hex'); + this._scriptBuffer = new buffer.Buffer(script, 'hex'); } else if (_.isString(script)) { // human readable string script this._script = new Script(script); diff --git a/lib/transaction/output.js b/lib/transaction/output.js index 963d1cc..7e2a261 100644 --- a/lib/transaction/output.js +++ b/lib/transaction/output.js @@ -12,15 +12,20 @@ var errors = require('../errors'); var MAX_SAFE_INTEGER = 0x1fffffffffffff; -function Output(params) { +function Output(args) { if (!(this instanceof Output)) { - return new Output(params); + return new Output(args); } - if (params) { - if (JSUtil.isValidJSON(params)) { - return Output.fromJSON(params); + if (JSUtil.isValidJSON(args)) { + return Output.fromJSON(args); + } else if (_.isObject(args)) { + this.satoshis = args.satoshis; + if (_.isString(args.script) && JSUtil.isHexa(args.script)) { + args.script = new buffer.Buffer(args.script, 'hex'); } - return this._fromObject(params); + this.setScript(args.script); + } else { + throw new TypeError('Unrecognized argument for Output'); } } @@ -28,17 +33,6 @@ Object.defineProperty(Output.prototype, 'script', { configurable: false, enumerable: true, get: function() { - if (!this._script) { - try { - this._script = Script.fromBuffer(this._scriptBuffer); - } catch(e) { - if(e instanceof errors.Script.InvalidBuffer) { - this._script = null; - } else { - throw e; - } - } - } return this._script; } }); @@ -84,45 +78,49 @@ Output.prototype.invalidSatoshis = function() { return false; }; -Output.prototype._fromObject = function(param) { - this.satoshis = param.satoshis; - if (param.script || param.scriptBuffer) { - this.setScript(param.script || param.scriptBuffer); - } - return this; -}; - Output.prototype.toObject = function toObject() { - return { - satoshis: this.satoshis, - script: this.script.toString() + var obj = { + satoshis: this.satoshis }; + obj.script = this._scriptBuffer.toString('hex'); + return obj; }; Output.prototype.toJSON = function toJSON() { return JSON.stringify(this.toObject()); }; -Output.fromJSON = function(json) { - if (JSUtil.isValidJSON(json)) { - json = JSON.parse(json); - } +Output.fromJSON = function(data) { + $.checkArgument(JSUtil.isValidJSON(data), 'data must be valid JSON'); + var json = JSON.parse(data); return new Output({ - satoshis: json.satoshis || +json.valuebn, + satoshis: Number(json.satoshis), script: new Script(json.script) }); }; +Output.prototype.setScriptFromBuffer = function(buffer) { + this._scriptBuffer = buffer; + try { + this._script = Script.fromBuffer(this._scriptBuffer); + } catch(e) { + if (e instanceof errors.Script.InvalidBuffer) { + this._script = null; + } else { + throw e; + } + } +}; + Output.prototype.setScript = function(script) { if (script instanceof Script) { this._scriptBuffer = script.toBuffer(); this._script = script; } else if (_.isString(script)) { - this._script = new Script(script); + this._script = Script.fromString(script); this._scriptBuffer = this._script.toBuffer(); } else if (bufferUtil.isBuffer(script)) { - this._scriptBuffer = script; - this._script = null; + this.setScriptFromBuffer(script); } else { throw new TypeError('Invalid argument type: script'); } @@ -130,19 +128,25 @@ Output.prototype.setScript = function(script) { }; Output.prototype.inspect = function() { - return ''; + var scriptStr; + if (this.script) { + scriptStr = this.script.inspect(); + } else { + scriptStr = this._scriptBuffer.toString('hex'); + } + return ''; }; Output.fromBufferReader = function(br) { - var output = new Output(); - output.satoshis = br.readUInt64LEBN(); + var obj = {}; + obj.satoshis = br.readUInt64LEBN(); var size = br.readVarintNum(); if (size !== 0) { - output._scriptBuffer = br.read(size); + obj.script = br.read(size); } else { - output._scriptBuffer = new buffer.Buffer([]); + obj.script = new buffer.Buffer([]); } - return output; + return new Output(obj); }; Output.prototype.toBufferWriter = function(writer) { diff --git a/test/block.js b/test/block.js index d55152b..ba669fd 100644 --- a/test/block.js +++ b/test/block.js @@ -192,8 +192,8 @@ describe('Block', function() { }], outputs: [{ satoshis: 5000000000, - script: '65 0x0496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be' + - '63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858ee OP_CHECKSIG' + script: '410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c' + + '52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac' }], nLockTime: 0 }] diff --git a/test/transaction/input/input.js b/test/transaction/input/input.js index c07b550..8603f46 100644 --- a/test/transaction/input/input.js +++ b/test/transaction/input/input.js @@ -30,14 +30,26 @@ describe('Transaction.Input', function() { script: new Script(), satoshis: 1000000 }; - var coinbaseJSON = '{"prevTxId":"0000000000000000000000000000000000000000000000000000000000000000"' + - ',"outputIndex":4294967295,"script":""}'; - var otherJSON = '{"txidbuf":"a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458"' + - ',"txoutnum":0,"seqnum":4294967295,"script":"71 0x3044022006553276ec5b885ddf5cc1d7' + - '9e1e3dadbb404b60ad4cc00318e215654f13242102200757c17b36e3d0492fb9cf597032e5afbea67a59274e64af' + - '5a05d12e5ea2303901 33 0x0223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5e",' + - '"output":{"satoshis":100000,"script":"OP_DUP OP_HASH160 20 0x88d9931ea73d60eaf7e5671efc0552b' + - '912911f2a OP_EQUALVERIFY OP_CHECKSIG"}}'; + + var coinbaseJSON = JSON.stringify({ + prevTxId: '0000000000000000000000000000000000000000000000000000000000000000', + outputIndex: 4294967295, + script:'' + }); + + var otherJSON = JSON.stringify({ + txidbuf: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', + txoutnum: 0, + seqnum:4294967295, + script: '71 0x3044022006553276ec5b885ddf5cc1d79e1e3dadbb404b60ad4cc00318e21565' + + '4f13242102200757c17b36e3d0492fb9cf597032e5afbea67a59274e64af5a05d12e5ea2303901 ' + + '33 0x0223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5e', + output: { + 'satoshis':100000, + 'script':'OP_DUP OP_HASH160 20 0x88d9931ea73d60eaf7e5671efc0552b912911f2a ' + + 'OP_EQUALVERIFY OP_CHECKSIG' + } + }); it('has abstract methods: "getSignatures", "isFullySigned", "addSignature", "clearSignatures"', function() { var input = new Input(output); diff --git a/test/transaction/output.js b/test/transaction/output.js index c6ca965..a12c46e 100644 --- a/test/transaction/output.js +++ b/test/transaction/output.js @@ -22,6 +22,12 @@ describe('Output', function() { script: Script.empty() }); + it('throws error with unrecognized argument', function() { + (function() { + var out = new Output(12345); + }).should.throw(TypeError); + }); + it('can be assigned a satoshi amount in big number', function() { var newOutput = new Output({ satoshis: new BN(100), @@ -88,8 +94,13 @@ describe('Output', function() { expectEqualOutputs(output, deserialized); }); + it('can instantiate from JSON', function() { + var out = new Output(JSON.stringify(output.toObject())); + should.exist(out); + }); + it('can set a script from a buffer', function() { - var newOutput = Output(output); + var newOutput = new Output(output.toObject()); newOutput.setScript(Script().add(0).toBuffer()); newOutput.inspect().should.equal('>'); }); @@ -121,6 +132,34 @@ describe('Output', function() { expectEqualOutputs(newOutput, otherOutput); }); + it('toObject will handle an invalid (null) script', function() { + // block 000000000000000b7e48f88e86ceee3e97b4df7c139f5411d14735c1b3c36791 (livenet) + // transaction index 2 + // txid ebc9fa1196a59e192352d76c0f6e73167046b9d37b8302b6bb6968dfd279b767 + var transaction = bitcore.Transaction(); + transaction.fromString('01000000019ac03d5ae6a875d970128ef9086cef276a1919684a6988023cc7254691d97e6d010000006b4830450221009d41dc793ba24e65f571473d40b299b6459087cea1509f0d381740b1ac863cb6022039c425906fcaf51b2b84d8092569fb3213de43abaff2180e2a799d4fcb4dd0aa012102d5ede09a8ae667d0f855ef90325e27f6ce35bbe60a1e6e87af7f5b3c652140fdffffffff080100000000000000010101000000000000000202010100000000000000014c0100000000000000034c02010100000000000000014d0100000000000000044dffff010100000000000000014e0100000000000000064effffffff0100000000'); + var obj = transaction.toObject(); + obj.outputs[2].script.should.equal('4c'); + obj.outputs[4].script.should.equal('4d'); + obj.outputs[6].script.should.equal('4e'); + }); + + it('#toObject roundtrip will handle an invalid (null) script', function() { + var invalidOutputScript = new Buffer('0100000000000000014c', 'hex'); + var br = new bitcore.encoding.BufferReader(invalidOutputScript); + var output = Output.fromBufferReader(br); + var output2 = new Output(output.toObject()); + should.equal(output2.script, null); + should.equal(output2._scriptBuffer.toString('hex'), '4c'); + }); + + it('inspect will work with an invalid (null) script', function() { + var invalidOutputScript = new Buffer('0100000000000000014c', 'hex'); + var br = new bitcore.encoding.BufferReader(invalidOutputScript); + var output = Output.fromBufferReader(br); + output.inspect().should.equal(''); + }); + it('roundtrips to/from JSON', function() { var json = output2.toJSON(); var o3 = new Output(json); @@ -134,22 +173,20 @@ describe('Output', function() { it('sets script to null if it is an InvalidBuffer', function() { var output = new Output({ - satoshis: 1000 + satoshis: 1000, + script: new Buffer('4c', 'hex') }); - output._scriptBuffer = new Buffer('4c', 'hex'); - - var result = output.script; - should.equal(result, null); + should.equal(output.script, null); }); it('should throw an error if Script throws an error that is not InvalidBuffer', function() { - var output = new Output({ - satoshis: 1000 + var output = Output({ + satoshis: 1000, + script: new Script() }); - output._scriptBuffer = 'bad'; - (function() { - var result = output.script; + output.setScriptFromBuffer('bad'); }).should.throw('Invalid hex string'); }); + }); diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index e58117b..c4498f0 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -32,6 +32,7 @@ describe('Transaction', function() { }); var testScript = 'OP_DUP OP_HASH160 20 0x88d9931ea73d60eaf7e5671efc0552b912911f2a OP_EQUALVERIFY OP_CHECKSIG'; + var testScriptHex = '76a91488d9931ea73d60eaf7e5671efc0552b912911f2a88ac'; var testPrevTx = 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458'; var testAmount = 1020000; var testTransaction = new Transaction() @@ -45,7 +46,7 @@ describe('Transaction', function() { it('can serialize to a plain javascript object', function() { var object = testTransaction.toObject(); object.inputs[0].output.satoshis.should.equal(testAmount); - object.inputs[0].output.script.toString().should.equal(testScript); + object.inputs[0].output.script.should.equal(testScriptHex); object.inputs[0].prevTxId.should.equal(testPrevTx); object.inputs[0].outputIndex.should.equal(0); object.outputs[0].satoshis.should.equal(testAmount - 10000);