blocktrail-sdk
Version:
BlockTrail's Developer Friendly API binding for NodeJS
286 lines (245 loc) • 8.66 kB
JavaScript
var assert = require('assert');
var bitcoin = require('bitcoinjs-lib');
var SizeEstimation = {
SIZE_DER_SIGNATURE: 72,
SIZE_V0_P2WSH: 36
};
SizeEstimation.getPublicKeySize = function(isCompressed) {
return isCompressed ? 33 : 65;
};
SizeEstimation.getLengthForScriptPush = function(length) {
if (length < 75) {
return 1;
} else if (length <= 0xff) {
return 2;
} else if (length <= 0xffff) {
return 3;
} else if (length <= 0xffffffff) {
return 5;
} else {
throw new Error("Size of pushdata too large");
}
};
SizeEstimation.getLengthForVarInt = function(length) {
if (length < 253) {
return 1;
}
// Rest have a prefix byte
var numBytes;
if (length < 65535) {
numBytes = 2;
} else if (length < 4294967295) {
numBytes = 4;
} else if (length < 18446744073709551615) {
numBytes = 8;
} else {
throw new Error("Size of varint too large");
}
return 1 + numBytes;
};
SizeEstimation.estimateMultisigStackSize = function(m, keys) {
// Initialize with OP_0
var stackSizes = [0];
var i;
for (i = 0; i < m; i++) {
stackSizes.push(SizeEstimation.SIZE_DER_SIGNATURE);
}
var scriptSize = 1; // OP_$m
for (i = 0; i < keys.length; i++) {
scriptSize += this.getLengthForScriptPush(keys[i].length) + keys[i].length;
}
scriptSize += 2; // OP_$n OP_CHECKMULTISIG
return [stackSizes, scriptSize];
};
SizeEstimation.estimateP2PKStackSize = function(key) {
var stackSizes = [SizeEstimation.SIZE_DER_SIGNATURE];
var scriptSize = this.getLengthForScriptPush(key.length) + key.length + 1; // KEY OP_CHECKSIG
return [stackSizes, scriptSize];
};
SizeEstimation.estimateP2PKHStackSize = function(isCompressed) {
if (typeof isCompressed === 'undefined') {
isCompressed = true;
}
var stackSizes = [this.SIZE_DER_SIGNATURE, this.getPublicKeySize(isCompressed)];
var scriptSize = 2 + this.getLengthForScriptPush(20) + 20 + 2;
return [stackSizes, scriptSize];
};
/**
* As pure a function as it gets, but without the overhead
* of checking everything. Make sure your calls are correct.
*
* @param {Buffer} stackSizes
* @param {Buffer} isWitness
* @param {Buffer} rs
* @param {Buffer} ws
*/
SizeEstimation.estimateStackSignatureSize = function(stackSizes, isWitness, rs, ws) {
assert(ws === null || isWitness);
var scriptSigSizes = [];
var witnessSizes = [];
if (isWitness) {
witnessSizes = stackSizes;
if (ws instanceof Buffer) {
witnessSizes.push(ws.length);
}
} else {
scriptSigSizes = stackSizes;
}
if (rs instanceof Buffer) {
scriptSigSizes.push(rs.length);
}
var self = this;
var scriptSigSize = 0;
scriptSigSizes.map(function(elementLen) {
scriptSigSize += self.getLengthForScriptPush(elementLen) + elementLen;
});
scriptSigSize += self.getLengthForVarInt(scriptSigSize);
var witnessSize = 0;
if (witnessSizes.length > 0) {
witnessSizes.map(function(elementLen) {
witnessSize += self.getLengthForVarInt(elementLen) + elementLen;
});
witnessSize += self.getLengthForVarInt(witnessSizes.length);
}
return [scriptSigSize, witnessSize];
};
/**
*
* @param {Buffer} script - main script, can equal RS/WS too
* @param {Buffer} redeemScript
* @param {Buffer} witnessScript
* @param {boolean} isWitness - required, covers P2WPKH and so on
* @param {boolean} compressed - only strictly required for p2pkh
*/
SizeEstimation.estimateInputFromScripts = function(script, redeemScript, witnessScript, isWitness, compressed) {
assert(witnessScript === null || isWitness);
var stackSizes;
if (bitcoin.script.multisig.output.check(script)) {
var multisig = bitcoin.script.multisig.output.decode(script);
stackSizes = this.estimateMultisigStackSize(multisig.m, multisig.pubKeys)[0];
} else if (bitcoin.script.pubKey.output.check(script)) {
var p2pk = bitcoin.script.pubKey.output.decode(script);
stackSizes = this.estimateP2PKStackSize(p2pk)[0];
} else if (bitcoin.script.pubKeyHash.output.check(script)) {
stackSizes = this.estimateP2PKHStackSize(compressed)[0];
} else {
throw new Error("Unsupported script type");
}
return this.estimateStackSignatureSize(stackSizes, isWitness, redeemScript, witnessScript);
};
SizeEstimation.estimateUtxo = function(utxo, compressed) {
var spk = Buffer.from(utxo.scriptpubkey_hex, 'hex');
var rs = null;
var ws = null;
if (utxo.redeem_script) {
if (typeof utxo.redeem_script === 'string') {
rs = Buffer.from(utxo.redeem_script, 'hex')
} else if (utxo.redeem_script instanceof Buffer) {
rs = utxo.redeem_script;
}
}
if (utxo.witness_script) {
if (typeof utxo.witness_script === 'string') {
ws = Buffer.from(utxo.witness_script, 'hex')
} else if (utxo.witness_script instanceof Buffer) {
ws = utxo.witness_script;
}
}
var witness = false;
var signScript = spk;
if (bitcoin.script.scriptHash.output.check(signScript)) {
if (null === rs) {
throw new Error("Cant estimate, missing redeem script");
}
signScript = rs;
}
if (bitcoin.script.witnessPubKeyHash.output.check(signScript)) {
var p2wpkh = bitcoin.script.witnessPubKeyHash.output.decode(signScript);
signScript = bitcoin.script.pubKeyHash.output.encode(p2wpkh);
witness = true;
} else if (bitcoin.script.witnessScriptHash.output.check(signScript)) {
if (null === ws) {
throw new Error("Can't estimate, missing witness script");
}
signScript = ws;
witness = true;
}
var types = bitcoin.script.types;
var allowedTypes = [types.MULTISIG, types.P2PKH, types.P2PK];
var type = bitcoin.script.classifyOutput(signScript);
if (allowedTypes.indexOf(type) === -1) {
throw new Error("Unsupported script type");
}
var estimation = this.estimateInputFromScripts(signScript, rs, ws, witness, compressed);
return {
scriptSig: estimation[0],
witness: estimation[1]
};
};
/**
* Returns the size of input related data, given a set
* of utxos we can estimate for. witness data is included
* if withWitness=true, and is required for vsize/weight
* calculations
* @param {object} utxos
* @param {boolean} withWitness
* @returns {number}
*/
SizeEstimation.estimateInputsSize = function(utxos, withWitness) {
var inputSize = 0;
var witnessSize = 0;
utxos.map(function(utxo) {
var estimate = SizeEstimation.estimateUtxo(utxo);
// txid + vout + sequence + scriptSig
inputSize += 32 + 4 + 4 + estimate.scriptSig;
if (withWitness) {
witnessSize += estimate.witness;
}
});
if (withWitness && witnessSize > 0) {
inputSize += 2 + witnessSize;
}
return inputSize;
};
/**
* Calculates number of bytes to serialize tx outputs
* @param {Array} outs
* @returns {number}
*/
SizeEstimation.calculateOutputsSize = function(outs) {
var outputsSize = 0;
outs.map(function(out) {
var scriptSize = SizeEstimation.getLengthForVarInt(out.script.length);
outputsSize += 8 + scriptSize + (out.script.length);
});
return outputsSize;
};
/**
* Returns the transactions weight.
*
* @param {bitcoin.Transaction} tx
* @param {array} utxos
* @returns {*}
*/
SizeEstimation.estimateTxWeight = function(tx, utxos) {
var outSize = SizeEstimation.calculateOutputsSize(tx.outs);
// version + vinLen + vin + voutLen + vout + nlocktime
var baseSize = 4 + SizeEstimation.getLengthForVarInt(utxos.length) + this.estimateInputsSize(utxos, false) +
SizeEstimation.getLengthForVarInt(tx.outs.length) + outSize + 4;
// version + vinLen + vin (includes witness) + voutLen + vout + nlocktime
var witSize = 4 + SizeEstimation.getLengthForVarInt(utxos.length) + this.estimateInputsSize(utxos, true) +
SizeEstimation.getLengthForVarInt(tx.outs.length) + outSize + 4;
return (3 * baseSize) + witSize;
};
/**
* Returns the vsize for a transaction. Same as size
* for transaction with no witness data.
*
* @param {bitcoin.Transaction} tx
* @param {array} utxos
* @returns {Number}
*/
SizeEstimation.estimateTxVsize = function(tx, utxos) {
return parseInt(Math.ceil(SizeEstimation.estimateTxWeight(tx, utxos) / 4), 10);
};
module.exports = SizeEstimation;