Skip to content

Commit

Permalink
Add Supranational BLS implementation (#2453)
Browse files Browse the repository at this point in the history
* Replace BLSPublicKey.isValid() with fromBytesCompressedValidate() method
* Make BLSPublicKey fromBytesCompressed/toBytesCompressed operating with Bytes48 type. SecretKey.toBytes() returns Bytes32
* Rename BLSPublicKey/BLSSignature.from/toBytes() methods to from/toSSZBytes() to be more explicit
* Hide constructors. Make PublicKey and Signature implementations to be lazily evaluated inside corresponding BLS wrappers
* Fix DLL binary attribute
* Validate BlstSignature when creating from bytes
* Add mikuli and blst specific infinity signature tests
* Add BlstPublicKey.fromBytesUncompressed for testing
* For BLSPublicKey/BLSSignature use bytes representation for hashCode() and equals() since we need ability to compare valid (from SSZ standpoint) 'empty' instances, but shouldn't decode them to real instances
* In case of fail fast when deserializing invalid signature bytes, just mark the signature invalid. This is to deal with empty signatures (valid from SSZ perspective)
* Fix reference tests running on Windows
* Add Mikuli fallback when couldn't load Blst
* Move SWIG wrapper to a separate project
* Update license for jblst
* Implement BlstSecretKey.destroy()
* Temporary handling infinite pubkey/signature as a special case until supranational/blst#11 is resolved
* Generate KeyPair from seed in an implementation independent way. Throw exception when instantiating SecretKey from non-valid BLS12381 scalar value
* Make BLSPublicKey.getPublicKey() package private
* Make BLSSignature.getSignature() package private
* Make BlstBLS12381.INSTANCE initialization more reliable and make it as Optional
* Move BLS constants to BLSConstants class. Make a clear separation of BLSPublicKey fromSSZBytes() and fromBytesCompressed() though they are equivalent with current SSZ implementation
* Make a distinction of BLSSignature from/toSSZBytes() and from/toBytesCompressed() though they are equivalent with the current SSZ implementation
  • Loading branch information
Nashatyrev authored Jul 31, 2020
1 parent 0460c61 commit b3d2e07
Show file tree
Hide file tree
Showing 86 changed files with 1,749 additions and 322 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.bat -text
*.pcap binary
*.blocks binary
*.dll binary
*.dylib binary
*.so binary
*.gz binary
Expand Down
2 changes: 2 additions & 0 deletions bls/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ dependencies {
implementation 'org.apache.tuweni:tuweni-crypto'
implementation 'org.apache.tuweni:tuweni-ssz'
implementation 'org.miracl.milagro.amcl:milagro-crypto-java'
implementation 'tech.pegasys:jblst'


testImplementation project(':logging')
testImplementation('com.googlecode.json-simple:json-simple') {
Expand Down
8 changes: 6 additions & 2 deletions bls/src/main/java/tech/pegasys/teku/bls/BLS.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import tech.pegasys.teku.bls.impl.BLS12381;
import tech.pegasys.teku.bls.impl.PublicKey;
import tech.pegasys.teku.bls.impl.PublicKeyMessagePair;
import tech.pegasys.teku.bls.impl.blst.BlstBLS12381;
import tech.pegasys.teku.bls.impl.mikuli.MikuliBLS12381;

/**
Expand All @@ -43,7 +44,8 @@ public class BLS {

private static final Logger LOG = LogManager.getLogger();

private static BLS12381 BlsImpl = MikuliBLS12381.INSTANCE;
private static final BLS12381 BlsImpl =
BlstBLS12381.INSTANCE.map(bls -> (BLS12381) bls).orElse(MikuliBLS12381.INSTANCE);

/*
* The following are the methods used directly in the Ethereum 2.0 specifications. These strictly adhere to the standard.
Expand Down Expand Up @@ -91,8 +93,10 @@ public static boolean verify(BLSPublicKey publicKey, Bytes message, BLSSignature
*
* @param signatures the list of signatures to be aggregated
* @return the aggregated signature
* @throws IllegalArgumentException if any of supplied signatures is invalid
*/
public static BLSSignature aggregate(List<BLSSignature> signatures) {
public static BLSSignature aggregate(List<BLSSignature> signatures)
throws IllegalArgumentException {
checkArgument(signatures.size() > 0, "Aggregating zero signatures is invalid.");
return new BLSSignature(
getBlsImpl()
Expand Down
12 changes: 12 additions & 0 deletions bls/src/main/java/tech/pegasys/teku/bls/BLSConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@

package tech.pegasys.teku.bls;

import java.math.BigInteger;
import java.nio.ByteOrder;
import org.apache.tuweni.bytes.Bytes32;

public class BLSConstants {

public static final int BLS_PUBKEY_SIZE = 48;
public static final int BLS_SIGNATURE_SIZE = 96;

static final Bytes32 CURVE_ORDER_BYTES =
Bytes32.fromHexString("0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001");
static final BigInteger CURVE_ORDER_BI =
CURVE_ORDER_BYTES.toUnsignedBigInteger(ByteOrder.BIG_ENDIAN);

public static boolean VERIFICATION_DISABLED = false;

public static void disableBLSVerification() {
Expand Down
6 changes: 5 additions & 1 deletion bls/src/main/java/tech/pegasys/teku/bls/BLSKeyPair.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import com.google.common.base.MoreObjects;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.Random;
import org.apache.tuweni.bytes.Bytes32;
import tech.pegasys.teku.bls.impl.KeyPair;

public final class BLSKeyPair {
Expand All @@ -39,7 +41,9 @@ public static BLSKeyPair random(final SecureRandom srng) {
* @return a keypair generated from a seed
*/
public static BLSKeyPair random(int seed) {
return new BLSKeyPair(BLS.getBlsImpl().generateKeyPair(seed));
BLSSecretKey pseudoRandomSecretBytes =
BLSSecretKey.fromBytesModR(Bytes32.random(new Random(seed)));
return new BLSKeyPair(pseudoRandomSecretBytes);
}

private final BLSPublicKey publicKey;
Expand Down
112 changes: 70 additions & 42 deletions bls/src/main/java/tech/pegasys/teku/bls/BLSPublicKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes48;
import org.apache.tuweni.ssz.SSZ;
import tech.pegasys.teku.bls.impl.PublicKey;
import tech.pegasys.teku.ssz.sos.SimpleOffsetSerializable;
Expand All @@ -26,7 +30,7 @@ public final class BLSPublicKey implements SimpleOffsetSerializable {

// The number of SimpleSerialize basic types in this SSZ Container/POJO.
public static final int SSZ_FIELD_COUNT = 1;
public static final int BLS_PUBKEY_SIZE = 48;
public static final int SSZ_BLS_PUBKEY_SIZE = BLSConstants.BLS_PUBKEY_SIZE;

/**
* Generates a compressed, serialized, random, valid public key based on a seed.
Expand All @@ -43,7 +47,21 @@ public static BLSPublicKey random(int seed) {
* @return the empty public key as per the Eth2 spec
*/
public static BLSPublicKey empty() {
return BLSPublicKey.fromBytes(Bytes.wrap(new byte[BLS_PUBKEY_SIZE]));
return BLSPublicKey.fromBytesCompressed(Bytes48.ZERO);
}

/**
* Aggregates list of PublicKeys, returns the public key that corresponds to G1 point at infinity
* if list is empty
*
* @param publicKeys The list of public keys to aggregate
* @return PublicKey The public key
*/
public static BLSPublicKey aggregate(List<BLSPublicKey> publicKeys) {
return new BLSPublicKey(
BLS.getBlsImpl()
.aggregatePublicKeys(
publicKeys.stream().map(BLSPublicKey::getPublicKey).collect(Collectors.toList())));
}

@Override
Expand All @@ -53,36 +71,49 @@ public int getSSZFieldCount() {

@Override
public List<Bytes> get_fixed_parts() {
return List.of(toBytes());
return List.of(toSSZBytes());
}

public static BLSPublicKey fromBytes(Bytes bytes) {
public static BLSPublicKey fromSSZBytes(Bytes bytes) {
checkArgument(
bytes.size() == BLS_PUBKEY_SIZE,
"Expected " + BLS_PUBKEY_SIZE + " bytes but received %s.",
bytes.size() == SSZ_BLS_PUBKEY_SIZE,
"Expected " + SSZ_BLS_PUBKEY_SIZE + " bytes but received %s.",
bytes.size());
return SSZ.decode(
bytes,
reader ->
new BLSPublicKey(
BLS.getBlsImpl().publicKeyFromCompressed(reader.readFixedBytes(BLS_PUBKEY_SIZE))));
}

public static BLSPublicKey fromBytesCompressed(Bytes bytes) {
return new BLSPublicKey(BLS.getBlsImpl().publicKeyFromCompressed(bytes));
reader -> new BLSPublicKey(Bytes48.wrap(reader.readFixedBytes(SSZ_BLS_PUBKEY_SIZE))));
}

private final PublicKey publicKey;

/**
* Copy constructor.
* Create a PublicKey from 48-byte compressed format
*
* @param publicKey A BLSPublicKey
* @param bytes 48 bytes to read the public key from
* @return a public key. Note that implementation may lazily evaluate passed bytes so the method
* may not immediately fail if the supplied bytes are invalid. Use {@link
* BLSPublicKey#fromBytesCompressedValidate(Bytes48)} to validate immediately
* @throws IllegalArgumentException If the supplied bytes are not a valid public key However if
* implementing class lazily parses bytes the exception might not be thrown on invalid input
* but throw on later usage. Use {@link BLSPublicKey#fromBytesCompressedValidate(Bytes48)} if
* need to immediately ensure input validity
*/
public BLSPublicKey(BLSPublicKey publicKey) {
this.publicKey = publicKey.getPublicKey();
public static BLSPublicKey fromBytesCompressed(Bytes48 bytes) throws IllegalArgumentException {
return new BLSPublicKey(bytes);
}

public static BLSPublicKey fromBytesCompressedValidate(Bytes48 bytes)
throws IllegalArgumentException {
BLSPublicKey ret = new BLSPublicKey(bytes);
ret.getPublicKey().forceValidation();
return ret;
}

// Sometimes we are dealing with random, invalid pubkey points, e.g. when testing.
// Let's only interpret the raw data into a point when necessary to do so.
// And vice versa while aggregating we are dealing with points only so let's
// convert point to raw data when necessary to do so.
private final Supplier<PublicKey> publicKey;
private final Supplier<Bytes48> bytesCompressed;

/**
* Construct from a BLSSecretKey object.
*
Expand All @@ -97,42 +128,39 @@ public BLSPublicKey(BLSSecretKey secretKey) {
*
* @param publicKey A Mikuli PublicKey
*/
public BLSPublicKey(PublicKey publicKey) {
BLSPublicKey(PublicKey publicKey) {
this(() -> publicKey, Suppliers.memoize(publicKey::toBytesCompressed));
}

BLSPublicKey(Bytes48 bytesCompressed) {
this(
Suppliers.memoize(() -> BLS.getBlsImpl().publicKeyFromCompressed(bytesCompressed)),
() -> bytesCompressed);
}

private BLSPublicKey(Supplier<PublicKey> publicKey, Supplier<Bytes48> bytesCompressed) {
this.publicKey = publicKey;
this.bytesCompressed = bytesCompressed;
}

/**
* Returns the SSZ serialization of the <em>compressed</em> form of the signature.
*
* @return the serialization of the compressed form of the signature.
*/
public Bytes toBytes() {
public Bytes toSSZBytes() {
return SSZ.encode(
writer -> {
writer.writeFixedBytes(publicKey.toBytesCompressed());
writer.writeFixedBytes(toBytesCompressed());
});
}

public Bytes toBytesCompressed() {
return publicKey.toBytesCompressed();
public Bytes48 toBytesCompressed() {
return bytesCompressed.get();
}

public PublicKey getPublicKey() {
return publicKey;
}

/**
* Force validation of the given key's contents.
*
* @return true if the given key is valid, false otherwise
*/
public boolean isValid() {
try {
getPublicKey().forceValidation();
return true;
} catch (IllegalArgumentException e) {
return false;
}
PublicKey getPublicKey() {
return publicKey.get();
}

@Override
Expand All @@ -155,11 +183,11 @@ public boolean equals(Object obj) {
}

BLSPublicKey other = (BLSPublicKey) obj;
return Objects.equals(this.getPublicKey(), other.getPublicKey());
return Objects.equals(this.toBytesCompressed(), other.toBytesCompressed());
}

@Override
public int hashCode() {
return Objects.hash(publicKey);
return Objects.hash(toBytesCompressed());
}
}
55 changes: 43 additions & 12 deletions bls/src/main/java/tech/pegasys/teku/bls/BLSSecretKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,44 @@

package tech.pegasys.teku.bls;

import java.math.BigInteger;
import java.nio.ByteOrder;
import java.util.Objects;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import tech.pegasys.teku.bls.impl.SecretKey;

public final class BLSSecretKey {
/**
* Creates a secret key instance from bytes
*
* @param bytes Should be in range [0, )
* @throws IllegalArgumentException if bytes are not in the valid range
*/
public static BLSSecretKey fromBytes(Bytes32 bytes) throws IllegalArgumentException {
if (bytes.compareTo(BLSConstants.CURVE_ORDER_BYTES) >= 0) {
throw new IllegalArgumentException(
"Invalid bytes for secret key (0 <= SK < r, where r is "
+ BLSConstants.CURVE_ORDER_BYTES
+ "): "
+ bytes);
} else {
return new BLSSecretKey(BLS.getBlsImpl().secretKeyFromBytes(bytes));
}
}

public static BLSSecretKey fromBytes(Bytes32 bytes) {
return new BLSSecretKey(BLS.getBlsImpl().secretKeyFromBytes(bytes));
static BLSSecretKey fromBytesModR(Bytes32 secretKeyBytes) {
final Bytes32 keyBytes;
if (secretKeyBytes.compareTo(BLSConstants.CURVE_ORDER_BYTES) >= 0) {
BigInteger validSK =
secretKeyBytes
.toUnsignedBigInteger(ByteOrder.BIG_ENDIAN)
.mod(BLSConstants.CURVE_ORDER_BI);
keyBytes = Bytes32.leftPad(Bytes.wrap(validSK.toByteArray()));
} else {
keyBytes = secretKeyBytes;
}
return fromBytes(keyBytes);
}

private SecretKey secretKey;
Expand All @@ -35,19 +64,16 @@ public BLSSecretKey(SecretKey secretKey) {
this.secretKey = secretKey;
}

public SecretKey getSecretKey() {
SecretKey getSecretKey() {
return secretKey;
}

public Bytes toBytes() {
final Bytes bytes = secretKey.toBytes();
if (bytes.size() == 48) {
final int paddingLength = 48 - 32;
if (bytes.slice(0, paddingLength).isZero()) {
return bytes.slice(paddingLength, 32);
}
}
return bytes;
public BLSPublicKey toPublicKey() {
return new BLSPublicKey(getSecretKey().derivePublicKey());
}

public Bytes32 toBytes() {
return secretKey.toBytes();
}

/** Overwrites the key with zeros so that it is no longer in memory */
Expand All @@ -67,4 +93,9 @@ public boolean equals(final Object o) {
public int hashCode() {
return Objects.hash(secretKey);
}

@Override
public String toString() {
return "BLSSecretKey{" + toBytes() + '}';
}
}
Loading

0 comments on commit b3d2e07

Please sign in to comment.