diff --git a/.gitignore b/.gitignore index 71b051e8..913bc0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ npm-debug.log +key.key node_modules/ diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index d6d87343..9fcda252 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -495,77 +495,6 @@ Pool.prototype._probeValidateCache = function probeValidateCache(tx) { } }; -Pool.prototype.validateTx = function validateTx(tx, cb, _params) { - // Probe cache first - var result = this._probeValidateCache(tx); - if (result) { - bcoin.utils.nextTick(function() { - cb(null, result); - }); - return; - } - - // Do not perform similar parallel validations - if (!this.validate.reqs.add(tx.hash('hex'), cb)) - return; - - if (!_params) - _params = { depth: 0, range: null }; - - // Propagate range to improve speed of search - var depth = _params.depth; - var range = _params.range; - - var result = { - included: this.chain.hasMerkle(tx.hash()), - valid: false - }; - - console.log('validateTx: ', tx.hash('hex'), depth, result.included, _params.path); - if (depth >= this.validate.minDepth && result.included) { - result.valid = true; - bcoin.utils.nextTick(function() { - cb(null, result); - }); - return; - } - - // Load all inputs and validate them - var self = this; - async.map(tx.inputs, function(input, cb) { - var out = null; - self.getTx(input.out.hash, range, function(t, range) { - out = t; - self.validateTx(out, onSubvalidate, { - depth: depth + 1, - range: range, - path: (_params.path || []).concat(tx.hash('hex')) - }); - }); - - function onSubvalidate(err, subres) { - if (err) - return cb(err); - - cb(null, { - input: input, - tx: out, - valid: subres.valid - }); - } - }, function(err, inputs) { - if (err) { - result.valid = false; - self.validate.reqs.fullfill(tx.hash('hex'), err, result); - return cb(err, result); - } - - self._addValidateCache(tx, result); - self.validate.reqs.fullfill(tx.hash('hex'), null, result); - cb(null, result); - }); -}; - function LoadRequest(pool, type, hash, cb) { this.pool = pool this.type = type; diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index b4cbdbbf..1ab04a72 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -43,3 +43,107 @@ exports.filterFlags = { all: 1, pubkeyOnly: 2 }; + +exports.opcodes = { + 0: 0, + pushdata1: 0x4c, + pushdata2: 0x4d, + pushdata4: 0x4e, + negate1: 0x4f, + + nop: 0x61, + if_: 0x63, + notif: 0x64, + else_: 0x67, + endif: 0x68, + verify: 0x69, + ret: 0x6a, + + toaltstack: 0x6b, + fromaltstack: 0x6c, + ifdup: 0x73, + depth: 0x74, + drop: 0x75, + dup: 0x76, + nip: 0x77, + over: 0x78, + pick: 0x79, + roll: 0x7a, + rot: 0x7b, + swap: 0x7c, + tuck: 0x7d, + drop2: 0x6d, + dup2: 0x6e, + dup3: 0x6f, + over2: 0x70, + rot2: 0x71, + swap2: 0x72, + + cat: 0x74, + substr: 0x7f, + left: 0x80, + right: 0x81, + size: 0x82, + + invert: 0x83, + and: 0x84, + or: 0x85, + xor: 0x86, + eq: 0x87, + eqverify: 0x88, + + add1: 0x8b, + sub1: 0x8c, + mul2: 0x8d, + div2: 0x8e, + negate: 0x8f, + abs: 0x90, + not: 0x91, + noteq0: 0x92, + add: 0x93, + sub: 0x94, + mul: 0x95, + div: 0x96, + mod: 0x97, + lshift: 0x98, + rshift: 0x99, + booland: 0x9a, + boolor: 0x9b, + numeq: 0x9c, + numeqverify: 0x9d, + numneq: 0x9e, + lt: 0x9f, + gt: 0xa0, + lte: 0xa1, + gte: 0xa2, + min: 0xa3, + max: 0xa4, + within: 0xa5, + + ripemd160: 0xa6, + sha1: 0xa7, + sha256: 0xa8, + hash160: 0xa9, + hash256: 0xaa, + codesep: 0xab, + checksig: 0xac, + checksigverify: 0xad, + checkmultisig: 0xae, + checkmultisigverify: 0xaf +}; + +for (var i = 1; i <= 16; i++) + exports.opcodes[i] = 0x50 + i; + +exports.opcodesByVal = new Array(256); +Object.keys(exports.opcodes).forEach(function(name) { + exports.opcodesByVal[exports.opcodes[name]] = name; +}); + +// Little-endian hash type +exports.hashType = { + all: [ 1, 0, 0, 0 ], + none: [ 2, 0, 0, 0 ], + single: [ 3, 0, 0, 0 ], + anyonecaypay: [ 0x80, 0, 0, 0 ], +}; diff --git a/lib/bcoin/protocol/framer.js b/lib/bcoin/protocol/framer.js index c0d79eff..fea82073 100644 --- a/lib/bcoin/protocol/framer.js +++ b/lib/bcoin/protocol/framer.js @@ -211,3 +211,41 @@ Framer.prototype.getBlocks = function getBlocks(hashes, stop) { return this.packet('getblocks', p); }; + +Framer.tx = function tx(tx) { + var p = []; + var off = writeU32(p, tx.version, 0); + off += varint(p, tx.inputs.length, off); + + for (var i = 0; i < tx.inputs.length; i++) { + var input = tx.inputs[i]; + + off += utils.copy(utils.toArray(input.out.hash, 'hex'), p, off, true); + off += writeU32(p, input.out.index, off); + + var s = bcoin.script.encode(input.script); + off += varint(p, s.length, off); + off += utils.copy(s, p, off, true); + + off += writeU32(p, input.seq, off); + } + + off += varint(p, tx.outputs.length, off); + for (var i = 0; i < tx.outputs.length; i++) { + var output = tx.outputs[i]; + + // Put LE value + var value = output.value.toArray().slice().reverse(); + assert(value.length <= 8); + off += utils.copy(value, p, off, true); + for (var j = value.length; j < 8; j++, off++) + p[off] = 0; + + var s = bcoin.script.encode(output.script); + off += varint(p, s.length, off); + off += utils.copy(s, p, off, true); + } + off += writeU32(p, tx.lock, off); + + return p; +}; diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index 2e4a4e63..f91acc47 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -1,5 +1,72 @@ +var bcoin = require('../bcoin'); +var constants = bcoin.protocol.constants; +var utils = bcoin.protocol.utils; var script = exports; -script.parse = function parse(s) { - return s; +script.decode = function decode(s) { + if (!s) + return []; + var opcodes = []; + for (var i = 0; i < s.length;) { + var b = s[i++]; + + // Next `b` bytes should be pushed to stack + if (b >= 0x01 && b <= 0x75) { + opcodes.push(s.slice(i, i + b)); + i += b; + continue; + } + + var opcode = constants.opcodesByVal[b]; + if (opcode === 'pushdata1') { + var len = s[i++]; + opcodes.push(s.slice(i, i + len)); + i += 2 + len; + } else if (opcode === 'pushdata2') { + var len = readU16(s, i); + i += 2; + opcodes.push(s.slice(i, i + len)); + i += len; + } else if (opcode === 'pushdata4') { + var len = readU32(s, i); + i += 4; + opcodes.push(s.slice(i, i + len)); + i += len; + } else { + opcodes.push(opcode || b); + } + } + return opcodes; +}; + +script.encode = function encode(s) { + if (!s) + return []; + var opcodes = constants.opcodes; + var res = []; + for (var i = 0; i < s.length; i++) { + var instr = s[i]; + + // Push value to stack + if (Array.isArray(instr)) { + if (1 <= instr.length && instr.length <= 0x75) { + res = res.concat(instr.length, instr); + } else if (instr.length <= 0xff) { + res = res.concat(opcodes['pushdata1'], instr.length, instr); + } else if (instr.length <= 0xffff) { + res.push(opcodes['pushdata2']); + utils.writeU16(res, instr.length, res.length); + res = res.concat(instr); + } else { + res.push(opcodes['pushdata4']); + utils.writeU32(res, instr.length, res.length); + res = res.concat(instr); + } + continue; + } + + res.push(opcodes[instr] || instr); + } + + return res; }; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 86a79e1e..0bb6efa4 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -1,3 +1,5 @@ +var bn = require('bn.js'); + var bcoin = require('../bcoin'); var utils = bcoin.utils; @@ -6,30 +8,34 @@ function TX(data) { return new TX(data); this.type = 'tx'; - this.version = data.version; - this.inputs = data.inputs.map(function(input) { - return { - out: { - hash: utils.toHex(input.out.hash), - index: bcoin.script.parse(input.out.index) - }, - script: input.script, - seq: input.seq - }; - }); - this.outputs = data.outputs.map(function(output) { - return { - value: output.value, - script: bcoin.script.parse(output.script) - }; - }); - this.lock = data.lock; + if (!data) + data = {}; + + this.version = data.version || 1; + this.inputs = []; + this.outputs = []; + this.lock = data.lock || 0; this._hash = null; this._raw = data._raw || null; + + if (data.inputs) { + data.inputs.forEach(function(input) { + this.input(input); + }, this); + } + if (data.outputs) { + data.outputs.forEach(function(out) { + this.out(out); + }, this); + } } module.exports = TX; +TX.prototype.clone = function clone() { + return new TX(this); +}; + TX.prototype.hash = function hash(enc) { if (!this._hash) { // First, obtain the raw TX data @@ -41,9 +47,60 @@ TX.prototype.hash = function hash(enc) { return enc === 'hex' ? utils.toHex(this._hash) : this._hash; }; -TX.prototype.render = function render(framer) { - return []; +TX.prototype.render = function render() { + return bcoin.protocol.framer.tx(this); }; -TX.prototype.verify = function verify() { +TX.prototype.input = function input(i, index) { + if (i instanceof TX) + i = { tx: i, index: i }; + + var hash; + if (i.tx) + hash = i.tx.hash('hex'); + else if (i.out) + hash = i.out.hash; + else + hash = i.hash; + + this.inputs.push({ + out: { + tx: i.tx, + hash: hash, + index: i.out ? i.out.index : i.index, + }, + script: bcoin.script.decode(i.script), + seq: i.seq === undefined ? 0xffffffff : i.seq + }); + + return this; +}; + +TX.prototype.out = function out(output, value) { + if (typeof output === 'string') { + output = { + address: output, + value: value + }; + } + + var script = bcoin.script.decode(output.script); + + // Default script if given address + if (output.address) { + script = [ + 'dup', + 'hash160', + bcoin.wallet.addr2hash(output.address), + 'eqverify', + 'checksig' + ]; + } + + this.outputs.push({ + value: new bn(output.value), + script: script + }); + + return this; }; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index fa110116..6970759f 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -170,8 +170,10 @@ utils.writeAscii = function writeAscii(dst, str, off) { return i; }; -utils.copy = function copy(src, dst, off) { - var len = Math.min(dst.length - off, src.length); +utils.copy = function copy(src, dst, off, force) { + var len = src.length; + if (!force) + len = Math.min(dst.length - off, len); for (var i = 0; i < len; i++) dst[i + off] = src[i]; return i; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 20f8d7b5..6b05512b 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -1,3 +1,4 @@ +var assert = require('assert'); var bcoin = require('../bcoin'); var utils = bcoin.utils; @@ -9,18 +10,26 @@ function Wallet() { } module.exports = Wallet; -Wallet.prototype.getAddress = function getAddress() { +Wallet.prototype.getHash = function getHash() { var pub = this.key.getPublic('array'); - var keyHash = utils.ripesha(pub); + return utils.ripesha(pub); +}; + +Wallet.prototype.getAddress = function getAddress() { + return Wallet.hash2addr(this.getHash()); +}; + +Wallet.hash2addr = function hash2addr(hash) { + hash = utils.toArray(hash, 'hex'); // Add version - keyHash = [ 0 ].concat(keyHash); + hash = [ 0 ].concat(hash); - var addr = keyHash.concat(utils.checksum(keyHash)); + var addr = hash.concat(utils.checksum(hash)); return utils.toBase58(addr); -} +}; -Wallet.prototype.validateAddress = function validateAddress(addr) { +Wallet.addr2hash = function addr2hash(addr) { if (!Array.isArray(addr)) addr = utils.fromBase58(addr); @@ -28,10 +37,56 @@ Wallet.prototype.validateAddress = function validateAddress(addr) { return false; if (addr[0] !== 0) return false; + var chk = utils.checksum(addr.slice(0, -4)); if (utils.readU32(chk, 0) !== utils.readU32(addr, 21)) return false; - return true; + return addr.slice(1, -4); +}; + +Wallet.prototype.validateAddress = function validateAddress(addr) { + var p = Wallet.addr2hash(addr); + return !!p; }; Wallet.validateAddress = Wallet.prototype.validateAddress; + +Wallet.prototype.own = function own(tx) { + return tx.outputs.some(function(output) { + return output.script.length === 5 && + output.script[0] === 'dup' && + output.script[1] === 'hash160' && + utils.toHex(output.script[2]) === utils.toHex(this.getHash()) && + output.script[3] === 'eqverify' && + output.script[4] === 'checksig'; + }, this); +}; + +Wallet.prototype.sign = function sign(tx, type) { + if (!type) + type = 'all'; + assert.equal(type, 'all'); + + // Filter inputs that this wallet own + var inputs = tx.inputs.filter(function(input) { + return input.out.tx && this.own(input.out.tx); + }, this); + var pub = this.key.getPublic('array'); + + // Add signature script to each input + inputs.forEach(function(input, i) { + var copy = input.tx.clone(); + var s = input.out.tx.getSubscript(); + + copy.inputs.forEach(function(input, j) { + input.script = i === j ? s : []; + }); + + var verifyStr = copy.render(); + verifyStr = verifyStr.concat(bcoin.protocol.constants.hashType[type]); + var hash = utils.dsha256(verifyStr); + var signature = this.key.sign(hash).toDER(); + }, this); + + return inputs.length; +}; diff --git a/test/script-test.js b/test/script-test.js new file mode 100644 index 00000000..ede7ec74 --- /dev/null +++ b/test/script-test.js @@ -0,0 +1,25 @@ +var assert = require('assert'); +var bcoin = require('../'); + +describe('Script', function() { + it('should encode/decode script', function() { + var src = '20' + + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' + + '20' + + '101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f' + + 'ac'; + + var decoded = bcoin.script.decode(bcoin.utils.toArray(src, 'hex')); + assert.equal(decoded.length, 3); + assert.equal( + bcoin.utils.toHex(decoded[0]), + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'); + assert.equal( + bcoin.utils.toHex(decoded[1]), + '101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f'); + assert.equal(decoded[2], 'checksig'); + + var dst = bcoin.script.encode(decoded); + assert.equal(bcoin.utils.toHex(dst), src); + }); +}); diff --git a/test/tx-test.js b/test/tx-test.js new file mode 100644 index 00000000..99c1c024 --- /dev/null +++ b/test/tx-test.js @@ -0,0 +1,20 @@ +var assert = require('assert'); +var bn = require('bn.js'); +var bcoin = require('../'); + +describe('TX', function() { + var parser = bcoin.protocol.parser(); + + it('should decode/encode with parser/framer', function() { + var raw = '010000000125393c67cd4f581456dd0805fa8e9db3abdf90dbe1d4b53e28' + + '6490f35d22b6f2010000006b483045022100f4fa5ced20d2dbd2f905809d' + + '79ebe34e03496ef2a48a04d0a9a1db436a211dd202203243d086398feb4a' + + 'c21b3b79884079036cd5f3707ba153b383eabefa656512dd0121022ebabe' + + 'fede28804b331608d8ef11e1d65b5a920720db8a644f046d156b3a73c0ff' + + 'ffffff0254150000000000001976a9140740345f114e1a1f37ac1cc442b4' + + '32b91628237e88ace7d27b00000000001976a91495ad422bb5911c2c9fe6' + + 'ce4f82a13c85f03d9b2e88ac00000000'; + var tx = bcoin.tx(parser.parseTx(bcoin.utils.toArray(raw, 'hex'))); + assert.equal(bcoin.utils.toHex(tx.render()), raw); + }); +}); diff --git a/test/wallet-test.js b/test/wallet-test.js index cf558c2d..f25c476d 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -16,4 +16,22 @@ describe('Wallet', function() { it('should fail to validate invalid address', function() { assert(!bcoin.wallet.validateAddress('1KQ1wMNwXHUYj1nv2xzsRcKUH8gVFpTFUc')); }); + it('should sign/verify TX', function() { + var w = bcoin.wallet(); + + // Input transcation + var src = bcoin.tx({ + outputs: [{ + value: 5460 * 2, + address: w.getAddress() + }] + }); + assert(w.own(src)); + + var tx = bcoin.tx() + .input(src, 1) + .out(w.getAddress(), 5460); + + w.sign(tx); + }); });