TX_MULTISIG support

This commit is contained in:
Matias Alejo Garcia 2014-03-31 14:41:27 -03:00
parent 5dd945f2b7
commit 2af6ab7650
6 changed files with 364 additions and 45 deletions

View File

@ -553,15 +553,28 @@ Transaction.prototype.getSize = function getHash() {
};
Transaction.prototype.isComplete = function() {
var l = this.ins.length;
var ret = true;
var l = this.ins.length;
for (var i = 0; i < l; i++) {
if (buffertools.compare(this.ins[i].s, util.EMPTY_BUFFER) === 0) {
ret = false;
break;
var script = new Script(this.ins[i].s);
// Multisig?
if (!Buffer.isBuffer(script.chunks[0]) && script.chunks[0] ===0) {
for (var i = 1; i < script.chunks.length; i++) {
if (buffertools.compare(script.chunks[i], util.EMPTY_BUFFER) === 0){
ret = false;
break;
}
}
}
else {
if (buffertools.compare(this.ins[i].s, util.EMPTY_BUFFER) === 0) {
ret = false;
break;
}
}
};
return ret;
};

View File

@ -89,7 +89,7 @@ var Transaction = imports.Transaction || require('./Transaction');
var FEE_PER_1000B_SAT = parseInt(0.0001 * util.COIN);
function TransactionBuilder(opts) {
var opts = opts || {};
opts = opts || {};
this.txobj = {};
this.txobj.version = 1;
this.txobj.lock_time = opts.lockTime || 0;
@ -144,18 +144,29 @@ TransactionBuilder.prototype._setInputMap = function() {
var l = this.selectedUtxos.length;
for (var i = 0; i < l; i++) {
var s = this.selectedUtxos[i];
var utxo = this.selectedUtxos[i];
var scriptBuf = new Buffer(utxo.scriptPubKey, 'hex');
var scriptPubKey = new Script(scriptBuf);
var scriptType = scriptPubKey.classify();
if (scriptType === Script.TX_UNKNOWN)
throw new Error('unkown output type at:' + i +
' Type:' + scriptPubKey.getRawOutType());
inputMap.push({
address: s.address,
scriptPubKey: s.scriptPubKey
address: utxo.address, //TODO que pasa en multisig normal?
scriptPubKeyHex: utxo.scriptPubKey,
scriptPubKey: scriptPubKey,
scriptType: scriptType,
i: i,
});
}
this.inputMap = inputMap;
return this;
};
TransactionBuilder.prototype.getSelectedUnspent = function(neededAmountSat) {
TransactionBuilder.prototype.getSelectedUnspent = function() {
return this.selectedUtxos;
};
@ -360,14 +371,6 @@ TransactionBuilder._mapKeys = function(keys) {
return walletKeyMap;
};
TransactionBuilder._checkSupportedScriptType = function (s) {
if (s.classify() !== Script.TX_PUBKEYHASH) {
throw new Error('scriptSig type:' + s.getRawOutType() +
' not supported yet');
}
};
TransactionBuilder._signHashAndVerify = function(wk, txSigHash) {
var triesLeft = 10, sigRaw;
@ -387,44 +390,208 @@ TransactionBuilder.prototype._checkTx = function() {
throw new Error('tx is not defined');
};
TransactionBuilder.prototype._signPubKey = function(walletKeyMap, input, txSigHash) {
if (this.tx.ins[input.i].s.length > 0) return {};
var wk = walletKeyMap[input.address];
if (!wk) return;
var sigRaw = TransactionBuilder._signHashAndVerify(wk, txSigHash);
var sigType = new Buffer(1);
sigType[0] = this.signhash;
var sig = Buffer.concat([sigRaw, sigType]);
var scriptSig = new Script();
scriptSig.chunks.push(sig);
scriptSig.updateBuffer();
return {isFullySigned: true, script: scriptSig.getBuffer()};
};
TransactionBuilder.prototype._signPubKeyHash = function(walletKeyMap, input, txSigHash) {
if (this.tx.ins[input.i].s.length > 0) return {};
var wk = walletKeyMap[input.address];
if (!wk) return;
var sigRaw = TransactionBuilder._signHashAndVerify(wk, txSigHash);
var sigType = new Buffer(1);
sigType[0] = this.signhash;
var sig = Buffer.concat([sigRaw, sigType]);
var scriptSig = new Script();
scriptSig.chunks.push(sig);
scriptSig.chunks.push(wk.privKey.public);
scriptSig.updateBuffer();
return {isFullySigned: true, script: scriptSig.getBuffer()};
};
// FOR TESTING
/*
var _dumpChunks = function (scriptSig, label) {
console.log('## DUMP: ' + label + ' ##');
for(var i=0; i<scriptSig.chunks.length; i++) {
console.log('\tCHUNK ', i, scriptSig.chunks[i]);
}
};
*/
TransactionBuilder.prototype._initMultiSig = function(scriptSig, nreq) {
var wasUpdated = false;
if (scriptSig.chunks.length < nreq + 1) {
wasUpdated = true;
scriptSig.writeN(0);
while (scriptSig.chunks.length <= nreq)
scriptSig.chunks.push(util.EMPTY_BUFFER);
}
return wasUpdated;
};
TransactionBuilder.prototype._isSignedWithKey = function(wk, scriptSig, txSigHash, nreq) {
var ret=0;
for(var i=1; i<=nreq; i++) {
var chunk = scriptSig.chunks[i];
if (chunk ===0 || chunk.length === 0) continue;
var sigRaw = new Buffer(chunk.slice(0,chunk.length-1));
if(wk.privKey.verifySignatureSync(txSigHash, sigRaw) === true) {
ret=true;
}
}
return ret;
};
TransactionBuilder.prototype._chunkIsEmpty = function(chunk) {
return chunk === 0 || // when serializing and back, EMPTY_BUFFER becomes 0
buffertools.compare(chunk, util.EMPTY_BUFFER) === 0;
};
TransactionBuilder.prototype._updateMultiSig = function(wk, scriptSig, txSigHash, nreq) {
var wasUpdated = this._initMultiSig(scriptSig, nreq);
if (this._isSignedWithKey(wk,scriptSig, txSigHash, nreq))
return null;
// Find an empty slot and sign
for(var i=1; i<=nreq; i++) {
var chunk = scriptSig.chunks[i];
if (!this._chunkIsEmpty(chunk))
continue;
// Add signature
var sigRaw = TransactionBuilder._signHashAndVerify(wk, txSigHash);
var sigType = new Buffer(1);
sigType[0] = this.signhash;
var sig = Buffer.concat([sigRaw, sigType]);
scriptSig.chunks[i] = sig;
scriptSig.updateBuffer();
wasUpdated=true;
break;
}
return wasUpdated ? scriptSig : null;
};
TransactionBuilder.prototype._multiFindKey = function(walletKeyMap,pubKeyHash) {
var wk;
[ networks.livenet, networks.testnet].forEach(function(n) {
[ n.addressPubkey, n.addressScript].forEach(function(v) {
var a = new Address(v,pubKeyHash);
if (!wk && walletKeyMap[a]) {
wk = walletKeyMap[a];
}
});
});
return wk;
};
TransactionBuilder.prototype._countMultiSig = function(script) {
var nsigs = 0;
for (var i = 1; i < script.chunks.length; i++)
if (!this._chunkIsEmpty(script.chunks[i]))
nsigs++;
return nsigs;
};
TransactionBuilder.prototype.countInputMultiSig = function(i) {
var s = new Script(this.tx.ins[i].s);
if (!s.chunks.length || s.chunks[0] !== 0)
return 0; // does not seems multisig
return this._countMultiSig(s);
};
TransactionBuilder.prototype._signMultiSig = function(walletKeyMap, input, txSigHash) {
var pubkeys = input.scriptPubKey.capture(),
nreq = input.scriptPubKey.chunks[0] - 80, //see OP_2-OP_16
l = pubkeys.length,
originalScriptBuf = this.tx.ins[input.i].s;
var scriptSig = new Script (originalScriptBuf);
for(var j=0; j<l && this._countMultiSig(scriptSig)<nreq; j++) {
var pubKeyHash = util.sha256ripe160(pubkeys[j]);
var wk = this._multiFindKey(walletKeyMap, pubKeyHash);
if (!wk) continue;
var newScriptSig = this._updateMultiSig(wk, scriptSig, txSigHash, nreq);
if (newScriptSig)
scriptSig = newScriptSig;
}
return {
isFullySigned: this._countMultiSig(scriptSig) === nreq,
script: scriptSig.getBuffer(),
};
};
var fnToSign = {};
fnToSign[Script.TX_PUBKEYHASH] = TransactionBuilder.prototype._signPubKeyHash;
fnToSign[Script.TX_PUBKEY] = TransactionBuilder.prototype._signPubKey;
fnToSign[Script.TX_MULTISIG] = TransactionBuilder.prototype._signMultiSig;
fnToSign[Script.TX_SCRIPTHASH] = TransactionBuilder.prototype._signScriptHash;
//if (!this.hashToScriptMap) throw new Error('hashToScriptMap not set');
TransactionBuilder.prototype.sign = function(keys) {
this._checkTx();
var tx = this.tx,
ins = tx.ins,
l = ins.length;
l = ins.length,
walletKeyMap = TransactionBuilder._mapKeys(keys);
var walletKeyMap = TransactionBuilder._mapKeys(keys);
for (var i = 0; i < l; i++) {
var im = this.inputMap[i];
if (typeof im === 'undefined') continue;
var wk = walletKeyMap[im.address];
if (!wk) continue;
var input = this.inputMap[i];
var scriptBuf = new Buffer(im.scriptPubKey, 'hex');
var txSigHash = this.tx.hashForSignature(
input.scriptPubKey, i, this.signhash);
//TODO: support p2sh
var s = new Script(scriptBuf);
TransactionBuilder._checkSupportedScriptType(s);
var txSigHash = this.tx.hashForSignature(s, i, this.signhash);
var sigRaw = TransactionBuilder._signHashAndVerify(wk, txSigHash);
var sigType = new Buffer(1);
sigType[0] = this.signhash;
var sig = Buffer.concat([sigRaw, sigType]);
var scriptSig = new Script();
scriptSig.chunks.push(sig);
scriptSig.chunks.push(wk.privKey.public);
scriptSig.updateBuffer();
tx.ins[i].s = scriptSig.getBuffer();
this.inputsSigned++;
var ret = fnToSign[input.scriptType].call(this, walletKeyMap, input, txSigHash);
if (ret && ret.script) {
tx.ins[i].s = ret.script; //esto no aqui TODO
if (ret.isFullySigned) this.inputsSigned++;
}
}
return this;
};
// [addr -> script]
TransactionBuilder.prototype.setHashToScriptMap = function(hashToScriptMap) {
this.hashToScriptMap= hashToScriptMap;
};
TransactionBuilder.prototype.isFullySigned = function() {
return this.inputsSigned === this.tx.ins.length;
};

View File

@ -9,7 +9,7 @@ var run = function() {
var buffertools = bitcore.buffertools;
var Address = bitcore.Address;
var util = bitcore.util;
var opts = {network: networks.livenet};
var opts = {network: networks.testnet};
var p = console.log;

View File

@ -10,7 +10,7 @@
{
"address": "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ",
"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2",
"scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ad",
"scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac",
"vout": 0,
"confirmations": 1,
"amount": 0.1
@ -18,7 +18,7 @@
{
"address": "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ",
"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc3",
"scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ae",
"scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac",
"vout": 3,
"confirmations": 0,
"amount": 1

View File

@ -29,6 +29,43 @@
"cSq7yo4fvsbMyWVN945VUGUWMaSazZPWqBVJZyoGsHmNq6W4HVBV",
"cPa87VgwZfowGZYaEenoQeJgRfKW6PhZ1R65EHTkN1K19cSvc92G",
"cPQ9DSbBRLva9av5nqeF5AGrh3dsdW8p2E5jS4P8bDWZAoQTeeKB"
],
"unspentPubKey": [
{
"address": "mqqnn93xN81eZTLqj7Wk2cacBBTR8agFZ5",
"scriptPubKey": "2102aa869ff719f23d9959dca340cbf3b72770294c64005e53e0429948aa6e9701d1ac",
"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1",
"vout": 1,
"amount": 1,
"confirmations":7
}
],
"keyStringsPubKey": [
"cTSvhK2b3XxJezmDjVN5x1KTCtui4NaLhvb78nvprpVAiqHgQvMm"
],
"unspentMulti": [
{
"address": [
"n4JAZc4cJimQbky5wxZUEDeAFZtGaZrjWK",
"msge5muNmBSRDn5nsaRcHCU6dg2zimA8wQ",
"mvz9MjocpyXdgXqRcZYazsdE8iThdvjdhk",
"miQGZ2gybQe7UvUQDBYsgcctUteij5pTpm",
"mu9kmhGrzREKsWaXUEUrsRLLMG4UMPy1LF"
],
"scriptPubKey": "532103bf025eb410407aec5a67c975ce222e363bb88c69bb1acce45d20d85602df2ec52103d76dd6d99127f4b733e772f0c0a09c573ac7e4d69b8bf50272292da2e093de2c2103dd9acd8dd1816c825d6b0739339c171ae2cb10efb53699680537865b07086e9b2102371cabbaf466c3a536034b4bda64ad515807bffd87488f44f93c2373d4d189c9210264cd444358f8d57f8637a7309f9736806f4883aebc4fe7da4bad1e4b37f2d12c55ae",
"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1",
"vout": 1,
"amount": 1,
"confirmations":7
}
],
"keyStringsMulti": [
"cP6JBHuQf7yqeqtdKRd22ibF3VehDv7G6BdzxSNABgrv3jFJUGoN",
"cQfRwF7XLSM5xGUpF8PZvob2MZyULvZPA2j5cat2RKDJrja7FtCZ",
"cUkYub4jtFVYymHh38yMMW36nJB4pXG5Pzd5QjResq79kAndkJcg",
"cMyBgowsyrJRufoKWob73rMQB1PBqDdwFt8z4TJ6APN2HkmX1Ttm",
"cN9yZCom6hAZpHtCp8ovE1zFa7RqDf3Cr4W6AwH2tp59Jjh9JcXu"
]
}

View File

@ -220,7 +220,6 @@ describe('TransactionBuilder', function() {
amount: 0.08
}];
//console.log('[test.TransactionBuilder.js.216:outs:]',outs, outs.length); //TODO
return new TransactionBuilder(opts)
.setUnspent(testdata.dataUnspentSign.unspent)
.setOutputs(outs);
@ -395,4 +394,107 @@ describe('TransactionBuilder', function() {
util.valueToBigInt(tx.outs[N].v).cmp(970000).should.equal(0);
tx.isComplete().should.equal(false);
});
it('should sign a p2pubkey tx', function() {
var opts = {
remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd',
};
var outs = outs || [{
address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE',
amount: 0.08
}];
var b = new TransactionBuilder(opts)
.setUnspent(testdata.dataUnspentSign.unspentPubKey)
.setOutputs(outs)
.sign(testdata.dataUnspentSign.keyStringsPubKey);
b.isFullySigned().should.equal(true);
var tx = b.build();
tx.isComplete().should.equal(true);
tx.ins.length.should.equal(1);
tx.outs.length.should.equal(2);
});
it('should sign a multisig tx', function() {
var opts = {
remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd',
};
var outs = outs || [{
address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE',
amount: 0.08
}];
var b = new TransactionBuilder(opts)
.setUnspent(testdata.dataUnspentSign.unspentMulti)
.setOutputs(outs);
b.sign(testdata.dataUnspentSign.keyStringsMulti);
b.isFullySigned().should.equal(true);
var tx = b.build();
tx.ins.length.should.equal(1);
tx.outs.length.should.equal(2);
tx.isComplete().should.equal(true);
});
it('should sign a multisig tx in steps (3-5)', function() {
var opts = {
remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd',
};
var outs = outs || [{
address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE',
amount: 0.08
}];
var b = new TransactionBuilder(opts)
.setUnspent(testdata.dataUnspentSign.unspentMulti)
.setOutputs(outs);
var k1 = testdata.dataUnspentSign.keyStringsMulti.slice(0,1);
var k2 = testdata.dataUnspentSign.keyStringsMulti.slice(1,2);
var k3 = testdata.dataUnspentSign.keyStringsMulti.slice(2,3);
b.sign(k1);
b.isFullySigned().should.equal(false);
b.sign(k2);
b.isFullySigned().should.equal(false);
b.sign(k3);
b.isFullySigned().should.equal(true);
var tx = b.build();
tx.ins.length.should.equal(1);
tx.outs.length.should.equal(2);
tx.isComplete().should.equal(true);
});
it('should count multisig signs (3-5)', function() {
var opts = {
remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd',
};
var outs = outs || [{
address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE',
amount: 0.08
}];
var b = new TransactionBuilder(opts)
.setUnspent(testdata.dataUnspentSign.unspentMulti)
.setOutputs(outs);
var k1 = testdata.dataUnspentSign.keyStringsMulti.slice(0,1);
var k2 = testdata.dataUnspentSign.keyStringsMulti.slice(1,2);
var k3 = testdata.dataUnspentSign.keyStringsMulti.slice(2,3);
b.countInputMultiSig(0).should.equal(0);
b.sign(k1);
b.isFullySigned().should.equal(false);
b.countInputMultiSig(0).should.equal(1);
b.sign(k2);
b.isFullySigned().should.equal(false);
b.countInputMultiSig(0).should.equal(2);
b.sign(k3);
b.isFullySigned().should.equal(true);
b.countInputMultiSig(0).should.equal(3);
});
});