This commit is contained in:
Fedor Indutny 2014-05-04 23:24:32 +04:00
parent 6fd7173c89
commit 458e606359
11 changed files with 419 additions and 103 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
npm-debug.log
key.key
node_modules/

View File

@ -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;

View File

@ -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 ],
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;

View File

@ -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;
};

25
test/script-test.js Normal file
View File

@ -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);
});
});

20
test/tx-test.js Normal file
View File

@ -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);
});
});

View File

@ -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);
});
});