Skip to content

Commit b29713d

Browse files
authoredOct 18, 2022
ENT-6947 Intern common types to reduce heap footprint (corda#7239)
ENT-6947: Implement interning for SecureHash, CordaX500Name, PublicKey, AsbtractParty and SignatureAttachmentConstraint, including automatic detection of internable types off companion objects in AMQP & Kyro deserialization. In some cases, add new factory methods to companion objects, and make main code base use them. Performance tested in performance cluster with no negative impact visible (so default concurrency setting seems okay). Testing suggests 5-6x memory saving for tokens in TokensSDK in memory selector. Should see approx. 1 million tokens per GB or better (1.5 million for the tokens we tested with).
1 parent 3238638 commit b29713d

File tree

31 files changed

+707
-82
lines changed

31 files changed

+707
-82
lines changed
 

‎.ci/api-current.txt

+287-2
Large diffs are not rendered by default.

‎.idea/codeStyles/Project.xml

+2-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.idea/codeStyles/codeStyleConfig.xml

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class StandaloneCordaRPClientTest {
118118

119119
val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it ->
120120
it.copyTo(NULL_OUTPUT_STREAM)
121-
SecureHash.SHA256(it.hash().asBytes())
121+
SecureHash.createSHA256(it.hash().asBytes())
122122
}
123123
assertEquals(attachment.sha256, hash)
124124
}
@@ -133,7 +133,7 @@ class StandaloneCordaRPClientTest {
133133

134134
val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it ->
135135
it.copyTo(NULL_OUTPUT_STREAM)
136-
SecureHash.SHA256(it.hash().asBytes())
136+
SecureHash.createSHA256(it.hash().asBytes())
137137
}
138138
assertEquals(attachment.sha256, hash)
139139
}

‎common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ package net.corda.common.logging
99
* (originally added to source control for ease of use)
1010
*/
1111

12-
internal const val CURRENT_MAJOR_RELEASE = "4.10-SNAPSHOT"
12+
internal const val CURRENT_MAJOR_RELEASE = "4.10-SNAPSHOT"

‎core-deterministic/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import net.corda.gradle.jarfilter.JarFilterTask
22
import net.corda.gradle.jarfilter.MetaFixerTask
33
import proguard.gradle.ProGuardTask
4+
45
import static org.gradle.api.JavaVersion.VERSION_1_8
56

67
plugins {
@@ -77,6 +78,7 @@ def patchCore = tasks.register('patchCore', Zip) {
7778
exclude 'net/corda/core/serialization/internal/AttachmentsHolderImpl.class'
7879
exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class'
7980
exclude 'net/corda/core/internal/rules/*.class'
81+
exclude 'net/corda/core/internal/utilities/PrivateInterner*.class'
8082
}
8183

8284
reproducibleFileOrder = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package net.corda.core.internal.utilities
2+
3+
import net.corda.core.KeepForDJVM
4+
5+
@KeepForDJVM
6+
class PrivateInterner<T>(val verifier: IternabilityVerifier<T> = AlwaysInternableVerifier()) {
7+
// DJVM implementation does not intern and does not use Guava
8+
fun <S : T> intern(sample: S): S = sample
9+
10+
@KeepForDJVM
11+
companion object {
12+
@Suppress("UNUSED_PARAMETER")
13+
fun findFor(clazz: Class<*>?): PrivateInterner<Any>? = null
14+
}
15+
}
16+

‎core/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ dependencies {
5858
testCompile "org.assertj:assertj-core:${assertj_version}"
5959

6060
// Guava: Google utilities library.
61-
testCompile "com.google.guava:guava:$guava_version"
61+
compile "com.google.guava:guava:$guava_version"
6262

6363
// For caches rather than guava
6464
compile "com.github.ben-manes.caffeine:caffeine:$caffeine_version"

‎core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package net.corda.core.contracts
22

3+
import net.corda.core.CordaInternal
34
import net.corda.core.DoNotImplement
45
import net.corda.core.KeepForDJVM
56
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
67
import net.corda.core.crypto.SecureHash
78
import net.corda.core.crypto.isFulfilledBy
89
import net.corda.core.internal.AttachmentWithContext
910
import net.corda.core.internal.isUploaderTrusted
11+
import net.corda.core.internal.utilities.Internable
12+
import net.corda.core.internal.utilities.PrivateInterner
1013
import net.corda.core.serialization.CordaSerializable
1114
import net.corda.core.transactions.TransactionBuilder
1215
import net.corda.core.utilities.loggerFor
@@ -119,7 +122,16 @@ data class SignatureAttachmentConstraint(val key: PublicKey) : AttachmentConstra
119122
return if (!key.isFulfilledBy(attachment.signerKeys.map { it })) {
120123
log.warn("Untrusted signing key: expected $key. but contract attachment contains ${attachment.signerKeys}")
121124
false
122-
}
123-
else true
125+
} else true
126+
}
127+
128+
companion object : Internable<SignatureAttachmentConstraint> {
129+
@CordaInternal
130+
override val interner = PrivateInterner<SignatureAttachmentConstraint>()
131+
132+
/**
133+
* Factory method to be used in preference to the constructor.
134+
*/
135+
fun create(key: PublicKey) = interner.intern(SignatureAttachmentConstraint(key))
124136
}
125137
}

