Browse Source

TX_MULTISIG support

patch-2
Matias Alejo Garcia 11 years ago
parent
commit
2af6ab7650
  1. 23
      Transaction.js
  2. 239
      TransactionBuilder.js
  3. 2
      examples/CreateScript.js
  4. 4
      test/data/unspent.json
  5. 37
      test/data/unspentSign.json
  6. 104
      test/test.TransactionBuilder.js

23
Transaction.js

@ -553,15 +553,28 @@ Transaction.prototype.getSize = function getHash() {
}; };
Transaction.prototype.isComplete = function() { Transaction.prototype.isComplete = function() {
var l = this.ins.length;
var ret = true; var ret = true;
var l = this.ins.length;
for (var i = 0; i < l; i++) { for (var i = 0; i < l; i++) {
if (buffertools.compare(this.ins[i].s, util.EMPTY_BUFFER) === 0) { var script = new Script(this.ins[i].s);
ret = false; // Multisig?
break; 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; return ret;
}; };

239
TransactionBuilder.js

@ -89,7 +89,7 @@ var Transaction = imports.Transaction || require('./Transaction');
var FEE_PER_1000B_SAT = parseInt(0.0001 * util.COIN); var FEE_PER_1000B_SAT = parseInt(0.0001 * util.COIN);
function TransactionBuilder(opts) { function TransactionBuilder(opts) {
var opts = opts || {}; opts = opts || {};
this.txobj = {}; this.txobj = {};
this.txobj.version = 1; this.txobj.version = 1;
this.txobj.lock_time = opts.lockTime || 0; this.txobj.lock_time = opts.lockTime || 0;
@ -144,18 +144,29 @@ TransactionBuilder.prototype._setInputMap = function() {
var l = this.selectedUtxos.length; var l = this.selectedUtxos.length;
for (var i = 0; i < l; i++) { 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({ inputMap.push({
address: s.address, address: utxo.address, //TODO que pasa en multisig normal?
scriptPubKey: s.scriptPubKey scriptPubKeyHex: utxo.scriptPubKey,
scriptPubKey: scriptPubKey,
scriptType: scriptType,
i: i,
}); });
} }
this.inputMap = inputMap; this.inputMap = inputMap;
return this; return this;
}; };
TransactionBuilder.prototype.getSelectedUnspent = function(neededAmountSat) { TransactionBuilder.prototype.getSelectedUnspent = function() {
return this.selectedUtxos; return this.selectedUtxos;
}; };
@ -360,14 +371,6 @@ TransactionBuilder._mapKeys = function(keys) {
return walletKeyMap; 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) { TransactionBuilder._signHashAndVerify = function(wk, txSigHash) {
var triesLeft = 10, sigRaw; var triesLeft = 10, sigRaw;
@ -387,44 +390,208 @@ TransactionBuilder.prototype._checkTx = function() {
throw new Error('tx is not defined'); 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) { TransactionBuilder.prototype.sign = function(keys) {
this._checkTx(); this._checkTx();
var tx = this.tx, var tx = this.tx,
ins = tx.ins, 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++) { for (var i = 0; i < l; i++) {
var im = this.inputMap[i]; var input = this.inputMap[i];
if (typeof im === 'undefined') continue;
var wk = walletKeyMap[im.address];
if (!wk) continue;
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 ret = fnToSign[input.scriptType].call(this, walletKeyMap, input, txSigHash);
var sigRaw = TransactionBuilder._signHashAndVerify(wk, txSigHash); if (ret && ret.script) {
var sigType = new Buffer(1); tx.ins[i].s = ret.script; //esto no aqui TODO
sigType[0] = this.signhash; if (ret.isFullySigned) this.inputsSigned++;
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++;
} }
return this; return this;
}; };
// [addr -> script]
TransactionBuilder.prototype.setHashToScriptMap = function(hashToScriptMap) {
this.hashToScriptMap= hashToScriptMap;
};
TransactionBuilder.prototype.isFullySigned = function() { TransactionBuilder.prototype.isFullySigned = function() {
return this.inputsSigned === this.tx.ins.length; return this.inputsSigned === this.tx.ins.length;
}; };

2
examples/CreateScript.js

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

4
test/data/unspent.json

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

37
test/data/unspentSign.json

@ -29,6 +29,43 @@
"cSq7yo4fvsbMyWVN945VUGUWMaSazZPWqBVJZyoGsHmNq6W4HVBV", "cSq7yo4fvsbMyWVN945VUGUWMaSazZPWqBVJZyoGsHmNq6W4HVBV",
"cPa87VgwZfowGZYaEenoQeJgRfKW6PhZ1R65EHTkN1K19cSvc92G", "cPa87VgwZfowGZYaEenoQeJgRfKW6PhZ1R65EHTkN1K19cSvc92G",
"cPQ9DSbBRLva9av5nqeF5AGrh3dsdW8p2E5jS4P8bDWZAoQTeeKB" "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"
] ]
} }

104
test/test.TransactionBuilder.js

@ -220,7 +220,6 @@ describe('TransactionBuilder', function() {
amount: 0.08 amount: 0.08
}]; }];
//console.log('[test.TransactionBuilder.js.216:outs:]',outs, outs.length); //TODO
return new TransactionBuilder(opts) return new TransactionBuilder(opts)
.setUnspent(testdata.dataUnspentSign.unspent) .setUnspent(testdata.dataUnspentSign.unspent)
.setOutputs(outs); .setOutputs(outs);
@ -395,4 +394,107 @@ describe('TransactionBuilder', function() {
util.valueToBigInt(tx.outs[N].v).cmp(970000).should.equal(0); util.valueToBigInt(tx.outs[N].v).cmp(970000).should.equal(0);
tx.isComplete().should.equal(false); 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);
});
}); });

Loading…
Cancel
Save