Merge pull request #1385 from bitcoinjs/lowR
Add low R value signing as option to ECPair.sign
This commit is contained in:
commit
39bd08002b
10 changed files with 1174 additions and 1147 deletions
2205
package-lock.json
generated
2205
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -45,7 +45,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "10.12.18",
|
"@types/node": "10.12.18",
|
||||||
"bech32": "^1.1.2",
|
"bech32": "^1.1.2",
|
||||||
"bip32": "^2.0.0",
|
"bip32": "^2.0.3",
|
||||||
"bip66": "^1.1.0",
|
"bip66": "^1.1.0",
|
||||||
"bitcoin-ops": "^1.4.0",
|
"bitcoin-ops": "^1.4.0",
|
||||||
"bs58check": "^2.0.0",
|
"bs58check": "^2.0.0",
|
||||||
|
@ -69,11 +69,11 @@
|
||||||
"hoodwink": "^2.0.0",
|
"hoodwink": "^2.0.0",
|
||||||
"minimaldata": "^1.0.2",
|
"minimaldata": "^1.0.2",
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"nyc": "^13.3.0",
|
"nyc": "^14.1.1",
|
||||||
"prettier": "1.16.4",
|
"prettier": "1.16.4",
|
||||||
"proxyquire": "^2.0.1",
|
"proxyquire": "^2.0.1",
|
||||||
"rimraf": "^2.6.3",
|
"rimraf": "^2.6.3",
|
||||||
"tslint": "5.13.1",
|
"tslint": "^5.16.0",
|
||||||
"typescript": "3.2.2"
|
"typescript": "3.2.2"
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|
|
@ -35,10 +35,25 @@ class ECPair {
|
||||||
throw new Error('Missing private key');
|
throw new Error('Missing private key');
|
||||||
return wif.encode(this.network.wif, this.__D, this.compressed);
|
return wif.encode(this.network.wif, this.__D, this.compressed);
|
||||||
}
|
}
|
||||||
sign(hash) {
|
sign(hash, lowR = false) {
|
||||||
if (!this.__D)
|
if (!this.__D)
|
||||||
throw new Error('Missing private key');
|
throw new Error('Missing private key');
|
||||||
return ecc.sign(hash, this.__D);
|
if (lowR === false) {
|
||||||
|
return ecc.sign(hash, this.__D);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let sig = ecc.sign(hash, this.__D);
|
||||||
|
const extraData = Buffer.alloc(32, 0);
|
||||||
|
let counter = 0;
|
||||||
|
// if first try is lowR, skip the loop
|
||||||
|
// for second try and on, add extra entropy counting up
|
||||||
|
while (sig[0] > 0x7f) {
|
||||||
|
counter++;
|
||||||
|
extraData.writeUIntLE(counter, 0, 6);
|
||||||
|
sig = ecc.signWithEntropy(hash, this.__D, extraData);
|
||||||
|
}
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
verify(hash, signature) {
|
verify(hash, signature) {
|
||||||
return ecc.verify(hash, this.publicKey, signature);
|
return ecc.verify(hash, this.publicKey, signature);
|
||||||
|
|
|
@ -29,6 +29,7 @@ class TransactionBuilder {
|
||||||
this.__INPUTS = [];
|
this.__INPUTS = [];
|
||||||
this.__TX = new transaction_1.Transaction();
|
this.__TX = new transaction_1.Transaction();
|
||||||
this.__TX.version = 2;
|
this.__TX.version = 2;
|
||||||
|
this.__USE_LOW_R = false;
|
||||||
}
|
}
|
||||||
static fromTransaction(transaction, network) {
|
static fromTransaction(transaction, network) {
|
||||||
const txb = new TransactionBuilder(network);
|
const txb = new TransactionBuilder(network);
|
||||||
|
@ -53,6 +54,14 @@ class TransactionBuilder {
|
||||||
});
|
});
|
||||||
return txb;
|
return txb;
|
||||||
}
|
}
|
||||||
|
setLowR(setting) {
|
||||||
|
typeforce(typeforce.maybe(typeforce.Boolean), setting);
|
||||||
|
if (setting === undefined) {
|
||||||
|
setting = true;
|
||||||
|
}
|
||||||
|
this.__USE_LOW_R = setting;
|
||||||
|
return setting;
|
||||||
|
}
|
||||||
setLockTime(locktime) {
|
setLockTime(locktime) {
|
||||||
typeforce(types.UInt32, locktime);
|
typeforce(types.UInt32, locktime);
|
||||||
// if any signatures exist, throw
|
// if any signatures exist, throw
|
||||||
|
@ -159,7 +168,7 @@ class TransactionBuilder {
|
||||||
if (ourPubKey.length !== 33 && input.hasWitness) {
|
if (ourPubKey.length !== 33 && input.hasWitness) {
|
||||||
throw new Error('BIP143 rejects uncompressed public keys in P2WPKH or P2WSH');
|
throw new Error('BIP143 rejects uncompressed public keys in P2WPKH or P2WSH');
|
||||||
}
|
}
|
||||||
const signature = keyPair.sign(signatureHash);
|
const signature = keyPair.sign(signatureHash, this.__USE_LOW_R);
|
||||||
input.signatures[i] = bscript.signature.encode(signature, hashType);
|
input.signatures[i] = bscript.signature.encode(signature, hashType);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
|
@ -259,4 +259,26 @@ describe('ECPair', () => {
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe('optional low R signing', () => {
|
||||||
|
const sig = Buffer.from('95a6619140fca3366f1d3b013b0367c4f86e39508a50fdce' +
|
||||||
|
'e5245fbb8bd60aa6086449e28cf15387cf9f85100bfd0838624ca96759e59f65c10a00' +
|
||||||
|
'16b86f5229', 'hex')
|
||||||
|
const sigLowR = Buffer.from('6a2660c226e8055afad317eeba918a304be79208d505' +
|
||||||
|
'3bc5ea4a5e4c5892b4a061c717c5284ae5202d721c0e49b4717b79966280906b1d3b52' +
|
||||||
|
'95d1fdde963c35', 'hex')
|
||||||
|
const lowRKeyPair = ECPair.fromWIF('L3nThUzbAwpUiBAjR5zCu66ybXSPMr2zZ3ikp' +
|
||||||
|
'ScpTPiYTxBynfZu')
|
||||||
|
const dataToSign = Buffer.from('b6c5c548a7f6164c8aa7af5350901626ebd69f9ae' +
|
||||||
|
'2c1ecf8871f5088ec204cfe', 'hex')
|
||||||
|
|
||||||
|
it('signs with normal R by default', () => {
|
||||||
|
const signed = lowRKeyPair.sign(dataToSign)
|
||||||
|
assert.deepStrictEqual(sig, signed)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('signs with low R when true is passed', () => {
|
||||||
|
const signed = lowRKeyPair.sign(dataToSign, true)
|
||||||
|
assert.deepStrictEqual(sigLowR, signed)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -338,6 +338,25 @@ describe('TransactionBuilder', () => {
|
||||||
assert.strictEqual(txb.build().toHex(), '0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006a47304402205f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f02205f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f0121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078fffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000')
|
assert.strictEqual(txb.build().toHex(), '0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006a47304402205f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f02205f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f0121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078fffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('supports low R signature signing', () => {
|
||||||
|
let txb = new TransactionBuilder()
|
||||||
|
txb.setVersion(1)
|
||||||
|
txb.addInput('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 1)
|
||||||
|
txb.addOutput('1111111111111111111114oLvT2', 100000)
|
||||||
|
txb.sign(0, keyPair)
|
||||||
|
// high R
|
||||||
|
assert.strictEqual(txb.build().toHex(), '0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006b483045022100b872677f35c9c14ad9c41d83649fb049250f32574e0b2547d67e209ed14ff05d022059b36ad058be54e887a1a311d5c393cb4941f6b93a0b090845ec67094de8972b01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000')
|
||||||
|
|
||||||
|
txb = new TransactionBuilder()
|
||||||
|
txb.setVersion(1)
|
||||||
|
txb.addInput('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 1)
|
||||||
|
txb.addOutput('1111111111111111111114oLvT2', 100000)
|
||||||
|
txb.setLowR()
|
||||||
|
txb.sign(0, keyPair)
|
||||||
|
// low R
|
||||||
|
assert.strictEqual(txb.build().toHex(), '0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010000006a473044022012a601efa8756ebe83e9ac7a7db061c3147e3b49d8be67685799fe51a4c8c62f02204d568d301d5ce14af390d566d4fd50e7b8ee48e71ec67786c029e721194dae3601210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff01a0860100000000001976a914000000000000000000000000000000000000000088ac00000000')
|
||||||
|
})
|
||||||
|
|
||||||
fixtures.invalid.sign.forEach(f => {
|
fixtures.invalid.sign.forEach(f => {
|
||||||
it('throws ' + f.exception + (f.description ? ' (' + f.description + ')' : ''), () => {
|
it('throws ' + f.exception + (f.description ? ' (' + f.description + ')' : ''), () => {
|
||||||
const txb = construct(f, true)
|
const txb = construct(f, true)
|
||||||
|
|
|
@ -25,7 +25,7 @@ export interface ECPairInterface {
|
||||||
publicKey: Buffer;
|
publicKey: Buffer;
|
||||||
privateKey?: Buffer;
|
privateKey?: Buffer;
|
||||||
toWIF(): string;
|
toWIF(): string;
|
||||||
sign(hash: Buffer): Buffer;
|
sign(hash: Buffer, lowR?: boolean): Buffer;
|
||||||
verify(hash: Buffer, signature: Buffer): boolean;
|
verify(hash: Buffer, signature: Buffer): boolean;
|
||||||
getPublicKey?(): Buffer;
|
getPublicKey?(): Buffer;
|
||||||
}
|
}
|
||||||
|
@ -62,9 +62,23 @@ class ECPair implements ECPairInterface {
|
||||||
return wif.encode(this.network.wif, this.__D, this.compressed);
|
return wif.encode(this.network.wif, this.__D, this.compressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
sign(hash: Buffer): Buffer {
|
sign(hash: Buffer, lowR: boolean = false): Buffer {
|
||||||
if (!this.__D) throw new Error('Missing private key');
|
if (!this.__D) throw new Error('Missing private key');
|
||||||
return ecc.sign(hash, this.__D);
|
if (lowR === false) {
|
||||||
|
return ecc.sign(hash, this.__D);
|
||||||
|
} else {
|
||||||
|
let sig = ecc.sign(hash, this.__D);
|
||||||
|
const extraData = Buffer.alloc(32, 0);
|
||||||
|
let counter = 0;
|
||||||
|
// if first try is lowR, skip the loop
|
||||||
|
// for second try and on, add extra entropy counting up
|
||||||
|
while (sig[0] > 0x7f) {
|
||||||
|
counter++;
|
||||||
|
extraData.writeUIntLE(counter, 0, 6);
|
||||||
|
sig = ecc.signWithEntropy(hash, this.__D, extraData);
|
||||||
|
}
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(hash: Buffer, signature: Buffer): boolean {
|
verify(hash: Buffer, signature: Buffer): boolean {
|
||||||
|
|
|
@ -94,6 +94,7 @@ export class TransactionBuilder {
|
||||||
private __PREV_TX_SET: { [index: string]: boolean };
|
private __PREV_TX_SET: { [index: string]: boolean };
|
||||||
private __INPUTS: TxbInput[];
|
private __INPUTS: TxbInput[];
|
||||||
private __TX: Transaction;
|
private __TX: Transaction;
|
||||||
|
private __USE_LOW_R: boolean;
|
||||||
|
|
||||||
// WARNING: maximumFeeRate is __NOT__ to be relied on,
|
// WARNING: maximumFeeRate is __NOT__ to be relied on,
|
||||||
// it's just another potential safety mechanism (safety in-depth)
|
// it's just another potential safety mechanism (safety in-depth)
|
||||||
|
@ -105,6 +106,16 @@ export class TransactionBuilder {
|
||||||
this.__INPUTS = [];
|
this.__INPUTS = [];
|
||||||
this.__TX = new Transaction();
|
this.__TX = new Transaction();
|
||||||
this.__TX.version = 2;
|
this.__TX.version = 2;
|
||||||
|
this.__USE_LOW_R = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLowR(setting?: boolean): boolean {
|
||||||
|
typeforce(typeforce.maybe(typeforce.Boolean), setting);
|
||||||
|
if (setting === undefined) {
|
||||||
|
setting = true;
|
||||||
|
}
|
||||||
|
this.__USE_LOW_R = setting;
|
||||||
|
return setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLockTime(locktime: number): void {
|
setLockTime(locktime: number): void {
|
||||||
|
@ -266,7 +277,7 @@ export class TransactionBuilder {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const signature = keyPair.sign(signatureHash);
|
const signature = keyPair.sign(signatureHash, this.__USE_LOW_R);
|
||||||
input.signatures![i] = bscript.signature.encode(signature, hashType!);
|
input.signatures![i] = bscript.signature.encode(signature, hashType!);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
4
types/ecpair.d.ts
vendored
4
types/ecpair.d.ts
vendored
|
@ -11,7 +11,7 @@ export interface ECPairInterface {
|
||||||
publicKey: Buffer;
|
publicKey: Buffer;
|
||||||
privateKey?: Buffer;
|
privateKey?: Buffer;
|
||||||
toWIF(): string;
|
toWIF(): string;
|
||||||
sign(hash: Buffer): Buffer;
|
sign(hash: Buffer, lowR?: boolean): Buffer;
|
||||||
verify(hash: Buffer, signature: Buffer): boolean;
|
verify(hash: Buffer, signature: Buffer): boolean;
|
||||||
getPublicKey?(): Buffer;
|
getPublicKey?(): Buffer;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ declare class ECPair implements ECPairInterface {
|
||||||
readonly privateKey: Buffer | undefined;
|
readonly privateKey: Buffer | undefined;
|
||||||
readonly publicKey: Buffer;
|
readonly publicKey: Buffer;
|
||||||
toWIF(): string;
|
toWIF(): string;
|
||||||
sign(hash: Buffer): Buffer;
|
sign(hash: Buffer, lowR?: boolean): Buffer;
|
||||||
verify(hash: Buffer, signature: Buffer): boolean;
|
verify(hash: Buffer, signature: Buffer): boolean;
|
||||||
}
|
}
|
||||||
declare function fromPrivateKey(buffer: Buffer, options?: ECPairOptions): ECPair;
|
declare function fromPrivateKey(buffer: Buffer, options?: ECPairOptions): ECPair;
|
||||||
|
|
2
types/transaction_builder.d.ts
vendored
2
types/transaction_builder.d.ts
vendored
|
@ -9,7 +9,9 @@ export declare class TransactionBuilder {
|
||||||
private __PREV_TX_SET;
|
private __PREV_TX_SET;
|
||||||
private __INPUTS;
|
private __INPUTS;
|
||||||
private __TX;
|
private __TX;
|
||||||
|
private __USE_LOW_R;
|
||||||
constructor(network?: Network, maximumFeeRate?: number);
|
constructor(network?: Network, maximumFeeRate?: number);
|
||||||
|
setLowR(setting?: boolean): boolean;
|
||||||
setLockTime(locktime: number): void;
|
setLockTime(locktime: number): void;
|
||||||
setVersion(version: number): void;
|
setVersion(version: number): void;
|
||||||
addInput(txHash: Buffer | string | Transaction, vout: number, sequence?: number, prevOutScript?: Buffer): number;
|
addInput(txHash: Buffer | string | Transaction, vout: number, sequence?: number, prevOutScript?: Buffer): number;
|
||||||
|
|
Loading…
Reference in a new issue