‎core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ package net.corda.core.crypto
33
import net.corda.core.KeepForDJVM
44
import net.corda.core.serialization.deserialize
55
import java.io.ByteArrayOutputStream
6-
import java.security.*
6+
import java.security.InvalidAlgorithmParameterException
7+
import java.security.InvalidKeyException
8+
import java.security.PrivateKey
9+
import java.security.Provider
10+
import java.security.PublicKey
11+
import java.security.Signature
12+
import java.security.SignatureException
713
import java.security.spec.AlgorithmParameterSpec
814

915
/**
@@ -80,7 +86,7 @@ class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
8086
fun engineVerify(sigBytes: ByteArray): Boolean {
8187
val sig = sigBytes.deserialize<CompositeSignaturesWithKeys>()
8288
return if (verifyKey.isFulfilledBy(sig.sigs.map { it.by })) {
83-
val clearData = SecureHash.SHA256(buffer.toByteArray())
89+
val clearData = SecureHash.createSHA256(buffer.toByteArray())
8490
sig.sigs.all { it.isValid(clearData) }
8591
} else {
8692
false

‎core/src/main/kotlin/net/corda/core/crypto/Crypto.kt

+18-13
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import net.corda.core.DeleteForDJVM
55
import net.corda.core.KeepForDJVM
66
import net.corda.core.crypto.internal.AliasPrivateKey
77
import net.corda.core.crypto.internal.Instances.withSignature
8-
import net.corda.core.crypto.internal.`id-Curve25519ph`
98
import net.corda.core.crypto.internal.bouncyCastlePQCProvider
109
import net.corda.core.crypto.internal.cordaBouncyCastleProvider
1110
import net.corda.core.crypto.internal.cordaSecurityProvider
11+
import net.corda.core.crypto.internal.`id-Curve25519ph`
1212
import net.corda.core.crypto.internal.providerMap
13+
import net.corda.core.internal.utilities.PrivateInterner
1314
import net.corda.core.serialization.serialize
1415
import net.i2p.crypto.eddsa.EdDSAEngine
1516
import net.i2p.crypto.eddsa.EdDSAPrivateKey
@@ -710,7 +711,8 @@ object Crypto {
710711
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
711712
else
712713
keyPairGenerator.initialize(signatureScheme.keySize!!, newSecureRandom())
713-
return keyPairGenerator.generateKeyPair()
714+
val newKeyPair = keyPairGenerator.generateKeyPair()
715+
return KeyPair(internPublicKey(newKeyPair.public), newKeyPair.private)
714716
}
715717

716718
/**
@@ -840,7 +842,7 @@ object Crypto {
840842
val publicKeySpec = ECPublicKeySpec(pointQ, parameterSpec)
841843
val publicKeyD = BCECPublicKey(privateKey.algorithm, publicKeySpec, BouncyCastleProvider.CONFIGURATION)
842844

843-
return KeyPair(publicKeyD, privateKeyD)
845+
return KeyPair(internPublicKey(publicKeyD), privateKeyD)
844846
}
845847

846848
// Deterministically generate an EdDSA key.
@@ -853,7 +855,7 @@ object Crypto {
853855
val bytes = macBytes.copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length.
854856
val privateKeyD = EdDSAPrivateKeySpec(bytes, params)
855857
val publicKeyD = EdDSAPublicKeySpec(privateKeyD.a, params)
856-
return KeyPair(EdDSAPublicKey(publicKeyD), EdDSAPrivateKey(privateKeyD))
858+
return KeyPair(internPublicKey(EdDSAPublicKey(publicKeyD)), EdDSAPrivateKey(privateKeyD))
857859
}
858860

859861
/**
@@ -892,7 +894,7 @@ object Crypto {
892894
val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length.
893895
val priv = EdDSAPrivateKeySpec(bytes, params)
894896
val pub = EdDSAPublicKeySpec(priv.a, params)
895-
return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv))
897+
return KeyPair(internPublicKey(EdDSAPublicKey(pub)), EdDSAPrivateKey(priv))
896898
}
897899

898900
// Custom key pair generator from an entropy required for various tests. It is similar to deriveKeyPairECDSA,
@@ -918,7 +920,7 @@ object Crypto {
918920
val publicKeySpec = ECPublicKeySpec(pointQ, parameterSpec)
919921
val pub = BCECPublicKey("EC", publicKeySpec, BouncyCastleProvider.CONFIGURATION)
920922

921-
return KeyPair(pub, priv)
923+
return KeyPair(internPublicKey(pub), priv)
922924
}
923925

924926
// Compute the HMAC-SHA512 using a privateKey as the MAC_key and a seed ByteArray.
@@ -990,11 +992,14 @@ object Crypto {
990992
}
991993
}
992994

995+
private val interner = PrivateInterner<PublicKey>()
996+
private fun internPublicKey(key: PublicKey): PublicKey = interner.intern(key)
997+
993998
private fun convertIfBCEdDSAPublicKey(key: PublicKey): PublicKey {
994-
return when (key) {
999+
return internPublicKey(when (key) {
9951000
is BCEdDSAPublicKey -> EdDSAPublicKey(X509EncodedKeySpec(key.encoded))
9961001
else -> key
997-
}
1002+
})
9981003
}
9991004

10001005
private fun convertIfBCEdDSAPrivateKey(key: PrivateKey): PrivateKey {
@@ -1026,11 +1031,11 @@ object Crypto {
10261031
@JvmStatic
10271032
fun toSupportedPublicKey(key: PublicKey): PublicKey {
10281033
return when (key) {
1029-
is BCECPublicKey -> key
1030-
is BCRSAPublicKey -> key
1031-
is BCSphincs256PublicKey -> key
1032-
is EdDSAPublicKey -> key
1033-
is CompositeKey -> key
1034+
is BCECPublicKey -> internPublicKey(key)
1035+
is BCRSAPublicKey -> internPublicKey(key)
1036+
is BCSphincs256PublicKey -> internPublicKey(key)
1037+
is EdDSAPublicKey -> internPublicKey(key)
1038+
is CompositeKey -> internPublicKey(key)
10341039
is BCEdDSAPublicKey -> convertIfBCEdDSAPublicKey(key)
10351040
else -> decodePublicKey(key.encoded)
10361041
}

‎core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt

+27-15
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
package net.corda.core.crypto
44

55
import io.netty.util.concurrent.FastThreadLocal
6+
import net.corda.core.CordaInternal
67
import net.corda.core.DeleteForDJVM
78
import net.corda.core.KeepForDJVM
89
import net.corda.core.crypto.internal.DigestAlgorithmFactory
10+
import net.corda.core.internal.utilities.Internable
11+
import net.corda.core.internal.utilities.PrivateInterner
912
import net.corda.core.serialization.CordaSerializable
1013
import net.corda.core.utilities.OpaqueBytes
1114
import net.corda.core.utilities.parseAsHex
@@ -66,7 +69,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
6669
}
6770

6871
override fun generate(data: ByteArray): SecureHash {
69-
return HASH(algorithm, digestAs(algorithm, data))
72+
return interner.intern(HASH(algorithm, digestAs(algorithm, data)))
7073
}
7174
}
7275

@@ -110,7 +113,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
110113
return if(concatAlgorithm == SHA2_256) {
111114
concatBytes.sha256()
112115
} else {
113-
HASH(concatAlgorithm, digestAs(concatAlgorithm, concatBytes))
116+
interner.intern(HASH(concatAlgorithm, digestAs(concatAlgorithm, concatBytes)))
114117
}
115118
}
116119

@@ -121,12 +124,15 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
121124
fun reHash() : SecureHash = hashAs(algorithm, bytes)
122125

123126
// Like static methods in Java, except the 'companion' is a singleton that can have state.
124-
companion object {
127+
companion object : Internable<SecureHash> {
125128
const val SHA2_256 = "SHA-256"
126129
const val SHA2_384 = "SHA-384"
127130
const val SHA2_512 = "SHA-512"
128131
const val DELIMITER = ':'
129132

133+
@CordaInternal
134+
override val interner = PrivateInterner<SecureHash>()
135+
130136
/**
131137
* Converts a SecureHash hash value represented as a {algorithm:}hexadecimal [String] into a [SecureHash].
132138
* @param str An optional algorithm id followed by a delimiter and the sequence of hexadecimal digits that represents a hash value.
@@ -157,7 +163,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
157163
val digestLength = digestFor(algorithm).digestLength
158164
val data = value.parseAsHex()
159165
return when (data.size) {
160-
digestLength -> HASH(algorithm, data)
166+
digestLength -> interner.intern(HASH(algorithm, data))
161167
else -> throw IllegalArgumentException("Provided string is ${data.size} bytes not $digestLength bytes in hex: $value")
162168
}
163169
}
@@ -171,12 +177,18 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
171177
fun parse(str: String?): SHA256 {
172178
return str?.toUpperCase()?.parseAsHex()?.let {
173179
when (it.size) {
174-
32 -> SHA256(it)
180+
32 -> interner.intern(SHA256(it))
175181
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
176182
}
177183
} ?: throw IllegalArgumentException("Provided string is null")
178184
}
179185

186+
/**
187+
* Factory method for SHA256 to be used in preference to the constructor.
188+
*/
189+
@JvmStatic
190+
fun createSHA256(bytes: ByteArray): SHA256 = interner.intern(SHA256(bytes))
191+
180192
private val messageDigests: ConcurrentMap<String, DigestSupplier> = ConcurrentHashMap()
181193

