Skip to content

Commit 63b446b

Browse files
author
Arik Sosman
committed
Add segwit support to SDK
Reviewers: alex, john, mark Reviewed By: alex, mark Subscribers: ben Differential Revision: https://phabricator.bitgo.com/D6233
1 parent 6d5bd40 commit 63b446b

File tree

6 files changed

+132
-72
lines changed

6 files changed

+132
-72
lines changed

.arclint

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
},
1515
"spelling": {
1616
"type": "spelling"
17-
},
18-
"eslint": {
19-
"type": "eslint",
20-
"include": "(\\.js$)"
2117
}
2218
}
2319
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bitgo",
3-
"version": "3.7.0",
3+
"version": "3.8.0",
44
"description": "BitGo Javascript SDK",
55
"main": "./src/index.js",
66
"keywords": [

src/transactionBuilder.js

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
var Q = require('q');
99
var bitcoin = require('./bitcoin');
10+
var bitcoinCash = require('./bitcoinCash');
1011
var common = require('./common');
1112
var Util = require('./util');
1213
var _ = require('lodash');
1314

1415
const P2SH_INPUT_SIZE = 295;
16+
const P2SH_P2WSH_INPUT_SIZE = 139;
1517
const P2PKH_INPUT_SIZE = 160;
1618
const OUTPUT_SIZE = 34;
1719
const TX_OVERHEAD_SIZE = 10;
@@ -363,10 +365,14 @@ exports.createTransaction = function(params) {
363365
unspents = _.filter(unspents, function(unspent) {
364366
return unspent.value > minInputValue;
365367
});
368+
let segwitInputCount = 0;
366369
unspents.every(function(unspent) {
370+
if (unspent.witnessScript) {
371+
segwitInputCount++;
372+
}
367373
inputAmount += unspent.value;
368-
var script = new Buffer(unspent.script, 'hex');
369-
transaction.addInput(unspent.tx_hash, unspent.tx_output_n, 0xffffffff, script);
374+
transaction.addInput(unspent.tx_hash, unspent.tx_output_n, 0xffffffff);
375+
370376
return (inputAmount < (feeSingleKeySourceAddress ? totalOutputAmount : totalAmount));
371377
});
372378

@@ -386,7 +392,8 @@ exports.createTransaction = function(params) {
386392
}
387393

388394
txInfo = {
389-
nP2SHInputs: transaction.tx.ins.length - (feeSingleKeySourceAddress ? 1 : 0),
395+
nP2SHInputs: transaction.tx.ins.length - (feeSingleKeySourceAddress ? 1 : 0) - segwitInputCount,
396+
nP2SHP2WSHInputs: segwitInputCount,
390397
nP2PKHInputs: feeSingleKeySourceAddress ? 1 : 0,
391398
nOutputs: (
392399
recipients.length + 1 + // recipients and change
@@ -398,6 +405,7 @@ exports.createTransaction = function(params) {
398405

399406
estTxSize = estimateTransactionSize({
400407
nP2SHInputs: txInfo.nP2SHInputs,
408+
nP2SHP2WSHInputs: txInfo.nP2SHP2WSHInputs,
401409
nP2PKHInputs: txInfo.nP2PKHInputs,
402410
nOutputs: txInfo.nOutputs
403411
});
@@ -407,6 +415,7 @@ exports.createTransaction = function(params) {
407415
bitgo: params.wallet.bitgo,
408416
feeRate: feeRate,
409417
nP2SHInputs: txInfo.nP2SHInputs,
418+
nP2SHP2WSHInputs: txInfo.nP2SHP2WSHInputs,
410419
nP2PKHInputs: txInfo.nP2PKHInputs,
411420
nOutputs: txInfo.nOutputs
412421
});
@@ -559,7 +568,10 @@ exports.createTransaction = function(params) {
559568
return params.changeAddress;
560569
} else {
561570
// Otherwise create a new address per output, for privacy
562-
return params.wallet.createAddress({chain: 1, validate: validate})
571+
// determine if segwit or not
572+
const isSegwit = bitgo.getConstants().enableSegwit;
573+
const changeChain = isSegwit ? 11 : 1;
574+
return params.wallet.createAddress({ chain: changeChain, validate: validate })
563575
.then(function(result) {
564576
return result.address;
565577
});
@@ -614,7 +626,7 @@ exports.createTransaction = function(params) {
614626
var serialize = function() {
615627
// only need to return the unspents that were used and just the chainPath, redeemScript, and instant flag
616628
var pickedUnspents = _.map(unspents, function(unspent) {
617-
return _.pick(unspent, ['chainPath', 'redeemScript', 'instant']);
629+
return _.pick(unspent, ['chainPath', 'redeemScript', 'instant', 'witnessScript', 'value']);
618630
});
619631
var prunedUnspents = _.slice(pickedUnspents, 0, transaction.tx.ins.length - feeSingleKeyUnspentsUsed.length);
620632
_.each(feeSingleKeyUnspentsUsed, function(feeUnspent) {
@@ -669,20 +681,30 @@ exports.createTransaction = function(params) {
669681
* @returns size: estimated size of the transaction in bytes
670682
*/
671683
var estimateTransactionSize = function(params) {
672-
if (typeof(params.nP2SHInputs) !== 'number' || params.nP2SHInputs < 1) {
684+
if (!_.isInteger(params.nP2SHInputs) || params.nP2SHInputs < 0) {
673685
throw new Error('expecting positive nP2SHInputs');
674686
}
675-
if ((params.nP2PKHInputs) && (typeof(params.nP2PKHInputs) !== 'number')) {
687+
if (!_.isInteger(params.nP2PKHInputs) || params.nP2PKHInputs < 0) {
676688
throw new Error('expecting positive nP2PKHInputs to be numeric');
677689
}
678-
if (typeof(params.nOutputs) !== 'number' || params.nOutputs < 1) {
690+
if (!_.isInteger(params.nP2SHP2WSHInputs) || params.nP2SHP2WSHInputs < 0) {
691+
throw new Error('expecting positive nP2SHP2WSHInputs to be numeric');
692+
}
693+
if ((params.nP2SHInputs + params.nP2SHP2WSHInputs) < 1) {
694+
throw new Error('expecting at least one nP2SHInputs or nP2SHP2WSHInputs');
695+
}
696+
if (!_.isInteger(params.nOutputs) || params.nOutputs < 1) {
679697
throw new Error('expecting positive nOutputs');
680698
}
681699

700+
701+
682702
var estimatedSize = P2SH_INPUT_SIZE * params.nP2SHInputs +
703+
P2SH_P2WSH_INPUT_SIZE * (params.nP2SHP2WSHInputs || 0) +
683704
P2PKH_INPUT_SIZE * (params.nP2PKHInputs || 0) +
684705
OUTPUT_SIZE * params.nOutputs +
685-
TX_OVERHEAD_SIZE;
706+
// if the tx contains at least one segwit input, the tx overhead is increased by 1
707+
TX_OVERHEAD_SIZE + (params.nP2SHP2WSHInputs > 0 ? 1 : 0);
686708

687709
return estimatedSize;
688710
};
@@ -736,16 +758,16 @@ exports.signTransaction = function(params) {
736758

737759
var validate = (params.validate === undefined) ? true : params.validate;
738760
var privKey;
739-
if (typeof(params.transactionHex) != 'string') {
761+
if (typeof(params.transactionHex) !== 'string') {
740762
throw new Error('expecting the transaction hex as a string');
741763
}
742764
if (!Array.isArray(params.unspents)) {
743765
throw new Error('expecting the unspents array');
744766
}
745-
if (typeof(validate) != 'boolean') {
767+
if (typeof(validate) !== 'boolean') {
746768
throw new Error('expecting validate to be a boolean');
747769
}
748-
if (typeof(keychain) != 'object' || typeof(keychain.xprv) != 'string') {
770+
if (typeof(keychain) !== 'object' || typeof(keychain.xprv) !== 'string') {
749771
if (typeof(params.signingKey) === 'string') {
750772
privKey = bitcoin.ECPair.fromWIF(params.signingKey, bitcoin.getNetwork());
751773
keychain = undefined;
@@ -759,7 +781,7 @@ exports.signTransaction = function(params) {
759781
feeSingleKey = bitcoin.ECPair.fromWIF(params.feeSingleKeyWIF, bitcoin.getNetwork());
760782
}
761783

762-
var transaction = bitcoin.Transaction.fromHex(params.transactionHex);
784+
var transaction = bitcoinCash.Transaction.fromHex(params.transactionHex);
763785
if (transaction.ins.length !== params.unspents.length) {
764786
throw new Error('length of unspents array should equal to the number of transaction inputs');
765787
}
@@ -769,47 +791,57 @@ exports.signTransaction = function(params) {
769791
var rootExtKey = bitcoin.HDNode.fromBase58(keychain.xprv);
770792
hdPath = bitcoin.hdPath(rootExtKey);
771793
}
772-
var txb;
773794

774-
for (var index = 0; index < transaction.ins.length; ++index) {
775-
if (params.unspents[index].redeemScript === false) {
795+
var txb = bitcoinCash.TransactionBuilder.fromTransaction(transaction, rootExtKey.keyPair.network);
796+
797+
for (let index = 0; index < txb.tx.ins.length; ++index) {
798+
const currentUnspent = params.unspents[index];
799+
if (currentUnspent.redeemScript === false) {
776800
// this is the input from a single key fee address
777801
if (!feeSingleKey) {
778802
throw new Error('single key address used in input but feeSingleKeyWIF not provided');
779803
}
780804

781-
txb = bitcoin.TransactionBuilder.fromTransaction(transaction);
782805
txb.sign(index, feeSingleKey);
783-
transaction = txb.buildIncomplete();
784806
continue;
785807
}
786808

809+
const chainPath = currentUnspent.chainPath;
787810
if (hdPath) {
788811
var subPath = keychain.walletSubPath || '/0/0';
789-
var path = keychain.path + subPath + params.unspents[index].chainPath;
812+
var path = keychain.path + subPath + chainPath;
790813
privKey = hdPath.deriveKey(path);
791814
}
792815

816+
const isSegwitInput = !!currentUnspent.witnessScript;
817+
793818
// subscript is the part of the output script after the OP_CODESEPARATOR.
794819
// Since we are only ever signing p2sh outputs, which do not have
795820
// OP_CODESEPARATORS, it is always the output script.
796-
var subscript = new Buffer(params.unspents[index].redeemScript, 'hex');
821+
let subscript = new Buffer(currentUnspent.redeemScript, 'hex');
822+
currentUnspent.validationScript = subscript;
797823

798824
// In order to sign with bitcoinjs-lib, we must use its transaction
799825
// builder, confusingly named the same exact thing as our transaction
800826
// builder, but with inequivalent behavior.
801-
txb = bitcoin.TransactionBuilder.fromTransaction(transaction);
802827
try {
803-
txb.sign(index, privKey, subscript, bitcoin.Transaction.SIGHASH_ALL);
828+
if (isSegwitInput) {
829+
const witnessScript = new Buffer(currentUnspent.witnessScript, 'hex');
830+
currentUnspent.validationScript = witnessScript;
831+
txb.sign(index, privKey, subscript, bitcoin.Transaction.SIGHASH_ALL, currentUnspent.value, witnessScript);
832+
} else {
833+
txb.sign(index, privKey, subscript, bitcoin.Transaction.SIGHASH_ALL);
834+
}
804835
} catch (e) {
805836
return Q.reject('Failed to sign input #' + index);
806837
}
807838

808-
// Build the "incomplete" transaction, i.e. one that does not have all
809-
// the signatures (since we are only signing the first of 2 signatures in
810-
// a 2-of-3 multisig).
811-
transaction = txb.buildIncomplete();
839+
}
840+
841+
// reserialize transaction
842+
transaction = txb.build();
812843

844+
for (let index = 0; index < transaction.ins.length; ++index) {
813845
// bitcoinjs-lib adds one more OP_0 than we need. It creates one OP_0 for
814846
// every n public keys in an m-of-n multisig, and replaces the OP_0s with
815847
// the signature of the nth public key, then removes any remaining OP_0s
@@ -819,30 +851,21 @@ exports.signTransaction = function(params) {
819851
// chronological order, but is not compatible with the BitGo API, which
820852
// assumes m OP_0s for m-of-n multisig (or m-1 after the first signature
821853
// is created). Thus we need to remove the superfluous OP_0.
822-
var chunks = bitcoin.script.decompile(transaction.ins[index].script);
823-
if (chunks.length !== 5) {
824-
throw new Error('unexpected number of chunks in the OP_CHECKMULTISIG script after signing');
825-
}
826-
if (chunks[1]) {
827-
chunks.splice(2, 1); // The extra OP_0 is the third chunk
828-
} else if (chunks[2]) {
829-
chunks.splice(1, 1); // The extra OP_0 is the second chunk
830-
}
831854

832-
transaction.ins[index].script = bitcoin.script.compile(chunks);
855+
const currentUnspent = params.unspents[index];
833856

834857
// The signatures are validated server side and on the bitcoin network, so
835858
// the signature validation is optional and can be disabled by setting:
836859
// validate = false
837860
if (validate) {
838-
if (exports.verifyInputSignatures(transaction, index, subscript) !== -1) {
861+
if (exports.verifyInputSignatures(transaction, index, currentUnspent.validationScript, false, currentUnspent.value) !== -1) {
839862
throw new Error('number of signatures is invalid - something went wrong when signing');
840863
}
841864
}
842865
}
843866

844867
return Q.when({
845-
transactionHex: transaction.toBuffer().toString('hex')
868+
transactionHex: transaction.toHex()
846869
});
847870
};
848871

@@ -855,22 +878,34 @@ exports.signTransaction = function(params) {
855878
* @param inputIndex the input index to verify
856879
* @param pubScript the redeem script to verify with
857880
* @param ignoreKeyIndices array of multisig keys indexes (in order of keychains on the wallet). e.g. [1] to ignore backup keys
881+
* @param amount
858882
* @returns {number}
859883
*/
860-
exports.verifyInputSignatures = function(transaction, inputIndex, pubScript, ignoreKeyIndices) {
884+
exports.verifyInputSignatures = function(transaction, inputIndex, pubScript, ignoreKeyIndices, amount) {
861885
if (inputIndex < 0 || inputIndex >= transaction.ins.length) {
862886
throw new Error('illegal index');
863887
}
864888

865889
ignoreKeyIndices = ignoreKeyIndices || [];
866-
var sigScript = transaction.ins[inputIndex].script;
890+
const currentTransactionInput = transaction.ins[inputIndex];
891+
var sigScript = currentTransactionInput.script;
867892
var sigsNeeded = 1;
868893
var sigs = [];
869894
var pubKeys = [];
870895
var decompiledSigScript = bitcoin.script.decompile(sigScript);
871896

897+
const isSegwitInput = currentTransactionInput.witness.length > 0;
898+
if (isSegwitInput) {
899+
decompiledSigScript = currentTransactionInput.witness;
900+
sigScript = bitcoin.script.compile(decompiledSigScript);
901+
if (!amount) {
902+
return 0;
903+
}
904+
}
905+
872906
// Check the script type to determine number of signatures, the pub keys, and the script to hash.
873-
switch (bitcoin.script.classifyInput(sigScript, true)) {
907+
const inputClassification = bitcoinCash.script.classifyInput(sigScript, true);
908+
switch (inputClassification) {
874909
case 'scripthash':
875910
// Replace the pubScript with the P2SH Script.
876911
pubScript = decompiledSigScript[decompiledSigScript.length - 1];
@@ -897,16 +932,21 @@ exports.verifyInputSignatures = function(transaction, inputIndex, pubScript, ign
897932
return 0;
898933
}
899934

900-
var numVerifiedSignatures = 0;
901-
for (var sigIndex = 0; sigIndex < sigs.length; ++sigIndex) {
935+
let numVerifiedSignatures = 0;
936+
for (let sigIndex = 0; sigIndex < sigs.length; ++sigIndex) {
902937
// If this is an OP_0, then its been left as a placeholder for a future sig.
903-
if (sigs[sigIndex] == bitcoin.opcodes.OP_0) {
938+
if (sigs[sigIndex] === bitcoin.opcodes.OP_0) {
904939
continue;
905940
}
906941

907942
var hashType = sigs[sigIndex][sigs[sigIndex].length - 1];
908943
sigs[sigIndex] = sigs[sigIndex].slice(0, sigs[sigIndex].length - 1); // pop hash type from end
909-
var signatureHash = transaction.hashForSignature(inputIndex, pubScript, hashType);
944+
let signatureHash;
945+
if (isSegwitInput) {
946+
signatureHash = transaction.hashForWitnessV0(inputIndex, pubScript, amount, hashType);
947+
} else {
948+
signatureHash = transaction.hashForSignature(inputIndex, pubScript, hashType);
949+
}
910950

911951
var validSig = false;
912952

src/v2/wallet.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ Wallet.prototype.createAddress = function(params, callback) {
257257
params = params || {};
258258
common.validateParams(params, [], [], callback);
259259

260+
// TODO: verify address generation
260261
params.chain = params.chain || 0;
261262
return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/address'))
262263
.send(params)
@@ -543,6 +544,7 @@ Wallet.prototype.signTransaction = function(params, callback) {
543544
}
544545
userPrv = this.bitgo.decrypt({ input: userEncryptedPrv, password: params.walletPassphrase });
545546
}
547+
546548
var self = this;
547549
return Q.fcall(function() {
548550
const signingParams = _.extend({}, params, { txPrebuild: txPrebuild, prv: userPrv });
@@ -617,6 +619,7 @@ Wallet.prototype.sendMany = function(params, callback) {
617619
throw new Error('Only one of prebuildTx and recipients may be specified');
618620
}
619621

622+
// TODO: use Array.isArray
620623
if (params.recipients && !(params.recipients instanceof Array)) {
621624
throw new Error('expecting recipients array');
622625
}
@@ -633,8 +636,8 @@ Wallet.prototype.sendMany = function(params, callback) {
633636

634637
// pass in either the prebuild promise or, if undefined, the actual prebuild
635638
return Q.all([txPrebuildPromise || txPrebuild, userKeychainPromise])
636-
// preserve the "this"-reference in signTransaction
637639
.spread(function(txPrebuild, userKeychain) {
640+
// TODO: fix blob for
638641
var signingParams = _.extend({}, params, { txPrebuild: txPrebuild, keychain: userKeychain });
639642
return self.signTransaction(signingParams);
640643
})

src/v2/wallets.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Wallets.prototype.add = function(params, callback) {
116116
keys: params.keys
117117
};
118118

119+
// TODO: replace all IFs with single pick line
119120
if (params.enterprise) {
120121
walletParams.enterprise = params.enterprise;
121122
}

0 commit comments

Comments
 (0)