Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bitcore sig validation #3639

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/bitcore-lib-ltc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ litecore.deps._ = require('lodash');

// Internal usage, exposed for testing/advanced tweaking
litecore.Transaction.sighash = require('./lib/transaction/sighash');
litecore.Transaction.sighashwitness = require('./lib/transaction/sighashwitness');
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ PublicKeyHashInput.prototype.isValidSignature = function(transaction, signature,
// FIXME: Refactor signature so this is not necessary
signature.signature.nhashtype = signature.sigtype;
if (this.output.script.isWitnessPublicKeyHashOut() || this.output.script.isScriptHashOut()) {
var scriptCode = this.getScriptCode();
var scriptCode = this.getScriptCode(signature.publicKey);
var satoshisBuffer = this.getSatoshisBuffer();
return SighashWitness.verify(
transaction,
Expand Down
1 change: 1 addition & 0 deletions packages/bitcore-lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ bitcore.deps._ = require('lodash');

// Internal usage, exposed for testing/advanced tweaking
bitcore.Transaction.sighash = require('./lib/transaction/sighash');
bitcore.Transaction.sighashwitness = require('./lib/transaction/sighashwitness');
184 changes: 175 additions & 9 deletions packages/bitcore-lib/lib/transaction/input/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ var BufferUtil = require('../../util/buffer');
var JSUtil = require('../../util/js');
var Script = require('../../script');
var Sighash = require('../sighash');
var SighashWitness = require('../sighashwitness');
var Output = require('../output');
const TransactionSignature = require('../signature');
const Signature = require('../../crypto/signature');
const PublicKey = require('../../publickey');
const OpCode = require('../../opcode');
const BN = require('../../crypto/bn');

var MAXINT = 0xffffffff; // Math.pow(2, 32) - 1;
var DEFAULT_SEQNUMBER = MAXINT;
Expand Down Expand Up @@ -161,6 +167,154 @@ Input.prototype.getSignatures = function() {
);
};

/**
*
* @param {Transaction} transaction
* @param {number} inputIndex The index of the input in the transaction
* @param {Script|Buffer|string} scriptPubKey (optional) required for PublicKeyIn or MultisigIn input that does not have the output attached to it.
* @param {number|BN} satoshis (optional) required for PayToWitnessScriptHash input
* @returns {Array<TransactionSignature>}
*/
Input.prototype.extractSignatures = function(transaction, inputIndex, scriptPubKey, satoshis) {
$.checkArgument(JSUtil.isNaturalNumber(inputIndex), 'inputIndex is not a natural number');
$.checkState(this.script, 'Missing input script');

if (this.script.isPublicKeyIn()) {
const sig = Signature.fromTxFormat(this.script.chunks[0].buf);
if (!this.output) {
$.checkArgument(scriptPubKey, 'scriptPubKey is required when the input is not a full UTXO');
this.output = { script: new Script(scriptPubKey) };
}
const publicKey = this.output.script.chunks[0] && this.output.script.chunks[0].buf;
$.checkArgument(publicKey, 'No public key found from UTXO scriptPubKey');
return [new TransactionSignature({
signature: sig,
publicKey: new PublicKey(publicKey),
sigtype: sig.nhashtype,
inputIndex,
prevTxId: this.prevTxId,
outputIndex: this.outputIndex
})];
} else if (this.script.isPublicKeyHashIn()) {
const sig = Signature.fromTxFormat(this.script.chunks[0].buf);
return [new TransactionSignature({
signature: sig,
publicKey: this.script.chunks[1].buf,
sigtype: sig.nhashtype,
inputIndex,
prevTxId: this.prevTxId,
outputIndex: this.outputIndex
})];
} else if (this.hasWitnesses()) {
const witnessSigs = [];
const lastWitness = Script.fromBuffer(this.witnesses[this.witnesses.length - 1]);
if (lastWitness.toBuffer().length > 72 && lastWitness.chunks.at(-1).opcodenum === OpCode.OP_CHECKMULTISIG) {
// multisig
$.checkArgument((this.output && this.output._satoshisBN) || satoshis, 'Missing required satoshis parameter');
const satoshisBuffer = new BufferWriter().writeUInt64LEBN(this.output ? this.output._satoshisBN :new BN(satoshis)).toBuffer();
const scriptCode = new BufferWriter().writeVarintNum(lastWitness.toBuffer().length).write(lastWitness.toBuffer()).concat();
const numKeys = parseInt(OpCode(lastWitness.chunks.at(-2).opcodenum).toString().replace('OP_', ''));
const publicKeys = lastWitness.chunks.slice(-2 - numKeys, -2).map(c => c.buf);
for (const sigBuf of this.witnesses.slice(1, -1)) {
const sig = Signature.fromTxFormat(sigBuf);
let pkFound = false;
for (const pk of publicKeys) {
const txSig = new TransactionSignature({
signature: sig,
publicKey: pk,
sigtype: sig.nhashtype,
inputIndex,
prevTxId: this.prevTxId,
outputIndex: this.outputIndex
});
if (this.isValidSignature(transaction, txSig, 'ecdsa', { scriptCode, satoshisBuffer })) {
witnessSigs.push(txSig);
pkFound = true;
break;
}
}
$.checkState(pkFound, 'No public key found for multisig signature');
}
} else {
for (let i = 0; i < this.witnesses.length; i = i + 2) {
const sig = Signature.fromTxFormat(this.witnesses[i]);
witnessSigs.push(new TransactionSignature({
signature: sig,
publicKey: this.witnesses[i+1],
sigtype: sig.nhashtype,
inputIndex,
prevTxId: this.prevTxId,
outputIndex: this.outputIndex
}));
}
}
return witnessSigs;
} else if (this.script.isMultisigIn()) {
$.checkArgument(transaction, 'Missing transaction parameter');
if (!this.output) {
$.checkArgument(scriptPubKey, 'scriptPubKey is required when the input is not a full UTXO');
this.output = { script: new Script(scriptPubKey) };
}
const publicKeys = this.output.script.chunks.filter(c => c.opcodenum === 33).map(c => c.buf);
$.checkArgument(publicKeys.length > 0, 'No public keys found from UTXO scriptPubKey');
const sigs = [];
for (const chunk of this.script.chunks.slice(1)) {
const sig = Signature.fromTxFormat(chunk.buf);
let pkFound = false;
for (const pk of publicKeys) {
const txSig = new TransactionSignature({
signature: sig,
publicKey: pk,
sigtype: sig.nhashtype,
inputIndex,
prevTxId: this.prevTxId,
outputIndex: this.outputIndex
});
if (this.isValidSignature(transaction, txSig, 'ecdsa')) {
sigs.push(txSig);
pkFound = true;
break;
}
}
$.checkState(pkFound, 'No public key found for multisig signature');
}
return sigs;
} else if (this.script.isScriptHashIn()) {
$.checkArgument(transaction, 'Missing transaction parameter');
const redeemScript = Script.fromBuffer(this.script.chunks[this.script.chunks.length - 1].buf);
if (!this.output) { // if this is a generic Input
this.output = { script: redeemScript };
}
const publicKeys = redeemScript.chunks.filter(c => c.opcodenum === 33).map(c => c.buf);
const sigs = [];
for (const chunk of this.script.chunks.slice(1, -1)) {
const sig = Signature.fromTxFormat(chunk.buf);
let pkFound = false;
for (const pk of publicKeys) {
const txSig = new TransactionSignature({
signature: sig,
publicKey: pk,
sigtype: sig.nhashtype,
inputIndex,
prevTxId: this.prevTxId,
outputIndex: this.outputIndex
});
if (this.isValidSignature(transaction, txSig, 'ecdsa')) {
sigs.push(txSig);
pkFound = true;
break;
}
}
$.checkState(pkFound, 'No public key found for multisig signature');
}
return sigs;

}

// Unsigned input
return [];
};

Input.prototype.getSatoshisBuffer = function() {
$.checkState(this.output instanceof Output);
$.checkState(this.output._satoshisBN);
Expand Down Expand Up @@ -199,18 +353,30 @@ Input.prototype.setWitnesses = function(witnesses) {
this.witnesses = witnesses;
};

Input.prototype.isValidSignature = function(transaction, signature, signingMethod) {
Input.prototype.isValidSignature = function(transaction, signature, signingMethod, witnessScriptHash) {
// FIXME: Refactor signature so this is not necessary
signingMethod = signingMethod || 'ecdsa';
signature.signature.nhashtype = signature.sigtype;
return Sighash.verify(
transaction,
signature.signature,
signature.publicKey,
signature.inputIndex,
this.output.script,
signingMethod
);
if (witnessScriptHash) {
return SighashWitness.verify(
transaction,
signature.signature,
signature.publicKey,
signature.inputIndex,
witnessScriptHash.scriptCode,
witnessScriptHash.satoshisBuffer,
signingMethod
);
} else {
return Sighash.verify(
transaction,
signature.signature,
signature.publicKey,
signature.inputIndex,
this.output.script,
signingMethod
);
}
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ PublicKeyHashInput.prototype.isValidSignature = function(transaction, signature,
// FIXME: Refactor signature so this is not necessary
signature.signature.nhashtype = signature.sigtype;
if (this.output.script.isWitnessPublicKeyHashOut() || this.output.script.isScriptHashOut()) {
var scriptCode = this.getScriptCode();
var scriptCode = this.getScriptCode(signature.publicKey);
var satoshisBuffer = this.getSatoshisBuffer();
return SighashWitness.verify(
transaction,
Expand Down
6 changes: 3 additions & 3 deletions packages/bitcore-lib/lib/transaction/sighashwitness.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ var _ = require('lodash');
var sighash = function sighash(transaction, sighashType, inputNumber, scriptCode, satoshisBuffer) {
/* jshint maxstatements: 50 */

var hashPrevouts;
var hashSequence;
var hashOutputs;
var hashPrevouts = Buffer.alloc(32);
var hashSequence = Buffer.alloc(32);
var hashOutputs = Buffer.alloc(32);

if (!(sighashType & Signature.SIGHASH_ANYONECANPAY)) {
var buffers = [];
Expand Down
13 changes: 13 additions & 0 deletions packages/bitcore-lib/lib/transaction/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,19 @@ Transaction.prototype.getSignatures = function(privKey, sigtype, signingMethod)
return results;
};

/**
* Returns the signature from a signed transaction input
* @param {number} inputIndex Index of the input to extract the signature from
* @param {Script|Buffer|string} scriptPubKey (optional) required for PublicKeyIn or MultisigIn input that does not have the output attached to it.
* @returns {Array<TransactionSignature>}
*/
Transaction.prototype.extractSignatures = function(inputIndex, scriptPubKey, satoshis) {
$.checkArgument(JSUtil.isNaturalNumber(inputIndex), 'inputIndex must be a natural number');
$.checkArgument(inputIndex >= 0 && inputIndex < this.inputs.length, 'inputIndex is out of range');
const input = this.inputs[inputIndex];
return input.extractSignatures(this, inputIndex, scriptPubKey, satoshis);
};

/**
* Add a signature to the transaction
*
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcore-lib/test/crypto/signature.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ describe('Signature', function() {
});


describe('@isTxDER', function() {
describe('#isTxDER', function() {
it('should know this is a DER signature', function() {
var sighex = '3042021e17cfe77536c3fb0526bd1a72d7a8e0973f463add210be14063c8a9c37632022061bfa677f825ded82ba0863fb0c46ca1388dd3e647f6a93c038168b59d131a5101';
var sigbuf = Buffer.from(sighex, 'hex');
Expand Down
81 changes: 81 additions & 0 deletions packages/bitcore-lib/test/transaction/input/multisig.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,85 @@ describe('MultiSigInput', function() {
transaction.inputs[0].isValidSignature(transaction, transaction.inputs[0].signatures[0]).should.be.true;
transaction.inputs[0].isValidSignature(transaction, transaction.inputs[0].signatures[2]).should.be.true;
});

it('should validate a SIGHASH_ALL signature', function() {
const transaction = new Transaction()
.from(output, [public1, public2, public3], 2)
.to(address, 1000000)
.sign([privateKey1, privateKey2]); // defaults SIGHASH_ALL
const input = transaction.inputs[0];
input.isFullySigned().should.equal(true);

const sigs = transaction.extractSignatures(0);
sigs.length.should.equal(2);
for (const sig of sigs) {
sig.sigtype.should.equal(Signature.SIGHASH_ALL);
input.isValidSignature(transaction, sig).should.equal(true);
}
});

it('should validate a SIGHASH_NONE signature', function() {
const transaction = new Transaction()
.from(output, [public1, public2, public3], 2)
.to(address, 1000000)
.sign([privateKey1, privateKey2], Signature.SIGHASH_NONE);
const input = transaction.inputs[0];
input.isFullySigned().should.equal(true);

const sigs = transaction.extractSignatures(0);
sigs.length.should.equal(2);
for (const sig of sigs) {
sig.sigtype.should.equal(Signature.SIGHASH_NONE);
input.isValidSignature(transaction, sig).should.equal(true);
}
});

it('should validate a SIGHASH_SINGLE signature', function() {
const transaction = new Transaction()
.from(output, [public1, public2, public3], 2)
.to(address, 1000000)
.sign([privateKey1, privateKey2], Signature.SIGHASH_SINGLE);
const input = transaction.inputs[0];
input.isFullySigned().should.equal(true);

const sigs = transaction.extractSignatures(0);
sigs.length.should.equal(2);
for (const sig of sigs) {
sig.sigtype.should.equal(Signature.SIGHASH_SINGLE);
input.isValidSignature(transaction, sig).should.equal(true);
}
});

it('should validate a SIGHASH_ANYONECANPAY signature', function() {
const transaction = new Transaction()
.from(output, [public1, public2, public3], 2)
.to(address, 1000000)
.sign([privateKey1, privateKey2], Signature.SIGHASH_ANYONECANPAY);
const input = transaction.inputs[0];
input.isFullySigned().should.equal(true);

const sigs = transaction.extractSignatures(0);
sigs.length.should.equal(2);
for (const sig of sigs) {
sig.sigtype.should.equal(Signature.SIGHASH_ANYONECANPAY);
input.isValidSignature(transaction, sig).should.equal(true);
}
});

it('should validate a signature from a raw tx', function() {
const transaction = new Transaction()
.from(output, [public1, public2, public3], 2)
.to(address, 1000000)
.sign([privateKey1, privateKey2]);
const input = transaction.inputs[0];
input.isFullySigned().should.equal(true);

const anonymousTx = new Transaction(transaction.uncheckedSerialize());
const sigs = anonymousTx.extractSignatures(0, output.script);
sigs.length.should.equal(2);
for (const sig of sigs) {
sig.sigtype.should.equal(Signature.SIGHASH_ALL);
input.isValidSignature(transaction, sig).should.equal(true);
}
});
});