182194
private fun digestFor(algorithm: String): DigestSupplier {
@@ -202,9 +214,9 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
202214
fun hashAs(algorithm: String, bytes: ByteArray): SecureHash {
203215
val hashBytes = digestAs(algorithm, bytes)
204216
return if (algorithm == SHA2_256) {
205-
SHA256(hashBytes)
217+
interner.intern(SHA256(hashBytes))
206218
} else {
207-
HASH(algorithm, hashBytes)
219+
interner.intern(HASH(algorithm, hashBytes))
208220
}
209221
}
210222

@@ -222,7 +234,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
222234
} else {
223235
val digest = digestFor(algorithm).get()
224236
val hash = digest.componentDigest(bytes)
225-
HASH(algorithm, hash)
237+
interner.intern(HASH(algorithm, hash))
226238
}
227239
}
228240

@@ -240,7 +252,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
240252
} else {
241253
val digest = digestFor(algorithm).get()
242254
val hash = digest.nonceDigest(bytes)
243-
HASH(algorithm, hash)
255+
interner.intern(HASH(algorithm, hash))
244256
}
245257
}
246258

@@ -249,7 +261,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
249261
* @param bytes The [ByteArray] to hash.
250262
*/
251263
@JvmStatic
252-
fun sha256(bytes: ByteArray) = SHA256(digestAs(SHA2_256, bytes))
264+
fun sha256(bytes: ByteArray) = interner.intern(SHA256(digestAs(SHA2_256, bytes)))
253265

254266
/**
255267
* Computes the SHA-256 hash of the [ByteArray], and then computes the SHA-256 hash of the hash.
@@ -282,7 +294,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
282294
randomSHA256()
283295
} else {
284296
val digest = digestFor(algorithm)
285-
HASH(algorithm, digest.get().digest(secureRandomBytes(digest.digestLength)))
297+
interner.intern(HASH(algorithm, digest.get().digest(secureRandomBytes(digest.digestLength))))
286298
}
287299
}
288300

@@ -291,7 +303,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
291303
* This field provides more intuitive access from Java.
292304
*/
293305
@JvmField
294-
val zeroHash: SHA256 = SHA256(ByteArray(32) { 0.toByte() })
306+
val zeroHash: SHA256 = interner.intern(SHA256(ByteArray(32) { 0.toByte() }))
295307

296308
/**
297309
* A SHA-256 hash value consisting of 32 0x00 bytes.
@@ -305,7 +317,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
305317
* This field provides more intuitive access from Java.
306318
*/
307319
@JvmField
308-
val allOnesHash: SHA256 = SHA256(ByteArray(32) { 255.toByte() })
320+
val allOnesHash: SHA256 = interner.intern(SHA256(ByteArray(32) { 255.toByte() }))
309321

310322
/**
311323
* A SHA-256 hash value consisting of 32 0xFF bytes.
@@ -323,8 +335,8 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
323335
return hashConstants.getOrPut(algorithm) {
324336
val digestLength = digestFor(algorithm).digestLength
325337
HashConstants(
326-
zero = HASH(algorithm, ByteArray(digestLength)),
327-
allOnes = HASH(algorithm, ByteArray(digestLength) { 255.toByte() })
338+
zero = interner.intern(HASH(algorithm, ByteArray(digestLength))),
339+
allOnes = interner.intern(HASH(algorithm, ByteArray(digestLength) { 255.toByte() }))
328340
)
329341
}
330342
}

0 commit comments

Comments
 (0)
Please sign in to comment.