mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-03-13 11:36:15 +01:00
[DRAFT] ECKey: when signing, grind for low R values
The goal is to make transaction sizes more predictable, simplifying the fee calculation. This change will make sure signatures will always have DER encodings of 70 bytes or less, without counting the sighash flags byte. Note that one of our tests for a BIP143 native P2WPKH test vector had to be changed, because that vector had been produced pre-grinding and hasn't been updated since. However, the change only affects a P2PK input which isn't really the focus of the test vector. Also see similar changes in other projects: https://github.com/bitcoin/bitcoin/pull/13666 https://github.com/rust-bitcoin/rust-secp256k1/pull/259 https://github.com/bitcoin-s/bitcoin-s/pull/1342 + https://github.com/bitcoin-s/bitcoin-s/pull/2408 https://github.com/bisq-network/bisq/pull/7238 TODO extract HMacDSAKCalculatorWithEntrophy to top level class? TODO make encodeToCompact() its own commit? TODO more test vectors from other projects TODO should sigs with DER encodings *less* than 70 bytes also be retried?
This commit is contained in:
parent
5edb96cbb8
commit
f2701dceee
3 changed files with 112 additions and 6 deletions
|
@ -46,9 +46,11 @@ import org.bouncycastle.asn1.DLSequence;
|
|||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
import org.bouncycastle.asn1.x9.X9IntegerConverter;
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
||||
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
|
@ -69,6 +71,8 @@ import javax.annotation.Nullable;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SignatureException;
|
||||
|
@ -491,6 +495,26 @@ public class ECKey implements EncryptableItem {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the sig has a low R component, and will thus be 70 bytes or less in DER encoding (without the
|
||||
* sighash flags byte).
|
||||
*
|
||||
* @return true if sig has a low R component
|
||||
*/
|
||||
public boolean hasLowR() {
|
||||
byte[] compact = ByteUtils.bigIntegerToBytes(r, 32);
|
||||
return compact[0] >= 0;
|
||||
}
|
||||
|
||||
public byte[] encodeToCompact() {
|
||||
byte[] compactR = ByteUtils.bigIntegerToBytes(r, 32);
|
||||
byte[] compactS = ByteUtils.bigIntegerToBytes(s, 32);
|
||||
byte[] compact = new byte[64];
|
||||
System.arraycopy(compactR, 0, compact, 32 - compactR.length, compactR.length);
|
||||
System.arraycopy(compactS, 0, compact, 64 - compactS.length, compactS.length);
|
||||
return compact;
|
||||
}
|
||||
|
||||
/**
|
||||
* DER is an international standard for serializing data structures which is widely used in cryptography.
|
||||
* It's somewhat like protocol buffers but less convenient. This method returns a standard DER encoding
|
||||
|
@ -600,11 +624,26 @@ public class ECKey implements EncryptableItem {
|
|||
|
||||
protected ECDSASignature doSign(Sha256Hash input, BigInteger privateKeyForSigning) {
|
||||
Objects.requireNonNull(privateKeyForSigning);
|
||||
ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()));
|
||||
HMacDSAKCalculatorWithEntrophy kCalculator = new HMacDSAKCalculatorWithEntrophy(new SHA256Digest());
|
||||
ECDSASigner signer = new ECDSASigner(kCalculator);
|
||||
ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKeyForSigning, CURVE);
|
||||
signer.init(true, privKey);
|
||||
|
||||
// first try to sign without additional entropy
|
||||
BigInteger[] components = signer.generateSignature(input.getBytes());
|
||||
return new ECDSASignature(components[0], components[1]).toCanonicalised();
|
||||
ECDSASignature signature = new ECDSASignature(components[0], components[1]);
|
||||
|
||||
// grind for low R values by adding entropy to the K calculation via RFC 6979 section 3.6.
|
||||
// see discussion at https://github.com/bitcoin/bitcoin/pull/13666
|
||||
for (int counter = 1; !signature.hasLowR() && counter < Integer.MAX_VALUE; counter++) {
|
||||
byte[] entrophy =
|
||||
ByteBuffer.allocate(32).order(ByteOrder.LITTLE_ENDIAN).putInt(0, counter).array();
|
||||
kCalculator.setEntropy(entrophy);
|
||||
components = signer.generateSignature(input.getBytes());
|
||||
signature = new ECDSASignature(components[0], components[1]);
|
||||
}
|
||||
|
||||
return signature.toCanonicalised();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1384,4 +1423,42 @@ public class ECKey implements EncryptableItem {
|
|||
throw new RuntimeException(e); // Cannot happen.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom K calculator with ability to add additional entropy to the calculation. This is needed for grinding low
|
||||
* signature R values. Before calling {@link #setEntropy(byte[])} no entropy is added.
|
||||
*/
|
||||
private static class HMacDSAKCalculatorWithEntrophy extends HMacDSAKCalculator {
|
||||
@Nullable
|
||||
byte[] entrophy = null;
|
||||
|
||||
private HMacDSAKCalculatorWithEntrophy(Digest digest) {
|
||||
super(digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add 32 bytes of additional entropy to the K calculation via RFC 6979.
|
||||
*
|
||||
* @param entropy 32 bytes of entropy
|
||||
* @see
|
||||
* <a href="https://www.rfc-editor.org/rfc/rfc6979#section-3.6">RFC 6979 section 3.6. "Additional data…"</a>
|
||||
*/
|
||||
public void setEntropy(byte[] entropy) {
|
||||
Objects.requireNonNull(entropy);
|
||||
checkArgument(entropy.length == 32, () -> "entropy must be 32 bytes");
|
||||
this.entrophy = entropy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initAdditionalInput0(HMac hmac0) {
|
||||
if (entrophy != null)
|
||||
hmac0.update(entrophy, 0, 32);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initAdditionalInput1(HMac hmac1) {
|
||||
if (entrophy != null)
|
||||
hmac1.update(entrophy, 0, 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -287,7 +287,7 @@ public class TransactionTest {
|
|||
@Test
|
||||
public void testWitnessSignatureP2WPKH() {
|
||||
// test vector P2WPKH from:
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#native-p2wpkh
|
||||
String txHex = "01000000" // version
|
||||
+ "02" // num txIn
|
||||
+ "fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f" + "00000000" + "00" + "eeffffff" // txIn
|
||||
|
@ -319,7 +319,11 @@ public class TransactionTest {
|
|||
TransactionSignature txSig0 = tx.calculateSignature(0, key0,
|
||||
scriptPubKey0,
|
||||
Transaction.SigHash.ALL, false);
|
||||
assertEquals("30450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01",
|
||||
assertEquals(
|
||||
// with grinding for low R values:
|
||||
"30440220414110cf9b6d81fe3f22a8cc8bf867c1bbe0672ccfb2e8338db9d6f907105b2802201cfee6a5cb8a6239572963de01744ae66dd4a3af5b1413b489c165bf14ce297601",
|
||||
// without grinding:
|
||||
// "30450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01",
|
||||
ByteUtils.formatHex(txSig0.encodeToBitcoin()));
|
||||
|
||||
Script witnessScript = ScriptBuilder.createP2PKHOutputScript(key1);
|
||||
|
@ -351,7 +355,10 @@ public class TransactionTest {
|
|||
+ "01" // flag
|
||||
+ "02" // num txIn
|
||||
+ "fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f" + "00000000"
|
||||
+ "494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01"
|
||||
// with grinding for low R values:
|
||||
+ "484730440220414110cf9b6d81fe3f22a8cc8bf867c1bbe0672ccfb2e8338db9d6f907105b2802201cfee6a5cb8a6239572963de01744ae66dd4a3af5b1413b489c165bf14ce297601"
|
||||
// without grinding:
|
||||
// + "494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01"
|
||||
+ "eeffffff" // txIn
|
||||
+ "ef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a" + "01000000" + "00" + "ffffffff" // txIn
|
||||
+ "02" // num txOut
|
||||
|
@ -371,7 +378,7 @@ public class TransactionTest {
|
|||
@Test
|
||||
public void testWitnessSignatureP2SH_P2WPKH() {
|
||||
// test vector P2SH-P2WPKH from:
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#p2sh-p2wpkh
|
||||
String txHex = "01000000" // version
|
||||
+ "01" // num txIn
|
||||
+ "db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477" + "01000000" + "00" + "feffffff" // txIn
|
||||
|
|
|
@ -614,4 +614,26 @@ public class ECKeyTest {
|
|||
bytes[0] = 42;
|
||||
ECKey.fromPrivate(bytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signGrindLowR() {
|
||||
// test case from https://github.com/rust-bitcoin/rust-secp256k1/pull/259/files
|
||||
Sha256Hash msg = Sha256Hash.wrap(
|
||||
ByteUtils.parseHex("887d04bb1cf1b1554f1b268dfe62d13064ca67ae45348d50d1392ce2d13418ac"));
|
||||
ECKey sk = ECKey.fromPrivate(
|
||||
ByteUtils.parseHex("57f0148f94d13095cfda539d0da0d1541304b678d8b36e243980aab4e1b7cead"));
|
||||
ECDSASignature sig = sk.sign(msg); // grinds 5 times
|
||||
assertEquals(
|
||||
"047dd4d049db02b430d24c41c7925b2725bcd5a85393513bdec04b4dc363632b1054d0180094122b380f4cfa391e6296244da773173e78fc745c1b9c79f7b713",
|
||||
ByteUtils.formatHex(sig.encodeToCompact()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signAlwaysProducesMax70ByteDER() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ECKey sk = new ECKey();
|
||||
ECDSASignature sig = sk.sign(Sha256Hash.ZERO_HASH);
|
||||
assertTrue(sig.encodeToDER().length <= 70); // without sighash flags byte
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue