txp* test passing
This commit is contained in:
parent
966818c53a
commit
753b890658
5 changed files with 417 additions and 193 deletions
|
|
@ -297,9 +297,10 @@ PublicKeyRing.prototype.forPaths = function(paths) {
|
|||
};
|
||||
|
||||
|
||||
// returns pubkey -> copayerId.
|
||||
PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) {
|
||||
|
||||
var inKeyMap = {}, ret = [];
|
||||
var inKeyMap = {}, ret = {};
|
||||
for(var i in pubkeys ){
|
||||
inKeyMap[pubkeys[i]] = 1;
|
||||
};
|
||||
|
|
@ -309,7 +310,7 @@ PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) {
|
|||
for(var copayerIndex in keys[i] ){
|
||||
var kHex = keys[i][copayerIndex].toString('hex');
|
||||
if (inKeyMap[kHex]) {
|
||||
ret.push(this.copayerIds[copayerIndex]);
|
||||
ret[kHex] =this.copayerIds[copayerIndex];
|
||||
delete inKeyMap[kHex];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,16 +11,16 @@ var buffertools = bitcore.buffertools;
|
|||
var preconditions = require('preconditions').instance();
|
||||
|
||||
var VERSION = 1;
|
||||
var CORE_FIELDS = ['builderObj','inputChainPaths', 'version'];
|
||||
var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version'];
|
||||
|
||||
|
||||
function TxProposal(opts) {
|
||||
preconditions.checkArgument(opts);
|
||||
preconditions.checkArgument(opts.inputChainPaths,'no inputChainPaths');
|
||||
preconditions.checkArgument(opts.creator,'no creator');
|
||||
preconditions.checkArgument(opts.createdTs,'no createdTs');
|
||||
preconditions.checkArgument(opts.builder,'no builder');
|
||||
preconditions.checkArgument(opts.inputChainPaths,'no inputChainPaths');
|
||||
preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths');
|
||||
preconditions.checkArgument(opts.creator, 'no creator');
|
||||
preconditions.checkArgument(opts.createdTs, 'no createdTs');
|
||||
preconditions.checkArgument(opts.builder, 'no builder');
|
||||
preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths');
|
||||
|
||||
this.inputChainPaths = opts.inputChainPaths;
|
||||
this.version = opts.version;
|
||||
|
|
@ -38,11 +38,63 @@ function TxProposal(opts) {
|
|||
this.sentTxid = opts.sentTxid || null;
|
||||
this.comment = opts.comment || null;
|
||||
this.readonly = opts.readonly || null;
|
||||
|
||||
this.sync();
|
||||
this._sync();
|
||||
}
|
||||
|
||||
|
||||
TxProposal.prototype._check = function() {
|
||||
|
||||
if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) {
|
||||
throw new Error('Invalid tx proposal');
|
||||
}
|
||||
|
||||
var tx = this.builder.build();
|
||||
if (!tx.ins.length)
|
||||
throw new Error('Invalid tx proposal: no ins');
|
||||
|
||||
for (var i in tx.ins) {
|
||||
var scriptSig = tx.ins[i].s;
|
||||
if (!scriptSig || !scriptSig.length) {
|
||||
throw new Error('Invalid tx proposal: no signatures');
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < tx.ins.length; i++) {
|
||||
var hashType = tx.getHashType(i);
|
||||
if (hashType && hashType !== Transaction.SIGHASH_ALL)
|
||||
throw new Error('Invalid tx proposal: bad signatures');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TxProposal.prototype._updateSignedBy = function() {
|
||||
this._inputSignatures = [];
|
||||
|
||||
var tx = this.builder.build();
|
||||
for (var i in tx.ins) {
|
||||
var scriptSig = new Script(tx.ins[i].s);
|
||||
var signatureCount = scriptSig.countSignatures();
|
||||
var info = TxProposal._infoFromRedeemScript(scriptSig);
|
||||
var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL);
|
||||
var signatureIndexes = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash);
|
||||
if (signatureIndexes.length !== signatureCount)
|
||||
throw new Error('Invalid signature');
|
||||
this._inputSignatures[i] = signatureIndexes.map(function(i) {
|
||||
var r = info.keys[i].toString('hex');
|
||||
return r;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
TxProposal.prototype._sync = function() {
|
||||
this._check();
|
||||
this._updateSignedBy();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
TxProposal.prototype.getId = function() {
|
||||
preconditions.checkState(this.builder);
|
||||
return this.builder.build().getNormalizedHash().toString('hex');
|
||||
};
|
||||
|
||||
|
|
@ -54,23 +106,15 @@ TxProposal.prototype.toObj = function() {
|
|||
};
|
||||
|
||||
|
||||
TxProposal.prototype.toObjForNetwork = function() {
|
||||
var o = this.toObj;
|
||||
|
||||
var newOutput = {};
|
||||
CORE_FIELDS.forEach(function(k){
|
||||
newOutput[k] = o[k];
|
||||
TxProposal.trim = function() {
|
||||
var o = this.toObj();
|
||||
var ret = {};
|
||||
CORE_FIELDS.forEach(function(k) {
|
||||
ret[k] = o[k];
|
||||
});
|
||||
return newOutput;
|
||||
return ret;
|
||||
};
|
||||
|
||||
TxProposal.prototype.sync = function() {
|
||||
this._check();
|
||||
this._updateSignedBy();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// fromObj => from a trusted source
|
||||
TxProposal.fromObj = function(o, forceOpts) {
|
||||
preconditions.checkArgument(o.builderObj);
|
||||
|
|
@ -93,17 +137,6 @@ TxProposal.fromObj = function(o, forceOpts) {
|
|||
return new TxProposal(o);
|
||||
};
|
||||
|
||||
TxProposal.fromObjUntrusted = function(o, forceOpts, senderId) {
|
||||
var newInput = {};
|
||||
CORE_FIELDS.forEach(function(k){
|
||||
newInput[k] = o[k];
|
||||
});
|
||||
if (newInput.version !== VERSION)
|
||||
throw new Error('Peer using different version');
|
||||
|
||||
return TxProposal.fromObj(newInput, forceOpts, senderId);
|
||||
};
|
||||
|
||||
|
||||
|
||||
TxProposal._formatKeys = function(keys) {
|
||||
|
|
@ -158,49 +191,6 @@ TxProposal._infoFromRedeemScript = function(s) {
|
|||
};
|
||||
};
|
||||
|
||||
TxProposal.prototype._updateSignedBy = function() {
|
||||
this._inputSignatures = [];
|
||||
|
||||
var tx = this.builder.build();
|
||||
for (var i in tx.ins) {
|
||||
var scriptSig = new Script(tx.ins[i].s);
|
||||
var signatureCount = scriptSig.countSignatures();
|
||||
var info = TxProposal._infoFromRedeemScript(scriptSig);
|
||||
var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL);
|
||||
var signatureIndexes = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash);
|
||||
if (signatureIndexes.length !== signatureCount)
|
||||
throw new Error('Invalid signature');
|
||||
this._inputSignatures[i] = signatureIndexes.map(function(i) {
|
||||
var r = info.keys[i].toString('hex');
|
||||
return r;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
TxProposal.prototype._check = function() {
|
||||
|
||||
if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) {
|
||||
throw new Error('Invalid tx proposal');
|
||||
}
|
||||
|
||||
var tx = this.builder.build();
|
||||
if (!tx.ins.length)
|
||||
throw new Error('Invalid tx proposal: no ins');
|
||||
|
||||
for(var i in tx.ins){
|
||||
var scriptSig = tx.ins[i].s;
|
||||
if (!scriptSig || !scriptSig.length) {
|
||||
throw new Error('Invalid tx proposal: no signatures');
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < tx.ins.length; i++) {
|
||||
var hashType = tx.getHashType(i);
|
||||
if (hashType && hashType !== Transaction.SIGHASH_ALL)
|
||||
throw new Error('Invalid tx proposal: bad signatures');
|
||||
}
|
||||
};
|
||||
|
||||
TxProposal.prototype.mergeBuilder = function(incoming) {
|
||||
var b0 = this.builder;
|
||||
var b1 = incoming.builder;
|
||||
|
|
@ -213,12 +203,12 @@ TxProposal.prototype.mergeBuilder = function(incoming) {
|
|||
|
||||
|
||||
TxProposal.prototype.setSeen = function(copayerId) {
|
||||
if (!this.seenBy[copayerId])
|
||||
if (!this.seenBy[copayerId])
|
||||
this.seenBy[copayerId] = Date.now();
|
||||
};
|
||||
|
||||
TxProposal.prototype.setRejected = function(copayerId) {
|
||||
if (!this.rejectedBy[copayerId] && !this.signedBy)
|
||||
if (!this.rejectedBy[copayerId] && !this.signedBy)
|
||||
this.rejectedBy[copayerId] = Date.now();
|
||||
};
|
||||
|
||||
|
|
@ -227,55 +217,78 @@ TxProposal.prototype.setSent = function(sentTxid) {
|
|||
this.sentTs = Date.now();
|
||||
};
|
||||
|
||||
/* OTDO
|
||||
events.push({
|
||||
type: 'seen',
|
||||
cId: k,
|
||||
txId: ntxid
|
||||
});
|
||||
events.push({
|
||||
type: 'signed',
|
||||
cId: k,
|
||||
txId: ntxid
|
||||
});
|
||||
events.push({
|
||||
type: 'rejected',
|
||||
cId: k,
|
||||
txId: ntxid
|
||||
});
|
||||
ret.events = this.mergeMetadata(incoming);
|
||||
*/
|
||||
|
||||
|
||||
TxProposal.prototype._allSignatures = function() {
|
||||
var ret = {};
|
||||
for(var i in this._inputSignatures)
|
||||
for (var i in this._inputSignatures)
|
||||
for (var j in this._inputSignatures[i])
|
||||
ret[this._inputSignatures[i][j]] = true;
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) {
|
||||
var newCopayers = {},
|
||||
oldCopayers = {}, newSignedBy = {}, readOnlyPeers = {}, isNew = 1;
|
||||
|
||||
for(var k in this.signedBy) {
|
||||
oldCopayers[k] = 1;
|
||||
isNew = 0;
|
||||
};
|
||||
|
||||
if (isNew == 0 && (!this.creator || !this.createdTs))
|
||||
throw new Error('Existing TX has no creator');
|
||||
|
||||
if (isNew == 0 && (!this.signedBy[this.creator]))
|
||||
throw new Error('Existing TX is not signed by creator');
|
||||
|
||||
var iSig = this._inputSignatures[0];
|
||||
for(var i in iSig){
|
||||
var copayerId = keyMap[iSig[i]];
|
||||
if (!copayerId)
|
||||
throw new Error('Found unknown signature')
|
||||
|
||||
if (oldCopayers[copayerId]) {
|
||||
//Already have it. Do nothing
|
||||
} else {
|
||||
newCopayers[copayerId] = Date.now();
|
||||
delete oldCopayers[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!newCopayers[senderId] && !readOnlyPeers[senderId])
|
||||
throw new Error('TX must have a (new) senders signature')
|
||||
|
||||
if (isNew && Object.keys(newCopayers).length>1)
|
||||
throw new Error('New TX must have only 1 signature');
|
||||
|
||||
// Handler creator / createdTs.
|
||||
// from senderId, and must be signed by senderId
|
||||
if (isNew) {
|
||||
this.creator = Object.keys(newCopayers)[0];
|
||||
this.createdTs = Date.now();
|
||||
}
|
||||
|
||||
//Ended. Update this.
|
||||
for(var i in newCopayers) {
|
||||
this.signedBy[i] = newCopayers[i];
|
||||
}
|
||||
|
||||
// signedBy has preference over rejectedBy
|
||||
for(var i in this.signedBy) {
|
||||
delete this.rejectedBy[i];
|
||||
}
|
||||
|
||||
return Object.keys(newCopayers);
|
||||
};
|
||||
|
||||
// merge will not merge any metadata.
|
||||
TxProposal.prototype.merge = function(incoming) {
|
||||
var ret = {};
|
||||
var newSignatures = [];
|
||||
incoming.sync();
|
||||
|
||||
var prevInputSignatures = this._allSignatures();
|
||||
|
||||
ret.hasChanged = this.mergeBuilder(incoming);
|
||||
this._updateSignedBy();
|
||||
|
||||
if (ret.hasChanged)
|
||||
for(var i in this._inputSignatures)
|
||||
for (var j in this._inputSignatures[i])
|
||||
if (!prevInputSignatures[this._inputSignatures[i][j]])
|
||||
newSignatures.push(this._inputSignatures[i][j]);
|
||||
|
||||
ret.newSignatures = newSignatures;
|
||||
|
||||
return ret;
|
||||
var hasChanged = this.mergeBuilder(incoming);
|
||||
this._sync();
|
||||
return hasChanged;
|
||||
};
|
||||
|
||||
//This should be on bitcore / Transaction
|
||||
|
|
|
|||
|
|
@ -56,21 +56,30 @@ TxProposals.prototype.toObj = function() {
|
|||
};
|
||||
|
||||
|
||||
TxProposals.prototype.merge = function(inTxp, allowedPubKeys) {
|
||||
var myTxps = this.txps;
|
||||
TxProposals.prototype.merge = function(inObj, senderId, copayersForPubkeys, builderOpts) {
|
||||
var safeObj = inObj.trimUntrustedObj();
|
||||
var incomingTx = TxProposal.fromObj(safeObj, builderOpts);
|
||||
incomingTx._sync();
|
||||
|
||||
var myTxps = this.txps;
|
||||
var ntxid = inTxp.getId();
|
||||
var ret = {};
|
||||
var ret = {
|
||||
ntxid: ntxid
|
||||
};
|
||||
|
||||
if (myTxps[ntxid]) {
|
||||
var v0 = myTxps[ntxid];
|
||||
var v1 = inTxp;
|
||||
ret = v0.merge(v1, allowedPubKeys);
|
||||
}
|
||||
else {
|
||||
this.txps[ntxid] = inTxp;
|
||||
|
||||
// Merge an existing txProposal
|
||||
ret.hasChanged = myTxps[ntxid].merge(inTxp, allowedPubKeys);
|
||||
|
||||
|
||||
} else {
|
||||
// Create a new one
|
||||
ret.new = 1;
|
||||
this.txps[ntxid] = inTxp;
|
||||
}
|
||||
|
||||
ret.txp = this.txps[ntxid];
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function Wallet(opts) {
|
|||
}
|
||||
|
||||
|
||||
Wallet.builderOpts = {
|
||||
Wallet.builderOpts = {
|
||||
lockTime: null,
|
||||
signhash: bitcore.Transaction.SIGNHASH_ALL,
|
||||
fee: null,
|
||||
|
|
@ -132,72 +132,119 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) {
|
|||
|
||||
Wallet.prototype._processProposalEvents = function(mergeInfo) {
|
||||
var ev = [];
|
||||
if (mergeInfo.new) {
|
||||
ev = {
|
||||
type: 'new',
|
||||
cid: senderId
|
||||
if (mergeInfo) {
|
||||
if (mergeInfo.new) {
|
||||
ev = {
|
||||
type: 'new',
|
||||
cid: senderId
|
||||
}
|
||||
} else {
|
||||
for (var i in mergeInfo.newCopayers) {
|
||||
var copayerId = mergeInfo.newCopayers[i];
|
||||
ev.push({
|
||||
type: 'signed',
|
||||
cid: copayerId
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i in mergeInfo.newCopayers) {
|
||||
var copayerId = mergeInfo.newCopayers[i];
|
||||
ev.push({
|
||||
type: 'signed',
|
||||
cid: copayerId
|
||||
});
|
||||
}
|
||||
ev = {
|
||||
type: 'corrupt',
|
||||
cId: senderId,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
if (ev)
|
||||
this.emit('txProposalEvent', ev);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/* OTDO
|
||||
events.push({
|
||||
type: 'signed',
|
||||
cId: k,
|
||||
txId: ntxid
|
||||
});
|
||||
*/
|
||||
Wallet.prototype._getKeyMap = function(tpx, senderId) {
|
||||
|
||||
this.publicKeyRing.copayersForPubkeys(txp._inputSignatures[0], txp.paths);
|
||||
|
||||
var keyMapStr = JSON.stringify(keyMap);
|
||||
// All inputs must be signed with the same copayers
|
||||
for (var i in m.txp._inputSignatures) {
|
||||
if (!i) continue;
|
||||
var inputKeyMapStr = JSON.stringify(
|
||||
this.publicKeyRing.copayersForPubkeys(txp._inputSignatures[i], txp.paths));
|
||||
|
||||
if (inputKeyMapStr !== keyMapStr)
|
||||
throw new Error('found inputs with different signatures in Tx from:' + senderId);
|
||||
}
|
||||
if (ev)
|
||||
this.emit('txProposalEvent',ev);
|
||||
};
|
||||
|
||||
|
||||
Wallet.prototype._handleTxProposal = function(senderId, data) {
|
||||
this.log('RECV TXPROPOSAL: ', data);
|
||||
var mergeInfo, ntxid;
|
||||
var m;
|
||||
|
||||
try {
|
||||
mergeInfo = this.txProposals.mergeFromObj(data.txProposal, senderId, Wallet.builderOpts);
|
||||
mergeInfo.newCopayers=[];
|
||||
for (var i in mergeInfo.newSignatures) {
|
||||
var k = mergeInfo.newSignatures[i];
|
||||
mergeInfo.newCopayers.push(this.getCopayerIdFromPubKey(k));
|
||||
};
|
||||
ntxid = mergeInfo.inTxp.getId();
|
||||
m = this.txProposals.mergeObj(senderId, data.txProposal, Wallet.builderOpts);
|
||||
|
||||
var keyMap = this._getKeyMap(m.tpx,senderId);
|
||||
ret.newCopayers = m.txp.setCopayers(senderId, keyMap);
|
||||
|
||||
} catch (e) {
|
||||
var corruptEvent = {
|
||||
type: 'corrupt',
|
||||
cId: senderId,
|
||||
error: e,
|
||||
};
|
||||
this.emit('txProposalEvent', corruptEvent);
|
||||
return;
|
||||
this.log('Corrupt TX proposal received', senderId, e); //TODO
|
||||
}
|
||||
this.sendSeen(ntxid);
|
||||
|
||||
if (mergeInfo.hasChanged)
|
||||
this.sendTxProposal(ntxid);
|
||||
if (m) {
|
||||
this.emit('txProposalsUpdated');
|
||||
this.store();
|
||||
|
||||
this.emit('txProposalsUpdated');
|
||||
this.store();
|
||||
this._processProposalEvents(senderId, mergeInfo);
|
||||
this.sendSeen(m.ntxid);
|
||||
|
||||
if (m.hasChanged)
|
||||
this.sendTxProposal(m.ntxid);
|
||||
}
|
||||
|
||||
this._processProposalEvents(senderId, m);
|
||||
};
|
||||
|
||||
|
||||
Wallet.prototype._handleReject = function(senderId, data, isInbound) {
|
||||
this.log('RECV REJECT:', data);
|
||||
// TODO check that has not signed.
|
||||
//
|
||||
this.txProposals.txps[data.ntxid].setRejected(senderId);
|
||||
this.emit('txProposalsUpdated');
|
||||
|
||||
var txp = this.txProposals.txps[data.ntxid];
|
||||
|
||||
if (!txp)
|
||||
throw new Error('Received Reject for an unkwown TX from:' + senderId);
|
||||
|
||||
if (txp.signedBy[senderId])
|
||||
throw new Error('Received Reject for an already signed TX from:' + senderId);
|
||||
|
||||
txp.setRejected(senderId);
|
||||
this.store();
|
||||
|
||||
this.emit('txProposalsUpdated');
|
||||
this.emit('txProposalEvent', {
|
||||
type: 'rejected',
|
||||
cId: senderId,
|
||||
txId: data.ntxid,
|
||||
});
|
||||
};
|
||||
|
||||
Wallet.prototype._handleSeen = function(senderId, data, isInbound) {
|
||||
this.log('RECV SEEN:', data);
|
||||
this.txProposals.txps[data.ntxid].setSeen(senderId);
|
||||
this.emit('txProposalsUpdated');
|
||||
this.store();
|
||||
this.emit('txProposalsUpdated');
|
||||
this.emit('txProposalEvent', {
|
||||
type: 'seen',
|
||||
cId: senderId,
|
||||
txId: data.ntxid,
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -245,8 +292,10 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) {
|
|||
break;
|
||||
case 'reject':
|
||||
this._handleReject(senderId, data, isInbound);
|
||||
break;
|
||||
case 'seen':
|
||||
this._handleReject(senderId, data, isInbound);
|
||||
this._handleSeen(senderId, data, isInbound);
|
||||
break;
|
||||
case 'txProposal':
|
||||
this._handleTxProposal(senderId, data, isInbound);
|
||||
break;
|
||||
|
|
@ -796,7 +845,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
|
|||
};
|
||||
}
|
||||
|
||||
for (var k in Wallet.builderOpts){
|
||||
for (var k in Wallet.builderOpts) {
|
||||
opts[k] = Wallet.builderOpts[k];
|
||||
}
|
||||
|
||||
|
|
@ -821,8 +870,8 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
|
|||
|
||||
|
||||
var tx = b.build();
|
||||
if (!tx.countInputSignatures(0))
|
||||
throw new Error ('Could not sign generated tx');
|
||||
if (!tx.countInputSignatures(0))
|
||||
throw new Error('Could not sign generated tx');
|
||||
|
||||
var me = {};
|
||||
me[myId] = now;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue