From 69de1f01acf9ead170449ed51ff31ed0b8c5b396 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 19 Nov 2014 15:50:22 +0100 Subject: [PATCH] ECKey/DeterministicKey: replace ECPoint with a LazyECPoint wrapper that doesn't delays parsing of key bytes into a key structure until it's needed. The process of decoding keys from the wallet previously involved decompressing/recompressing them which was taking ~seconds for hundreds of keys on Dalvik/2012 era Androids. After this patch loading such a wallet takes a few hundred milliseconds, most of which is spent inside RIPEMD160. --- .../main/java/org/bitcoinj/core/ECKey.java | 33 +++- .../org/bitcoinj/crypto/DeterministicKey.java | 22 ++- .../org/bitcoinj/crypto/HDKeyDerivation.java | 4 +- .../java/org/bitcoinj/crypto/LazyECPoint.java | 185 ++++++++++++++++++ .../wallet/DeterministicKeyChain.java | 2 +- 5 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/crypto/LazyECPoint.java diff --git a/core/src/main/java/org/bitcoinj/core/ECKey.java b/core/src/main/java/org/bitcoinj/core/ECKey.java index e10f6478..4feb80b2 100644 --- a/core/src/main/java/org/bitcoinj/core/ECKey.java +++ b/core/src/main/java/org/bitcoinj/core/ECKey.java @@ -141,7 +141,7 @@ public class ECKey implements EncryptableItem, Serializable { // The two parts of the key. If "priv" is set, "pub" can always be calculated. If "pub" is set but not "priv", we // can only verify signatures not make them. protected final BigInteger priv; // A field element. - protected final ECPoint pub; + protected final LazyECPoint pub; // Creation time of the key in seconds since the epoch, or zero if the key was deserialized from a version that did // not have this field. @@ -173,11 +173,16 @@ public class ECKey implements EncryptableItem, Serializable { ECPrivateKeyParameters privParams = (ECPrivateKeyParameters) keypair.getPrivate(); ECPublicKeyParameters pubParams = (ECPublicKeyParameters) keypair.getPublic(); priv = privParams.getD(); - pub = CURVE.getCurve().decodePoint(pubParams.getQ().getEncoded(true)); + pub = new LazyECPoint(CURVE.getCurve(), pubParams.getQ().getEncoded(true)); creationTimeSeconds = Utils.currentTimeSeconds(); } protected ECKey(@Nullable BigInteger priv, ECPoint pub) { + this.priv = priv; + this.pub = new LazyECPoint(checkNotNull(pub)); + } + + protected ECKey(@Nullable BigInteger priv, LazyECPoint pub) { this.priv = priv; this.pub = checkNotNull(pub); } @@ -186,16 +191,24 @@ public class ECKey implements EncryptableItem, Serializable { * Utility for compressing an elliptic curve point. Returns the same point if it's already compressed. * See the ECKey class docs for a discussion of point compression. */ - public static ECPoint compressPoint(ECPoint uncompressed) { - return CURVE.getCurve().decodePoint(uncompressed.getEncoded(true)); + public static ECPoint compressPoint(ECPoint point) { + return point.isCompressed() ? point : CURVE.getCurve().decodePoint(point.getEncoded(true)); + } + + public static LazyECPoint compressPoint(LazyECPoint point) { + return point.isCompressed() ? point : new LazyECPoint(compressPoint(point.get())); } /** * Utility for decompressing an elliptic curve point. Returns the same point if it's already compressed. * See the ECKey class docs for a discussion of point compression. */ - public static ECPoint decompressPoint(ECPoint compressed) { - return CURVE.getCurve().decodePoint(compressed.getEncoded(false)); + public static ECPoint decompressPoint(ECPoint point) { + return !point.isCompressed() ? point : CURVE.getCurve().decodePoint(point.getEncoded(false)); + } + + public static LazyECPoint decompressPoint(LazyECPoint point) { + return !point.isCompressed() ? point : new LazyECPoint(decompressPoint(point.get())); } /** @@ -283,7 +296,7 @@ public class ECKey implements EncryptableItem, Serializable { if (!pub.isCompressed()) return this; else - return new ECKey(priv, decompressPoint(pub)); + return new ECKey(priv, decompressPoint(pub.get())); } /** @@ -340,12 +353,12 @@ public class ECKey implements EncryptableItem, Serializable { ECPoint point = CURVE.getG().multiply(privKey); if (compressed) point = compressPoint(point); - this.pub = point; + this.pub = new LazyECPoint(point); } else { // We expect the pubkey to be in regular encoded form, just as a BigInteger. Therefore the first byte is // a special marker byte. // TODO: This is probably not a useful API and may be confusing. - this.pub = CURVE.getCurve().decodePoint(pubKey); + this.pub = new LazyECPoint(CURVE.getCurve().decodePoint(pubKey)); } } @@ -431,7 +444,7 @@ public class ECKey implements EncryptableItem, Serializable { /** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */ public ECPoint getPubKeyPoint() { - return pub; + return pub.get(); } /** diff --git a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java index ba793cd6..1accec32 100644 --- a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java @@ -49,7 +49,7 @@ public class DeterministicKey extends ECKey { /** Constructs a key from its components. This is not normally something you should use. */ public DeterministicKey(ImmutableList childNumberPath, byte[] chainCode, - ECPoint publicAsPoint, + LazyECPoint publicAsPoint, @Nullable BigInteger priv, @Nullable DeterministicKey parent) { super(priv, compressPoint(checkNotNull(publicAsPoint))); @@ -59,6 +59,14 @@ public class DeterministicKey extends ECKey { this.chainCode = Arrays.copyOf(chainCode, chainCode.length); } + public DeterministicKey(ImmutableList childNumberPath, + byte[] chainCode, + ECPoint publicAsPoint, + @Nullable BigInteger priv, + @Nullable DeterministicKey parent) { + this(childNumberPath, chainCode, new LazyECPoint(publicAsPoint), priv, parent); + } + /** Constructs a key from its components. This is not normally something you should use. */ public DeterministicKey(ImmutableList childNumberPath, byte[] chainCode, @@ -74,7 +82,10 @@ public class DeterministicKey extends ECKey { /** Constructs a key from its components. This is not normally something you should use. */ public DeterministicKey(ImmutableList childNumberPath, byte[] chainCode, - KeyCrypter crypter, ECPoint pub, EncryptedData priv, @Nullable DeterministicKey parent) { + KeyCrypter crypter, + LazyECPoint pub, + EncryptedData priv, + @Nullable DeterministicKey parent) { this(childNumberPath, chainCode, pub, null, parent); this.encryptedPrivateKey = checkNotNull(priv); this.keyCrypter = checkNotNull(crypter); @@ -82,7 +93,7 @@ public class DeterministicKey extends ECKey { /** Clones the key */ public DeterministicKey(DeterministicKey keyToClone, DeterministicKey newParent) { - super(keyToClone.priv, keyToClone.pub); + super(keyToClone.priv, keyToClone.pub.get()); this.parent = newParent; this.childNumberPath = keyToClone.childNumberPath; this.chainCode = keyToClone.chainCode; @@ -155,8 +166,7 @@ public class DeterministicKey extends ECKey { */ public DeterministicKey getPubOnly() { if (isPubKeyOnly()) return this; - //final DeterministicKey parentPub = getParent() == null ? null : getParent().getPubOnly(); - return new DeterministicKey(getPath(), getChainCode(), getPubKeyPoint(), null, parent); + return new DeterministicKey(getPath(), getChainCode(), pub, null, parent); } @@ -413,7 +423,7 @@ public class DeterministicKey extends ECKey { checkArgument(!buffer.hasRemaining(), "Found unexpected data in key"); if (pub) { ECPoint point = ECKey.CURVE.getCurve().decodePoint(data); - return new DeterministicKey(path, chainCode, point, null, parent); + return new DeterministicKey(path, chainCode, new LazyECPoint(point), null, parent); } else { return new DeterministicKey(path, chainCode, new BigInteger(1, data), parent); } diff --git a/core/src/main/java/org/bitcoinj/crypto/HDKeyDerivation.java b/core/src/main/java/org/bitcoinj/crypto/HDKeyDerivation.java index 4ced76b1..3e21e81f 100644 --- a/core/src/main/java/org/bitcoinj/crypto/HDKeyDerivation.java +++ b/core/src/main/java/org/bitcoinj/crypto/HDKeyDerivation.java @@ -86,7 +86,7 @@ public final class HDKeyDerivation { } public static DeterministicKey createMasterPubKeyFromBytes(byte[] pubKeyBytes, byte[] chainCode) { - return new DeterministicKey(ImmutableList.of(), chainCode, ECKey.CURVE.getCurve().decodePoint(pubKeyBytes), null, null); + return new DeterministicKey(ImmutableList.of(), chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), pubKeyBytes), null, null); } /** @@ -130,7 +130,7 @@ public final class HDKeyDerivation { return new DeterministicKey( HDUtils.append(parent.getPath(), childNumber), rawKey.chainCode, - ECKey.CURVE.getCurve().decodePoint(rawKey.keyBytes), // c'tor will compress + new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes), null, parent); } else { diff --git a/core/src/main/java/org/bitcoinj/crypto/LazyECPoint.java b/core/src/main/java/org/bitcoinj/crypto/LazyECPoint.java new file mode 100644 index 00000000..6833dce9 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/crypto/LazyECPoint.java @@ -0,0 +1,185 @@ +package org.bitcoinj.crypto; + +import org.spongycastle.math.ec.ECCurve; +import org.spongycastle.math.ec.ECFieldElement; +import org.spongycastle.math.ec.ECPoint; + +import javax.annotation.Nullable; +import java.math.BigInteger; +import java.util.Arrays; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A wrapper around ECPoint that delays decoding of the point for as long as possible. This is useful because point + * encode/decode in Bouncy Castle is quite slow especially on Dalvik, as it often involves decompression/recompression. + */ +public class LazyECPoint { + // If curve is set, bits is also set. If curve is unset, point is set and bits is unset. Point can be set along + // with curve and bits when the cached form has been accessed and thus must have been converted. + + // These fields are all effectively final - once set they won't change again. However they can be set after + // construction. + private ECCurve curve; + private byte[] bits; + + @Nullable + private ECPoint point; + + public LazyECPoint(ECCurve curve, byte[] bits) { + this.curve = curve; + this.bits = bits; + } + + public LazyECPoint(ECPoint point) { + this.point = checkNotNull(point); + } + + public ECPoint get() { + if (point == null) + point = curve.decodePoint(bits); + return point; + } + + // Delegated methods. + + public ECPoint getDetachedPoint() { + return get().getDetachedPoint(); + } + + public byte[] getEncoded() { + if (bits != null) + return Arrays.copyOf(bits, bits.length); + else + return get().getEncoded(); + } + + public boolean isInfinity() { + return get().isInfinity(); + } + + public ECPoint timesPow2(int e) { + return get().timesPow2(e); + } + + public ECFieldElement getYCoord() { + return get().getYCoord(); + } + + public ECFieldElement[] getZCoords() { + return get().getZCoords(); + } + + public boolean isNormalized() { + return get().isNormalized(); + } + + public boolean isCompressed() { + if (bits != null) + return bits[0] == 2 || bits[0] == 3; + else + return get().isCompressed(); + } + + public ECPoint multiply(BigInteger k) { + return get().multiply(k); + } + + public ECPoint subtract(ECPoint b) { + return get().subtract(b); + } + + public boolean isValid() { + return get().isValid(); + } + + public ECPoint scaleY(ECFieldElement scale) { + return get().scaleY(scale); + } + + public ECFieldElement getXCoord() { + return get().getXCoord(); + } + + public ECPoint scaleX(ECFieldElement scale) { + return get().scaleX(scale); + } + + public boolean equals(ECPoint other) { + return get().equals(other); + } + + public ECPoint negate() { + return get().negate(); + } + + public ECPoint threeTimes() { + return get().threeTimes(); + } + + public ECFieldElement getZCoord(int index) { + return get().getZCoord(index); + } + + public byte[] getEncoded(boolean compressed) { + if (compressed == isCompressed() && bits != null) + return Arrays.copyOf(bits, bits.length); + else + return get().getEncoded(compressed); + } + + public ECPoint add(ECPoint b) { + return get().add(b); + } + + public ECPoint twicePlus(ECPoint b) { + return get().twicePlus(b); + } + + public ECCurve getCurve() { + return get().getCurve(); + } + + public ECPoint normalize() { + return get().normalize(); + } + + public ECFieldElement getY() { + return get().getY(); + } + + public ECPoint twice() { + return get().twice(); + } + + public ECFieldElement getAffineYCoord() { + return get().getAffineYCoord(); + } + + public ECFieldElement getAffineXCoord() { + return get().getAffineXCoord(); + } + + public ECFieldElement getX() { + return get().getX(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LazyECPoint point1 = (LazyECPoint) o; + if (bits != null && point1.bits != null) + return Arrays.equals(bits, point1.bits); + else + return get().equals(point1.get()); + } + + @Override + public int hashCode() { + if (bits != null) + return Arrays.hashCode(bits); + else + return get().hashCode(); + } +} diff --git a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java index db66c277..66f0d348 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java @@ -797,7 +797,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { for (int i : key.getDeterministicKey().getPathList()) path.add(new ChildNumber(i)); // Deserialize the public key and path. - ECPoint pubkey = ECKey.CURVE.getCurve().decodePoint(key.getPublicKey().toByteArray()); + LazyECPoint pubkey = new LazyECPoint(ECKey.CURVE.getCurve(), key.getPublicKey().toByteArray()); final ImmutableList immutablePath = ImmutableList.copyOf(path); // Possibly create the chain, if we didn't already do so yet. boolean isWatchingAccountKey = false;